backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 23 Feb 2012 19:06:14 +0100
changeset 8262 272e10526679
parent 8260 5a81fa526b30 (diff)
parent 8261 d4d9c88d4a5f (current diff)
child 8265 9747ab9230ad
backport stable
--- a/__init__.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/__init__.py	Thu Feb 23 19:06:14 2012 +0100
@@ -55,6 +55,7 @@
 
 # make all exceptions accessible from the package
 from cubicweb._exceptions import *
+from logilab.common.registry import ObjectNotFound, NoSelectableObject, RegistryNotFound
 
 # convert eid to the right type, raise ValueError if it's not a valid eid
 typed_eid = int
@@ -77,25 +78,24 @@
                "Binary objects must use raw strings, not %s" % data.__class__
         StringIO.write(self, data)
 
-    def to_file(self, filename):
+    def to_file(self, fobj):
         """write a binary to disk
 
         the writing is performed in a safe way for files stored on
         Windows SMB shares
         """
         pos = self.tell()
-        with open(filename, 'wb') as fobj:
-            self.seek(0)
-            if sys.platform == 'win32':
-                while True:
-                    # the 16kB chunksize comes from the shutil module
-                    # in stdlib
-                    chunk = self.read(16*1024)
-                    if not chunk:
-                        break
-                    fobj.write(chunk)
-            else:
-                fobj.write(self.read())
+        self.seek(0)
+        if sys.platform == 'win32':
+            while True:
+                # the 16kB chunksize comes from the shutil module
+                # in stdlib
+                chunk = self.read(16*1024)
+                if not chunk:
+                    break
+                fobj.write(chunk)
+        else:
+            fobj.write(self.read())
         self.seek(pos)
 
     @staticmethod
--- a/__pkginfo__.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/__pkginfo__.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,5 +1,5 @@
 # pylint: disable=W0622,C0103
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 14, 4)
+numversion = (3, 15, 0)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -40,7 +40,7 @@
 ]
 
 __depends__ = {
-    'logilab-common': '>= 0.57.0',
+    'logilab-common': '>= 0.58.0',
     'logilab-mtconverter': '>= 0.8.0',
     'rql': '>= 0.28.0',
     'yams': '>= 0.34.0',
@@ -52,7 +52,7 @@
     'Twisted': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '>= 1.8.1',
+    'logilab-database': '>= 1.8.2',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
     }
 
@@ -63,6 +63,7 @@
     'fyzz': '>= 0.1.0',         # for sparql
     'vobject': '>= 0.6.0',      # for ical view
     'rdflib': None,             #
+    'pyzmq': None,
     #'Products.FCKeditor':'',
     #'SimpleTAL':'>= 4.1.6',
     }
--- a/_exceptions.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/_exceptions.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,10 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Exceptions shared by different cubicweb packages.
+"""Exceptions shared by different cubicweb packages."""
 
-
-"""
 __docformat__ = "restructuredtext en"
 
 from yams import ValidationError
@@ -114,32 +112,8 @@
 
 # registry exceptions #########################################################
 
-class RegistryException(CubicWebException):
-    """raised when an unregistered view is called"""
-
-class RegistryNotFound(RegistryException):
-    """raised when an unknown registry is requested
-
-    this is usually a programming/typo error...
-    """
-
-class ObjectNotFound(RegistryException):
-    """raised when an unregistered object is requested
-
-    this may be a programming/typo or a misconfiguration error
-    """
-
-class NoSelectableObject(RegistryException):
-    """raised when no appobject is selectable for a given context."""
-    def __init__(self, args, kwargs, appobjects):
-        self.args = args
-        self.kwargs = kwargs
-        self.appobjects = appobjects
-
-    def __str__(self):
-        return ('args: %s, kwargs: %s\ncandidates: %s'
-                % (self.args, self.kwargs.keys(), self.appobjects))
-
+# pre 3.15 bw compat
+from logilab.common.registry import RegistryException, ObjectNotFound, NoSelectableObject
 
 class UnknownProperty(RegistryException):
     """property found in database but unknown in registry"""
@@ -161,3 +135,4 @@
 
 # pylint: disable=W0611
 from logilab.common.clcommands import BadCommandUsage
+
--- a/appobject.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/appobject.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -35,281 +35,25 @@
 from logging import getLogger
 from warnings import warn
 
-from logilab.common.deprecation import deprecated
+from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.decorators import classproperty
 from logilab.common.logging_ext import set_log_methods
+from logilab.common.registry import yes
 
 from cubicweb.cwconfig import CubicWebConfiguration
-
-def class_regid(cls):
-    """returns a unique identifier for an appobject class"""
-    return cls.__regid__
-
-# helpers for debugging selectors
-TRACED_OIDS = None
-
-def _trace_selector(cls, selector, args, ret):
-    # /!\ lltrace decorates pure function or __call__ method, this
-    #     means argument order may be different
-    if isinstance(cls, Selector):
-        selname = str(cls)
-        vobj = args[0]
-    else:
-        selname = selector.__name__
-        vobj = cls
-    if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
-        #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
-        print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
-
-def lltrace(selector):
-    """use this decorator on your selectors so the becomes traceable with
-    :class:`traced_selection`
-    """
-    # don't wrap selectors if not in development mode
-    if CubicWebConfiguration.mode == 'system': # XXX config.debug
-        return selector
-    def traced(cls, *args, **kwargs):
-        ret = selector(cls, *args, **kwargs)
-        if TRACED_OIDS is not None:
-            _trace_selector(cls, selector, args, ret)
-        return ret
-    traced.__name__ = selector.__name__
-    traced.__doc__ = selector.__doc__
-    return traced
-
-class traced_selection(object):
-    """
-    Typical usage is :
-
-    .. sourcecode:: python
-
-        >>> from cubicweb.selectors import traced_selection
-        >>> with traced_selection():
-        ...     # some code in which you want to debug selectors
-        ...     # for all objects
-
-    Don't forget the 'from __future__ import with_statement' at the module top-level
-    if you're using python prior to 2.6.
-
-    This will yield lines like this in the logs::
-
-        selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
-
-    You can also give to :class:`traced_selection` the identifiers of objects on
-    which you want to debug selection ('oid1' and 'oid2' in the example above).
-
-    .. sourcecode:: python
-
-        >>> with traced_selection( ('regid1', 'regid2') ):
-        ...     # some code in which you want to debug selectors
-        ...     # for objects with __regid__ 'regid1' and 'regid2'
-
-    A potentially usefull point to set up such a tracing function is
-    the `cubicweb.vregistry.Registry.select` method body.
-    """
-
-    def __init__(self, traced='all'):
-        self.traced = traced
-
-    def __enter__(self):
-        global TRACED_OIDS
-        TRACED_OIDS = self.traced
-
-    def __exit__(self, exctype, exc, traceback):
-        global TRACED_OIDS
-        TRACED_OIDS = None
-        return traceback is None
-
-# selector base classes and operations ########################################
-
-def objectify_selector(selector_func):
-    """Most of the time, a simple score function is enough to build a selector.
-    The :func:`objectify_selector` decorator turn it into a proper selector
-    class::
-
-        @objectify_selector
-        def one(cls, req, rset=None, **kwargs):
-            return 1
-
-        class MyView(View):
-            __select__ = View.__select__ & one()
-
-    """
-    return type(selector_func.__name__, (Selector,),
-                {'__doc__': selector_func.__doc__,
-                 '__call__': lambda self, *a, **kw: selector_func(*a, **kw)})
-
-
-def _instantiate_selector(selector):
-    """ensures `selector` is a `Selector` instance
-
-    NOTE: This should only be used locally in build___select__()
-    XXX: then, why not do it ??
-    """
-    if isinstance(selector, types.FunctionType):
-        return objectify_selector(selector)()
-    if isinstance(selector, type) and issubclass(selector, Selector):
-        return selector()
-    return selector
-
-
-class Selector(object):
-    """base class for selector classes providing implementation
-    for operators ``&``, ``|`` and  ``~``
-
-    This class is only here to give access to binary operators, the
-    selector logic itself should be implemented in the __call__ method
+# XXX for bw compat
+from logilab.common.registry import objectify_predicate, traced_selection, Predicate
 
 
-    a selector is called to help choosing the correct object for a
-    particular context by returning a score (`int`) telling how well
-    the class given as first argument apply to the given context.
-
-    0 score means that the class doesn't apply.
-    """
-
-    @property
-    def func_name(self):
-        # backward compatibility
-        return self.__class__.__name__
-
-    def search_selector(self, selector):
-        """search for the given selector, selector instance or tuple of
-        selectors in the selectors tree. Return None if not found.
-        """
-        if self is selector:
-            return self
-        if (isinstance(selector, type) or isinstance(selector, tuple)) and \
-               isinstance(self, selector):
-            return self
-        return None
-
-    def __str__(self):
-        return self.__class__.__name__
-
-    def __and__(self, other):
-        return AndSelector(self, other)
-    def __rand__(self, other):
-        return AndSelector(other, self)
-    def __iand__(self, other):
-        return AndSelector(self, other)
-    def __or__(self, other):
-        return OrSelector(self, other)
-    def __ror__(self, other):
-        return OrSelector(other, self)
-    def __ior__(self, other):
-        return OrSelector(self, other)
-
-    def __invert__(self):
-        return NotSelector(self)
-
-    # XXX (function | function) or (function & function) not managed yet
-
-    def __call__(self, cls, *args, **kwargs):
-        return NotImplementedError("selector %s must implement its logic "
-                                   "in its __call__ method" % self.__class__)
-
-    def __repr__(self):
-        return u'<Selector %s at %x>' % (self.__class__.__name__, id(self))
-
-
-class MultiSelector(Selector):
-    """base class for compound selector classes"""
-
-    def __init__(self, *selectors):
-        self.selectors = self.merge_selectors(selectors)
-
-    def __str__(self):
-        return '%s(%s)' % (self.__class__.__name__,
-                           ','.join(str(s) for s in self.selectors))
-
-    @classmethod
-    def merge_selectors(cls, selectors):
-        """deal with selector instanciation when necessary and merge
-        multi-selectors if possible:
+objectify_selector = deprecated('[3.15] objectify_selector has been renamed to objectify_predicates in logilab.common.registry')(objectify_predicate)
+traced_selection = deprecated('[3.15] traced_selection has been moved to logilab.common.registry')(traced_selection)
+Selector = class_renamed(
+    'Selector', Predicate,
+    '[3.15] Selector has been renamed to Predicate in logilab.common.registry')
 
-        AndSelector(AndSelector(sel1, sel2), AndSelector(sel3, sel4))
-        ==> AndSelector(sel1, sel2, sel3, sel4)
-        """
-        merged_selectors = []
-        for selector in selectors:
-            try:
-                selector = _instantiate_selector(selector)
-            except Exception:
-                pass
-            #assert isinstance(selector, Selector), selector
-            if isinstance(selector, cls):
-                merged_selectors += selector.selectors
-            else:
-                merged_selectors.append(selector)
-        return merged_selectors
-
-    def search_selector(self, selector):
-        """search for the given selector or selector instance (or tuple of
-        selectors) in the selectors tree. Return None if not found
-        """
-        for childselector in self.selectors:
-            if childselector is selector:
-                return childselector
-            found = childselector.search_selector(selector)
-            if found is not None:
-                return found
-        # if not found in children, maybe we are looking for self?
-        return super(MultiSelector, self).search_selector(selector)
-
-
-class AndSelector(MultiSelector):
-    """and-chained selectors (formerly known as chainall)"""
-    @lltrace
-    def __call__(self, cls, *args, **kwargs):
-        score = 0
-        for selector in self.selectors:
-            partscore = selector(cls, *args, **kwargs)
-            if not partscore:
-                return 0
-            score += partscore
-        return score
-
-
-class OrSelector(MultiSelector):
-    """or-chained selectors (formerly known as chainfirst)"""
-    @lltrace
-    def __call__(self, cls, *args, **kwargs):
-        for selector in self.selectors:
-            partscore = selector(cls, *args, **kwargs)
-            if partscore:
-                return partscore
-        return 0
-
-class NotSelector(Selector):
-    """negation selector"""
-    def __init__(self, selector):
-        self.selector = selector
-
-    @lltrace
-    def __call__(self, cls, *args, **kwargs):
-        score = self.selector(cls, *args, **kwargs)
-        return int(not score)
-
-    def __str__(self):
-        return 'NOT(%s)' % self.selector
-
-
-class yes(Selector):
-    """Return the score given as parameter, with a default score of 0.5 so any
-    other selector take precedence.
-
-    Usually used for appobjects which can be selected whatever the context, or
-    also sometimes to add arbitrary points to a score.
-
-    Take care, `yes(0)` could be named 'no'...
-    """
-    def __init__(self, score=0.5):
-        self.score = score
-
-    def __call__(self, *args, **kwargs):
-        return self.score
-
+@deprecated('[3.15] lltrace decorator can now be removed')
+def lltrace(func):
+    return func
 
 # the base class for all appobjects ############################################
 
@@ -464,3 +208,6 @@
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
 
 set_log_methods(AppObject, getLogger('cubicweb.appobject'))
+
+# defined here to avoid warning on usage on the AppObject class
+yes = deprecated('[3.15] yes has been moved to logilab.common.registry')(yes)
--- a/cwvreg.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/cwvreg.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,12 +15,12 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-""".. VRegistry:
+""".. RegistryStore:
 
-The `VRegistry`
----------------
+The `RegistryStore`
+-------------------
 
-The `VRegistry` can be seen as a two-level dictionary. It contains
+The `RegistryStore` can be seen as a two-level dictionary. It contains
 all dynamically loaded objects (subclasses of :ref:`appobject`) to
 build a |cubicweb| application. Basically:
 
@@ -34,7 +34,7 @@
 A *registry* holds a specific kind of application objects. There is
 for instance a registry for entity classes, another for views, etc...
 
-The `VRegistry` has two main responsibilities:
+The `RegistryStore` has two main responsibilities:
 
 - being the access point to all registries
 
@@ -76,13 +76,13 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Here are the registration methods that you can use in the `registration_callback`
-to register your objects to the `VRegistry` instance given as argument (usually
+to register your objects to the `RegistryStore` instance given as argument (usually
 named `vreg`):
 
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_all
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_and_replace
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.register
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.unregister
 
 Examples:
 
@@ -193,41 +193,44 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+import sys
+from os.path import join, dirname, realpath
 from warnings import warn
 from datetime import datetime, date, time, timedelta
 
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import deprecated, class_deprecated
 from logilab.common.modutils import cleanup_sys_modules
+from logilab.common.registry import (
+    RegistryStore, Registry, classid,
+    ObjectNotFound, NoSelectableObject, RegistryNotFound)
 
 from rql import RQLHelper
 from yams.constraints import BASE_CONVERTERS
 
-from cubicweb import (ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid,
-                      ObjectNotFound, NoSelectableObject, RegistryNotFound,
-                      CW_EVENT_MANAGER)
-from cubicweb.vregistry import VRegistry, Registry, class_regid, classid
+from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
+                      Binary, UnknownProperty, UnknownEid)
 from cubicweb.rtags import RTAGS
+from cubicweb.predicates import (implements, appobject_selectable,
+                                 _reset_is_instance_cache)
 
 def clear_rtag_objects():
     for rtag in RTAGS:
         rtag.clear()
 
 def use_interfaces(obj):
-    """return interfaces used by the given object by searching for implements
-    selectors
+    """return interfaces required by the given object by searching for
+    `implements` predicate
     """
-    from cubicweb.selectors import implements
     impl = obj.__select__.search_selector(implements)
     if impl:
         return sorted(impl.expected_ifaces)
     return ()
 
 def require_appobject(obj):
-    """return interfaces used by the given object by searching for implements
-    selectors
+    """return appobjects required by the given object by searching for
+    `appobject_selectable` predicate
     """
-    from cubicweb.selectors import appobject_selectable
     impl = obj.__select__.search_selector(appobject_selectable)
     if impl:
         return (impl.registry, impl.regids)
@@ -253,16 +256,13 @@
                       key=lambda x: x.cw_propval('order'))
 
 
-VRegistry.REGISTRY_FACTORY[None] = CWRegistry
-
 
 class ETypeRegistry(CWRegistry):
 
     def clear_caches(self):
         clear_cache(self, 'etype_class')
         clear_cache(self, 'parent_classes')
-        from cubicweb import selectors
-        selectors._reset_is_instance_cache(self.vreg)
+        _reset_is_instance_cache(self.vreg)
 
     def initialization_completed(self):
         """on registration completed, clear etype_class internal cache
@@ -272,7 +272,7 @@
         self.clear_caches()
 
     def register(self, obj, **kwargs):
-        oid = kwargs.get('oid') or class_regid(obj)
+        oid = kwargs.get('oid') or obj.__regid__
         if oid != 'Any' and not oid in self.schema:
             self.error('don\'t register %s, %s type not defined in the '
                        'schema', obj, oid)
@@ -354,8 +354,6 @@
             fetchattrs_list.append(set(etypecls.fetch_attrs))
         return reduce(set.intersection, fetchattrs_list)
 
-VRegistry.REGISTRY_FACTORY['etypes'] = ETypeRegistry
-
 
 class ViewsRegistry(CWRegistry):
 
@@ -389,8 +387,6 @@
                 self.exception('error while trying to select %s view for %s',
                                vid, rset)
 
-VRegistry.REGISTRY_FACTORY['views'] = ViewsRegistry
-
 
 class ActionsRegistry(CWRegistry):
     def poss_visible_objects(self, *args, **kwargs):
@@ -408,8 +404,6 @@
             result.setdefault(action.category, []).append(action)
         return result
 
-VRegistry.REGISTRY_FACTORY['actions'] = ActionsRegistry
-
 
 class CtxComponentsRegistry(CWRegistry):
     def poss_visible_objects(self, *args, **kwargs):
@@ -445,8 +439,6 @@
             component.cw_extra_kwargs['context'] = context
         return thisctxcomps
 
-VRegistry.REGISTRY_FACTORY['ctxcomponents'] = CtxComponentsRegistry
-
 
 class BwCompatCWRegistry(object):
     def __init__(self, vreg, oldreg, redirecttoreg):
@@ -462,14 +454,15 @@
     def clear(self): pass
     def initialization_completed(self): pass
 
-class CubicWebVRegistry(VRegistry):
+
+class CWRegistryStore(RegistryStore):
     """Central registry for the cubicweb instance, extending the generic
-    VRegistry with some cubicweb specific stuff.
+    RegistryStore with some cubicweb specific stuff.
 
     This is one of the central object in cubicweb instance, coupling
     dynamically loaded objects with the schema and the configuration objects.
 
-    It specializes the VRegistry by adding some convenience methods to access to
+    It specializes the RegistryStore by adding some convenience methods to access to
     stored objects. Currently we have the following registries of objects known
     by the web instance (library may use some others additional registries):
 
@@ -492,11 +485,29 @@
       plugged into the application
     """
 
+    REGISTRY_FACTORY = {None: CWRegistry,
+                        'etypes': ETypeRegistry,
+                        'views': ViewsRegistry,
+                        'actions': ActionsRegistry,
+                        'ctxcomponents': CtxComponentsRegistry,
+                        }
+
     def __init__(self, config, initlog=True):
         if initlog:
             # first init log service
             config.init_log()
-        super(CubicWebVRegistry, self).__init__(config)
+        super(CWRegistryStore, self).__init__(config.debugmode)
+        self.config = config
+        # need to clean sys.path this to avoid import confusion pb (i.e.  having
+        # the same module loaded as 'cubicweb.web.views' subpackage and as
+        # views' or 'web.views' subpackage. This is mainly for testing purpose,
+        # we should'nt need this in production environment
+        for webdir in (join(dirname(realpath(__file__)), 'web'),
+                       join(dirname(__file__), 'web')):
+            if webdir in sys.path:
+                sys.path.remove(webdir)
+        if CW_SOFTWARE_ROOT in sys.path:
+            sys.path.remove(CW_SOFTWARE_ROOT)
         self.schema = None
         self.initialized = False
         # XXX give force_reload (or refactor [re]loading...)
@@ -515,10 +526,10 @@
             return self[regid]
 
     def items(self):
-        return [item for item in super(CubicWebVRegistry, self).items()
+        return [item for item in super(CWRegistryStore, self).items()
                 if not item[0] in ('propertydefs', 'propertyvalues')]
     def iteritems(self):
-        return (item for item in super(CubicWebVRegistry, self).iteritems()
+        return (item for item in super(CWRegistryStore, self).iteritems()
                 if not item[0] in ('propertydefs', 'propertyvalues'))
 
     def values(self):
@@ -528,7 +539,7 @@
 
     def reset(self):
         CW_EVENT_MANAGER.emit('before-registry-reset', self)
-        super(CubicWebVRegistry, self).reset()
+        super(CWRegistryStore, self).reset()
         self._needs_iface = {}
         self._needs_appobject = {}
         # two special registries, propertydefs which care all the property
@@ -597,7 +608,7 @@
         the given `ifaces` interfaces at the end of the registration process.
 
         Extra keyword arguments are given to the
-        :meth:`~cubicweb.cwvreg.CubicWebVRegistry.register` function.
+        :meth:`~cubicweb.cwvreg.CWRegistryStore.register` function.
         """
         self.register(obj, **kwargs)
         if not isinstance(ifaces,  (tuple, list)):
@@ -613,7 +624,7 @@
         If `clear` is true, all objects with the same identifier will be
         previously unregistered.
         """
-        super(CubicWebVRegistry, self).register(obj, *args, **kwargs)
+        super(CWRegistryStore, self).register(obj, *args, **kwargs)
         # XXX bw compat
         ifaces = use_interfaces(obj)
         if ifaces:
@@ -630,7 +641,7 @@
     def register_objects(self, path):
         """overriden to give cubicweb's extrapath (eg cubes package's __path__)
         """
-        super(CubicWebVRegistry, self).register_objects(
+        super(CWRegistryStore, self).register_objects(
             path, self.config.extrapath)
 
     def initialization_completed(self):
@@ -685,7 +696,7 @@
                     self.debug('unregister %s (no %s object in registry %s)',
                                classid(obj), ' or '.join(regids), regname)
                     self.unregister(obj)
-        super(CubicWebVRegistry, self).initialization_completed()
+        super(CWRegistryStore, self).initialization_completed()
         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)
--- a/dbapi.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/dbapi.py	Thu Feb 23 19:06:14 2012 +0100
@@ -58,9 +58,9 @@
     attributes since classes are not designed to be shared among multiple
     registries.
     """
-    defaultcls = cwvreg.VRegistry.REGISTRY_FACTORY[None]
+    defaultcls = cwvreg.CWRegistryStore.REGISTRY_FACTORY[None]
 
-    etypescls = cwvreg.VRegistry.REGISTRY_FACTORY['etypes']
+    etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
     orig_etype_class = etypescls.orig_etype_class = etypescls.etype_class
     @monkeypatch(defaultcls)
     def etype_class(self, etype):
@@ -75,7 +75,7 @@
         return usercls
 
 def multiple_connections_unfix():
-    etypescls = cwvreg.VRegistry.REGISTRY_FACTORY['etypes']
+    etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
     etypescls.etype_class = etypescls.orig_etype_class
 
 
@@ -192,7 +192,7 @@
     elif setvreg:
         if mulcnx:
             multiple_connections_fix()
-        vreg = cwvreg.CubicWebVRegistry(config, initlog=initlog)
+        vreg = cwvreg.CWRegistryStore(config, initlog=initlog)
         schema = repo.get_schema()
         for oldetype, newetype in ETYPE_NAME_MAP.items():
             if oldetype in schema:
@@ -207,7 +207,7 @@
 
 def in_memory_repo(config):
     """Return and in_memory Repository object from a config (or vreg)"""
-    if isinstance(config, cwvreg.CubicWebVRegistry):
+    if isinstance(config, cwvreg.CWRegistryStore):
         vreg = config
         config = None
     else:
--- a/debian/control	Thu Feb 23 19:05:36 2012 +0100
+++ b/debian/control	Thu Feb 23 19:06:14 2012 +0100
@@ -35,8 +35,9 @@
 Conflicts: cubicweb-multisources
 Replaces: cubicweb-multisources
 Provides: cubicweb-multisources
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.1), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
 Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
+Suggests: python-zmq
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -99,7 +100,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.57.0), python-yams (>= 0.34.0), python-rql (>= 0.28.0), python-lxml
+Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.34.0), python-rql (>= 0.28.0), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/devtools/__init__.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/devtools/__init__.py	Thu Feb 23 19:06:14 2012 +0100
@@ -480,8 +480,8 @@
             session = repo._sessions[cnx.sessionid]
             session.set_cnxset()
             _commit = session.commit
-            def keep_cnxset_commit():
-                _commit(free_cnxset=False)
+            def keep_cnxset_commit(free_cnxset=False):
+                _commit(free_cnxset=free_cnxset)
             session.commit = keep_cnxset_commit
             pre_setup_func(session, self.config)
             session.commit()
--- a/devtools/devctl.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/devtools/devctl.py	Thu Feb 23 19:06:14 2012 +0100
@@ -107,7 +107,7 @@
     notice that relation definitions description and static vocabulary
     should be marked using '_' and extracted using xgettext
     """
-    from cubicweb.cwvreg import CubicWebVRegistry
+    from cubicweb.cwvreg import CWRegistryStore
     if cubedir:
         cube = osp.split(cubedir)[-1]
         config = DevConfiguration(cube)
@@ -119,7 +119,7 @@
         cube = libconfig = None
     cleanup_sys_modules(config)
     schema = config.load_schema(remove_unused_rtypes=False)
-    vreg = CubicWebVRegistry(config)
+    vreg = CWRegistryStore(config)
     # set_schema triggers objects registrations
     vreg.set_schema(schema)
     w(DEFAULT_POT_HEAD)
@@ -138,13 +138,13 @@
     w('\n')
     vregdone = set()
     if libconfig is not None:
-        from cubicweb.cwvreg import CubicWebVRegistry, clear_rtag_objects
+        from cubicweb.cwvreg import CWRegistryStore, clear_rtag_objects
         libschema = libconfig.load_schema(remove_unused_rtypes=False)
         afs = deepcopy(uicfg.autoform_section)
         appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu)
         clear_rtag_objects()
         cleanup_sys_modules(libconfig)
-        libvreg = CubicWebVRegistry(libconfig)
+        libvreg = CWRegistryStore(libconfig)
         libvreg.set_schema(libschema) # trigger objects registration
         libafs = uicfg.autoform_section
         libappearsin_addmenu = uicfg.actionbox_appearsin_addmenu
--- a/devtools/fake.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/devtools/fake.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -23,7 +23,7 @@
 from logilab.database import get_db_helper
 
 from cubicweb.req import RequestSessionBase
-from cubicweb.cwvreg import CubicWebVRegistry
+from cubicweb.cwvreg import CWRegistryStore
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.web.http_headers import Headers
 
@@ -34,6 +34,7 @@
     translations = {}
     uiprops = {}
     apphome = None
+    debugmode = False
     def __init__(self, appid='data', apphome=None, cubes=()):
         self.appid = appid
         self.apphome = apphome
@@ -56,7 +57,7 @@
 
     def __init__(self, *args, **kwargs):
         if not (args or 'vreg' in kwargs):
-            kwargs['vreg'] = CubicWebVRegistry(FakeConfig(), initlog=False)
+            kwargs['vreg'] = CWRegistryStore(FakeConfig(), initlog=False)
         kwargs['https'] = False
         self._url = kwargs.pop('url', None) or 'view?rql=Blop&vid=blop'
         super(FakeRequest, self).__init__(*args, **kwargs)
@@ -144,7 +145,7 @@
         if vreg is None:
             vreg = getattr(self.repo, 'vreg', None)
         if vreg is None:
-            vreg = CubicWebVRegistry(FakeConfig(), initlog=False)
+            vreg = CWRegistryStore(FakeConfig(), initlog=False)
         self.vreg = vreg
         self.cnxset = FakeConnectionsSet()
         self.user = user or FakeUser()
@@ -179,7 +180,7 @@
         self._count = 0
         self.schema = schema
         self.config = config or FakeConfig()
-        self.vreg = vreg or CubicWebVRegistry(self.config, initlog=False)
+        self.vreg = vreg or CWRegistryStore(self.config, initlog=False)
         self.vreg.schema = schema
         self.sources = []
 
--- a/devtools/test/data/views.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/devtools/test/data/views.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,7 +18,7 @@
 """only for unit tests !"""
 
 from cubicweb.view import EntityView
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 
 HTML_PAGE = u"""<html>
   <body>
--- a/devtools/testlib.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/devtools/testlib.py	Thu Feb 23 19:06:14 2012 +0100
@@ -605,7 +605,7 @@
         dump = json.dumps
         args = [dump(arg) for arg in args]
         req = self.request(fname=fname, pageid='123', arg=args)
-        ctrl = self.vreg['controllers'].select('json', req)
+        ctrl = self.vreg['controllers'].select('ajax', req)
         return ctrl.publish(), req
 
     def app_publish(self, req, path='view'):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.15.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,62 @@
+Whats new in CubicWeb 3.15
+==========================
+
+
+API changes
+-----------
+
+* The base registry implementation has been moved to a new
+  `logilab.common.registry` module (see #1916014). This includes code from :
+
+  * `cubicweb.vreg` (the whole things that was in there)
+  * `cw.appobject` (base selectors and all).
+
+  In the process, some renaming was done:
+
+  * the top level registry is now `RegistryStore` (was `VRegistry`), but that
+    should not impact cubicweb client code ;
+
+  * former selectors functions are now known as "predicate", though you still use
+    predicates to build an object'selector ;
+
+  * for consistency, the `objectify_selector` decoraror has hence be renamed to
+    `objectify_predicate` ;
+
+  * on the CubicWeb side, the `selectors` module has been renamed to
+    `predicates`.
+
+  Debugging refactoring dropped the more need for the `lltrace` decorator.
+
+  There should be full backward compat with proper deprecation warnings.
+
+  Notice the `yes` predicate and `objectify_predicate` decorator, as well as the
+  `traced_selection` function should now be imported from the
+  `logilab.common.registry` module.
+
+
+Unintrusive API changes
+-----------------------
+
+* new 'ldapfeed' source type, designed to replace 'ldapuser' source with
+  data-feed (i.e. copy based) source ideas.
+
+
+RQL
+---
+
+
+
+User interface changes
+----------------------
+
+
+
+Configuration
+-------------
+
+Base schema changes
+-------------------
+Email address 'read' permission is now more restrictive: only managers and
+users to which an address belong may see them. Application that wish other
+settings should set them explicitly.
+
--- a/doc/book/en/annexes/faq.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/annexes/faq.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -194,7 +194,7 @@
         """return an iterator on possible objects in this registry for the given
         context
         """
-        from cubicweb.selectors import traced_selection
+        from logilab.common.registry import traced_selection
         with traced_selection():
             for appobjects in self.itervalues():
                 try:
@@ -217,7 +217,7 @@
     def _select_view_and_rset(self, rset):
         ...
         try:
-            from cubicweb.selectors import traced_selection
+            from logilab.common.registry import traced_selection
             with traced_selection():
                 view = self._cw.vreg['views'].select(vid, req, rset=rset)
         except ObjectNotFound:
--- a/doc/book/en/annexes/rql/language.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/annexes/rql/language.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -614,15 +614,18 @@
 +--------------------------+----------------------------------------+
 | :func:`YEAR(Date)`       | return the year of a date or datetime  |
 +--------------------------+----------------------------------------+
-| :func:`MONTH(Date)`      | return the year of a date or datetime  |
+| :func:`MONTH(Date)`      | return the month of a date or datetime |
 +--------------------------+----------------------------------------+
-| :func:`DAY(Date)`        | return the year of a date or datetime  |
+| :func:`DAY(Date)`        | return the day of a date or datetime   |
++--------------------------+----------------------------------------+
+| :func:`HOUR(Datetime)`   | return the hours of a datetime         |
 +--------------------------+----------------------------------------+
-| :func:`HOUR(Datetime)`   | return the year of a datetime          |
+| :func:`MINUTE(Datetime)` | return the minutes of a datetime       |
 +--------------------------+----------------------------------------+
-| :func:`MINUTE(Datetime)` | return the year of a datetime          |
+| :func:`SECOND(Datetime)` | return the seconds of a datetime       |
 +--------------------------+----------------------------------------+
-| :func:`SECOND(Datetime)` | return the year of a datetime          |
+| :func:`WEEKDAY(Date)`    | return the day of week of a date or    |
+|                          | datetime.  Sunday == 1, Saturday == 7. |
 +--------------------------+----------------------------------------+
 
 .. _RQLOtherFunctions:
--- a/doc/book/en/devrepo/datamodel/definition.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -186,6 +186,9 @@
 * `default`: default value of the attribute. In case of date types, the values
   which could be used correspond to the RQL keywords `TODAY` and `NOW`.
 
+* `metadata`: Is also accepted as an argument of the attribute contructor. It is
+  not really an attribute property. see `Metadata`_ for details.
+
 Properties for `String` attributes:
 
 * `fulltextindexed`: boolean indicating if the attribute is part of
@@ -567,17 +570,41 @@
  In any case, identifiers starting with "CW" or "cw" are reserved for
  internal use by the framework.
 
+ .. _Metadata:
+
+ Some attribute using the name of another attribute as prefix are considered
+ metadata.  For example, if an EntityType have both a ``data`` and
+ ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata
+ of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch
+ metadata related to an attribute. There are only three valid metadata names:
+ ``format``, ``encoding`` and ``name``.
+
 
 The name of the Python attribute corresponds to the name of the attribute
 or the relation in *CubicWeb* application.
 
 An attribute is defined in the schema as follows::
 
-    attr_name = attr_type(properties)
+    attr_name = AttrType(*properties, metadata={})
+
+where
+
+* `AttrType`: is one of the type listed in EntityType_,
+
+* `properties`: is a list of the attribute needs to satisfy (see `Properties`_
+  for more details),
 
-where `attr_type` is one of the type listed above and `properties` is
-a list of the attribute needs to satisfy (see `Properties`_
-for more details).
+* `metadata`: is a dictionary of meta attributes related to ``attr_name``.
+  Dictionary keys are the name of the meta attribute. Dictionary values
+  attributes objects (like the content of ``AttrType``). For each entry of the
+  metadata dictionary a ``<attr_name>_<key> = <value>`` attribute is
+  automaticaly added to the EntityType.  see `Metadata`_ section for details
+  about valid key.
+
+
+ ---
+
+While building your schema
 
 * it is possible to use the attribute `meta` to flag an entity type as a `meta`
   (e.g. used to describe/categorize other entities)
--- a/doc/book/en/devrepo/entityclasses/adapters.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devrepo/entityclasses/adapters.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -45,9 +45,9 @@
 
    Adapters came with the notion of service identified by the registry identifier
    of an adapters, hence dropping the need for explicit interface and the
-   :class:`cubicweb.selectors.implements` selector. You should instead use
-   :class:`cubicweb.selectors.is_instance` when you want to select on an entity
-   type, or :class:`cubicweb.selectors.adaptable` when you want to select on a
+   :class:`cubicweb.predicates.implements` selector. You should instead use
+   :class:`cubicweb.predicates.is_instance` when you want to select on an entity
+   type, or :class:`cubicweb.predicates.adaptable` when you want to select on a
    service.
 
 
@@ -79,7 +79,7 @@
 
 .. sourcecode:: python
 
-    from cubicweb.selectors import implements
+    from cubicweb.predicates import implements
     from cubicweb.interfaces import ITree
     from cubicweb.mixins import ITreeMixIn
 
@@ -97,7 +97,7 @@
 
 .. sourcecode:: python
 
-    from cubicweb.selectors import adaptable, is_instance
+    from cubicweb.predicates import adaptable, is_instance
     from cubicweb.entities.adapters import ITreeAdapter
 
     class MyEntityITreeAdapter(ITreeAdapter):
--- a/doc/book/en/devrepo/repo/hooks.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devrepo/repo/hooks.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -26,7 +26,7 @@
 .. sourcecode:: python
 
    from cubicweb import ValidationError
-   from cubicweb.selectors import is_instance
+   from cubicweb.predicates import is_instance
    from cubicweb.server.hook import Hook
 
    class PersonAgeRange(Hook):
@@ -162,6 +162,44 @@
 :ref:`adv_tuto_security_propagation`.
 
 
+Inter-instance communication
+----------------------------
+
+If your application consists of several instances, you may need some means to
+communicate between them.  Cubicweb provides a publish/subscribe mechanism
+using ØMQ_.  In order to use it, use
+:meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
+`repo.app_instances_bus` object.  The `callback` will get the message (as a
+list).  A message can be sent by calling
+:meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
+The first element of the message is the topic which is used for filtering and
+dispatching messages.
+
+.. _ØMQ: http://www.zeromq.org/
+
+.. sourcecode:: python
+
+  class FooHook(hook.Hook):
+      events = ('server_startup',)
+      __regid__ = 'foo_startup'
+
+      def __call__(self):
+          def callback(msg):
+              self.info('received message: %s', ' '.join(msg))
+          self.repo.app_instances_bus.subscribe('hello', callback)
+
+.. sourcecode:: python
+
+  def do_foo(self):
+      actually_do_foo()
+      self._cw.repo.app_instances_bus.publish(['hello', 'world'])
+
+The `zmq-address-pub` configuration variable contains the address used
+by the instance for sending messages, e.g. `tcp://*:1234`.  The
+`zmq-address-sub` variable contains a comma-separated list of addresses
+to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
+
+
 Hooks writing tips
 ------------------
 
--- a/doc/book/en/devrepo/vreg.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devrepo/vreg.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -1,5 +1,5 @@
-The VRegistry, selectors and application objects
-================================================
+The Registry, selectors and application objects
+===============================================
 
 This chapter deals with some of the  core concepts of the |cubicweb| framework
 which make it different from other frameworks (and maybe not easy to
@@ -13,107 +13,108 @@
 :ref:`VRegistryIntro` chapter.
 
 .. autodocstring:: cubicweb.cwvreg
-.. autodocstring:: cubicweb.selectors
+.. autodocstring:: cubicweb.predicates
 .. automodule:: cubicweb.appobject
 
-Base selectors
---------------
+Base predicates
+---------------
 
-Selectors are scoring functions that are called by the registry to tell whenever
-an appobject can be selected in a given context. Selector sets are for instance
-the glue that tie views to the data model. Using them appropriately is an
+Predicates are scoring functions that are called by the registry to tell whenever
+an appobject can be selected in a given context. Predicates may be chained
+together using operators to build a selector. A selector is the glue that tie
+views to the data model or whatever input context. Using them appropriately is an
 essential part of the construction of well behaved cubes.
 
-Of course you may have to write your own set of selectors as your needs grows and
-you get familiar with the framework (see :ref:`CustomSelectors`).
+Of course you may have to write your own set of predicates as your needs grows
+and you get familiar with the framework (see :ref:`CustomPredicates`).
 
-Here is a description of generic selectors provided by CubicWeb that should suit
+Here is a description of generic predicates provided by CubicWeb that should suit
 most of your needs.
 
-Bare selectors
-~~~~~~~~~~~~~~
-Those selectors are somewhat dumb, which doesn't mean they're not (very) useful.
+Bare predicates
+~~~~~~~~~~~~~~~
+Those predicates are somewhat dumb, which doesn't mean they're not (very) useful.
 
 .. autoclass:: cubicweb.appobject.yes
-.. autoclass:: cubicweb.selectors.match_kwargs
-.. autoclass:: cubicweb.selectors.appobject_selectable
-.. autoclass:: cubicweb.selectors.adaptable
-.. autoclass:: cubicweb.selectors.configuration_values
+.. autoclass:: cubicweb.predicates.match_kwargs
+.. autoclass:: cubicweb.predicates.appobject_selectable
+.. autoclass:: cubicweb.predicates.adaptable
+.. autoclass:: cubicweb.predicates.configuration_values
 
 
-Result set selectors
+Result set predicates
 ~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for a result set in the context ('rset' argument or
+Those predicates are looking for a result set in the context ('rset' argument or
 the input context) and match or not according to its shape. Some of these
-selectors have different behaviour if a particular cell of the result set is
+predicates have different behaviour if a particular cell of the result set is
 specified using 'row' and 'col' arguments of the input context or not.
 
-.. autoclass:: cubicweb.selectors.none_rset
-.. autoclass:: cubicweb.selectors.any_rset
-.. autoclass:: cubicweb.selectors.nonempty_rset
-.. autoclass:: cubicweb.selectors.empty_rset
-.. autoclass:: cubicweb.selectors.one_line_rset
-.. autoclass:: cubicweb.selectors.multi_lines_rset
-.. autoclass:: cubicweb.selectors.multi_columns_rset
-.. autoclass:: cubicweb.selectors.paginated_rset
-.. autoclass:: cubicweb.selectors.sorted_rset
-.. autoclass:: cubicweb.selectors.one_etype_rset
-.. autoclass:: cubicweb.selectors.multi_etypes_rset
+.. autoclass:: cubicweb.predicates.none_rset
+.. autoclass:: cubicweb.predicates.any_rset
+.. autoclass:: cubicweb.predicates.nonempty_rset
+.. autoclass:: cubicweb.predicates.empty_rset
+.. autoclass:: cubicweb.predicates.one_line_rset
+.. autoclass:: cubicweb.predicates.multi_lines_rset
+.. autoclass:: cubicweb.predicates.multi_columns_rset
+.. autoclass:: cubicweb.predicates.paginated_rset
+.. autoclass:: cubicweb.predicates.sorted_rset
+.. autoclass:: cubicweb.predicates.one_etype_rset
+.. autoclass:: cubicweb.predicates.multi_etypes_rset
 
 
-Entity selectors
-~~~~~~~~~~~~~~~~
-Those selectors are looking for either an `entity` argument in the input context,
+Entity predicates
+~~~~~~~~~~~~~~~~~
+Those predicates are looking for either an `entity` argument in the input context,
 or entity found in the result set ('rset' argument or the input context) and
 match or not according to entity's (instance or class) properties.
 
-.. autoclass:: cubicweb.selectors.non_final_entity
-.. autoclass:: cubicweb.selectors.is_instance
-.. autoclass:: cubicweb.selectors.score_entity
-.. autoclass:: cubicweb.selectors.rql_condition
-.. autoclass:: cubicweb.selectors.relation_possible
-.. autoclass:: cubicweb.selectors.partial_relation_possible
-.. autoclass:: cubicweb.selectors.has_related_entities
-.. autoclass:: cubicweb.selectors.partial_has_related_entities
-.. autoclass:: cubicweb.selectors.has_permission
-.. autoclass:: cubicweb.selectors.has_add_permission
-.. autoclass:: cubicweb.selectors.has_mimetype
-.. autoclass:: cubicweb.selectors.is_in_state
-.. autofunction:: cubicweb.selectors.on_fire_transition
-.. autoclass:: cubicweb.selectors.implements
+.. autoclass:: cubicweb.predicates.non_final_entity
+.. autoclass:: cubicweb.predicates.is_instance
+.. autoclass:: cubicweb.predicates.score_entity
+.. autoclass:: cubicweb.predicates.rql_condition
+.. autoclass:: cubicweb.predicates.relation_possible
+.. autoclass:: cubicweb.predicates.partial_relation_possible
+.. autoclass:: cubicweb.predicates.has_related_entities
+.. autoclass:: cubicweb.predicates.partial_has_related_entities
+.. autoclass:: cubicweb.predicates.has_permission
+.. autoclass:: cubicweb.predicates.has_add_permission
+.. autoclass:: cubicweb.predicates.has_mimetype
+.. autoclass:: cubicweb.predicates.is_in_state
+.. autofunction:: cubicweb.predicates.on_fire_transition
+.. autoclass:: cubicweb.predicates.implements
 
 
-Logged user selectors
-~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for properties of the user issuing the request.
+Logged user predicates
+~~~~~~~~~~~~~~~~~~~~~~
+Those predicates are looking for properties of the user issuing the request.
 
-.. autoclass:: cubicweb.selectors.match_user_groups
+.. autoclass:: cubicweb.predicates.match_user_groups
 
 
-Web request selectors
-~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for properties of *web* request, they can not be
+Web request predicates
+~~~~~~~~~~~~~~~~~~~~~~
+Those predicates are looking for properties of *web* request, they can not be
 used on the data repository side.
 
-.. autoclass:: cubicweb.selectors.no_cnx
-.. autoclass:: cubicweb.selectors.anonymous_user
-.. autoclass:: cubicweb.selectors.authenticated_user
-.. autoclass:: cubicweb.selectors.match_form_params
-.. autoclass:: cubicweb.selectors.match_search_state
-.. autoclass:: cubicweb.selectors.match_context_prop
-.. autoclass:: cubicweb.selectors.match_context
-.. autoclass:: cubicweb.selectors.match_view
-.. autoclass:: cubicweb.selectors.primary_view
-.. autoclass:: cubicweb.selectors.contextual
-.. autoclass:: cubicweb.selectors.specified_etype_implements
-.. autoclass:: cubicweb.selectors.attribute_edited
-.. autoclass:: cubicweb.selectors.match_transition
+.. autoclass:: cubicweb.predicates.no_cnx
+.. autoclass:: cubicweb.predicates.anonymous_user
+.. autoclass:: cubicweb.predicates.authenticated_user
+.. autoclass:: cubicweb.predicates.match_form_params
+.. autoclass:: cubicweb.predicates.match_search_state
+.. autoclass:: cubicweb.predicates.match_context_prop
+.. autoclass:: cubicweb.predicates.match_context
+.. autoclass:: cubicweb.predicates.match_view
+.. autoclass:: cubicweb.predicates.primary_view
+.. autoclass:: cubicweb.predicates.contextual
+.. autoclass:: cubicweb.predicates.specified_etype_implements
+.. autoclass:: cubicweb.predicates.attribute_edited
+.. autoclass:: cubicweb.predicates.match_transition
 
 
-Other selectors
-~~~~~~~~~~~~~~~
-.. autoclass:: cubicweb.selectors.match_exception
-.. autoclass:: cubicweb.selectors.debug_mode
+Other predicates
+~~~~~~~~~~~~~~~~
+.. autoclass:: cubicweb.predicates.match_exception
+.. autoclass:: cubicweb.predicates.debug_mode
 
-You'll also find some other (very) specific selectors hidden in other modules
-than :mod:`cubicweb.selectors`.
+You'll also find some other (very) specific predicates hidden in other modules
+than :mod:`cubicweb.predicates`.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/ajax.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,12 @@
+.. _ajax:
+
+Ajax
+----
+
+CubicWeb provides a few helpers to facilitate *javascript <-> python* communications.
+
+You can, for instance, register some python functions that will become
+callable from javascript through ajax calls. All the ajax URLs are handled
+by the ``AjaxController`` controller.
+
+.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/controllers.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devweb/controllers.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -22,10 +22,6 @@
   :exc:`NoSelectableObject` errors that may bubble up to its entry point, in an
   end-user-friendly way (but other programming errors will slip through)
 
-* the JSon controller (same module) provides services for Ajax calls,
-  typically using JSON as a serialization format for input, and
-  sometimes using either JSON or XML for output;
-
 * the JSonpController is a wrapper around the ``ViewController`` that
   provides jsonp_ services. Padding can be specified with the
   ``callback`` request parameter. Only *jsonexport* / *ejsonexport*
@@ -36,10 +32,6 @@
 * the Login/Logout controllers make effective user login or logout
   requests
 
-.. warning::
-
-  JsonController will probably be renamed into AjaxController soon since
-  it has nothing to do with json per se.
 
 .. _jsonp: http://en.wikipedia.org/wiki/JSONP
 
@@ -64,6 +56,13 @@
 * the MailBugReport controller (web/views/basecontrollers.py) allows
   to quickly have a `reportbug` feature in one's application
 
+* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`
+  (:mod:`cubicweb.web.views.ajaxcontroller`) provides
+  services for Ajax calls, typically using JSON as a serialization format
+  for input, and sometimes using either JSON or XML for output. See
+  :ref:`ajax` chapter for more information.
+
+
 Registration
 ++++++++++++
 
--- a/doc/book/en/devweb/edition/form.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devweb/edition/form.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -232,7 +232,7 @@
     from logilab.common import date
     from logilab.mtconverter import xml_escape
     from cubicweb.view import View
-    from cubicweb.selectors import match_kwargs
+    from cubicweb.predicates import match_kwargs
     from cubicweb.web import RequestError, ProcessFormError
     from cubicweb.web import formfields as fields, formwidgets as wdgs
     from cubicweb.web.views import forms, calendar
--- a/doc/book/en/devweb/index.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devweb/index.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -12,6 +12,7 @@
    request
    views/index
    rtags
+   ajax
    js
    css
    edition/index
--- a/doc/book/en/devweb/js.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devweb/js.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -72,21 +72,22 @@
 A simple example with asyncRemoteExec
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-In the python side, we have to extend the ``BaseController``
-class. The ``@jsonize`` decorator ensures that the return value of the
-method is encoded as JSON data. By construction, the JSonController
-inputs everything in JSON format.
+On the python side, we have to define an
+:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
+simplest way to do that is to use the
+:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
+details on this, refer to :ref:`ajax`).
 
 .. sourcecode: python
 
-    from cubicweb.web.views.basecontrollers import JSonController, jsonize
+    from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
-    @monkeypatch(JSonController)
-    @jsonize
+    # serialize output to json to get it back easily on the javascript side
+    @ajaxfunc(output_type='json')
     def js_say_hello(self, name):
         return u'hello %s' % name
 
-In the javascript side, we do the asynchronous call. Notice how it
+On the javascript side, we do the asynchronous call. Notice how it
 creates a `deferred` object. Proper treatment of the return value or
 error handling has to be done through the addCallback and addErrback
 methods.
@@ -131,7 +132,7 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The server side implementation of `reloadComponent` is the
-js_component method of the JSonController.
+:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.
 
 The following function implements a two-steps method to delete a
 standard bookmark and refresh the UI, while keeping the UI responsive.
@@ -166,7 +167,8 @@
 
 
 * `url` (mandatory) should be a complete url (typically referencing
-  the JSonController, but this is not strictly mandatory)
+  the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
+  but this is not strictly mandatory)
 
 * `data` (optional) is a dictionary of values given to the
   controller specified through an `url` argument; some keys may have a
@@ -204,25 +206,23 @@
 
 .. sourcecode:: python
 
-    from cubicweb import typed_eid
-    from cubicweb.web.views.basecontrollers import JSonController, xhtmlize
+    from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
-    @monkeypatch(JSonController)
-    @xhtmlize
+    @ajaxfunc(output_type='xhtml')
     def js_frob_status(self, eid, frobname):
-        entity = self._cw.entity_from_eid(typed_eid(eid))
+        entity = self._cw.entity_from_eid(eid)
         return entity.view('frob', name=frobname)
 
 .. sourcecode:: javascript
 
-    function update_some_div(divid, eid, frobname) {
+    function updateSomeDiv(divid, eid, frobname) {
         var params = {fname:'frob_status', eid: eid, frobname:frobname};
         jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
      }
 
 In this example, the url argument is the base json url of a cube
 instance (it should contain something like
-`http://myinstance/json?`). The actual JSonController method name is
+`http://myinstance/ajax?`). The actual AjaxController method name is
 encoded in the `params` dictionary using the `fname` key.
 
 A more real-life example
@@ -250,7 +250,7 @@
         w(u'</div>')
         self._cw.add_onload(u"""
             jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
-                   load_now('#lazy-%(vid)s');});"""
+                   loadNow('#lazy-%(vid)s');});"""
             % {'event': 'load_%s' % vid, 'vid': vid})
 
 This creates a `div` with a specific event associated to it.
@@ -271,7 +271,7 @@
 
 .. sourcecode:: javascript
 
-    function load_now(eltsel) {
+    function loadNow(eltsel) {
         var lazydiv = jQuery(eltsel);
         lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
     }
@@ -306,18 +306,77 @@
         """trigger an event that will force immediate loading of the view
         on dom readyness
         """
-        self._cw.add_onload("trigger_load('%s');" % vid)
+        self._cw.add_onload("triggerLoad('%s');" % vid)
 
 The browser-side definition follows.
 
 .. sourcecode:: javascript
 
-    function trigger_load(divid) {
+    function triggerLoad(divid) {
         jQuery('#lazy-' + divd).trigger('load_' + divid);
     }
 
 
-.. XXX userCallback / user_callback
+python/ajax dynamic callbacks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+CubicWeb provides a way to dynamically register a function and make it
+callable from the javascript side. The typical use case for this is a
+situation where you have everything at hand to implement an action
+(whether it be performing a RQL query or executing a few python
+statements) that you'd like to defer to a user click in the web
+interface.  In other words, generate an HTML ``<a href=...`` link that
+would execute your few lines of code.
+
+The trick is to create a python function and store this function in
+the user's session data. You will then be able to access it later.
+While this might sound hard to implement, it's actually quite easy
+thanks to the ``_cw.user_callback()``. This method takes a function,
+registers it and returns a javascript instruction suitable for
+``href`` or ``onclick`` usage. The call is then performed
+asynchronously.
+
+Here's a simplified example taken from the vcreview_ cube that will
+generate a link to change an entity state directly without the
+standard intermediate *comment / validate* step:
+
+.. sourcecode:: python
+
+    def entity_call(self, entity):
+        # [...]
+        def change_state(req, eid):
+            entity = req.entity_from_eid(eid)
+            entity.cw_adapt_to('IWorkflowable').fire_transition('done')
+        url = self._cw.user_callback(change_state, (entity.eid,))
+        self.w(tags.input(type='button', onclick=url, value=self._cw._('mark as done')))
+
+
+The ``change_state`` callback function is registered with
+``self._cw.user_callback()`` which returns the ``url`` value directly
+used for the ``onclick`` attribute of the button. On the javascript
+side, the ``userCallback()`` function is used but you most probably
+won't have to bother with it.
+
+Of course, when dealing with session data, the question of session
+cleaning pops up immediately. If you use ``user_callback()``, the
+registered function will be deleted automatically at some point
+as any other session data. If you want your function to be deleted once
+the web page is unloaded or when the user has clicked once on your link, then
+``_cw.register_onetime_callback()`` is what you need. It behaves as
+``_cw.user_callback()`` but stores the function in page data instead
+of global session data.
+
+
+.. Warning::
+
+  Be careful when registering functions with closures, keep in mind that
+  enclosed data will be kept in memory until the session gets cleared. Also,
+  if you keep entities or any object referecing the current ``req`` object, you
+  might have problems reusing them later because the underlying session
+  might have been closed at the time the callback gets executed.
+
+
+.. _vcreview: http://www.cubicweb.org/project/cubicweb-vcreview
 
 Javascript library: overview
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -356,12 +415,12 @@
 
 .. toctree::
     :maxdepth: 1
-    
+
     js_api/index
 
 
 Testing javascript
-~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~
 
 You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
 inside the python unittest run . You simply have to define a new class that
--- a/doc/book/en/devweb/views/primary.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devweb/views/primary.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -176,7 +176,7 @@
 
 .. sourcecode:: python
 
-   from cubicweb.selectors import is_instance
+   from cubicweb.predicates import is_instance
    from cubicweb.web.views.primary import Primaryview
 
    class BlogEntryPrimaryView(PrimaryView):
@@ -206,7 +206,7 @@
 .. sourcecode:: python
 
  from logilab.mtconverter import xml_escape
- from cubicweb.selectors import is_instance, one_line_rset
+ from cubicweb.predicates import is_instance, one_line_rset
  from cubicweb.web.views.primary import Primaryview
 
  class BlogPrimaryView(PrimaryView):
--- a/doc/book/en/devweb/views/startup.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/devweb/views/startup.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -3,7 +3,7 @@
 
 Startup views are views requiring no context, from which you usually start
 browsing (for instance the index page). The usual selectors are
-:class:`~cubicweb.selectors.none_rset` or :class:`~cubicweb.selectors.yes`.
+:class:`~cubicweb.predicates.none_rset` or :class:`~logilab.common.registry.yes`.
 
 You'll find here a description of startup views provided by the framework.
 
--- a/doc/book/en/tutorials/advanced/part02_security.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/tutorials/advanced/part02_security.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -187,7 +187,7 @@
 
 .. sourcecode:: python
 
-    from cubicweb.selectors import is_instance
+    from cubicweb.predicates import is_instance
     from cubicweb.server import hook
 
     class SetVisibilityOp(hook.DataOperationMixIn, hook.Operation):
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -23,7 +23,7 @@
 
 .. sourcecode:: python
 
-    from cubicweb.selectors import is_instance
+    from cubicweb.predicates import is_instance
     from cubicweb.web import component
     from cubicweb.web.views import error
 
@@ -210,7 +210,7 @@
 
 .. sourcecode:: python
 
-    from cubicweb.selectors import is_instance
+    from cubicweb.predicates import is_instance
     from cubicweb.web.views import navigation
 
 
--- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -190,7 +190,7 @@
 
 .. sourcecode:: python
 
-  from cubicweb.selectors import none_rset
+  from cubicweb.predicates import none_rset
   from cubicweb.web.views import bookmark
   from cubes.zone import views as zone
   from cubes.tag import views as tag
--- a/doc/book/en/tutorials/base/customizing-the-application.rst	Thu Feb 23 19:05:36 2012 +0100
+++ b/doc/book/en/tutorials/base/customizing-the-application.rst	Thu Feb 23 19:06:14 2012 +0100
@@ -199,8 +199,8 @@
     particular context. When looking for a particular view (e.g. given an
     identifier), |cubicweb| computes for each available view with that identifier
     a score which is returned by the selector. Then the view with the highest
-    score is used. The standard library of selectors is in
-    :mod:`cubicweb.selector`.
+    score is used. The standard library of predicates is in
+    :mod:`cubicweb.predicates`.
 
 A view has a set of methods inherited from the :class:`cubicweb.view.View` class,
 though you usually don't derive directly from this class but from one of its more
@@ -310,7 +310,7 @@
 
 .. sourcecode:: python
 
-  from cubicweb.selectors import is_instance
+  from cubicweb.predicates import is_instance
   from cubicweb.web.views import primary
 
   class CommunityPrimaryView(primary.PrimaryView):
--- a/entities/adapters.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/entities/adapters.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -29,7 +29,7 @@
 from logilab.common.deprecation import class_deprecated
 
 from cubicweb import ValidationError, view
-from cubicweb.selectors import (implements, is_instance, relation_possible,
+from cubicweb.predicates import (implements, is_instance, relation_possible,
                                 match_exception)
 from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
 
--- a/entities/wfobjs.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/entities/wfobjs.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -32,7 +32,7 @@
 
 from cubicweb.entities import AnyEntity, fetch_config
 from cubicweb.view import EntityAdapter
-from cubicweb.selectors import relation_possible
+from cubicweb.predicates import relation_possible
 from cubicweb.mixins import MI_REL_TRIGGERS
 
 class WorkflowException(Exception): pass
--- a/entity.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/entity.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -24,6 +24,7 @@
 from logilab.common import interface
 from logilab.common.decorators import cached
 from logilab.common.deprecation import deprecated
+from logilab.common.registry import yes
 from logilab.mtconverter import TransformData, TransformError, xml_escape
 
 from rql.utils import rqlvar_maker
@@ -34,7 +35,6 @@
 from cubicweb import Unauthorized, typed_eid, neg_role
 from cubicweb.utils import support_args
 from cubicweb.rset import ResultSet
-from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
 from cubicweb.req import _check_cw_unsafe
 from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
--- a/hooks/integrity.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/integrity.py	Thu Feb 23 19:06:14 2012 +0100
@@ -28,7 +28,7 @@
 from cubicweb import ValidationError
 from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
                              RQLConstraint, RQLUniqueConstraint)
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.uilib import soup2xhtml
 from cubicweb.server import hook
 
--- a/hooks/metadata.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/metadata.py	Thu Feb 23 19:06:14 2012 +0100
@@ -21,7 +21,7 @@
 
 from datetime import datetime
 
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.server import hook
 from cubicweb.server.edition import EditedEntity
 
--- a/hooks/notification.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/notification.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,7 +22,7 @@
 from logilab.common.textutils import normalize_text
 
 from cubicweb import RegistryNotFound
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.server import hook
 from cubicweb.sobjects.supervising import SupervisionMailOp
 
--- a/hooks/security.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/security.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,8 +21,9 @@
 
 __docformat__ = "restructuredtext en"
 
+from logilab.common.registry import objectify_predicate
+
 from cubicweb import Unauthorized
-from cubicweb.selectors import objectify_selector, lltrace
 from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
 
 
@@ -64,8 +65,7 @@
             rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto)
 
 
-@objectify_selector
-@lltrace
+@objectify_predicate
 def write_security_enabled(cls, req, **kwargs):
     if req is None or not req.write_security:
         return 0
--- a/hooks/syncschema.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/syncschema.py	Thu Feb 23 19:06:14 2012 +0100
@@ -32,7 +32,7 @@
 from logilab.common.decorators import clear_cache
 
 from cubicweb import ValidationError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
 from cubicweb.server import hook, schemaserial as ss
--- a/hooks/syncsession.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/syncsession.py	Thu Feb 23 19:06:14 2012 +0100
@@ -21,7 +21,7 @@
 
 from yams.schema import role_name
 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.server import hook
 
 
--- a/hooks/syncsources.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/syncsources.py	Thu Feb 23 19:06:14 2012 +0100
@@ -23,7 +23,7 @@
 from yams.schema import role_name
 
 from cubicweb import ValidationError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.server import SOURCE_TYPES, hook
 
 class SourceHook(hook.Hook):
--- a/hooks/workflow.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/hooks/workflow.py	Thu Feb 23 19:06:14 2012 +0100
@@ -24,7 +24,7 @@
 from yams.schema import role_name
 
 from cubicweb import RepositoryError, ValidationError
-from cubicweb.selectors import is_instance, adaptable
+from cubicweb.predicates import is_instance, adaptable
 from cubicweb.server import hook
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/zmq.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+from cubicweb.server import hook
+
+class ZMQStopHook(hook.Hook):
+    __regid__ = 'zmqstop'
+    events = ('server_shutdown',)
+
+    def __call__(self):
+        self.repo.app_instances_bus.stop()
+
+class ZMQStartHook(hook.Hook):
+    __regid__ = 'zmqstart'
+    events = ('server_startup',)
+
+    def __call__(self):
+        config = self.repo.config
+        address_pub = config.get('zmq-address-pub')
+        if not address_pub:
+            return
+        from cubicweb.server import cwzmq
+        self.repo.app_instances_bus = cwzmq.ZMQComm()
+        self.repo.app_instances_bus.add_publisher(address_pub)
+        def clear_cache_callback(msg):
+            self.debug('clear_caches: %s', ' '.join(msg))
+            self.repo.clear_caches(msg[1:])
+        self.repo.app_instances_bus.add_subscription('delete', clear_cache_callback)
+        for address in config.get('zmq-address-sub'):
+            self.repo.app_instances_bus.add_subscriber(address)
+        self.repo.app_instances_bus.start()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.0_Any.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,10 @@
+sync_schema_props_perms('EmailAddress')
+
+for source in rql('CWSource X WHERE X type "ldapuser"').entities():
+    config = source.dictconfig
+    host = config.pop('host', u'ldap')
+    protocol = config.pop('protocol', u'ldap')
+    source.set_attributes(url=u'%s://%s' % (protocol, host))
+    source.update_config(skip_unknown=True, **config)
+
+commit()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/ldapuser2ldapfeed.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,76 @@
+"""turn a pyro source into a datafeed source
+
+Once this script is run, execute c-c db-check to cleanup relation tables.
+"""
+import sys
+
+try:
+    source_name, = __args__
+    source = repo.sources_by_uri[source_name]
+except ValueError:
+    print('you should specify the source name as script argument (i.e. after --'
+          ' on the command line)')
+    sys.exit(1)
+except KeyError:
+    print '%s is not an active source' % source_name
+    sys.exit(1)
+
+# check source is reachable before doing anything
+if not source.get_connection().cnx:
+    print '%s is not reachable. Fix this before running this script' % source_name
+    sys.exit(1)
+
+raw_input('Ensure you have shutdown all instances of this application before continuing.'
+          ' Type enter when ready.')
+
+system_source = repo.system_source
+
+from datetime import datetime
+from cubicweb.server.edition import EditedEntity
+
+
+session.mode = 'write' # hold on the connections set
+
+print '******************** backport entity content ***************************'
+
+todelete = {}
+for entity in rql('Any X WHERE X cw_source S, S eid %(s)s', {'s': source.eid}).entities():
+        etype = entity.__regid__
+        if not source.support_entity(etype):
+            print "source doesn't support %s, delete %s" % (etype, entity.eid)
+        else:
+            try:
+                entity.complete()
+            except Exception:
+                print '%s %s much probably deleted, delete it (extid %s)' % (
+                    etype, entity.eid, entity.cw_metainformation()['extid'])
+            else:
+                print 'get back', etype, entity.eid
+                entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
+                if not entity.creation_date:
+                    entity.cw_edited['creation_date'] = datetime.now()
+                if not entity.modification_date:
+                    entity.cw_edited['modification_date'] = datetime.now()
+                if not entity.upassword:
+                    entity.cw_edited['upassword'] = u''
+                if not entity.cwuri:
+                    entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
+                        source.urls[0], entity.cw_metainformation()['extid'])
+                print entity.cw_edited
+                system_source.add_entity(session, entity)
+                sql("UPDATE entities SET source='system' "
+                    "WHERE eid=%(eid)s", {'eid': entity.eid})
+                continue
+        todelete.setdefault(etype, []).append(entity)
+
+# only cleanup entities table, remaining stuff should be cleaned by a c-c
+# db-check to be run after this script
+for entities in todelete.values():
+    system_source.delete_info_multi(session, entities, source_name)
+
+
+source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0)
+source_ent.set_attributes(type=u"ldapfeed", parser=u"ldapfeed")
+
+
+commit()
--- a/mixins.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/mixins.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -23,7 +23,7 @@
 from logilab.common.decorators import cached
 from logilab.common.deprecation import deprecated, class_deprecated
 
-from cubicweb.selectors import implements
+from cubicweb.predicates import implements
 from cubicweb.interfaces import ITree
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/predicates.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,1569 @@
+# 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/>.
+""".. _Selectors:
+
+Predicates and selectors
+------------------------
+
+A predicate is a class testing a particular aspect of a context. A selector is
+built by combining existant predicates or even selectors.
+
+Using and combining existant predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can combine predicates using the `&`, `|` and `~` operators.
+
+When two predicates are combined using the `&` operator, it means that
+both should return a positive score. On success, the sum of scores is
+returned.
+
+When two predicates are combined using the `|` operator, it means that
+one of them should return a positive score. On success, the first
+positive score is returned.
+
+You can also "negate" a predicate by precedeing it by the `~` unary operator.
+
+Of course you can use parenthesis to balance expressions.
+
+Example
+~~~~~~~
+
+The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
+the blog entity itself.
+
+To do that, one defines a method on entity classes that returns the
+RSS stream url for a given entity. The default implementation on
+:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
+as base for all others) and a specific implementation on `Blog` will
+do what we want.
+
+But when we have a result set containing several `Blog` entities (or
+different entities), we don't know on which entity to call the
+aforementioned method. In this case, we keep the generic behaviour.
+
+Hence we have two cases here, one for a single-entity rsets, the other for
+multi-entities rsets.
+
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
+
+.. sourcecode:: python
+
+  class RSSIconBox(box.Box):
+    ''' just display the RSS icon on uniform result set '''
+    __select__ = box.Box.__select__ & non_final_entity()
+
+It takes into account:
+
+* the inherited selection criteria (one has to look them up in the class
+  hierarchy to know the details)
+
+* :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets
+  containing non final entities (a 'final entity' being synonym for entity
+  attributes type, eg `String`, `Int`, etc)
+
+This matches our second case. Hence we have to provide a specific component for
+the first case:
+
+.. sourcecode:: python
+
+  class EntityRSSIconBox(RSSIconBox):
+    '''just display the RSS icon on uniform result set for a single entity'''
+    __select__ = RSSIconBox.__select__ & one_line_rset()
+
+Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which
+filters result sets of size 1. Thus, on a result set containing multiple
+entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
+selectable. However for a result set with one entity, the `EntityRSSIconBox`
+class will have a higher score than `RSSIconBox`, which is what we wanted.
+
+Of course, once this is done, you have to:
+
+* fill in the call method of `EntityRSSIconBox`
+
+* provide the default implementation of the method returning the RSS stream url
+  on :class:`~cubicweb.entities.AnyEntity`
+
+* redefine this method on `Blog`.
+
+
+When to use selectors?
+~~~~~~~~~~~~~~~~~~~~~~
+
+Selectors are to be used whenever arises the need of dispatching on the shape or
+content of a result set or whatever else context (value in request form params,
+authenticated user groups, etc...). That is, almost all the time.
+
+Here is a quick example:
+
+.. sourcecode:: python
+
+    class UserLink(component.Component):
+	'''if the user is the anonymous user, build a link to login else a link
+	to the connected user object with a logout link
+	'''
+	__regid__ = 'loggeduserlink'
+
+	def call(self):
+	    if self._cw.session.anonymous_session:
+		# display login link
+		...
+	    else:
+		# display a link to the connected user object with a loggout link
+		...
+
+The proper way to implement this with |cubicweb| is two have two different
+classes sharing the same identifier but with different selectors so you'll get
+the correct one according to the context.
+
+.. sourcecode:: python
+
+    class UserLink(component.Component):
+	'''display a link to the connected user object with a loggout link'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & authenticated_user()
+
+	def call(self):
+            # display useractions and siteactions
+	    ...
+
+    class AnonUserLink(component.Component):
+	'''build a link to login'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & anonymous_user()
+
+	def call(self):
+	    # display login link
+            ...
+
+The big advantage, aside readability once you're familiar with the
+system, is that your cube becomes much more easily customizable by
+improving componentization.
+
+
+.. _CustomPredicates:
+
+Defining your own predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autodocstring:: cubicweb.appobject::objectify_predicate
+
+In other cases, you can take a look at the following abstract base classes:
+
+.. autoclass:: cubicweb.predicates.ExpectedValuePredicate
+.. autoclass:: cubicweb.predicates.EClassPredicate
+.. autoclass:: cubicweb.predicates.EntityPredicate
+
+.. _DebuggingSelectors:
+
+Debugging selection
+~~~~~~~~~~~~~~~~~~~
+
+Once in a while, one needs to understand why a view (or any application object)
+is, or is not selected appropriately. Looking at which predicates fired (or did
+not) is the way. The :class:`logilab.common.registry.traced_selection` context
+manager to help with that, *if you're running your instance in debug mode*.
+
+.. autoclass:: logilab.common.registry.traced_selection
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+import logging
+from warnings import warn
+from operator import eq
+
+from logilab.common.compat import all, any
+from logilab.common.interface import implements as implements_iface
+from logilab.common.registry import Predicate, objectify_predicate
+
+from yams.schema import BASE_TYPES, role_name
+from rql.nodes import Function
+
+from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
+                      CW_EVENT_MANAGER, role)
+# even if not used, let yes here so it's importable through this module
+from cubicweb.uilib import eid_param
+from cubicweb.schema import split_expression
+
+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 ###########################################
+
+class PartialPredicateMixIn(object):
+    """convenience mix-in for predicates that will look into the containing
+    class to find missing information.
+
+    cf. `cubicweb.web.action.LinkToEntityAction` for instance
+    """
+    def __call__(self, cls, *args, **kwargs):
+        self.complete(cls)
+        return super(PartialPredicateMixIn, self).__call__(cls, *args, **kwargs)
+
+
+class EClassPredicate(Predicate):
+    """abstract class for predicates working on *entity class(es)* specified
+    explicitly or found of the result set.
+
+    Here are entity lookup / scoring rules:
+
+    * if `entity` is specified, return score for this entity's class
+
+    * elif `rset`, `select` and `filtered_variable` are specified, return score
+      for the possible classes for variable in the given rql :class:`Select`
+      node
+
+    * elif `rset` and `row` are specified, return score for the class of the
+      entity found in the specified cell, using column specified by `col` or 0
+
+    * elif `rset` is specified return score for each entity class found in the
+      column specified specified by the `col` argument or in column 0 if not
+      specified
+
+    When there are several classes to be evaluated, return the sum of scores for
+    each entity class unless:
+
+      - `mode` == 'all' (the default) and some entity class is scored
+        to 0, in which case 0 is returned
+
+      - `mode` == 'any', in which case the first non-zero score is
+        returned
+
+      - `accept_none` is False and some cell in the column has a None value
+        (this may occurs with outer join)
+    """
+    def __init__(self, once_is_enough=None, accept_none=True, mode='all'):
+        if once_is_enough is not None:
+            warn("[3.14] once_is_enough is deprecated, use mode='any'",
+                 DeprecationWarning, stacklevel=2)
+            if once_is_enough:
+                mode = 'any'
+        assert mode in ('any', 'all'), 'bad mode %s' % mode
+        self.once_is_enough = mode == 'any'
+        self.accept_none = accept_none
+
+    def __call__(self, cls, req, rset=None, row=None, col=0, entity=None,
+                 select=None, filtered_variable=None,
+                 accept_none=None,
+                 **kwargs):
+        if entity is not None:
+            return self.score_class(entity.__class__, req)
+        if not rset:
+            return 0
+        if select is not None and filtered_variable is not None:
+            etypes = set(sol[filtered_variable.name] for sol in select.solutions)
+        elif row is None:
+            if accept_none is None:
+                accept_none = self.accept_none
+            if not accept_none and \
+                   any(rset[i][col] is None for i in xrange(len(rset))):
+                return 0
+            etypes = rset.column_types(col)
+        else:
+            etype = rset.description[row][col]
+            # may have None in rset.description on outer join
+            if etype is None or rset.rows[row][col] is None:
+                return 0
+            etypes = (etype,)
+        score = 0
+        for etype in etypes:
+            escore = self.score(cls, req, etype)
+            if not escore and not self.once_is_enough:
+                return 0
+            elif self.once_is_enough:
+                return escore
+            score += escore
+        return score
+
+    def score(self, cls, req, etype):
+        if etype in BASE_TYPES:
+            return 0
+        return self.score_class(req.vreg['etypes'].etype_class(etype), req)
+
+    def score_class(self, eclass, req):
+        raise NotImplementedError()
+
+
+class EntityPredicate(EClassPredicate):
+    """abstract class for predicates working on *entity instance(s)* specified
+    explicitly or found of the result set.
+
+    Here are entity lookup / scoring rules:
+
+    * if `entity` is specified, return score for this entity
+
+    * elif `row` is specified, return score for the entity found in the
+      specified cell, using column specified by `col` or 0
+
+    * else return the sum of scores for each entity found in the column
+      specified specified by the `col` argument or in column 0 if not specified,
+      unless:
+
+      - `mode` == 'all' (the default) and some entity class is scored
+        to 0, in which case 0 is returned
+
+      - `mode` == 'any', in which case the first non-zero score is
+        returned
+
+      - `accept_none` is False and some cell in the column has a None value
+        (this may occurs with outer join)
+
+    .. Note::
+       using :class:`EntityPredicate` or :class:`EClassPredicate` as base predicate
+       class impacts performance, since when no entity or row is specified the
+       later works on every different *entity class* found in the result set,
+       while the former works on each *entity* (eg each row of the result set),
+       which may be much more costly.
+    """
+
+    def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+                 **kwargs):
+        if not rset and not kwargs.get('entity'):
+            return 0
+        score = 0
+        if kwargs.get('entity'):
+            score = self.score_entity(kwargs['entity'])
+        elif row is None:
+            col = col or 0
+            if accept_none is None:
+                accept_none = self.accept_none
+            for row, rowvalue in enumerate(rset.rows):
+                if rowvalue[col] is None: # outer join
+                    if not accept_none:
+                        return 0
+                    continue
+                escore = self.score(req, rset, row, col)
+                if not escore and not self.once_is_enough:
+                    return 0
+                elif self.once_is_enough:
+                    return escore
+                score += escore
+        else:
+            col = col or 0
+            etype = rset.description[row][col]
+            if etype is not None: # outer join
+                score = self.score(req, rset, row, col)
+        return score
+
+    def score(self, req, rset, row, col):
+        try:
+            return self.score_entity(rset.get_entity(row, col))
+        except NotAnEntity:
+            return 0
+
+    def score_entity(self, entity):
+        raise NotImplementedError()
+
+
+class ExpectedValuePredicate(Predicate):
+    """Take a list of expected values as initializer argument and store them
+    into the :attr:`expected` set attribute. You may also give a set as single
+    argument, which will then be referenced as set of expected values,
+    allowing modifications to the given set to be considered.
+
+    You should implement one of :meth:`_values_set(cls, req, **kwargs)` or
+    :meth:`_get_value(cls, req, **kwargs)` method which should respectively
+    return the set of values or the unique possible value for the given context.
+
+    You may also specify a `mode` behaviour as argument, as explained below.
+
+    Returned score is:
+
+    - 0 if `mode` == 'all' (the default) and at least one expected
+      values isn't found
+
+    - 0 if `mode` == 'any' and no expected values isn't found at all
+
+    - else the number of matching values
+
+    Notice `mode` = 'any' with a single expected value has no effect at all.
+    """
+    def __init__(self, *expected, **kwargs):
+        assert expected, self
+        if len(expected) == 1 and isinstance(expected[0], set):
+            self.expected = expected[0]
+        else:
+            self.expected = frozenset(expected)
+        mode = kwargs.pop('mode', 'all')
+        assert mode in ('any', 'all'), 'bad mode %s' % mode
+        self.once_is_enough = mode == 'any'
+        assert not kwargs, 'unexpected arguments %s' % kwargs
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                           ','.join(sorted(str(s) for s in self.expected)))
+
+    def __call__(self, cls, req, **kwargs):
+        values = self._values_set(cls, req, **kwargs)
+        matching = len(values & self.expected)
+        if self.once_is_enough:
+            return matching
+        if matching == len(self.expected):
+            return matching
+        return 0
+
+    def _values_set(self, cls, req, **kwargs):
+        return frozenset( (self._get_value(cls, req, **kwargs),) )
+
+    def _get_value(self, cls, req, **kwargs):
+        raise NotImplementedError()
+
+
+# bare predicates ##############################################################
+
+class match_kwargs(ExpectedValuePredicate):
+    """Return non-zero score if parameter names specified as initializer
+    arguments are specified in the input context.
+
+
+    Return a score corresponding to the number of expected parameters.
+
+    When multiple parameters are expected, all of them should be found in
+    the input context unless `mode` keyword argument is given to 'any',
+    in which case a single matching parameter is enough.
+    """
+
+    def _values_set(self, cls, req, **kwargs):
+        return frozenset(kwargs)
+
+
+class appobject_selectable(Predicate):
+    """Return 1 if another appobject is selectable using the same input context.
+
+    Initializer arguments:
+
+    * `registry`, a registry name
+
+    * `regids`, object identifiers in this registry, one of them should be
+      selectable.
+    """
+    selectable_score = 1
+    def __init__(self, registry, *regids):
+        self.registry = registry
+        self.regids = regids
+
+    def __call__(self, cls, req, **kwargs):
+        for regid in self.regids:
+            try:
+                req.vreg[self.registry].select(regid, req, **kwargs)
+                return self.selectable_score
+            except NoSelectableObject:
+                continue
+        return 0
+
+
+class adaptable(appobject_selectable):
+    """Return 1 if another appobject is selectable using the same input context.
+
+    Initializer arguments:
+
+    * `regids`, adapter identifiers (e.g. interface names) to which the context
+      (usually entities) should be adaptable. One of them should be selectable
+      when multiple identifiers are given.
+    """
+    def __init__(self, *regids):
+        super(adaptable, self).__init__('adapters', *regids)
+
+    def __call__(self, cls, req, **kwargs):
+        kwargs.setdefault('accept_none', False)
+        # being adaptable to an interface should takes precedence other is_instance('Any'),
+        # but not other explicit is_instance('SomeEntityType'), and:
+        # * is_instance('Any') score is 1
+        # * is_instance('SomeEntityType') score is at least 2
+        score = super(adaptable, self).__call__(cls, req, **kwargs)
+        if score >= 2:
+            return score - 0.5
+        if score == 1:
+            return score + 0.5
+        return score
+
+
+class configuration_values(Predicate):
+    """Return 1 if the instance has an option set to a given value(s) in its
+    configuration file.
+    """
+    # XXX this predicate could be evaluated on startup
+    def __init__(self, key, values):
+        self._key = key
+        if not isinstance(values, (tuple, list)):
+            values = (values,)
+        self._values = frozenset(values)
+
+    def __call__(self, cls, req, **kwargs):
+        try:
+            return self._score
+        except AttributeError:
+            if req is None:
+                config = kwargs['repo'].config
+            else:
+                config = req.vreg.config
+            self._score = config[self._key] in self._values
+        return self._score
+
+
+# rset predicates ##############################################################
+
+@objectify_predicate
+def none_rset(cls, req, rset=None, **kwargs):
+    """Return 1 if the result set is None (eg usually not specified)."""
+    if rset is None:
+        return 1
+    return 0
+
+
+# XXX == ~ none_rset
+@objectify_predicate
+def any_rset(cls, req, rset=None, **kwargs):
+    """Return 1 for any result set, whatever the number of rows in it, even 0."""
+    if rset is not None:
+        return 1
+    return 0
+
+
+@objectify_predicate
+def nonempty_rset(cls, req, rset=None, **kwargs):
+    """Return 1 for result set containing one ore more rows."""
+    if rset is not None and rset.rowcount:
+        return 1
+    return 0
+
+
+# XXX == ~ nonempty_rset
+@objectify_predicate
+def empty_rset(cls, req, rset=None, **kwargs):
+    """Return 1 for result set which doesn't contain any row."""
+    if rset is not None and rset.rowcount == 0:
+        return 1
+    return 0
+
+
+# XXX == multi_lines_rset(1)
+@objectify_predicate
+def one_line_rset(cls, req, rset=None, row=None, **kwargs):
+    """Return 1 if the result set is of size 1, or greater but a specific row in
+      the result set is specified ('row' argument).
+    """
+    if rset is None and 'entity' in kwargs:
+        return 1
+    if rset is not None and (row is not None or rset.rowcount == 1):
+        return 1
+    return 0
+
+
+class multi_lines_rset(Predicate):
+    """Return 1 if the operator expression matches between `num` elements
+    in the result set and the `expected` value if defined.
+
+    By default, multi_lines_rset(expected) matches equality expression:
+        `nb` row(s) in result set equals to expected value
+    But, you can perform richer comparisons by overriding default operator:
+        multi_lines_rset(expected, operator.gt)
+
+    If `expected` is None, return 1 if the result set contains *at least*
+    two rows.
+    If rset is None, return 0.
+    """
+    def __init__(self, expected=None, operator=eq):
+        self.expected = expected
+        self.operator = operator
+
+    def match_expected(self, num):
+        if self.expected is None:
+            return num > 1
+        return self.operator(num, self.expected)
+
+    def __call__(self, cls, req, rset=None, **kwargs):
+        return int(rset is not None and self.match_expected(rset.rowcount))
+
+
+class multi_columns_rset(multi_lines_rset):
+    """If `nb` is specified, return 1 if the result set has exactly `nb` column
+    per row. Else (`nb` is None), return 1 if the result set contains *at least*
+    two columns per row. Return 0 for empty result set.
+    """
+
+    def __call__(self, cls, req, rset=None, **kwargs):
+        # 'or 0' since we *must not* return None
+        return rset and self.match_expected(len(rset.rows[0])) or 0
+
+
+class paginated_rset(Predicate):
+    """Return 1 or more for result set with more rows than one or more page
+    size.  You can specify expected number of pages to the initializer (default
+    to one), and you'll get that number of pages as score if the result set is
+    big enough.
+
+    Page size is searched in (respecting order):
+    * a `page_size` argument
+    * a `page_size` form parameters
+    * the `navigation.page-size` property (see :ref:`PersistentProperties`)
+    """
+    def __init__(self, nbpages=1):
+        assert nbpages > 0
+        self.nbpages = nbpages
+
+    def __call__(self, cls, req, rset=None, **kwargs):
+        if rset is None:
+            return 0
+        page_size = kwargs.get('page_size')
+        if page_size is None:
+            page_size = req.form.get('page_size')
+            if page_size is None:
+                page_size = req.property_value('navigation.page-size')
+            else:
+                page_size = int(page_size)
+        if rset.rowcount <= (page_size*self.nbpages):
+            return 0
+        return self.nbpages
+
+
+@objectify_predicate
+def sorted_rset(cls, req, rset=None, **kwargs):
+    """Return 1 for sorted result set (e.g. from an RQL query containing an
+    ORDERBY clause), with exception that it will return 0 if the rset is
+    'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
+    """
+    if rset is None:
+        return 0
+    selects = rset.syntax_tree().children
+    if (len(selects) > 1 or
+        not selects[0].orderby or
+        (isinstance(selects[0].orderby[0].term, Function) and
+         selects[0].orderby[0].term.name == 'FTIRANK')
+        ):
+        return 0
+    return 2
+
+
+# XXX == multi_etypes_rset(1)
+@objectify_predicate
+def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
+    """Return 1 if the result set contains entities which are all of the same
+    type in the column specified by the `col` argument of the input context, or
+    in column 0.
+    """
+    if rset is None:
+        return 0
+    if len(rset.column_types(col)) != 1:
+        return 0
+    return 1
+
+
+class multi_etypes_rset(multi_lines_rset):
+    """If `nb` is specified, return 1 if the result set contains `nb` different
+    types of entities in the column specified by the `col` argument of the input
+    context, or in column 0. If `nb` is None, return 1 if the result set contains
+    *at least* two different types of entities.
+    """
+
+    def __call__(self, cls, req, rset=None, col=0, **kwargs):
+        # 'or 0' since we *must not* return None
+        return rset and self.match_expected(len(rset.column_types(col))) or 0
+
+
+@objectify_predicate
+def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
+    """Return positive score if the result set at the specified row / col
+    contains the eid of the logged user.
+    """
+    if rset is None:
+        return 0
+    return req.user.eid == rset[row or 0][col]
+
+
+# entity predicates #############################################################
+
+class non_final_entity(EClassPredicate):
+    """Return 1 for entity of a non final entity type(s). Remember, "final"
+    entity types are String, Int, etc... This is equivalent to
+    `is_instance('Any')` but more optimized.
+
+    See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+    class lookup / score rules according to the input context.
+    """
+    def score(self, cls, req, etype):
+        if etype in BASE_TYPES:
+            return 0
+        return 1
+
+    def score_class(self, eclass, req):
+        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 = {}
+
+CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
+
+class is_instance(EClassPredicate):
+    """Return non-zero score for entity that is an instance of the one of given
+    type(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 registry at selection time.
+
+    See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+    class lookup / score rules according to the input context.
+
+    .. note:: the score will reflect class proximity so the most specific object
+              will be selected.
+    """
+
+    def __init__(self, *expected_etypes, **kwargs):
+        super(is_instance, self).__init__(**kwargs)
+        self.expected_etypes = expected_etypes
+        for etype in self.expected_etypes:
+            assert isinstance(etype, basestring), etype
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                           ','.join(str(s) for s in self.expected_etypes))
+
+    def score_class(self, eclass, req):
+        # cache on vreg to avoid reloading issues
+        try:
+            cache = req.vreg._is_instance_predicate_cache
+        except AttributeError:
+            # XXX 'before-registry-reset' not called for db-api connections
+            cache = req.vreg._is_instance_predicate_cache = {}
+        try:
+            expected_eclasses = cache[self]
+        except KeyError:
+            # turn list of entity types as string into a list of
+            #  (entity class, parent classes)
+            etypesreg = req.vreg['etypes']
+            expected_eclasses = cache[self] = []
+            for etype in self.expected_etypes:
+                try:
+                    expected_eclasses.append(etypesreg.etype_class(etype))
+                except KeyError:
+                    continue # entity type not in the schema
+        parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
+        score = 0
+        for expectedcls in expected_eclasses:
+            # adjust score according to class proximity
+            if expectedcls is eclass:
+                score += len(parents) + 4
+            elif expectedcls is any: # Any
+                score += 1
+            else:
+                for index, basecls in enumerate(reversed(parents)):
+                    if expectedcls is basecls:
+                        score += index + 3
+                        break
+        return score
+
+
+class score_entity(EntityPredicate):
+    """Return score according to an arbitrary function given as argument which
+    will be called with input content entity as argument.
+
+    This is a very useful predicate that will usually interest you since it
+    allows a lot of things without having to write a specific predicate.
+
+    The function can return arbitrary value which will be casted to an integer
+    value at the end.
+
+    See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+    lookup / score rules according to the input context.
+    """
+    def __init__(self, scorefunc, once_is_enough=None, mode='all'):
+        super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
+        def intscore(*args, **kwargs):
+            score = scorefunc(*args, **kwargs)
+            if not score:
+                return 0
+            if isinstance(score, (int, long)):
+                return score
+            return 1
+        self.score_entity = intscore
+
+
+class has_mimetype(EntityPredicate):
+    """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
+
+    You can give 'image/' to match any image for instance, or 'image/png' to match
+    only PNG images.
+    """
+    def __init__(self, mimetype, once_is_enough=None, mode='all'):
+        super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
+        self.mimetype = mimetype
+
+    def score_entity(self, entity):
+        idownloadable = entity.cw_adapt_to('IDownloadable')
+        if idownloadable is None:
+            return 0
+        mt = idownloadable.download_content_type()
+        if not (mt and mt.startswith(self.mimetype)):
+            return 0
+        return 1
+
+
+class relation_possible(EntityPredicate):
+    """Return 1 for entity that supports the relation, provided that the
+    request's user may do some `action` on it (see below).
+
+    The relation is specified by the following initializer arguments:
+
+    * `rtype`, the name of the relation
+
+    * `role`, the role of the entity in the relation, either 'subject' or
+      'object', default to 'subject'
+
+    * `target_etype`, optional name of an entity type that should be supported
+      at the other end of the relation
+
+    * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
+      default to 'read') which must be granted to the user, else a 0 score will
+      be returned. Give None if you don't want any permission checking.
+
+    * `strict`, boolean (default to False) telling what to do when the user has
+      not globally the permission for the action (eg the action is not granted
+      to one of the user's groups)
+
+      - when strict is False, if there are some local role defined for this
+        action (e.g. using rql expressions), then the permission will be
+        considered as granted
+
+      - when strict is True, then the permission will be actually checked for
+        each entity
+
+    Setting `strict` to True impacts performance for large result set since
+    you'll then get the :class:`~cubicweb.predicates.EntityPredicate` behaviour
+    while otherwise you get the :class:`~cubicweb.predicates.EClassPredicate`'s
+    one. See those classes documentation for entity lookup / score rules
+    according to the input context.
+    """
+
+    def __init__(self, rtype, role='subject', target_etype=None,
+                 action='read', strict=False, **kwargs):
+        super(relation_possible, self).__init__(**kwargs)
+        self.rtype = rtype
+        self.role = role
+        self.target_etype = target_etype
+        self.action = action
+        self.strict = strict
+
+    # hack hack hack
+    def __call__(self, cls, req, **kwargs):
+        # hack hack hack
+        if self.strict:
+            return EntityPredicate.__call__(self, cls, req, **kwargs)
+        return EClassPredicate.__call__(self, cls, req, **kwargs)
+
+    def score(self, *args):
+        if self.strict:
+            return EntityPredicate.score(self, *args)
+        return EClassPredicate.score(self, *args)
+
+    def _get_rschema(self, eclass):
+        eschema = eclass.e_schema
+        try:
+            if self.role == 'object':
+                return eschema.objrels[self.rtype]
+            else:
+                return eschema.subjrels[self.rtype]
+        except KeyError:
+            return None
+
+    def score_class(self, eclass, req):
+        rschema = self._get_rschema(eclass)
+        if rschema is None:
+            return 0 # relation not supported
+        eschema = eclass.e_schema
+        if self.target_etype is not None:
+            try:
+                rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
+            except KeyError:
+                return 0
+            if self.action and not rdef.may_have_permission(self.action, req):
+                return 0
+            teschema = req.vreg.schema.eschema(self.target_etype)
+            if not teschema.may_have_permission('read', req):
+                return 0
+        elif self.action:
+            return rschema.may_have_permission(self.action, req, eschema, self.role)
+        return 1
+
+    def score_entity(self, entity):
+        rschema = self._get_rschema(entity)
+        if rschema is None:
+            return 0 # relation not supported
+        if self.action:
+            if self.target_etype is not None:
+                rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
+            if self.role == 'subject':
+                if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid):
+                    return 0
+            elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid):
+                return 0
+        if self.target_etype is not None:
+            req = entity._cw
+            teschema = req.vreg.schema.eschema(self.target_etype)
+            if not teschema.may_have_permission('read', req):
+                return 0
+        return 1
+
+
+class partial_relation_possible(PartialPredicateMixIn, relation_possible):
+    """Same as :class:~`cubicweb.predicates.relation_possible`, but will look for
+    attributes of the selected class to get information which is otherwise
+    expected by the initializer, except for `action` and `strict` which are kept
+    as initializer arguments.
+
+    This is useful to predefine predicate of an abstract class designed to be
+    customized.
+    """
+    def __init__(self, action='read', **kwargs):
+        super(partial_relation_possible, self).__init__(None, None, None,
+                                                        action, **kwargs)
+
+    def complete(self, cls):
+        self.rtype = cls.rtype
+        self.role = role(cls)
+        self.target_etype = getattr(cls, 'target_etype', None)
+
+
+class has_related_entities(EntityPredicate):
+    """Return 1 if entity support the specified relation and has some linked
+    entities by this relation , optionaly filtered according to the specified
+    target type.
+
+    The relation is specified by the following initializer arguments:
+
+    * `rtype`, the name of the relation
+
+    * `role`, the role of the entity in the relation, either 'subject' or
+      'object', default to 'subject'.
+
+    * `target_etype`, optional name of an entity type that should be found
+      at the other end of the relation
+
+    See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+    lookup / score rules according to the input context.
+    """
+    def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
+        super(has_related_entities, self).__init__(**kwargs)
+        self.rtype = rtype
+        self.role = role
+        self.target_etype = target_etype
+
+    def score_entity(self, entity):
+        relpossel = relation_possible(self.rtype, self.role, self.target_etype)
+        if not relpossel.score_class(entity.__class__, entity._cw):
+            return 0
+        rset = entity.related(self.rtype, self.role)
+        if self.target_etype:
+            return any(r for r in rset.description if r[0] == self.target_etype)
+        return rset and 1 or 0
+
+
+class partial_has_related_entities(PartialPredicateMixIn, has_related_entities):
+    """Same as :class:~`cubicweb.predicates.has_related_entity`, but will look
+    for attributes of the selected class to get information which is otherwise
+    expected by the initializer.
+
+    This is useful to predefine predicate of an abstract class designed to be
+    customized.
+    """
+    def __init__(self, **kwargs):
+        super(partial_has_related_entities, self).__init__(None, None, None,
+                                                           **kwargs)
+
+    def complete(self, cls):
+        self.rtype = cls.rtype
+        self.role = role(cls)
+        self.target_etype = getattr(cls, 'target_etype', None)
+
+
+class has_permission(EntityPredicate):
+    """Return non-zero score if request's user has the permission to do the
+    requested action on the entity. `action` is an entity schema action (eg one
+    of 'read', 'add', 'delete', 'update').
+
+    Here are entity lookup / scoring rules:
+
+    * if `entity` is specified, check permission is granted for this entity
+
+    * elif `row` is specified, check permission is granted for the entity found
+      in the specified cell
+
+    * else check permission is granted for each entity found in the column
+      specified specified by the `col` argument or in column 0
+    """
+    def __init__(self, action):
+        self.action = action
+
+    # don't use EntityPredicate.__call__ but this optimized implementation to
+    # avoid considering each entity when it's not necessary
+    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+        if kwargs.get('entity'):
+            return self.score_entity(kwargs['entity'])
+        if rset is None:
+            return 0
+        if row is None:
+            score = 0
+            need_local_check = []
+            geteschema = req.vreg.schema.eschema
+            user = req.user
+            action = self.action
+            for etype in rset.column_types(0):
+                if etype in BASE_TYPES:
+                    return 0
+                eschema = geteschema(etype)
+                if not user.matching_groups(eschema.get_groups(action)):
+                    if eschema.has_local_role(action):
+                        # have to ckeck local roles
+                        need_local_check.append(eschema)
+                        continue
+                    else:
+                        # even a local role won't be enough
+                        return 0
+                score += 1
+            if need_local_check:
+                # check local role for entities of necessary types
+                for i, row in enumerate(rset):
+                    if not rset.description[i][col] in need_local_check:
+                        continue
+                    # micro-optimisation instead of calling self.score(req,
+                    # rset, i, col): rset may be large
+                    if not rset.get_entity(i, col).cw_has_perm(action):
+                        return 0
+                score += 1
+            return score
+        return self.score(req, rset, row, col)
+
+    def score_entity(self, entity):
+        if entity.cw_has_perm(self.action):
+            return 1
+        return 0
+
+
+class has_add_permission(EClassPredicate):
+    """Return 1 if request's user has the add permission on entity type
+    specified in the `etype` initializer argument, or according to entity found
+    in the input content if not specified.
+
+    It also check that then entity type is not a strict subobject (e.g. may only
+    be used as a composed of another entity).
+
+    See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+    class lookup / score rules according to the input context when `etype` is
+    not specified.
+    """
+    def __init__(self, etype=None, **kwargs):
+        super(has_add_permission, self).__init__(**kwargs)
+        self.etype = etype
+
+    def __call__(self, cls, req, **kwargs):
+        if self.etype is None:
+            return super(has_add_permission, self).__call__(cls, req, **kwargs)
+        return self.score(cls, req, self.etype)
+
+    def score_class(self, eclass, req):
+        eschema = eclass.e_schema
+        if eschema.final or eschema.is_subobject(strict=True) \
+               or not eschema.has_perm(req, 'add'):
+            return 0
+        return 1
+
+
+class rql_condition(EntityPredicate):
+    """Return non-zero score if arbitrary rql specified in `expression`
+    initializer argument return some results for entity found in the input
+    context. Returned score is the number of items returned by the rql
+    condition.
+
+    `expression` is expected to be a string containing an rql expression, which
+    must use 'X' variable to represent the context entity and may use 'U' to
+    represent the request's user.
+
+    .. warning::
+        If simply testing value of some attribute/relation of context entity (X),
+        you should rather use the :class:`score_entity` predicate which will
+        benefit from the ORM's request entities cache.
+
+    See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+    lookup / score rules according to the input context.
+    """
+    def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
+        super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
+        self.user_condition = user_condition
+        if user_condition:
+            rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
+        elif 'U' in frozenset(split_expression(expression)):
+            rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
+        else:
+            rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
+        self.rql = rql
+
+    def __str__(self):
+        return '%s(%r)' % (self.__class__.__name__, self.rql)
+
+    def __call__(self, cls, req, **kwargs):
+        if self.user_condition:
+            try:
+                return req.execute(self.rql, {'u': req.user.eid})[0][0]
+            except Unauthorized:
+                return 0
+        else:
+            return super(rql_condition, self).__call__(cls, req, **kwargs)
+
+    def _score(self, req, eid):
+        try:
+            return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0]
+        except Unauthorized:
+            return 0
+
+    def score(self, req, rset, row, col):
+        return self._score(req, rset[row][col])
+
+    def score_entity(self, entity):
+        return self._score(entity._cw, entity.eid)
+
+
+# workflow predicates ###########################################################
+
+class is_in_state(score_entity):
+    """Return 1 if entity is in one of the states given as argument list
+
+    You should use this instead of your own :class:`score_entity` predicate to
+    avoid some gotchas:
+
+    * possible views gives a fake entity with no state
+    * you must use the latest tr info thru the workflow adapter for repository
+      side checking of the current state
+
+    In debug mode, this predicate can raise :exc:`ValueError` for unknown states names
+    (only checked on entities without a custom workflow)
+
+    :rtype: int
+    """
+    def __init__(self, *expected):
+        assert expected, self
+        self.expected = frozenset(expected)
+        def score(entity, expected=self.expected):
+            adapted = entity.cw_adapt_to('IWorkflowable')
+            # in debug mode only (time consuming)
+            if entity._cw.vreg.config.debugmode:
+                # validation can only be done for generic etype workflow because
+                # expected transition list could have been changed for a custom
+                # workflow (for the current entity)
+                if not entity.custom_workflow:
+                    self._validate(adapted)
+            return self._score(adapted)
+        super(is_in_state, self).__init__(score)
+
+    def _score(self, adapted):
+        trinfo = adapted.latest_trinfo()
+        if trinfo is None: # entity is probably in it's initial state
+            statename = adapted.state
+        else:
+            statename = trinfo.new_state.name
+        return statename in self.expected
+
+    def _validate(self, adapted):
+        wf = adapted.current_workflow
+        valid = [n.name for n in wf.reverse_state_of]
+        unknown = sorted(self.expected.difference(valid))
+        if unknown:
+            raise ValueError("%s: unknown state(s): %s"
+                             % (wf.name, ",".join(unknown)))
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                           ','.join(str(s) for s in self.expected))
+
+
+def on_fire_transition(etype, tr_name, from_state_name=None):
+    """Return 1 when entity of the type `etype` is going through transition of
+    the name `tr_name`.
+
+    If `from_state_name` is specified, this predicate will also check the
+    incoming state.
+
+    You should use this predicate on 'after_add_entity' hook, since it's actually
+    looking for addition of `TrInfo` entities. Hence in the hook, `self.entity`
+    will reference the matching `TrInfo` entity, allowing to get all the
+    transition details (including the entity to which is applied the transition
+    but also its original state, transition, destination state, user...).
+
+    See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
+    """
+    def match_etype_and_transition(trinfo):
+        # take care trinfo.transition is None when calling change_state
+        return (trinfo.transition and trinfo.transition.name == tr_name
+                # is_instance() first two arguments are 'cls' (unused, so giving
+                # None is fine) and the request/session
+                and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity))
+
+    return is_instance('TrInfo') & score_entity(match_etype_and_transition)
+
+
+class match_transition(ExpectedValuePredicate):
+    """Return 1 if `transition` argument is found in the input context which has
+    a `.name` attribute matching one of the expected names given to the
+    initializer.
+
+    This predicate is expected to be used to customise the status change form in
+    the web ui.
+    """
+    def __call__(self, cls, req, transition=None, **kwargs):
+        # XXX check this is a transition that apply to the object?
+        if transition is None:
+            treid = req.form.get('treid', None)
+            if treid:
+                transition = req.entity_from_eid(treid)
+        if transition is not None and getattr(transition, 'name', None) in self.expected:
+            return 1
+        return 0
+
+
+# logged user predicates ########################################################
+
+@objectify_predicate
+def no_cnx(cls, req, **kwargs):
+    """Return 1 if the web session has no connection set. This occurs when
+    anonymous access is not allowed and user isn't authenticated.
+
+    May only be used on the web side, not on the data repository side.
+    """
+    if not req.cnx:
+        return 1
+    return 0
+
+@objectify_predicate
+def authenticated_user(cls, req, **kwargs):
+    """Return 1 if the user is authenticated (e.g. not the anonymous user).
+
+    May only be used on the web side, not on the data repository side.
+    """
+    if req.session.anonymous_session:
+        return 0
+    return 1
+
+
+# XXX == ~ authenticated_user()
+def anonymous_user():
+    """Return 1 if the user is not authenticated (e.g. is the anonymous user).
+
+    May only be used on the web side, not on the data repository side.
+    """
+    return ~ authenticated_user()
+
+class match_user_groups(ExpectedValuePredicate):
+    """Return a non-zero score if request's user is in at least one of the
+    groups given as initializer argument. Returned score is the number of groups
+    in which the user is.
+
+    If the special 'owners' group is given and `rset` is specified in the input
+    context:
+
+    * if `row` is specified check the entity at the given `row`/`col` (default
+      to 0) is owned by the user
+
+    * else check all entities in `col` (default to 0) are owned by the user
+    """
+
+    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+        if not getattr(req, 'cnx', True): # default to True for repo session instances
+            return 0
+        user = req.user
+        if user is None:
+            return int('guests' in self.expected)
+        score = user.matching_groups(self.expected)
+        if not score and 'owners' in self.expected and rset:
+            if row is not None:
+                if not user.owns(rset[row][col]):
+                    return 0
+                score = 1
+            else:
+                score = all(user.owns(r[col]) for r in rset)
+        return score
+
+# Web request predicates ########################################################
+
+# XXX deprecate
+@objectify_predicate
+def primary_view(cls, req, view=None, **kwargs):
+    """Return 1 if:
+
+    * *no view is specified* in the input context
+
+    * a view is specified and its `.is_primary()` method return True
+
+    This predicate is usually used by contextual components that only want to
+    appears for the primary view of an entity.
+    """
+    if view is not None and not view.is_primary():
+        return 0
+    return 1
+
+
+@objectify_predicate
+def contextual(cls, req, view=None, **kwargs):
+    """Return 1 if view's contextual property is true"""
+    if view is not None and view.contextual:
+        return 1
+    return 0
+
+
+class match_view(ExpectedValuePredicate):
+    """Return 1 if a view is specified an as its registry id is in one of the
+    expected view id given to the initializer.
+    """
+    def __call__(self, cls, req, view=None, **kwargs):
+        if view is None or not view.__regid__ in self.expected:
+            return 0
+        return 1
+
+
+class match_context(ExpectedValuePredicate):
+
+    def __call__(self, cls, req, context=None, **kwargs):
+        if not context in self.expected:
+            return 0
+        return 1
+
+
+# XXX deprecate
+@objectify_predicate
+def match_context_prop(cls, req, context=None, **kwargs):
+    """Return 1 if:
+
+    * no `context` is specified in input context (take care to confusion, here
+      `context` refers to a string given as an argument to the input context...)
+
+    * specified `context` is matching the context property value for the
+      appobject using this predicate
+
+    * the appobject's context property value is None
+
+    This predicate is usually used by contextual components that want to appears
+    in a configurable place.
+    """
+    if context is None:
+        return 1
+    propval = req.property_value('%s.%s.context' % (cls.__registry__,
+                                                    cls.__regid__))
+    if propval and context != propval:
+        return 0
+    return 1
+
+
+class match_search_state(ExpectedValuePredicate):
+    """Return 1 if the current request search state is in one of the expected
+    states given to the initializer.
+
+    Known search states are either 'normal' or 'linksearch' (eg searching for an
+    object to create a relation with another).
+
+    This predicate is usually used by action that want to appears or not according
+    to the ui search state.
+    """
+
+    def __call__(self, cls, req, **kwargs):
+        try:
+            if not req.search_state[0] in self.expected:
+                return 0
+        except AttributeError:
+            return 1 # class doesn't care about search state, accept it
+        return 1
+
+
+class match_form_params(ExpectedValuePredicate):
+    """Return non-zero score if parameter names specified as initializer
+    arguments are specified in request's form parameters.
+
+    Return a score corresponding to the number of expected parameters.
+
+    When multiple parameters are expected, all of them should be found in
+    the input context unless `mode` keyword argument is given to 'any',
+    in which case a single matching parameter is enough.
+    """
+
+    def _values_set(self, cls, req, **kwargs):
+        return frozenset(req.form)
+
+
+class match_edited_type(ExpectedValuePredicate):
+    """return non-zero if main edited entity type is the one specified as
+    initializer argument, or is among initializer arguments if `mode` == 'any'.
+    """
+
+    def _values_set(self, cls, req, **kwargs):
+        try:
+            return frozenset((req.form['__type:%s' % req.form['__maineid']],))
+        except KeyError:
+            return frozenset()
+
+
+class match_form_id(ExpectedValuePredicate):
+    """return non-zero if request form identifier is the one specified as
+    initializer argument, or is among initializer arguments if `mode` == 'any'.
+    """
+
+    def _values_set(self, cls, req, **kwargs):
+        try:
+            return frozenset((req.form['__form_id'],))
+        except KeyError:
+            return frozenset()
+
+
+class specified_etype_implements(is_instance):
+    """Return non-zero score if the entity type specified by an 'etype' key
+    searched in (by priority) input context kwargs and request form parameters
+    match a known entity type (case insensitivly), and it's associated entity
+    class is of one of the type(s) given to the initializer. If multiple
+    arguments are given, matching one of them is enough.
+
+    .. note:: as with :class:`~cubicweb.predicates.is_instance`, entity types
+              should be given as string and the score will reflect class
+              proximity so the most specific object will be selected.
+
+    This predicate is usually used by views holding entity creation forms (since
+    we've no result set to work on).
+    """
+
+    def __call__(self, cls, req, **kwargs):
+        try:
+            etype = kwargs['etype']
+        except KeyError:
+            try:
+                etype = req.form['etype']
+            except KeyError:
+                return 0
+            else:
+                # only check this is a known type if etype comes from req.form,
+                # else we want the error to propagate
+                try:
+                    etype = req.vreg.case_insensitive_etypes[etype.lower()]
+                    req.form['etype'] = etype
+                except KeyError:
+                    return 0
+        score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
+        if score:
+            eschema = req.vreg.schema.eschema(etype)
+            if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
+                return score
+        return 0
+
+
+class attribute_edited(EntityPredicate):
+    """Scores if the specified attribute has been edited This is useful for
+    selection of forms by the edit controller.
+
+    The initial use case is on a form, in conjunction with match_transition,
+    which will not score at edit time::
+
+     is_instance('Version') & (match_transition('ready') |
+                               attribute_edited('publication_date'))
+    """
+    def __init__(self, attribute, once_is_enough=None, mode='all'):
+        super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
+        self._attribute = attribute
+
+    def score_entity(self, entity):
+        return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
+
+
+# Other predicates ##############################################################
+
+class match_exception(ExpectedValuePredicate):
+    """Return 1 if exception given as `exc` in the input context is an instance
+    of one of the class given on instanciation of this predicate.
+    """
+    def __init__(self, *expected):
+        assert expected, self
+        # we want a tuple, not a set as done in the parent class
+        self.expected = expected
+
+    def __call__(self, cls, req, exc=None, **kwargs):
+        if exc is not None and isinstance(exc, self.expected):
+            return 1
+        return 0
+
+
+@objectify_predicate
+def debug_mode(cls, req, rset=None, **kwargs):
+    """Return 1 if running in debug mode."""
+    return req.vreg.config.debugmode and 1 or 0
--- a/pylintext.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/pylintext.py	Thu Feb 23 19:06:14 2012 +0100
@@ -15,14 +15,14 @@
 
 
 def cubicweb_transform(module):
-    # handle objectify_selector decorator. Only look at module level functions,
-    # should be enough
+    # handle objectify_predicate decorator (and its former name until bw compat
+    # is kept). Only look at module level functions, should be enough.
     for assnodes in module.locals.values():
         for node in assnodes:
             if isinstance(node, scoped_nodes.Function) and node.decorators:
                 for decorator in node.decorators.nodes:
                     for infered in decorator.infer():
-                        if infered.name == 'objectify_selector':
+                        if infered.name in ('objectify_predicate', 'objectify_selector'):
                             turn_function_to_class(node)
                             break
                     else:
--- a/schema.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/schema.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -1247,13 +1247,9 @@
 
 # XXX deprecated
 
-from yams.buildobjs import RichString
 from yams.constraints import StaticVocabularyConstraint
 
-try: # for yams < 0.35
-    RichString = class_moved(RichString)
-except TypeError:
-    RichString = moved('yams.buildobjs', 'RichString')
+RichString = moved('yams.buildobjs', 'RichString')
 
 StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint)
 FormatConstraint = class_moved(FormatConstraint)
--- a/schemas/base.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/schemas/base.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -51,7 +51,9 @@
 class EmailAddress(EntityType):
     """an electronic mail address associated to a short alias"""
     __permissions__ = {
-        'read':   ('managers', 'users', 'guests',), # XXX if P use_email X, U has_read_permission P
+        # application that wishes public email, or use it for something else
+        # than users (eg Company, Person), should explicitly change permissions
+        'read':   ('managers', ERQLExpression('U use_email X')),
         'add':    ('managers', 'users',),
         'delete': ('managers', 'owners', ERQLExpression('P use_email X, U has_update_permission P')),
         'update': ('managers', 'owners', ERQLExpression('P use_email X, U has_update_permission P')),
--- a/selectors.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/selectors.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,1603 +15,27 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-""".. _Selectors:
 
-Selectors
----------
-
-Using and combining existant selectors
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can combine selectors using the `&`, `|` and `~` operators.
-
-When two selectors are combined using the `&` operator, it means that
-both should return a positive score. On success, the sum of scores is
-returned.
-
-When two selectors are combined using the `|` operator, it means that
-one of them should return a positive score. On success, the first
-positive score is returned.
-
-You can also "negate" a selector by precedeing it by the `~` unary operator.
-
-Of course you can use parenthesis to balance expressions.
-
-Example
-~~~~~~~
-
-The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
-the blog entity itself.
-
-To do that, one defines a method on entity classes that returns the
-RSS stream url for a given entity. The default implementation on
-:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
-as base for all others) and a specific implementation on `Blog` will
-do what we want.
-
-But when we have a result set containing several `Blog` entities (or
-different entities), we don't know on which entity to call the
-aforementioned method. In this case, we keep the generic behaviour.
-
-Hence we have two cases here, one for a single-entity rsets, the other for
-multi-entities rsets.
-
-In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
-
-.. sourcecode:: python
-
-  class RSSIconBox(box.Box):
-    ''' just display the RSS icon on uniform result set '''
-    __select__ = box.Box.__select__ & non_final_entity()
-
-It takes into account:
-
-* the inherited selection criteria (one has to look them up in the class
-  hierarchy to know the details)
-
-* :class:`~cubicweb.selectors.non_final_entity`, which filters on result sets
-  containing non final entities (a 'final entity' being synonym for entity
-  attributes type, eg `String`, `Int`, etc)
-
-This matches our second case. Hence we have to provide a specific component for
-the first case:
-
-.. sourcecode:: python
-
-  class EntityRSSIconBox(RSSIconBox):
-    '''just display the RSS icon on uniform result set for a single entity'''
-    __select__ = RSSIconBox.__select__ & one_line_rset()
-
-Here, one adds the :class:`~cubicweb.selectors.one_line_rset` selector, which
-filters result sets of size 1. Thus, on a result set containing multiple
-entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
-selectable. However for a result set with one entity, the `EntityRSSIconBox`
-class will have a higher score than `RSSIconBox`, which is what we wanted.
-
-Of course, once this is done, you have to:
-
-* fill in the call method of `EntityRSSIconBox`
-
-* provide the default implementation of the method returning the RSS stream url
-  on :class:`~cubicweb.entities.AnyEntity`
-
-* redefine this method on `Blog`.
-
-
-When to use selectors?
-~~~~~~~~~~~~~~~~~~~~~~
-
-Selectors are to be used whenever arises the need of dispatching on the shape or
-content of a result set or whatever else context (value in request form params,
-authenticated user groups, etc...). That is, almost all the time.
-
-Here is a quick example:
-
-.. sourcecode:: python
-
-    class UserLink(component.Component):
-	'''if the user is the anonymous user, build a link to login else a link
-	to the connected user object with a logout link
-	'''
-	__regid__ = 'loggeduserlink'
-
-	def call(self):
-	    if self._cw.session.anonymous_session:
-		# display login link
-		...
-	    else:
-		# display a link to the connected user object with a loggout link
-		...
-
-The proper way to implement this with |cubicweb| is two have two different
-classes sharing the same identifier but with different selectors so you'll get
-the correct one according to the context.
-
-.. sourcecode:: python
-
-    class UserLink(component.Component):
-	'''display a link to the connected user object with a loggout link'''
-	__regid__ = 'loggeduserlink'
-	__select__ = component.Component.__select__ & authenticated_user()
-
-	def call(self):
-            # display useractions and siteactions
-	    ...
-
-    class AnonUserLink(component.Component):
-	'''build a link to login'''
-	__regid__ = 'loggeduserlink'
-	__select__ = component.Component.__select__ & anonymous_user()
-
-	def call(self):
-	    # display login link
-            ...
-
-The big advantage, aside readability once you're familiar with the
-system, is that your cube becomes much more easily customizable by
-improving componentization.
-
-
-.. _CustomSelectors:
-
-Defining your own selectors
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. autodocstring:: cubicweb.appobject::objectify_selector
-
-In other cases, you can take a look at the following abstract base classes:
-
-.. autoclass:: cubicweb.selectors.ExpectedValueSelector
-.. autoclass:: cubicweb.selectors.EClassSelector
-.. autoclass:: cubicweb.selectors.EntitySelector
-
-Also, think to use the :func:`lltrace` decorator on your selector class' :meth:`__call__` method
-or below the :func:`objectify_selector` decorator of your selector function so it gets
-traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
-
-.. autofunction:: cubicweb.appobject.lltrace
-
-.. note::
-  Selectors __call__ should *always* return a positive integer, and shall never
-  return `None`.
-
-
-.. _DebuggingSelectors:
-
-Debugging selection
-~~~~~~~~~~~~~~~~~~~
-
-Once in a while, one needs to understand why a view (or any application object)
-is, or is not selected appropriately. Looking at which selectors fired (or did
-not) is the way. The :class:`cubicweb.appobject.traced_selection` context
-manager to help with that, *if you're running your instance in debug mode*.
-
-.. autoclass:: cubicweb.appobject.traced_selection
-
-"""
-
-__docformat__ = "restructuredtext en"
-
-import logging
 from warnings import warn
-from operator import eq
-
-from logilab.common.deprecation import class_renamed, deprecated
-from logilab.common.compat import all, any
-from logilab.common.interface import implements as implements_iface
-
-from yams.schema import BASE_TYPES, role_name
-from rql.nodes import Function
-
-from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
-                      CW_EVENT_MANAGER, role)
-# even if not used, let yes here so it's importable through this module
-from cubicweb.uilib import eid_param
-from cubicweb.appobject import Selector, objectify_selector, lltrace, yes
-from cubicweb.schema import split_expression
-
-from cubicweb.appobject import traced_selection # XXX for bw compat
-
-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 selectors / mixin helpers ###########################################
-
-class PartialSelectorMixIn(object):
-    """convenience mix-in for selectors that will look into the containing
-    class to find missing information.
-
-    cf. `cubicweb.web.action.LinkToEntityAction` for instance
-    """
-    def __call__(self, cls, *args, **kwargs):
-        self.complete(cls)
-        return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
-
-
-class EClassSelector(Selector):
-    """abstract class for selectors working on *entity class(es)* specified
-    explicitly or found of the result set.
-
-    Here are entity lookup / scoring rules:
-
-    * if `entity` is specified, return score for this entity's class
-
-    * elif `rset`, `select` and `filtered_variable` are specified, return score
-      for the possible classes for variable in the given rql :class:`Select`
-      node
-
-    * elif `rset` and `row` are specified, return score for the class of the
-      entity found in the specified cell, using column specified by `col` or 0
-
-    * elif `rset` is specified return score for each entity class found in the
-      column specified specified by the `col` argument or in column 0 if not
-      specified
-
-    When there are several classes to be evaluated, return the sum of scores for
-    each entity class unless:
-
-      - `mode` == 'all' (the default) and some entity class is scored
-        to 0, in which case 0 is returned
-
-      - `mode` == 'any', in which case the first non-zero score is
-        returned
-
-      - `accept_none` is False and some cell in the column has a None value
-        (this may occurs with outer join)
-    """
-    def __init__(self, once_is_enough=None, accept_none=True, mode='all'):
-        if once_is_enough is not None:
-            warn("[3.14] once_is_enough is deprecated, use mode='any'",
-                 DeprecationWarning, stacklevel=2)
-            if once_is_enough:
-                mode = 'any'
-        assert mode in ('any', 'all'), 'bad mode %s' % mode
-        self.once_is_enough = mode == 'any'
-        self.accept_none = accept_none
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, entity=None,
-                 select=None, filtered_variable=None,
-                 accept_none=None,
-                 **kwargs):
-        if entity is not None:
-            return self.score_class(entity.__class__, req)
-        if not rset:
-            return 0
-        if select is not None and filtered_variable is not None:
-            etypes = set(sol[filtered_variable.name] for sol in select.solutions)
-        elif row is None:
-            if accept_none is None:
-                accept_none = self.accept_none
-            if not accept_none and \
-                   any(rset[i][col] is None for i in xrange(len(rset))):
-                return 0
-            etypes = rset.column_types(col)
-        else:
-            etype = rset.description[row][col]
-            # may have None in rset.description on outer join
-            if etype is None or rset.rows[row][col] is None:
-                return 0
-            etypes = (etype,)
-        score = 0
-        for etype in etypes:
-            escore = self.score(cls, req, etype)
-            if not escore and not self.once_is_enough:
-                return 0
-            elif self.once_is_enough:
-                return escore
-            score += escore
-        return score
-
-    def score(self, cls, req, etype):
-        if etype in BASE_TYPES:
-            return 0
-        return self.score_class(req.vreg['etypes'].etype_class(etype), req)
-
-    def score_class(self, eclass, req):
-        raise NotImplementedError()
-
-
-class EntitySelector(EClassSelector):
-    """abstract class for selectors working on *entity instance(s)* specified
-    explicitly or found of the result set.
-
-    Here are entity lookup / scoring rules:
-
-    * if `entity` is specified, return score for this entity
-
-    * elif `row` is specified, return score for the entity found in the
-      specified cell, using column specified by `col` or 0
-
-    * else return the sum of scores for each entity found in the column
-      specified specified by the `col` argument or in column 0 if not specified,
-      unless:
-
-      - `mode` == 'all' (the default) and some entity class is scored
-        to 0, in which case 0 is returned
-
-      - `mode` == 'any', in which case the first non-zero score is
-        returned
-
-      - `accept_none` is False and some cell in the column has a None value
-        (this may occurs with outer join)
-
-    .. Note::
-       using :class:`EntitySelector` or :class:`EClassSelector` as base selector
-       class impacts performance, since when no entity or row is specified the
-       later works on every different *entity class* found in the result set,
-       while the former works on each *entity* (eg each row of the result set),
-       which may be much more costly.
-    """
 
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
-                 **kwargs):
-        if not rset and not kwargs.get('entity'):
-            return 0
-        score = 0
-        if kwargs.get('entity'):
-            score = self.score_entity(kwargs['entity'])
-        elif row is None:
-            col = col or 0
-            if accept_none is None:
-                accept_none = self.accept_none
-            for row, rowvalue in enumerate(rset.rows):
-                if rowvalue[col] is None: # outer join
-                    if not accept_none:
-                        return 0
-                    continue
-                escore = self.score(req, rset, row, col)
-                if not escore and not self.once_is_enough:
-                    return 0
-                elif self.once_is_enough:
-                    return escore
-                score += escore
-        else:
-            col = col or 0
-            etype = rset.description[row][col]
-            if etype is not None: # outer join
-                score = self.score(req, rset, row, col)
-        return score
-
-    def score(self, req, rset, row, col):
-        try:
-            return self.score_entity(rset.get_entity(row, col))
-        except NotAnEntity:
-            return 0
-
-    def score_entity(self, entity):
-        raise NotImplementedError()
-
-
-class ExpectedValueSelector(Selector):
-    """Take a list of expected values as initializer argument and store them
-    into the :attr:`expected` set attribute. You may also give a set as single
-    argument, which will then be referenced as set of expected values,
-    allowing modifications to the given set to be considered.
-
-    You should implement one of :meth:`_values_set(cls, req, **kwargs)` or
-    :meth:`_get_value(cls, req, **kwargs)` method which should respectively
-    return the set of values or the unique possible value for the given context.
-
-    You may also specify a `mode` behaviour as argument, as explained below.
-
-    Returned score is:
-
-    - 0 if `mode` == 'all' (the default) and at least one expected
-      values isn't found
-
-    - 0 if `mode` == 'any' and no expected values isn't found at all
-
-    - else the number of matching values
-
-    Notice `mode` = 'any' with a single expected value has no effect at all.
-    """
-    def __init__(self, *expected, **kwargs):
-        assert expected, self
-        if len(expected) == 1 and isinstance(expected[0], set):
-            self.expected = expected[0]
-        else:
-            self.expected = frozenset(expected)
-        mode = kwargs.pop('mode', 'all')
-        assert mode in ('any', 'all'), 'bad mode %s' % mode
-        self.once_is_enough = mode == 'any'
-        assert not kwargs, 'unexpected arguments %s' % kwargs
-
-    def __str__(self):
-        return '%s(%s)' % (self.__class__.__name__,
-                           ','.join(sorted(str(s) for s in self.expected)))
-
-    @lltrace
-    def __call__(self, cls, req, **kwargs):
-        values = self._values_set(cls, req, **kwargs)
-        matching = len(values & self.expected)
-        if self.once_is_enough:
-            return matching
-        if matching == len(self.expected):
-            return matching
-        return 0
-
-    def _values_set(self, cls, req, **kwargs):
-        return frozenset( (self._get_value(cls, req, **kwargs),) )
-
-    def _get_value(self, cls, req, **kwargs):
-        raise NotImplementedError()
-
-
-# bare selectors ##############################################################
-
-class match_kwargs(ExpectedValueSelector):
-    """Return non-zero score if parameter names specified as initializer
-    arguments are specified in the input context.
-
-
-    Return a score corresponding to the number of expected parameters.
-
-    When multiple parameters are expected, all of them should be found in
-    the input context unless `mode` keyword argument is given to 'any',
-    in which case a single matching parameter is enough.
-    """
-
-    def _values_set(self, cls, req, **kwargs):
-        return frozenset(kwargs)
-
-
-class appobject_selectable(Selector):
-    """Return 1 if another appobject is selectable using the same input context.
-
-    Initializer arguments:
-
-    * `registry`, a registry name
-
-    * `regids`, object identifiers in this registry, one of them should be
-      selectable.
-    """
-    selectable_score = 1
-    def __init__(self, registry, *regids):
-        self.registry = registry
-        self.regids = regids
-
-    @lltrace
-    def __call__(self, cls, req, **kwargs):
-        for regid in self.regids:
-            try:
-                req.vreg[self.registry].select(regid, req, **kwargs)
-                return self.selectable_score
-            except NoSelectableObject:
-                continue
-        return 0
-
-
-class adaptable(appobject_selectable):
-    """Return 1 if another appobject is selectable using the same input context.
-
-    Initializer arguments:
-
-    * `regids`, adapter identifiers (e.g. interface names) to which the context
-      (usually entities) should be adaptable. One of them should be selectable
-      when multiple identifiers are given.
-    """
-    def __init__(self, *regids):
-        super(adaptable, self).__init__('adapters', *regids)
-
-    def __call__(self, cls, req, **kwargs):
-        kwargs.setdefault('accept_none', False)
-        # being adaptable to an interface should takes precedence other is_instance('Any'),
-        # but not other explicit is_instance('SomeEntityType'), and:
-        # * is_instance('Any') score is 1
-        # * is_instance('SomeEntityType') score is at least 2
-        score = super(adaptable, self).__call__(cls, req, **kwargs)
-        if score >= 2:
-            return score - 0.5
-        if score == 1:
-            return score + 0.5
-        return score
-
-
-class configuration_values(Selector):
-    """Return 1 if the instance has an option set to a given value(s) in its
-    configuration file.
-    """
-    # XXX this selector could be evaluated on startup
-    def __init__(self, key, values):
-        self._key = key
-        if not isinstance(values, (tuple, list)):
-            values = (values,)
-        self._values = frozenset(values)
-
-    @lltrace
-    def __call__(self, cls, req, **kwargs):
-        try:
-            return self._score
-        except AttributeError:
-            if req is None:
-                config = kwargs['repo'].config
-            else:
-                config = req.vreg.config
-            self._score = config[self._key] in self._values
-        return self._score
-
-
-# rset selectors ##############################################################
+from logilab.common.deprecation import deprecated, class_renamed
 
-@objectify_selector
-@lltrace
-def none_rset(cls, req, rset=None, **kwargs):
-    """Return 1 if the result set is None (eg usually not specified)."""
-    if rset is None:
-        return 1
-    return 0
-
-
-# XXX == ~ none_rset
-@objectify_selector
-@lltrace
-def any_rset(cls, req, rset=None, **kwargs):
-    """Return 1 for any result set, whatever the number of rows in it, even 0."""
-    if rset is not None:
-        return 1
-    return 0
-
-
-@objectify_selector
-@lltrace
-def nonempty_rset(cls, req, rset=None, **kwargs):
-    """Return 1 for result set containing one ore more rows."""
-    if rset is not None and rset.rowcount:
-        return 1
-    return 0
-
-
-# XXX == ~ nonempty_rset
-@objectify_selector
-@lltrace
-def empty_rset(cls, req, rset=None, **kwargs):
-    """Return 1 for result set which doesn't contain any row."""
-    if rset is not None and rset.rowcount == 0:
-        return 1
-    return 0
-
-
-# XXX == multi_lines_rset(1)
-@objectify_selector
-@lltrace
-def one_line_rset(cls, req, rset=None, row=None, **kwargs):
-    """Return 1 if the result set is of size 1, or greater but a specific row in
-      the result set is specified ('row' argument).
-    """
-    if rset is None and 'entity' in kwargs:
-        return 1
-    if rset is not None and (row is not None or rset.rowcount == 1):
-        return 1
-    return 0
-
-
-class multi_lines_rset(Selector):
-    """Return 1 if the operator expression matches between `num` elements
-    in the result set and the `expected` value if defined.
-
-    By default, multi_lines_rset(expected) matches equality expression:
-        `nb` row(s) in result set equals to expected value
-    But, you can perform richer comparisons by overriding default operator:
-        multi_lines_rset(expected, operator.gt)
-
-    If `expected` is None, return 1 if the result set contains *at least*
-    two rows.
-    If rset is None, return 0.
-    """
-    def __init__(self, expected=None, operator=eq):
-        self.expected = expected
-        self.operator = operator
-
-    def match_expected(self, num):
-        if self.expected is None:
-            return num > 1
-        return self.operator(num, self.expected)
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, **kwargs):
-        return int(rset is not None and self.match_expected(rset.rowcount))
-
-
-class multi_columns_rset(multi_lines_rset):
-    """If `nb` is specified, return 1 if the result set has exactly `nb` column
-    per row. Else (`nb` is None), return 1 if the result set contains *at least*
-    two columns per row. Return 0 for empty result set.
-    """
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, **kwargs):
-        # 'or 0' since we *must not* return None
-        return rset and self.match_expected(len(rset.rows[0])) or 0
-
-
-class paginated_rset(Selector):
-    """Return 1 or more for result set with more rows than one or more page
-    size.  You can specify expected number of pages to the initializer (default
-    to one), and you'll get that number of pages as score if the result set is
-    big enough.
-
-    Page size is searched in (respecting order):
-    * a `page_size` argument
-    * a `page_size` form parameters
-    * the `navigation.page-size` property (see :ref:`PersistentProperties`)
-    """
-    def __init__(self, nbpages=1):
-        assert nbpages > 0
-        self.nbpages = nbpages
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, **kwargs):
-        if rset is None:
-            return 0
-        page_size = kwargs.get('page_size')
-        if page_size is None:
-            page_size = req.form.get('page_size')
-            if page_size is None:
-                page_size = req.property_value('navigation.page-size')
-            else:
-                page_size = int(page_size)
-        if rset.rowcount <= (page_size*self.nbpages):
-            return 0
-        return self.nbpages
-
-
-@objectify_selector
-@lltrace
-def sorted_rset(cls, req, rset=None, **kwargs):
-    """Return 1 for sorted result set (e.g. from an RQL query containing an
-    ORDERBY clause), with exception that it will return 0 if the rset is
-    'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
-    """
-    if rset is None:
-        return 0
-    selects = rset.syntax_tree().children
-    if (len(selects) > 1 or
-        not selects[0].orderby or
-        (isinstance(selects[0].orderby[0].term, Function) and
-         selects[0].orderby[0].term.name == 'FTIRANK')
-        ):
-        return 0
-    return 2
-
-
-# XXX == multi_etypes_rset(1)
-@objectify_selector
-@lltrace
-def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
-    """Return 1 if the result set contains entities which are all of the same
-    type in the column specified by the `col` argument of the input context, or
-    in column 0.
-    """
-    if rset is None:
-        return 0
-    if len(rset.column_types(col)) != 1:
-        return 0
-    return 1
-
-
-class multi_etypes_rset(multi_lines_rset):
-    """If `nb` is specified, return 1 if the result set contains `nb` different
-    types of entities in the column specified by the `col` argument of the input
-    context, or in column 0. If `nb` is None, return 1 if the result set contains
-    *at least* two different types of entities.
-    """
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, col=0, **kwargs):
-        # 'or 0' since we *must not* return None
-        return rset and self.match_expected(len(rset.column_types(col))) or 0
-
-
-@objectify_selector
-def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
-    """Return positive score if the result set at the specified row / col
-    contains the eid of the logged user.
-    """
-    if rset is None:
-        return 0
-    return req.user.eid == rset[row or 0][col]
-
-
-# entity selectors #############################################################
-
-class non_final_entity(EClassSelector):
-    """Return 1 for entity of a non final entity type(s). Remember, "final"
-    entity types are String, Int, etc... This is equivalent to
-    `is_instance('Any')` but more optimized.
-
-    See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
-    class lookup / score rules according to the input context.
-    """
-    def score(self, cls, req, etype):
-        if etype in BASE_TYPES:
-            return 0
-        return 1
-
-    def score_class(self, eclass, req):
-        return 1 # necessarily true if we're there
+from cubicweb.predicates import *
 
 
-class implements(EClassSelector):
-    """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.selectors.EClassSelector` 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.selectors.is_instance` or
-              :class:`~cubicweb.selectors.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 selector 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_selector_cache = {}
-
-CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
-
-class is_instance(EClassSelector):
-    """Return non-zero score for entity that is an instance of the one of given
-    type(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 registry at selection time.
-
-    See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
-    class lookup / score rules according to the input context.
-
-    .. note:: the score will reflect class proximity so the most specific object
-              will be selected.
-    """
-
-    def __init__(self, *expected_etypes, **kwargs):
-        super(is_instance, self).__init__(**kwargs)
-        self.expected_etypes = expected_etypes
-        for etype in self.expected_etypes:
-            assert isinstance(etype, basestring), etype
-
-    def __str__(self):
-        return '%s(%s)' % (self.__class__.__name__,
-                           ','.join(str(s) for s in self.expected_etypes))
-
-    def score_class(self, eclass, req):
-        # cache on vreg to avoid reloading issues
-        try:
-            cache = req.vreg._is_instance_selector_cache
-        except AttributeError:
-            # XXX 'before-registry-reset' not called for db-api connections
-            cache = req.vreg._is_instance_selector_cache = {}
-        try:
-            expected_eclasses = cache[self]
-        except KeyError:
-            # turn list of entity types as string into a list of
-            #  (entity class, parent classes)
-            etypesreg = req.vreg['etypes']
-            expected_eclasses = cache[self] = []
-            for etype in self.expected_etypes:
-                try:
-                    expected_eclasses.append(etypesreg.etype_class(etype))
-                except KeyError:
-                    continue # entity type not in the schema
-        parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
-        score = 0
-        for expectedcls in expected_eclasses:
-            # adjust score according to class proximity
-            if expectedcls is eclass:
-                score += len(parents) + 4
-            elif expectedcls is any: # Any
-                score += 1
-            else:
-                for index, basecls in enumerate(reversed(parents)):
-                    if expectedcls is basecls:
-                        score += index + 3
-                        break
-        return score
-
-
-class score_entity(EntitySelector):
-    """Return score according to an arbitrary function given as argument which
-    will be called with input content entity as argument.
-
-    This is a very useful selector that will usually interest you since it
-    allows a lot of things without having to write a specific selector.
-
-    The function can return arbitrary value which will be casted to an integer
-    value at the end.
-
-    See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
-    lookup / score rules according to the input context.
-    """
-    def __init__(self, scorefunc, once_is_enough=None, mode='all'):
-        super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
-        def intscore(*args, **kwargs):
-            score = scorefunc(*args, **kwargs)
-            if not score:
-                return 0
-            if isinstance(score, (int, long)):
-                return score
-            return 1
-        self.score_entity = intscore
-
-
-class has_mimetype(EntitySelector):
-    """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
-
-    You can give 'image/' to match any image for instance, or 'image/png' to match
-    only PNG images.
-    """
-    def __init__(self, mimetype, once_is_enough=None, mode='all'):
-        super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
-        self.mimetype = mimetype
-
-    def score_entity(self, entity):
-        idownloadable = entity.cw_adapt_to('IDownloadable')
-        if idownloadable is None:
-            return 0
-        mt = idownloadable.download_content_type()
-        if not (mt and mt.startswith(self.mimetype)):
-            return 0
-        return 1
-
-
-class relation_possible(EntitySelector):
-    """Return 1 for entity that supports the relation, provided that the
-    request's user may do some `action` on it (see below).
-
-    The relation is specified by the following initializer arguments:
-
-    * `rtype`, the name of the relation
-
-    * `role`, the role of the entity in the relation, either 'subject' or
-      'object', default to 'subject'
-
-    * `target_etype`, optional name of an entity type that should be supported
-      at the other end of the relation
-
-    * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
-      default to 'read') which must be granted to the user, else a 0 score will
-      be returned. Give None if you don't want any permission checking.
-
-    * `strict`, boolean (default to False) telling what to do when the user has
-      not globally the permission for the action (eg the action is not granted
-      to one of the user's groups)
-
-      - when strict is False, if there are some local role defined for this
-        action (e.g. using rql expressions), then the permission will be
-        considered as granted
-
-      - when strict is True, then the permission will be actually checked for
-        each entity
-
-    Setting `strict` to True impacts performance for large result set since
-    you'll then get the :class:`~cubicweb.selectors.EntitySelector` behaviour
-    while otherwise you get the :class:`~cubicweb.selectors.EClassSelector`'s
-    one. See those classes documentation for entity lookup / score rules
-    according to the input context.
-    """
-
-    def __init__(self, rtype, role='subject', target_etype=None,
-                 action='read', strict=False, **kwargs):
-        super(relation_possible, self).__init__(**kwargs)
-        self.rtype = rtype
-        self.role = role
-        self.target_etype = target_etype
-        self.action = action
-        self.strict = strict
-
-    # hack hack hack
-    def __call__(self, cls, req, **kwargs):
-        # hack hack hack
-        if self.strict:
-            return EntitySelector.__call__(self, cls, req, **kwargs)
-        return EClassSelector.__call__(self, cls, req, **kwargs)
-
-    def score(self, *args):
-        if self.strict:
-            return EntitySelector.score(self, *args)
-        return EClassSelector.score(self, *args)
+warn('[3.15] cubicweb.selectors renamed into cubicweb.predicates',
+     DeprecationWarning, stacklevel=2)
 
-    def _get_rschema(self, eclass):
-        eschema = eclass.e_schema
-        try:
-            if self.role == 'object':
-                return eschema.objrels[self.rtype]
-            else:
-                return eschema.subjrels[self.rtype]
-        except KeyError:
-            return None
-
-    def score_class(self, eclass, req):
-        rschema = self._get_rschema(eclass)
-        if rschema is None:
-            return 0 # relation not supported
-        eschema = eclass.e_schema
-        if self.target_etype is not None:
-            try:
-                rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
-            except KeyError:
-                return 0
-            if self.action and not rdef.may_have_permission(self.action, req):
-                return 0
-            teschema = req.vreg.schema.eschema(self.target_etype)
-            if not teschema.may_have_permission('read', req):
-                return 0
-        elif self.action:
-            return rschema.may_have_permission(self.action, req, eschema, self.role)
-        return 1
-
-    def score_entity(self, entity):
-        rschema = self._get_rschema(entity)
-        if rschema is None:
-            return 0 # relation not supported
-        if self.action:
-            if self.target_etype is not None:
-                rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
-            if self.role == 'subject':
-                if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid):
-                    return 0
-            elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid):
-                return 0
-        if self.target_etype is not None:
-            req = entity._cw
-            teschema = req.vreg.schema.eschema(self.target_etype)
-            if not teschema.may_have_permission('read', req):
-                return 0
-        return 1
-
-
-class partial_relation_possible(PartialSelectorMixIn, relation_possible):
-    """Same as :class:~`cubicweb.selectors.relation_possible`, but will look for
-    attributes of the selected class to get information which is otherwise
-    expected by the initializer, except for `action` and `strict` which are kept
-    as initializer arguments.
-
-    This is useful to predefine selector of an abstract class designed to be
-    customized.
-    """
-    def __init__(self, action='read', **kwargs):
-        super(partial_relation_possible, self).__init__(None, None, None,
-                                                        action, **kwargs)
-
-    def complete(self, cls):
-        self.rtype = cls.rtype
-        self.role = role(cls)
-        self.target_etype = getattr(cls, 'target_etype', None)
-
-
-class has_related_entities(EntitySelector):
-    """Return 1 if entity support the specified relation and has some linked
-    entities by this relation , optionaly filtered according to the specified
-    target type.
-
-    The relation is specified by the following initializer arguments:
-
-    * `rtype`, the name of the relation
-
-    * `role`, the role of the entity in the relation, either 'subject' or
-      'object', default to 'subject'.
-
-    * `target_etype`, optional name of an entity type that should be found
-      at the other end of the relation
-
-    See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
-    lookup / score rules according to the input context.
-    """
-    def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
-        super(has_related_entities, self).__init__(**kwargs)
-        self.rtype = rtype
-        self.role = role
-        self.target_etype = target_etype
-
-    def score_entity(self, entity):
-        relpossel = relation_possible(self.rtype, self.role, self.target_etype)
-        if not relpossel.score_class(entity.__class__, entity._cw):
-            return 0
-        rset = entity.related(self.rtype, self.role)
-        if self.target_etype:
-            return any(r for r in rset.description if r[0] == self.target_etype)
-        return rset and 1 or 0
-
-
-class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
-    """Same as :class:~`cubicweb.selectors.has_related_entity`, but will look
-    for attributes of the selected class to get information which is otherwise
-    expected by the initializer.
-
-    This is useful to predefine selector of an abstract class designed to be
-    customized.
-    """
-    def __init__(self, **kwargs):
-        super(partial_has_related_entities, self).__init__(None, None, None,
-                                                           **kwargs)
-
-    def complete(self, cls):
-        self.rtype = cls.rtype
-        self.role = role(cls)
-        self.target_etype = getattr(cls, 'target_etype', None)
-
-
-class has_permission(EntitySelector):
-    """Return non-zero score if request's user has the permission to do the
-    requested action on the entity. `action` is an entity schema action (eg one
-    of 'read', 'add', 'delete', 'update').
-
-    Here are entity lookup / scoring rules:
-
-    * if `entity` is specified, check permission is granted for this entity
-
-    * elif `row` is specified, check permission is granted for the entity found
-      in the specified cell
-
-    * else check permission is granted for each entity found in the column
-      specified specified by the `col` argument or in column 0
-    """
-    def __init__(self, action):
-        self.action = action
-
-    # don't use EntitySelector.__call__ but this optimized implementation to
-    # avoid considering each entity when it's not necessary
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        if kwargs.get('entity'):
-            return self.score_entity(kwargs['entity'])
-        if rset is None:
-            return 0
-        if row is None:
-            score = 0
-            need_local_check = []
-            geteschema = req.vreg.schema.eschema
-            user = req.user
-            action = self.action
-            for etype in rset.column_types(0):
-                if etype in BASE_TYPES:
-                    return 0
-                eschema = geteschema(etype)
-                if not user.matching_groups(eschema.get_groups(action)):
-                    if eschema.has_local_role(action):
-                        # have to ckeck local roles
-                        need_local_check.append(eschema)
-                        continue
-                    else:
-                        # even a local role won't be enough
-                        return 0
-                score += 1
-            if need_local_check:
-                # check local role for entities of necessary types
-                for i, row in enumerate(rset):
-                    if not rset.description[i][col] in need_local_check:
-                        continue
-                    # micro-optimisation instead of calling self.score(req,
-                    # rset, i, col): rset may be large
-                    if not rset.get_entity(i, col).cw_has_perm(action):
-                        return 0
-                score += 1
-            return score
-        return self.score(req, rset, row, col)
-
-    def score_entity(self, entity):
-        if entity.cw_has_perm(self.action):
-            return 1
-        return 0
-
-
-class has_add_permission(EClassSelector):
-    """Return 1 if request's user has the add permission on entity type
-    specified in the `etype` initializer argument, or according to entity found
-    in the input content if not specified.
-
-    It also check that then entity type is not a strict subobject (e.g. may only
-    be used as a composed of another entity).
-
-    See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
-    class lookup / score rules according to the input context when `etype` is
-    not specified.
-    """
-    def __init__(self, etype=None, **kwargs):
-        super(has_add_permission, self).__init__(**kwargs)
-        self.etype = etype
-
-    @lltrace
-    def __call__(self, cls, req, **kwargs):
-        if self.etype is None:
-            return super(has_add_permission, self).__call__(cls, req, **kwargs)
-        return self.score(cls, req, self.etype)
-
-    def score_class(self, eclass, req):
-        eschema = eclass.e_schema
-        if eschema.final or eschema.is_subobject(strict=True) \
-               or not eschema.has_perm(req, 'add'):
-            return 0
-        return 1
-
-
-class rql_condition(EntitySelector):
-    """Return non-zero score if arbitrary rql specified in `expression`
-    initializer argument return some results for entity found in the input
-    context. Returned score is the number of items returned by the rql
-    condition.
+# XXX pre 3.15 bw compat
+from cubicweb.appobject import (objectify_selector, traced_selection,
+                                lltrace, yes)
 
-    `expression` is expected to be a string containing an rql expression, which
-    must use 'X' variable to represent the context entity and may use 'U' to
-    represent the request's user.
-
-    .. warning::
-        If simply testing value of some attribute/relation of context entity (X),
-        you should rather use the :class:`score_entity` selector which will
-        benefit from the ORM's request entities cache.
-
-    See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
-    lookup / score rules according to the input context.
-    """
-    def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
-        super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
-        self.user_condition = user_condition
-        if user_condition:
-            rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
-        elif 'U' in frozenset(split_expression(expression)):
-            rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
-        else:
-            rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
-        self.rql = rql
-
-    def __str__(self):
-        return '%s(%r)' % (self.__class__.__name__, self.rql)
-
-    @lltrace
-    def __call__(self, cls, req, **kwargs):
-        if self.user_condition:
-            try:
-                return req.execute(self.rql, {'u': req.user.eid})[0][0]
-            except Unauthorized:
-                return 0
-        else:
-            return super(rql_condition, self).__call__(cls, req, **kwargs)
-
-    def _score(self, req, eid):
-        try:
-            return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0]
-        except Unauthorized:
-            return 0
-
-    def score(self, req, rset, row, col):
-        return self._score(req, rset[row][col])
-
-    def score_entity(self, entity):
-        return self._score(entity._cw, entity.eid)
-
-
-# workflow selectors ###########################################################
-
-class is_in_state(score_entity):
-    """Return 1 if entity is in one of the states given as argument list
-
-    You should use this instead of your own :class:`score_entity` selector to
-    avoid some gotchas:
-
-    * possible views gives a fake entity with no state
-    * you must use the latest tr info thru the workflow adapter for repository
-      side checking of the current state
-
-    In debug mode, this selector can raise :exc:`ValueError` for unknown states names
-    (only checked on entities without a custom workflow)
-
-    :rtype: int
-    """
-    def __init__(self, *expected):
-        assert expected, self
-        self.expected = frozenset(expected)
-        def score(entity, expected=self.expected):
-            adapted = entity.cw_adapt_to('IWorkflowable')
-            # in debug mode only (time consuming)
-            if entity._cw.vreg.config.debugmode:
-                # validation can only be done for generic etype workflow because
-                # expected transition list could have been changed for a custom
-                # workflow (for the current entity)
-                if not entity.custom_workflow:
-                    self._validate(adapted)
-            return self._score(adapted)
-        super(is_in_state, self).__init__(score)
-
-    def _score(self, adapted):
-        trinfo = adapted.latest_trinfo()
-        if trinfo is None: # entity is probably in it's initial state
-            statename = adapted.state
-        else:
-            statename = trinfo.new_state.name
-        return statename in self.expected
-
-    def _validate(self, adapted):
-        wf = adapted.current_workflow
-        valid = [n.name for n in wf.reverse_state_of]
-        unknown = sorted(self.expected.difference(valid))
-        if unknown:
-            raise ValueError("%s: unknown state(s): %s"
-                             % (wf.name, ",".join(unknown)))
-
-    def __str__(self):
-        return '%s(%s)' % (self.__class__.__name__,
-                           ','.join(str(s) for s in self.expected))
-
-
-def on_fire_transition(etype, tr_name, from_state_name=None):
-    """Return 1 when entity of the type `etype` is going through transition of
-    the name `tr_name`.
-
-    If `from_state_name` is specified, this selector will also check the
-    incoming state.
-
-    You should use this selector on 'after_add_entity' hook, since it's actually
-    looking for addition of `TrInfo` entities. Hence in the hook, `self.entity`
-    will reference the matching `TrInfo` entity, allowing to get all the
-    transition details (including the entity to which is applied the transition
-    but also its original state, transition, destination state, user...).
-
-    See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
-    """
-    def match_etype_and_transition(trinfo):
-        # take care trinfo.transition is None when calling change_state
-        return (trinfo.transition and trinfo.transition.name == tr_name
-                # is_instance() first two arguments are 'cls' (unused, so giving
-                # None is fine) and the request/session
-                and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity))
-
-    return is_instance('TrInfo') & score_entity(match_etype_and_transition)
-
-
-class match_transition(ExpectedValueSelector):
-    """Return 1 if `transition` argument is found in the input context which has
-    a `.name` attribute matching one of the expected names given to the
-    initializer.
-
-    This selector is expected to be used to customise the status change form in
-    the web ui.
-    """
-    @lltrace
-    def __call__(self, cls, req, transition=None, **kwargs):
-        # XXX check this is a transition that apply to the object?
-        if transition is None:
-            treid = req.form.get('treid', None)
-            if treid:
-                transition = req.entity_from_eid(treid)
-        if transition is not None and getattr(transition, 'name', None) in self.expected:
-            return 1
-        return 0
-
-
-# logged user selectors ########################################################
-
-@objectify_selector
-@lltrace
-def no_cnx(cls, req, **kwargs):
-    """Return 1 if the web session has no connection set. This occurs when
-    anonymous access is not allowed and user isn't authenticated.
-
-    May only be used on the web side, not on the data repository side.
-    """
-    if not req.cnx:
-        return 1
-    return 0
-
-@objectify_selector
-@lltrace
-def authenticated_user(cls, req, **kwargs):
-    """Return 1 if the user is authenticated (e.g. not the anonymous user).
-
-    May only be used on the web side, not on the data repository side.
-    """
-    if req.session.anonymous_session:
-        return 0
-    return 1
-
-
-# XXX == ~ authenticated_user()
-def anonymous_user():
-    """Return 1 if the user is not authenticated (e.g. is the anonymous user).
-
-    May only be used on the web side, not on the data repository side.
-    """
-    return ~ authenticated_user()
-
-class match_user_groups(ExpectedValueSelector):
-    """Return a non-zero score if request's user is in at least one of the
-    groups given as initializer argument. Returned score is the number of groups
-    in which the user is.
-
-    If the special 'owners' group is given and `rset` is specified in the input
-    context:
-
-    * if `row` is specified check the entity at the given `row`/`col` (default
-      to 0) is owned by the user
-
-    * else check all entities in `col` (default to 0) are owned by the user
-    """
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        if not getattr(req, 'cnx', True): # default to True for repo session instances
-            return 0
-        user = req.user
-        if user is None:
-            return int('guests' in self.expected)
-        score = user.matching_groups(self.expected)
-        if not score and 'owners' in self.expected and rset:
-            if row is not None:
-                if not user.owns(rset[row][col]):
-                    return 0
-                score = 1
-            else:
-                score = all(user.owns(r[col]) for r in rset)
-        return score
-
-# Web request selectors ########################################################
+ExpectedValueSelector = class_renamed('ExpectedValueSelector',
+                                      ExpectedValuePredicate)
+EClassSelector = class_renamed('EClassSelector', EClassPredicate)
+EntitySelector = class_renamed('EntitySelector', EntityPredicate)
 
-# XXX deprecate
-@objectify_selector
-@lltrace
-def primary_view(cls, req, view=None, **kwargs):
-    """Return 1 if:
-
-    * *no view is specified* in the input context
-
-    * a view is specified and its `.is_primary()` method return True
-
-    This selector is usually used by contextual components that only want to
-    appears for the primary view of an entity.
-    """
-    if view is not None and not view.is_primary():
-        return 0
-    return 1
-
-
-@objectify_selector
-@lltrace
-def contextual(cls, req, view=None, **kwargs):
-    """Return 1 if view's contextual property is true"""
-    if view is not None and view.contextual:
-        return 1
-    return 0
-
-
-class match_view(ExpectedValueSelector):
-    """Return 1 if a view is specified an as its registry id is in one of the
-    expected view id given to the initializer.
-    """
-    @lltrace
-    def __call__(self, cls, req, view=None, **kwargs):
-        if view is None or not view.__regid__ in self.expected:
-            return 0
-        return 1
-
-
-class match_context(ExpectedValueSelector):
-
-    @lltrace
-    def __call__(self, cls, req, context=None, **kwargs):
-        if not context in self.expected:
-            return 0
-        return 1
-
-
-# XXX deprecate
-@objectify_selector
-@lltrace
-def match_context_prop(cls, req, context=None, **kwargs):
-    """Return 1 if:
-
-    * no `context` is specified in input context (take care to confusion, here
-      `context` refers to a string given as an argument to the input context...)
-
-    * specified `context` is matching the context property value for the
-      appobject using this selector
-
-    * the appobject's context property value is None
-
-    This selector is usually used by contextual components that want to appears
-    in a configurable place.
-    """
-    if context is None:
-        return 1
-    propval = req.property_value('%s.%s.context' % (cls.__registry__,
-                                                    cls.__regid__))
-    if propval and context != propval:
-        return 0
-    return 1
-
-
-class match_search_state(ExpectedValueSelector):
-    """Return 1 if the current request search state is in one of the expected
-    states given to the initializer.
-
-    Known search states are either 'normal' or 'linksearch' (eg searching for an
-    object to create a relation with another).
-
-    This selector is usually used by action that want to appears or not according
-    to the ui search state.
-    """
-
-    @lltrace
-    def __call__(self, cls, req, **kwargs):
-        try:
-            if not req.search_state[0] in self.expected:
-                return 0
-        except AttributeError:
-            return 1 # class doesn't care about search state, accept it
-        return 1
-
-
-class match_form_params(ExpectedValueSelector):
-    """Return non-zero score if parameter names specified as initializer
-    arguments are specified in request's form parameters.
-
-    Return a score corresponding to the number of expected parameters.
-
-    When multiple parameters are expected, all of them should be found in
-    the input context unless `mode` keyword argument is given to 'any',
-    in which case a single matching parameter is enough.
-    """
-
-    def _values_set(self, cls, req, **kwargs):
-        return frozenset(req.form)
-
-
-class match_edited_type(ExpectedValueSelector):
-    """return non-zero if main edited entity type is the one specified as
-    initializer argument, or is among initializer arguments if `mode` == 'any'.
-    """
-
-    def _values_set(self, cls, req, **kwargs):
-        try:
-            return frozenset((req.form['__type:%s' % req.form['__maineid']],))
-        except KeyError:
-            return frozenset()
-
-
-class match_form_id(ExpectedValueSelector):
-    """return non-zero if request form identifier is the one specified as
-    initializer argument, or is among initializer arguments if `mode` == 'any'.
-    """
-
-    def _values_set(self, cls, req, **kwargs):
-        try:
-            return frozenset((req.form['__form_id'],))
-        except KeyError:
-            return frozenset()
-
-
-class specified_etype_implements(is_instance):
-    """Return non-zero score if the entity type specified by an 'etype' key
-    searched in (by priority) input context kwargs and request form parameters
-    match a known entity type (case insensitivly), and it's associated entity
-    class is of one of the type(s) given to the initializer. If multiple
-    arguments are given, matching one of them is enough.
-
-    .. note:: as with :class:`~cubicweb.selectors.is_instance`, entity types
-              should be given as string and the score will reflect class
-              proximity so the most specific object will be selected.
-
-    This selector is usually used by views holding entity creation forms (since
-    we've no result set to work on).
-    """
-
-    @lltrace
-    def __call__(self, cls, req, **kwargs):
-        try:
-            etype = kwargs['etype']
-        except KeyError:
-            try:
-                etype = req.form['etype']
-            except KeyError:
-                return 0
-            else:
-                # only check this is a known type if etype comes from req.form,
-                # else we want the error to propagate
-                try:
-                    etype = req.vreg.case_insensitive_etypes[etype.lower()]
-                    req.form['etype'] = etype
-                except KeyError:
-                    return 0
-        score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
-        if score:
-            eschema = req.vreg.schema.eschema(etype)
-            if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
-                return score
-        return 0
-
-
-class attribute_edited(EntitySelector):
-    """Scores if the specified attribute has been edited This is useful for
-    selection of forms by the edit controller.
-
-    The initial use case is on a form, in conjunction with match_transition,
-    which will not score at edit time::
-
-     is_instance('Version') & (match_transition('ready') |
-                               attribute_edited('publication_date'))
-    """
-    def __init__(self, attribute, once_is_enough=None, mode='all'):
-        super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
-        self._attribute = attribute
-
-    def score_entity(self, entity):
-        return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
-
-
-# Other selectors ##############################################################
-
-class match_exception(ExpectedValueSelector):
-    """Return 1 if exception given as `exc` in the input context is an instance
-    of one of the class given on instanciation of this predicate.
-    """
-    def __init__(self, *expected):
-        assert expected, self
-        # we want a tuple, not a set as done in the parent class
-        self.expected = expected
-
-    @lltrace
-    def __call__(self, cls, req, exc=None, **kwargs):
-        if exc is not None and isinstance(exc, self.expected):
-            return 1
-        return 0
-
-
-@objectify_selector
-def debug_mode(cls, req, rset=None, **kwargs):
-    """Return 1 if running in debug mode."""
-    return req.vreg.config.debugmode and 1 or 0
-
-
-## deprecated stuff ############################################################
+# XXX pre 3.7? bw compat
 
 
 class on_transition(is_in_state):
@@ -1620,17 +44,17 @@
     Especially useful to match passed transition to enable notifications when
     your workflow allows several transition to the same states.
 
-    Note that if workflow `change_state` adapter method is used, this selector
+    Note that if workflow `change_state` adapter method is used, this predicate
     will not be triggered.
 
-    You should use this instead of your own :class:`score_entity` selector to
+    You should use this instead of your own :class:`score_entity` predicate to
     avoid some gotchas:
 
     * possible views gives a fake entity with no state
     * you must use the latest tr info thru the workflow adapter for repository
       side checking of the current state
 
-    In debug mode, this selector can raise:
+    In debug mode, this predicate can raise:
     :raises: :exc:`ValueError` for unknown transition names
         (etype workflow only not checked in custom workflow)
 
@@ -1654,12 +78,13 @@
             raise ValueError("%s: unknown transition(s): %s"
                              % (wf.name, ",".join(unknown)))
 
+
 entity_implements = class_renamed('entity_implements', is_instance)
 
-class _but_etype(EntitySelector):
+class _but_etype(EntityPredicate):
     """accept if the given entity types are not found in the result set.
 
-    See `EntitySelector` documentation for behaviour when row is not specified.
+    See `EntityPredicate` documentation for behaviour when row is not specified.
 
     :param *etypes: entity types (`basestring`) which should be refused
     """
@@ -1674,8 +99,7 @@
 
 but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
 
-
-# XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
+# XXX deprecated the one_* variants of predicates below w/ multi_xxx(nb=1)?
 #     take care at the implementation though (looking for the 'row' argument's
 #     value)
 two_lines_rset = class_renamed('two_lines_rset', multi_lines_rset)
--- a/server/__init__.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/__init__.py	Thu Feb 23 19:06:14 2012 +0100
@@ -271,7 +271,8 @@
 
 # available sources registry
 SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'),
-                'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
+                'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
+                'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'),
                 'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'),
-                'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
+                'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
                 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/cwzmq.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+from threading import Thread
+import zmq
+from zmq.eventloop import ioloop
+import zmq.eventloop.zmqstream
+
+from logging import getLogger
+from cubicweb import set_log_methods
+
+ctx = zmq.Context()
+
+class ZMQComm(object):
+    def __init__(self):
+        self.ioloop = ioloop.IOLoop()
+        self._topics = {}
+        self._subscribers = []
+        self.publisher = None
+
+    def add_publisher(self, address):
+        assert self.publisher is None, "more than one publisher is not supported"
+        self.publisher = Publisher(self.ioloop, address)
+
+    def add_subscription(self, topic, callback):
+        for subscriber in self._subscribers:
+            subscriber.subscribe(topic, callback)
+        self._topics[topic] = callback
+
+    def add_subscriber(self, address):
+        subscriber = Subscriber(self.ioloop, address)
+        for topic, callback in self._topics.iteritems():
+            subscriber.subscribe(topic, callback)
+        self._subscribers.append(subscriber)
+
+    def publish(self, msg):
+        assert self.publisher is not None, "can't publish without a publisher"
+        self.publisher.send(msg)
+
+    def start(self):
+        Thread(target=self.ioloop.start).start()
+
+    def stop(self):
+        self.ioloop.add_callback(self.ioloop.stop)
+
+    def __del__(self):
+        self.ioloop.close()
+
+
+class Publisher(object):
+    def __init__(self, ioloop, address):
+        self.address = address
+        self._topics = {}
+        self._subscribers = []
+        self.ioloop = ioloop
+        def callback():
+            s = ctx.socket(zmq.PUB)
+            self.stream = zmq.eventloop.zmqstream.ZMQStream(s, io_loop=ioloop)
+            self.stream.bind(self.address)
+            self.debug('start publisher on %s', self.address)
+        ioloop.add_callback(callback)
+
+    def send(self, msg):
+        self.ioloop.add_callback(lambda:self.stream.send_multipart(msg))
+
+
+class Subscriber(object):
+    def __init__(self, ioloop, address):
+        self.address = address
+        self.dispatch_table = {}
+        self.ioloop = ioloop
+        def callback():
+            s = ctx.socket(zmq.SUB)
+            self.stream = zmq.eventloop.zmqstream.ZMQStream(s, io_loop=ioloop)
+            self.stream.on_recv(self.dispatch)
+            self.stream.connect(self.address)
+            self.debug('start subscriber on %s', self.address)
+        ioloop.add_callback(callback)
+
+    def dispatch(self, msg):
+        try:
+            f = self.dispatch_table[msg[0]]
+        except KeyError:
+            return
+        f(msg)
+
+    def subscribe(self, topic, callback):
+        self.dispatch_table[topic] = callback
+        self.ioloop.add_callback(lambda: self.stream.setsockopt(zmq.SUBSCRIBE, topic))
+
+
+set_log_methods(Publisher, getLogger('cubicweb.zmq.pub'))
+set_log_methods(Subscriber, getLogger('cubicweb.zmq.sub'))
--- a/server/hook.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/hook.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -233,7 +233,7 @@
 or rollback() will restore the hooks.
 
 
-Hooks specific selector
+Hooks specific predicate
 ~~~~~~~~~~~~~~~~~~~~~~~
 .. autoclass:: cubicweb.server.hook.match_rtype
 .. autoclass:: cubicweb.server.hook.match_rtype_sets
@@ -258,13 +258,13 @@
 from logilab.common.decorators import classproperty, cached
 from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.logging_ext import set_log_methods
+from logilab.common.registry import (Predicate, NotPredicate, OrPredicate,
+                                     classid, objectify_predicate, yes)
 
 from cubicweb import RegistryNotFound
-from cubicweb.vregistry import classid
-from cubicweb.cwvreg import CWRegistry, VRegistry
-from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
-                                is_instance)
-from cubicweb.appobject import AppObject, NotSelector, OrSelector
+from cubicweb.cwvreg import CWRegistry, CWRegistryStore
+from cubicweb.predicates import ExpectedValuePredicate, is_instance
+from cubicweb.appobject import AppObject
 from cubicweb.server.session import security_enabled
 
 ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
@@ -338,14 +338,15 @@
         pruned hooks are the one which:
 
         * are disabled at the session level
-        * have a match_rtype or an is_instance selector which does not
-          match the rtype / etype of the relations / entities for
-          which we are calling the hooks. This works because the
-          repository calls the hooks grouped by rtype or by etype when
-          using the entities or eids_to_from keyword arguments
 
-        Only hooks with a simple selector or an AndSelector of simple
-        selectors are considered for disabling.
+        * have a selector containing a :class:`match_rtype` or an
+          :class:`is_instance` predicate which does not match the rtype / etype
+          of the relations / entities for which we are calling the hooks. This
+          works because the repository calls the hooks grouped by rtype or by
+          etype when using the entities or eids_to_from keyword arguments
+
+        Only hooks with a simple predicate or an AndPredicate of simple
+        predicates are considered for disabling.
 
         """
         if 'entity' in kwargs:
@@ -410,24 +411,22 @@
 
 
 for event in ALL_HOOKS:
-    VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
+    CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
 
 @deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)')
 def entity_oldnewvalue(entity, attr):
     return entity.cw_edited.oldnewvalue(attr)
 
 
-# some hook specific selectors #################################################
+# some hook specific predicates #################################################
 
-@objectify_selector
-@lltrace
+@objectify_predicate
 def enabled_category(cls, req, **kwargs):
     if req is None:
         return True # XXX how to deactivate server startup / shutdown event
     return req.is_hook_activated(cls)
 
-@objectify_selector
-@lltrace
+@objectify_predicate
 def from_dbapi_query(cls, req, **kwargs):
     if req.running_dbapi_query:
         return 1
@@ -440,9 +439,9 @@
         return iter(chain(*self.iterators))
 
 
-class match_rtype(ExpectedValueSelector):
+class match_rtype(ExpectedValuePredicate):
     """accept if parameters specified as initializer arguments are specified
-    in named arguments given to the selector
+    in named arguments given to the predicate
 
     :param \*expected: parameters (eg `basestring`) which are expected to be
                        found in named arguments (kwargs)
@@ -453,7 +452,6 @@
         self.toetypes = more.pop('toetypes', None)
         assert not more, "unexpected kwargs in match_rtype: %s" % more
 
-    @lltrace
     def __call__(self, cls, req, *args, **kwargs):
         if kwargs.get('rtype') not in self.expected:
             return 0
@@ -466,10 +464,10 @@
         return 1
 
 
-class match_rtype_sets(ExpectedValueSelector):
+class match_rtype_sets(ExpectedValuePredicate):
     """accept if the relation type is in one of the sets given as initializer
-    argument. The goal of this selector is that it keeps reference to original sets,
-    so modification to thoses sets are considered by the selector. For instance
+    argument. The goal of this predicate is that it keeps reference to original sets,
+    so modification to thoses sets are considered by the predicate. For instance
 
     MYSET = set()
 
@@ -489,7 +487,6 @@
     def __init__(self, *expected):
         self.expected = expected
 
-    @lltrace
     def __call__(self, cls, req, *args, **kwargs):
         for rel_set in self.expected:
             if kwargs.get('rtype') in rel_set:
@@ -535,7 +532,7 @@
     @cached
     def filterable_selectors(cls):
         search = cls.__select__.search_selector
-        if search((NotSelector, OrSelector)):
+        if search((NotPredicate, OrPredicate)):
             return None, None
         enabled_cat = search(enabled_category)
         main_filter = search((is_instance, match_rtype))
@@ -583,7 +580,7 @@
     Notice there are no default behaviour defined when a watched relation is
     deleted, you'll have to handle this by yourself.
 
-    You usually want to use the :class:`match_rtype_sets` selector on concrete
+    You usually want to use the :class:`match_rtype_sets` predicate on concrete
     classes.
     """
     events = ('after_add_relation',)
@@ -1067,6 +1064,8 @@
         remove inserted eid from repository type/source cache
         """
         try:
-            self.session.repo.clear_caches(self.get_data())
+            eids = self.get_data()
+            self.session.repo.clear_caches(eids)
+            self.session.repo.app_instances_bus.publish(['delete'] + list(str(eid) for eid in eids))
         except KeyError:
             pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/ldaputils.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,362 @@
+# 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/>.
+"""cubicweb utilities for ldap sources
+
+Part of the code is coming form Zope's LDAPUserFolder
+
+Copyright (c) 2004 Jens Vagelpohl.
+All Rights Reserved.
+
+This software is subject to the provisions of the Zope Public License,
+Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+FOR A PARTICULAR PURPOSE.
+"""
+
+from __future__ import division # XXX why?
+
+import ldap
+from ldap.ldapobject import ReconnectLDAPObject
+from ldap.filter import filter_format
+from ldapurl import LDAPUrl
+
+from cubicweb import ValidationError, AuthenticationError
+from cubicweb.server.sources import ConnectionWrapper
+
+_ = unicode
+
+# 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,
+              }
+
+
+class LDAPSourceMixIn(object):
+    """a mix-in for LDAP based source"""
+    options = (
+        ('auth-mode',
+         {'type' : 'choice',
+          'default': 'simple',
+          'choices': ('simple', 'cram_md5', 'digest_md5', 'gssapi'),
+          'help': 'authentication mode used to authenticate user to the ldap.',
+          'group': 'ldap-source', 'level': 3,
+          }),
+        ('auth-realm',
+         {'type' : 'string',
+          'default': None,
+          'help': 'realm to use when using gssapi/kerberos authentication.',
+          'group': 'ldap-source', 'level': 3,
+          }),
+
+        ('data-cnx-dn',
+         {'type' : 'string',
+          'default': '',
+          'help': 'user dn to use to open data connection to the ldap (eg used \
+to respond to rql queries). Leave empty for anonymous bind',
+          'group': 'ldap-source', 'level': 1,
+          }),
+        ('data-cnx-password',
+         {'type' : 'string',
+          'default': '',
+          'help': 'password to use to open data connection to the ldap (eg used to respond to rql queries). Leave empty for anonymous bind.',
+          'group': 'ldap-source', 'level': 1,
+          }),
+
+        ('user-base-dn',
+         {'type' : 'string',
+          'default': 'ou=People,dc=logilab,dc=fr',
+          'help': 'base DN to lookup for users',
+          'group': 'ldap-source', 'level': 1,
+          }),
+        ('user-scope',
+         {'type' : 'choice',
+          'default': 'ONELEVEL',
+          'choices': ('BASE', 'ONELEVEL', 'SUBTREE'),
+          'help': 'user search scope (valid values: "BASE", "ONELEVEL", "SUBTREE")',
+          'group': 'ldap-source', 'level': 1,
+          }),
+        ('user-classes',
+         {'type' : 'csv',
+          'default': ('top', 'posixAccount'),
+          'help': 'classes of user (with Active Directory, you want to say "user" here)',
+          'group': 'ldap-source', 'level': 1,
+          }),
+        ('user-filter',
+         {'type': 'string',
+          'default': '',
+          'help': 'additional filters to be set in the ldap query to find valid users',
+          'group': 'ldap-source', 'level': 2,
+          }),
+        ('user-login-attr',
+         {'type' : 'string',
+          'default': 'uid',
+          'help': 'attribute used as login on authentication (with Active Directory, you want to use "sAMAccountName" here)',
+          'group': 'ldap-source', 'level': 1,
+          }),
+        ('user-default-group',
+         {'type' : 'csv',
+          'default': ('users',),
+          'help': 'name of a group in which ldap users will be by default. \
+You can set multiple groups by separating them by a comma.',
+          'group': 'ldap-source', 'level': 1,
+          }),
+        ('user-attrs-map',
+         {'type' : 'named',
+          'default': {'uid': 'login', 'gecos': 'email'},
+          'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
+          'group': 'ldap-source', 'level': 1,
+          }),
+
+    )
+
+    _conn = None
+
+    def _entity_update(self, source_entity):
+        if self.urls:
+            if len(self.urls) > 1:
+                raise ValidationError(source_entity, {'url': _('can only have one url')})
+            try:
+                protocol, hostport = self.urls[0].split('://')
+            except ValueError:
+                raise ValidationError(source_entity, {'url': _('badly formatted url')})
+            if protocol not in PROTO_PORT:
+                raise ValidationError(source_entity, {'url': _('unsupported protocol')})
+
+    def update_config(self, source_entity, typedconfig):
+        """update configuration from source entity. `typedconfig` is config
+        properly typed with defaults set
+        """
+        self.authmode = typedconfig['auth-mode']
+        self._authenticate = getattr(self, '_auth_%s' % self.authmode)
+        self.cnx_dn = typedconfig['data-cnx-dn']
+        self.cnx_pwd = typedconfig['data-cnx-password']
+        self.user_base_dn = str(typedconfig['user-base-dn'])
+        self.user_base_scope = globals()[typedconfig['user-scope']]
+        self.user_login_attr = typedconfig['user-login-attr']
+        self.user_default_groups = typedconfig['user-default-group']
+        self.user_attrs = typedconfig['user-attrs-map']
+        self.user_rev_attrs = {'eid': 'dn'}
+        for ldapattr, cwattr in self.user_attrs.items():
+            self.user_rev_attrs[cwattr] = ldapattr
+        self.base_filters = [filter_format('(%s=%s)', ('objectClass', o))
+                             for o in typedconfig['user-classes']]
+        if typedconfig['user-filter']:
+            self.base_filters.append(typedconfig['user-filter'])
+        self._conn = None
+
+    def connection_info(self):
+        assert len(self.urls) == 1, self.urls
+        protocol, hostport = self.urls[0].split('://')
+        if protocol != 'ldapi' and not ':' in hostport:
+            hostport = '%s:%s' % (hostport, PROTO_PORT[protocol])
+        return protocol, hostport
+
+    def get_connection(self):
+        """open and return a connection to the source"""
+        if self._conn is None:
+            try:
+                self._connect()
+            except Exception:
+                self.exception('unable to connect to ldap')
+        return ConnectionWrapper(self._conn)
+
+    def authenticate(self, session, login, password=None, **kwargs):
+        """return CWUser eid for the given login/password if this account is
+        defined in this source, else raise `AuthenticationError`
+
+        two queries are needed since passwords are stored crypted, so we have
+        to fetch the salt first
+        """
+        self.info('ldap authenticate %s', login)
+        if not password:
+            # On Windows + ADAM this would have succeeded (!!!)
+            # You get Authenticated as: 'NT AUTHORITY\ANONYMOUS LOGON'.
+            # we really really don't want that
+            raise AuthenticationError()
+        searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))]
+        searchfilter.extend(self.base_filters)
+        searchstr = '(&%s)' % ''.join(searchfilter)
+        # first search the user
+        try:
+            user = self._search(session, self.user_base_dn,
+                                self.user_base_scope, searchstr)[0]
+        except IndexError:
+            # no such user
+            raise AuthenticationError()
+        # check password by establishing a (unused) connection
+        try:
+            self._connect(user, password)
+        except ldap.LDAPError, ex:
+            # Something went wrong, most likely bad credentials
+            self.info('while trying to authenticate %s: %s', user, ex)
+            raise AuthenticationError()
+        except Exception:
+            self.error('while trying to authenticate %s', user, exc_info=True)
+            raise AuthenticationError()
+        eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session)
+        if eid < 0:
+            # user has been moved away from this source
+            raise AuthenticationError()
+        return eid
+
+    def object_exists_in_ldap(self, dn):
+        cnx = self.get_connection().cnx #session.cnxset.connection(self.uri).cnx
+        if cnx is None:
+            return True # ldap unreachable, suppose it exists
+        try:
+            cnx.search_s(dn, self.user_base_scope)
+        except ldap.PARTIAL_RESULTS:
+            pass
+        except ldap.NO_SUCH_OBJECT:
+            return False
+        return True
+
+    def _connect(self, user=None, userpwd=None):
+        protocol, hostport = self.connection_info()
+        self.info('connecting %s://%s as %s', protocol, hostport,
+                  user and user['dn'] or 'anonymous')
+        # don't require server certificate when using ldaps (will
+        # enable self signed certs)
+        ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+        url = LDAPUrl(urlscheme=protocol, hostport=hostport)
+        conn = ReconnectLDAPObject(url.initializeUrl())
+        # Set the protocol version - version 3 is preferred
+        try:
+            conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
+        except ldap.LDAPError: # Invalid protocol version, fall back safely
+            conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
+        # Deny auto-chasing of referrals to be safe, we handle them instead
+        #try:
+        #    connection.set_option(ldap.OPT_REFERRALS, 0)
+        #except ldap.LDAPError: # Cannot set referrals, so do nothing
+        #    pass
+        #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
+        #conn.timeout = op_timeout
+        # Now bind with the credentials given. Let exceptions propagate out.
+        if user is None:
+            # no user specified, we want to initialize the 'data' connection,
+            assert self._conn is None
+            self._conn = conn
+            # XXX always use simple bind for data connection
+            if not self.cnx_dn:
+                conn.simple_bind_s(self.cnx_dn, self.cnx_pwd)
+            else:
+                self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd)
+        else:
+            # user specified, we want to check user/password, no need to return
+            # the connection which will be thrown out
+            self._authenticate(conn, user, userpwd)
+        return conn
+
+    def _auth_simple(self, conn, user, userpwd):
+        conn.simple_bind_s(user['dn'], userpwd)
+
+    def _auth_cram_md5(self, conn, user, userpwd):
+        from ldap import sasl
+        auth_token = sasl.cram_md5(user['dn'], userpwd)
+        conn.sasl_interactive_bind_s('', auth_token)
+
+    def _auth_digest_md5(self, conn, user, userpwd):
+        from ldap import sasl
+        auth_token = sasl.digest_md5(user['dn'], userpwd)
+        conn.sasl_interactive_bind_s('', auth_token)
+
+    def _auth_gssapi(self, conn, user, userpwd):
+        # print XXX not proper sasl/gssapi
+        import kerberos
+        if not kerberos.checkPassword(user[self.user_login_attr], userpwd):
+            raise Exception('BAD login / mdp')
+        #from ldap import sasl
+        #conn.sasl_interactive_bind_s('', sasl.gssapi())
+
+    def _search(self, session, base, scope,
+                searchstr='(objectClass=*)', attrs=()):
+        """make an ldap query"""
+        self.debug('ldap search %s %s %s %s %s', self.uri, base, scope,
+                   searchstr, list(attrs))
+        # XXX for now, we do not have connections set support for LDAP, so
+        # this is always self._conn
+        cnx = self.get_connection().cnx #session.cnxset.connection(self.uri).cnx
+        if cnx is None:
+            # 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 []
+        try:
+            res = cnx.search_s(base, scope, searchstr, attrs)
+        except ldap.PARTIAL_RESULTS:
+            res = cnx.result(all=0)[1]
+        except ldap.NO_SUCH_OBJECT:
+            self.info('ldap NO SUCH OBJECT %s %s %s', base, scope, searchstr)
+            self._process_no_such_object(session, base)
+            return []
+        # except ldap.REFERRAL, e:
+        #     cnx = self.handle_referral(e)
+        #     try:
+        #         res = cnx.search_s(base, scope, searchstr, attrs)
+        #     except ldap.PARTIAL_RESULTS:
+        #         res_type, res = cnx.result(all=0)
+        result = []
+        for rec_dn, rec_dict in res:
+            # When used against Active Directory, "rec_dict" may not be
+            # be a dictionary in some cases (instead, it can be a list)
+            #
+            # An example of a useless "res" entry that can be ignored
+            # from AD is
+            # (None, ['ldap://ForestDnsZones.PORTAL.LOCAL/DC=ForestDnsZones,DC=PORTAL,DC=LOCAL'])
+            # This appears to be some sort of internal referral, but
+            # we can't handle it, so we need to skip over it.
+            try:
+                items = rec_dict.iteritems()
+            except AttributeError:
+                continue
+            else:
+                itemdict = self._process_ldap_item(rec_dn, items)
+                result.append(itemdict)
+        #print '--->', result
+        self.debug('ldap built results %s', len(result))
+        return result
+
+    def _process_ldap_item(self, dn, iterator):
+        """Turn an ldap received item into a proper dict."""
+        itemdict = {'dn': dn}
+        for key, value in iterator:
+            if not isinstance(value, str):
+                try:
+                    for i in range(len(value)):
+                        value[i] = unicode(value[i], 'utf8')
+                except Exception:
+                    pass
+            if isinstance(value, list) and len(value) == 1:
+                itemdict[key] = value = value[0]
+        return itemdict
+
+    def _process_no_such_object(self, session, dn):
+        """Some search return NO_SUCH_OBJECT error, handle this (usually because
+        an object whose dn is no more existent in ldap as been encountered).
+
+        Do nothing by default, let sub-classes handle that.
+        """
--- a/server/migractions.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/migractions.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -51,14 +51,13 @@
 from yams.schema import RelationDefinitionSchema
 
 from cubicweb import CW_SOFTWARE_ROOT, AuthenticationError, ExecutionError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES,
                              PURE_VIRTUAL_RTYPES,
                              CubicWebRelationSchema, order_eschemas)
 from cubicweb.cwvreg import CW_EVENT_MANAGER
 from cubicweb.dbapi import get_repository, repo_connect
 from cubicweb.migration import MigrationHelper, yes
-from cubicweb.server.session import hooks_control
 from cubicweb.server import hook
 try:
     from cubicweb.server import SOURCE_TYPES, schemaserial as ss
@@ -152,7 +151,7 @@
             elif options.backup_db:
                 self.backup_database(askconfirm=False)
         # disable notification during migration
-        with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, 'notification'):
+        with self.session.allow_all_hooks_but('notification'):
             super(ServerMigrationHelper, self).migrate(vcconf, toupgrade, options)
 
     def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
@@ -376,6 +375,9 @@
             self.confirm = yes
             self.execscript_confirm = yes
             try:
+                if event == 'postcreate':
+                    with self.session.allow_all_hooks_but():
+                        return self.cmd_process_script(apc, funcname, *args, **kwargs)
                 return self.cmd_process_script(apc, funcname, *args, **kwargs)
             finally:
                 self.confirm = confirm
@@ -698,8 +700,9 @@
                                                  str(totype))
         # execute post-create files
         for cube in reversed(newcubes):
-            self.cmd_exec_event_script('postcreate', cube)
-            self.commit()
+            with self.session.allow_all_hooks_but():
+                self.cmd_exec_event_script('postcreate', cube)
+                self.commit()
 
     def cmd_remove_cube(self, cube, removedeps=False):
         removedcubes = super(ServerMigrationHelper, self).cmd_remove_cube(
@@ -1468,7 +1471,7 @@
     def rqliter(self, rql, kwargs=None, ask_confirm=True):
         return ForRqlIterator(self, rql, kwargs, ask_confirm)
 
-    # broken db commands ######################################################
+    # low-level commands to repair broken system database ######################
 
     def cmd_change_attribute_type(self, etype, attr, newtype, commit=True):
         """low level method to change the type of an entity attribute. This is
--- a/server/mssteps.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/mssteps.py	Thu Feb 23 19:06:14 2012 +0100
@@ -159,7 +159,9 @@
         if self.outputtable:
             self.plan.create_temp_table(self.outputtable)
             sql = 'INSERT INTO %s %s' % (self.outputtable, sql)
-        return self.plan.sqlexec(sql, self.plan.args)
+            self.plan.syssource.doexec(self.plan.session, sql, self.plan.args)
+        else:
+            return self.plan.sqlexec(sql, self.plan.args)
 
     def get_sql(self):
         self.inputmap = inputmap = self.children[-1].outputmap
--- a/server/repository.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/repository.py	Thu Feb 23 19:06:14 2012 +0100
@@ -120,6 +120,20 @@
                             {'x': eidfrom, 'y': eidto})
 
 
+class NullEventBus(object):
+    def publish(self, msg):
+        pass
+
+    def add_subscription(self, topic, callback):
+        pass
+
+    def start(self):
+        pass
+
+    def stop(self):
+        pass
+
+
 class Repository(object):
     """a repository provides access to a set of persistent storages for
     entities and relations
@@ -130,10 +144,11 @@
     def __init__(self, config, vreg=None):
         self.config = config
         if vreg is None:
-            vreg = cwvreg.CubicWebVRegistry(config)
+            vreg = cwvreg.CWRegistryStore(config)
         self.vreg = vreg
         self.pyro_registered = False
         self.pyro_uri = None
+        self.app_instances_bus = NullEventBus()
         self.info('starting repository from %s', self.config.apphome)
         # dictionary of opened sessions
         self._sessions = {}
@@ -436,8 +451,10 @@
         """validate authentication, raise AuthenticationError on failure, return
         associated CWUser's eid on success.
         """
-        for source in self.sources:
-            if source.support_entity('CWUser'):
+        # iter on sources_by_uri then check enabled source since sources doesn't
+        # contain copy based sources
+        for source in self.sources_by_uri.itervalues():
+            if self.config.source_enabled(source) and source.support_entity('CWUser'):
                 try:
                     return source.authenticate(session, login, **authinfo)
                 except AuthenticationError:
--- a/server/serverconfig.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/serverconfig.py	Thu Feb 23 19:06:14 2012 +0100
@@ -207,6 +207,19 @@
 and if not set, it will be choosen randomly',
           'group': 'pyro', 'level': 3,
           }),
+
+         ('zmq-address-sub',
+          {'type' : 'csv',
+           'default' : None,
+           'help': ('List of ZMQ addresses to subscribe to (requires pyzmq)'),
+           'group': 'zmq', 'level': 1,
+           }),
+         ('zmq-address-pub',
+          {'type' : 'string',
+           'default' : None,
+           'help': ('ZMQ address to use for publishing (requires pyzmq)'),
+           'group': 'zmq', 'level': 1,
+           }),
         ) + CubicWebConfiguration.options)
 
     # should we init the connections pool (eg connect to sources). This is
--- a/server/session.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/session.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -29,12 +29,12 @@
 
 from logilab.common.deprecation import deprecated
 from logilab.common.textutils import unormalize
+from logilab.common.registry import objectify_predicate
 from rql import CoercionError
 from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
 from yams import BASE_TYPES
 
 from cubicweb import Binary, UnknownEid, QueryError, schema
-from cubicweb.selectors import objectify_selector
 from cubicweb.req import RequestSessionBase
 from cubicweb.dbapi import ConnectionProperties
 from cubicweb.utils import make_uid, RepeatList
@@ -74,23 +74,23 @@
             except CoercionError:
                 return None
 
-@objectify_selector
+@objectify_predicate
 def is_user_session(cls, req, **kwargs):
-    """repository side only selector returning 1 if the session is a regular
+    """repository side only predicate returning 1 if the session is a regular
     user session and not an internal session
     """
     return not req.is_internal_session
 
-@objectify_selector
+@objectify_predicate
 def is_internal_session(cls, req, **kwargs):
-    """repository side only selector returning 1 if the session is not a regular
+    """repository side only predicate returning 1 if the session is not a regular
     user session but an internal session
     """
     return req.is_internal_session
 
-@objectify_selector
+@objectify_predicate
 def repairing(cls, req, **kwargs):
-    """repository side only selector returning 1 if the session is not a regular
+    """repository side only predicate returning 1 if the session is not a regular
     user session but an internal session
     """
     return req.vreg.config.repairing
--- a/server/sources/datafeed.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/sources/datafeed.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,6 +22,7 @@
 
 import urllib2
 import StringIO
+from os.path import exists
 from datetime import datetime, timedelta
 from base64 import b64decode
 from cookielib import CookieJar
@@ -102,6 +103,7 @@
                          if url.strip()]
         else:
             self.urls = []
+
     def update_config(self, source_entity, typedconfig):
         """update configuration from source entity. `typedconfig` is config
         properly typed with defaults set
@@ -200,10 +202,11 @@
             self.warning("some error occured, don't attempt to delete entities")
         elif self.config['delete-entities'] and myuris:
             byetype = {}
-            for eid, etype in myuris.values():
-                byetype.setdefault(etype, []).append(str(eid))
-            self.error('delete %s entities %s', self.uri, byetype)
+            for extid, (eid, etype) in myuris.iteritems():
+                if parser.is_deleted(extid, etype, eid):
+                    byetype.setdefault(etype, []).append(str(eid))
             for etype, eids in byetype.iteritems():
+                self.warning('delete %s %s entities', len(eids), etype)
                 session.execute('DELETE %s X WHERE X eid IN (%s)'
                                 % (etype, ','.join(eids)))
         self.update_latest_retrieval(session)
@@ -276,6 +279,7 @@
         dataimport.init()
         return dataimport
 
+
 class DataFeedParser(AppObject):
     __registry__ = 'parsers'
 
@@ -287,6 +291,13 @@
         self.stats = {'created': set(),
                       'updated': set()}
 
+    def normalize_url(self, url):
+        from cubicweb.sobjects import URL_MAPPING # available after registration
+        for mappedurl in URL_MAPPING:
+            if url.startswith(mappedurl):
+                return url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
+        return url
+
     def add_schema_config(self, schemacfg, checkonly=False):
         """added CWSourceSchemaConfig, modify mapping accordingly"""
         msg = schemacfg._cw._("this parser doesn't use a mapping")
@@ -358,6 +369,25 @@
     def notify_updated(self, entity):
         return self.stats['updated'].add(entity.eid)
 
+    def is_deleted(self, extid, etype, eid):
+        """return True if the entity of given external id, entity type and eid
+        is actually deleted. Always return True by default, put more sensible
+        stuff in sub-classes.
+        """
+        return True
+
+    def update_if_necessary(self, entity, attrs):
+        self.notify_updated(entity)
+        entity.complete(tuple(attrs))
+        # check modification date and compare attribute values to only update
+        # what's actually needed
+        mdate = attrs.get('modification_date')
+        if not mdate or mdate > entity.modification_date:
+            attrs = dict( (k, v) for k, v in attrs.iteritems()
+                          if v != getattr(entity, k))
+            if attrs:
+                entity.set_attributes(**attrs)
+
 
 class DataFeedXMLParser(DataFeedParser):
 
@@ -393,11 +423,7 @@
 
     def parse(self, url):
         if url.startswith('http'):
-            from cubicweb.sobjects.parsers import URL_MAPPING
-            for mappedurl in URL_MAPPING:
-                if url.startswith(mappedurl):
-                    url = url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
-                    break
+            url = self.normalize_url(url)
             self.source.info('GET %s', url)
             stream = _OPENER.open(url)
         elif url.startswith('file://'):
@@ -412,6 +438,17 @@
     def process_item(self, *args):
         raise NotImplementedError
 
+    def is_deleted(self, extid, etype, eid):
+        if extid.startswith('http'):
+            try:
+                _OPENER.open(self.normalize_url(extid)) # XXX HTTP HEAD request
+            except urllib2.HTTPError, ex:
+                if ex.code == 404:
+                    return True
+        elif extid.startswith('file://'):
+            return exists(extid[7:])
+        return False
+
 # use a cookie enabled opener to use session cookie if any
 _OPENER = urllib2.build_opener()
 try:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/ldapfeed.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,46 @@
+# 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/>.
+"""cubicweb ldap feed source
+
+unlike ldapuser source, this source is copy based and will import ldap content
+(beside passwords for authentication) into the system source.
+"""
+
+from cubicweb.server.sources import datafeed
+from cubicweb.server import ldaputils
+
+
+class LDAPFeedSource(ldaputils.LDAPSourceMixIn,
+                     datafeed.DataFeedSource):
+    """LDAP feed source"""
+    support_entities = {'CWUser': False}
+    use_cwuri_as_url = True
+
+    options = datafeed.DataFeedSource.options + ldaputils.LDAPSourceMixIn.options
+
+    def update_config(self, source_entity, typedconfig):
+        """update configuration from source entity. `typedconfig` is config
+        properly typed with defaults set
+        """
+        datafeed.DataFeedSource.update_config(self, source_entity, typedconfig)
+        ldaputils.LDAPSourceMixIn.update_config(self, source_entity, typedconfig)
+
+    def _entity_update(self, source_entity):
+        datafeed.DataFeedSource._entity_update(self, source_entity)
+        ldaputils.LDAPSourceMixIn._entity_update(self, source_entity)
+
--- a/server/sources/ldapuser.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/sources/ldapuser.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,33 +18,20 @@
 """cubicweb ldap user source
 
 this source is for now limited to a read-only CWUser source
-
-Part of the code is coming form Zope's LDAPUserFolder
-
-Copyright (c) 2004 Jens Vagelpohl.
-All Rights Reserved.
-
-This software is subject to the provisions of the Zope Public License,
-Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-FOR A PARTICULAR PURPOSE.
 """
 from __future__ import division
 from base64 import b64decode
 
 import ldap
-from ldap.ldapobject import ReconnectLDAPObject
-from ldap.filter import filter_format, escape_filter_chars
-from ldapurl import LDAPUrl
+from ldap.filter import escape_filter_chars
 
 from rql.nodes import Relation, VariableRef, Constant, Function
 
-from cubicweb import AuthenticationError, UnknownEid, RepositoryError
+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,
-                                     ConnectionWrapper, TimedCache)
+                                     TimedCache)
 
 # search scopes
 BASE = ldap.SCOPE_BASE
@@ -58,97 +45,11 @@
               }
 
 
-class LDAPUserSource(AbstractSource):
+class LDAPUserSource(ldaputils.LDAPSourceMixIn, AbstractSource):
     """LDAP read-only CWUser source"""
     support_entities = {'CWUser': False}
 
-    options = (
-        ('host',
-         {'type' : 'string',
-          'default': 'ldap',
-          'help': 'ldap host. It may contains port information using \
-<host>:<port> notation.',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('protocol',
-         {'type' : 'choice',
-          'default': 'ldap',
-          'choices': ('ldap', 'ldaps', 'ldapi'),
-          'help': 'ldap protocol (allowed values: ldap, ldaps, ldapi)',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('auth-mode',
-         {'type' : 'choice',
-          'default': 'simple',
-          'choices': ('simple', 'cram_md5', 'digest_md5', 'gssapi'),
-          'help': 'authentication mode used to authenticate user to the ldap.',
-          'group': 'ldap-source', 'level': 3,
-          }),
-        ('auth-realm',
-         {'type' : 'string',
-          'default': None,
-          'help': 'realm to use when using gssapi/kerberos authentication.',
-          'group': 'ldap-source', 'level': 3,
-          }),
-
-        ('data-cnx-dn',
-         {'type' : 'string',
-          'default': '',
-          'help': 'user dn to use to open data connection to the ldap (eg used \
-to respond to rql queries). Leave empty for anonymous bind',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('data-cnx-password',
-         {'type' : 'string',
-          'default': '',
-          'help': 'password to use to open data connection to the ldap (eg used to respond to rql queries). Leave empty for anonymous bind.',
-          'group': 'ldap-source', 'level': 1,
-          }),
-
-        ('user-base-dn',
-         {'type' : 'string',
-          'default': 'ou=People,dc=logilab,dc=fr',
-          'help': 'base DN to lookup for users',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('user-scope',
-         {'type' : 'choice',
-          'default': 'ONELEVEL',
-          'choices': ('BASE', 'ONELEVEL', 'SUBTREE'),
-          'help': 'user search scope (valid values: "BASE", "ONELEVEL", "SUBTREE")',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('user-classes',
-         {'type' : 'csv',
-          'default': ('top', 'posixAccount'),
-          'help': 'classes of user (with Active Directory, you want to say "user" here)',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('user-filter',
-         {'type': 'string',
-          'default': '',
-          'help': 'additional filters to be set in the ldap query to find valid users',
-          'group': 'ldap-source', 'level': 2,
-          }),
-        ('user-login-attr',
-         {'type' : 'string',
-          'default': 'uid',
-          'help': 'attribute used as login on authentication (with Active Directory, you want to use "sAMAccountName" here)',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('user-default-group',
-         {'type' : 'csv',
-          'default': ('users',),
-          'help': 'name of a group in which ldap users will be by default. \
-You can set multiple groups by separating them by a comma.',
-          'group': 'ldap-source', 'level': 1,
-          }),
-        ('user-attrs-map',
-         {'type' : 'named',
-          'default': {'uid': 'login', 'gecos': 'email'},
-          'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
-          'group': 'ldap-source', 'level': 1,
-          }),
+    options = ldaputils.LDAPSourceMixIn.options + (
 
         ('synchronization-interval',
          {'type' : 'time',
@@ -168,35 +69,32 @@
 
     def __init__(self, repo, source_config, eid=None):
         AbstractSource.__init__(self, repo, source_config, eid)
-        self.update_config(None, self.check_conf_dict(eid, source_config))
-        self._conn = None
+        self.update_config(None, self.check_conf_dict(eid, source_config,
+                                                      fail_if_unknown=False))
+
+    def _entity_update(self, source_entity):
+        # XXX copy from datafeed source
+        if source_entity.url:
+            self.urls = [url.strip() for url in source_entity.url.splitlines()
+                         if url.strip()]
+        else:
+            self.urls = []
+        # /end XXX
+        ldaputils.LDAPSourceMixIn._entity_update(self, source_entity)
 
     def update_config(self, source_entity, typedconfig):
         """update configuration from source entity. `typedconfig` is config
         properly typed with defaults set
         """
-        self.host = typedconfig['host']
-        self.protocol = typedconfig['protocol']
-        self.authmode = typedconfig['auth-mode']
-        self._authenticate = getattr(self, '_auth_%s' % self.authmode)
-        self.cnx_dn = typedconfig['data-cnx-dn']
-        self.cnx_pwd = typedconfig['data-cnx-password']
-        self.user_base_dn = str(typedconfig['user-base-dn'])
-        self.user_base_scope = globals()[typedconfig['user-scope']]
-        self.user_login_attr = typedconfig['user-login-attr']
-        self.user_default_groups = typedconfig['user-default-group']
-        self.user_attrs = typedconfig['user-attrs-map']
-        self.user_rev_attrs = {'eid': 'dn'}
-        for ldapattr, cwattr in self.user_attrs.items():
-            self.user_rev_attrs[cwattr] = ldapattr
-        self.base_filters = [filter_format('(%s=%s)', ('objectClass', o))
-                             for o in typedconfig['user-classes']]
-        if typedconfig['user-filter']:
-            self.base_filters.append(typedconfig['user-filter'])
+        ldaputils.LDAPSourceMixIn.update_config(self, source_entity, typedconfig)
         self._interval = typedconfig['synchronization-interval']
         self._cache_ttl = max(71, typedconfig['cache-life-time'])
         self.reset_caches()
-        self._conn = None
+        # 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"""
@@ -207,21 +105,24 @@
         """method called by the repository once ready to handle request"""
         if activated:
             self.info('ldap init')
+            self._entity_update(source_entity)
             # set minimum period of 5min 1s (the additional second is to
             # minimize resonnance effet)
-            self.repo.looping_task(max(301, self._interval), self.synchronize)
+            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):
+        self.pull_data(self.repo.internal_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)
-        try:
-            ldap_emailattr = self.user_rev_attrs['email']
-        except KeyError:
-            return # no email in ldap, we're done
+        ldap_emailattr = self.user_rev_attrs['email']
+        assert ldap_emailattr
         session = self.repo.internal_session()
         execute = session.execute
         try:
@@ -268,54 +169,6 @@
             session.commit()
             session.close()
 
-    def get_connection(self):
-        """open and return a connection to the source"""
-        if self._conn is None:
-            try:
-                self._connect()
-            except Exception:
-                self.exception('unable to connect to ldap:')
-        return ConnectionWrapper(self._conn)
-
-    def authenticate(self, session, login, password=None, **kwargs):
-        """return CWUser eid for the given login/password if this account is
-        defined in this source, else raise `AuthenticationError`
-
-        two queries are needed since passwords are stored crypted, so we have
-        to fetch the salt first
-        """
-        self.info('ldap authenticate %s', login)
-        if not password:
-            # On Windows + ADAM this would have succeeded (!!!)
-            # You get Authenticated as: 'NT AUTHORITY\ANONYMOUS LOGON'.
-            # we really really don't want that
-            raise AuthenticationError()
-        searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))]
-        searchfilter.extend(self.base_filters)
-        searchstr = '(&%s)' % ''.join(searchfilter)
-        # first search the user
-        try:
-            user = self._search(session, self.user_base_dn,
-                                self.user_base_scope, searchstr)[0]
-        except IndexError:
-            # no such user
-            raise AuthenticationError()
-        # check password by establishing a (unused) connection
-        try:
-            self._connect(user, password)
-        except ldap.LDAPError, ex:
-            # Something went wrong, most likely bad credentials
-            self.info('while trying to authenticate %s: %s', user, ex)
-            raise AuthenticationError()
-        except Exception:
-            self.error('while trying to authenticate %s', user, exc_info=True)
-            raise AuthenticationError()
-        eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session)
-        if eid < 0:
-            # user has been moved away from this source
-            raise AuthenticationError()
-        return eid
-
     def ldap_name(self, var):
         if var.stinfo['relations']:
             relname = iter(var.stinfo['relations']).next().r_type
@@ -383,7 +236,7 @@
             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)
+                session.set_shared_data('sources_error', msg % self.uri, txdata=True)
                 return []
         return results
 
@@ -459,127 +312,18 @@
         #print '--> ldap result', result
         return result
 
-
-    def _connect(self, user=None, userpwd=None):
-        if self.protocol == 'ldapi':
-            hostport = self.host
-        elif not ':' in self.host:
-            hostport = '%s:%s' % (self.host, PROTO_PORT[self.protocol])
-        else:
-            hostport = self.host
-        self.info('connecting %s://%s as %s', self.protocol, hostport,
-                  user and user['dn'] or 'anonymous')
-        # don't require server certificate when using ldaps (will
-        # enable self signed certs)
-        ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
-        url = LDAPUrl(urlscheme=self.protocol, hostport=hostport)
-        conn = ReconnectLDAPObject(url.initializeUrl())
-        # Set the protocol version - version 3 is preferred
-        try:
-            conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
-        except ldap.LDAPError: # Invalid protocol version, fall back safely
-            conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
-        # Deny auto-chasing of referrals to be safe, we handle them instead
-        #try:
-        #    connection.set_option(ldap.OPT_REFERRALS, 0)
-        #except ldap.LDAPError: # Cannot set referrals, so do nothing
-        #    pass
-        #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
-        #conn.timeout = op_timeout
-        # Now bind with the credentials given. Let exceptions propagate out.
-        if user is None:
-            # no user specified, we want to initialize the 'data' connection,
-            assert self._conn is None
-            self._conn = conn
-            # XXX always use simple bind for data connection
-            if not self.cnx_dn:
-                conn.simple_bind_s(self.cnx_dn, self.cnx_pwd)
-            else:
-                self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd)
-        else:
-            # user specified, we want to check user/password, no need to return
-            # the connection which will be thrown out
-            self._authenticate(conn, user, userpwd)
-        return conn
-
-    def _auth_simple(self, conn, user, userpwd):
-        conn.simple_bind_s(user['dn'], userpwd)
-
-    def _auth_cram_md5(self, conn, user, userpwd):
-        from ldap import sasl
-        auth_token = sasl.cram_md5(user['dn'], userpwd)
-        conn.sasl_interactive_bind_s('', auth_token)
-
-    def _auth_digest_md5(self, conn, user, userpwd):
-        from ldap import sasl
-        auth_token = sasl.digest_md5(user['dn'], userpwd)
-        conn.sasl_interactive_bind_s('', auth_token)
+    def _process_ldap_item(self, dn, iterator):
+        itemdict = super(LDAPUserSource, self)._process_ldap_item(dn, iterator)
+        self._cache[dn] = itemdict
+        return itemdict
 
-    def _auth_gssapi(self, conn, user, userpwd):
-        # print XXX not proper sasl/gssapi
-        import kerberos
-        if not kerberos.checkPassword(user[self.user_login_attr], userpwd):
-            raise Exception('BAD login / mdp')
-        #from ldap import sasl
-        #conn.sasl_interactive_bind_s('', sasl.gssapi())
-
-    def _search(self, session, base, scope,
-                searchstr='(objectClass=*)', attrs=()):
-        """make an ldap query"""
-        self.debug('ldap search %s %s %s %s %s', self.uri, base, scope,
-                   searchstr, list(attrs))
-        # XXX for now, we do not have connections set support for LDAP, so
-        # this is always self._conn
-        cnx = session.cnxset.connection(self.uri).cnx
-        try:
-            res = cnx.search_s(base, scope, searchstr, attrs)
-        except ldap.PARTIAL_RESULTS:
-            res = cnx.result(all=0)[1]
-        except ldap.NO_SUCH_OBJECT:
-            self.info('ldap NO SUCH OBJECT')
-            eid = self.repo.extid2eid(self, base, 'CWUser', session, insert=False)
-            if eid:
-                self.warning('deleting ldap user with eid %s and dn %s',
-                             eid, base)
-                entity = session.entity_from_eid(eid, 'CWUser')
-                self.repo.delete_info(session, entity, self.uri)
-                self.reset_caches()
-            return []
-        # except ldap.REFERRAL, e:
-        #     cnx = self.handle_referral(e)
-        #     try:
-        #         res = cnx.search_s(base, scope, searchstr, attrs)
-        #     except ldap.PARTIAL_RESULTS:
-        #         res_type, res = cnx.result(all=0)
-        result = []
-        for rec_dn, rec_dict in res:
-            # When used against Active Directory, "rec_dict" may not be
-            # be a dictionary in some cases (instead, it can be a list)
-            # An example of a useless "res" entry that can be ignored
-            # from AD is
-            # (None, ['ldap://ForestDnsZones.PORTAL.LOCAL/DC=ForestDnsZones,DC=PORTAL,DC=LOCAL'])
-            # This appears to be some sort of internal referral, but
-            # we can't handle it, so we need to skip over it.
-            try:
-                items =  rec_dict.items()
-            except AttributeError:
-                # 'items' not found on rec_dict, skip
-                continue
-            for key, value in items: # XXX syt: huuum ?
-                if not isinstance(value, str):
-                    try:
-                        for i in range(len(value)):
-                            value[i] = unicode(value[i], 'utf8')
-                    except Exception:
-                        pass
-                if isinstance(value, list) and len(value) == 1:
-                    rec_dict[key] = value = value[0]
-            rec_dict['dn'] = rec_dn
-            self._cache[rec_dn] = rec_dict
-            result.append(rec_dict)
-        #print '--->', result
-        self.debug('ldap built results %s', len(result))
-        return result
+    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
@@ -604,13 +348,13 @@
         self.debug('ldap after entity insertion')
         super(LDAPUserSource, self).after_entity_insertion(
             session, lid, entity, sourceparams)
-        dn = lid
         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:
-            emailaddr = self._cache[dn][self.user_rev_attrs['email']]
+            # lid = dn
+            emailaddr = self._cache[lid][self.user_rev_attrs['email']]
         except KeyError:
             return
         if isinstance(emailaddr, list):
@@ -632,6 +376,7 @@
         """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})
--- a/server/sources/pyrorql.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/sources/pyrorql.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -367,7 +367,7 @@
         if cu is None:
             # this is a ConnectionWrapper instance
             msg = session._("can't connect to source %s, some data may be missing")
-            session.set_shared_data('sources_error', msg % self.uri)
+            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
             return []
         translator = RQL2RQL(self)
         try:
@@ -383,7 +383,7 @@
         except Exception, ex:
             self.exception(str(ex))
             msg = session._("error while querying source %s, some data may be missing")
-            session.set_shared_data('sources_error', msg % self.uri)
+            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
             return []
         descr = rset.description
         if rset:
--- a/server/sources/storages.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/sources/storages.py	Thu Feb 23 19:06:14 2012 +0100
@@ -18,6 +18,7 @@
 """custom storages for the system source"""
 
 import os
+import sys
 from os import unlink, path as osp
 from contextlib import contextmanager
 
@@ -112,11 +113,29 @@
 
 class BytesFileSystemStorage(Storage):
     """store Bytes attribute value on the file system"""
-    def __init__(self, defaultdir, fsencoding='utf-8'):
+    def __init__(self, defaultdir, fsencoding='utf-8', wmode=0444):
         if type(defaultdir) is unicode:
             defaultdir = defaultdir.encode(fsencoding)
         self.default_directory = defaultdir
         self.fsencoding = fsencoding
+        # extra umask to use when creating file
+        # 0444 as in "only allow read bit in permission"
+        self._wmode = wmode
+
+    def _writecontent(self, path, binary):
+        """write the content of a binary in readonly file
+
+        As the bfss never alter a create file it does not prevent it to work as
+        intended. This is a beter safe than sorry approach.
+        """
+        write_flag = os.O_WRONLY | os.O_CREAT | os.O_EXCL
+        if sys.platform == 'win32':
+            write_flag |= os.O_BINARY
+        fd = os.open(path, write_flag, self._wmode)
+        fileobj = os.fdopen(fd, 'wb')
+        binary.to_file(fileobj)
+        fileobj.close()
+
 
     def callback(self, source, session, value):
         """sql generator callback when some attribute with a custom storage is
@@ -138,7 +157,7 @@
             fpath = self.new_fs_path(entity, attr)
             # bytes storage used to store file's path
             entity.cw_edited.edited_attribute(attr, Binary(fpath))
-            binary.to_file(fpath)
+            self._writecontent(fpath, binary)
             AddFileOp.get_instance(entity._cw).add_data(fpath)
         return binary
 
@@ -171,7 +190,7 @@
                 fpath = self.new_fs_path(entity, attr)
                 assert not osp.exists(fpath)
                 # write attribute value on disk
-                binary.to_file(fpath)
+                self._writecontent(fpath, binary)
                 # Mark the new file as added during the transaction.
                 # The file will be removed on rollback
                 AddFileOp.get_instance(entity._cw).add_data(fpath)
--- a/server/sqlutils.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/sqlutils.py	Thu Feb 23 19:06:14 2012 +0100
@@ -214,31 +214,11 @@
         # callback lookup for each *cell* in results when there is nothing to
         # lookup
         if not column_callbacks:
-            return self._process_result(cursor)
+            return self.dbhelper.dbapi_module.process_cursor(cursor, self._dbencoding,
+                                                             Binary)
         assert session
         return self._cb_process_result(cursor, column_callbacks, session)
 
-    def _process_result(self, cursor):
-        # begin bind to locals for optimization
-        descr = cursor.description
-        encoding = self._dbencoding
-        process_value = self._process_value
-        binary = Binary
-        # /end
-        cursor.arraysize = 100
-        while True:
-            results = cursor.fetchmany()
-            if not results:
-                break
-            for line in results:
-                result = []
-                for col, value in enumerate(line):
-                    if value is None:
-                        result.append(value)
-                        continue
-                    result.append(process_value(value, descr[col], encoding, binary))
-                yield result
-
     def _cb_process_result(self, cursor, column_callbacks, session):
         # begin bind to locals for optimization
         descr = cursor.description
--- a/server/test/unittest_ldapuser.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_ldapuser.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -25,6 +25,8 @@
 from socket import socket, error as socketerror
 
 from logilab.common.testlib import TestCase, unittest_main, mock_object, Tags
+
+from cubicweb import AuthenticationError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import RQLGeneratorTC
 from cubicweb.devtools.httptest import get_available_port
@@ -32,15 +34,14 @@
 
 from cubicweb.server.sources.ldapuser import *
 
-CONFIG = u'''host=%s
-user-base-dn=ou=People,dc=cubicweb,dc=test
+CONFIG = u'''user-base-dn=ou=People,dc=cubicweb,dc=test
 user-scope=ONELEVEL
 user-classes=top,posixAccount
 user-login-attr=uid
 user-default-group=users
 user-attrs-map=gecos:email,uid:login
 '''
-
+URL = None
 
 def setUpModule(*args):
     create_slapd_configuration(LDAPUserSourceTC.config)
@@ -49,7 +50,7 @@
     terminate_slapd()
 
 def create_slapd_configuration(config):
-    global slapd_process, CONFIG
+    global slapd_process, URL
     basedir = join(config.apphome, "ldapdb")
     slapdconf = join(config.apphome, "slapd.conf")
     confin = file(join(config.apphome, "slapd.conf.in")).read()
@@ -78,7 +79,7 @@
     else:
         raise EnvironmentError('Cannot start slapd with cmdline="%s" (from directory "%s")' %
                                (" ".join(cmdline), os.getcwd()))
-    CONFIG = CONFIG % host
+    URL = u'ldap://%s' % host
 
 def terminate_slapd():
     global slapd_process
@@ -93,23 +94,95 @@
         print "DONE"
     del slapd_process
 
-class LDAPUserSourceTC(CubicWebTC):
+
+
+
+class LDAPFeedSourceTC(CubicWebTC):
+    test_db_id = 'ldap-feed'
+
+    @classmethod
+    def pre_setup_database(cls, session, config):
+        session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
+                              url=URL, config=CONFIG)
+        session.commit()
+        isession = session.repo.internal_session()
+        lfsource = isession.repo.sources_by_uri['ldapuser']
+        stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+
+    def setUp(self):
+        super(LDAPFeedSourceTC, self).setUp()
+        # ldap source url in the database may use a different port as the one
+        # just attributed
+        lfsource = self.repo.sources_by_uri['ldapuser']
+        lfsource.urls = [URL]
+
+    def assertMetadata(self, entity):
+        self.assertTrue(entity.creation_date)
+        self.assertTrue(entity.modification_date)
+
+    def test_authenticate(self):
+        source = self.repo.sources_by_uri['ldapuser']
+        self.session.set_cnxset()
+        # ensure we won't be logged against
+        self.assertRaises(AuthenticationError,
+                          source.authenticate, self.session, 'toto', 'toto')
+        self.assertTrue(source.authenticate(self.session, 'syt', 'syt'))
+        self.assertTrue(self.repo.connect('syt', password='syt'))
+
+    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, ())
+        self.assertEqual(e.primary_email[0].address, 'Sylvain Thenault')
+        # 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_copy_to_system_source(self):
+        source = self.repo.sources_by_uri['ldapuser']
+        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
+        self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': eid})
+        self.commit()
+        source.reset_caches()
+        rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
+        self.assertEqual(len(rset), 1)
+        e = rset.get_entity(0, 0)
+        self.assertEqual(e.eid, eid)
+        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system', 'use-cwuri-as-url': False},
+                                                  'type': 'CWUser',
+                                                  'extid': None})
+        self.assertEqual(e.cw_source[0].name, 'system')
+        self.assertTrue(e.creation_date)
+        self.assertTrue(e.modification_date)
+        # XXX test some password has been set
+        source.pull_data(self.session)
+        rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
+        self.assertEqual(len(rset), 1)
+
+
+class LDAPUserSourceTC(LDAPFeedSourceTC):
     test_db_id = 'ldap-user'
     tags = CubicWebTC.tags | Tags(('ldap'))
 
     @classmethod
     def pre_setup_database(cls, session, config):
         session.create_entity('CWSource', name=u'ldapuser', type=u'ldapuser',
-                              config=CONFIG)
+                              url=URL, config=CONFIG)
         session.commit()
         # XXX keep it there
         session.execute('CWUser U')
 
-    def test_authenticate(self):
-        source = self.repo.sources_by_uri['ldapuser']
-        self.session.set_cnxset()
-        self.assertRaises(AuthenticationError,
-                          source.authenticate, self.session, 'toto', 'toto')
+    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['ldapuser']
@@ -121,8 +194,7 @@
         e = rset.get_entity(0, 0)
         self.assertEqual(e.login, 'syt')
         e.complete()
-        self.assertEqual(e.creation_date, None)
-        self.assertEqual(e.modification_date, None)
+        self.assertMetadata(e)
         self.assertEqual(e.firstname, None)
         self.assertEqual(e.surname, None)
         self.assertEqual(e.in_group[0].name, 'users')
@@ -347,27 +419,6 @@
         rset = cu.execute('Any F WHERE X has_text "iaminguestsgrouponly", X firstname F')
         self.assertEqual(rset.rows, [[None]])
 
-    def test_copy_to_system_source(self):
-        source = self.repo.sources_by_uri['ldapuser']
-        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
-        self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': eid})
-        self.commit()
-        source.reset_caches()
-        rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
-        self.assertEqual(len(rset), 1)
-        e = rset.get_entity(0, 0)
-        self.assertEqual(e.eid, eid)
-        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system', 'use-cwuri-as-url': False},
-                                                  'type': 'CWUser',
-                                                  'extid': None})
-        self.assertEqual(e.cw_source[0].name, 'system')
-        self.assertTrue(e.creation_date)
-        self.assertTrue(e.modification_date)
-        # XXX test some password has been set
-        source.synchronize()
-        rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
-        self.assertEqual(len(rset), 1)
-
     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',
@@ -403,7 +454,6 @@
                      '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):
@@ -438,6 +488,7 @@
         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'))
--- a/server/test/unittest_msplanner.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_msplanner.py	Thu Feb 23 19:06:14 2012 +0100
@@ -914,14 +914,16 @@
                     ('FetchStep', [('Any X WHERE X is CWUser', [{'X': 'CWUser'}])],
                      [self.ldap, self.system], None, {'X': 'table2.C0'}, []),
                     ('UnionFetchStep', [
-                        ('FetchStep', [('Any X WHERE EXISTS(X owned_by %s), X is Basket' % ueid, [{'X': 'Basket'}])],
+                        ('FetchStep', [('Any X WHERE EXISTS(%s use_email X), X is EmailAddress' % ueid,
+      [{'X': 'EmailAddress'}]),
+                                       ('Any X WHERE EXISTS(X owned_by %s), X is Basket' % ueid, [{'X': 'Basket'}])],
                           [self.system], {}, {'X': 'table0.C0'}, []),
                         ('UnionFetchStep',
                          [('FetchStep', [('Any X WHERE X is IN(Card, Note, State)',
                                           [{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])],
                            [self.cards, self.system], {}, {'X': 'table0.C0'}, []),
                           ('FetchStep',
-                           [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+                           [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
                              [{'X': 'BaseTransition'}, {'X': 'Bookmark'},
                               {'X': 'CWAttribute'}, {'X': 'CWCache'},
                               {'X': 'CWConstraint'}, {'X': 'CWConstraintType'},
@@ -931,7 +933,7 @@
                               {'X': 'CWSource'},
                               {'X': 'CWUniqueTogetherConstraint'},
                               {'X': 'Comment'}, {'X': 'Division'},
-                              {'X': 'Email'}, {'X': 'EmailAddress'},
+                              {'X': 'Email'},
                               {'X': 'EmailPart'}, {'X': 'EmailThread'},
                               {'X': 'ExternalUri'}, {'X': 'File'},
                               {'X': 'Folder'}, {'X': 'Old'},
@@ -972,7 +974,8 @@
                     ('FetchStep', [('Any X WHERE X is CWUser', [{'X': 'CWUser'}])],
                      [self.ldap, self.system], None, {'X': 'table3.C0'}, []),
                     ('UnionFetchStep',
-                     [('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is Basket' % ueid,
+                     [('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(%s use_email X), ET is CWEType, X is EmailAddress' % ueid,
+      [{'ET': 'CWEType', 'X': 'EmailAddress'}]), ('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is Basket' % ueid,
                                       [{'ET': 'CWEType', 'X': 'Basket'}])],
                        [self.system], {}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []),
                       ('FetchStep', [('Any ET,X WHERE X is ET, (EXISTS(X owned_by %(ueid)s)) OR ((((EXISTS(D concerne C?, C owned_by %(ueid)s, C type "X", X identity D, C is Division, D is Affaire)) OR (EXISTS(H concerne G?, G owned_by %(ueid)s, G type "X", X identity H, G is SubDivision, H is Affaire))) OR (EXISTS(I concerne F?, F owned_by %(ueid)s, F type "X", X identity I, F is Societe, I is Affaire))) OR (EXISTS(J concerne E?, E owned_by %(ueid)s, X identity J, E is Note, J is Affaire))), ET is CWEType, X is Affaire' % {'ueid': ueid},
@@ -987,7 +990,7 @@
                        [self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []),
                       # extra UnionFetchStep could be avoided but has no cost, so don't care
                       ('UnionFetchStep',
-                       [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+                       [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
                                         [{'X': 'BaseTransition', 'ET': 'CWEType'},
                                          {'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'},
                                          {'X': 'CWCache', 'ET': 'CWEType'}, {'X': 'CWConstraint', 'ET': 'CWEType'},
@@ -1000,7 +1003,7 @@
                                          {'X': 'CWUniqueTogetherConstraint', 'ET': 'CWEType'},
                                          {'X': 'Comment', 'ET': 'CWEType'},
                                          {'X': 'Division', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'},
-                                         {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'},
+                                         {'X': 'EmailPart', 'ET': 'CWEType'},
                                          {'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'},
                                          {'X': 'File', 'ET': 'CWEType'}, {'X': 'Folder', 'ET': 'CWEType'},
                                          {'X': 'Old', 'ET': 'CWEType'}, {'X': 'Personne', 'ET': 'CWEType'},
--- a/server/test/unittest_postgres.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_postgres.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,3 +1,21 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of Logilab-common.
+#
+# Logilab-common is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 2.1 of the License, or (at your
+# option) any later version.
+#
+# Logilab-common is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with Logilab-common.  If not, see <http://www.gnu.org/licenses/>.
+
 from __future__ import with_statement
 
 import socket
@@ -7,7 +25,7 @@
 
 from cubicweb.devtools import ApptestConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.entities.adapters import IFTIndexableAdapter
 
 AT_LOGILAB = socket.gethostname().endswith('.logilab.fr') # XXX
@@ -32,7 +50,7 @@
                                content=u'cubicweb cubicweb')
         self.commit()
         self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
-                          [[c1.eid], [c3.eid], [c2.eid]])
+                         [(c1.eid,), (c3.eid,), (c2.eid,)])
 
 
     def test_attr_weight(self):
@@ -49,7 +67,7 @@
                                    content=u'autre chose')
             self.commit()
             self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
-                              [[c3.eid], [c1.eid], [c2.eid]])
+                             [(c3.eid,), (c1.eid,), (c2.eid,)])
 
     def test_entity_weight(self):
         class PersonneIFTIndexableAdapter(IFTIndexableAdapter):
@@ -62,7 +80,7 @@
             c3 = req.create_entity('Comment', content=u'cubicweb cubicweb cubicweb', comments=c1)
             self.commit()
             self.assertEqual(req.execute('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
-                              [[c1.eid], [c3.eid], [c2.eid]])
+                              [(c1.eid,), (c3.eid,), (c2.eid,)])
 
 
     def test_tz_datetime(self):
--- a/server/test/unittest_querier.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_querier.py	Thu Feb 23 19:06:14 2012 +0100
@@ -120,7 +120,7 @@
         self.assertEqual(len(union.children), 1)
         self.assertEqual(len(union.children[0].with_), 1)
         subq = union.children[0].with_[0].query
-        self.assertEqual(len(subq.children), 3)
+        self.assertEqual(len(subq.children), 4)
         self.assertEqual([t.as_string() for t in union.children[0].selection],
                           ['ETN','COUNT(X)'])
         self.assertEqual([t.as_string() for t in union.children[0].groupby],
@@ -145,7 +145,7 @@
                                        'X': 'Affaire',
                                        'ET': 'CWEType', 'ETN': 'String'}])
         rql, solutions = partrqls[1]
-        self.assertEqual(rql,  'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
+        self.assertEqual(rql,  'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
         self.assertListEqual(sorted(solutions),
                               sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'},
@@ -166,7 +166,6 @@
                                       {'X': 'CWUniqueTogetherConstraint', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Email', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'EmailAddress', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailPart', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailThread', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'ExternalUri', 'ETN': 'String', 'ET': 'CWEType'},
@@ -187,12 +186,14 @@
                                       {'X': 'WorkflowTransition', 'ETN': 'String', 'ET': 'CWEType'}]))
         rql, solutions = partrqls[2]
         self.assertEqual(rql,
+                         'Any ETN,X WHERE X is ET, ET name ETN, EXISTS(%(D)s use_email X), '
+                         'ET is CWEType, X is EmailAddress')
+        self.assertEqual(solutions, [{'X': 'EmailAddress', 'ET': 'CWEType', 'ETN': 'String'}])
+        rql, solutions = partrqls[3]
+        self.assertEqual(rql,
                           'Any ETN,X WHERE X is ET, ET name ETN, EXISTS(X owned_by %(C)s), '
                           'ET is CWEType, X is Basket')
-        self.assertEqual(solutions, [{'ET': 'CWEType',
-                                       'X': 'Basket',
-                                       'ETN': 'String',
-                                       }])
+        self.assertEqual(solutions, [{'X': 'Basket', 'ET': 'CWEType', 'ETN': 'String'}])
 
     def test_preprocess_security_aggregat(self):
         plan = self._prepare_plan('Any MAX(X)')
@@ -202,7 +203,7 @@
         self.assertEqual(len(union.children), 1)
         self.assertEqual(len(union.children[0].with_), 1)
         subq = union.children[0].with_[0].query
-        self.assertEqual(len(subq.children), 3)
+        self.assertEqual(len(subq.children), 4)
         self.assertEqual([t.as_string() for t in union.children[0].selection],
                           ['MAX(X)'])
 
@@ -1117,13 +1118,12 @@
         (using cachekey on sql generation returned always the same query for an eid,
         whatever the relation)
         """
-        s = self.user_groups_session('users')
-        aeid, = self.o.execute(s, 'INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')[0]
+        aeid, = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')[0]
         # XXX would be nice if the rql below was enough...
         #'INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y'
-        eeid, = self.o.execute(s, 'INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
-        self.o.execute(s, "DELETE Email X")
-        sqlc = s.cnxset['system']
+        eeid, = self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
+        self.execute("DELETE Email X")
+        sqlc = self.session.cnxset['system']
         sqlc.execute('SELECT * FROM recipients_relation')
         self.assertEqual(len(sqlc.fetchall()), 0)
         sqlc.execute('SELECT * FROM owned_by_relation WHERE eid_from=%s'%eeid)
--- a/server/test/unittest_repository.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_repository.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,5 +1,5 @@
 # -*- coding: iso-8859-1 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -34,7 +34,7 @@
 
 from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
                       UnknownEid, AuthenticationError, Unauthorized, QueryError)
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.schema import CubicWebSchema, RQLConstraint
 from cubicweb.dbapi import connect, multiple_connections_unfix
 from cubicweb.devtools.testlib import CubicWebTC
--- a/server/test/unittest_schemaserial.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_schemaserial.py	Thu Feb 23 19:06:14 2012 +0100
@@ -141,7 +141,7 @@
                               [
             ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
              {'se': None, 'rt': None, 'oe': None,
-              'description': u'', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 7, 'defaultval': u'text/plain', 'indexed': False, 'cardinality': u'?1'}),
+              'description': u'', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 3, 'defaultval': u'text/plain', 'indexed': False, 'cardinality': u'?1'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
              {'x': None, 'value': u'None', 'ct': 'FormatConstraint_eid'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
--- a/server/test/unittest_security.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_security.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -646,5 +646,13 @@
                           self.execute, 'SET TI to_state S WHERE TI eid %(ti)s, S name "pitetre"',
                           {'ti': trinfo.eid})
 
+    def test_emailaddress_security(self):
+        self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
+        self.execute('INSERT EmailAddress X: X address "anon", U use_email X WHERE U login "anon"').get_entity(0, 0)
+        self.commit()
+        self.assertEqual(len(self.execute('Any X WHERE X is EmailAddress')), 2)
+        self.login('anon')
+        self.assertEqual(len(self.execute('Any X WHERE X is EmailAddress')), 1)
+
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_storage.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/server/test/unittest_storage.py	Thu Feb 23 19:06:14 2012 +0100
@@ -22,12 +22,13 @@
 from logilab.common.testlib import unittest_main, tag, Tags
 from cubicweb.devtools.testlib import CubicWebTC
 
+import os
 import os.path as osp
 import shutil
 import tempfile
 
 from cubicweb import Binary, QueryError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.server.sources import storages
 from cubicweb.server.hook import Hook, Operation
 
@@ -90,6 +91,8 @@
         expected_filepath = osp.join(self.tempdir, '%s_data_%s' %
                                      (f1.eid, f1.data_name))
         self.assertTrue(osp.isfile(expected_filepath))
+        # file should be read only
+        self.assertFalse(os.access(expected_filepath, os.W_OK))
         self.assertEqual(file(expected_filepath).read(), 'the-data')
         self.rollback()
         self.assertFalse(osp.isfile(expected_filepath))
--- a/sobjects/__init__.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/sobjects/__init__.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,6 +15,16 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""server side objects
+"""server side objects"""
+
+import os.path as osp
 
-"""
+def registration_callback(vreg):
+    vreg.register_all(globals().values(), __name__)
+    global URL_MAPPING
+    URL_MAPPING = {}
+    if vreg.config.apphome:
+        url_mapping_file = osp.join(vreg.config.apphome, 'urlmapping.py')
+        if osp.exists(url_mapping_file):
+            URL_MAPPING = eval(file(url_mapping_file).read())
+            vreg.info('using url mapping %s from %s', URL_MAPPING, url_mapping_file)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/cwxmlparser.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,502 @@
+# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# 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/>.
+"""datafeed parser for xml generated by cubicweb
+
+Example of mapping for CWEntityXMLParser::
+
+  {u'CWUser': {                                        # EntityType
+      (u'in_group', u'subject', u'link'): [            # (rtype, role, action)
+          (u'CWGroup', {u'linkattr': u'name'})],       #   -> rules = [(EntityType, options), ...]
+      (u'tags', u'object', u'link-or-create'): [       # (...)
+          (u'Tag', {u'linkattr': u'name'})],           #   -> ...
+      (u'use_email', u'subject', u'copy'): [           # (...)
+          (u'EmailAddress', {})]                       #   -> ...
+      }
+   }
+
+"""
+
+from datetime import datetime, timedelta, time
+from urllib import urlencode
+from cgi import parse_qs # in urlparse with python >= 2.6
+
+from logilab.common.date import todate, totime
+from logilab.common.textutils import splitstrip, text_to_dict
+from logilab.common.decorators import classproperty
+
+from yams.constraints import BASE_CONVERTERS
+from yams.schema import role_name as rn
+
+from cubicweb import ValidationError, RegistryException, typed_eid
+from cubicweb.view import Component
+from cubicweb.server.sources import datafeed
+from cubicweb.server.hook import match_rtype
+
+# XXX see cubicweb.cwvreg.YAMS_TO_PY
+# XXX see cubicweb.web.views.xmlrss.SERIALIZERS
+DEFAULT_CONVERTERS = BASE_CONVERTERS.copy()
+DEFAULT_CONVERTERS['String'] = unicode
+DEFAULT_CONVERTERS['Password'] = lambda x: x.encode('utf8')
+def convert_date(ustr):
+    return todate(datetime.strptime(ustr, '%Y-%m-%d'))
+DEFAULT_CONVERTERS['Date'] = convert_date
+def convert_datetime(ustr):
+    if '.' in ustr: # assume %Y-%m-%d %H:%M:%S.mmmmmm
+        ustr = ustr.split('.',1)[0]
+    return datetime.strptime(ustr, '%Y-%m-%d %H:%M:%S')
+DEFAULT_CONVERTERS['Datetime'] = convert_datetime
+# XXX handle timezone, though this will be enough as TZDatetime are
+# serialized without time zone by default (UTC time). See
+# cw.web.views.xmlrss.SERIALIZERS.
+DEFAULT_CONVERTERS['TZDatetime'] = convert_datetime
+def convert_time(ustr):
+    return totime(datetime.strptime(ustr, '%H:%M:%S'))
+DEFAULT_CONVERTERS['Time'] = convert_time
+DEFAULT_CONVERTERS['TZTime'] = convert_time
+def convert_interval(ustr):
+    return time(seconds=int(ustr))
+DEFAULT_CONVERTERS['Interval'] = convert_interval
+
+def extract_typed_attrs(eschema, stringdict, converters=DEFAULT_CONVERTERS):
+    typeddict = {}
+    for rschema in eschema.subject_relations():
+        if rschema.final and rschema in stringdict:
+            if rschema in ('eid', 'cwuri', 'cwtype', 'cwsource'):
+                continue
+            attrtype = eschema.destination(rschema)
+            value = stringdict[rschema]
+            if value is not None:
+                value = converters[attrtype](value)
+            typeddict[rschema.type] = value
+    return typeddict
+
+def rtype_role_rql(rtype, role):
+    if role == 'object':
+        return 'Y %s X WHERE X eid %%(x)s' % rtype
+    else:
+        return 'X %s Y WHERE X eid %%(x)s' % rtype
+
+
+class CWEntityXMLParser(datafeed.DataFeedXMLParser):
+    """datafeed parser for the 'xml' entity view
+
+    Most of the logic is delegated to the following components:
+
+    * an "item builder" component, turning an etree xml node into a specific
+      python dictionary representing an entity
+
+    * "action" components, selected given an entity, a relation and its role in
+      the relation, and responsible to link the entity to given related items
+      (eg dictionary)
+
+    So the parser is only doing the gluing service and the connection to the
+    source.
+    """
+    __regid__ = 'cw.entityxml'
+
+    def __init__(self, *args, **kwargs):
+        super(CWEntityXMLParser, self).__init__(*args, **kwargs)
+        self._parsed_urls = {}
+        self._processed_entities = set()
+
+    def select_linker(self, action, rtype, role, entity=None):
+        try:
+            return self._cw.vreg['components'].select(
+                'cw.entityxml.action.%s' % action, self._cw, entity=entity,
+                rtype=rtype, role=role, parser=self)
+        except RegistryException:
+            raise RegistryException('Unknown action %s' % action)
+
+    def list_actions(self):
+        reg = self._cw.vreg['components']
+        return sorted(clss[0].action for rid, clss in reg.iteritems()
+                      if rid.startswith('cw.entityxml.action.'))
+
+    # mapping handling #########################################################
+
+    def add_schema_config(self, schemacfg, checkonly=False):
+        """added CWSourceSchemaConfig, modify mapping accordingly"""
+        _ = self._cw._
+        try:
+            rtype = schemacfg.schema.rtype.name
+        except AttributeError:
+            msg = _("entity and relation types can't be mapped, only attributes "
+                    "or relations")
+            raise ValidationError(schemacfg.eid, {rn('cw_for_schema', 'subject'): msg})
+        if schemacfg.options:
+            options = text_to_dict(schemacfg.options)
+        else:
+            options = {}
+        try:
+            role = options.pop('role')
+            if role not in ('subject', 'object'):
+                raise KeyError
+        except KeyError:
+            msg = _('"role=subject" or "role=object" must be specified in options')
+            raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
+        try:
+            action = options.pop('action')
+            linker = self.select_linker(action, rtype, role)
+            linker.check_options(options, schemacfg.eid)
+        except KeyError:
+            msg = _('"action" must be specified in options; allowed values are '
+                    '%s') % ', '.join(self.list_actions())
+            raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
+        except RegistryException:
+            msg = _('allowed values for "action" are %s') % ', '.join(self.list_actions())
+            raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
+        if not checkonly:
+            if role == 'subject':
+                etype = schemacfg.schema.stype.name
+                ttype = schemacfg.schema.otype.name
+            else:
+                etype = schemacfg.schema.otype.name
+                ttype = schemacfg.schema.stype.name
+            etyperules = self.source.mapping.setdefault(etype, {})
+            etyperules.setdefault((rtype, role, action), []).append(
+                (ttype, options) )
+            self.source.mapping_idx[schemacfg.eid] = (
+                etype, rtype, role, action, ttype)
+
+    def del_schema_config(self, schemacfg, checkonly=False):
+        """deleted CWSourceSchemaConfig, modify mapping accordingly"""
+        etype, rtype, role, action, ttype = self.source.mapping_idx[schemacfg.eid]
+        rules = self.source.mapping[etype][(rtype, role, action)]
+        rules = [x for x in rules if not x[0] == ttype]
+        if not rules:
+            del self.source.mapping[etype][(rtype, role, action)]
+
+    # import handling ##########################################################
+
+    def process(self, url, raise_on_error=False, partialcommit=True):
+        """IDataFeedParser main entry point"""
+        if url.startswith('http'): # XXX similar loose test as in parse of sources.datafeed
+            url = self.complete_url(url)
+        super(CWEntityXMLParser, self).process(url, raise_on_error, partialcommit)
+
+    def parse_etree(self, parent):
+        for node in list(parent):
+            builder = self._cw.vreg['components'].select(
+                'cw.entityxml.item-builder', self._cw, node=node,
+                parser=self)
+            yield builder.build_item()
+
+    def process_item(self, item, rels):
+        """
+        item and rels are what's returned by the item builder `build_item` method:
+
+        * `item` is an {attribute: value} dictionary
+        * `rels` is for relations and structured as
+           {role: {relation: [(related item, related rels)...]}
+        """
+        entity = self.extid2entity(str(item['cwuri']),  item['cwtype'],
+                                   cwsource=item['cwsource'], item=item)
+        if entity is None:
+            return None
+        if entity.eid in self._processed_entities:
+            return entity
+        self._processed_entities.add(entity.eid)
+        if not (self.created_during_pull(entity) or self.updated_during_pull(entity)):
+            attrs = extract_typed_attrs(entity.e_schema, item)
+            self.update_if_necessary(entity, attrs)
+        self.process_relations(entity, rels)
+        return entity
+
+    def process_relations(self, entity, rels):
+        etype = entity.__regid__
+        for (rtype, role, action), rules in self.source.mapping.get(etype, {}).iteritems():
+            try:
+                related_items = rels[role][rtype]
+            except KeyError:
+                self.import_log.record_error('relation %s-%s not found in xml export of %s'
+                                             % (rtype, role, etype))
+                continue
+            try:
+                linker = self.select_linker(action, rtype, role, entity)
+            except RegistryException:
+                self.import_log.record_error('no linker for action %s' % action)
+            else:
+                linker.link_items(related_items, rules)
+
+    def before_entity_copy(self, entity, sourceparams):
+        """IDataFeedParser callback"""
+        attrs = extract_typed_attrs(entity.e_schema, sourceparams['item'])
+        entity.cw_edited.update(attrs)
+
+
+    def normalize_url(self, url):
+        """overriden to add vid=xml"""
+        url = super(CWEntityXMLParser, self).normalize_url(url)
+        if url.startswih('http'):
+            try:
+                url, qs = url.split('?', 1)
+            except ValueError:
+                params = {}
+            else:
+                params = parse_qs(qs)
+            if not 'vid' in params:
+                params['vid'] = ['xml']
+            return url + '?' + self._cw.build_url_params(**params)
+        return url
+
+    def complete_url(self, url, etype=None, known_relations=None):
+        """append to the url's query string information about relation that should
+        be included in the resulting xml, according to source mapping.
+
+        If etype is not specified, try to guess it using the last path part of
+        the url, i.e. the format used by default in cubicweb to map all entities
+        of a given type as in 'http://mysite.org/EntityType'.
+
+        If `known_relations` is given, it should be a dictionary of already
+        known relations, so they don't get queried again.
+        """
+        try:
+            url, qs = url.split('?', 1)
+        except ValueError:
+            qs = ''
+        # XXX vid will be added by later call to normalize_url (in parent class)
+        params = parse_qs(qs)
+        if etype is None:
+            try:
+                etype = url.rsplit('/', 1)[1]
+            except ValueError:
+                return url + '?' + self._cw.build_url_params(**params)
+            try:
+                etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
+            except KeyError:
+                return url + '?' + self._cw.build_url_params(**params)
+        relations = params.setdefault('relation', [])
+        for rtype, role, _ in self.source.mapping.get(etype, ()):
+            if known_relations and rtype in known_relations.get('role', ()):
+                continue
+            reldef = '%s-%s' % (rtype, role)
+            if not reldef in relations:
+                relations.append(reldef)
+        return url + '?' + self._cw.build_url_params(**params)
+
+    def complete_item(self, item, rels):
+        try:
+            return self._parsed_urls[item['cwuri']]
+        except KeyError:
+            itemurl = self.complete_url(item['cwuri'], item['cwtype'], rels)
+            item_rels = list(self.parse(itemurl))
+            assert len(item_rels) == 1, 'url %s expected to bring back one '\
+                   'and only one entity, got %s' % (itemurl, len(item_rels))
+            self._parsed_urls[item['cwuri']] = item_rels[0]
+            if rels:
+                # XXX (do it better) merge relations
+                new_rels = item_rels[0][1]
+                new_rels.get('subject', {}).update(rels.get('subject', {}))
+                new_rels.get('object', {}).update(rels.get('object', {}))
+            return item_rels[0]
+
+
+class CWEntityXMLItemBuilder(Component):
+    __regid__ = 'cw.entityxml.item-builder'
+
+    def __init__(self, _cw, parser, node, **kwargs):
+        super(CWEntityXMLItemBuilder, self).__init__(_cw, **kwargs)
+        self.parser = parser
+        self.node = node
+
+    def build_item(self):
+        """parse a XML document node and return two dictionaries defining (part
+        of) an entity:
+
+        - {attribute: value}
+        - {role: {relation: [(related item, related rels)...]}
+        """
+        node = self.node
+        item = dict(node.attrib.items())
+        item['cwtype'] = unicode(node.tag)
+        item.setdefault('cwsource', None)
+        try:
+            item['eid'] = typed_eid(item['eid'])
+        except KeyError:
+            # cw < 3.11 compat mode XXX
+            item['eid'] = typed_eid(node.find('eid').text)
+            item['cwuri'] = node.find('cwuri').text
+        rels = {}
+        for child in node:
+            role = child.get('role')
+            if role:
+                # relation
+                related = rels.setdefault(role, {}).setdefault(child.tag, [])
+                related += self.parser.parse_etree(child)
+            elif child.text:
+                # attribute
+                item[child.tag] = unicode(child.text)
+            else:
+                # None attribute (empty tag)
+                item[child.tag] = None
+        return item, rels
+
+
+class CWEntityXMLActionCopy(Component):
+    """implementation of cubicweb entity xml parser's'copy' action
+
+    Takes no option.
+    """
+    __regid__ = 'cw.entityxml.action.copy'
+
+    def __init__(self, _cw, parser, rtype, role, entity=None, **kwargs):
+        super(CWEntityXMLActionCopy, self).__init__(_cw, **kwargs)
+        self.parser = parser
+        self.rtype = rtype
+        self.role = role
+        self.entity = entity
+
+    @classproperty
+    def action(cls):
+        return cls.__regid__.rsplit('.', 1)[-1]
+
+    def check_options(self, options, eid):
+        self._check_no_options(options, eid)
+
+    def _check_no_options(self, options, eid, msg=None):
+        if options:
+            if msg is None:
+                msg = self._cw._("'%s' action doesn't take any options") % self.action
+            raise ValidationError(eid, {rn('options', 'subject'): msg})
+
+    def link_items(self, others, rules):
+        assert not any(x[1] for x in rules), "'copy' action takes no option"
+        ttypes = frozenset([x[0] for x in rules])
+        eids = [] # local eids
+        for item, rels in others:
+            if item['cwtype'] in ttypes:
+                item, rels = self.parser.complete_item(item, rels)
+                other_entity = self.parser.process_item(item, rels)
+                if other_entity is not None:
+                    eids.append(other_entity.eid)
+        if eids:
+            self._set_relation(eids)
+        else:
+            self._clear_relation(ttypes)
+
+    def _clear_relation(self, ttypes):
+        if not self.parser.created_during_pull(self.entity):
+            if len(ttypes) > 1:
+                typerestr = ', Y is IN(%s)' % ','.join(ttypes)
+            else:
+                typerestr = ', Y is %s' % ','.join(ttypes)
+            self._cw.execute('DELETE ' + rtype_role_rql(self.rtype, self.role) + typerestr,
+                             {'x': self.entity.eid})
+
+    def _set_relation(self, eids):
+        assert eids
+        rtype = self.rtype
+        rqlbase = rtype_role_rql(rtype, self.role)
+        eidstr = ','.join(str(eid) for eid in eids)
+        self._cw.execute('DELETE %s, NOT Y eid IN (%s)' % (rqlbase, eidstr),
+                         {'x': self.entity.eid})
+        if self.role == 'object':
+            rql = 'SET %s, Y eid IN (%s), NOT Y %s X' % (rqlbase, eidstr, rtype)
+        else:
+            rql = 'SET %s, Y eid IN (%s), NOT X %s Y' % (rqlbase, eidstr, rtype)
+        self._cw.execute(rql, {'x': self.entity.eid})
+
+
+class CWEntityXMLActionLink(CWEntityXMLActionCopy):
+    """implementation of cubicweb entity xml parser's'link' action
+
+    requires a 'linkattr' option to control search of the linked entity.
+    """
+    __regid__ = 'cw.entityxml.action.link'
+
+    def check_options(self, options, eid):
+        if not 'linkattr' in options:
+            msg = self._cw._("'%s' action requires 'linkattr' option") % self.action
+            raise ValidationError(eid, {rn('options', 'subject'): msg})
+
+    create_when_not_found = False
+
+    def link_items(self, others, rules):
+        for ttype, options in rules:
+            searchattrs = splitstrip(options.get('linkattr', ''))
+            self._related_link(ttype, others, searchattrs)
+
+    def _related_link(self, ttype, others, searchattrs):
+        def issubset(x,y):
+            return all(z in y for z in x)
+        eids = [] # local eids
+        log = self.parser.import_log
+        for item, rels in others:
+            if item['cwtype'] != ttype:
+                continue
+            if not issubset(searchattrs, item):
+                item, rels = self.parser.complete_item(item, rels)
+                if not issubset(searchattrs, item):
+                    log.record_error('missing attribute, got %s expected keys %s'
+                                     % (item, searchattrs))
+                    continue
+            # XXX str() needed with python < 2.6
+            kwargs = dict((str(attr), item[attr]) for attr in searchattrs)
+            targets = self._find_entities(item, kwargs)
+            if len(targets) == 1:
+                entity = targets[0]
+            elif not targets and self.create_when_not_found:
+                entity = self._cw.create_entity(item['cwtype'], **kwargs)
+            else:
+                if len(targets) > 1:
+                    log.record_error('ambiguous link: found %s entity %s with attributes %s'
+                                     % (len(targets), item['cwtype'], kwargs))
+                else:
+                    log.record_error('can not find %s entity with attributes %s'
+                                     % (item['cwtype'], kwargs))
+                continue
+            eids.append(entity.eid)
+            self.parser.process_relations(entity, rels)
+        if eids:
+            self._set_relation(eids)
+        else:
+            self._clear_relation((ttype,))
+
+    def _find_entities(self, item, kwargs):
+        return tuple(self._cw.find_entities(item['cwtype'], **kwargs))
+
+
+class CWEntityXMLActionLinkInState(CWEntityXMLActionLink):
+    """custom implementation of cubicweb entity xml parser's'link' action for
+    in_state relation
+    """
+    __select__ = match_rtype('in_state')
+
+    def check_options(self, options, eid):
+        super(CWEntityXMLActionLinkInState, self).check_options(options, eid)
+        if not 'name' in options['linkattr']:
+            msg = self._cw._("'%s' action for in_state relation should at least have 'linkattr=name' option") % self.action
+            raise ValidationError(eid, {rn('options', 'subject'): msg})
+
+    def _find_entities(self, item, kwargs):
+        assert 'name' in item # XXX else, complete_item
+        state_name = item['name']
+        wf = self.entity.cw_adapt_to('IWorkflowable').current_workflow
+        state = wf.state_by_name(state_name)
+        if state is None:
+            return ()
+        return (state,)
+
+
+class CWEntityXMLActionLinkOrCreate(CWEntityXMLActionLink):
+    """implementation of cubicweb entity xml parser's'link-or-create' action
+
+    requires a 'linkattr' option to control search of the linked entity.
+    """
+    __regid__ = 'cw.entityxml.action.link-or-create'
+    create_when_not_found = True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/ldapparser.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,107 @@
+# copyright 2011-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# 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 feed source
+
+unlike ldapuser source, this source is copy based and will import ldap content
+(beside passwords for authentication) into the system source.
+"""
+from base64 import b64decode
+
+from logilab.common.decorators import cached
+
+from cubicweb.server.sources import datafeed
+
+class DataFeedlDAPParser(datafeed.DataFeedParser):
+    __regid__ = 'ldapfeed'
+    # attributes that may appears in source user_attrs dict which are not
+    # attributes of the cw user
+    non_attribute_keys = set(('email',))
+
+    def process(self, url, raise_on_error=False, partialcommit=True):
+        """IDataFeedParser main entry point"""
+        source = self.source
+        searchstr = '(&%s)' % ''.join(source.base_filters)
+        try:
+            ldap_emailattr = source.user_rev_attrs['email']
+        except KeyError:
+            ldap_emailattr = None
+        for userdict in source._search(self._cw, source.user_base_dn,
+                                       source.user_base_scope, searchstr):
+            entity = self.extid2entity(userdict['dn'], 'CWUser', **userdict)
+            if not self.created_during_pull(entity):
+                self.notify_updated(entity)
+                attrs = self.ldap2cwattrs(userdict)
+                self.update_if_necessary(entity, attrs)
+                self._process_email(entity, userdict)
+
+    def ldap2cwattrs(self, sdict, tdict=None):
+        if tdict is None:
+            tdict = {}
+        for sattr, tattr in self.source.user_attrs.iteritems():
+            if tattr not in self.non_attribute_keys:
+                tdict[tattr] = sdict[sattr]
+        return tdict
+
+    def before_entity_copy(self, entity, sourceparams):
+        if entity.__regid__ == 'EmailAddress':
+            entity.cw_edited['address'] = sourceparams['address']
+        else:
+            self.ldap2cwattrs(sourceparams, entity.cw_edited)
+        return entity
+
+    def after_entity_copy(self, entity, sourceparams):
+        super(DataFeedlDAPParser, self).after_entity_copy(entity, sourceparams)
+        if entity.__regid__ == 'EmailAddress':
+            return
+        groups = [self._get_group(n) for n in self.source.user_default_groups]
+        entity.set_relations(in_group=groups)
+        self._process_email(entity, sourceparams)
+
+    def is_deleted(self, extid, etype, eid):
+        try:
+            extid, _ = extid.rsplit('@@', 1)
+        except ValueError:
+            pass
+        return self.source.object_exists_in_ldap(extid)
+
+    def _process_email(self, entity, userdict):
+        try:
+            emailaddrs = userdict[self.source.user_rev_attrs['email']]
+        except KeyError:
+            return # no email for that user, nothing to do
+        if not isinstance(emailaddrs, list):
+            emailaddrs = [emailaddrs]
+        for emailaddr in emailaddrs:
+            # search for existant email first, may be coming from another source
+            rset = self._cw.execute('EmailAddress X WHERE X address %(addr)s',
+                                   {'addr': emailaddr})
+            if not rset:
+                # not found, create it. first forge an external id
+                emailextid = userdict['dn'] + '@@' + emailaddr
+                email = self.extid2entity(emailextid, 'EmailAddress',
+                                          address=emailaddr)
+                if entity.primary_email:
+                    entity.set_relations(use_email=email)
+                else:
+                    entity.set_relations(primary_email=email)
+            # XXX else check use_email relation?
+
+    @cached
+    def _get_group(self, name):
+        return self._cw.execute('Any X WHERE X is CWGroup, X name %(name)s',
+                                {'name': name}).get_entity(0, 0)
--- a/sobjects/notification.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/sobjects/notification.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -24,8 +24,8 @@
 
 from logilab.common.textutils import normalize_text
 from logilab.common.deprecation import class_renamed, class_moved, deprecated
+from logilab.common.registry import yes
 
-from cubicweb.selectors import yes
 from cubicweb.view import Component
 from cubicweb.mail import NotificationView as BaseNotificationView, SkipEmail
 from cubicweb.server.hook import SendMailOp
--- a/sobjects/parsers.py	Thu Feb 23 19:05:36 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,508 +0,0 @@
-# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""datafeed parser for xml generated by cubicweb
-
-Example of mapping for CWEntityXMLParser::
-
-  {u'CWUser': {                                        # EntityType
-      (u'in_group', u'subject', u'link'): [            # (rtype, role, action)
-          (u'CWGroup', {u'linkattr': u'name'})],       #   -> rules = [(EntityType, options), ...]
-      (u'tags', u'object', u'link-or-create'): [       # (...)
-          (u'Tag', {u'linkattr': u'name'})],           #   -> ...
-      (u'use_email', u'subject', u'copy'): [           # (...)
-          (u'EmailAddress', {})]                       #   -> ...
-      }
-   }
-
-"""
-
-import os.path as osp
-from datetime import datetime, timedelta, time
-from urllib import urlencode
-from cgi import parse_qs # in urlparse with python >= 2.6
-
-from logilab.common.date import todate, totime
-from logilab.common.textutils import splitstrip, text_to_dict
-from logilab.common.decorators import classproperty
-
-from yams.constraints import BASE_CONVERTERS
-from yams.schema import role_name as rn
-
-from cubicweb import ValidationError, RegistryException, typed_eid
-from cubicweb.view import Component
-from cubicweb.server.sources import datafeed
-from cubicweb.server.hook import match_rtype
-
-# XXX see cubicweb.cwvreg.YAMS_TO_PY
-# XXX see cubicweb.web.views.xmlrss.SERIALIZERS
-DEFAULT_CONVERTERS = BASE_CONVERTERS.copy()
-DEFAULT_CONVERTERS['String'] = unicode
-DEFAULT_CONVERTERS['Password'] = lambda x: x.encode('utf8')
-def convert_date(ustr):
-    return todate(datetime.strptime(ustr, '%Y-%m-%d'))
-DEFAULT_CONVERTERS['Date'] = convert_date
-def convert_datetime(ustr):
-    if '.' in ustr: # assume %Y-%m-%d %H:%M:%S.mmmmmm
-        ustr = ustr.split('.',1)[0]
-    return datetime.strptime(ustr, '%Y-%m-%d %H:%M:%S')
-DEFAULT_CONVERTERS['Datetime'] = convert_datetime
-# XXX handle timezone, though this will be enough as TZDatetime are
-# serialized without time zone by default (UTC time). See
-# cw.web.views.xmlrss.SERIALIZERS.
-DEFAULT_CONVERTERS['TZDatetime'] = convert_datetime
-def convert_time(ustr):
-    return totime(datetime.strptime(ustr, '%H:%M:%S'))
-DEFAULT_CONVERTERS['Time'] = convert_time
-DEFAULT_CONVERTERS['TZTime'] = convert_time
-def convert_interval(ustr):
-    return time(seconds=int(ustr))
-DEFAULT_CONVERTERS['Interval'] = convert_interval
-
-def extract_typed_attrs(eschema, stringdict, converters=DEFAULT_CONVERTERS):
-    typeddict = {}
-    for rschema in eschema.subject_relations():
-        if rschema.final and rschema in stringdict:
-            if rschema in ('eid', 'cwuri', 'cwtype', 'cwsource'):
-                continue
-            attrtype = eschema.destination(rschema)
-            value = stringdict[rschema]
-            if value is not None:
-                value = converters[attrtype](value)
-            typeddict[rschema.type] = value
-    return typeddict
-
-def rtype_role_rql(rtype, role):
-    if role == 'object':
-        return 'Y %s X WHERE X eid %%(x)s' % rtype
-    else:
-        return 'X %s Y WHERE X eid %%(x)s' % rtype
-
-
-class CWEntityXMLParser(datafeed.DataFeedXMLParser):
-    """datafeed parser for the 'xml' entity view
-
-    Most of the logic is delegated to the following components:
-
-    * an "item builder" component, turning an etree xml node into a specific
-      python dictionnary representing an entity
-
-    * "action" components, selected given an entity, a relation and its role in
-      the relation, and responsible to link the entity to given related items
-      (eg dictionnary)
-
-    So the parser is only doing the gluing service and the connection to the
-    source.
-    """
-    __regid__ = 'cw.entityxml'
-
-    def __init__(self, *args, **kwargs):
-        super(CWEntityXMLParser, self).__init__(*args, **kwargs)
-        self._parsed_urls = {}
-        self._processed_entities = set()
-
-    def select_linker(self, action, rtype, role, entity=None):
-        try:
-            return self._cw.vreg['components'].select(
-                'cw.entityxml.action.%s' % action, self._cw, entity=entity,
-                rtype=rtype, role=role, parser=self)
-        except RegistryException:
-            raise RegistryException('Unknown action %s' % action)
-
-    def list_actions(self):
-        reg = self._cw.vreg['components']
-        return sorted(clss[0].action for rid, clss in reg.iteritems()
-                      if rid.startswith('cw.entityxml.action.'))
-
-    # mapping handling #########################################################
-
-    def add_schema_config(self, schemacfg, checkonly=False):
-        """added CWSourceSchemaConfig, modify mapping accordingly"""
-        _ = self._cw._
-        try:
-            rtype = schemacfg.schema.rtype.name
-        except AttributeError:
-            msg = _("entity and relation types can't be mapped, only attributes "
-                    "or relations")
-            raise ValidationError(schemacfg.eid, {rn('cw_for_schema', 'subject'): msg})
-        if schemacfg.options:
-            options = text_to_dict(schemacfg.options)
-        else:
-            options = {}
-        try:
-            role = options.pop('role')
-            if role not in ('subject', 'object'):
-                raise KeyError
-        except KeyError:
-            msg = _('"role=subject" or "role=object" must be specified in options')
-            raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
-        try:
-            action = options.pop('action')
-            linker = self.select_linker(action, rtype, role)
-            linker.check_options(options, schemacfg.eid)
-        except KeyError:
-            msg = _('"action" must be specified in options; allowed values are '
-                    '%s') % ', '.join(self.list_actions())
-            raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
-        except RegistryException:
-            msg = _('allowed values for "action" are %s') % ', '.join(self.list_actions())
-            raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
-        if not checkonly:
-            if role == 'subject':
-                etype = schemacfg.schema.stype.name
-                ttype = schemacfg.schema.otype.name
-            else:
-                etype = schemacfg.schema.otype.name
-                ttype = schemacfg.schema.stype.name
-            etyperules = self.source.mapping.setdefault(etype, {})
-            etyperules.setdefault((rtype, role, action), []).append(
-                (ttype, options) )
-            self.source.mapping_idx[schemacfg.eid] = (
-                etype, rtype, role, action, ttype)
-
-    def del_schema_config(self, schemacfg, checkonly=False):
-        """deleted CWSourceSchemaConfig, modify mapping accordingly"""
-        etype, rtype, role, action, ttype = self.source.mapping_idx[schemacfg.eid]
-        rules = self.source.mapping[etype][(rtype, role, action)]
-        rules = [x for x in rules if not x[0] == ttype]
-        if not rules:
-            del self.source.mapping[etype][(rtype, role, action)]
-
-    # import handling ##########################################################
-
-    def process(self, url, raise_on_error=False, partialcommit=True):
-        """IDataFeedParser main entry point"""
-        if url.startswith('http'): # XXX similar loose test as in parse of sources.datafeed
-            url = self.complete_url(url)
-        super(CWEntityXMLParser, self).process(url, raise_on_error, partialcommit)
-
-    def parse_etree(self, parent):
-        for node in list(parent):
-            builder = self._cw.vreg['components'].select(
-                'cw.entityxml.item-builder', self._cw, node=node,
-                parser=self)
-            yield builder.build_item()
-
-    def process_item(self, item, rels):
-        """
-        item and rels are what's returned by the item builder `build_item` method:
-
-        * `item` is an {attribute: value} dictionary
-        * `rels` is for relations and structured as
-           {role: {relation: [(related item, related rels)...]}
-        """
-        entity = self.extid2entity(str(item['cwuri']),  item['cwtype'],
-                                   cwsource=item['cwsource'], item=item)
-        if entity is None:
-            return None
-        if entity.eid in self._processed_entities:
-            return entity
-        self._processed_entities.add(entity.eid)
-        if not (self.created_during_pull(entity) or self.updated_during_pull(entity)):
-            self.notify_updated(entity)
-            attrs = extract_typed_attrs(entity.e_schema, item)
-            # check modification date and compare attribute values to only
-            # update what's actually needed
-            entity.complete(tuple(attrs))
-            mdate = attrs.get('modification_date')
-            if not mdate or mdate > entity.modification_date:
-                attrs = dict( (k, v) for k, v in attrs.iteritems()
-                              if v != getattr(entity, k))
-                if attrs:
-                    entity.set_attributes(**attrs)
-        self.process_relations(entity, rels)
-        return entity
-
-    def process_relations(self, entity, rels):
-        etype = entity.__regid__
-        for (rtype, role, action), rules in self.source.mapping.get(etype, {}).iteritems():
-            try:
-                related_items = rels[role][rtype]
-            except KeyError:
-                self.import_log.record_error('relation %s-%s not found in xml export of %s'
-                                             % (rtype, role, etype))
-                continue
-            try:
-                linker = self.select_linker(action, rtype, role, entity)
-            except RegistryException:
-                self.import_log.record_error('no linker for action %s' % action)
-            else:
-                linker.link_items(related_items, rules)
-
-    def before_entity_copy(self, entity, sourceparams):
-        """IDataFeedParser callback"""
-        attrs = extract_typed_attrs(entity.e_schema, sourceparams['item'])
-        entity.cw_edited.update(attrs)
-
-    def complete_url(self, url, etype=None, known_relations=None):
-        """append to the url's query string information about relation that should
-        be included in the resulting xml, according to source mapping.
-
-        If etype is not specified, try to guess it using the last path part of
-        the url, i.e. the format used by default in cubicweb to map all entities
-        of a given type as in 'http://mysite.org/EntityType'.
-
-        If `known_relations` is given, it should be a dictionary of already
-        known relations, so they don't get queried again.
-        """
-        try:
-            url, qs = url.split('?', 1)
-        except ValueError:
-            qs = ''
-        params = parse_qs(qs)
-        if not 'vid' in params:
-            params['vid'] = ['xml']
-        if etype is None:
-            try:
-                etype = url.rsplit('/', 1)[1]
-            except ValueError:
-                return url + '?' + self._cw.build_url_params(**params)
-            try:
-                etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
-            except KeyError:
-                return url + '?' + self._cw.build_url_params(**params)
-        relations = params.setdefault('relation', [])
-        for rtype, role, _ in self.source.mapping.get(etype, ()):
-            if known_relations and rtype in known_relations.get('role', ()):
-                continue
-            reldef = '%s-%s' % (rtype, role)
-            if not reldef in relations:
-                relations.append(reldef)
-        return url + '?' + self._cw.build_url_params(**params)
-
-    def complete_item(self, item, rels):
-        try:
-            return self._parsed_urls[item['cwuri']]
-        except KeyError:
-            itemurl = self.complete_url(item['cwuri'], item['cwtype'], rels)
-            item_rels = list(self.parse(itemurl))
-            assert len(item_rels) == 1, 'url %s expected to bring back one '\
-                   'and only one entity, got %s' % (itemurl, len(item_rels))
-            self._parsed_urls[item['cwuri']] = item_rels[0]
-            if rels:
-                # XXX (do it better) merge relations
-                new_rels = item_rels[0][1]
-                new_rels.get('subject', {}).update(rels.get('subject', {}))
-                new_rels.get('object', {}).update(rels.get('object', {}))
-            return item_rels[0]
-
-
-class CWEntityXMLItemBuilder(Component):
-    __regid__ = 'cw.entityxml.item-builder'
-
-    def __init__(self, _cw, parser, node, **kwargs):
-        super(CWEntityXMLItemBuilder, self).__init__(_cw, **kwargs)
-        self.parser = parser
-        self.node = node
-
-    def build_item(self):
-        """parse a XML document node and return two dictionaries defining (part
-        of) an entity:
-
-        - {attribute: value}
-        - {role: {relation: [(related item, related rels)...]}
-        """
-        node = self.node
-        item = dict(node.attrib.items())
-        item['cwtype'] = unicode(node.tag)
-        item.setdefault('cwsource', None)
-        try:
-            item['eid'] = typed_eid(item['eid'])
-        except KeyError:
-            # cw < 3.11 compat mode XXX
-            item['eid'] = typed_eid(node.find('eid').text)
-            item['cwuri'] = node.find('cwuri').text
-        rels = {}
-        for child in node:
-            role = child.get('role')
-            if role:
-                # relation
-                related = rels.setdefault(role, {}).setdefault(child.tag, [])
-                related += self.parser.parse_etree(child)
-            elif child.text:
-                # attribute
-                item[child.tag] = unicode(child.text)
-            else:
-                # None attribute (empty tag)
-                item[child.tag] = None
-        return item, rels
-
-
-class CWEntityXMLActionCopy(Component):
-    """implementation of cubicweb entity xml parser's'copy' action
-
-    Takes no option.
-    """
-    __regid__ = 'cw.entityxml.action.copy'
-
-    def __init__(self, _cw, parser, rtype, role, entity=None, **kwargs):
-        super(CWEntityXMLActionCopy, self).__init__(_cw, **kwargs)
-        self.parser = parser
-        self.rtype = rtype
-        self.role = role
-        self.entity = entity
-
-    @classproperty
-    def action(cls):
-        return cls.__regid__.rsplit('.', 1)[-1]
-
-    def check_options(self, options, eid):
-        self._check_no_options(options, eid)
-
-    def _check_no_options(self, options, eid, msg=None):
-        if options:
-            if msg is None:
-                msg = self._cw._("'%s' action doesn't take any options") % self.action
-            raise ValidationError(eid, {rn('options', 'subject'): msg})
-
-    def link_items(self, others, rules):
-        assert not any(x[1] for x in rules), "'copy' action takes no option"
-        ttypes = frozenset([x[0] for x in rules])
-        eids = [] # local eids
-        for item, rels in others:
-            if item['cwtype'] in ttypes:
-                item, rels = self.parser.complete_item(item, rels)
-                other_entity = self.parser.process_item(item, rels)
-                if other_entity is not None:
-                    eids.append(other_entity.eid)
-        if eids:
-            self._set_relation(eids)
-        else:
-            self._clear_relation(ttypes)
-
-    def _clear_relation(self, ttypes):
-        if not self.parser.created_during_pull(self.entity):
-            if len(ttypes) > 1:
-                typerestr = ', Y is IN(%s)' % ','.join(ttypes)
-            else:
-                typerestr = ', Y is %s' % ','.join(ttypes)
-            self._cw.execute('DELETE ' + rtype_role_rql(self.rtype, self.role) + typerestr,
-                             {'x': self.entity.eid})
-
-    def _set_relation(self, eids):
-        assert eids
-        rtype = self.rtype
-        rqlbase = rtype_role_rql(rtype, self.role)
-        eidstr = ','.join(str(eid) for eid in eids)
-        self._cw.execute('DELETE %s, NOT Y eid IN (%s)' % (rqlbase, eidstr),
-                         {'x': self.entity.eid})
-        if self.role == 'object':
-            rql = 'SET %s, Y eid IN (%s), NOT Y %s X' % (rqlbase, eidstr, rtype)
-        else:
-            rql = 'SET %s, Y eid IN (%s), NOT X %s Y' % (rqlbase, eidstr, rtype)
-        self._cw.execute(rql, {'x': self.entity.eid})
-
-
-class CWEntityXMLActionLink(CWEntityXMLActionCopy):
-    """implementation of cubicweb entity xml parser's'link' action
-
-    requires a 'linkattr' option to control search of the linked entity.
-    """
-    __regid__ = 'cw.entityxml.action.link'
-
-    def check_options(self, options, eid):
-        if not 'linkattr' in options:
-            msg = self._cw._("'%s' action requires 'linkattr' option") % self.action
-            raise ValidationError(eid, {rn('options', 'subject'): msg})
-
-    create_when_not_found = False
-
-    def link_items(self, others, rules):
-        for ttype, options in rules:
-            searchattrs = splitstrip(options.get('linkattr', ''))
-            self._related_link(ttype, others, searchattrs)
-
-    def _related_link(self, ttype, others, searchattrs):
-        def issubset(x,y):
-            return all(z in y for z in x)
-        eids = [] # local eids
-        log = self.parser.import_log
-        for item, rels in others:
-            if item['cwtype'] != ttype:
-                continue
-            if not issubset(searchattrs, item):
-                item, rels = self.parser.complete_item(item, rels)
-                if not issubset(searchattrs, item):
-                    log.record_error('missing attribute, got %s expected keys %s'
-                                     % (item, searchattrs))
-                    continue
-            # XXX str() needed with python < 2.6
-            kwargs = dict((str(attr), item[attr]) for attr in searchattrs)
-            targets = self._find_entities(item, kwargs)
-            if len(targets) == 1:
-                entity = targets[0]
-            elif not targets and self.create_when_not_found:
-                entity = self._cw.create_entity(item['cwtype'], **kwargs)
-            else:
-                if len(targets) > 1:
-                    log.record_error('ambiguous link: found %s entity %s with attributes %s'
-                                     % (len(targets), item['cwtype'], kwargs))
-                else:
-                    log.record_error('can not find %s entity with attributes %s'
-                                     % (item['cwtype'], kwargs))
-                continue
-            eids.append(entity.eid)
-            self.parser.process_relations(entity, rels)
-        if eids:
-            self._set_relation(eids)
-        else:
-            self._clear_relation((ttype,))
-
-    def _find_entities(self, item, kwargs):
-        return tuple(self._cw.find_entities(item['cwtype'], **kwargs))
-
-
-class CWEntityXMLActionLinkInState(CWEntityXMLActionLink):
-    """custom implementation of cubicweb entity xml parser's'link' action for
-    in_state relation
-    """
-    __select__ = match_rtype('in_state')
-
-    def check_options(self, options, eid):
-        super(CWEntityXMLActionLinkInState, self).check_options(options, eid)
-        if not 'name' in options['linkattr']:
-            msg = self._cw._("'%s' action for in_state relation should at least have 'linkattr=name' option") % self.action
-            raise ValidationError(eid, {rn('options', 'subject'): msg})
-
-    def _find_entities(self, item, kwargs):
-        assert 'name' in item # XXX else, complete_item
-        state_name = item['name']
-        wf = self.entity.cw_adapt_to('IWorkflowable').current_workflow
-        state = wf.state_by_name(state_name)
-        if state is None:
-            return ()
-        return (state,)
-
-
-class CWEntityXMLActionLinkOrCreate(CWEntityXMLActionLink):
-    """implementation of cubicweb entity xml parser's'link-or-create' action
-
-    requires a 'linkattr' option to control search of the linked entity.
-    """
-    __regid__ = 'cw.entityxml.action.link-or-create'
-    create_when_not_found = True
-
-
-def registration_callback(vreg):
-    vreg.register_all(globals().values(), __name__)
-    global URL_MAPPING
-    URL_MAPPING = {}
-    if vreg.config.apphome:
-        url_mapping_file = osp.join(vreg.config.apphome, 'urlmapping.py')
-        if osp.exists(url_mapping_file):
-            URL_MAPPING = eval(file(url_mapping_file).read())
-            vreg.info('using url mapping %s from %s', URL_MAPPING, url_mapping_file)
--- a/sobjects/supervising.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/sobjects/supervising.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,7 +21,7 @@
 _ = unicode
 
 from cubicweb import UnknownEid
-from cubicweb.selectors import none_rset
+from cubicweb.predicates import none_rset
 from cubicweb.schema import display_name
 from cubicweb.view import Component
 from cubicweb.mail import format_mail
--- a/sobjects/test/data/sobjects/__init__.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/sobjects/test/data/sobjects/__init__.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -16,7 +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/>.
 
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.sobjects.notification import StatusChangeMixIn, NotificationView
 
 class UserStatusChangeView(StatusChangeMixIn, NotificationView):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/test/unittest_cwxmlparser.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,327 @@
+# copyright 2011-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# 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 __future__ import with_statement
+
+from datetime import datetime
+
+from cubicweb.devtools.testlib import CubicWebTC
+
+from cubicweb.sobjects.cwxmlparser import CWEntityXMLParser
+
+orig_parse = CWEntityXMLParser.parse
+
+def parse(self, url):
+    try:
+        url = RELATEDXML[url.split('?')[0]]
+    except KeyError:
+        pass
+    return orig_parse(self, url)
+
+def setUpModule():
+    CWEntityXMLParser.parse = parse
+
+def tearDownModule():
+    CWEntityXMLParser.parse = orig_parse
+
+
+BASEXML = ''.join(u'''
+<rset size="1">
+ <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="system">
+  <login>sthenault</login>
+  <upassword>toto</upassword>
+  <last_login_time>2011-01-25 14:14:06</last_login_time>
+  <creation_date>2010-01-22 10:27:59</creation_date>
+  <modification_date>2011-01-25 14:14:06</modification_date>
+  <use_email role="subject">
+    <EmailAddress cwuri="http://pouet.org/6" eid="6"/>
+  </use_email>
+  <in_group role="subject">
+    <CWGroup cwuri="http://pouet.org/7" eid="7"/>
+    <CWGroup cwuri="http://pouet.org/8" eid="8"/>
+  </in_group>
+  <tags role="object">
+    <Tag cwuri="http://pouet.org/9" eid="9"/>
+    <Tag cwuri="http://pouet.org/10" eid="10"/>
+  </tags>
+  <in_state role="subject">
+    <State cwuri="http://pouet.org/11" eid="11" name="activated"/>
+  </in_state>
+ </CWUser>
+</rset>
+'''.splitlines())
+
+RELATEDXML = {
+    'http://pouet.org/6': u'''
+<rset size="1">
+ <EmailAddress eid="6" cwuri="http://pouet.org/6">
+  <address>syt@logilab.fr</address>
+  <modification_date>2010-04-13 14:35:56</modification_date>
+  <creation_date>2010-04-13 14:35:56</creation_date>
+  <tags role="object">
+    <Tag cwuri="http://pouet.org/9" eid="9"/>
+  </tags>
+ </EmailAddress>
+</rset>
+''',
+    'http://pouet.org/7': u'''
+<rset size="1">
+ <CWGroup eid="7" cwuri="http://pouet.org/7">
+  <name>users</name>
+  <tags role="object">
+    <Tag cwuri="http://pouet.org/9" eid="9"/>
+  </tags>
+ </CWGroup>
+</rset>
+''',
+    'http://pouet.org/8': u'''
+<rset size="1">
+ <CWGroup eid="8" cwuri="http://pouet.org/8">
+  <name>unknown</name>
+ </CWGroup>
+</rset>
+''',
+    'http://pouet.org/9': u'''
+<rset size="1">
+ <Tag eid="9" cwuri="http://pouet.org/9">
+  <name>hop</name>
+ </Tag>
+</rset>
+''',
+    'http://pouet.org/10': u'''
+<rset size="1">
+ <Tag eid="10" cwuri="http://pouet.org/10">
+  <name>unknown</name>
+ </Tag>
+</rset>
+''',
+    }
+
+
+OTHERXML = ''.join(u'''
+<rset size="1">
+ <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="myfeed">
+  <login>sthenault</login>
+  <upassword>toto</upassword>
+  <last_login_time>2011-01-25 14:14:06</last_login_time>
+  <creation_date>2010-01-22 10:27:59</creation_date>
+  <modification_date>2011-01-25 14:14:06</modification_date>
+  <in_group role="subject">
+    <CWGroup cwuri="http://pouet.org/7" eid="7"/>
+  </in_group>
+ </CWUser>
+</rset>
+'''.splitlines()
+)
+
+
+class CWEntityXMLParserTC(CubicWebTC):
+    """/!\ this test use a pre-setup database /!\, if you modify above xml,
+    REMOVE THE DATABASE TEMPLATE else it won't be considered
+    """
+    test_db_id = 'xmlparser'
+    @classmethod
+    def pre_setup_database(cls, session, config):
+        myfeed = session.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+                                   parser=u'cw.entityxml', url=BASEXML)
+        myotherfeed = session.create_entity('CWSource', name=u'myotherfeed', type=u'datafeed',
+                                            parser=u'cw.entityxml', url=OTHERXML)
+        session.commit()
+        myfeed.init_mapping([(('CWUser', 'use_email', '*'),
+                              u'role=subject\naction=copy'),
+                             (('CWUser', 'in_group', '*'),
+                              u'role=subject\naction=link\nlinkattr=name'),
+                             (('CWUser', 'in_state', '*'),
+                              u'role=subject\naction=link\nlinkattr=name'),
+                             (('*', 'tags', '*'),
+                              u'role=object\naction=link-or-create\nlinkattr=name'),
+                            ])
+        myotherfeed.init_mapping([(('CWUser', 'in_group', '*'),
+                                   u'role=subject\naction=link\nlinkattr=name'),
+                                  (('CWUser', 'in_state', '*'),
+                                   u'role=subject\naction=link\nlinkattr=name'),
+                                  ])
+        session.create_entity('Tag', name=u'hop')
+
+    def test_complete_url(self):
+        dfsource = self.repo.sources_by_uri['myfeed']
+        parser = dfsource._get_parser(self.session)
+        self.assertEqual(parser.complete_url('http://www.cubicweb.org/CWUser'),
+                         'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+        self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser'),
+                         'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+        self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
+                         'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
+        self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
+                         'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
+        self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
+                         'http://www.cubicweb.org/?rql=cwuser&relation=hop')
+
+
+    def test_actions(self):
+        dfsource = self.repo.sources_by_uri['myfeed']
+        self.assertEqual(dfsource.mapping,
+                         {u'CWUser': {
+                             (u'in_group', u'subject', u'link'): [
+                                 (u'CWGroup', {u'linkattr': u'name'})],
+                             (u'in_state', u'subject', u'link'): [
+                                 (u'State', {u'linkattr': u'name'})],
+                             (u'tags', u'object', u'link-or-create'): [
+                                 (u'Tag', {u'linkattr': u'name'})],
+                             (u'use_email', u'subject', u'copy'): [
+                                 (u'EmailAddress', {})]
+                             },
+                          u'CWGroup': {
+                             (u'tags', u'object', u'link-or-create'): [
+                                 (u'Tag', {u'linkattr': u'name'})],
+                             },
+                          u'EmailAddress': {
+                             (u'tags', u'object', u'link-or-create'): [
+                                 (u'Tag', {u'linkattr': u'name'})],
+                             },
+                          })
+        session = self.repo.internal_session(safe=True)
+        stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+        self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
+        self.assertEqual(len(stats['created']), 2)
+        self.assertEqual(stats['updated'], set())
+
+        user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
+        self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
+        self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
+        self.assertEqual(user.cwuri, 'http://pouet.org/5')
+        self.assertEqual(user.cw_source[0].name, 'myfeed')
+        self.assertEqual(user.absolute_url(), 'http://pouet.org/5')
+        self.assertEqual(len(user.use_email), 1)
+        # copy action
+        email = user.use_email[0]
+        self.assertEqual(email.address, 'syt@logilab.fr')
+        self.assertEqual(email.cwuri, 'http://pouet.org/6')
+        self.assertEqual(email.absolute_url(), 'http://pouet.org/6')
+        self.assertEqual(email.cw_source[0].name, 'myfeed')
+        self.assertEqual(len(email.reverse_tags), 1)
+        self.assertEqual(email.reverse_tags[0].name, 'hop')
+        # link action
+        self.assertFalse(self.execute('CWGroup X WHERE X name "unknown"'))
+        groups = sorted([g.name for g in user.in_group])
+        self.assertEqual(groups, ['users'])
+        group = user.in_group[0]
+        self.assertEqual(len(group.reverse_tags), 1)
+        self.assertEqual(group.reverse_tags[0].name, 'hop')
+        # link or create action
+        tags = set([(t.name, t.cwuri.replace(str(t.eid), ''), t.cw_source[0].name)
+                    for t in user.reverse_tags])
+        self.assertEqual(tags, set((('hop', 'http://testing.fr/cubicweb/', 'system'),
+                                    ('unknown', 'http://testing.fr/cubicweb/', 'system')))
+                         )
+        session.set_cnxset()
+        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
+            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+        self.assertEqual(stats['created'], set())
+        self.assertEqual(len(stats['updated']), 2)
+        self.repo._type_source_cache.clear()
+        self.repo._extid_cache.clear()
+        session.set_cnxset()
+        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
+            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+        self.assertEqual(stats['created'], set())
+        self.assertEqual(len(stats['updated']), 2)
+        session.commit()
+
+        # test move to system source
+        self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': email.eid})
+        self.commit()
+        rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
+        self.assertEqual(len(rset), 1)
+        e = rset.get_entity(0, 0)
+        self.assertEqual(e.eid, email.eid)
+        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
+                                                             'use-cwuri-as-url': False},
+                                                  'type': 'EmailAddress',
+                                                  'extid': None})
+        self.assertEqual(e.cw_source[0].name, 'system')
+        self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
+        self.commit()
+        # test everything is still fine after source synchronization
+        session.set_cnxset()
+        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
+            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+        rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
+        self.assertEqual(len(rset), 1)
+        e = rset.get_entity(0, 0)
+        self.assertEqual(e.eid, email.eid)
+        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
+                                                             'use-cwuri-as-url': False},
+                                                  'type': 'EmailAddress',
+                                                  'extid': None})
+        self.assertEqual(e.cw_source[0].name, 'system')
+        self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
+        session.commit()
+
+        # test delete entity
+        e.cw_delete()
+        self.commit()
+        # test everything is still fine after source synchronization
+        session.set_cnxset()
+        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
+            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+        rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
+        self.assertEqual(len(rset), 0)
+        rset = self.sexecute('Any X WHERE X use_email E, X login "sthenault"')
+        self.assertEqual(len(rset), 0)
+
+    def test_external_entity(self):
+        dfsource = self.repo.sources_by_uri['myotherfeed']
+        session = self.repo.internal_session(safe=True)
+        stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+        user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
+        self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
+        self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
+        self.assertEqual(user.cwuri, 'http://pouet.org/5')
+        self.assertEqual(user.cw_source[0].name, 'myfeed')
+
+    def test_noerror_missing_fti_attribute(self):
+        dfsource = self.repo.sources_by_uri['myfeed']
+        session = self.repo.internal_session(safe=True)
+        parser = dfsource._get_parser(session)
+        dfsource.process_urls(parser, ['''
+<rset size="1">
+ <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
+  <title>how-to</title>
+ </Card>
+</rset>
+'''], raise_on_error=True)
+
+    def test_noerror_unspecified_date(self):
+        dfsource = self.repo.sources_by_uri['myfeed']
+        session = self.repo.internal_session(safe=True)
+        parser = dfsource._get_parser(session)
+        dfsource.process_urls(parser, ['''
+<rset size="1">
+ <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
+  <title>how-to</title>
+  <content>how-to</content>
+  <synopsis>how-to</synopsis>
+  <creation_date/>
+ </Card>
+</rset>
+'''], raise_on_error=True)
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/sobjects/test/unittest_parsers.py	Thu Feb 23 19:05:36 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,327 +0,0 @@
-# copyright 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import with_statement
-
-from datetime import datetime
-
-from cubicweb.devtools.testlib import CubicWebTC
-
-from cubicweb.sobjects.parsers import CWEntityXMLParser
-
-orig_parse = CWEntityXMLParser.parse
-
-def parse(self, url):
-    try:
-        url = RELATEDXML[url.split('?')[0]]
-    except KeyError:
-        pass
-    return orig_parse(self, url)
-
-def setUpModule():
-    CWEntityXMLParser.parse = parse
-
-def tearDownModule():
-    CWEntityXMLParser.parse = orig_parse
-
-
-BASEXML = ''.join(u'''
-<rset size="1">
- <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="system">
-  <login>sthenault</login>
-  <upassword>toto</upassword>
-  <last_login_time>2011-01-25 14:14:06</last_login_time>
-  <creation_date>2010-01-22 10:27:59</creation_date>
-  <modification_date>2011-01-25 14:14:06</modification_date>
-  <use_email role="subject">
-    <EmailAddress cwuri="http://pouet.org/6" eid="6"/>
-  </use_email>
-  <in_group role="subject">
-    <CWGroup cwuri="http://pouet.org/7" eid="7"/>
-    <CWGroup cwuri="http://pouet.org/8" eid="8"/>
-  </in_group>
-  <tags role="object">
-    <Tag cwuri="http://pouet.org/9" eid="9"/>
-    <Tag cwuri="http://pouet.org/10" eid="10"/>
-  </tags>
-  <in_state role="subject">
-    <State cwuri="http://pouet.org/11" eid="11" name="activated"/>
-  </in_state>
- </CWUser>
-</rset>
-'''.splitlines())
-
-RELATEDXML = {
-    'http://pouet.org/6': u'''
-<rset size="1">
- <EmailAddress eid="6" cwuri="http://pouet.org/6">
-  <address>syt@logilab.fr</address>
-  <modification_date>2010-04-13 14:35:56</modification_date>
-  <creation_date>2010-04-13 14:35:56</creation_date>
-  <tags role="object">
-    <Tag cwuri="http://pouet.org/9" eid="9"/>
-  </tags>
- </EmailAddress>
-</rset>
-''',
-    'http://pouet.org/7': u'''
-<rset size="1">
- <CWGroup eid="7" cwuri="http://pouet.org/7">
-  <name>users</name>
-  <tags role="object">
-    <Tag cwuri="http://pouet.org/9" eid="9"/>
-  </tags>
- </CWGroup>
-</rset>
-''',
-    'http://pouet.org/8': u'''
-<rset size="1">
- <CWGroup eid="8" cwuri="http://pouet.org/8">
-  <name>unknown</name>
- </CWGroup>
-</rset>
-''',
-    'http://pouet.org/9': u'''
-<rset size="1">
- <Tag eid="9" cwuri="http://pouet.org/9">
-  <name>hop</name>
- </Tag>
-</rset>
-''',
-    'http://pouet.org/10': u'''
-<rset size="1">
- <Tag eid="10" cwuri="http://pouet.org/10">
-  <name>unknown</name>
- </Tag>
-</rset>
-''',
-    }
-
-
-OTHERXML = ''.join(u'''
-<rset size="1">
- <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="myfeed">
-  <login>sthenault</login>
-  <upassword>toto</upassword>
-  <last_login_time>2011-01-25 14:14:06</last_login_time>
-  <creation_date>2010-01-22 10:27:59</creation_date>
-  <modification_date>2011-01-25 14:14:06</modification_date>
-  <in_group role="subject">
-    <CWGroup cwuri="http://pouet.org/7" eid="7"/>
-  </in_group>
- </CWUser>
-</rset>
-'''.splitlines()
-)
-
-
-class CWEntityXMLParserTC(CubicWebTC):
-    """/!\ this test use a pre-setup database /!\, if you modify above xml,
-    REMOVE THE DATABASE TEMPLATE else it won't be considered
-    """
-    test_db_id = 'xmlparser'
-    @classmethod
-    def pre_setup_database(cls, session, config):
-        myfeed = session.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
-                                   parser=u'cw.entityxml', url=BASEXML)
-        myotherfeed = session.create_entity('CWSource', name=u'myotherfeed', type=u'datafeed',
-                                            parser=u'cw.entityxml', url=OTHERXML)
-        session.commit()
-        myfeed.init_mapping([(('CWUser', 'use_email', '*'),
-                              u'role=subject\naction=copy'),
-                             (('CWUser', 'in_group', '*'),
-                              u'role=subject\naction=link\nlinkattr=name'),
-                             (('CWUser', 'in_state', '*'),
-                              u'role=subject\naction=link\nlinkattr=name'),
-                             (('*', 'tags', '*'),
-                              u'role=object\naction=link-or-create\nlinkattr=name'),
-                            ])
-        myotherfeed.init_mapping([(('CWUser', 'in_group', '*'),
-                                   u'role=subject\naction=link\nlinkattr=name'),
-                                  (('CWUser', 'in_state', '*'),
-                                   u'role=subject\naction=link\nlinkattr=name'),
-                                  ])
-        session.create_entity('Tag', name=u'hop')
-
-    def test_complete_url(self):
-        dfsource = self.repo.sources_by_uri['myfeed']
-        parser = dfsource._get_parser(self.session)
-        self.assertEqual(parser.complete_url('http://www.cubicweb.org/CWUser'),
-                         'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=xml')
-        self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser'),
-                         'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=xml')
-        self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
-                         'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
-        self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
-                         'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
-        self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
-                         'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=xml')
-
-
-    def test_actions(self):
-        dfsource = self.repo.sources_by_uri['myfeed']
-        self.assertEqual(dfsource.mapping,
-                         {u'CWUser': {
-                             (u'in_group', u'subject', u'link'): [
-                                 (u'CWGroup', {u'linkattr': u'name'})],
-                             (u'in_state', u'subject', u'link'): [
-                                 (u'State', {u'linkattr': u'name'})],
-                             (u'tags', u'object', u'link-or-create'): [
-                                 (u'Tag', {u'linkattr': u'name'})],
-                             (u'use_email', u'subject', u'copy'): [
-                                 (u'EmailAddress', {})]
-                             },
-                          u'CWGroup': {
-                             (u'tags', u'object', u'link-or-create'): [
-                                 (u'Tag', {u'linkattr': u'name'})],
-                             },
-                          u'EmailAddress': {
-                             (u'tags', u'object', u'link-or-create'): [
-                                 (u'Tag', {u'linkattr': u'name'})],
-                             },
-                          })
-        session = self.repo.internal_session(safe=True)
-        stats = dfsource.pull_data(session, force=True, raise_on_error=True)
-        self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
-        self.assertEqual(len(stats['created']), 2)
-        self.assertEqual(stats['updated'], set())
-
-        user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
-        self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
-        self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
-        self.assertEqual(user.cwuri, 'http://pouet.org/5')
-        self.assertEqual(user.cw_source[0].name, 'myfeed')
-        self.assertEqual(user.absolute_url(), 'http://pouet.org/5')
-        self.assertEqual(len(user.use_email), 1)
-        # copy action
-        email = user.use_email[0]
-        self.assertEqual(email.address, 'syt@logilab.fr')
-        self.assertEqual(email.cwuri, 'http://pouet.org/6')
-        self.assertEqual(email.absolute_url(), 'http://pouet.org/6')
-        self.assertEqual(email.cw_source[0].name, 'myfeed')
-        self.assertEqual(len(email.reverse_tags), 1)
-        self.assertEqual(email.reverse_tags[0].name, 'hop')
-        # link action
-        self.assertFalse(self.execute('CWGroup X WHERE X name "unknown"'))
-        groups = sorted([g.name for g in user.in_group])
-        self.assertEqual(groups, ['users'])
-        group = user.in_group[0]
-        self.assertEqual(len(group.reverse_tags), 1)
-        self.assertEqual(group.reverse_tags[0].name, 'hop')
-        # link or create action
-        tags = set([(t.name, t.cwuri.replace(str(t.eid), ''), t.cw_source[0].name)
-                    for t in user.reverse_tags])
-        self.assertEqual(tags, set((('hop', 'http://testing.fr/cubicweb/', 'system'),
-                                    ('unknown', 'http://testing.fr/cubicweb/', 'system')))
-                         )
-        session.set_cnxset()
-        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
-            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
-        self.assertEqual(stats['created'], set())
-        self.assertEqual(len(stats['updated']), 2)
-        self.repo._type_source_cache.clear()
-        self.repo._extid_cache.clear()
-        session.set_cnxset()
-        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
-            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
-        self.assertEqual(stats['created'], set())
-        self.assertEqual(len(stats['updated']), 2)
-        session.commit()
-
-        # test move to system source
-        self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': email.eid})
-        self.commit()
-        rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
-        self.assertEqual(len(rset), 1)
-        e = rset.get_entity(0, 0)
-        self.assertEqual(e.eid, email.eid)
-        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
-                                                             'use-cwuri-as-url': False},
-                                                  'type': 'EmailAddress',
-                                                  'extid': None})
-        self.assertEqual(e.cw_source[0].name, 'system')
-        self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
-        self.commit()
-        # test everything is still fine after source synchronization
-        session.set_cnxset()
-        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
-            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
-        rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
-        self.assertEqual(len(rset), 1)
-        e = rset.get_entity(0, 0)
-        self.assertEqual(e.eid, email.eid)
-        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
-                                                             'use-cwuri-as-url': False},
-                                                  'type': 'EmailAddress',
-                                                  'extid': None})
-        self.assertEqual(e.cw_source[0].name, 'system')
-        self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
-        session.commit()
-
-        # test delete entity
-        e.cw_delete()
-        self.commit()
-        # test everything is still fine after source synchronization
-        session.set_cnxset()
-        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
-            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
-        rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
-        self.assertEqual(len(rset), 0)
-        rset = self.sexecute('Any X WHERE X use_email E, X login "sthenault"')
-        self.assertEqual(len(rset), 0)
-
-    def test_external_entity(self):
-        dfsource = self.repo.sources_by_uri['myotherfeed']
-        session = self.repo.internal_session(safe=True)
-        stats = dfsource.pull_data(session, force=True, raise_on_error=True)
-        user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
-        self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
-        self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
-        self.assertEqual(user.cwuri, 'http://pouet.org/5')
-        self.assertEqual(user.cw_source[0].name, 'myfeed')
-
-    def test_noerror_missing_fti_attribute(self):
-        dfsource = self.repo.sources_by_uri['myfeed']
-        session = self.repo.internal_session(safe=True)
-        parser = dfsource._get_parser(session)
-        dfsource.process_urls(parser, ['''
-<rset size="1">
- <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
-  <title>how-to</title>
- </Card>
-</rset>
-'''], raise_on_error=True)
-
-    def test_noerror_unspecified_date(self):
-        dfsource = self.repo.sources_by_uri['myfeed']
-        session = self.repo.internal_session(safe=True)
-        parser = dfsource._get_parser(session)
-        dfsource.process_urls(parser, ['''
-<rset size="1">
- <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
-  <title>how-to</title>
-  <content>how-to</content>
-  <synopsis>how-to</synopsis>
-  <creation_date/>
- </Card>
-</rset>
-'''], raise_on_error=True)
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/test/unittest_entity.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/test/unittest_entity.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -19,7 +19,10 @@
 """unit tests for cubicweb.web.views.entities module"""
 
 from datetime import datetime
+
 from logilab.common import tempattr
+from logilab.common.decorators import clear_cache
+
 from cubicweb import Binary, Unauthorized
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.mttransforms import HAS_TAL
@@ -316,12 +319,22 @@
                          'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, '
                          'S login AA, S firstname AB, S surname AC, S modification_date AD')
         self.login('anon')
-        email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
-        rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
-        self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
-                         'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, '
-                         'S login AA, S firstname AB, S surname AC, S modification_date AD, '
-                         'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
+        rperms = self.schema['EmailAddress'].permissions['read']
+        clear_cache(self.schema['EmailAddress'], 'get_groups')
+        clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+        self.schema['EmailAddress'].permissions['read'] = ('managers', 'users', 'guests',)
+        try:
+            email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
+            rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
+            self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+                             'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, '
+                             'S login AA, S firstname AB, S surname AC, S modification_date AD, '
+                             'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
+        finally:
+            clear_cache(self.schema['EmailAddress'], 'get_groups')
+            clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+            self.schema['EmailAddress'].permissions['read'] = rperms
+
 
     def test_unrelated_rql_security_nonexistant(self):
         self.login('anon')
@@ -459,31 +472,40 @@
                           1)
 
     def test_unrelated_security(self):
-        email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
-        rset = email.unrelated('use_email', 'CWUser', 'object')
-        self.assertEqual([x.login for x in rset.entities()], [u'admin', u'anon'])
-        user = self.request().user
-        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEqual([x.address for x in rset.entities()], [u'hop'])
-        req = self.request()
-        self.create_user(req, 'toto')
-        self.login('toto')
-        email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
-        rset = email.unrelated('use_email', 'CWUser', 'object')
-        self.assertEqual([x.login for x in rset.entities()], ['toto'])
-        user = self.request().user
-        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEqual([x.address for x in rset.entities()], ['hop'])
-        user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
-        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEqual([x.address for x in rset.entities()], [])
-        self.login('anon')
-        email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
-        rset = email.unrelated('use_email', 'CWUser', 'object')
-        self.assertEqual([x.login for x in rset.entities()], [])
-        user = self.request().user
-        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEqual([x.address for x in rset.entities()], [])
+        rperms = self.schema['EmailAddress'].permissions['read']
+        clear_cache(self.schema['EmailAddress'], 'get_groups')
+        clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+        self.schema['EmailAddress'].permissions['read'] = ('managers', 'users', 'guests',)
+        try:
+            email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
+            rset = email.unrelated('use_email', 'CWUser', 'object')
+            self.assertEqual([x.login for x in rset.entities()], [u'admin', u'anon'])
+            user = self.request().user
+            rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+            self.assertEqual([x.address for x in rset.entities()], [u'hop'])
+            req = self.request()
+            self.create_user(req, 'toto')
+            self.login('toto')
+            email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
+            rset = email.unrelated('use_email', 'CWUser', 'object')
+            self.assertEqual([x.login for x in rset.entities()], ['toto'])
+            user = self.request().user
+            rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+            self.assertEqual([x.address for x in rset.entities()], ['hop'])
+            user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
+            rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+            self.assertEqual([x.address for x in rset.entities()], [])
+            self.login('anon')
+            email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
+            rset = email.unrelated('use_email', 'CWUser', 'object')
+            self.assertEqual([x.login for x in rset.entities()], [])
+            user = self.request().user
+            rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+            self.assertEqual([x.address for x in rset.entities()], [])
+        finally:
+            clear_cache(self.schema['EmailAddress'], 'get_groups')
+            clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+            self.schema['EmailAddress'].permissions['read'] = rperms
 
     def test_unrelated_new_entity(self):
         e = self.vreg['etypes'].etype_class('CWUser')(self.request())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_predicates.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,342 @@
+# 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/>.
+"""unit tests for selectors mechanism"""
+from __future__ import with_statement
+
+from operator import eq, lt, le, gt
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb import Binary
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.predicates import (is_instance, adaptable, match_kwargs, match_user_groups,
+                                multi_lines_rset, score_entity, is_in_state,
+                                rql_condition, relation_possible)
+from cubicweb.selectors import on_transition # XXX on_transition is deprecated
+from cubicweb.web import action
+
+
+
+class ImplementsSelectorTC(CubicWebTC):
+    def test_etype_priority(self):
+        req = self.request()
+        f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
+        rset = f.as_rset()
+        anyscore = is_instance('Any')(f.__class__, req, rset=rset)
+        idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
+        self.assertTrue(idownscore > anyscore, (idownscore, anyscore))
+        filescore = is_instance('File')(f.__class__, req, rset=rset)
+        self.assertTrue(filescore > idownscore, (filescore, idownscore))
+
+    def test_etype_inheritance_no_yams_inheritance(self):
+        cls = self.vreg['etypes'].etype_class('Personne')
+        self.assertFalse(is_instance('Societe').score_class(cls, self.request()))
+
+    def test_yams_inheritance(self):
+        cls = self.vreg['etypes'].etype_class('Transition')
+        self.assertEqual(is_instance('BaseTransition').score_class(cls, self.request()),
+                          3)
+
+    def test_outer_join(self):
+        req = self.request()
+        rset = req.execute('Any U,B WHERE B? bookmarked_by U, U login "anon"')
+        self.assertEqual(is_instance('Bookmark')(None, req, rset=rset, row=0, col=1),
+                         0)
+
+
+class WorkflowSelectorTC(CubicWebTC):
+    def _commit(self):
+        self.commit()
+        self.wf_entity.cw_clear_all_caches()
+
+    def setup_database(self):
+        wf = self.shell().add_workflow("wf_test", 'StateFull', default=True)
+        created   = wf.add_state('created', initial=True)
+        validated = wf.add_state('validated')
+        abandoned = wf.add_state('abandoned')
+        wf.add_transition('validate', created, validated, ('managers',))
+        wf.add_transition('forsake', (created, validated,), abandoned, ('managers',))
+
+    def setUp(self):
+        super(WorkflowSelectorTC, self).setUp()
+        self.req = self.request()
+        self.wf_entity = self.req.create_entity('StateFull', name=u'')
+        self.rset = self.wf_entity.as_rset()
+        self.adapter = self.wf_entity.cw_adapt_to('IWorkflowable')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'created')
+        # enable debug mode to state/transition validation on the fly
+        self.vreg.config.debugmode = True
+
+    def tearDown(self):
+        self.vreg.config.debugmode = False
+        super(WorkflowSelectorTC, self).tearDown()
+
+    def test_is_in_state(self):
+        for state in ('created', 'validated', 'abandoned'):
+            selector = is_in_state(state)
+            self.assertEqual(selector(None, self.req, rset=self.rset),
+                             state=="created")
+
+        self.adapter.fire_transition('validate')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'validated')
+
+        selector = is_in_state('created')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        selector = is_in_state('validated')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+        selector = is_in_state('validated', 'abandoned')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+        selector = is_in_state('abandoned')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+        self.adapter.fire_transition('forsake')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'abandoned')
+
+        selector = is_in_state('created')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        selector = is_in_state('validated')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        selector = is_in_state('validated', 'abandoned')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+        self.assertEqual(self.adapter.state, 'abandoned')
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+
+    def test_is_in_state_unvalid_names(self):
+        selector = is_in_state("unknown")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, rset=self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown state(s): unknown")
+        selector = is_in_state("weird", "unknown", "created", "weird")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, rset=self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown state(s): unknown,weird")
+
+    def test_on_transition(self):
+        for transition in ('validate', 'forsake'):
+            selector = on_transition(transition)
+            self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+        self.adapter.fire_transition('validate')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'validated')
+
+        selector = on_transition("validate")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+        selector = on_transition("validate", "forsake")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+        selector = on_transition("forsake")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+        self.adapter.fire_transition('forsake')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'abandoned')
+
+        selector = on_transition("validate")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        selector = on_transition("validate", "forsake")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+        selector = on_transition("forsake")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+
+    def test_on_transition_unvalid_names(self):
+        selector = on_transition("unknown")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, rset=self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown transition(s): unknown")
+        selector = on_transition("weird", "unknown", "validate", "weird")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, rset=self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown transition(s): unknown,weird")
+
+    def test_on_transition_with_no_effect(self):
+        """selector will not be triggered with `change_state()`"""
+        self.adapter.change_state('validated')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'validated')
+
+        selector = on_transition("validate")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        selector = on_transition("validate", "forsake")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        selector = on_transition("forsake")
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+
+class RelationPossibleTC(CubicWebTC):
+
+    def test_rqlst_1(self):
+        req = self.request()
+        selector = relation_possible('in_group')
+        select = self.vreg.parse(req, 'Any X WHERE X is CWUser').children[0]
+        score = selector(None, req, rset=1,
+                         select=select, filtered_variable=select.defined_vars['X'])
+        self.assertEqual(score, 1)
+
+    def test_rqlst_2(self):
+        req = self.request()
+        selector = relation_possible('in_group')
+        select = self.vreg.parse(req, 'Any 1, COUNT(X) WHERE X is CWUser, X creation_date XD, '
+                                 'Y creation_date YD, Y is CWGroup '
+                                 'HAVING DAY(XD)=DAY(YD)').children[0]
+        score = selector(None, req, rset=1,
+                         select=select, filtered_variable=select.defined_vars['X'])
+        self.assertEqual(score, 1)
+
+
+class MatchUserGroupsTC(CubicWebTC):
+    def test_owners_group(self):
+        """tests usage of 'owners' group with match_user_group"""
+        class SomeAction(action.Action):
+            __regid__ = 'yo'
+            category = 'foo'
+            __select__ = match_user_groups('owners')
+        self.vreg._loadedmods[__name__] = {}
+        self.vreg.register(SomeAction)
+        SomeAction.__registered__(self.vreg['actions'])
+        self.assertTrue(SomeAction in self.vreg['actions']['yo'], self.vreg['actions'])
+        try:
+            # login as a simple user
+            req = self.request()
+            self.create_user(req, 'john')
+            self.login('john')
+            # it should not be possible to use SomeAction not owned objects
+            req = self.request()
+            rset = req.execute('Any G WHERE G is CWGroup, G name "managers"')
+            self.assertFalse('yo' in dict(self.pactions(req, rset)))
+            # insert a new card, and check that we can use SomeAction on our object
+            self.execute('INSERT Card C: C title "zoubidou"')
+            self.commit()
+            req = self.request()
+            rset = req.execute('Card C WHERE C title "zoubidou"')
+            self.assertTrue('yo' in dict(self.pactions(req, rset)), self.pactions(req, rset))
+            # make sure even managers can't use the action
+            self.restore_connection()
+            req = self.request()
+            rset = req.execute('Card C WHERE C title "zoubidou"')
+            self.assertFalse('yo' in dict(self.pactions(req, rset)))
+        finally:
+            del self.vreg[SomeAction.__registry__][SomeAction.__regid__]
+
+
+class MultiLinesRsetSelectorTC(CubicWebTC):
+    def setUp(self):
+        super(MultiLinesRsetSelectorTC, self).setUp()
+        self.req = self.request()
+        self.req.execute('INSERT CWGroup G: G name "group1"')
+        self.req.execute('INSERT CWGroup G: G name "group2"')
+        self.commit()
+        self.rset = self.req.execute('Any G WHERE G is CWGroup')
+
+    def test_default_op_in_selector(self):
+        expected = len(self.rset)
+        selector = multi_lines_rset(expected)
+        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected + 1)
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected - 1)
+        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+        self.assertEqual(selector(None, self.req, None), 0)
+
+    def test_without_rset(self):
+        expected = len(self.rset)
+        selector = multi_lines_rset(expected)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected + 1)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected - 1)
+        self.assertEqual(selector(None, self.req, None), 0)
+
+    def test_with_operators(self):
+        expected = len(self.rset)
+
+        # Format     'expected', 'operator', 'assert'
+        testdata = (( expected,         eq,        1),
+                    ( expected+1,       eq,        0),
+                    ( expected-1,       eq,        0),
+                    ( expected,         le,        1),
+                    ( expected+1,       le,        1),
+                    ( expected-1,       le,        0),
+                    ( expected-1,       gt,        1),
+                    ( expected,         gt,        0),
+                    ( expected+1,       gt,        0),
+                    ( expected+1,       lt,        1),
+                    ( expected,         lt,        0),
+                    ( expected-1,       lt,        0))
+
+        for (expected, operator, assertion) in testdata:
+            selector = multi_lines_rset(expected, operator)
+            yield self.assertEqual, selector(None, self.req, rset=self.rset), assertion
+
+    def test_match_kwargs_default(self):
+        selector = match_kwargs( set( ('a', 'b') ) )
+        self.assertEqual(selector(None, None, a=1, b=2), 2)
+        self.assertEqual(selector(None, None, a=1), 0)
+        self.assertEqual(selector(None, None, c=1), 0)
+        self.assertEqual(selector(None, None, a=1, c=1), 0)
+
+    def test_match_kwargs_any(self):
+        selector = match_kwargs( set( ('a', 'b') ), mode='any')
+        self.assertEqual(selector(None, None, a=1, b=2), 2)
+        self.assertEqual(selector(None, None, a=1), 1)
+        self.assertEqual(selector(None, None, c=1), 0)
+        self.assertEqual(selector(None, None, a=1, c=1), 1)
+
+
+class ScoreEntitySelectorTC(CubicWebTC):
+
+    def test_intscore_entity_selector(self):
+        req = self.request()
+        rset = req.execute('Any E WHERE E eid 1')
+        selector = score_entity(lambda x: None)
+        self.assertEqual(selector(None, req, rset=rset), 0)
+        selector = score_entity(lambda x: "something")
+        self.assertEqual(selector(None, req, rset=rset), 1)
+        selector = score_entity(lambda x: object)
+        self.assertEqual(selector(None, req, rset=rset), 1)
+        rset = req.execute('Any G LIMIT 2 WHERE G is CWGroup')
+        selector = score_entity(lambda x: 10)
+        self.assertEqual(selector(None, req, rset=rset), 20)
+        selector = score_entity(lambda x: 10, mode='any')
+        self.assertEqual(selector(None, req, rset=rset), 10)
+
+    def test_rql_condition_entity(self):
+        req = self.request()
+        selector = rql_condition('X identity U')
+        rset = req.user.as_rset()
+        self.assertEqual(selector(None, req, rset=rset), 1)
+        self.assertEqual(selector(None, req, entity=req.user), 1)
+        self.assertEqual(selector(None, req), 0)
+
+    def test_rql_condition_user(self):
+        req = self.request()
+        selector = rql_condition('U login "admin"', user_condition=True)
+        self.assertEqual(selector(None, req), 1)
+        selector = rql_condition('U login "toto"', user_condition=True)
+        self.assertEqual(selector(None, req), 0)
+
+if __name__ == '__main__':
+    unittest_main()
+
--- a/test/unittest_req.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/test/unittest_req.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/test/unittest_selectors.py	Thu Feb 23 19:05:36 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,455 +0,0 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unit tests for selectors mechanism"""
-from __future__ import with_statement
-
-from operator import eq, lt, le, gt
-from logilab.common.testlib import TestCase, unittest_main
-
-from cubicweb import Binary
-from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.appobject import Selector, AndSelector, OrSelector
-from cubicweb.selectors import (is_instance, adaptable, match_kwargs, match_user_groups,
-                                multi_lines_rset, score_entity, is_in_state,
-                                on_transition, rql_condition, relation_possible)
-from cubicweb.web import action
-
-
-class _1_(Selector):
-    def __call__(self, *args, **kwargs):
-        return 1
-
-class _0_(Selector):
-    def __call__(self, *args, **kwargs):
-        return 0
-
-def _2_(*args, **kwargs):
-    return 2
-
-
-class SelectorsTC(TestCase):
-    def test_basic_and(self):
-        selector = _1_() & _1_()
-        self.assertEqual(selector(None), 2)
-        selector = _1_() & _0_()
-        self.assertEqual(selector(None), 0)
-        selector = _0_() & _1_()
-        self.assertEqual(selector(None), 0)
-
-    def test_basic_or(self):
-        selector = _1_() | _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _1_() | _0_()
-        self.assertEqual(selector(None), 1)
-        selector = _0_() | _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _0_() | _0_()
-        self.assertEqual(selector(None), 0)
-
-    def test_selector_and_function(self):
-        selector = _1_() & _2_
-        self.assertEqual(selector(None), 3)
-        selector = _2_ & _1_()
-        self.assertEqual(selector(None), 3)
-
-    def test_three_and(self):
-        selector = _1_() & _1_() & _1_()
-        self.assertEqual(selector(None), 3)
-        selector = _1_() & _0_() & _1_()
-        self.assertEqual(selector(None), 0)
-        selector = _0_() & _1_() & _1_()
-        self.assertEqual(selector(None), 0)
-
-    def test_three_or(self):
-        selector = _1_() | _1_() | _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _1_() | _0_() | _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _0_() | _1_() | _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _0_() | _0_() | _0_()
-        self.assertEqual(selector(None), 0)
-
-    def test_composition(self):
-        selector = (_1_() & _1_()) & (_1_() & _1_())
-        self.assertTrue(isinstance(selector, AndSelector))
-        self.assertEqual(len(selector.selectors), 4)
-        self.assertEqual(selector(None), 4)
-        selector = (_1_() & _0_()) | (_1_() & _1_())
-        self.assertTrue(isinstance(selector, OrSelector))
-        self.assertEqual(len(selector.selectors), 2)
-        self.assertEqual(selector(None), 2)
-
-    def test_search_selectors(self):
-        sel = is_instance('something')
-        self.assertIs(sel.search_selector(is_instance), sel)
-        csel = AndSelector(sel, Selector())
-        self.assertIs(csel.search_selector(is_instance), sel)
-        csel = AndSelector(Selector(), sel)
-        self.assertIs(csel.search_selector(is_instance), sel)
-        self.assertIs(csel.search_selector((AndSelector, OrSelector)), csel)
-        self.assertIs(csel.search_selector((OrSelector, AndSelector)), csel)
-        self.assertIs(csel.search_selector((is_instance, score_entity)),  sel)
-        self.assertIs(csel.search_selector((score_entity, is_instance)), sel)
-
-    def test_inplace_and(self):
-        selector = _1_()
-        selector &= _1_()
-        selector &= _1_()
-        self.assertEqual(selector(None), 3)
-        selector = _1_()
-        selector &= _0_()
-        selector &= _1_()
-        self.assertEqual(selector(None), 0)
-        selector = _0_()
-        selector &= _1_()
-        selector &= _1_()
-        self.assertEqual(selector(None), 0)
-        selector = _0_()
-        selector &= _0_()
-        selector &= _0_()
-        self.assertEqual(selector(None), 0)
-
-    def test_inplace_or(self):
-        selector = _1_()
-        selector |= _1_()
-        selector |= _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _1_()
-        selector |= _0_()
-        selector |= _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _0_()
-        selector |= _1_()
-        selector |= _1_()
-        self.assertEqual(selector(None), 1)
-        selector = _0_()
-        selector |= _0_()
-        selector |= _0_()
-        self.assertEqual(selector(None), 0)
-
-
-class ImplementsSelectorTC(CubicWebTC):
-    def test_etype_priority(self):
-        req = self.request()
-        f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
-        rset = f.as_rset()
-        anyscore = is_instance('Any')(f.__class__, req, rset=rset)
-        idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
-        self.assertTrue(idownscore > anyscore, (idownscore, anyscore))
-        filescore = is_instance('File')(f.__class__, req, rset=rset)
-        self.assertTrue(filescore > idownscore, (filescore, idownscore))
-
-    def test_etype_inheritance_no_yams_inheritance(self):
-        cls = self.vreg['etypes'].etype_class('Personne')
-        self.assertFalse(is_instance('Societe').score_class(cls, self.request()))
-
-    def test_yams_inheritance(self):
-        cls = self.vreg['etypes'].etype_class('Transition')
-        self.assertEqual(is_instance('BaseTransition').score_class(cls, self.request()),
-                          3)
-
-    def test_outer_join(self):
-        req = self.request()
-        rset = req.execute('Any U,B WHERE B? bookmarked_by U, U login "anon"')
-        self.assertEqual(is_instance('Bookmark')(None, req, rset=rset, row=0, col=1),
-                         0)
-
-
-class WorkflowSelectorTC(CubicWebTC):
-    def _commit(self):
-        self.commit()
-        self.wf_entity.cw_clear_all_caches()
-
-    def setup_database(self):
-        wf = self.shell().add_workflow("wf_test", 'StateFull', default=True)
-        created   = wf.add_state('created', initial=True)
-        validated = wf.add_state('validated')
-        abandoned = wf.add_state('abandoned')
-        wf.add_transition('validate', created, validated, ('managers',))
-        wf.add_transition('forsake', (created, validated,), abandoned, ('managers',))
-
-    def setUp(self):
-        super(WorkflowSelectorTC, self).setUp()
-        self.req = self.request()
-        self.wf_entity = self.req.create_entity('StateFull', name=u'')
-        self.rset = self.wf_entity.as_rset()
-        self.adapter = self.wf_entity.cw_adapt_to('IWorkflowable')
-        self._commit()
-        self.assertEqual(self.adapter.state, 'created')
-        # enable debug mode to state/transition validation on the fly
-        self.vreg.config.debugmode = True
-
-    def tearDown(self):
-        self.vreg.config.debugmode = False
-        super(WorkflowSelectorTC, self).tearDown()
-
-    def test_is_in_state(self):
-        for state in ('created', 'validated', 'abandoned'):
-            selector = is_in_state(state)
-            self.assertEqual(selector(None, self.req, rset=self.rset),
-                             state=="created")
-
-        self.adapter.fire_transition('validate')
-        self._commit()
-        self.assertEqual(self.adapter.state, 'validated')
-
-        selector = is_in_state('created')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        selector = is_in_state('validated')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-        selector = is_in_state('validated', 'abandoned')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-        selector = is_in_state('abandoned')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
-        self.adapter.fire_transition('forsake')
-        self._commit()
-        self.assertEqual(self.adapter.state, 'abandoned')
-
-        selector = is_in_state('created')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        selector = is_in_state('validated')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        selector = is_in_state('validated', 'abandoned')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-        self.assertEqual(self.adapter.state, 'abandoned')
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-
-    def test_is_in_state_unvalid_names(self):
-        selector = is_in_state("unknown")
-        with self.assertRaises(ValueError) as cm:
-            selector(None, self.req, rset=self.rset)
-        self.assertEqual(str(cm.exception),
-                         "wf_test: unknown state(s): unknown")
-        selector = is_in_state("weird", "unknown", "created", "weird")
-        with self.assertRaises(ValueError) as cm:
-            selector(None, self.req, rset=self.rset)
-        self.assertEqual(str(cm.exception),
-                         "wf_test: unknown state(s): unknown,weird")
-
-    def test_on_transition(self):
-        for transition in ('validate', 'forsake'):
-            selector = on_transition(transition)
-            self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
-        self.adapter.fire_transition('validate')
-        self._commit()
-        self.assertEqual(self.adapter.state, 'validated')
-
-        selector = on_transition("validate")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-        selector = on_transition("validate", "forsake")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-        selector = on_transition("forsake")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
-        self.adapter.fire_transition('forsake')
-        self._commit()
-        self.assertEqual(self.adapter.state, 'abandoned')
-
-        selector = on_transition("validate")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        selector = on_transition("validate", "forsake")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-        selector = on_transition("forsake")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-
-    def test_on_transition_unvalid_names(self):
-        selector = on_transition("unknown")
-        with self.assertRaises(ValueError) as cm:
-            selector(None, self.req, rset=self.rset)
-        self.assertEqual(str(cm.exception),
-                         "wf_test: unknown transition(s): unknown")
-        selector = on_transition("weird", "unknown", "validate", "weird")
-        with self.assertRaises(ValueError) as cm:
-            selector(None, self.req, rset=self.rset)
-        self.assertEqual(str(cm.exception),
-                         "wf_test: unknown transition(s): unknown,weird")
-
-    def test_on_transition_with_no_effect(self):
-        """selector will not be triggered with `change_state()`"""
-        self.adapter.change_state('validated')
-        self._commit()
-        self.assertEqual(self.adapter.state, 'validated')
-
-        selector = on_transition("validate")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        selector = on_transition("validate", "forsake")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        selector = on_transition("forsake")
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
-
-class RelationPossibleTC(CubicWebTC):
-
-    def test_rqlst_1(self):
-        req = self.request()
-        selector = relation_possible('in_group')
-        select = self.vreg.parse(req, 'Any X WHERE X is CWUser').children[0]
-        score = selector(None, req, rset=1,
-                         select=select, filtered_variable=select.defined_vars['X'])
-        self.assertEqual(score, 1)
-
-    def test_rqlst_2(self):
-        req = self.request()
-        selector = relation_possible('in_group')
-        select = self.vreg.parse(req, 'Any 1, COUNT(X) WHERE X is CWUser, X creation_date XD, '
-                                 'Y creation_date YD, Y is CWGroup '
-                                 'HAVING DAY(XD)=DAY(YD)').children[0]
-        score = selector(None, req, rset=1,
-                         select=select, filtered_variable=select.defined_vars['X'])
-        self.assertEqual(score, 1)
-
-
-class MatchUserGroupsTC(CubicWebTC):
-    def test_owners_group(self):
-        """tests usage of 'owners' group with match_user_group"""
-        class SomeAction(action.Action):
-            __regid__ = 'yo'
-            category = 'foo'
-            __select__ = match_user_groups('owners')
-        self.vreg._loadedmods[__name__] = {}
-        self.vreg.register(SomeAction)
-        SomeAction.__registered__(self.vreg['actions'])
-        self.assertTrue(SomeAction in self.vreg['actions']['yo'], self.vreg['actions'])
-        try:
-            # login as a simple user
-            req = self.request()
-            self.create_user(req, 'john')
-            self.login('john')
-            # it should not be possible to use SomeAction not owned objects
-            req = self.request()
-            rset = req.execute('Any G WHERE G is CWGroup, G name "managers"')
-            self.assertFalse('yo' in dict(self.pactions(req, rset)))
-            # insert a new card, and check that we can use SomeAction on our object
-            self.execute('INSERT Card C: C title "zoubidou"')
-            self.commit()
-            req = self.request()
-            rset = req.execute('Card C WHERE C title "zoubidou"')
-            self.assertTrue('yo' in dict(self.pactions(req, rset)), self.pactions(req, rset))
-            # make sure even managers can't use the action
-            self.restore_connection()
-            req = self.request()
-            rset = req.execute('Card C WHERE C title "zoubidou"')
-            self.assertFalse('yo' in dict(self.pactions(req, rset)))
-        finally:
-            del self.vreg[SomeAction.__registry__][SomeAction.__regid__]
-
-
-class MultiLinesRsetSelectorTC(CubicWebTC):
-    def setUp(self):
-        super(MultiLinesRsetSelectorTC, self).setUp()
-        self.req = self.request()
-        self.req.execute('INSERT CWGroup G: G name "group1"')
-        self.req.execute('INSERT CWGroup G: G name "group2"')
-        self.commit()
-        self.rset = self.req.execute('Any G WHERE G is CWGroup')
-
-    def test_default_op_in_selector(self):
-        expected = len(self.rset)
-        selector = multi_lines_rset(expected)
-        self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-        self.assertEqual(selector(None, self.req, None), 0)
-        selector = multi_lines_rset(expected + 1)
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        self.assertEqual(selector(None, self.req, None), 0)
-        selector = multi_lines_rset(expected - 1)
-        self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-        self.assertEqual(selector(None, self.req, None), 0)
-
-    def test_without_rset(self):
-        expected = len(self.rset)
-        selector = multi_lines_rset(expected)
-        self.assertEqual(selector(None, self.req, None), 0)
-        selector = multi_lines_rset(expected + 1)
-        self.assertEqual(selector(None, self.req, None), 0)
-        selector = multi_lines_rset(expected - 1)
-        self.assertEqual(selector(None, self.req, None), 0)
-
-    def test_with_operators(self):
-        expected = len(self.rset)
-
-        # Format     'expected', 'operator', 'assert'
-        testdata = (( expected,         eq,        1),
-                    ( expected+1,       eq,        0),
-                    ( expected-1,       eq,        0),
-                    ( expected,         le,        1),
-                    ( expected+1,       le,        1),
-                    ( expected-1,       le,        0),
-                    ( expected-1,       gt,        1),
-                    ( expected,         gt,        0),
-                    ( expected+1,       gt,        0),
-                    ( expected+1,       lt,        1),
-                    ( expected,         lt,        0),
-                    ( expected-1,       lt,        0))
-
-        for (expected, operator, assertion) in testdata:
-            selector = multi_lines_rset(expected, operator)
-            yield self.assertEqual, selector(None, self.req, rset=self.rset), assertion
-
-    def test_match_kwargs_default(self):
-        selector = match_kwargs( set( ('a', 'b') ) )
-        self.assertEqual(selector(None, None, a=1, b=2), 2)
-        self.assertEqual(selector(None, None, a=1), 0)
-        self.assertEqual(selector(None, None, c=1), 0)
-        self.assertEqual(selector(None, None, a=1, c=1), 0)
-
-    def test_match_kwargs_any(self):
-        selector = match_kwargs( set( ('a', 'b') ), mode='any')
-        self.assertEqual(selector(None, None, a=1, b=2), 2)
-        self.assertEqual(selector(None, None, a=1), 1)
-        self.assertEqual(selector(None, None, c=1), 0)
-        self.assertEqual(selector(None, None, a=1, c=1), 1)
-
-
-class ScoreEntitySelectorTC(CubicWebTC):
-
-    def test_intscore_entity_selector(self):
-        req = self.request()
-        rset = req.execute('Any E WHERE E eid 1')
-        selector = score_entity(lambda x: None)
-        self.assertEqual(selector(None, req, rset=rset), 0)
-        selector = score_entity(lambda x: "something")
-        self.assertEqual(selector(None, req, rset=rset), 1)
-        selector = score_entity(lambda x: object)
-        self.assertEqual(selector(None, req, rset=rset), 1)
-        rset = req.execute('Any G LIMIT 2 WHERE G is CWGroup')
-        selector = score_entity(lambda x: 10)
-        self.assertEqual(selector(None, req, rset=rset), 20)
-        selector = score_entity(lambda x: 10, mode='any')
-        self.assertEqual(selector(None, req, rset=rset), 10)
-
-    def test_rql_condition_entity(self):
-        req = self.request()
-        selector = rql_condition('X identity U')
-        rset = req.user.as_rset()
-        self.assertEqual(selector(None, req, rset=rset), 1)
-        self.assertEqual(selector(None, req, entity=req.user), 1)
-        self.assertEqual(selector(None, req), 0)
-
-    def test_rql_condition_user(self):
-        req = self.request()
-        selector = rql_condition('U login "admin"', user_condition=True)
-        self.assertEqual(selector(None, req), 1)
-        selector = rql_condition('U login "toto"', user_condition=True)
-        self.assertEqual(selector(None, req), 0)
-
-if __name__ == '__main__':
-    unittest_main()
-
--- a/test/unittest_vregistry.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/test/unittest_vregistry.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,7 +22,7 @@
 
 from cubicweb import CW_SOFTWARE_ROOT as BASE
 from cubicweb.appobject import AppObject
-from cubicweb.cwvreg import CubicWebVRegistry, UnknownProperty
+from cubicweb.cwvreg import CWRegistryStore, UnknownProperty
 from cubicweb.devtools import TestServerConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.view import EntityAdapter
@@ -39,7 +39,7 @@
 
     def setUp(self):
         config = TestServerConfiguration('data')
-        self.vreg = CubicWebVRegistry(config)
+        self.vreg = CWRegistryStore(config)
         config.bootstrap_cubes()
         self.vreg.schema = config.load_schema()
 
--- a/view.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/view.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -26,17 +26,17 @@
 from functools import partial
 
 from logilab.common.deprecation import deprecated
+from logilab.common.registry import classid, yes
 from logilab.mtconverter import xml_escape
 
 from rql import nodes
 
 from cubicweb import NotAnEntity
-from cubicweb.selectors import yes, non_final_entity, nonempty_rset, none_rset
+from cubicweb.predicates import non_final_entity, nonempty_rset, none_rset
 from cubicweb.appobject import AppObject
 from cubicweb.utils import UStringIO, HTMLStream
 from cubicweb.uilib import domid, js
 from cubicweb.schema import display_name
-from cubicweb.vregistry import classid
 
 # robots control
 NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
--- a/vregistry.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/vregistry.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,487 +15,9 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-* the vregistry handles various types of objects interacting
-  together. The vregistry handles registration of dynamically loaded
-  objects and provides a convenient api to access those objects
-  according to a context
-
-* to interact with the vregistry, objects should inherit from the
-  AppObject abstract class
-
-* the selection procedure has been generalized by delegating to a
-  selector, which is responsible to score the appobject according to the
-  current state (req, rset, row, col). At the end of the selection, if
-  a appobject class has been found, an instance of this class is
-  returned. The selector is instantiated at appobject registration
-"""
-
-__docformat__ = "restructuredtext en"
-
-import sys
-from os import listdir, stat
-from os.path import dirname, join, realpath, isdir, exists
-from logging import getLogger
 from warnings import warn
-
-from logilab.common.deprecation import deprecated, class_moved
-from logilab.common.logging_ext import set_log_methods
-
-from cubicweb import CW_SOFTWARE_ROOT
-from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
-from cubicweb.appobject import AppObject, class_regid
-
-
-def _toload_info(path, extrapath, _toload=None):
-    """return a dictionary of <modname>: <modpath> and an ordered list of
-    (file, module name) to load
-    """
-    from logilab.common.modutils import modpath_from_file
-    if _toload is None:
-        assert isinstance(path, list)
-        _toload = {}, []
-    for fileordir in path:
-        if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
-            subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
-            _toload_info(subfiles, extrapath, _toload)
-        elif fileordir[-3:] == '.py':
-            modpath = modpath_from_file(fileordir, extrapath)
-            # omit '__init__' from package's name to avoid loading that module
-            # once for each name when it is imported by some other appobject
-            # module. This supposes import in modules are done as::
-            #
-            #   from package import something
-            #
-            # not::
-            #
-            #  from package.__init__ import something
-            #
-            # which seems quite correct.
-            if modpath[-1] == '__init__':
-                modpath.pop()
-            modname = '.'.join(modpath)
-            _toload[0][modname] = fileordir
-            _toload[1].append((fileordir, modname))
-    return _toload
-
-
-def classid(cls):
-    """returns a unique identifier for an appobject class"""
-    return '%s.%s' % (cls.__module__, cls.__name__)
-
-def class_registries(cls, registryname):
-    if registryname:
-        return (registryname,)
-    return cls.__registries__
-
-
-class Registry(dict):
-
-    def __init__(self, config):
-        super(Registry, self).__init__()
-        self.config = config
-
-    def __getitem__(self, name):
-        """return the registry (dictionary of class objects) associated to
-        this name
-        """
-        try:
-            return super(Registry, self).__getitem__(name)
-        except KeyError:
-            raise ObjectNotFound(name), None, sys.exc_info()[-1]
-
-    def initialization_completed(self):
-        for appobjects in self.itervalues():
-            for appobjectcls in appobjects:
-                appobjectcls.__registered__(self)
-
-    def register(self, obj, oid=None, clear=False):
-        """base method to add an object in the registry"""
-        assert not '__abstract__' in obj.__dict__
-        oid = oid or class_regid(obj)
-        assert oid
-        if clear:
-            appobjects = self[oid] =  []
-        else:
-            appobjects = self.setdefault(oid, [])
-        assert not obj in appobjects, \
-               'object %s is already registered' % obj
-        appobjects.append(obj)
-
-    def register_and_replace(self, obj, replaced):
-        # XXXFIXME this is a duplication of unregister()
-        # remove register_and_replace in favor of unregister + register
-        # or simplify by calling unregister then register here
-        if not isinstance(replaced, basestring):
-            replaced = classid(replaced)
-        # prevent from misspelling
-        assert obj is not replaced, 'replacing an object by itself: %s' % obj
-        registered_objs = self.get(class_regid(obj), ())
-        for index, registered in enumerate(registered_objs):
-            if classid(registered) == replaced:
-                del registered_objs[index]
-                break
-        else:
-            self.warning('trying to replace an unregistered view %s by %s',
-                         replaced, obj)
-        self.register(obj)
-
-    def unregister(self, obj):
-        clsid = classid(obj)
-        oid = class_regid(obj)
-        for registered in self.get(oid, ()):
-            # use classid() to compare classes because vreg will probably
-            # have its own version of the class, loaded through execfile
-            if classid(registered) == clsid:
-                self[oid].remove(registered)
-                break
-        else:
-            self.warning('can\'t remove %s, no id %s in the registry',
-                         clsid, oid)
-
-    def all_objects(self):
-        """return a list containing all objects in this registry.
-        """
-        result = []
-        for objs in self.values():
-            result += objs
-        return result
-
-    # dynamic selection methods ################################################
-
-    def object_by_id(self, oid, *args, **kwargs):
-        """return object with the `oid` identifier. Only one object is expected
-        to be found.
-
-        raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
-
-        raise :exc:`AssertionError` if there is more than one object there
-        """
-        objects = self[oid]
-        assert len(objects) == 1, objects
-        return objects[0](*args, **kwargs)
-
-    def select(self, __oid, *args, **kwargs):
-        """return the most specific object among those with the given oid
-        according to the given context.
-
-        raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
-
-        raise :exc:`NoSelectableObject` if not object apply
-        """
-        obj =  self._select_best(self[__oid], *args, **kwargs)
-        if obj is None:
-            raise NoSelectableObject(args, kwargs, self[__oid] )
-        return obj
-
-    def select_or_none(self, __oid, *args, **kwargs):
-        """return the most specific object among those with the given oid
-        according to the given context, or None if no object applies.
-        """
-        try:
-            return self.select(__oid, *args, **kwargs)
-        except (NoSelectableObject, ObjectNotFound):
-            return None
-
-    def possible_objects(self, *args, **kwargs):
-        """return an iterator on possible objects in this registry for the given
-        context
-        """
-        for appobjects in self.itervalues():
-            obj = self._select_best(appobjects,  *args, **kwargs)
-            if obj is None:
-                continue
-            yield obj
-
-    def _select_best(self, appobjects, *args, **kwargs):
-        """return an instance of the most specific object according
-        to parameters
-
-        return None if not object apply (don't raise `NoSelectableObject` since
-        it's costly when searching appobjects using `possible_objects`
-        (e.g. searching for hooks).
-        """
-        score, winners = 0, None
-        for appobject in appobjects:
-            appobjectscore = appobject.__select__(appobject, *args, **kwargs)
-            if appobjectscore > score:
-                score, winners = appobjectscore, [appobject]
-            elif appobjectscore > 0 and appobjectscore == score:
-                winners.append(appobject)
-        if winners is None:
-            return None
-        if len(winners) > 1:
-            # log in production environement / test, error while debugging
-            msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
-            if self.config.debugmode or self.config.mode == 'test':
-                # raise bare exception in debug mode
-                raise Exception(msg % (winners, args, kwargs.keys()))
-            self.error(msg, winners, args, kwargs.keys())
-        # return the result of calling the appobject
-        return winners[0](*args, **kwargs)
-
-    # these are overridden by set_log_methods below
-    # only defining here to prevent pylint from complaining
-    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-
-class VRegistry(dict):
-    """class responsible to register, propose and select the various
-    elements used to build the web interface. Currently, we have templates,
-    views, actions and components.
-    """
+from logilab.common.deprecation import class_moved
+warn('[3.15] moved to logilab.common.registry', DeprecationWarning, stacklevel=2)
+from logilab.common.registry import *
 
-    def __init__(self, config):
-        super(VRegistry, self).__init__()
-        self.config = config
-        # need to clean sys.path this to avoid import confusion pb (i.e.  having
-        # the same module loaded as 'cubicweb.web.views' subpackage and as
-        # views' or 'web.views' subpackage. This is mainly for testing purpose,
-        # we should'nt need this in production environment
-        for webdir in (join(dirname(realpath(__file__)), 'web'),
-                       join(dirname(__file__), 'web')):
-            if webdir in sys.path:
-                sys.path.remove(webdir)
-        if CW_SOFTWARE_ROOT in sys.path:
-            sys.path.remove(CW_SOFTWARE_ROOT)
-
-    def reset(self):
-        # don't use self.clear, we want to keep existing subdictionaries
-        for subdict in self.itervalues():
-            subdict.clear()
-        self._lastmodifs = {}
-
-    def __getitem__(self, name):
-        """return the registry (dictionary of class objects) associated to
-        this name
-        """
-        try:
-            return super(VRegistry, self).__getitem__(name)
-        except KeyError:
-            raise RegistryNotFound(name), None, sys.exc_info()[-1]
-
-    # methods for explicit (un)registration ###################################
-
-    # default class, when no specific class set
-    REGISTRY_FACTORY = {None: Registry}
-
-    def registry_class(self, regid):
-        try:
-            return self.REGISTRY_FACTORY[regid]
-        except KeyError:
-            return self.REGISTRY_FACTORY[None]
-
-    def setdefault(self, regid):
-        try:
-            return self[regid]
-        except KeyError:
-            self[regid] = self.registry_class(regid)(self.config)
-            return self[regid]
-
-#     def clear(self, key):
-#         regname, oid = key.split('.')
-#         self[regname].pop(oid, None)
-
-    def register_all(self, objects, modname, butclasses=()):
-        """register all `objects` given. Objects which are not from the module
-        `modname` or which are in `butclasses` won't be registered.
-
-        Typical usage is:
-
-        .. sourcecode:: python
-
-            vreg.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
-
-        So you get partially automatic registration, keeping manual registration
-        for some object (to use
-        :meth:`~cubicweb.cwvreg.CubicWebRegistry.register_and_replace` for
-        instance)
-        """
-        for obj in objects:
-            try:
-                if obj.__module__ != modname or obj in butclasses:
-                    continue
-                oid = class_regid(obj)
-            except AttributeError:
-                continue
-            if oid and not '__abstract__' in obj.__dict__:
-                self.register(obj, oid=oid)
-
-    def register(self, obj, registryname=None, oid=None, clear=False):
-        """register `obj` application object into `registryname` or
-        `obj.__registry__` if not specified, with identifier `oid` or
-        `obj.__regid__` if not specified.
-
-        If `clear` is true, all objects with the same identifier will be
-        previously unregistered.
-        """
-        assert not '__abstract__' in obj.__dict__
-        try:
-            vname = obj.__name__
-        except AttributeError:
-            # XXX may occurs?
-            vname = obj.__class__.__name__
-        for registryname in class_registries(obj, registryname):
-            registry = self.setdefault(registryname)
-            registry.register(obj, oid=oid, clear=clear)
-            self.debug('register %s in %s[\'%s\']',
-                       vname, registryname, oid or class_regid(obj))
-        self._loadedmods.setdefault(obj.__module__, {})[classid(obj)] = obj
-
-    def unregister(self, obj, registryname=None):
-        """unregister `obj` application object from the registry `registryname` or
-        `obj.__registry__` if not specified.
-        """
-        for registryname in class_registries(obj, registryname):
-            self[registryname].unregister(obj)
-
-    def register_and_replace(self, obj, replaced, registryname=None):
-        """register `obj` application object into `registryname` or
-        `obj.__registry__` if not specified. If found, the `replaced` object
-        will be unregistered first (else a warning will be issued as it's
-        generally unexpected).
-        """
-        for registryname in class_registries(obj, registryname):
-            self[registryname].register_and_replace(obj, replaced)
-
-    # initialization methods ###################################################
-
-    def init_registration(self, path, extrapath=None):
-        self.reset()
-        # compute list of all modules that have to be loaded
-        self._toloadmods, filemods = _toload_info(path, extrapath)
-        # XXX is _loadedmods still necessary ? It seems like it's useful
-        #     to avoid loading same module twice, especially with the
-        #     _load_ancestors_then_object logic but this needs to be checked
-        self._loadedmods = {}
-        return filemods
-
-    def register_objects(self, path, extrapath=None):
-        # load views from each directory in the instance's path
-        filemods = self.init_registration(path, extrapath)
-        for filepath, modname in filemods:
-            self.load_file(filepath, modname)
-        self.initialization_completed()
-
-    def initialization_completed(self):
-        for regname, reg in self.iteritems():
-            reg.initialization_completed()
-
-    def _mdate(self, filepath):
-        try:
-            return stat(filepath)[-2]
-        except OSError:
-            # this typically happens on emacs backup files (.#foo.py)
-            self.warning('Unable to load %s. It is likely to be a backup file',
-                         filepath)
-            return None
-
-    def is_reload_needed(self, path):
-        """return True if something module changed and the registry should be
-        reloaded
-        """
-        lastmodifs = self._lastmodifs
-        for fileordir in path:
-            if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
-                if self.is_reload_needed([join(fileordir, fname)
-                                          for fname in listdir(fileordir)]):
-                    return True
-            elif fileordir[-3:] == '.py':
-                mdate = self._mdate(fileordir)
-                if mdate is None:
-                    continue # backup file, see _mdate implementation
-                elif "flymake" in fileordir:
-                    # flymake + pylint in use, don't consider these they will corrupt the registry
-                    continue
-                if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate:
-                    self.info('File %s changed since last visit', fileordir)
-                    return True
-        return False
-
-    def load_file(self, filepath, modname):
-        """load app objects from a python file"""
-        from logilab.common.modutils import load_module_from_name
-        if modname in self._loadedmods:
-            return
-        self._loadedmods[modname] = {}
-        mdate = self._mdate(filepath)
-        if mdate is None:
-            return # backup file, see _mdate implementation
-        elif "flymake" in filepath:
-            # flymake + pylint in use, don't consider these they will corrupt the registry
-            return
-        # set update time before module loading, else we get some reloading
-        # weirdness in case of syntax error or other error while importing the
-        # module
-        self._lastmodifs[filepath] = mdate
-        # load the module
-        module = load_module_from_name(modname)
-        self.load_module(module)
-
-    def load_module(self, module):
-        self.info('loading %s from %s', module.__name__, module.__file__)
-        if hasattr(module, 'registration_callback'):
-            module.registration_callback(self)
-        else:
-            for objname, obj in vars(module).items():
-                if objname.startswith('_'):
-                    continue
-                self._load_ancestors_then_object(module.__name__, obj)
-
-    def _load_ancestors_then_object(self, modname, appobjectcls):
-        """handle automatic appobject class registration:
-
-        - first ensure parent classes are already registered
-
-        - class with __abstract__ == True in their local dictionnary or
-          with a name starting with an underscore are not registered
-
-        - appobject class needs to have __registry__ and __regid__ attributes
-          set to a non empty string to be registered.
-        """
-        # imported classes
-        objmodname = getattr(appobjectcls, '__module__', None)
-        if objmodname != modname:
-            if objmodname in self._toloadmods:
-                self.load_file(self._toloadmods[objmodname], objmodname)
-            return
-        # skip non registerable object
-        try:
-            if not issubclass(appobjectcls, AppObject):
-                return
-        except TypeError:
-            return
-        clsid = classid(appobjectcls)
-        if clsid in self._loadedmods[modname]:
-            return
-        self._loadedmods[modname][clsid] = appobjectcls
-        for parent in appobjectcls.__bases__:
-            self._load_ancestors_then_object(modname, parent)
-        if (appobjectcls.__dict__.get('__abstract__')
-            or appobjectcls.__name__[0] == '_'
-            or not appobjectcls.__registries__
-            or not class_regid(appobjectcls)):
-            return
-        try:
-            self.register(appobjectcls)
-        except Exception, ex:
-            if self.config.mode in ('test', 'dev'):
-                raise
-            self.exception('appobject %s registration failed: %s',
-                           appobjectcls, ex)
-    # 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
-
-
-# init logging
-set_log_methods(VRegistry, getLogger('cubicweb.vreg'))
-set_log_methods(Registry, getLogger('cubicweb.registry'))
-
-
-# XXX bw compat functions #####################################################
-
-from cubicweb.appobject import objectify_selector, AndSelector, OrSelector, Selector
-
-Selector = class_moved(Selector)
+VRegistry = class_moved(RegistryStore, old_name='VRegistry', message='[3.15] VRegistry moved to logilab.common.registry as RegistryStore')
--- a/web/__init__.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/__init__.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/web/action.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/action.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -68,8 +68,8 @@
 _ = unicode
 
 from cubicweb import target
-from cubicweb.selectors import (partial_relation_possible, match_search_state,
-                                one_line_rset)
+from cubicweb.predicates import (partial_relation_possible, match_search_state,
+                                 one_line_rset)
 from cubicweb.appobject import AppObject
 
 
--- a/web/application.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/application.py	Thu Feb 23 19:06:14 2012 +0100
@@ -276,7 +276,7 @@
                  vreg=None):
         self.info('starting web instance from %s', config.apphome)
         if vreg is None:
-            vreg = cwvreg.CubicWebVRegistry(config)
+            vreg = cwvreg.CWRegistryStore(config)
         self.vreg = vreg
         # connect to the repository and get instance's schema
         self.repo = config.repository(vreg)
@@ -450,7 +450,7 @@
         req.remove_header('Etag')
         req.reset_message()
         req.reset_headers()
-        if req.json_request:
+        if req.ajax_request:
             raise RemoteCallFailed(unicode(ex))
         try:
             req.data['ex'] = ex
--- a/web/box.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/box.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -25,7 +25,7 @@
 
 from cubicweb import Unauthorized, role as get_role
 from cubicweb.schema import display_name
-from cubicweb.selectors import no_cnx, one_line_rset
+from cubicweb.predicates import no_cnx, one_line_rset
 from cubicweb.view import View
 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
--- a/web/component.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/component.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -32,7 +32,7 @@
 from cubicweb.uilib import js, domid
 from cubicweb.utils import json_dumps, js_href
 from cubicweb.view import ReloadableMixIn, Component
-from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset,
+from cubicweb.predicates import (no_cnx, paginated_rset, one_line_rset,
                                 non_final_entity, partial_relation_possible,
                                 partial_has_related_entities)
 from cubicweb.appobject import AppObject
--- a/web/controller.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/controller.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,8 +20,8 @@
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
 
-from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
 from cubicweb.mail import format_mail
 from cubicweb.web import LOGGER, Redirect, RequestError
--- a/web/data/cubicweb.ajax.box.js	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.ajax.box.js	Thu Feb 23 19:06:14 2012 +0100
@@ -13,11 +13,11 @@
     if (separator) {
         value = $.map(value.split(separator), jQuery.trim);
     }
-    var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(fname, null, eid, value));
     d.addCallback(function() {
             $('#' + holderid).empty();
             var formparams = ajaxFuncArgs('render', null, 'ctxcomponents', boxid, eid);
-            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml(AJAX_BASE_URL, formparams);
             if (msg) {
                 document.location.hash = '#header';
                 updateMessage(msg);
@@ -26,10 +26,10 @@
 }
 
 function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) {
-    var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(delfname, null, eid, relatedeid));
     d.addCallback(function() {
             var formparams = ajaxFuncArgs('render', null, 'ctxcomponents', boxid, eid);
-            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml(AJAX_BASE_URL, formparams);
             if (msg) {
                 document.location.hash = '#header';
                 updateMessage(msg);
@@ -69,7 +69,7 @@
     }
     else {
         var inputid = holderid + 'Input';
-        var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
+        var deferred = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(unrelfname, null, eid));
         deferred.addCallback(function (unrelated) {
             var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
             holder.append(input).show();
--- a/web/data/cubicweb.ajax.js	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.ajax.js	Thu Feb 23 19:06:14 2012 +0100
@@ -86,8 +86,9 @@
 
 });
 
-
+var AJAX_PREFIX_URL = 'ajax';
 var JSON_BASE_URL = baseuri() + 'json?';
+var AJAX_BASE_URL = baseuri() + AJAX_PREFIX_URL + '?';
 
 
 jQuery.extend(cw.ajax, {
@@ -439,7 +440,7 @@
  * emulation of gettext's _ shortcut
  */
 function _(message) {
-    return loadRemote('json', ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
+    return loadRemote(AJAX_BASE_URL, ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
 }
 
 /**
@@ -495,19 +496,19 @@
         }
         extraparams['rql'] = rql;
         extraparams['vid'] = vid;
-        $fragment.loadxhtml('json', ajaxFuncArgs('view', extraparams));
+        $fragment.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams));
     }
 }
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    loadRemote('json', ajaxFuncArgs('unload_page_data'), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'GET', true);
 }
 
 function removeBookmark(beid) {
-    var d = loadRemote('json', ajaxFuncArgs('delete_bookmark', null, beid));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('delete_bookmark', null, beid));
     d.addCallback(function(boxcontent) {
-        $('#bookmarks_box').loadxhtml('json',
+        $('#bookmarks_box').loadxhtml(AJAX_BASE_URL,
                                       ajaxFuncArgs('render', null, 'ctxcomponents',
                                                    'bookmarks_box'));
         document.location.hash = '#header';
@@ -517,7 +518,7 @@
 
 function userCallback(cbname) {
     setProgressCursor();
-    var d = loadRemote('json', ajaxFuncArgs('user_callback', null, cbname));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('user_callback', null, cbname));
     d.addCallback(resetCursor);
     d.addErrback(resetCursor);
     d.addErrback(remoteCallFailed);
@@ -527,7 +528,7 @@
 function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
     var d = userCallback(cbname);
     d.addCallback(function() {
-        $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {'rql': rql},
+        $('#' + nodeid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {'rql': rql},
                                                        registry, compid));
         if (msg) {
             updateMessage(msg);
@@ -553,7 +554,7 @@
  */
 function unregisterUserCallback(cbname) {
     setProgressCursor();
-    var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unregister_user_callback',
                                             null, cbname));
     d.addCallback(resetCursor);
     d.addErrback(resetCursor);
@@ -679,7 +680,7 @@
 	var compid = this.id.replace("_", ".").rstrip(creationEid);
 	var params = ajaxFuncArgs('render', null, 'ctxcomponents',
 				  compid, actualEid);
-	$(this).loadxhtml('json', params, null, 'swap', true);
+	$(this).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true);
     });
     $compsholder.attr('id', context + actualEid);
 }
@@ -694,7 +695,7 @@
     var ajaxArgs = ['render', formparams, registry, compid];
     ajaxArgs = ajaxArgs.concat(cw.utils.sliceList(arguments, 4));
     var params = ajaxFuncArgs.apply(null, ajaxArgs);
-    return $('#'+domid).loadxhtml('json', params, null, 'swap');
+    return $('#'+domid).loadxhtml(AJAX_BASE_URL, params, null, 'swap');
 }
 
 /* ajax tabs ******************************************************************/
@@ -738,8 +739,8 @@
         nodeid = nodeid || (compid + 'Component');
         extraargs = extraargs || {};
         var node = cw.jqNode(nodeid);
-        return node.loadxhtml('json', ajaxFuncArgs('component', null, compid,
-                                                   rql, registry, extraargs));
+        return node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('component', null, compid,
+                                                          rql, registry, extraargs));
     }
 );
 
@@ -775,7 +776,7 @@
             // passing `props` directly to loadxml because replacePageChunk
             // is sometimes called (abusively) with some extra parameters in `vid`
             var mode = swap ? 'swap': 'replace';
-            var url = JSON_BASE_URL + asURL(props);
+            var url = AJAX_BASE_URL + asURL(props);
             jQuery(node).loadxhtml(url, params, 'get', mode);
         } else {
             cw.log('Node', nodeId, 'not found');
@@ -798,7 +799,7 @@
         arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
     };
     var result = jQuery.ajax({
-        url: JSON_BASE_URL,
+        url: AJAX_BASE_URL,
         data: props,
         async: false,
         traditional: true
@@ -818,7 +819,7 @@
         arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
     };
     // XXX we should inline the content of loadRemote here
-    var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
+    var deferred = loadRemote(AJAX_BASE_URL, props, 'POST');
     deferred = deferred.addErrback(remoteCallFailed);
     deferred = deferred.addErrback(resetCursor);
     deferred = deferred.addCallback(resetCursor);
--- a/web/data/cubicweb.css	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.css	Thu Feb 23 19:06:14 2012 +0100
@@ -76,7 +76,7 @@
   letter-spacing: 0.015em;
   padding: 0.6em;
   margin: 0 2em 1.7em;
-  background-color: %(listingHihligthedBgColor)s;
+  background-color: %(listingHighlightedBgColor)s;
   border: 1px solid %(listingBorderColor)s;
 }
 
@@ -572,7 +572,7 @@
   vertical-align: bottom;
 }
 
-input#norql{
+input.norql{
   width:155px;
   margin-right: 2px;
 }
@@ -808,7 +808,7 @@
 
 table.listing input,
 table.listing textarea {
- background: %(listingHihligthedBgColor)s;
+ background: %(listingHighlightedBgColor)s;
 }
 
 table.htableForm label, table.oneRowTableForm label {
--- a/web/data/cubicweb.edition.js	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.edition.js	Thu Feb 23 19:06:14 2012 +0100
@@ -28,7 +28,7 @@
             pageid: pageid,
             arg: $.map([key, varname, tabindex], jQuery.toJSON)
         };
-        cw.jqNode('div:value:' + varname).loadxhtml(JSON_BASE_URL, args, 'post');
+        cw.jqNode('div:value:' + varname).loadxhtml(AJAX_BASE_URL, args, 'post');
     }
 }
 
@@ -170,8 +170,8 @@
     // add hidden parameter
     var entityForm = jQuery('#entityForm');
     var oid = optionNode.id.substring(2); // option id is prefixed by "id"
-    loadRemote('json', ajaxFuncArgs('add_pending_inserts', null,
-                                    [oid.split(':')]), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null,
+                                           [oid.split(':')]), 'GET', true);
     var selectNode = optionNode.parentNode;
     // remove option node
     selectNode.removeChild(optionNode);
@@ -209,8 +209,8 @@
         }
     }
     elementId = elementId.substring(2, elementId.length);
-    loadRemote('json', ajaxFuncArgs('remove_pending_insert', null,
-                                    elementId.split(':')), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_insert', null,
+                                           elementId.split(':')), 'GET', true);
 }
 
 /**
@@ -234,7 +234,7 @@
  * * `nodeId`, eid_from:r_type:eid_to
  */
 function addPendingDelete(nodeId, eid) {
-    var d = loadRemote('json', ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
     d.addCallback(function() {
         // and strike entity view
         cw.jqNode('span' + nodeId).addClass('pendingDelete');
@@ -249,7 +249,7 @@
  * * `nodeId`, eid_from:r_type:eid_to
  */
 function cancelPendingDelete(nodeId, eid) {
-    var d = loadRemote('json', ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
     d.addCallback(function() {
         // reset link's CSS class
         cw.jqNode('span' + nodeId).removeClass('pendingDelete');
@@ -275,7 +275,7 @@
 function selectForAssociation(tripletIdsString, originalEid) {
     var tripletlist = $.map(tripletIdsString.split('-'),
                             function(x) { return [x.split(':')] ;});
-    var d = loadRemote('json', ajaxFuncArgs('add_pending_inserts', null, tripletlist));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null, tripletlist));
     d.addCallback(function() {
         var args = {
             vid: 'edition',
@@ -308,7 +308,7 @@
 function addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore) {
     insertBefore = insertBefore || cw.getNode('add' + rtype + ':' + peid + 'link').parentNode;
     var args = ajaxFuncArgs('inline_creation_form', null, peid, petype, ttype, rtype, role, i18nctx);
-    var d = loadRemote('json', args);
+    var d = loadRemote(AJAX_BASE_URL, args);
     d.addCallback(function(response) {
         var dom = getDomFromResponse(response);
         loadAjaxHtmlHead(dom);
@@ -587,7 +587,7 @@
     try {
         var zipped = cw.utils.formContents(formid);
         var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
-        var d = loadRemote('json', args, 'POST');
+        var d = loadRemote(AJAX_BASE_URL, args, 'POST');
     } catch(ex) {
         cw.log('got exception', ex);
         return false;
--- a/web/data/cubicweb.facets.js	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.facets.js	Thu Feb 23 19:06:14 2012 +0100
@@ -56,7 +56,7 @@
     var zipped = facetFormContent($form);
     zipped[0].push('facetargs');
     zipped[1].push(vidargs);
-    var d = loadRemote('json', ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
     d.addCallback(function(result) {
         var rql = result[0];
         var $bkLink = jQuery('#facetBkLink');
@@ -87,7 +87,7 @@
         if (vid) { // XXX see copyParam above. Need cleanup
             extraparams['vid'] = vid;
         }
-        d = $('#' + divid).loadxhtml('json', ajaxFuncArgs('view', extraparams),
+        d = $('#' + divid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams),
                                      null, 'swap');
         d.addCallback(function() {
             // XXX rql/vid in extraparams
@@ -99,14 +99,14 @@
             // now
             var $node = jQuery('#edit_box');
             if ($node.length) {
-                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
                     'rql': rql
                 },
                 'ctxcomponents', 'edit_box'));
             }
             $node = jQuery('#breadcrumbs');
             if ($node.length) {
-                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
                     'rql': rql
                 },
                 'ctxcomponents', 'breadcrumbs'));
@@ -118,7 +118,7 @@
             mainvar = zipped[1][index];
         }
 
-        var d = loadRemote('json', ajaxFuncArgs('filter_select_content', null, toupdate, rql, mainvar));
+        var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('filter_select_content', null, toupdate, rql, mainvar));
         d.addCallback(function(updateMap) {
             for (facetName in updateMap) {
                 var values = updateMap[facetName];
--- a/web/data/cubicweb.iprogress.css	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.iprogress.css	Thu Feb 23 19:06:14 2012 +0100
@@ -62,11 +62,11 @@
 }
 
 table.progress tr.highlighted {
-  background-color: %(listingHihligthedBgColor)s;
+  background-color: %(listingHighlightedBgColor)s;
 }
 
 table.progress tr.highlighted .progressbarback {
-  border: 1px solid %(listingHihligthedBgColor)s;
+  border: 1px solid %(listingHighlightedBgColor)s;
 }
 
 table.progress .progressbarback {
--- a/web/data/cubicweb.js	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.js	Thu Feb 23 19:06:14 2012 +0100
@@ -84,11 +84,13 @@
     },
 
     sortValueExtraction: function (node) {
-	var sortvalue = jQuery(node).attr('cubicweb:sortvalue');
-	if (sortvalue === undefined) {
-	    return '';
-	}
-	return cw.evalJSON(sortvalue);
+        var $node = $(node);
+        var sortvalue = $node.attr('cubicweb:sortvalue');
+        // No metadata found, use cell content as sort key
+        if (sortvalue === undefined) {
+            return $node.text();
+        }
+        return cw.evalJSON(sortvalue);
     }
 });
 
--- a/web/data/cubicweb.reledit.js	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.reledit.js	Thu Feb 23 19:06:14 2012 +0100
@@ -53,7 +53,7 @@
                 return;
             }
         }
-        jQuery('#'+params.divid+'-reledit').loadxhtml(JSON_BASE_URL, params, 'post');
+        jQuery('#'+params.divid+'-reledit').loadxhtml(AJAX_BASE_URL, params, 'post');
         jQuery(cw).trigger('reledit-reloaded', params);
     },
 
@@ -69,7 +69,7 @@
                     pageid: pageid, action: action,
                     eid: eid, divid: divid, formid: formid,
                     reload: reload, vid: vid};
-        var d = jQuery('#'+divid+'-reledit').loadxhtml(JSON_BASE_URL, args, 'post');
+        var d = jQuery('#'+divid+'-reledit').loadxhtml(AJAX_BASE_URL, args, 'post');
         d.addCallback(function () {cw.reledit.showInlineEditionForm(divid);});
     }
 });
--- a/web/data/cubicweb.widgets.js	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/cubicweb.widgets.js	Thu Feb 23 19:06:14 2012 +0100
@@ -45,11 +45,11 @@
 });
 
 function postJSON(url, data, callback) {
-    return jQuery.post(url, data, callback, 'json');
+    return jQuery.post(url, data, callback, AJAX_BASE_URL);
 }
 
 function getJSON(url, data, callback) {
-    return jQuery.get(url, data, callback, 'json');
+    return jQuery.get(url, data, callback, AJAX_BASE_URL);
 }
 
 
--- a/web/data/uiprops.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/data/uiprops.py	Thu Feb 23 19:06:14 2012 +0100
@@ -146,7 +146,7 @@
 # table listing & co ###########################################################
 listingBorderColor = '#ccc'
 listingHeaderBgColor = '#efefef'
-listingHihligthedBgColor = '#fbfbfb'
+listingHighlightedBgColor = '#fbfbfb'
 
 # puce
 bulletDownImg = 'url("puce_down.png") 98% 6px no-repeat'
--- a/web/facet.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/facet.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -59,6 +59,7 @@
 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
 from logilab.common.compat import all
 from logilab.common.deprecation import deprecated
+from logilab.common.registry import yes
 
 from rql import nodes, utils
 
@@ -66,7 +67,7 @@
 from cubicweb.schema import display_name
 from cubicweb.uilib import css_em_num_value
 from cubicweb.utils import make_uid
-from cubicweb.selectors import match_context_prop, partial_relation_possible, yes
+from cubicweb.predicates import match_context_prop, partial_relation_possible
 from cubicweb.appobject import AppObject
 from cubicweb.web import RequestError, htmlwidgets
 
--- a/web/request.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/request.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -82,7 +82,7 @@
 
 class CubicWebRequestBase(DBAPIRequest):
     """abstract HTTP request, should be extended according to the HTTP backend"""
-    json_request = False # to be set to True by json controllers
+    ajax_request = False # to be set to True by ajax controllers
 
     def __init__(self, vreg, https, form=None):
         super(CubicWebRequestBase, self).__init__(vreg)
@@ -121,6 +121,16 @@
             self.html_headers.define_var('pageid', pid, override=False)
         self.pageid = pid
 
+    def _get_json_request(self):
+        warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
+             DeprecationWarning, stacklevel=2)
+        return self.ajax_request
+    def _set_json_request(self, value):
+        warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
+             DeprecationWarning, stacklevel=2)
+        self.ajax_request = value
+    json_request = property(_get_json_request, _set_json_request)
+
     @property
     def authmode(self):
         return self.vreg.config['auth-mode']
--- a/web/test/unittest_views_basecontrollers.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Thu Feb 23 19:06:14 2012 +0100
@@ -20,15 +20,19 @@
 from __future__ import with_statement
 
 from logilab.common.testlib import unittest_main, mock_object
+from logilab.common.decorators import monkeypatch
 
 from cubicweb import Binary, NoSelectableObject, ValidationError
 from cubicweb.view import STRICT_DOCTYPE
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.utils import json_dumps
 from cubicweb.uilib import rql_for_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError
+from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError, RemoteCallFailed
 from cubicweb.entities.authobjs import CWUser
 from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes
+from cubicweb.web.views.basecontrollers import JSonController, xhtmlize, jsonize
+from cubicweb.web.views.ajaxcontroller import ajaxfunc, AjaxFunction
+
 u = unicode
 
 def req_form(user):
@@ -557,11 +561,12 @@
 
 
 
-class JSONControllerTC(CubicWebTC):
+class AjaxControllerTC(CubicWebTC):
+    tested_controller = 'ajax'
 
     def ctrl(self, req=None):
         req = req or self.request(url='http://whatever.fr/')
-        return self.vreg['controllers'].select('json', req)
+        return self.vreg['controllers'].select(self.tested_controller, req)
 
     def setup_database(self):
         req = self.request()
@@ -679,8 +684,89 @@
         self.assertEqual(self.remote_call('format_date', '2007-01-01 12:00:00')[0],
                           json_dumps('2007/01/01'))
 
+    def test_ajaxfunc_noparameter(self):
+        @ajaxfunc
+        def foo(self, x, y):
+            return 'hello'
+        self.assertTrue(issubclass(foo, AjaxFunction))
+        self.assertEqual(foo.__regid__, 'foo')
+        self.assertEqual(foo.check_pageid, False)
+        self.assertEqual(foo.output_type, None)
+        req = self.request()
+        f = foo(req)
+        self.assertEqual(f(12, 13), 'hello')
+
+    def test_ajaxfunc_checkpageid(self):
+        @ajaxfunc( check_pageid=True)
+        def foo(self, x, y):
+            pass
+        self.assertTrue(issubclass(foo, AjaxFunction))
+        self.assertEqual(foo.__regid__, 'foo')
+        self.assertEqual(foo.check_pageid, True)
+        self.assertEqual(foo.output_type, None)
+        # no pageid
+        req = self.request()
+        f = foo(req)
+        self.assertRaises(RemoteCallFailed, f, 12, 13)
+
+    def test_ajaxfunc_json(self):
+        @ajaxfunc(output_type='json')
+        def foo(self, x, y):
+            return x + y
+        self.assertTrue(issubclass(foo, AjaxFunction))
+        self.assertEqual(foo.__regid__, 'foo')
+        self.assertEqual(foo.check_pageid, False)
+        self.assertEqual(foo.output_type, 'json')
+        # no pageid
+        req = self.request()
+        f = foo(req)
+        self.assertEqual(f(12, 13), '25')
 
 
+class JSonControllerTC(AjaxControllerTC):
+    # NOTE: this class performs the same tests as AjaxController but with
+    #       deprecated 'json' controller (i.e. check backward compatibility)
+    tested_controller = 'json'
+
+    def setUp(self):
+        super(JSonControllerTC, self).setUp()
+        self.exposed_remote_funcs = [fname for fname in dir(JSonController)
+                                     if fname.startswith('js_')]
+
+    def tearDown(self):
+        super(JSonControllerTC, self).tearDown()
+        for funcname in dir(JSonController):
+            # remove functions added dynamically during tests
+            if funcname.startswith('js_') and funcname not in self.exposed_remote_funcs:
+                delattr(JSonController, funcname)
+
+    def test_monkeypatch_jsoncontroller(self):
+        self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+        @monkeypatch(JSonController)
+        def js_foo(self):
+            return u'hello'
+        res, req = self.remote_call('foo')
+        self.assertEqual(res, u'hello')
+
+    def test_monkeypatch_jsoncontroller_xhtmlize(self):
+        self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+        @monkeypatch(JSonController)
+        @xhtmlize
+        def js_foo(self):
+            return u'hello'
+        res, req = self.remote_call('foo')
+        self.assertEqual(res,
+                         '<?xml version="1.0"?>\n' + STRICT_DOCTYPE +
+                         u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">hello</div>')
+
+    def test_monkeypatch_jsoncontroller_jsonize(self):
+        self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+        @monkeypatch(JSonController)
+        @jsonize
+        def js_foo(self):
+            return 12
+        res, req = self.remote_call('foo')
+        self.assertEqual(res, '12')
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_viewselector.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/test/unittest_viewselector.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,9 +22,8 @@
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import CW_SOFTWARE_ROOT as BASE, Binary, UnknownProperty
-from cubicweb.selectors import (match_user_groups, is_instance,
-                                specified_etype_implements, rql_condition,
-                                traced_selection)
+from cubicweb.predicates import (match_user_groups, is_instance,
+                                 specified_etype_implements, rql_condition)
 from cubicweb.web import NoSelectableObject
 from cubicweb.web.action import Action
 from cubicweb.web.views import (
--- a/web/views/actions.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/actions.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -23,10 +23,10 @@
 from warnings import warn
 
 from logilab.mtconverter import xml_escape
+from logilab.common.registry import objectify_predicate, yes
 
 from cubicweb.schema import display_name
-from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import (EntitySelector, yes,
+from cubicweb.predicates import (EntityPredicate,
     one_line_rset, multi_lines_rset, one_etype_rset, relation_possible,
     nonempty_rset, non_final_entity, score_entity,
     authenticated_user, match_user_groups, match_search_state,
@@ -36,11 +36,11 @@
 from cubicweb.web.views import linksearch_select_url, vid_from_rset
 
 
-class has_editable_relation(EntitySelector):
+class has_editable_relation(EntityPredicate):
     """accept if some relations for an entity found in the result set is
     editable by the logged user.
 
-    See `EntitySelector` documentation for behaviour when row is not specified.
+    See `EntityPredicate` documentation for behaviour when row is not specified.
     """
 
     def score_entity(self, entity):
@@ -55,11 +55,11 @@
             return 1
         return 0
 
-@objectify_selector
+@objectify_predicate
 def match_searched_etype(cls, req, rset=None, **kwargs):
     return req.match_search_state(rset)
 
-@objectify_selector
+@objectify_predicate
 def view_is_not_default_view(cls, req, rset=None, **kwargs):
     # interesting if it propose another view than the current one
     vid = req.form.get('vid')
@@ -67,7 +67,7 @@
         return 1
     return 0
 
-@objectify_selector
+@objectify_predicate
 def addable_etype_empty_rset(cls, req, rset=None, **kwargs):
     if rset is not None and not rset.rowcount:
         rqlst = rset.syntax_tree()
@@ -130,7 +130,7 @@
         params = self._cw.form.copy()
         for param in ('vid', '__message') + controller.NAV_FORM_PARAMETERS:
             params.pop(param, None)
-        if self._cw.json_request:
+        if self._cw.ajax_request:
             path = 'view'
             if self.cw_rset is not None:
                 params = {'rql': self.cw_rset.printable_rql()}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/ajaxcontroller.py	Thu Feb 23 19:06:14 2012 +0100
@@ -0,0 +1,452 @@
+# 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/>.
+#
+# (disable pylint msg for client obj access to protected member as in obj._cw)
+# pylint: disable=W0212
+"""The ``ajaxcontroller`` module defines the :class:`AjaxController`
+controller and the ``ajax-funcs`` cubicweb registry.
+
+.. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxController
+   :members:
+
+``ajax-funcs`` registry hosts exposed remote functions, that is
+functions that can be called from the javascript world.
+
+To register a new remote function, either decorate your function
+with the :ref:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:
+
+.. sourcecode:: python
+
+    from cubicweb.predicates import mactch_user_groups
+    from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+    @ajaxfunc(output_type='json', selector=match_user_groups('managers'))
+    def list_users(self):
+        return [u for (u,) in self._cw.execute('Any L WHERE U login L')]
+
+or inherit from :class:`cubicwbe.web.views.ajaxcontroller.AjaxFunction` and
+implement the ``__call__`` method:
+
+.. sourcecode:: python
+
+    from cubicweb.web.views.ajaxcontroller import AjaxFunction
+    class ListUser(AjaxFunction):
+        __regid__ = 'list_users' # __regid__ is the name of the exposed function
+        __select__ = match_user_groups('managers')
+        output_type = 'json'
+
+        def __call__(self):
+            return [u for (u, ) in self._cw.execute('Any L WHERE U login L')]
+
+
+.. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxFunction
+   :members:
+
+.. autofunction:: cubicweb.web.views.ajaxcontroller.ajaxfunc
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+from functools import partial
+
+from logilab.common.date import strptime
+from logilab.common.registry import yes
+from logilab.common.deprecation import deprecated
+
+from cubicweb import ObjectNotFound, NoSelectableObject
+from cubicweb.appobject import AppObject
+from cubicweb.utils import json, json_dumps, UStringIO
+from cubicweb.uilib import exc_message
+from cubicweb.web import RemoteCallFailed, DirectResponse
+from cubicweb.web.controller import Controller
+from cubicweb.web.views import vid_from_rset
+from cubicweb.web.views import basecontrollers
+
+
+def optional_kwargs(extraargs):
+    if extraargs is None:
+        return {}
+    # we receive unicode keys which is not supported by the **syntax
+    return dict((str(key), value) for key, value in extraargs.iteritems())
+
+
+class AjaxController(Controller):
+    """AjaxController handles ajax remote calls from javascript
+
+    The following javascript function call:
+
+    .. sourcecode:: javascript
+
+      var d = asyncRemoteExec('foo', 12, "hello");
+      d.addCallback(function(result) {
+          alert('server response is: ' + result);
+      });
+
+    will generate an ajax HTTP GET on the following url::
+
+        BASE_URL/ajax?fname=foo&arg=12&arg="hello"
+
+    The AjaxController controller will therefore be selected to handle those URLs
+    and will itself select the :class:`cubicweb.web.views.ajaxcontroller.AjaxFunction`
+    matching the *fname* parameter.
+    """
+    __regid__ = 'ajax'
+
+    def publish(self, rset=None):
+        self._cw.ajax_request = True
+        try:
+            fname = self._cw.form['fname']
+        except KeyError:
+            raise RemoteCallFailed('no method specified')
+        try:
+            func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+        except ObjectNotFound:
+            # function not found in the registry, inspect JSonController for
+            # backward compatibility
+            try:
+                func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+                func = partial(func, self)
+            except AttributeError:
+                raise RemoteCallFailed('no %s method' % fname)
+            else:
+                self.warning('remote function %s found on JSonController, '
+                             'use AjaxFunction / @ajaxfunc instead', fname)
+        except NoSelectableObject:
+            raise RemoteCallFailed('method %s not available in this context'
+                                   % fname)
+        # no <arg> attribute means the callback takes no argument
+        args = self._cw.form.get('arg', ())
+        if not isinstance(args, (list, tuple)):
+            args = (args,)
+        try:
+            args = [json.loads(arg) for arg in args]
+        except ValueError, exc:
+            self.exception('error while decoding json arguments for '
+                           'js_%s: %s (err: %s)', fname, args, exc)
+            raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
+        try:
+            result = func(*args)
+        except (RemoteCallFailed, DirectResponse):
+            raise
+        except Exception, exc:
+            self.exception('an exception occurred while calling js_%s(%s): %s',
+                           fname, args, exc)
+            raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
+        if result is None:
+            return ''
+        # get unicode on @htmlize methods, encoded string on @jsonize methods
+        elif isinstance(result, unicode):
+            return result.encode(self._cw.encoding)
+        return result
+
+class AjaxFunction(AppObject):
+    """
+    Attributes on this base class are:
+
+    :attr: `check_pageid`: make sure the pageid received is valid before proceeding
+    :attr: `output_type`:
+
+           - *None*: no processing, no change on content-type
+
+           - *json*: serialize with `json_dumps` and set *application/json*
+                     content-type
+
+           - *xhtml*: wrap result in an XML node and forces HTML / XHTML
+                      content-type (use ``_cw.html_content_type()``)
+
+    """
+    __registry__ = 'ajax-func'
+    __select__ = yes()
+    __abstract__ = True
+
+    check_pageid = False
+    output_type = None
+
+    @staticmethod
+    def _rebuild_posted_form(names, values, action=None):
+        form = {}
+        for name, value in zip(names, values):
+            # remove possible __action_xxx inputs
+            if name.startswith('__action'):
+                if action is None:
+                    # strip '__action_' to get the actual action name
+                    action = name[9:]
+                continue
+            # form.setdefault(name, []).append(value)
+            if name in form:
+                curvalue = form[name]
+                if isinstance(curvalue, list):
+                    curvalue.append(value)
+                else:
+                    form[name] = [curvalue, value]
+            else:
+                form[name] = value
+        # simulate click on __action_%s button to help the controller
+        if action:
+            form['__action_%s' % action] = u'whatever'
+        return form
+
+    def validate_form(self, action, names, values):
+        self._cw.form = self._rebuild_posted_form(names, values, action)
+        return basecontrollers._validate_form(self._cw, self._cw.vreg)
+
+    def _exec(self, rql, args=None, rocheck=True):
+        """json mode: execute RQL and return resultset as json"""
+        rql = rql.strip()
+        if rql.startswith('rql:'):
+            rql = rql[4:]
+        if rocheck:
+            self._cw.ensure_ro_rql(rql)
+        try:
+            return self._cw.execute(rql, args)
+        except Exception, ex:
+            self.exception("error in _exec(rql=%s): %s", rql, ex)
+            return None
+        return None
+
+    def _call_view(self, view, paginate=False, **kwargs):
+        divid = self._cw.form.get('divid')
+        # we need to call pagination before with the stream set
+        try:
+            stream = view.set_stream()
+        except AttributeError:
+            stream = UStringIO()
+            kwargs['w'] = stream.write
+            assert not paginate
+        if divid == 'pageContent':
+            # ensure divid isn't reused by the view (e.g. table view)
+            del self._cw.form['divid']
+            # mimick main template behaviour
+            stream.write(u'<div id="pageContent">')
+            vtitle = self._cw.form.get('vtitle')
+            if vtitle:
+                stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+            paginate = True
+        nav_html = UStringIO()
+        if paginate and not view.handle_pagination:
+            view.paginate(w=nav_html.write)
+        stream.write(nav_html.getvalue())
+        if divid == 'pageContent':
+            stream.write(u'<div id="contentmain">')
+        view.render(**kwargs)
+        extresources = self._cw.html_headers.getvalue(skiphead=True)
+        if extresources:
+            stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
+            stream.write(extresources)
+            stream.write(u'</div>\n')
+        if divid == 'pageContent':
+            stream.write(u'</div>%s</div>' % nav_html.getvalue())
+        return stream.getvalue()
+
+
+def _ajaxfunc_factory(implementation, selector=yes(), _output_type=None,
+                      _check_pageid=False, regid=None):
+    """converts a standard python function into an AjaxFunction appobject"""
+    class AnAjaxFunc(AjaxFunction):
+        __regid__ = regid or implementation.__name__
+        __select__ = selector
+        output_type = _output_type
+        check_pageid = _check_pageid
+
+        def serialize(self, content):
+            if self.output_type is None:
+                return content
+            elif self.output_type == 'xhtml':
+                self._cw.set_content_type(self._cw.html_content_type())
+                return ''.join((self._cw.document_surrounding_div(),
+                                content.strip(), u'</div>'))
+            elif self.output_type == 'json':
+                self._cw.set_content_type('application/json')
+                return json_dumps(content)
+            raise RemoteCallFailed('no serializer found for output type %s'
+                                   % self.output_type)
+
+        def __call__(self, *args, **kwargs):
+            if self.check_pageid:
+                data = self._cw.session.data.get(self._cw.pageid)
+                if data is None:
+                    raise RemoteCallFailed(self._cw._('pageid-not-found'))
+            return self.serialize(implementation(self, *args, **kwargs))
+    AnAjaxFunc.__name__ = implementation.__name__
+    # make sure __module__ refers to the original module otherwise
+    # vreg.register(obj) will ignore ``obj``.
+    AnAjaxFunc.__module__ = implementation.__module__
+    return AnAjaxFunc
+
+
+def ajaxfunc(implementation=None, selector=yes(), output_type=None,
+             check_pageid=False, regid=None):
+    """promote a standard function to an ``AjaxFunction`` appobject.
+
+    All parameters are optional:
+
+    :param selector: a custom selector object if needed, default is ``yes()``
+
+    :param output_type: either None, 'json' or 'xhtml' to customize output
+                        content-type. Default is None
+
+    :param check_pageid: whether the function requires a valid `pageid` or not
+                         to proceed. Default is False.
+
+    :param regid: a custom __regid__ for the created ``AjaxFunction`` object. Default
+                  is to keep the wrapped function name.
+
+    ``ajaxfunc`` can be used both as a standalone decorator:
+
+    .. sourcecode:: python
+
+        @ajaxfunc
+        def my_function(self):
+            return 42
+
+    or as a parametrizable decorator:
+
+    .. sourcecode:: python
+
+        @ajaxfunc(output_type='json')
+        def my_function(self):
+            return 42
+
+    """
+    # if used as a parametrized decorator (e.g. @ajaxfunc(output_type='json'))
+    if implementation is None:
+        def _decorator(func):
+            return _ajaxfunc_factory(func, selector=selector,
+                                     _output_type=output_type,
+                                     _check_pageid=check_pageid,
+                                     regid=regid)
+        return _decorator
+    # else, used as a standalone decorator (i.e. @ajaxfunc)
+    return _ajaxfunc_factory(implementation, selector=selector,
+                             _output_type=output_type,
+                             _check_pageid=check_pageid, regid=regid)
+
+
+
+###############################################################################
+#  Cubicweb remote functions for :                                            #
+#  - appobject rendering                                                      #
+#  - user / page session data management                                      #
+###############################################################################
+@ajaxfunc(output_type='xhtml')
+def view(self):
+    # XXX try to use the page-content template
+    req = self._cw
+    rql = req.form.get('rql')
+    if rql:
+        rset = self._exec(rql)
+    elif 'eid' in req.form:
+        rset = self._cw.eid_rset(req.form['eid'])
+    else:
+        rset = None
+    vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
+    try:
+        viewobj = self._cw.vreg['views'].select(vid, req, rset=rset)
+    except NoSelectableObject:
+        vid = req.form.get('fallbackvid', 'noresult')
+        viewobj = self._cw.vreg['views'].select(vid, req, rset=rset)
+    viewobj.set_http_cache_headers()
+    req.validate_cache()
+    return self._call_view(viewobj, paginate=req.form.pop('paginate', False))
+
+
+@ajaxfunc(output_type='xhtml')
+def component(self, compid, rql, registry='components', extraargs=None):
+    if rql:
+        rset = self._exec(rql)
+    else:
+        rset = None
+    # XXX while it sounds good, addition of the try/except below cause pb:
+    # when filtering using facets return an empty rset, the edition box
+    # isn't anymore selectable, as expected. The pb is that with the
+    # try/except below, we see a "an error occurred" message in the ui, while
+    # we don't see it without it. Proper fix would probably be to deal with
+    # this by allowing facet handling code to tell to js_component that such
+    # error is expected and should'nt be reported.
+    #try:
+    comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
+                                          **optional_kwargs(extraargs))
+    #except NoSelectableObject:
+    #    raise RemoteCallFailed('unselectable')
+    return self._call_view(comp, **optional_kwargs(extraargs))
+
+@ajaxfunc(output_type='xhtml')
+def render(self, registry, oid, eid=None,
+              selectargs=None, renderargs=None):
+    if eid is not None:
+        rset = self._cw.eid_rset(eid)
+        # XXX set row=0
+    elif self._cw.form.get('rql'):
+        rset = self._cw.execute(self._cw.form['rql'])
+    else:
+        rset = None
+    viewobj = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
+                                             **optional_kwargs(selectargs))
+    return self._call_view(viewobj, **optional_kwargs(renderargs))
+
+
+@ajaxfunc(output_type='json')
+def i18n(self, msgids):
+    """returns the translation of `msgid`"""
+    return [self._cw._(msgid) for msgid in msgids]
+
+@ajaxfunc(output_type='json')
+def format_date(self, strdate):
+    """returns the formatted date for `msgid`"""
+    date = strptime(strdate, '%Y-%m-%d %H:%M:%S')
+    return self._cw.format_date(date)
+
+@ajaxfunc(output_type='json')
+def external_resource(self, resource):
+    """returns the URL of the external resource named `resource`"""
+    return self._cw.uiprops[resource]
+
+@ajaxfunc(output_type='json', check_pageid=True)
+def user_callback(self, cbname):
+    """execute the previously registered user callback `cbname`.
+
+    If matching callback is not found, return None
+    """
+    page_data = self._cw.session.data.get(self._cw.pageid, {})
+    try:
+        cb = page_data[cbname]
+    except KeyError:
+        self.warning('unable to find user callback %s', cbname)
+        return None
+    return cb(self._cw)
+
+
+@ajaxfunc
+def unregister_user_callback(self, cbname):
+    """unregister user callback `cbname`"""
+    self._cw.unregister_callback(self._cw.pageid, cbname)
+
+@ajaxfunc
+def unload_page_data(self):
+    """remove user's session data associated to current pageid"""
+    self._cw.session.data.pop(self._cw.pageid, None)
+
+@ajaxfunc(output_type='json')
+@deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
+def set_cookie(self, cookiename, cookievalue):
+    """generates the Set-Cookie HTTP reponse header corresponding
+    to `cookiename` / `cookievalue`.
+    """
+    cookiename, cookievalue = str(cookiename), str(cookievalue)
+    self._cw.set_cookie(cookiename, cookievalue)
--- a/web/views/ajaxedit.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/ajaxedit.py	Thu Feb 23 19:06:14 2012 +0100
@@ -21,7 +21,7 @@
 
 from cubicweb import role
 from cubicweb.view import View
-from cubicweb.selectors import match_form_params, match_kwargs
+from cubicweb.predicates import match_form_params, match_kwargs
 from cubicweb.web import component, stdmsgs, formwidgets as fw
 
 class AddRelationView(component.EditRelationMixIn, View):
--- a/web/views/autoform.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/autoform.py	Thu Feb 23 19:06:14 2012 +0100
@@ -126,18 +126,19 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import iclassmethod, cached
 from logilab.common.deprecation import deprecated
+from logilab.common.registry import classid
 
 from cubicweb import typed_eid, neg_role, uilib
-from cubicweb.vregistry import classid
 from cubicweb.schema import display_name
 from cubicweb.view import EntityView
-from cubicweb.selectors import (
+from cubicweb.predicates import (
     match_kwargs, match_form_params, non_final_entity,
     specified_etype_implements)
 from cubicweb.utils import json_dumps
 from cubicweb.web import (stdmsgs, uicfg, eid_param,
                           form as f, formwidgets as fw, formfields as ff)
 from cubicweb.web.views import forms
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 _AFS = uicfg.autoform_section
 _AFFK = uicfg.autoform_field_kwargs
@@ -437,6 +438,57 @@
         execute(rql, {'x': subj, 'y': obj})
 
 
+# ajax edition helpers ########################################################
+@ajaxfunc(output_type='xhtml', check_pageid=True)
+def inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
+    view = self._cw.vreg['views'].select('inline-creation', self._cw,
+                                         etype=ttype, rtype=rtype, role=role,
+                                         peid=peid, petype=petype)
+    return self._call_view(view, i18nctx=i18nctx)
+
+@ajaxfunc(output_type='json')
+def validate_form(self, action, names, values):
+    return self.validate_form(action, names, values)
+
+@ajaxfunc
+def cancel_edition(self, errorurl):
+    """cancelling edition from javascript
+
+    We need to clear associated req's data :
+      - errorurl
+      - pending insertions / deletions
+    """
+    self._cw.cancel_edition(errorurl)
+
+
+def _add_pending(req, eidfrom, rel, eidto, kind):
+    key = 'pending_%s' % kind
+    pendings = req.session.data.setdefault(key, set())
+    pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
+
+def _remove_pending(req, eidfrom, rel, eidto, kind):
+    key = 'pending_%s' % kind
+    pendings = req.session.data[key]
+    pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
+
+@ajaxfunc(output_type='json')
+def remove_pending_insert(self, (eidfrom, rel, eidto)):
+    _remove_pending(self._cw, eidfrom, rel, eidto, 'insert')
+
+@ajaxfunc(output_type='json')
+def add_pending_inserts(self, tripletlist):
+    for eidfrom, rel, eidto in tripletlist:
+        _add_pending(self._cw, eidfrom, rel, eidto, 'insert')
+
+@ajaxfunc(output_type='json')
+def remove_pending_delete(self, (eidfrom, rel, eidto)):
+    _remove_pending(self._cw, eidfrom, rel, eidto, 'delete')
+
+@ajaxfunc(output_type='json')
+def add_pending_delete(self, (eidfrom, rel, eidto)):
+    _add_pending(self._cw, eidfrom, rel, eidto, 'delete')
+
+
 class GenericRelationsWidget(fw.FieldWidget):
 
     def render(self, form, field, renderer):
--- a/web/views/basecomponents.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/basecomponents.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -26,12 +26,13 @@
 _ = unicode
 
 from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
 from logilab.common.deprecation import class_renamed
 from rql import parse
 
-from cubicweb.selectors import (yes, match_form_params, match_context,
-                                multi_etypes_rset, configuration_values,
-                                anonymous_user, authenticated_user)
+from cubicweb.predicates import (match_form_params, match_context,
+                                 multi_etypes_rset, configuration_values,
+                                 anonymous_user, authenticated_user)
 from cubicweb.schema import display_name
 from cubicweb.utils import wrap_on_write
 from cubicweb.uilib import toggle_action
@@ -187,7 +188,7 @@
         if msg is None:
             msgs = []
             if self._cw.cnx:
-                srcmsg = self._cw.get_shared_data('sources_error', pop=True)
+                srcmsg = self._cw.get_shared_data('sources_error', pop=True, txdata=True)
                 if srcmsg:
                     msgs.append(srcmsg)
             reqmsg = self._cw.message # XXX don't call self._cw.message twice
--- a/web/views/basecontrollers.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/basecontrollers.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,20 +22,21 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from logilab.common.date import strptime
+from warnings import warn
+
 from logilab.common.deprecation import deprecated
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, typed_eid)
-from cubicweb.utils import UStringIO, json, json_dumps
-from cubicweb.uilib import exc_message
-from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
-from cubicweb.mail import format_mail
-from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, facet
+from cubicweb.utils import json_dumps
+from cubicweb.predicates import (authenticated_user, anonymous_user,
+                                match_form_params)
+from cubicweb.web import Redirect, RemoteCallFailed
 from cubicweb.web.controller import Controller
-from cubicweb.web.views import vid_from_rset, formrenderers
+from cubicweb.web.views import vid_from_rset
 
 
+@deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead')
 def jsonize(func):
     """decorator to sets correct content_type and calls `json_dumps` on
     results
@@ -46,6 +47,7 @@
     wrapper.__name__ = func.__name__
     return wrapper
 
+@deprecated('[3.15] xhtmlize is deprecated, use AjaxFunction appobjects instead')
 def xhtmlize(func):
     """decorator to sets correct content_type and calls `xmlize` on results"""
     def wrapper(self, *args, **kwargs):
@@ -56,6 +58,7 @@
     wrapper.__name__ = func.__name__
     return wrapper
 
+@deprecated('[3.15] check_pageid is deprecated, use AjaxFunction appobjects instead')
 def check_pageid(func):
     """decorator which checks the given pageid is found in the
     user's session data
@@ -234,7 +237,7 @@
 </script>""" %  (domid, callback, errback, jsargs, cbargs)
 
     def publish(self, rset=None):
-        self._cw.json_request = True
+        self._cw.ajax_request = True
         # XXX unclear why we have a separated controller here vs
         # js_validate_form on the json controller
         status, args, entity = _validate_form(self._cw, self._cw.vreg)
@@ -242,339 +245,18 @@
             self._cw.encoding)
         return self.response(domid, status, args, entity)
 
-def optional_kwargs(extraargs):
-    if extraargs is None:
-        return {}
-    # we receive unicode keys which is not supported by the **syntax
-    return dict((str(key), value) for key, value in extraargs.iteritems())
-
 
 class JSonController(Controller):
     __regid__ = 'json'
 
     def publish(self, rset=None):
-        """call js_* methods. Expected form keys:
-
-        :fname: the method name without the js_ prefix
-        :args: arguments list (json)
-
-        note: it's the responsability of js_* methods to set the correct
-        response content type
-        """
-        self._cw.json_request = True
-        try:
-            fname = self._cw.form['fname']
-            func = getattr(self, 'js_%s' % fname)
-        except KeyError:
-            raise RemoteCallFailed('no method specified')
-        except AttributeError:
-            raise RemoteCallFailed('no %s method' % fname)
-        # no <arg> attribute means the callback takes no argument
-        args = self._cw.form.get('arg', ())
-        if not isinstance(args, (list, tuple)):
-            args = (args,)
-        try:
-            args = [json.loads(arg) for arg in args]
-        except ValueError, exc:
-            self.exception('error while decoding json arguments for js_%s: %s (err: %s)',
-                           fname, args, exc)
-            raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
-        try:
-            result = func(*args)
-        except (RemoteCallFailed, DirectResponse):
-            raise
-        except Exception, exc:
-            self.exception('an exception occurred while calling js_%s(%s): %s',
-                           fname, args, exc)
-            raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
-        if result is None:
-            return ''
-        # get unicode on @htmlize methods, encoded string on @jsonize methods
-        elif isinstance(result, unicode):
-            return result.encode(self._cw.encoding)
-        return result
-
-    def _rebuild_posted_form(self, names, values, action=None):
-        form = {}
-        for name, value in zip(names, values):
-            # remove possible __action_xxx inputs
-            if name.startswith('__action'):
-                if action is None:
-                    # strip '__action_' to get the actual action name
-                    action = name[9:]
-                continue
-            # form.setdefault(name, []).append(value)
-            if name in form:
-                curvalue = form[name]
-                if isinstance(curvalue, list):
-                    curvalue.append(value)
-                else:
-                    form[name] = [curvalue, value]
-            else:
-                form[name] = value
-        # simulate click on __action_%s button to help the controller
-        if action:
-            form['__action_%s' % action] = u'whatever'
-        return form
-
-    def _exec(self, rql, args=None, rocheck=True):
-        """json mode: execute RQL and return resultset as json"""
-        rql = rql.strip()
-        if rql.startswith('rql:'):
-            rql = rql[4:]
-        if rocheck:
-            self._cw.ensure_ro_rql(rql)
-        try:
-            return self._cw.execute(rql, args)
-        except Exception, ex:
-            self.exception("error in _exec(rql=%s): %s", rql, ex)
-            return None
-        return None
-
-    def _call_view(self, view, paginate=False, **kwargs):
-        divid = self._cw.form.get('divid')
-        # we need to call pagination before with the stream set
-        try:
-            stream = view.set_stream()
-        except AttributeError:
-            stream = UStringIO()
-            kwargs['w'] = stream.write
-            assert not paginate
-        if divid == 'pageContent':
-            # ensure divid isn't reused by the view (e.g. table view)
-            del self._cw.form['divid']
-            # mimick main template behaviour
-            stream.write(u'<div id="pageContent">')
-            vtitle = self._cw.form.get('vtitle')
-            if vtitle:
-                stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
-            paginate = True
-        nav_html = UStringIO()
-        if paginate and not view.handle_pagination:
-            view.paginate(w=nav_html.write)
-        stream.write(nav_html.getvalue())
-        if divid == 'pageContent':
-            stream.write(u'<div id="contentmain">')
-        view.render(**kwargs)
-        extresources = self._cw.html_headers.getvalue(skiphead=True)
-        if extresources:
-            stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
-            stream.write(extresources)
-            stream.write(u'</div>\n')
-        if divid == 'pageContent':
-            stream.write(u'</div>%s</div>' % nav_html.getvalue())
-        return stream.getvalue()
-
-    @xhtmlize
-    def js_view(self):
-        # XXX try to use the page-content template
-        req = self._cw
-        rql = req.form.get('rql')
-        if rql:
-            rset = self._exec(rql)
-        elif 'eid' in req.form:
-            rset = self._cw.eid_rset(req.form['eid'])
-        else:
-            rset = None
-        vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
-        try:
-            view = self._cw.vreg['views'].select(vid, req, rset=rset)
-        except NoSelectableObject:
-            vid = req.form.get('fallbackvid', 'noresult')
-            view = self._cw.vreg['views'].select(vid, req, rset=rset)
-        self.validate_cache(view)
-        return self._call_view(view, paginate=req.form.pop('paginate', False))
-
-    @xhtmlize
-    def js_prop_widget(self, propkey, varname, tabindex=None):
-        """specific method for CWProperty handling"""
-        entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
-        entity.eid = varname
-        entity['pkey'] = propkey
-        form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
-        form.build_context()
-        vfield = form.field_by_name('value')
-        renderer = formrenderers.FormRenderer(self._cw)
-        return vfield.render(form, renderer, tabindex=tabindex) \
-               + renderer.render_help(form, vfield)
-
-    @xhtmlize
-    def js_component(self, compid, rql, registry='components', extraargs=None):
-        if rql:
-            rset = self._exec(rql)
-        else:
-            rset = None
-        # XXX while it sounds good, addition of the try/except below cause pb:
-        # when filtering using facets return an empty rset, the edition box
-        # isn't anymore selectable, as expected. The pb is that with the
-        # try/except below, we see a "an error occurred" message in the ui, while
-        # we don't see it without it. Proper fix would probably be to deal with
-        # this by allowing facet handling code to tell to js_component that such
-        # error is expected and should'nt be reported.
-        #try:
-        comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
-                                              **optional_kwargs(extraargs))
-        #except NoSelectableObject:
-        #    raise RemoteCallFailed('unselectable')
-        return self._call_view(comp, **optional_kwargs(extraargs))
-
-    @xhtmlize
-    def js_render(self, registry, oid, eid=None,
-                  selectargs=None, renderargs=None):
-        if eid is not None:
-            rset = self._cw.eid_rset(eid)
-            # XXX set row=0
-        elif self._cw.form.get('rql'):
-            rset = self._cw.execute(self._cw.form['rql'])
-        else:
-            rset = None
-        view = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
-                                              **optional_kwargs(selectargs))
-        return self._call_view(view, **optional_kwargs(renderargs))
-
-    @check_pageid
-    @xhtmlize
-    def js_inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
-        view = self._cw.vreg['views'].select('inline-creation', self._cw,
-                                             etype=ttype, rtype=rtype, role=role,
-                                             peid=peid, petype=petype)
-        return self._call_view(view, i18nctx=i18nctx)
-
-    @jsonize
-    def js_validate_form(self, action, names, values):
-        return self.validate_form(action, names, values)
-
-    def validate_form(self, action, names, values):
-        self._cw.form = self._rebuild_posted_form(names, values, action)
-        return _validate_form(self._cw, self._cw.vreg)
-
-    @xhtmlize
-    def js_reledit_form(self):
-        req = self._cw
-        args = dict((x, req.form[x])
-                    for x in ('formid', 'rtype', 'role', 'reload', 'action'))
-        rset = req.eid_rset(typed_eid(self._cw.form['eid']))
-        try:
-            args['reload'] = json.loads(args['reload'])
-        except ValueError: # not true/false, an absolute url
-            assert args['reload'].startswith('http')
-        view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype'])
-        return self._call_view(view, **args)
-
-    @jsonize
-    def js_i18n(self, msgids):
-        """returns the translation of `msgid`"""
-        return [self._cw._(msgid) for msgid in msgids]
-
-    @jsonize
-    def js_format_date(self, strdate):
-        """returns the formatted date for `msgid`"""
-        date = strptime(strdate, '%Y-%m-%d %H:%M:%S')
-        return self._cw.format_date(date)
-
-    @jsonize
-    def js_external_resource(self, resource):
-        """returns the URL of the external resource named `resource`"""
-        return self._cw.uiprops[resource]
-
-    @check_pageid
-    @jsonize
-    def js_user_callback(self, cbname):
-        page_data = self._cw.session.data.get(self._cw.pageid, {})
-        try:
-            cb = page_data[cbname]
-        except KeyError:
-            return None
-        return cb(self._cw)
-
-    @jsonize
-    def js_filter_build_rql(self, names, values):
-        form = self._rebuild_posted_form(names, values)
-        self._cw.form = form
-        builder = facet.FilterRQLBuilder(self._cw)
-        return builder.build_rql()
-
-    @jsonize
-    def js_filter_select_content(self, facetids, rql, mainvar):
-        # Union unsupported yet
-        select = self._cw.vreg.parse(self._cw, rql).children[0]
-        filtered_variable = facet.get_filtered_variable(select, mainvar)
-        facet.prepare_select(select, filtered_variable)
-        update_map = {}
-        for fid in facetids:
-            fobj = facet.get_facet(self._cw, fid, select, filtered_variable)
-            update_map[fid] = fobj.possible_values()
-        return update_map
-
-    def js_unregister_user_callback(self, cbname):
-        self._cw.unregister_callback(self._cw.pageid, cbname)
-
-    def js_unload_page_data(self):
-        self._cw.session.data.pop(self._cw.pageid, None)
-
-    def js_cancel_edition(self, errorurl):
-        """cancelling edition from javascript
-
-        We need to clear associated req's data :
-          - errorurl
-          - pending insertions / deletions
-        """
-        self._cw.cancel_edition(errorurl)
-
-    def js_delete_bookmark(self, beid):
-        rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
-        self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid})
-
-    def js_node_clicked(self, treeid, nodeeid):
-        """add/remove eid in treestate cookie"""
-        from cubicweb.web.views.treeview import treecookiename
-        cookies = self._cw.get_cookie()
-        statename = treecookiename(treeid)
-        treestate = cookies.get(statename)
-        if treestate is None:
-            self._cw.set_cookie(statename, nodeeid)
-        else:
-            marked = set(filter(None, treestate.value.split(':')))
-            if nodeeid in marked:
-                marked.remove(nodeeid)
-            else:
-                marked.add(nodeeid)
-            self._cw.set_cookie(statename, ':'.join(marked))
-
-    @jsonize
-    @deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
-    def js_set_cookie(self, cookiename, cookievalue):
-        cookiename, cookievalue = str(cookiename), str(cookievalue)
-        self._cw.set_cookie(cookiename, cookievalue)
-
-    # relations edition stuff ##################################################
-
-    def _add_pending(self, eidfrom, rel, eidto, kind):
-        key = 'pending_%s' % kind
-        pendings = self._cw.session.data.setdefault(key, set())
-        pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-
-    def _remove_pending(self, eidfrom, rel, eidto, kind):
-        key = 'pending_%s' % kind
-        pendings = self._cw.session.data[key]
-        pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-
-    def js_remove_pending_insert(self, (eidfrom, rel, eidto)):
-        self._remove_pending(eidfrom, rel, eidto, 'insert')
-
-    def js_add_pending_inserts(self, tripletlist):
-        for eidfrom, rel, eidto in tripletlist:
-            self._add_pending(eidfrom, rel, eidto, 'insert')
-
-    def js_remove_pending_delete(self, (eidfrom, rel, eidto)):
-        self._remove_pending(eidfrom, rel, eidto, 'delete')
-
-    def js_add_pending_delete(self, (eidfrom, rel, eidto)):
-        self._add_pending(eidfrom, rel, eidto, 'delete')
+        warn('[3.15] JSONController is deprecated, use AjaxController instead',
+             DeprecationWarning)
+        ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli)
+        return ajax_controller.publish(rset)
 
 
 # XXX move to massmailing
-
 class MailBugReportController(Controller):
     __regid__ = 'reportbug'
     __select__ = match_form_params('description')
--- a/web/views/basetemplates.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/basetemplates.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,9 +22,9 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import class_renamed
+from logilab.common.registry import objectify_predicate
 
-from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import match_kwargs, no_cnx, anonymous_user
+from cubicweb.predicates import match_kwargs, no_cnx, anonymous_user
 from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW, StartupView
 from cubicweb.utils import UStringIO
 from cubicweb.schema import display_name
@@ -84,7 +84,7 @@
             self.w(u'<h2>%s</h2>' % msg)
 
 
-@objectify_selector
+@objectify_predicate
 def templatable_view(cls, req, rset, *args, **kwargs):
     view = kwargs.pop('view', None)
     if view is None:
--- a/web/views/baseviews.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/baseviews.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -84,9 +84,10 @@
 from rql import nodes
 
 from logilab.mtconverter import TransformError, xml_escape
+from logilab.common.registry import yes
 
 from cubicweb import NoSelectableObject, tags
-from cubicweb.selectors import yes, empty_rset, one_etype_rset, match_kwargs
+from cubicweb.predicates import empty_rset, one_etype_rset, match_kwargs
 from cubicweb.schema import display_name
 from cubicweb.view import EntityView, AnyRsetView, View
 from cubicweb.uilib import cut
@@ -157,7 +158,7 @@
     """:__regid__: *incontext*
 
     This view is used whenthe entity should be considered as displayed in its
-    context. By default it produces the result of `textincontext` wrapped in a
+    context. By default it produces the result of ``entity.dc_title()`` wrapped in a
     link leading to the primary view of the entity.
     """
     __regid__ = 'incontext'
@@ -165,18 +166,15 @@
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
         desc = cut(entity.dc_description(), 50)
-        self.w(u'<a href="%s" title="%s">' % (
-            xml_escape(entity.absolute_url()), xml_escape(desc)))
-        self.w(xml_escape(self._cw.view('textincontext', self.cw_rset,
-                                        row=row, col=col)))
-        self.w(u'</a>')
-
+        self.w(u'<a href="%s" title="%s">%s</a>' % (
+            xml_escape(entity.absolute_url()), xml_escape(desc),
+            xml_escape(entity.dc_title())))
 
 class OutOfContextView(EntityView):
     """:__regid__: *outofcontext*
 
     This view is used whenthe entity should be considered as displayed out of
-    its context. By default it produces the result of `textoutofcontext` wrapped
+    its context. By default it produces the result of ``entity.dc_long_title()`` wrapped
     in a link leading to the primary view of the entity.
     """
     __regid__ = 'outofcontext'
@@ -184,11 +182,9 @@
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
         desc = cut(entity.dc_description(), 50)
-        self.w(u'<a href="%s" title="%s">' % (
-            xml_escape(entity.absolute_url()), xml_escape(desc)))
-        self.w(xml_escape(self._cw.view('textoutofcontext', self.cw_rset,
-                                        row=row, col=col)))
-        self.w(u'</a>')
+        self.w(u'<a href="%s" title="%s">%s</a>' % (
+            xml_escape(entity.absolute_url()), xml_escape(desc),
+            xml_escape(entity.dc_long_title())))
 
 
 class OneLineView(EntityView):
@@ -205,9 +201,12 @@
         """the one line view for an entity: linked text view
         """
         entity = self.cw_rset.get_entity(row, col)
-        self.w(u'<a href="%s">' % xml_escape(entity.absolute_url()))
-        self.w(xml_escape(self._cw.view('text', self.cw_rset, row=row, col=col)))
-        self.w(u'</a>')
+        desc = cut(entity.dc_description(), 50)
+        title = cut(entity.dc_title(),
+                    self._cw.property_value('navigation.short-line-size'))
+        self.w(u'<a href="%s" title="%s">%s</a>' % (
+                xml_escape(entity.absolute_url()), xml_escape(desc),
+                xml_escape(title)))
 
 
 # text views ###################################################################
--- a/web/views/bookmark.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/bookmark.py	Thu Feb 23 19:06:14 2012 +0100
@@ -22,11 +22,12 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb import Unauthorized
-from cubicweb.selectors import is_instance, one_line_rset
+from cubicweb import Unauthorized, typed_eid
+from cubicweb.predicates import is_instance, one_line_rset
 from cubicweb.web import (action, component, uicfg, htmlwidgets,
                           formwidgets as fw)
 from cubicweb.web.views import primary
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 _abaa = uicfg.actionbox_appearsin_addmenu
 _abaa.tag_subject_of(('*', 'bookmarked_by', '*'), False)
@@ -133,3 +134,8 @@
             menu.append(self.link(req._('pick existing bookmarks'), url))
             self.append(menu)
         self.render_items(w)
+
+@ajaxfunc
+def delete_bookmark(self, beid):
+    rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
+    self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid})
--- a/web/views/boxes.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/boxes.py	Thu Feb 23 19:06:14 2012 +0100
@@ -36,7 +36,7 @@
 from logilab.common.deprecation import class_deprecated
 
 from cubicweb import Unauthorized
-from cubicweb.selectors import (match_user_groups, match_kwargs,
+from cubicweb.predicates import (match_user_groups, match_kwargs,
                                 non_final_entity, nonempty_rset,
                                 match_context, contextual)
 from cubicweb.utils import wrap_on_write
@@ -136,13 +136,13 @@
 
     title = _('search')
     order = 0
-    formdef = u"""<form action="%s">
-<table id="tsearch"><tr><td>
-<input id="norql" type="text" accesskey="q" tabindex="%s" title="search text" value="%s" name="rql" />
+    formdef = u"""<form action="%(action)s">
+<table id="%(id)s"><tr><td>
+<input class="norql" type="text" accesskey="q" tabindex="%(tabindex1)s" title="search text" value="%(value)s" name="rql" />
 <input type="hidden" name="__fromsearchbox" value="1" />
 <input type="hidden" name="subvid" value="tsearch" />
 </td><td>
-<input tabindex="%s" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
+<input tabindex="%(tabindex2)s" type="submit" class="rqlsubmit" value="" />
  </td></tr></table>
  </form>"""
 
@@ -155,8 +155,13 @@
             rql = self._cw.form.get('rql', '')
         else:
             rql = ''
-        w(self.formdef % (self._cw.build_url('view'), self._cw.next_tabindex(),
-                          xml_escape(rql), self._cw.next_tabindex()))
+        tabidx1 = self._cw.next_tabindex()
+        tabidx2 = self._cw.next_tabindex()
+        w(self.formdef % {'action': self._cw.build_url('view'),
+                          'value': xml_escape(rql),
+                          'id': self.cw_extra_kwargs.get('domid', 'tsearch'),
+                          'tabindex1': tabidx1,
+                          'tabindex2': tabidx2})
 
 
 # boxes disabled by default ###################################################
--- a/web/views/calendar.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/calendar.py	Thu Feb 23 19:06:14 2012 +0100
@@ -28,7 +28,7 @@
 
 from cubicweb.utils import json_dumps, make_uid
 from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements, adaptable
+from cubicweb.predicates import implements, adaptable
 from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
 
 # useful constants & functions ################################################
--- a/web/views/cwproperties.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/cwproperties.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -25,9 +25,8 @@
 from logilab.common.decorators import cached
 
 from cubicweb import UnknownProperty
-from cubicweb.selectors import (one_line_rset, none_rset, is_instance,
-                                match_user_groups, objectify_selector,
-                                logged_user_in_rset)
+from cubicweb.predicates import (one_line_rset, none_rset, is_instance,
+                                 match_user_groups, logged_user_in_rset)
 from cubicweb.view import StartupView
 from cubicweb.web import uicfg, stdmsgs
 from cubicweb.web.form import FormViewMixIn
@@ -35,6 +34,7 @@
 from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
                                       FieldWidget)
 from cubicweb.web.views import primary, formrenderers, editcontroller
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
 
@@ -419,6 +419,20 @@
         """
         return 'view', {}
 
+
+@ajaxfunc(output_type='xhtml')
+def prop_widget(self, propkey, varname, tabindex=None):
+    """specific method for CWProperty handling"""
+    entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
+    entity.eid = varname
+    entity['pkey'] = propkey
+    form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
+    form.build_context()
+    vfield = form.field_by_name('value')
+    renderer = formrenderers.FormRenderer(self._cw)
+    return vfield.render(form, renderer, tabindex=tabindex) \
+           + renderer.render_help(form, vfield)
+
 _afs = uicfg.autoform_section
 _afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
 _afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
--- a/web/views/cwsources.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/cwsources.py	Thu Feb 23 19:06:14 2012 +0100
@@ -29,7 +29,7 @@
 
 from cubicweb import Unauthorized, tags
 from cubicweb.utils import make_uid
-from cubicweb.selectors import (is_instance, score_entity, has_related_entities,
+from cubicweb.predicates import (is_instance, score_entity, has_related_entities,
                                 match_user_groups, match_kwargs, match_view)
 from cubicweb.view import EntityView, StartupView
 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
--- a/web/views/cwuser.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/cwuser.py	Thu Feb 23 19:06:14 2012 +0100
@@ -26,7 +26,7 @@
 
 from cubicweb import tags
 from cubicweb.schema import display_name
-from cubicweb.selectors import one_line_rset, is_instance, match_user_groups
+from cubicweb.predicates import one_line_rset, is_instance, match_user_groups
 from cubicweb.view import EntityView, StartupView
 from cubicweb.web import action, uicfg, formwidgets
 from cubicweb.web.views import tabs, tableview, actions, add_etype_button
--- a/web/views/debug.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/debug.py	Thu Feb 23 19:06:14 2012 +0100
@@ -25,7 +25,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import BadConnectionId
-from cubicweb.selectors import none_rset, match_user_groups
+from cubicweb.predicates import none_rset, match_user_groups
 from cubicweb.view import StartupView
 from cubicweb.web.views import actions, tabs
 
--- a/web/views/editcontroller.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/editcontroller.py	Thu Feb 23 19:06:14 2012 +0100
@@ -27,7 +27,7 @@
 
 from cubicweb import Binary, ValidationError, typed_eid
 from cubicweb.view import EntityAdapter, implements_adapter_compat
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
                           ProcessFormError)
 from cubicweb.web.views import basecontrollers, autoform
@@ -161,7 +161,7 @@
             neweid = entity.eid
         except ValidationError, ex:
             self._to_create[eid] = ex.entity
-            if self._cw.json_request: # XXX (syt) why?
+            if self._cw.ajax_request: # XXX (syt) why?
                 ex.entity = eid
             raise
         self._to_create[eid] = neweid
--- a/web/views/editforms.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/editforms.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -26,11 +26,12 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cached
+from logilab.common.registry import yes
 from logilab.common.deprecation import class_moved
 
 from cubicweb import tags
-from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
-                                specified_etype_implements, is_instance, yes)
+from cubicweb.predicates import (match_kwargs, one_line_rset, non_final_entity,
+                                specified_etype_implements, is_instance)
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
 from cubicweb.web import uicfg, stdmsgs, eid_param, \
--- a/web/views/editviews.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/editviews.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -25,8 +25,8 @@
 
 from cubicweb import typed_eid
 from cubicweb.view import EntityView, StartupView
-from cubicweb.selectors import (one_line_rset, non_final_entity,
-                                match_search_state)
+from cubicweb.predicates import (one_line_rset, non_final_entity,
+                                 match_search_state)
 from cubicweb.web import httpcache
 from cubicweb.web.views import baseviews, linksearch_select_url
 
--- a/web/views/emailaddress.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/emailaddress.py	Thu Feb 23 19:06:14 2012 +0100
@@ -22,7 +22,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.schema import display_name
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb import Unauthorized
 from cubicweb.web import uicfg
 from cubicweb.web.views import baseviews, primary, ibreadcrumbs
--- a/web/views/embedding.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/embedding.py	Thu Feb 23 19:06:14 2012 +0100
@@ -29,7 +29,7 @@
 
 from logilab.mtconverter import guess_encoding
 
-from cubicweb.selectors import (one_line_rset, score_entity, implements,
+from cubicweb.predicates import (one_line_rset, score_entity, implements,
                                 adaptable, match_search_state)
 from cubicweb.interfaces import IEmbedable
 from cubicweb.view import NOINDEX, NOFOLLOW, EntityAdapter, implements_adapter_compat
--- a/web/views/facets.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/facets.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -24,14 +24,15 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cachedproperty
+from logilab.common.registry import objectify_predicate, yes
 
-from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import (non_final_entity, multi_lines_rset,
-                                match_context_prop, yes, relation_possible)
+from cubicweb.predicates import (non_final_entity, multi_lines_rset,
+                                 match_context_prop, relation_possible)
 from cubicweb.utils import json_dumps
 from cubicweb.uilib import css_em_num_value
 from cubicweb.view import AnyRsetView
 from cubicweb.web import component, facet as facetbase
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 def facets(req, rset, context, mainvar=None, **kwargs):
     """return the base rql and a list of widgets for facets applying to the
@@ -81,7 +82,7 @@
     return baserql, [wdg for facet, wdg in wdgs if wdg is not None]
 
 
-@objectify_selector
+@objectify_predicate
 def contextview_selector(cls, req, rset=None, row=None, col=None, view=None,
                          **kwargs):
     if view:
@@ -96,7 +97,7 @@
         return len(wdgs)
     return 0
 
-@objectify_selector
+@objectify_predicate
 def has_facets(cls, req, rset=None, **kwargs):
     if rset is None or rset.rowcount < 2:
         return 0
@@ -313,6 +314,28 @@
             w(u'</div>')
         w(u'</div>\n')
 
+# python-ajax remote functions used by facet widgets #########################
+
+@ajaxfunc(output_type='json')
+def filter_build_rql(self, names, values):
+    form = self._rebuild_posted_form(names, values)
+    self._cw.form = form
+    builder = facetbase.FilterRQLBuilder(self._cw)
+    return builder.build_rql()
+
+@ajaxfunc(output_type='json')
+def filter_select_content(self, facetids, rql, mainvar):
+    # Union unsupported yet
+    select = self._cw.vreg.parse(self._cw, rql).children[0]
+    filtered_variable = facetbase.get_filtered_variable(select, mainvar)
+    facetbase.prepare_select(select, filtered_variable)
+    update_map = {}
+    for fid in facetids:
+        fobj = facetbase.get_facet(self._cw, fid, select, filtered_variable)
+        update_map[fid] = fobj.possible_values()
+    return update_map
+
+
 
 # facets ######################################################################
 
--- a/web/views/formrenderers.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/formrenderers.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -38,10 +38,11 @@
 from warnings import warn
 
 from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
 
 from cubicweb import tags, uilib
 from cubicweb.appobject import AppObject
-from cubicweb.selectors import is_instance, yes
+from cubicweb.predicates import is_instance
 from cubicweb.utils import json_dumps, support_args
 from cubicweb.web import eid_param, formwidgets as fwdgs
 
--- a/web/views/forms.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/forms.py	Thu Feb 23 19:06:14 2012 +0100
@@ -54,7 +54,7 @@
 
 from cubicweb import ValidationError, typed_eid
 from cubicweb.utils import support_args
-from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
+from cubicweb.predicates import non_final_entity, match_kwargs, one_line_rset
 from cubicweb.web import RequestError, ProcessFormError
 from cubicweb.web import uicfg, form, formwidgets as fwdgs
 from cubicweb.web.formfields import guess_field
@@ -406,7 +406,7 @@
             return self.force_session_key
         # XXX if this is a json request, suppose we should redirect to the
         # entity primary view
-        if self._cw.json_request and self.edited_entity.has_eid():
+        if self._cw.ajax_request and self.edited_entity.has_eid():
             return '%s#%s' % (self.edited_entity.absolute_url(), self.domid)
         # XXX we should not consider some url parameters that may lead to
         # different url after a validation error
--- a/web/views/ibreadcrumbs.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/ibreadcrumbs.py	Thu Feb 23 19:06:14 2012 +0100
@@ -27,7 +27,7 @@
 #from cubicweb.interfaces import IBreadCrumbs
 from cubicweb import tags, uilib
 from cubicweb.entity import Entity
-from cubicweb.selectors import (is_instance, one_line_rset, adaptable,
+from cubicweb.predicates import (is_instance, one_line_rset, adaptable,
                                 one_etype_rset, multi_lines_rset, any_rset,
                                 match_form_params)
 from cubicweb.view import EntityView, EntityAdapter
--- a/web/views/idownloadable.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/idownloadable.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -27,8 +27,8 @@
 
 from cubicweb import tags
 from cubicweb.view import EntityView
-from cubicweb.selectors import (one_line_rset, is_instance, match_context_prop,
-                                adaptable, has_mimetype)
+from cubicweb.predicates import (one_line_rset, is_instance, match_context_prop,
+                                 adaptable, has_mimetype)
 from cubicweb.mttransforms import ENGINE
 from cubicweb.web import component, httpcache
 from cubicweb.web.views import primary, baseviews
--- a/web/views/igeocodable.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/igeocodable.py	Thu Feb 23 19:06:14 2012 +0100
@@ -21,7 +21,7 @@
 
 from cubicweb.interfaces import IGeocodable
 from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
-from cubicweb.selectors import implements, adaptable
+from cubicweb.predicates import implements, adaptable
 from cubicweb.utils import json_dumps
 
 class IGeocodableAdapter(EntityAdapter):
--- a/web/views/iprogress.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/iprogress.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -26,7 +26,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import make_uid
-from cubicweb.selectors import adaptable
+from cubicweb.predicates import adaptable
 from cubicweb.schema import display_name
 from cubicweb.view import EntityView
 from cubicweb.web.views.tableview import EntityAttributesTableView
--- a/web/views/isioc.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/isioc.py	Thu Feb 23 19:06:14 2012 +0100
@@ -26,7 +26,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
-from cubicweb.selectors import implements, adaptable
+from cubicweb.predicates import implements, adaptable
 from cubicweb.interfaces import ISiocItem, ISiocContainer
 
 
--- a/web/views/management.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/management.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,8 +21,9 @@
 _ = unicode
 
 from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
 
-from cubicweb.selectors import yes, none_rset, match_user_groups, authenticated_user
+from cubicweb.predicates import none_rset, match_user_groups, authenticated_user
 from cubicweb.view import AnyRsetView, StartupView, EntityView, View
 from cubicweb.uilib import html_traceback, rest_traceback, exc_message
 from cubicweb.web import formwidgets as wdgs
--- a/web/views/massmailing.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/massmailing.py	Thu Feb 23 19:06:14 2012 +0100
@@ -22,7 +22,7 @@
 
 import operator
 
-from cubicweb.selectors import (is_instance, authenticated_user,
+from cubicweb.predicates import (is_instance, authenticated_user,
                                 adaptable, match_form_params)
 from cubicweb.view import EntityView
 from cubicweb.web import (Redirect, stdmsgs, controller, action,
--- a/web/views/navigation.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/navigation.py	Thu Feb 23 19:06:14 2012 +0100
@@ -55,7 +55,7 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import deprecated
 
-from cubicweb.selectors import (paginated_rset, sorted_rset,
+from cubicweb.predicates import (paginated_rset, sorted_rset,
                                 adaptable, implements)
 from cubicweb.uilib import cut
 from cubicweb.view import EntityAdapter, implements_adapter_compat
--- a/web/views/owl.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/owl.py	Thu Feb 23 19:06:14 2012 +0100
@@ -24,7 +24,7 @@
 from logilab.mtconverter import TransformError, xml_escape
 
 from cubicweb.view import StartupView, EntityView
-from cubicweb.selectors import none_rset, match_view
+from cubicweb.predicates import none_rset, match_view
 from cubicweb.web.action import Action
 from cubicweb.web.views import schema
 
--- a/web/views/plots.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/plots.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,14 +22,14 @@
 
 from logilab.common.date import datetime2ticks
 from logilab.common.deprecation import class_deprecated
+from logilab.common.registry import objectify_predicate
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import UStringIO, json_dumps
-from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import multi_columns_rset
+from cubicweb.predicates import multi_columns_rset
 from cubicweb.web.views import baseviews
 
-@objectify_selector
+@objectify_predicate
 def all_columns_are_numbers(cls, req, rset=None, *args, **kwargs):
     """accept result set with at least one line and two columns of result
     all columns after second must be of numerical types"""
@@ -38,14 +38,14 @@
             return 0
     return 1
 
-@objectify_selector
+@objectify_predicate
 def second_column_is_number(cls, req, rset=None, *args, **kwargs):
     etype = rset.description[0][1]
     if etype not  in ('Int', 'BigInt', 'Float'):
         return 0
     return 1
 
-@objectify_selector
+@objectify_predicate
 def columns_are_date_then_numbers(cls, req, rset=None, *args, **kwargs):
     etypes = rset.description[0]
     if etypes[0] not in ('Date', 'Datetime', 'TZDatetime'):
--- a/web/views/primary.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/primary.py	Thu Feb 23 19:06:14 2012 +0100
@@ -47,7 +47,7 @@
 
 from cubicweb import Unauthorized, NoSelectableObject
 from cubicweb.utils import support_args
-from cubicweb.selectors import match_kwargs, match_context
+from cubicweb.predicates import match_kwargs, match_context
 from cubicweb.view import EntityView
 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
 from cubicweb.web import uicfg, component
--- a/web/views/pyviews.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/pyviews.py	Thu Feb 23 19:06:14 2012 +0100
@@ -20,7 +20,7 @@
 __docformat__ = "restructuredtext en"
 
 from cubicweb.view import View
-from cubicweb.selectors import match_kwargs
+from cubicweb.predicates import match_kwargs
 from cubicweb.web.views import tableview
 
 
--- a/web/views/reledit.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/reledit.py	Thu Feb 23 19:06:14 2012 +0100
@@ -29,14 +29,15 @@
 from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.decorators import cached
 
-from cubicweb import neg_role
+from cubicweb import neg_role, typed_eid
 from cubicweb.schema import display_name
-from cubicweb.utils import json_dumps
-from cubicweb.selectors import non_final_entity, match_kwargs
+from cubicweb.utils import json, json_dumps
+from cubicweb.predicates import non_final_entity, match_kwargs
 from cubicweb.view import EntityView
 from cubicweb.web import uicfg, stdmsgs
 from cubicweb.web.form import FieldNotFound
 from cubicweb.web.formwidgets import Button, SubmitButton
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 class _DummyForm(object):
     __slots__ = ('event_args',)
@@ -394,3 +395,18 @@
 
 
 ClickAndEditFormView = class_renamed('ClickAndEditFormView', AutoClickAndEditFormView)
+
+
+@ajaxfunc(output_type='xhtml')
+def reledit_form(self):
+    req = self._cw
+    args = dict((x, req.form[x])
+                for x in ('formid', 'rtype', 'role', 'reload', 'action'))
+    rset = req.eid_rset(typed_eid(self._cw.form['eid']))
+    try:
+        args['reload'] = json.loads(args['reload'])
+    except ValueError: # not true/false, an absolute url
+        assert args['reload'].startswith('http')
+    view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype'])
+    return self._call_view(view, **args)
+
--- a/web/views/schema.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/schema.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -27,12 +27,13 @@
 
 from logilab.common.graph import GraphGenerator, DotBackend
 from logilab.common.ureports import Section, Table
+from logilab.common.registry import yes
 from logilab.mtconverter import xml_escape
 from yams import BASE_TYPES, schema2dot as s2d
 from yams.buildobjs import DEFAULT_ATTRPERMS
 
-from cubicweb.selectors import (is_instance, match_user_groups, match_kwargs,
-                                has_related_entities, authenticated_user, yes)
+from cubicweb.predicates import (is_instance, match_user_groups, match_kwargs,
+                                has_related_entities, authenticated_user)
 from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                              WORKFLOW_TYPES, INTERNAL_TYPES)
 from cubicweb.utils import make_uid
--- a/web/views/startup.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/startup.py	Thu Feb 23 19:06:14 2012 +0100
@@ -29,7 +29,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.view import StartupView
-from cubicweb.selectors import match_user_groups, is_instance
+from cubicweb.predicates import match_user_groups, is_instance
 from cubicweb.schema import display_name
 from cubicweb.web import uicfg, httpcache
 
--- a/web/views/tableview.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/tableview.py	Thu Feb 23 19:06:14 2012 +0100
@@ -70,9 +70,10 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cachedproperty
 from logilab.common.deprecation import class_deprecated
+from logilab.common.registry import yes
 
 from cubicweb import NoSelectableObject, tags
-from cubicweb.selectors import yes, nonempty_rset, match_kwargs, objectify_selector
+from cubicweb.predicates import nonempty_rset, match_kwargs, objectify_predicate
 from cubicweb.schema import display_name
 from cubicweb.utils import make_uid, js_dumps, JSString, UStringIO
 from cubicweb.uilib import toggle_action, limitsize, htmlescape, sgml_attributes, domid
@@ -82,7 +83,7 @@
                                       PopupBoxMenu)
 
 
-@objectify_selector
+@objectify_predicate
 def unreloadable_table(cls, req, rset=None,
                        displaycols=None, headers=None, cellvids=None,
                        paginate=False, displayactions=False, displayfilter=False,
@@ -458,12 +459,9 @@
     # layout callbacks #########################################################
 
     def facets_form(self, **kwargs):# XXX extracted from jqplot cube
-        try:
-            return self._cw.vreg['views'].select(
-                'facet.filtertable', self._cw, rset=self.cw_rset, view=self,
-                **kwargs)
-        except NoSelectableObject:
-            return None
+        return self._cw.vreg['views'].select_or_none(
+            'facet.filtertable', self._cw, rset=self.cw_rset, view=self,
+            **kwargs)
 
     @cachedproperty
     def domid(self):
@@ -863,7 +861,7 @@
 
 class EntityTableView(TableMixIn, EntityView):
     """This abstract table view is designed to be used with an
-    :class:`is_instance()` or :class:`adaptable` selector, hence doesn't depend
+    :class:`is_instance()` or :class:`adaptable` predicate, hence doesn't depend
     the result set shape as the :class:`TableView` does.
 
     It will display columns that should be defined using the `columns` class
--- a/web/views/tabs.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/tabs.py	Thu Feb 23 19:06:14 2012 +0100
@@ -25,7 +25,7 @@
 
 from cubicweb import NoSelectableObject, role
 from cubicweb import tags, uilib, utils
-from cubicweb.selectors import partial_has_related_entities
+from cubicweb.predicates import partial_has_related_entities
 from cubicweb.view import EntityView
 from cubicweb.web.views import primary
 
@@ -52,7 +52,7 @@
         if rql:
             urlparams['rql'] = rql
         elif eid:
-            urlparams['rql'] = uilib.rql_for_eid(eid)
+            urlparams['eid'] = eid
         elif rset:
             urlparams['rql'] = rset.printable_rql()
         if tabid is None:
--- a/web/views/timeline.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/timeline.py	Thu Feb 23 19:06:14 2012 +0100
@@ -25,7 +25,7 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.date import ustrftime
 
-from cubicweb.selectors import adaptable
+from cubicweb.predicates import adaptable
 from cubicweb.view import EntityView, StartupView
 from cubicweb.utils import json_dumps
 
--- a/web/views/timetable.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/timetable.py	Thu Feb 23 19:06:14 2012 +0100
@@ -23,7 +23,7 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.date import ONEDAY, date_range, todatetime
 
-from cubicweb.selectors import adaptable
+from cubicweb.predicates import adaptable
 from cubicweb.view import EntityView
 
 
--- a/web/views/treeview.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/treeview.py	Thu Feb 23 19:06:14 2012 +0100
@@ -27,10 +27,11 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import make_uid, json
-from cubicweb.selectors import adaptable
+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)
@@ -280,3 +281,20 @@
                        treeid=treeid, initial_load=False, **morekwargs)
         w(u'</li>')
 
+
+
+@ajaxfunc
+def node_clicked(self, treeid, nodeeid):
+    """add/remove eid in treestate cookie"""
+    cookies = self._cw.get_cookie()
+    statename = treecookiename(treeid)
+    treestate = cookies.get(statename)
+    if treestate is None:
+        self._cw.set_cookie(statename, nodeeid)
+    else:
+        marked = set(filter(None, treestate.value.split(':')))
+        if nodeeid in marked:
+            marked.remove(nodeeid)
+        else:
+            marked.add(nodeeid)
+        self._cw.set_cookie(statename, ':'.join(marked))
--- a/web/views/vcard.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/vcard.py	Thu Feb 23 19:06:14 2012 +0100
@@ -20,7 +20,7 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.view import EntityView
 
 _ = unicode
--- a/web/views/wdoc.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/wdoc.py	Thu Feb 23 19:06:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -27,9 +27,10 @@
 
 from logilab.common.changelog import ChangeLog
 from logilab.common.date import strptime, todate
+from logilab.common.registry import yes
 from logilab.mtconverter import CHARSET_DECL_RGX
 
-from cubicweb.selectors import match_form_params, yes
+from cubicweb.predicates import match_form_params
 from cubicweb.view import StartupView
 from cubicweb.uilib import rest_publish
 from cubicweb.web import NotFound, action
--- a/web/views/workflow.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/workflow.py	Thu Feb 23 19:06:14 2012 +0100
@@ -31,7 +31,7 @@
 from logilab.common.graph import escape
 
 from cubicweb import Unauthorized
-from cubicweb.selectors import (has_related_entities, one_line_rset,
+from cubicweb.predicates import (has_related_entities, one_line_rset,
                                 relation_possible, match_form_params,
                                 score_entity, is_instance, adaptable)
 from cubicweb.view import EntityView
--- a/web/views/xbel.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/xbel.py	Thu Feb 23 19:06:14 2012 +0100
@@ -22,7 +22,7 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
 from cubicweb.view import EntityView
 from cubicweb.web.views.xmlrss import XMLView
 
--- a/web/views/xmlrss.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/views/xmlrss.py	Thu Feb 23 19:06:14 2012 +0100
@@ -25,7 +25,7 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import (is_instance, non_final_entity, one_line_rset,
+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
--- a/web/webconfig.py	Thu Feb 23 19:05:36 2012 +0100
+++ b/web/webconfig.py	Thu Feb 23 19:06:14 2012 +0100
@@ -321,17 +321,20 @@
         if not (self.repairing or self.creating):
             self.global_set_option('base-url', baseurl)
         httpsurl = self['https-url']
-        if (self.debugmode or self.mode == 'test'):
-            datadir_path = 'data/'
-        else:
-            datadir_path = 'data/%s/' % self.instance_md5_version()
+        data_relpath = self.data_relpath()
         if httpsurl:
             if httpsurl[-1] != '/':
                 httpsurl += '/'
                 if not self.repairing:
                     self.global_set_option('https-url', httpsurl)
-            self.https_datadir_url = httpsurl + datadir_path
-        self.datadir_url = baseurl + datadir_path
+            self.https_datadir_url = httpsurl + data_relpath
+        self.datadir_url = baseurl + data_relpath
+
+    def data_relpath(self):
+        if (self.debugmode or self.mode == 'test'):
+            return 'data/'
+        else:
+            return 'data/%s/' % self.instance_md5_version()
 
     def _build_ui_properties(self):
         # self.datadir_url[:-1] to remove trailing /