3.15 is the new old stable oldstable
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Thu, 21 Mar 2013 18:13:31 +0100
brancholdstable
changeset 8746 88c71ad83d47
parent 8507 0c111b232927 (current diff)
parent 8742 bd374bd906f3 (diff)
child 8818 d8b0984c923c
3.15 is the new old stable
selectors.py
skeleton/debian/DISTNAME.prerm.tmpl
sobjects/parsers.py
sobjects/test/unittest_parsers.py
test/unittest_selectors.py
--- a/.hgtags	Wed Aug 01 10:30:48 2012 +0200
+++ b/.hgtags	Thu Mar 21 18:13:31 2013 +0100
@@ -250,9 +250,31 @@
 55fc796ed5d5f31245ae60bd148c9e42657a1af6 cubicweb-debian-version-3.14.5-1
 db021578232b885dc5e55dfca045332ce01e7f35 cubicweb-version-3.14.6
 75364c0994907764715bd5011f6a59d934dbeb7d cubicweb-debian-version-3.14.6-1
+0642b2d03acaa5e065cae7590e82b388a280ca22 cubicweb-version-3.15.0
+925db25a3250c5090cf640fc2b02bde5818b9798 cubicweb-debian-version-3.15.0-1
 3ba3ee5b3a89a54d1dc12ed41d5c12232eda1952 cubicweb-version-3.14.7
 20ee573bd2379a00f29ff27bb88a8a3344d4cdfe cubicweb-debian-version-3.14.7-1
 15fe07ff687238f8cc09d8e563a72981484085b3 cubicweb-version-3.14.8
 81394043ad226942ac0019b8e1d4f7058d67a49f cubicweb-debian-version-3.14.8-1
 9337812cef6b949eee89161190e0c3d68d7f32ea cubicweb-version-3.14.9
 68c762adf2d5a2c338910ef1091df554370586f0 cubicweb-debian-version-3.14.9-1
+783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1
+fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1
+2afc157ea9b2b92eccb0f2d704094e22ce8b5a05 cubicweb-version-3.15.2
+9aa5553b26520ceb68539e7a32721b5cd5393e16 cubicweb-debian-version-3.15.2-1
+0e012eb80990ca6f91aa9a8ad3324fbcf51435b1 cubicweb-version-3.15.3
+7ad423a5b6a883dbdf00e6c87a5f8ab121041640 cubicweb-debian-version-3.15.3-1
+63260486de89a9dc32128cd0eacef891a668977b cubicweb-version-3.15.4
+70cb36c826df86de465f9b69647cef7096dcf12c cubicweb-debian-version-3.15.4-1
+b0e086f451b7213fe63141438edc91a6b2da9072 cubicweb-version-3.15.5
+19e115ae5442c427c0adbda8b9d8ceccf2931b5c cubicweb-debian-version-3.15.5-1
+0163bd9f4880d5531e433c1500f9298a0adef6b7 cubicweb-version-3.15.6
+b05e156b8fe720494293b08e7060ba43ad57a5c8 cubicweb-debian-version-3.15.6-1
+d8916cee7b705fec66fa2797ab89ba3e3b617ced cubicweb-version-3.15.7
+c5400558f37079a8bf6f2cd27a1ffd49321f3d8b cubicweb-debian-version-3.15.7-1
+459d0c48dfafee903c15a5349d321f6e8f998cbb cubicweb-version-3.15.8
+4ef457479337396f63bf00c87cedcbb7cb5a6eee cubicweb-debian-version-3.15.8-1
+8bfc0753f1daa37a6a268287dd2848931fca1f95 cubicweb-version-3.15.9
+29fbc632a69667840294d7b38b0ca00e5f66ec19 cubicweb-debian-version-3.15.9-1
+89bdb5444cd20213d5af03c2612ceb28340cb760 cubicweb-version-3.15.10
+feca12e4a6188fbaae0cc48c6f8cc5f4202e1662 cubicweb-debian-version-3.15.10-1
--- a/__init__.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/__init__.py	Thu Mar 21 18:13:31 2013 +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.
@@ -41,6 +41,7 @@
 from StringIO import StringIO
 
 from logilab.common.logging_ext import set_log_methods
+from yams.constraints import BASE_CONVERTERS
 
 
 if os.environ.get('APYCOT_ROOT'):
@@ -55,6 +56,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 +79,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
@@ -120,6 +121,13 @@
         binary.seek(0)
         return binary
 
+def str_or_binary(value):
+    if isinstance(value, Binary):
+        return value
+    return str(value)
+BASE_CONVERTERS['Password'] = str_or_binary
+
+
 
 # use this dictionary to rename entity types while keeping bw compat
 ETYPE_NAME_MAP = {}
--- a/__pkginfo__.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/__pkginfo__.py	Thu Mar 21 18:13:31 2013 +0100
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 14, 9)
+numversion = (3, 15, 10)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -40,11 +40,10 @@
 ]
 
 __depends__ = {
-    'logilab-common': '>= 0.57.0',
+    'logilab-common': '>= 0.58.0',
     'logilab-mtconverter': '>= 0.8.0',
-    'rql': '>= 0.29.0',
+    'rql': '>= 0.31.2',
     'yams': '>= 0.34.0',
-    'docutils': '>= 0.6',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependancies
     'simplejson': '>= 2.0.9',
@@ -52,18 +51,20 @@
     'Twisted': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '>= 1.8.1',
+    'logilab-database': '>= 1.8.2',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
     'passlib': '',
     }
 
 __recommends__ = {
+    'docutils': '>= 0.6',
     'Pyro': '>= 3.9.1, < 4.0.0',
     'PIL': '',                  # for captcha
     'pycrypto': '',             # for crypto extensions
     '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	Wed Aug 01 10:30:48 2012 +0200
+++ b/_exceptions.py	Thu Mar 21 18:13:31 2013 +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
@@ -32,9 +30,10 @@
         if self.msg:
             if self.args:
                 return self.msg % tuple(self.args)
-            return self.msg
-        return ' '.join(unicode(arg) for arg in self.args)
-
+            else:
+                return self.msg
+        else:
+            return u' '.join(unicode(arg) for arg in self.args)
 
 class ConfigurationError(CubicWebException):
     """a misconfiguration error"""
@@ -83,6 +82,7 @@
 class UniqueTogetherError(RepositoryError):
     """raised when a unique_together constraint caused an IntegrityError"""
 
+
 # security exceptions #########################################################
 
 class Unauthorized(SecurityError):
@@ -103,6 +103,10 @@
         except Exception, ex:
             return str(ex)
 
+class Forbidden(SecurityError):
+    """raised when a user tries to perform a forbidden action
+    """
+
 # source exceptions ###########################################################
 
 class EidNotInSource(SourceException):
@@ -114,32 +118,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"""
@@ -154,6 +134,35 @@
     a non final entity
     """
 
+class UndoTransactionException(QueryError):
+    """Raised when undoing a transaction could not be performed completely.
+
+    Note that :
+      1) the partial undo operation might be acceptable
+         depending upon the final application
+
+      2) the undo operation can also fail with a `ValidationError` in
+         cases where the undoing breaks integrity constraints checked
+         immediately.
+
+      3) It might be that neither of those exception is raised but a
+         subsequent `commit` might raise a `ValidationError` in cases
+         where the undoing breaks integrity constraints checked at
+         commit time.
+
+    :type txuuix: int
+    :param txuuid: Unique identifier of the partialy undone transaction
+
+    :type errors: list
+    :param errors: List of errors occured during undoing
+    """
+    msg = u"The following error(s) occured while undoing transaction #%d : %s"
+
+    def __init__(self, txuuid, errors):
+        super(UndoTransactionException, self).__init__(txuuid, errors)
+        self.txuuid = txuuid
+        self.errors = errors
+
 # tools exceptions ############################################################
 
 class ExecutionError(Exception):
@@ -161,3 +170,4 @@
 
 # pylint: disable=W0611
 from logilab.common.clcommands import BadCommandUsage
+
--- a/appobject.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/appobject.py	Thu Mar 21 18:13:31 2013 +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/cwconfig.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/cwconfig.py	Thu Mar 21 18:13:31 2013 +0100
@@ -171,6 +171,7 @@
 
 import sys
 import os
+import stat
 import logging
 import logging.config
 from smtplib import SMTP
@@ -306,7 +307,10 @@
 _forced_mode = os.environ.get('CW_MODE')
 assert _forced_mode in (None, 'system', 'user')
 
-CWDEV = exists(join(CW_SOFTWARE_ROOT, '.hg'))
+# CWDEV tells whether directories such as i18n/, web/data/, etc. (ie containing
+# some other resources than python libraries) are located with the python code
+# or as a 'shared' cube
+CWDEV = exists(join(CW_SOFTWARE_ROOT, 'i18n'))
 
 try:
     _INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
@@ -386,14 +390,6 @@
           'help': 'allow users to login with their primary email if set',
           'group': 'main', 'level': 2,
           }),
-        ('use-request-subdomain',
-         {'type' : 'yn',
-          'default': None,
-          'help': ('if set, base-url subdomain is replaced by the request\'s '
-                   'host, to help managing sites with several subdomains in a '
-                   'single cubicweb instance'),
-          'group': 'main', 'level': 1,
-          }),
         ('mangle-emails',
          {'type' : 'yn',
           'default': False,
@@ -828,7 +824,7 @@
     _cubes = None
 
     def init_cubes(self, cubes):
-        assert self._cubes is None, self._cubes
+        assert self._cubes is None, repr(self._cubes)
         self._cubes = self.reorder_cubes(cubes)
         # load cubes'__init__.py file first
         for cube in cubes:
@@ -1081,7 +1077,12 @@
         If not, try to fix this, letting exception propagate when not possible.
         """
         if not exists(path):
-            os.makedirs(path)
+            self.info('creating %s directory', path)
+            try:
+                os.makedirs(path)
+            except OSError, ex:
+                self.warning('error while creating %s directory: %s', path, ex)
+                return
         if self['uid']:
             try:
                 uid = int(self['uid'])
@@ -1095,10 +1096,20 @@
                 return
         fstat = os.stat(path)
         if fstat.st_uid != uid:
-            os.chown(path, uid, os.getgid())
-        import stat
+            self.info('giving ownership of %s directory to %s', path, self['uid'])
+            try:
+                os.chown(path, uid, os.getgid())
+            except OSError, ex:
+                self.warning('error while giving ownership of %s directory to %s: %s',
+                             path, self['uid'], ex)
         if not (fstat.st_mode & stat.S_IWUSR):
-            os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+            self.info('forcing write permission on directory %s', path)
+            try:
+                os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+            except OSError, ex:
+                self.warning('error while forcing write permission on directory %s: %s',
+                             path, ex)
+                return
 
     @cached
     def instance_md5_version(self):
@@ -1160,8 +1171,11 @@
                 tr = translation('cubicweb', path, languages=[language])
                 self.translations[language] = (tr.ugettext, tr.upgettext)
             except (ImportError, AttributeError, IOError):
-                self.exception('localisation support error for language %s',
-                               language)
+                if self.mode != 'test':
+                    # in test contexts, data/i18n does not exist, hence
+                    # logging will only pollute the logs
+                    self.exception('localisation support error for language %s',
+                                   language)
 
     def vregistry_path(self):
         """return a list of files or directories where the registry will look
--- a/cwctl.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/cwctl.py	Thu Mar 21 18:13:31 2013 +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,6 +38,8 @@
 
 from os.path import exists, join, isfile, isdir, dirname, abspath
 
+from urlparse import urlparse
+
 from logilab.common.clcommands import CommandLine
 from logilab.common.shellutils import ASK
 
@@ -828,10 +830,7 @@
     in batch mode.
 
     By default it will connect to a local instance using an in memory
-    connection, unless -P option is specified, in which case you will be
-    connected through pyro. In the later case, you won't have access to
-    repository internals (session, etc...) so most migration commands won't be
-    available.
+    connection, unless an URL to a running instance is specified.
 
     Arguments after bare "--" string will not be processed by the shell command
     You can use it to pass extra arguments to your script and expect for
@@ -867,31 +866,45 @@
           'group': 'local'
           }),
 
-        ('pyro',
-         {'short': 'P', 'action' : 'store_true',
-          'help': 'connect to a running instance through Pyro.',
-          'group': 'remote',
-          }),
-        ('pyro-ns-host',
-         {'short': 'H', 'type' : 'string', 'metavar': '<host[:port]>',
-          'help': 'Pyro name server host. If not set, will be detected by '
-          'using a broadcast query.',
+        ('repo-uri',
+         {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
+          'help': 'URI of the CubicWeb repository to connect to. URI can be \
+pyro://[host:port] the Pyro name server host; if the pyro nameserver is not set, \
+it will be detected by using a broadcast query, a ZMQ URL or \
+inmemory:// (default) use an in-memory repository.',
           'group': 'remote'
           }),
         )
 
     def run(self, args):
         appid = args.pop(0)
-        if self.config.pyro:
+        if self.config.repo_uri:
+            uri = urlparse(self.config.repo_uri)
+            if uri.scheme == 'pyro':
+                cnxtype = uri.scheme
+                hostport = uri.netloc
+            elif uri.scheme == 'inmemory':
+                cnxtype = ''
+                hostport = ''
+            else:
+                cnxtype = 'zmq'
+                hostport = self.config.repo_uri
+        else:
+            cnxtype = ''
+
+        if cnxtype:
             from cubicweb import AuthenticationError
-            from cubicweb.dbapi import connect
+            from cubicweb.dbapi import connect, ConnectionProperties
             from cubicweb.server.utils import manager_userpasswd
             from cubicweb.server.migractions import ServerMigrationHelper
+            cnxprops = ConnectionProperties(cnxtype=cnxtype)
+
             while True:
                 try:
                     login, pwd = manager_userpasswd(msg=None)
                     cnx = connect(appid, login=login, password=pwd,
-                                  host=self.config.pyro_ns_host, mulcnx=False)
+                                  host=hostport, mulcnx=False,
+                                  cnxprops=cnxprops)
                 except AuthenticationError, ex:
                     print ex
                 except (KeyboardInterrupt, EOFError):
@@ -901,7 +914,7 @@
                     break
             cnx.load_appobjects()
             repo = cnx._repo
-            mih = ServerMigrationHelper(None, repo=repo, cnx=cnx,
+            mih = ServerMigrationHelper(None, repo=repo, cnx=cnx, verbosity=0,
                                          # hack so it don't try to load fs schema
                                         schema=1)
         else:
@@ -927,7 +940,7 @@
             else:
                 mih.interactive_shell()
         finally:
-            if not self.config.pyro:
+            if not cnxtype: # shutdown in-memory repo
                 mih.shutdown()
             else:
                 cnx.close()
--- a/cwvreg.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/cwvreg.py	Thu Mar 21 18:13:31 2013 +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,7 +256,11 @@
                       key=lambda x: x.cw_propval('order'))
 
 
-VRegistry.REGISTRY_FACTORY[None] = CWRegistry
+def related_appobject(obj, appobjectattr='__appobject__'):
+    """ adapts any object to a potential appobject bound to it
+    through the __appobject__ attribute
+    """
+    return getattr(obj, appobjectattr, obj)
 
 
 class ETypeRegistry(CWRegistry):
@@ -261,8 +268,7 @@
     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 +278,8 @@
         self.clear_caches()
 
     def register(self, obj, **kwargs):
-        oid = kwargs.get('oid') or class_regid(obj)
+        obj = related_appobject(obj)
+        oid = kwargs.get('oid') or obj.__regid__
         if oid != 'Any' and not oid in self.schema:
             self.error('don\'t register %s, %s type not defined in the '
                        'schema', obj, oid)
@@ -354,8 +361,6 @@
             fetchattrs_list.append(set(etypecls.fetch_attrs))
         return reduce(set.intersection, fetchattrs_list)
 
-VRegistry.REGISTRY_FACTORY['etypes'] = ETypeRegistry
-
 
 class ViewsRegistry(CWRegistry):
 
@@ -389,8 +394,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 +411,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 +446,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 +461,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 +492,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 +533,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):
@@ -526,9 +544,23 @@
     def itervalues(self):
         return (value for key, value in self.items())
 
+    def load_module(self, module):
+        """ variation from the base implementation:
+        apply related_appobject to the automatically registered objects
+        """
+        self.info('loading %s from %s', module.__name__, module.__file__)
+        if hasattr(module, 'registration_callback'):
+            module.registration_callback(self)
+            return
+        for objname, obj in vars(module).iteritems():
+            if objname.startswith('_'):
+                continue
+            self._load_ancestors_then_object(module.__name__,
+                                             related_appobject(obj))
+
     def reset(self):
         CW_EVENT_MANAGER.emit('before-registry-reset', self)
-        super(CubicWebVRegistry, self).reset()
+        super(CWRegistryStore, self).reset()
         self._needs_iface = {}
         self._needs_appobject = {}
         # two special registries, propertydefs which care all the property
@@ -541,6 +573,17 @@
                 self.register_property(key, **propdef)
         CW_EVENT_MANAGER.emit('after-registry-reset', self)
 
+    def register_all(self, objects, modname, butclasses=()):
+        butclasses = set(related_appobject(obj)
+                         for obj in butclasses)
+        objects = [related_appobject(obj) for obj in objects]
+        super(CWRegistryStore, self).register_all(objects, modname, butclasses)
+
+    def register_and_replace(self, obj, replaced):
+        obj = related_appobject(obj)
+        replaced = related_appobject(replaced)
+        super(CWRegistryStore, self).register_and_replace(obj, replaced)
+
     def set_schema(self, schema):
         """set instance'schema and load application objects"""
         self._set_schema(schema)
@@ -597,7 +640,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 +656,8 @@
         If `clear` is true, all objects with the same identifier will be
         previously unregistered.
         """
-        super(CubicWebVRegistry, self).register(obj, *args, **kwargs)
+        obj = related_appobject(obj)
+        super(CWRegistryStore, self).register(obj, *args, **kwargs)
         # XXX bw compat
         ifaces = use_interfaces(obj)
         if ifaces:
@@ -630,7 +674,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 +729,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/dataimport.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/dataimport.py	Thu Mar 21 18:13:31 2013 +0100
@@ -64,6 +64,8 @@
 .. BUG file with one column are not parsable
 .. TODO rollback() invocation is not possible yet
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import sys
--- a/dbapi.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/dbapi.py	Thu Mar 21 18:13:31 2013 +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
 
 
@@ -93,14 +93,18 @@
     Only 'in-memory' and 'pyro' are supported for now. Either vreg or config
     argument should be given
     """
-    assert method in ('pyro', 'inmemory')
+    assert method in ('pyro', 'inmemory', 'zmq')
     assert vreg or config
     if vreg and not config:
         config = vreg.config
     if method == 'inmemory':
         # get local access to the repository
         from cubicweb.server.repository import Repository
-        return Repository(config, vreg=vreg)
+        from cubicweb.server.utils import TasksManager
+        return Repository(config, TasksManager(), vreg=vreg)
+    elif method == 'zmq':
+        from cubicweb.zmqclient import ZMQRepositoryClient
+        return ZMQRepositoryClient(database)
     else: # method == 'pyro'
         # resolve the Pyro object
         from logilab.common.pyro_ext import ns_get_proxy, get_proxy
@@ -145,8 +149,8 @@
       the user login to use to authenticate.
 
     :host:
-      the pyro nameserver host. Will be detected using broadcast query if
-      unspecified.
+      - pyro: nameserver host. Will be detected using broadcast query if unspecified
+      - zmq: repository host socket address
 
     :group:
       the instance's pyro nameserver group. You don't have to specify it unless
@@ -183,6 +187,8 @@
             config.global_set_option('pyro-ns-host', host)
         if group:
             config.global_set_option('pyro-ns-group', group)
+    elif method == 'zmq':
+        config = cwconfig.CubicWebNoAppConfiguration()
     else:
         assert database
         config = cwconfig.instance_configuration(database)
@@ -192,7 +198,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 +213,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:
@@ -280,13 +286,16 @@
 
     def __init__(self, vreg, session=None):
         super(DBAPIRequest, self).__init__(vreg)
+        #: 'language' => translation_function() mapping
         try:
             # no vreg or config which doesn't handle translations
             self.translations = vreg.config.translations
         except AttributeError:
             self.translations = {}
+        #: Request language identifier eg: 'en'
+        self.lang = None
         self.set_default_language(vreg)
-        # cache entities built during the request
+        #: cache entities built during the request
         self._eid_cache = {}
         if session is not None:
             self.set_session(session)
@@ -299,6 +308,9 @@
     def from_controller(self):
         return 'view'
 
+    def get_option_value(self, option, foreid=None):
+        return self.cnx.get_option_value(option, foreid)
+
     def set_session(self, session, user=None):
         """method called by the session handler when the user is authenticated
         or an anonymous connection is open
@@ -335,6 +347,11 @@
             self.pgettext = lambda x, y: unicode(y)
         self.debug('request default language: %s', self.lang)
 
+    # server-side service call #################################################
+
+    def call_service(self, regid, async=False, **kwargs):
+        return self.cnx.call_service(regid, async, **kwargs)
+
     # entities cache management ###############################################
 
     def entity_cache(self, eid):
@@ -556,6 +573,12 @@
             except Exception:
                 pass
 
+    # server-side service call #################################################
+
+    @check_not_closed
+    def call_service(self, regid, async=False, **kwargs):
+        return self._repo.call_service(self.sessionid, regid, async, **kwargs)
+
     # connection initialization methods ########################################
 
     def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
@@ -577,7 +600,12 @@
             esubpath = list(subpath)
             esubpath.remove('views')
             esubpath.append(join('web', 'views'))
+        # first load available configs, necessary for proper persistent
+        # properties initialization
+        config.load_available_configs()
+        # then init cubes
         config.init_cubes(cubes)
+        # then load appobjects into the registry
         vpath = config.build_vregistry_path(reversed(config.cubes_path()),
                                             evobjpath=esubpath,
                                             tvobjpath=subpath)
--- a/debian/changelog	Wed Aug 01 10:30:48 2012 +0200
+++ b/debian/changelog	Thu Mar 21 18:13:31 2013 +0100
@@ -1,3 +1,73 @@
+cubicweb (3.15.10-1) squeeze; urgency=low
+
+  * New upstream release
+
+ -- Aurélien Campéas <aurelien.campeas@logilab.fr>  Tue, 19 Mar 2013 16:56:00 +0100
+
+cubicweb (3.15.9-1) squeeze; urgency=low
+
+  * New upstream release
+  * Don't compress txt files.  They're used by the doc's search functionality,
+    and the javascript gets confused if it receives gzip instead of text.
+  * Work around broken combination of jquery 1.4 and sphinx 0.6 in squeeze by
+    patching up doctools.js.
+
+ -- David Douard <david.douard@logilab.fr>  Fri, 25 Jan 2013 17:49:58 +0100
+
+cubicweb (3.15.8-1) squeeze; urgency=low
+
+  * New upstream release
+
+ -- Aurélien Campéas <aurelien.campeas@logilab.fr>  Wed, 09 Jan 2013 15:40:00 +0100
+
+cubicweb (3.15.7-1) squeeze; urgency=low
+
+  * New upstream release
+
+ -- David Douard <david.douard@logilab.fr>  Wed, 12 Dec 2012 22:10:45 +0100
+
+cubicweb (3.15.6-1) squeeze; urgency=low
+
+  * New upstream release
+
+ -- David Douard <david.douard@logilab.fr>  Fri, 30 Nov 2012 19:25:20 +0100
+
+cubicweb (3.15.5-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Aurélien Campéas <aurelien.campeas@logilab.fr>  Wed, 24 Oct 2012 12:07:00 +0200
+
+cubicweb (3.15.4-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Julien Cristau <jcristau@debian.org>  Fri, 31 Aug 2012 16:43:11 +0200
+
+cubicweb (3.15.3-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr>  Tue, 21 Aug 2012 14:19:31 +0200
+
+cubicweb (3.15.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 20 Jul 2012 15:17:17 +0200
+
+cubicweb (3.15.1-1) quantal; urgency=low
+
+  * new upstream release
+
+ -- David Douard <david.douard@logilab.fr>  Mon, 11 Jun 2012 09:45:24 +0200
+
+cubicweb (3.15.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 12 Apr 2012 13:52:05 +0200
+
 cubicweb (3.14.9-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Wed Aug 01 10:30:48 2012 +0200
+++ b/debian/control	Thu Mar 21 18:13:31 2013 +0100
@@ -7,18 +7,25 @@
            Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
            Aurélien Campéas <aurelien.campeas@logilab.fr>,
            Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 7), python (>= 2.5), python-central (>= 0.5), python-sphinx, python-logilab-common, python-unittest2
-# for the documentation:
-# python-sphinx, python-logilab-common, python-unittest2, logilab-doctools, logilab-xml
+Build-Depends:
+ debhelper (>= 7),
+ python (>= 2.5),
+ python-central (>= 0.5),
+ python-sphinx,
+ python-logilab-common,
+ python-unittest2,
+ python-logilab-mtconverter,
+ python-rql,
+ python-yams,
+ python-lxml,
 Standards-Version: 3.9.1
 Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.5, << 3.0
+XS-Python-Version: >= 2.5
 
 Package: cubicweb
 Architecture: all
 XB-Python-Version: ${python:Versions}
 Depends: ${misc:Depends}, ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython) | mysql | sqlite3
 Recommends: postgresql | mysql | sqlite3
 Description: the complete CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -35,8 +42,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, python-passlib
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, python-passlib
 Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
+Suggests: python-zmq
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -84,8 +92,8 @@
 Package: cubicweb-web
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3)
-Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-imaging, python-rdflib
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 2.0.9)
+Recommends: python-docutils (>= 0.6), python-vobject, fckeditor, python-fyzz, python-imaging, python-rdflib
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -99,7 +107,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.57.0), python-yams (>= 0.34.0), python-rql (>= 0.29.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.31.2), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/debian/rules	Wed Aug 01 10:30:48 2012 +0200
+++ b/debian/rules	Thu Mar 21 18:13:31 2013 +0100
@@ -11,10 +11,19 @@
 build-stamp:
 	dh_testdir
 	NO_SETUPTOOLS=1 python setup.py build
+	# cubicweb.foo needs to be importable by sphinx, so create a cubicweb symlink to the source dir
+	mkdir -p debian/pythonpath
+	ln -sf $(CURDIR) debian/pythonpath/cubicweb
 	# documentation build is now made optional since it can break for old
 	# distributions and we don't want to block a new release of Cubicweb
 	# because of documentation issues.
-	-PYTHONPATH=$(CURDIR)/.. $(MAKE) -C doc/book/en all
+	-PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all
+	# squeeze has a broken combination of jquery and sphinx, fix it up so search works(ish)
+	if grep -q jQuery\\.className doc/html/_static/doctools.js && grep -q "jQuery JavaScript Library v1\.4\." doc/html/_static/jquery.js; then \
+	    echo 'Patching doctools.js for jQuery 1.4 compat'; \
+	    sed -i 's/jQuery\.className.has(node\.parentNode, className)/jQuery(node.parentNode).hasClass(className)/' doc/html/_static/doctools.js; \
+	fi
+	rm -rf debian/pythonpath
 	touch build-stamp
 
 clean:
@@ -70,7 +79,7 @@
 	dh_installman -i
 	dh_installchangelogs -i
 	dh_link -i
-	dh_compress -i -X.py -X.ini -X.xml -X.js -X.rst
+	dh_compress -i -X.py -X.ini -X.xml -X.js -X.rst -X.txt
 	dh_fixperms -i
 	dh_installdeb -i
 	dh_gencontrol  -i
--- a/devtools/__init__.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/devtools/__init__.py	Thu Mar 21 18:13:31 2013 +0100
@@ -168,7 +168,7 @@
     def load_configuration(self):
         super(TestServerConfiguration, self).load_configuration()
         # no undo support in tests
-        self.global_set_option('undo-support', '')
+        self.global_set_option('undo-enabled', 'n')
 
     def main_config_file(self):
         """return instance's control configuration file"""
@@ -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/cwwindmill.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/devtools/cwwindmill.py	Thu Mar 21 18:13:31 2013 +0100
@@ -45,7 +45,7 @@
         tags = CubicWebServerTC.tags & Tags(('windmill',))
 
         def testWindmill(self):
-            self.skipTest("can't import windmill %s" % ex)
+            self.skipTest("can't import windmill")
 else:
     # Excerpt from :ref:`windmill.authoring.unit`
     class UnitTestReporter(functest.reports.FunctestReportInterface):
--- a/devtools/devctl.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/devtools/devctl.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/devtools/fake.py	Thu Mar 21 18:13:31 2013 +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
 
@@ -33,7 +33,9 @@
 class FakeConfig(dict, BaseApptestConfiguration):
     translations = {}
     uiprops = {}
+    https_uiprops = {}
     apphome = None
+    debugmode = False
     def __init__(self, appid='data', apphome=None, cubes=()):
         self.appid = appid
         self.apphome = apphome
@@ -43,6 +45,7 @@
         self['base-url'] = BASE_URL
         self['rql-cache-size'] = 3000
         self.datadir_url = BASE_URL + 'data/'
+        self.https_datadir_url = (BASE_URL + 'data/').replace('http://', 'https://')
 
     def cubes(self, expand=False):
         return self._cubes
@@ -56,12 +59,12 @@
 
     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._http_method = kwargs.pop('method', 'GET')
         self._url = kwargs.pop('url', None) or 'view?rql=Blop&vid=blop'
         super(FakeRequest, self).__init__(*args, **kwargs)
         self._session_data = {}
-        self._headers_in = Headers()
 
     def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
         super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure)
@@ -73,8 +76,8 @@
         """returns an ordered list of preferred languages"""
         return ('en',)
 
-    def header_if_modified_since(self):
-        return None
+    def http_method(self):
+        return self._http_method
 
     def relative_path(self, includeparams=True):
         """return the normalized path of the request (ie at least relative
@@ -89,35 +92,23 @@
             return url
         return url.split('?', 1)[0]
 
-    def get_header(self, header, default=None, raw=True):
-        """return the value associated with the given input header, raise
-        KeyError if the header is not set
-        """
-        if raw:
-            return self._headers_in.getRawHeaders(header, [default])[0]
-        return self._headers_in.getHeader(header, default)
-
-    ## extend request API to control headers in / out values
     def set_request_header(self, header, value, raw=False):
-        """set an input HTTP header"""
+        """set an incoming HTTP header (For test purpose only)"""
         if isinstance(value, basestring):
             value = [value]
-        if raw:
+        if raw: #
+            # adding encoded header is important, else page content
+            # will be reconverted back to unicode and apart unefficiency, this
+            # may cause decoding problem (e.g. when downloading a file)
             self._headers_in.setRawHeaders(header, value)
-        else:
-            self._headers_in.setHeader(header, value)
+        else: #
+            self._headers_in.setHeader(header, value) #
 
     def get_response_header(self, header, default=None, raw=False):
-        """return the value associated with the given input header,
-        raise KeyError if the header is not set
-        """
-        if raw:
-            return self.headers_out.getRawHeaders(header, default)[0]
-        else:
-            return self.headers_out.getHeader(header, default)
-
-    def validate_cache(self):
-        pass
+        """return output header (For test purpose only"""
+        if raw: #
+            return self.headers_out.getRawHeaders(header, [default])[0]
+        return self.headers_out.getHeader(header, default)
 
     def build_url_params(self, **kwargs):
         # overriden to get predictable resultts
@@ -144,7 +135,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 +170,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/devtools/test/data/views.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/devtools/testlib.py	Thu Mar 21 18:13:31 2013 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -45,6 +45,7 @@
 
 from cubicweb import ValidationError, NoSelectableObject, AuthenticationError
 from cubicweb import cwconfig, dbapi, devtools, web, server
+from cubicweb.utils import json
 from cubicweb.sobjects import notification
 from cubicweb.web import Redirect, application
 from cubicweb.server.session import Session, security_enabled
@@ -85,6 +86,11 @@
         protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES)
     return set(schema.entities()) - protected_entities
 
+class JsonValidator(object):
+    def parse_string(self, data):
+        json.loads(data)
+        return data
+
 # email handling, to test emails sent by an application ########################
 
 MAILBOX = []
@@ -476,6 +482,7 @@
         * using positional argument(s):
 
           .. sourcecode:: python
+
                 rdef = self.schema['CWUser'].rdef('login')
                 with self.temporary_permissions((rdef, {'read': ()})):
                     ...
@@ -484,6 +491,7 @@
         * using named argument(s):
 
           .. sourcecode:: python
+
                 rdef = self.schema['CWUser'].rdef('login')
                 with self.temporary_permissions(CWUser={'read': ()}):
                     ...
@@ -636,9 +644,9 @@
         return publisher
 
     requestcls = fake.FakeRequest
-    def request(self, rollbackfirst=False, url=None, **kwargs):
+    def request(self, rollbackfirst=False, url=None, headers={}, **kwargs):
         """return a web ui request"""
-        req = self.requestcls(self.vreg, url=url, form=kwargs)
+        req = self.requestcls(self.vreg, url=url, headers=headers, form=kwargs)
         if rollbackfirst:
             self.websession.cnx.rollback()
         req.set_session(self.websession)
@@ -649,11 +657,16 @@
         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'):
-        return self.app.publish(path, req)
+    def app_handle_request(self, req, path='view'):
+        return self.app.core_handle(req, path)
+
+    @deprecated("[3.15] app_handle_request is the new and better way"
+                " (beware of small semantic changes)")
+    def app_publish(self, *args, **kwargs):
+        return self.app_handle_request(*args, **kwargs)
 
     def ctrl_publish(self, req, ctrl='edit'):
         """call the publish method of the edit controller"""
@@ -690,6 +703,20 @@
         ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
         return self.ctrl_publish(req, ctrlid)
 
+    @staticmethod
+    def _parse_location(req, location):
+        try:
+            path, params = location.split('?', 1)
+        except ValueError:
+            path = location
+            params = {}
+        else:
+            cleanup = lambda p: (p[0], unquote(p[1]))
+            params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
+        if path.startswith(req.base_url()): # may be relative
+            path = path[len(req.base_url()):]
+        return path, params
+
     def expect_redirect(self, callback, req):
         """call the given callback with req as argument, expecting to get a
         Redirect exception
@@ -697,25 +724,24 @@
         try:
             callback(req)
         except Redirect, ex:
-            try:
-                path, params = ex.location.split('?', 1)
-            except ValueError:
-                path = ex.location
-                params = {}
-            else:
-                cleanup = lambda p: (p[0], unquote(p[1]))
-                params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
-            if path.startswith(req.base_url()): # may be relative
-                path = path[len(req.base_url()):]
-            return path, params
+            return self._parse_location(req, ex.location)
         else:
             self.fail('expected a Redirect exception')
 
-    def expect_redirect_publish(self, req, path='edit'):
+    def expect_redirect_handle_request(self, req, path='edit'):
         """call the publish method of the application publisher, expecting to
         get a Redirect exception
         """
-        return self.expect_redirect(lambda x: self.app_publish(x, path), req)
+        result = self.app_handle_request(req, path)
+        self.assertTrue(300 <= req.status_out <400, req.status_out)
+        location = req.get_response_header('location')
+        return self._parse_location(req, location)
+
+    @deprecated("[3.15] expect_redirect_handle_request is the new and better way"
+                " (beware of small semantic changes)")
+    def expect_redirect_publish(self, *args, **kwargs):
+        return self.expect_redirect_handle_request(*args, **kwargs)
+
 
     def set_auth_mode(self, authmode, anonuser=None):
         self.set_option('auth-mode', authmode)
@@ -741,13 +767,11 @@
 
     def assertAuthSuccess(self, req, origsession, nbsessions=1):
         sh = self.app.session_handler
-        path, params = self.expect_redirect(lambda x: self.app.connect(x), req)
+        self.app.connect(req)
         session = req.session
         self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
         self.assertEqual(session.login, origsession.login)
         self.assertEqual(session.anonymous_session, False)
-        self.assertEqual(path, 'view')
-        self.assertMessageEqual(req, params, 'welcome %s !' % req.user.login)
 
     def assertAuthFailure(self, req, nbsessions=0):
         self.app.connect(req)
@@ -775,11 +799,11 @@
         #'application/xhtml+xml': DTDValidator,
         'application/xml': htmlparser.SaxOnlyValidator,
         'text/xml': htmlparser.SaxOnlyValidator,
+        'application/json': JsonValidator,
         'text/plain': None,
         'text/comma-separated-values': None,
         'text/x-vcard': None,
         'text/calendar': None,
-        'application/json': None,
         'image/png': None,
         }
     # maps vid : validator name (override content_type_validators)
@@ -797,11 +821,14 @@
         :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo`
                   encapsulation the generated HTML
         """
-        req = req or rset and rset.req or self.request()
+        if req is None:
+            if rset is None:
+                req = self.request()
+            else:
+                req = rset.req
         req.form['vid'] = vid
-        kwargs['rset'] = rset
         viewsreg = self.vreg['views']
-        view = viewsreg.select(vid, req, **kwargs)
+        view = viewsreg.select(vid, req, rset=rset, **kwargs)
         # set explicit test description
         if rset is not None:
             self.set_description("testing vid=%s defined in %s with (%s)" % (
@@ -813,10 +840,8 @@
             viewfunc = view.render
         else:
             kwargs['view'] = view
-            templateview = viewsreg.select(template, req, **kwargs)
             viewfunc = lambda **k: viewsreg.main_template(req, template,
-                                                          **kwargs)
-        kwargs.pop('rset')
+                                                          rset=rset, **kwargs)
         return self._test_view(viewfunc, view, template, kwargs)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.15.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,96 @@
+What's new in CubicWeb 3.15?
+============================
+
+New functionnalities
+--------------------
+
+* Add Zmq server, based on the cutting edge ZMQ (http://www.zeromq.org/) socket
+  library.  This allows to access distant instance, in a similar way as Pyro.
+
+* Publish/subscribe mechanism using ZMQ for communication among cubicweb
+  instances.  The new zmq-address-sub and zmq-address-pub configuration variables
+  define where this communication occurs.  As of this release this mechanism is
+  used for entity cache invalidation.
+
+* Improved WSGI support. While there is still some caveats, most of the code
+  which as twisted only is now generic and allows related functionalities to work
+  with a WSGI front-end.
+
+* Full undo/transaction support : undo of modification has eventually been
+  implemented, and the configuration simplified (basically you activate it or not
+  on an instance basis).
+
+* Controlling HTTP status code used is not much more easier :
+
+  - `WebRequest` now has a `status_out` attribut to control the response status ;
+
+  - most web-side exceptions take an optional ``status`` argument.
+
+API changes
+-----------
+
+* 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.
+
+* All login forms are now submitted to <app_root>/login. Redirection to requested
+  page is now handled by the login controller (it was previously handle by the
+  session manager).
+
+* `Publisher.publish` has been renamed to `Publisher.handle_request`. This
+  method now contains generic version of logic previously handled by
+  Twisted. `Controller.publish` is **not** affected.
+
+Unintrusive API changes
+-----------------------
+
+* New 'ldapfeed' source type, designed to replace 'ldapuser' source with
+  data-feed (i.e. copy based) source ideas.
+
+* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
+
+* A new registry called `services` has appeared, where you can register
+  server-side `cubicweb.server.Service` child classes. Their `call` method can be
+  invoked from a web-side AppObject instance using new `self._cw.call_service`
+  method or a server-side one using `self.session.call_service`. This is a new
+  way to call server-side methods, much cleaner than monkey patching the
+  Repository class, which becomes a deprecated way to perform similar tasks.
+
+* a new `ajax-func` registry now hosts all remote functions (i.e. functions
+  callable through the `asyncRemoteExec` JS api). A convenience `ajaxfunc`
+  decorator will let you expose your python function easily without all the
+  appobject standard boilerplate. Backward compatibility is preserved.
+
+* the 'json' controller is now deprecated in favor of the 'ajax' one.
+
+* `WebRequest.build_url` can now take a __secure__ argument. When True cubicweb
+  try to generate an https url.
+
+
+User interface changes
+----------------------
+
+A new 'undohistory' view expose the undoable transactions and give access to undo
+some of them.
--- a/doc/book/en/admin/cubicweb-ctl.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/admin/cubicweb-ctl.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -44,7 +44,7 @@
 Create an instance
 -------------------
 
-You must ensure `~/cubicweb.d/` exists prior to this. On windows, the
+You must ensure `~/etc/cubicweb.d/` exists prior to this. On windows, the
 '~' part will probably expand to 'Documents and Settings/user'.
 
 To create an instance from an existing cube, execute the following
--- a/doc/book/en/admin/instance-config.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/admin/instance-config.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -17,6 +17,7 @@
 each option name is prefixed with its own section and followed by its
 default value if necessary, e.g. "`<section>.<option>` [value]."
 
+.. _`WebServerConfig`:
 
 Configuring the Web server
 --------------------------
--- a/doc/book/en/admin/ldap.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/admin/ldap.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -12,40 +12,55 @@
 
 At cube creation time, one is asked if more sources are wanted. LDAP
 is one possible option at this time. Of course, it is always possible
-to set it up later in the `source` configuration file, which we
-discuss there.
+to set it up later using the `CWSource` entity type, which we discuss
+there.
 
 It is possible to add as many LDAP sources as wanted, which translates
-in as many [ldapxxx] sections in the `source` configuration file.
+in as many `CWSource` entities as needed.
 
 The general principle of the LDAP source is, given a proper
 configuration, to create local users matching the users available in
-the directory, deriving local user attributes from directory users
+the directory and deriving local user attributes from directory users
 attributes. Then a periodic task ensures local user information
 synchronization with the directory.
 
+Users handled by such a source should not be edited directly from
+within the application instance itself. Rather, updates should happen
+at the LDAP server level.
+
 Credential checks are _always_ done against the LDAP server.
 
-The base functionality for this is in
-:file:`cubicweb/server/sources/ldapuser.py`.
+.. Note::
+
+  There are currently two ldap source types: the older `ldapuser` and
+  the newer `ldapfeed`. The older will be deprecated anytime soon, as
+  the newer has now gained all the features of the old and does not
+  suffer from some of its illnesses.
 
-Configurations options
-----------------------
+  The ldapfeed creates real `CWUser` entities, and then
+  activate/deactivate them depending on their presence/absence in the
+  corresponding LDAP source. Their attribute and state
+  (activated/deactivated) are hence managed by the source mechanism;
+  they should not be altered by other means (as such alterations may
+  be overridden in some subsequent source synchronisation).
 
-Let us enumerate the options (but please keep in mind that the
-authoritative source for these is in the aforementioned python
-module), by categories (LDAP server connection, LDAP schema mapping
-information, LDAP source internal configuration).
+
+Configurations options of an LDAPfeed source
+--------------------------------------------
+
+Let us enumerate the options by categories (LDAP server connection,
+LDAP schema mapping information).
 
 LDAP server connection options:
 
-* `host`, may contain port information using <host>:<port> notation.
-* `protocol`, choices are ldap, ldaps, ldapi
 * `auth-mode`, (choices are simple, cram_md5, digest_md5, gssapi, support
   for the later being partial as of now)
+
 * `auth-realm`, realm to use when using gssapi/kerberos authentication
+
 * `data-cnx-dn`, user dn to use to open data connection to the ldap (eg
   used to respond to rql queries)
+
 * `data-cnx-password`, password to use to open data connection to the
   ldap (eg used to respond to rql queries)
 
@@ -53,31 +68,45 @@
 leave data-cnx-dn and data-cnx-password empty. This is, however, quite
 unlikely in practice.
 
-LDAP schema mapping:
+LDAP schema mapping options:
 
 * `user-base-dn`, base DN to lookup for users
-* `user-scope`, user search scope
-* `user-classes`, classes of user
-* `user-attrs-map`, map from ldap user attributes to cubicweb attributes
-* `user-login-attr`, attribute used as login on authentication
+
+* `user-scope`, user search scope (valid values: "BASE", "ONELEVEL",
+  "SUBTREE")
 
-LDAP source internal configuration:
+* `user-classes`, classes of user (with Active Directory, you want to
+  say "user" here)
+
+* `user-filter`, additional filters to be set in the ldap query to
+  find valid users
+
+* `user-login-attr`, attribute used as login on authentication (with
+  Active Directory, you want to use "sAMAccountName" here)
 
 * `user-default-group`, name of a group in which ldap users will be by
   default. You can set multiple groups by separating them by a comma
-* `synchronization-interval`, interval between synchronization with the
-  ldap directory in seconds (default to once a day)
-* `cache-life-time`, life time of query cache in minutes (default to two hours).
+
+* `user-attrs-map`, map from ldap user attributes to cubicweb
+  attributes (with Active Directory, you want to use
+  sAMAccountName:login,mail:email,givenName:firstname,sn:surname)
+
 
 Other notes
 -----------
 
-* Yes, cubicweb is able to start if ldap cannot be reached, even on c-c start,
-  though that will slow down the instance, since it will indefinitly attempt
-  to connect to the ldap on each query on users.
+* Cubicweb is able to start if ldap cannot be reached, even on
+  cubicweb-ctl start ... If some source ldap server cannot be used
+  while an instance is running, the corresponding users won't be
+  authenticated but their status will not change (e.g. they will not
+  be deactivated)
 
-* Changing the name of the ldap server in your script is fine, changing the base
-  DN isn't since it's used to identify already known users from others
+* The user-base-dn is a key that helps cubicweb map CWUsers to LDAP
+  users: beware updating it
+
+* When a user is removed from an LDAP source, it is deactivated in the
+  CubicWeb instance; when a deactivated user comes back in the LDAP
+  source, it (automatically) is activated again
 
 * You can use the :class:`CWSourceHostConfig` to have variants for a source
   configuration according to the host the instance is running on. To do so go on
--- a/doc/book/en/admin/setup.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/admin/setup.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -51,7 +51,7 @@
 Depending on the distribution you are using, add the appropriate line to your
 `list of sources` (for example by editing ``/etc/apt/sources.list``).
 
-For Debian Squeeze (stable)::
+For Debian 6.0 Squeeze (stable)::
 
   deb http://download.logilab.org/production/ squeeze/
 
@@ -59,13 +59,9 @@
 
   deb http://download.logilab.org/production/ sid/
 
-For Ubuntu Lucid (Long Term Support) and newer::
-
-  deb http://download.logilab.org/production/ lucid/
+For Ubuntu 12.04 Precise Pangolin (Long Term Support) and newer::
 
-Note that for Ubuntu Maverick and newer, you shall use the `lucid`
-repository and install the ``libgecode19`` package from `lucid
-universe <http://packages.ubuntu.com/lucid/libgecode19>`_.
+  deb http://download.logilab.org/production/ precise/
 
 The repositories are signed with the `Logilab's gnupg key`_. You can download
 and register the key to avoid warnings::
--- a/doc/book/en/annexes/depends.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/annexes/depends.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -45,6 +45,9 @@
 * indexer - http://www.logilab.org/project/indexer -
   http://pypi.python.org/pypi/indexer - included in the forest
 
+* passlib - https://code.google.com/p/passlib/ -
+  http://pypi.python.org/pypi/passlib
+
 To use network communication between cubicweb instances / clients:
 
 * Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro
--- a/doc/book/en/annexes/faq.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/annexes/faq.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/annexes/rql/language.rst	Thu Mar 21 18:13:31 2013 +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:
@@ -637,7 +640,7 @@
 | :func:`FSPATH(X)`     | expect X to be an attribute whose value is stored in a             |
 |                       | :class:`BFSStorage` and return its path on the file system         |
 +-----------------------+--------------------------------------------------------------------+
-| :func:`FTKIRANK(X)`   | expect X to be an entity used in a has_text relation, and return a |
+| :func:`FTIRANK(X)`    | expect X to be an entity used in a has_text relation, and return a |
 |                       | number corresponding to the rank order of each resulting entity    |
 +-----------------------+--------------------------------------------------------------------+
 | :func:`CAST(Type, X)` | expect X to be an attribute and return it casted into the given    |
--- a/doc/book/en/devrepo/datamodel/definition.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Thu Mar 21 18:13:31 2013 +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/devcore/dbapi.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devrepo/devcore/dbapi.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -111,15 +111,18 @@
    :members:
 
 
-The `Cursor` API
-~~~~~~~~~~~~~~~~
+The `Cursor` and `Connection` API
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The whole cursor API is developped below.
 
 .. note::
 
-  In practice we use the `.execute` method on the _cw object of
+  In practice you'll usually use the `.execute` method on the _cw object of
   appobjects. Usage of other methods is quite rare.
 
 .. autoclass:: cubicweb.dbapi.Cursor
    :members:
+
+.. autoclass:: cubicweb.dbapi.Connection
+   :members:
--- a/doc/book/en/devrepo/entityclasses/adapters.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devrepo/entityclasses/adapters.rst	Thu Mar 21 18:13:31 2013 +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):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devrepo/fti.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,159 @@
+.. _fti:
+
+Full Text Indexing in CubicWeb
+------------------------------
+
+When an attribute is tagged as *fulltext-indexable* in the datamodel,
+CubicWeb will automatically trigger hooks to update the internal
+fulltext index (i.e the ``appears`` SQL table) each time this attribute
+is modified.
+
+CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
+fulltext on demand:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
+
+You can also rebuild the fulltext index for a given set of entity types:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
+
+In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
+will be rebuilt.
+
+
+Standard FTI process
+~~~~~~~~~~~~~~~~~~~~
+
+Considering an entity type ``ET``, the default *fti* process is to :
+
+1. fetch all entities of type ``ET``
+
+2. for each entity, adapt it to ``IFTIndexable`` (see
+   :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
+
+3. call
+   :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
+   the adapter which is supposed to return a dictionary *weight* ->
+   *list of words* as expected by
+   :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
+   tokenization of each attribute value is done by
+   :meth:`~logilab.database.fti.tokenize`.
+
+
+See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
+
+
+Yams and ``fultext_container``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible in the datamodel to indicate that fulltext-indexed
+attributes defined for an entity type will be used to index not the
+entity itself but a related entity. This is especially useful for
+composite entities. Let's take a look at (a simplified version of)
+the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
+
+.. sourcecode:: python
+
+  class CWUser(WorkflowableEntityType):
+      login     = String(required=True, unique=True, maxsize=64)
+      upassword = Password(required=True)
+
+  class EmailAddress(EntityType):
+      address = String(required=True,  fulltextindexed=True,
+                       indexed=True, unique=True, maxsize=128)
+
+
+  class use_email_relation(RelationDefinition):
+      name = 'use_email'
+      subject = 'CWUser'
+      object = 'EmailAddress'
+      cardinality = '*?'
+      composite = 'subject'
+
+
+The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
+and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
+in your application, if you use fulltext search to look for an email address, CubicWeb
+will return the ``EmailAddress`` itself. But the objects we'd like to index
+are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
+
+The simplest way to achieve that is to tag the ``use_email`` relation in
+the datamodel:
+
+.. sourcecode:: python
+
+  class use_email(RelationType):
+      fulltext_container = 'subject'
+
+
+Customizing how entities are fetched during ``db-rebuild-fti``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``db-rebuild-fti`` will call the
+:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
+method on your entity type.
+
+.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
+
+Now, suppose you've got a _huge_ table to index, you probably don't want to
+get all entities at once. So here's a simple customized example that will
+process block of 10000 entities:
+
+.. sourcecode:: python
+
+
+    class MyEntityClass(AnyEntity):
+        __regid__ = 'MyEntityClass'
+
+    @classmethod
+    def cw_fti_index_rql_queries(cls, req):
+        # get the default RQL method and insert LIMIT / OFFSET instructions
+        base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
+        selected, restrictions = base_rql.split(' WHERE ')
+        rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
+            selected, restrictions)
+        # count how many entities you'll have to index
+        count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
+        # iterate by blocks of 10000 entities
+        chunksize = 10000
+        for offset in xrange(0, count, chunksize):
+            print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
+            yield rql_template % {'limit': chunksize, 'offset': offset}
+
+Since you have access to ``req``, you can more or less fetch whatever you want.
+
+
+Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also customize the FTI process by providing your own ``get_words()``
+implementation:
+
+.. sourcecode:: python
+
+    from cubicweb.entities.adapters import IFTIndexableAdapter
+
+    class SearchIndexAdapter(IFTIndexableAdapter):
+        __regid__ = 'IFTIndexable'
+        __select__ = is_instance('MyEntityClass')
+
+        def fti_containers(self, _done=None):
+            """this should yield any entity that must be considered to
+            fulltext-index self.entity
+
+            CubicWeb's default implementation will look for yams'
+            ``fulltex_container`` property.
+            """
+            yield self.entity
+            yield self.entity.some_related_entity
+
+
+        def get_words(self):
+            # implement any logic here
+            # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
+            # for the actual signification of 'C'
+            return {'C': ['any', 'word', 'I', 'want']}
--- a/doc/book/en/devrepo/index.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devrepo/index.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -21,3 +21,5 @@
    testing.rst
    migration.rst
    profiling.rst
+   fti.rst
+
--- a/doc/book/en/devrepo/migration.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devrepo/migration.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -139,7 +139,7 @@
 * `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
   definitions of this type.
 
-* `rename_relation(oldname, newname, commit=True)`, renames a relation.
+* `rename_relationi_type(oldname, newname, commit=True)`, renames a relation type.
 
 * `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
   relation definition.
--- a/doc/book/en/devrepo/repo/hooks.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devrepo/repo/hooks.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devrepo/vreg.rst	Thu Mar 21 18:13:31 2013 +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 Mar 21 18:13:31 2013 +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 :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
+
+.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/controllers.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/controllers.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/edition/form.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/index.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -12,6 +12,7 @@
    request
    views/index
    rtags
+   ajax
    js
    css
    edition/index
--- a/doc/book/en/devweb/js.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/js.rst	Thu Mar 21 18:13:31 2013 +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/property.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/property.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -1,3 +1,5 @@
+.. _cwprops:
+
 The property mecanism
 ---------------------
 
--- a/doc/book/en/devweb/request.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/request.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -99,6 +99,7 @@
     document.ready(...) or another ajax-friendly one-time trigger event
   * `add_header(header, values)`: adds the header/value pair to the
     current html headers
+  * `status_out`: control the HTTP status of the response
 
 * `And more...`
 
--- a/doc/book/en/devweb/resource.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/resource.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -1,3 +1,5 @@
+.. _resources:
+
 Locate resources
 ----------------
 
--- a/doc/book/en/devweb/views/index.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/views/index.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -18,10 +18,11 @@
    boxes
    table
    xmlrss
-..   editforms
    urlpublish
    breadcrumbs
    idownloadable
    wdoc
+
+..   editforms
 ..   embedding
 
--- a/doc/book/en/devweb/views/primary.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/views/primary.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/views/startup.rst	Thu Mar 21 18:13:31 2013 +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/devweb/views/views.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/devweb/views/views.rst	Thu Mar 21 18:13:31 2013 +0100
@@ -32,8 +32,8 @@
 Basic class for views
 ~~~~~~~~~~~~~~~~~~~~~
 
-Class `View` (`cubicweb.view`)
-```````````````````````````````
+Class :class:`~cubicweb.view.View`
+``````````````````````````````````
 
 .. autoclass:: cubicweb.view.View
 
@@ -65,7 +65,7 @@
 
 Other basic view classes
 ````````````````````````
-Here are some of the subclasses of :ref:`View` defined in :ref:`cubicweb.view`
+Here are some of the subclasses of :class:`~cubicweb.view.View` defined in :mod:`cubicweb.view`
 that are more concrete as they relate to data rendering within the application:
 
 .. autoclass:: cubicweb.view.EntityView
--- a/doc/book/en/tutorials/advanced/part02_security.rst	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/tutorials/advanced/part02_security.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/doc/book/en/tutorials/base/customizing-the-application.rst	Thu Mar 21 18:13:31 2013 +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/__init__.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/entities/__init__.py	Thu Mar 21 18:13:31 2013 +0100
@@ -40,6 +40,24 @@
         """ return the url of the entity creation form for this entity type"""
         return req.build_url('add/%s' % cls.__regid__, **kwargs)
 
+    @classmethod
+    def cw_fti_index_rql_queries(cls, req):
+        """return the list of rql queries to fetch entities to FT-index
+
+        The default is to fetch all entities at once and to prefetch
+        indexable attributes but one could imagine iterating over
+        "smaller" resultsets if the table is very big or returning
+        a subset of entities that match some business-logic condition.
+        """
+        restrictions = ['X is %s' % cls.__regid__]
+        selected = ['X']
+        for attrschema in cls.e_schema.indexable_attributes():
+            varname = attrschema.type.upper()
+            restrictions.append('X %s %s' % (attrschema, varname))
+            selected.append(varname)
+        return ['Any %s WHERE %s' % (', '.join(selected),
+                                     ', '.join(restrictions))]
+
     # meta data api ###########################################################
 
     def dc_title(self):
--- a/entities/adapters.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/entities/adapters.py	Thu Mar 21 18:13:31 2013 +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
 
@@ -87,10 +87,20 @@
 
 
 class IFTIndexableAdapter(view.EntityAdapter):
+    """standard adapter to handle fulltext indexing
+
+    .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers
+    .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.get_words
+    """
     __regid__ = 'IFTIndexable'
     __select__ = is_instance('Any')
 
     def fti_containers(self, _done=None):
+        """return the list of entities to index when handling ``self.entity``
+
+        The actual list of entities depends on ``fulltext_container`` usage
+        in the datamodel definition
+        """
         if _done is None:
             _done = set()
         entity = self.entity
--- a/entities/test/unittest_base.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/entities/test/unittest_base.py	Thu Mar 21 18:13:31 2013 +0100
@@ -19,6 +19,7 @@
 """unit tests for cubicweb.entities.base module
 
 """
+from __future__ import with_statement
 
 from logilab.common.testlib import unittest_main
 from logilab.common.decorators import clear_cache
@@ -57,6 +58,12 @@
         self.assertEqual(dict((str(k), v) for k, v in self.schema['State'].meta_attributes().iteritems()),
                           {'description_format': ('format', 'description')})
 
+    def test_fti_rql_method(self):
+        eclass = self.vreg['etypes'].etype_class('EmailAddress')
+        self.assertEqual(['Any X, ALIAS, ADDRESS WHERE X is EmailAddress, '
+                          'X alias ALIAS, X address ADDRESS'],
+                         eclass.cw_fti_index_rql_queries(self.request()))
+
 
 class EmailAddressTC(BaseEntityTC):
     def test_canonical_form(self):
--- a/entities/test/unittest_wfobjs.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/entities/test/unittest_wfobjs.py	Thu Mar 21 18:13:31 2013 +0100
@@ -55,7 +55,8 @@
         wf.add_state(u'foo')
         with self.assertRaises(ValidationError) as cm:
             self.commit()
-        self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
+        self.assertEqual({'name-subject': 'workflow already has a state of that name'},
+                         cm.exception.errors)
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf2.add_state(u'foo', initial=True)
@@ -66,7 +67,8 @@
         bar.set_attributes(name=u'foo')
         with self.assertRaises(ValidationError) as cm:
             self.commit()
-        self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
+        self.assertEqual({'name-subject': 'workflow already has a state of that name'},
+                         cm.exception.errors)
 
     def test_duplicated_transition(self):
         wf = add_wf(self, 'Company')
--- a/entities/wfobjs.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/entities/wfobjs.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/entity.py	Thu Mar 21 18:13:31 2013 +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,
@@ -1112,6 +1112,9 @@
         # insert security RQL expressions granting the permission to 'add' the
         # relation into the rql syntax tree, if necessary
         rqlexprs = rdef.get_rqlexprs('add')
+        if not self.has_eid():
+            rqlexprs = [rqlexpr for rqlexpr in rqlexprs
+                        if searchedvar.name in rqlexpr.mainvars]
         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
             # compute a varmap suitable to RQLRewriter.rewrite argument
             varmap = dict((v, v) for v in (searchedvar.name, evar.name)
--- a/etwist/http.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/etwist/http.py	Thu Mar 21 18:13:31 2013 +0100
@@ -43,19 +43,3 @@
 
     def __repr__(self):
         return "<%s.%s code=%d>" % (self.__module__, self.__class__.__name__, self._code)
-
-
-def not_modified_response(twisted_request, headers_in):
-    headers_out = Headers()
-
-    for header in (
-        # Required from sec 10.3.5:
-        'date', 'etag', 'content-location', 'expires',
-        'cache-control', 'vary',
-        # Others:
-        'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
-        value = headers_in.getRawHeaders(header)
-        if value is not None:
-            headers_out.setRawHeaders(header, value)
-    return HTTPResponse(twisted_request=twisted_request,
-                        headers=headers_out)
--- a/etwist/request.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/etwist/request.py	Thu Mar 21 18:13:31 2013 +0100
@@ -27,27 +27,18 @@
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.web.httpcache import GMTOFFSET
 from cubicweb.web.http_headers import Headers
-from cubicweb.etwist.http import not_modified_response
 
 
 class CubicWebTwistedRequestAdapter(CubicWebRequestBase):
-    def __init__(self, req, vreg, https, base_url):
+    def __init__(self, req, vreg, https):
         self._twreq = req
-        self._base_url = base_url
-        super(CubicWebTwistedRequestAdapter, self).__init__(vreg, https, req.args)
+        super(CubicWebTwistedRequestAdapter, self).__init__(
+            vreg, https, req.args, headers=req.received_headers)
         for key, (name, stream) in req.files.iteritems():
             if name is None:
                 self.form[key] = (name, stream)
             else:
                 self.form[key] = (unicode(name, self.encoding), stream)
-        # XXX can't we keep received_headers?
-        self._headers_in = Headers()
-        for k, v in req.received_headers.iteritems():
-            self._headers_in.addRawHeader(k, v)
-
-    def base_url(self):
-        """return the root url of the instance"""
-        return self._base_url
 
     def http_method(self):
         """returns 'POST', 'GET', 'HEAD', etc."""
@@ -65,56 +56,3 @@
         if not includeparams:
             path = path.split('?', 1)[0]
         return path
-
-    def get_header(self, header, default=None, raw=True):
-        """return the value associated with the given input header, raise
-        KeyError if the header is not set
-        """
-        if raw:
-            return self._headers_in.getRawHeaders(header, [default])[0]
-        return self._headers_in.getHeader(header, default)
-
-    def _validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
-        exists and is still usable
-        """
-        if self.get_header('Cache-Control') in ('max-age=0', 'no-cache'):
-            # Expires header seems to be required by IE7
-            self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-            return
-        # when using both 'Last-Modified' and 'ETag' response headers
-        # (i.e. using respectively If-Modified-Since and If-None-Match request
-        # headers, see
-        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 for
-        # reference
-        last_modified = self.headers_out.getHeader('last-modified')
-        if last_modified is not None:
-            status = self._twreq.setLastModified(last_modified)
-            if status != http.CACHED:
-                return
-        etag = self.headers_out.getRawHeaders('etag')
-        if etag is not None:
-            status = self._twreq.setETag(etag[0])
-            if status == http.CACHED:
-                response = not_modified_response(self._twreq, self._headers_in)
-                raise DirectResponse(response)
-        # Expires header seems to be required by IE7
-        self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-
-    def header_accept_language(self):
-        """returns an ordered list of preferred languages"""
-        acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
-        for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
-                              reverse=True):
-            lang = lang.split('-')[0]
-            yield lang
-
-    def header_if_modified_since(self):
-        """If the HTTP header If-modified-since is set, return the equivalent
-        date time value (GMT), else return None
-        """
-        mtime = self.get_header('If-modified-since', raw=False)
-        if mtime:
-            # :/ twisted is returned a localized time stamp
-            return datetime.fromtimestamp(mtime) + GMTOFFSET
-        return None
--- a/etwist/server.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/etwist/server.py	Thu Mar 21 18:13:31 2013 +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.
@@ -48,7 +48,7 @@
 from cubicweb import (AuthenticationError, ConfigurationError,
                       CW_EVENT_MANAGER, CubicWebException)
 from cubicweb.utils import json_dumps
-from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
+from cubicweb.web import DirectResponse
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.web.http_headers import generateDateTime
 from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
@@ -57,7 +57,7 @@
 def start_task(interval, func):
     lc = task.LoopingCall(func)
     # wait until interval has expired to actually start the task, else we have
-    # to wait all task to be finished for the server to be actually started
+    # to wait all tasks to be finished for the server to be actually started
     lc.start(interval, now=False)
 
 def host_prefixed_baseurl(baseurl, host):
@@ -69,175 +69,6 @@
     return baseurl
 
 
-class ForbiddenDirectoryLister(resource.Resource):
-    def render(self, request):
-        return HTTPResponse(twisted_request=request,
-                            code=http.FORBIDDEN,
-                            stream='Access forbidden')
-
-
-class NoListingFile(static.File):
-    def __init__(self, config, path=None):
-        if path is None:
-            path = config.static_directory
-        static.File.__init__(self, path)
-        self.config = config
-
-    def set_expires(self, request):
-        if not self.config.debugmode:
-            # XXX: Don't provide additional resource information to error responses
-            #
-            # the HTTP RFC recommands not going further than 1 year ahead
-            expires = date.today() + timedelta(days=6*30)
-            request.setHeader('Expires', generateDateTime(mktime(expires.timetuple())))
-
-    def directoryListing(self):
-        return ForbiddenDirectoryLister()
-
-    def createSimilarFile(self, path):
-        # we override this method because twisted calls __init__
-        # which we overload with a different signature
-        f = self.__class__(self.config, path)
-        f.processors = self.processors
-        f.indexNames = self.indexNames[:]
-        f.childNotFound = self.childNotFound
-        return f
-
-
-class DataLookupDirectory(NoListingFile):
-    def __init__(self, config, path):
-        self.md5_version = config.instance_md5_version()
-        NoListingFile.__init__(self, config, path)
-        self.here = path
-        self._defineChildResources()
-        if self.config.debugmode:
-            self.data_modconcat_basepath = '/data/??'
-        else:
-            self.data_modconcat_basepath = '/data/%s/??' % self.md5_version
-
-    def _defineChildResources(self):
-        self.putChild(self.md5_version, self)
-
-    def getChild(self, path, request):
-        if not path:
-            uri = request.uri
-            if uri.startswith('/https/'):
-                uri = uri[6:]
-            if uri.startswith(self.data_modconcat_basepath):
-                resource_relpath = uri[len(self.data_modconcat_basepath):]
-                if resource_relpath:
-                    paths = resource_relpath.split(',')
-                    try:
-                        self.set_expires(request)
-                        return ConcatFiles(self.config, paths)
-                    except ConcatFileNotFoundError:
-                        return self.childNotFound
-            return self.directoryListing()
-        childpath = join(self.here, path)
-        dirpath, rid = self.config.locate_resource(childpath)
-        if dirpath is None:
-            # resource not found
-            return self.childNotFound
-        filepath = os.path.join(dirpath, rid)
-        if os.path.isdir(filepath):
-            resource = DataLookupDirectory(self.config, childpath)
-            # cache resource for this segment path to avoid recomputing
-            # directory lookup
-            self.putChild(path, resource)
-            return resource
-        else:
-            self.set_expires(request)
-            return NoListingFile(self.config, filepath)
-
-
-class FCKEditorResource(NoListingFile):
-
-    def getChild(self, path, request):
-        pre_path = request.path.split('/')[1:]
-        if pre_path[0] == 'https':
-            pre_path.pop(0)
-            uiprops = self.config.https_uiprops
-        else:
-            uiprops = self.config.uiprops
-        return static.File(osp.join(uiprops['FCKEDITOR_PATH'], path))
-
-
-class LongTimeExpiringFile(DataLookupDirectory):
-    """overrides static.File and sets a far future ``Expires`` date
-    on the resouce.
-
-    versions handling is done by serving static files by different
-    URLs for each version. For instance::
-
-      http://localhost:8080/data-2.48.2/cubicweb.css
-      http://localhost:8080/data-2.49.0/cubicweb.css
-      etc.
-
-    """
-    def _defineChildResources(self):
-        pass
-
-
-class ConcatFileNotFoundError(CubicWebException):
-    pass
-
-
-class ConcatFiles(LongTimeExpiringFile):
-    def __init__(self, config, paths):
-        _, ext = osp.splitext(paths[0])
-        self._resources = {}
-        # create a unique / predictable filename. We don't consider cubes
-        # version since uicache is cleared at server startup, and file's dates
-        # are checked in debug mode
-        fname = 'cache_concat_' + md5(';'.join(paths)).hexdigest() + ext
-        filepath = osp.join(config.appdatahome, 'uicache', fname)
-        LongTimeExpiringFile.__init__(self, config, filepath)
-        self._concat_cached_filepath(filepath, paths)
-
-    def _resource(self, path):
-        try:
-            return self._resources[path]
-        except KeyError:
-            self._resources[path] = self.config.locate_resource(path)
-            return self._resources[path]
-
-    def _concat_cached_filepath(self, filepath, paths):
-        if not self._up_to_date(filepath, paths):
-            with open(filepath, 'wb') as f:
-                for path in paths:
-                    dirpath, rid = self._resource(path)
-                    if rid is None:
-                        # In production mode log an error, do not return a 404
-                        # XXX the erroneous content is cached anyway
-                        LOGGER.error('concatenated data url error: %r file '
-                                     'does not exist', path)
-                        if self.config.debugmode:
-                            raise ConcatFileNotFoundError(path)
-                    else:
-                        for line in open(osp.join(dirpath, rid)):
-                            f.write(line)
-                        f.write('\n')
-
-    def _up_to_date(self, filepath, paths):
-        """
-        The concat-file is considered up-to-date if it exists.
-        In debug mode, an additional check is performed to make sure that
-        concat-file is more recent than all concatenated files
-        """
-        if not osp.isfile(filepath):
-            return False
-        if self.config.debugmode:
-            concat_lastmod = os.stat(filepath).st_mtime
-            for path in paths:
-                dirpath, rid = self._resource(path)
-                if rid is None:
-                    raise ConcatFileNotFoundError(path)
-                path = osp.join(dirpath, rid)
-                if os.stat(path).st_mtime > concat_lastmod:
-                    return False
-        return True
-
-
 class CubicWebRootResource(resource.Resource):
     def __init__(self, config, vreg=None):
         resource.Resource.__init__(self)
@@ -249,9 +80,6 @@
         self.https_url = config['https-url']
         global MAX_POST_LENGTH
         MAX_POST_LENGTH = config['max-post-length']
-        self.putChild('static', NoListingFile(config))
-        self.putChild('fckeditor', FCKEditorResource(self.config, ''))
-        self.putChild('data', DataLookupDirectory(self.config, ''))
 
     def init_publisher(self):
         config = self.config
@@ -329,88 +157,28 @@
         host = request.host
         # dual http/https access handling: expect a rewrite rule to prepend
         # 'https' to the path to detect https access
+        https = False
         if origpath.split('/', 2)[1] == 'https':
             origpath = origpath[6:]
             request.uri = request.uri[6:]
             https = True
-            baseurl = self.https_url or self.base_url
-        else:
-            https = False
-            baseurl = self.base_url
-        if self.config['use-request-subdomain']:
-            baseurl = host_prefixed_baseurl(baseurl, host)
-            self.warning('used baseurl is %s for this request', baseurl)
-        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https, baseurl)
-        if req.authmode == 'http':
-            # activate realm-based auth
-            realm = self.config['realm']
-            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
-        try:
-            self.appli.connect(req)
-        except Redirect, ex:
-            return self.redirect(request=req, location=ex.location)
-        if https and req.session.anonymous_session and self.config['https-deny-anonymous']:
-            # don't allow anonymous on https connection
-            return self.request_auth(request=req)
         if self.url_rewriter is not None:
             # XXX should occur before authentication?
-            try:
-                path = self.url_rewriter.rewrite(host, origpath, req)
-            except Redirect, ex:
-                return self.redirect(req, ex.location)
+            path = self.url_rewriter.rewrite(host, origpath, request)
             request.uri.replace(origpath, path, 1)
         else:
             path = origpath
-        if not path or path == "/":
-            path = 'view'
+        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https)
         try:
-            result = self.appli.publish(path, req)
+            ### Try to generate the actual request content
+            content = self.appli.handle_request(req, path)
         except DirectResponse, ex:
             return ex.response
-        except StatusResponse, ex:
-            return HTTPResponse(stream=ex.content, code=ex.status,
-                                twisted_request=req._twreq,
-                                headers=req.headers_out)
-        except AuthenticationError:
-            return self.request_auth(request=req)
-        except LogOut, ex:
-            if self.config['auth-mode'] == 'cookie' and ex.url:
-                return self.redirect(request=req, location=ex.url)
-            # in http we have to request auth to flush current http auth
-            # information
-            return self.request_auth(request=req, loggedout=True)
-        except Redirect, ex:
-            return self.redirect(request=req, location=ex.location)
-        # request may be referenced by "onetime callback", so clear its entity
-        # cache to avoid memory usage
-        req.drop_entity_cache()
-        return HTTPResponse(twisted_request=req._twreq, code=http.OK,
-                            stream=result, headers=req.headers_out)
-
-    def redirect(self, request, location):
-        self.debug('redirecting to %s', str(location))
-        request.headers_out.setHeader('location', str(location))
-        # 303 See other
-        return HTTPResponse(twisted_request=request._twreq, code=303,
-                            headers=request.headers_out)
-
-    def request_auth(self, request, loggedout=False):
-        if self.https_url and request.base_url() != self.https_url:
-            return self.redirect(request, self.https_url + 'login')
-        if self.config['auth-mode'] == 'http':
-            code = http.UNAUTHORIZED
-        else:
-            code = http.FORBIDDEN
-        if loggedout:
-            if request.https:
-                request._base_url =  self.base_url
-                request.https = False
-            content = self.appli.loggedout_content(request)
-        else:
-            content = self.appli.need_login_content(request)
-        return HTTPResponse(twisted_request=request._twreq,
-                            stream=content, code=code,
-                            headers=request.headers_out)
+        # at last: create twisted object
+        return HTTPResponse(code    = req.status_out,
+                            headers = req.headers_out,
+                            stream  = content,
+                            twisted_request=req._twreq)
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
--- a/etwist/test/unittest_server.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/etwist/test/unittest_server.py	Thu Mar 21 18:13:31 2013 +0100
@@ -19,8 +19,7 @@
 import os, os.path as osp, glob
 
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.etwist.server import (host_prefixed_baseurl, ConcatFiles,
-                                    ConcatFileNotFoundError)
+from cubicweb.etwist.server import host_prefixed_baseurl
 
 
 class HostPrefixedBaseURLTC(CubicWebTC):
@@ -54,30 +53,6 @@
         self._check('http://localhost:8080/hg/', 'code.cubicweb.org',
                     'http://localhost:8080/hg/')
 
-
-class ConcatFilesTC(CubicWebTC):
-
-    def tearDown(self):
-        super(ConcatFilesTC, self).tearDown()
-        self._cleanup_concat_cache()
-        self.config.debugmode = False
-
-    def _cleanup_concat_cache(self):
-        uicachedir = osp.join(self.config.apphome, 'uicache')
-        for fname in glob.glob(osp.join(uicachedir, 'cache_concat_*')):
-            os.unlink(osp.join(uicachedir, fname))
-
-    def test_cache(self):
-        concat = ConcatFiles(self.config, ('cubicweb.ajax.js', 'jquery.js'))
-        self.assertTrue(osp.isfile(concat.path))
-
-    def test_404(self):
-        # when not in debug mode, should not crash
-        ConcatFiles(self.config, ('cubicweb.ajax.js', 'dummy.js'))
-        # in debug mode, raise error
-        self.config.debugmode = True
-        try:
-            self.assertRaises(ConcatFileNotFoundError, ConcatFiles, self.config,
-                              ('cubicweb.ajax.js', 'dummy.js'))
-        finally:
-            self.config.debugmode = False
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/ext/rest.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/ext/rest.py	Thu Mar 21 18:13:31 2013 +0100
@@ -96,7 +96,16 @@
                             **options)], []
 
 def rql_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
-    """:rql:`Any X,Y WHERE X is CWUser, X login Y:table`"""
+    """:rql:`<rql-expr>` or :rql:`<rql-expr>:<vid>`
+
+    Example: :rql:`Any X,Y WHERE X is CWUser, X login Y:table`
+
+    Replace the directive with the output of applying the view to the resultset
+    returned by the query.
+
+    "X eid %(userid)s" can be used in the RQL query for this query will be
+    executed with the argument {'userid': _cw.user.eid}.
+    """
     _cw = inliner.document.settings.context._cw
     text = text.strip()
     if ':' in text:
--- a/hooks/__init__.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/__init__.py	Thu Mar 21 18:13:31 2013 +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,9 +23,9 @@
 
 from cubicweb.server import hook
 
-class ServerStartupHook(hook.Hook):
-    """task to cleanup expirated auth cookie entities"""
-    __regid__ = 'cw.start-looping-tasks'
+class TransactionsCleanupStartupHook(hook.Hook):
+    """start task to cleanup transaction data"""
+    __regid__ = 'cw.looping-tasks.transactions-cleanup'
     events = ('server_startup',)
 
     def __call__(self):
@@ -46,9 +46,16 @@
                 session.commit()
             finally:
                 session.close()
-        if self.repo.config['undo-support']:
+        if self.repo.config['undo-enabled']:
             self.repo.looping_task(60*60*24, cleanup_old_transactions,
                                    self.repo)
+
+class UpdateFeedsStartupHook(hook.Hook):
+    """start task to update datafeed based sources"""
+    __regid__ = 'cw.looping-tasks.update-feeds'
+    events = ('server_startup',)
+
+    def __call__(self):
         def update_feeds(repo):
             # don't iter on repo.sources which doesn't include copy based
             # sources (the one we're looking for)
@@ -66,6 +73,13 @@
                     session.close()
         self.repo.looping_task(60, update_feeds, self.repo)
 
+
+class DataImportsCleanupStartupHook(hook.Hook):
+    """start task to cleanup old data imports (ie datafeed import logs)"""
+    __regid__ = 'cw.looping-tasks.dataimports-cleanup'
+    events = ('server_startup',)
+
+    def __call__(self):
         def expire_dataimports(repo=self.repo):
             for source in repo.sources_by_eid.itervalues():
                 if (not source.copy_based_source
@@ -74,7 +88,8 @@
                 session = repo.internal_session()
                 try:
                     mindate = datetime.now() - timedelta(seconds=source.config['logs-lifetime'])
-                    session.execute('DELETE CWDataImport X WHERE X start_timestamp < %(time)s', {'time': mindate})
+                    session.execute('DELETE CWDataImport X WHERE X start_timestamp < %(time)s',
+                                    {'time': mindate})
                     session.commit()
                 finally:
                     session.close()
--- a/hooks/integrity.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/integrity.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/metadata.py	Thu Mar 21 18:13:31 2013 +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
 
@@ -199,17 +199,12 @@
             entity = self._cw.entity_from_eid(self.eidfrom)
             # copy entity if necessary
             if not oldsource.repo_source.copy_based_source:
-                entity.complete(skip_bytes=False)
+                entity.complete(skip_bytes=False, skip_pwd=False)
                 if not entity.creation_date:
                     entity.cw_attr_cache['creation_date'] = datetime.now()
                 if not entity.modification_date:
                     entity.cw_attr_cache['modification_date'] = datetime.now()
                 entity.cw_attr_cache['cwuri'] = u'%s%s' % (self._cw.base_url(), entity.eid)
-                for rschema, attrschema in entity.e_schema.attribute_definitions():
-                    if attrschema == 'Password' and \
-                       rschema.rdef(entity.e_schema, attrschema).cardinality[0] == '1':
-                        from logilab.common.shellutils import generate_password
-                        entity.cw_attr_cache[rschema.type] = generate_password()
                 entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
                 syssource.add_entity(self._cw, entity)
             # we don't want the moved entity to be reimported later.  To
--- a/hooks/notification.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/notification.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/security.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/syncschema.py	Thu Mar 21 18:13:31 2013 +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
@@ -244,7 +244,7 @@
     * create the necessary table
     * set creation_date and modification_date by creating the necessary
       CWAttribute entities
-    * add owned_by relation by creating the necessary CWRelation entity
+    * add <meta rtype> relation by creating the necessary CWRelation entity
     """
     entity = None # make pylint happy
 
@@ -270,9 +270,16 @@
             except KeyError:
                 self.critical('rtype %s was not handled at cwetype creation time', rtype)
                 continue
+            if not rschema.rdefs:
+                self.warning('rtype %s has no relation definition yet', rtype)
+                continue
             sampletype = rschema.subjects()[0]
             desttype = rschema.objects()[0]
-            rdef = copy(rschema.rdef(sampletype, desttype))
+            try:
+                rdef = copy(rschema.rdef(sampletype, desttype))
+            except KeyError:
+                # this combo does not exist because this is not a universal META_RTYPE
+                continue
             rdef.subject = _MockEntity(eid=entity.eid)
             mock = _MockEntity(eid=None)
             ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
@@ -755,7 +762,13 @@
         cols = ['%s%s' % (prefix, c) for c in self.cols]
         sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols)
         for sql in sqls:
-            session.system_sql(sql)
+            try:
+                session.system_sql(sql)
+            except Exception, exc: # should be ProgrammingError
+                if sql.startswith('DROP'):
+                    self.error('execute of `%s` failed (cause: %s)', sql, exc)
+                    continue
+                raise
 
     # XXX revertprecommit_event
 
--- a/hooks/syncsession.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/syncsession.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/syncsources.py	Thu Mar 21 18:13:31 2013 +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/test/unittest_syncschema.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/test/unittest_syncschema.py	Thu Mar 21 18:13:31 2013 +0100
@@ -20,10 +20,12 @@
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb import ValidationError
+from cubicweb.schema import META_RTYPES
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.devtools.repotest import schema_eids_idx, restore_schema_eids_idx
 
+
 def tearDownModule(*args):
     del SchemaModificationHooksTC.schema_eids
 
@@ -116,6 +118,33 @@
         self.assertFalse(schema.has_entity('concerne2'))
         self.assertFalse('concerne2' in schema['CWUser'].subject_relations())
 
+    def test_metartype_with_nordefs(self):
+        META_RTYPES.add('custom_meta')
+        self.execute('INSERT CWRType X: X name "custom_meta", X description "", '
+                     'X final FALSE, X symmetric FALSE')
+        self.commit()
+        eeid = self.execute('INSERT CWEType X: X name "NEWEtype", '
+                            'X description "", X final FALSE')[0][0]
+        self._set_perms(eeid)
+        self.commit()
+        META_RTYPES.remove('custom_meta')
+
+    def test_metartype_with_somerdefs(self):
+        META_RTYPES.add('custom_meta')
+        self.execute('INSERT CWRType X: X name "custom_meta", X description "", '
+                     'X final FALSE, X symmetric FALSE')
+        self.commit()
+        rdefeid = self.execute('INSERT CWRelation X: X cardinality "**", X relation_type RT, '
+                               '   X from_entity E, X to_entity E '
+                               'WHERE RT name "custom_meta", E name "CWUser"')[0][0]
+        self._set_perms(rdefeid)
+        self.commit()
+        eeid = self.execute('INSERT CWEType X: X name "NEWEtype", '
+                            'X description "", X final FALSE')[0][0]
+        self._set_perms(eeid)
+        self.commit()
+        META_RTYPES.remove('custom_meta')
+
     def test_is_instance_of_insertions(self):
         seid = self.execute('INSERT Transition T: T name "subdiv"')[0][0]
         is_etypes = [etype for etype, in self.execute('Any ETN WHERE X eid %s, X is ET, ET name ETN' % seid)]
--- a/hooks/workflow.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/hooks/workflow.py	Thu Mar 21 18:13:31 2013 +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 Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,75 @@
+# -*- 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()
+
+
+class ZMQRepositoryServerStopHook(hook.Hook):
+    __regid__ = 'zmqrepositoryserverstop'
+    events = ('server_shutdown',)
+
+    def __call__(self):
+        server = getattr(self.repo, 'zmq_repo_server', None)
+        if server:
+            self.repo.zmq_repo_server.quit()
+
+class ZMQRepositoryServerStartHook(hook.Hook):
+    __regid__ = 'zmqrepositoryserverstart'
+    events = ('server_startup',)
+
+    def __call__(self):
+        config = self.repo.config
+        if config.name == 'repository':
+            # start-repository command already starts a zmq repo
+            return
+        address = config.get('zmq-repository-address')
+        if not address:
+            return
+        from cubicweb.server import cwzmq
+        self.repo.zmq_repo_server = server = cwzmq.ZMQRepositoryServer(self.repo)
+        server.connect(address)
+        self.repo.threaded_task(server.run)
+
--- a/i18n/de.po	Wed Aug 01 10:30:48 2012 +0200
+++ b/i18n/de.po	Thu Mar 21 18:13:31 2013 +0100
@@ -145,6 +145,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID nicht gefunden)"
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -218,6 +222,10 @@
 msgid "About this site"
 msgstr "Ãœber diese Seite"
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
 msgid "Any"
 msgstr "irgendein"
 
@@ -263,6 +271,10 @@
 msgid "Browse by entity type"
 msgstr "nach Identitätstyp navigieren"
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -390,14 +402,6 @@
 
 #, python-format
 msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
-"diese Relation existiert nicht mehr in dem Schema."
-
-#, python-format
-msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
@@ -423,6 +427,10 @@
 msgid "Click to sort on this column"
 msgstr ""
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "DEBUG"
 msgstr ""
 
@@ -448,6 +456,14 @@
 msgid "Decimal_plural"
 msgstr "Dezimalzahlen"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Detected problems"
 msgstr ""
 
@@ -848,6 +864,11 @@
 msgid "This WorkflowTransition"
 msgstr "Dieser Workflow-Ãœbergang"
 
+msgid ""
+"This action is forbidden. If you think it should be allowed, please contact "
+"the site administrator."
+msgstr ""
+
 msgid "This entity type permissions:"
 msgstr "Berechtigungen für diesen Entitätstyp"
 
@@ -872,12 +893,22 @@
 msgid "URLs from which content will be imported. You can put one url per line"
 msgstr ""
 
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
 msgid "UniqueConstraint"
 msgstr "eindeutige Einschränkung"
 
 msgid "Unreachable objects"
 msgstr "unzugängliche Objekte"
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Used by:"
 msgstr "benutzt von:"
 
@@ -1265,6 +1296,9 @@
 msgid "bad value"
 msgstr "Unzulässiger Wert"
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr "Basis-URL"
 
@@ -1349,6 +1383,9 @@
 msgid "can not resolve entity types:"
 msgstr "Die Typen konnten nicht ermittelt werden:"
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr "kann nicht geändert werden"
 
@@ -1386,6 +1423,22 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -2015,9 +2068,6 @@
 msgid "date"
 msgstr "Datum"
 
-msgid "day"
-msgstr ""
-
 msgid "deactivate"
 msgstr "deaktivieren"
 
@@ -2672,6 +2722,9 @@
 msgid "has_text"
 msgstr "enthält Text"
 
+msgid "header-center"
+msgstr ""
+
 msgid "header-left"
 msgstr ""
 
@@ -3031,9 +3084,6 @@
 msgid "log in"
 msgstr "anmelden"
 
-msgid "log out first"
-msgstr "Melden Sie sich zuerst ab."
-
 msgid "login"
 msgstr "Anmeldung"
 
@@ -3136,9 +3186,6 @@
 msgid "monday"
 msgstr "Montag"
 
-msgid "month"
-msgstr ""
-
 msgid "more actions"
 msgstr "weitere Aktionen"
 
@@ -3241,6 +3288,9 @@
 msgid "new"
 msgstr "neu"
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "weitere Ergebnisse"
 
@@ -3442,6 +3492,9 @@
 msgid "preferences"
 msgstr "Einstellungen"
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "vorige Ergebnisse"
 
@@ -4028,6 +4081,10 @@
 msgid "there is no previous page"
 msgstr ""
 
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
 msgid "this action is not reversible!"
 msgstr "Achtung! Diese Aktion ist unumkehrbar."
 
@@ -4111,9 +4168,6 @@
 msgid "to_state_object"
 msgstr "Ãœbergang zu diesem Zustand"
 
-msgid "today"
-msgstr ""
-
 msgid "todo_by"
 msgstr "zu erledigen bis"
 
@@ -4238,6 +4292,9 @@
 msgid "unauthorized value"
 msgstr "ungültiger Wert"
 
+msgid "undefined user"
+msgstr ""
+
 msgid "undo"
 msgstr "rückgängig machen"
 
@@ -4265,6 +4322,9 @@
 msgid "unknown vocabulary:"
 msgstr "Unbekanntes Wörterbuch : "
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "Passwort"
 
@@ -4474,9 +4534,6 @@
 msgid "wednesday"
 msgstr "Mittwoch"
 
-msgid "week"
-msgstr "Woche"
-
 #, python-format
 msgid "welcome %s !"
 msgstr "Willkommen %s !"
@@ -4573,45 +4630,15 @@
 msgid "you should un-inline relation %s which is supported and may be crossed "
 msgstr ""
 
-#~ msgid "(loading ...)"
-#~ msgstr "(laden...)"
-
-#~ msgid "Schema of the data model"
-#~ msgstr "Schema des Datenmodells"
-
-#~ msgid "csv entities export"
-#~ msgstr "CSV-Export von Entitäten"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "Folgen Sie diesem Link, falls Javascript deaktiviert ist."
-
 #~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
 #~ msgstr ""
-#~ "Wie formatiert man das Datum Interface im (\"man strftime\" für die "
-#~ "Beschreibung des neuen Formats"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Wie formatiert man das Datum im Interface (\"man strftime\" für die "
-#~ "Beschreibung des Formats)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Wie man die Uhrzeit im Interface (\"man strftime\" für die "
-#~ "Formatbeschreibung)"
-
-#~ msgid "instance schema"
-#~ msgstr "Schema der Instanz"
-
-#~ msgid "rss"
-#~ msgstr "RSS"
-
-#~ msgid "xbel"
-#~ msgstr "XBEL"
-
-#~ msgid "xml"
-#~ msgstr "XML"
+#~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
+#~ "diese Relation existiert nicht mehr in dem Schema."
+
+#~ msgid "log out first"
+#~ msgstr "Melden Sie sich zuerst ab."
+
+#~ msgid "week"
+#~ msgstr "Woche"
--- a/i18n/en.po	Wed Aug 01 10:30:48 2012 +0200
+++ b/i18n/en.po	Thu Mar 21 18:13:31 2013 +0100
@@ -137,6 +137,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr ""
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -207,6 +211,10 @@
 msgid "About this site"
 msgstr ""
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
 msgid "Any"
 msgstr ""
 
@@ -252,6 +260,10 @@
 msgid "Browse by entity type"
 msgstr ""
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -374,12 +386,6 @@
 
 #, python-format
 msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-
-#, python-format
-msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
@@ -399,6 +405,10 @@
 msgid "Click to sort on this column"
 msgstr ""
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "DEBUG"
 msgstr ""
 
@@ -424,6 +434,14 @@
 msgid "Decimal_plural"
 msgstr "Decimal numbers"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Detected problems"
 msgstr ""
 
@@ -822,6 +840,11 @@
 msgid "This WorkflowTransition"
 msgstr "This workflow-transition"
 
+msgid ""
+"This action is forbidden. If you think it should be allowed, please contact "
+"the site administrator."
+msgstr ""
+
 msgid "This entity type permissions:"
 msgstr ""
 
@@ -846,12 +869,22 @@
 msgid "URLs from which content will be imported. You can put one url per line"
 msgstr ""
 
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
 msgid "UniqueConstraint"
 msgstr "unique constraint"
 
 msgid "Unreachable objects"
 msgstr ""
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Used by:"
 msgstr ""
 
@@ -1220,6 +1253,9 @@
 msgid "bad value"
 msgstr ""
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr ""
 
@@ -1304,6 +1340,9 @@
 msgid "can not resolve entity types:"
 msgstr ""
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr ""
 
@@ -1340,6 +1379,22 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -1970,9 +2025,6 @@
 msgid "date"
 msgstr ""
 
-msgid "day"
-msgstr ""
-
 msgid "deactivate"
 msgstr ""
 
@@ -2610,6 +2662,9 @@
 msgid "has_text"
 msgstr "has text"
 
+msgid "header-center"
+msgstr ""
+
 msgid "header-left"
 msgstr "header (left)"
 
@@ -2948,9 +3003,6 @@
 msgid "log in"
 msgstr ""
 
-msgid "log out first"
-msgstr ""
-
 msgid "login"
 msgstr ""
 
@@ -3052,9 +3104,6 @@
 msgid "monday"
 msgstr ""
 
-msgid "month"
-msgstr ""
-
 msgid "more actions"
 msgstr ""
 
@@ -3155,6 +3204,9 @@
 msgid "new"
 msgstr ""
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "next results"
 
@@ -3355,6 +3407,9 @@
 msgid "preferences"
 msgstr ""
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "previous results"
 
@@ -3926,6 +3981,10 @@
 msgid "there is no previous page"
 msgstr ""
 
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
 msgid "this action is not reversible!"
 msgstr ""
 
@@ -4009,9 +4068,6 @@
 msgid "to_state_object"
 msgstr "transitions to this state"
 
-msgid "today"
-msgstr ""
-
 msgid "todo_by"
 msgstr "to do by"
 
@@ -4136,6 +4192,9 @@
 msgid "unauthorized value"
 msgstr ""
 
+msgid "undefined user"
+msgstr ""
+
 msgid "undo"
 msgstr ""
 
@@ -4163,6 +4222,9 @@
 msgid "unknown vocabulary:"
 msgstr ""
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "password"
 
@@ -4361,9 +4423,6 @@
 msgid "wednesday"
 msgstr ""
 
-msgid "week"
-msgstr ""
-
 #, python-format
 msgid "welcome %s !"
 msgstr ""
--- a/i18n/es.po	Wed Aug 01 10:30:48 2012 +0200
+++ b/i18n/es.po	Thu Mar 21 18:13:31 2013 +0100
@@ -146,6 +146,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID INEXISTENTE"
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -219,6 +223,10 @@
 msgid "About this site"
 msgstr "Información del Sistema"
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
 msgid "Any"
 msgstr "Cualquiera"
 
@@ -264,6 +272,10 @@
 msgid "Browse by entity type"
 msgstr "Busca por tipo de entidad"
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -390,14 +402,6 @@
 
 #, python-format
 msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
-"relación ya no existe en el esquema."
-
-#, python-format
-msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
@@ -423,6 +427,10 @@
 msgid "Click to sort on this column"
 msgstr ""
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "DEBUG"
 msgstr ""
 
@@ -448,6 +456,14 @@
 msgid "Decimal_plural"
 msgstr "Decimales"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Detected problems"
 msgstr "Problemas detectados"
 
@@ -849,6 +865,11 @@
 msgid "This WorkflowTransition"
 msgstr "Esta transición de Workflow"
 
+msgid ""
+"This action is forbidden. If you think it should be allowed, please contact "
+"the site administrator."
+msgstr ""
+
 msgid "This entity type permissions:"
 msgstr "Permisos para este tipo de entidad:"
 
@@ -875,12 +896,22 @@
 "URLs desde el cual el contenido sera importado. Usted puede incluir un URL "
 "por línea."
 
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
 msgid "UniqueConstraint"
 msgstr "Restricción de Unicidad"
 
 msgid "Unreachable objects"
 msgstr "Objetos inaccesibles"
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Used by:"
 msgstr "Utilizado por :"
 
@@ -1276,6 +1307,9 @@
 msgid "bad value"
 msgstr "Valor erróneo"
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr "Url de base"
 
@@ -1360,6 +1394,9 @@
 msgid "can not resolve entity types:"
 msgstr "Imposible de interpretar los tipos de entidades:"
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr "No puede ser modificado"
 
@@ -1396,6 +1433,22 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -2044,9 +2097,6 @@
 msgid "date"
 msgstr "Fecha"
 
-msgid "day"
-msgstr "día"
-
 msgid "deactivate"
 msgstr "Desactivar"
 
@@ -2713,6 +2763,9 @@
 msgid "has_text"
 msgstr "Contiene el texto"
 
+msgid "header-center"
+msgstr ""
+
 msgid "header-left"
 msgstr "encabezado (izquierdo)"
 
@@ -3073,9 +3126,6 @@
 msgid "log in"
 msgstr "Acceder"
 
-msgid "log out first"
-msgstr "Desconéctese primero"
-
 msgid "login"
 msgstr "Usuario"
 
@@ -3177,9 +3227,6 @@
 msgid "monday"
 msgstr "Lunes"
 
-msgid "month"
-msgstr "mes"
-
 msgid "more actions"
 msgstr "Más acciones"
 
@@ -3282,6 +3329,9 @@
 msgid "new"
 msgstr "Nuevo"
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "Siguientes resultados"
 
@@ -3483,6 +3533,9 @@
 msgid "preferences"
 msgstr "Preferencias"
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "Resultados Anteriores"
 
@@ -4078,6 +4131,10 @@
 msgid "there is no previous page"
 msgstr ""
 
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
 msgid "this action is not reversible!"
 msgstr "Esta acción es irreversible!."
 
@@ -4161,9 +4218,6 @@
 msgid "to_state_object"
 msgstr "Transición hacia este Estado"
 
-msgid "today"
-msgstr "hoy"
-
 msgid "todo_by"
 msgstr "Asignada a"
 
@@ -4288,6 +4342,9 @@
 msgid "unauthorized value"
 msgstr "Valor no permitido"
 
+msgid "undefined user"
+msgstr ""
+
 msgid "undo"
 msgstr "Anular"
 
@@ -4315,6 +4372,9 @@
 msgid "unknown vocabulary:"
 msgstr "Vocabulario desconocido: "
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "Contraseña"
 
@@ -4522,9 +4582,6 @@
 msgid "wednesday"
 msgstr "Miércoles"
 
-msgid "week"
-msgstr "sem."
-
 #, python-format
 msgid "welcome %s !"
 msgstr "¡ Bienvenido %s  !"
@@ -4624,54 +4681,24 @@
 "usted debe  quitar la puesta en línea de la relación %s que es aceptada y "
 "puede ser cruzada"
 
-#~ msgid "(loading ...)"
-#~ msgstr "(Cargando ...)"
-
-#~ msgid "Schema of the data model"
-#~ msgstr "Esquema del modelo de datos"
-
-#~ msgid "add a CWSourceSchemaConfig"
-#~ msgstr "agregar una parte de mapeo"
-
-#~ msgid "csv entities export"
-#~ msgstr "Exportar entidades en csv"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "Seleccione esta liga si javascript esta desactivado"
-
 #~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
-#~ msgstr ""
-#~ "Formato de fecha y hora que se utilizará por defecto en la interfaz "
-#~ "(\"man strftime\" para mayor información del formato)"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
 #~ msgstr ""
-#~ "Formato de fecha que se utilizará por defecto en la interfaz (\"man "
-#~ "strftime\" para mayor información  del formato)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Formato de hora que se utilizará por defecto en la interfaz (\"man "
-#~ "strftime\" para mayor información del formato)"
-
-#~ msgid "instance schema"
-#~ msgstr "Esquema de la Instancia"
-
-#~ msgid "rdf"
-#~ msgstr "rdf"
-
-#~ msgid "rss"
-#~ msgstr "RSS"
-
-#~ msgid "siteinfo"
-#~ msgstr "información"
-
-#~ msgid "xbel"
-#~ msgstr "xbel"
-
-#~ msgid "xml"
-#~ msgstr "xml"
+#~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
+#~ "relación ya no existe en el esquema."
+
+#~ msgid "day"
+#~ msgstr "día"
+
+#~ msgid "log out first"
+#~ msgstr "Desconéctese primero"
+
+#~ msgid "month"
+#~ msgstr "mes"
+
+#~ msgid "today"
+#~ msgstr "hoy"
+
+#~ msgid "week"
+#~ msgstr "sem."
--- a/i18n/fr.po	Wed Aug 01 10:30:48 2012 +0200
+++ b/i18n/fr.po	Thu Mar 21 18:13:31 2013 +0100
@@ -4,7 +4,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2012-02-08 17:43+0100\n"
+"PO-Revision-Date: 2012-02-15 16:08+0100\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
 "Language: \n"
@@ -147,6 +147,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID INTROUVABLE)"
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr "entité #%d (supprimée)"
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -219,6 +223,10 @@
 msgid "About this site"
 msgstr "À propos de ce site"
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s"
+
 msgid "Any"
 msgstr "Tous"
 
@@ -264,6 +272,10 @@
 msgid "Browse by entity type"
 msgstr "Naviguer par type d'entité"
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr "Par %(user)s le %(dt)s [%(undo_link)s] "
+
 msgid "Bytes"
 msgstr "Donnée binaires"
 
@@ -390,14 +402,6 @@
 
 #, python-format
 msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"Ne peut restaurer la relation %(rtype)s de l'entité %(eid)s, cette relation "
-"n'existe plus dans le schéma"
-
-#, python-format
-msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
@@ -423,6 +427,10 @@
 msgid "Click to sort on this column"
 msgstr "Cliquer pour trier sur cette colonne"
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s crée : %(entity)s"
+
 msgid "DEBUG"
 msgstr "DEBUG"
 
@@ -448,6 +456,14 @@
 msgid "Decimal_plural"
 msgstr "Nombres décimaux"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr "Relation supprimée : %(entity_from)s %(rtype)s %(entity_to)s"
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s supprimée : %(entity)s"
+
 msgid "Detected problems"
 msgstr "Problèmes détectés"
 
@@ -849,6 +865,13 @@
 msgid "This WorkflowTransition"
 msgstr "Cette transition workflow"
 
+msgid ""
+"This action is forbidden. If you think it should be allowed, please contact "
+"the site administrator."
+msgstr ""
+"Cette action est interdite. Si toutefois vous pensez qu'elle devrait être "
+"autorisée, veuillez contacter l'administrateur du site."
+
 msgid "This entity type permissions:"
 msgstr "Permissions pour ce type d'entité"
 
@@ -875,12 +898,22 @@
 "URLs depuis lesquelles le contenu sera importé. Vous pouvez mettre une URL "
 "par ligne."
 
+msgid "Undoable actions"
+msgstr "Action annulables"
+
+msgid "Undoing"
+msgstr "Annuler"
+
 msgid "UniqueConstraint"
 msgstr "contrainte d'unicité"
 
 msgid "Unreachable objects"
 msgstr "Objets inaccessibles"
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s mise à jour : %(entity)s"
+
 msgid "Used by:"
 msgstr "Utilisé par :"
 
@@ -1277,6 +1310,9 @@
 msgid "bad value"
 msgstr "mauvaise valeur"
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr "url de base"
 
@@ -1362,6 +1398,9 @@
 msgid "can not resolve entity types:"
 msgstr "impossible d'interpréter les types d'entités :"
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr "ne peut-être modifié"
 
@@ -1398,6 +1437,28 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+"impossible de rétablir l'entité %(eid)s de type %(eschema)s, cible de la "
+"relation %(rtype)s (eid %(value)s) n'existe plus"
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+"impossible de rétablir la relation %(rtype)s sur l'entité %(eid)s, cette "
+"relation n'existe plus dans le schéma."
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+"impossible de rétablir l'état de l'entité %s, elle a été supprimée entre-"
+"temps"
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -2050,9 +2111,6 @@
 msgid "date"
 msgstr "date"
 
-msgid "day"
-msgstr "jour"
-
 msgid "deactivate"
 msgstr "désactiver"
 
@@ -2716,6 +2774,9 @@
 msgid "has_text"
 msgstr "contient le texte"
 
+msgid "header-center"
+msgstr "en-tête (centre)"
+
 msgid "header-left"
 msgstr "en-tête (gauche)"
 
@@ -3074,9 +3135,6 @@
 msgid "log in"
 msgstr "s'identifier"
 
-msgid "log out first"
-msgstr "déconnecter vous d'abord"
-
 msgid "login"
 msgstr "identifiant"
 
@@ -3178,9 +3236,6 @@
 msgid "monday"
 msgstr "lundi"
 
-msgid "month"
-msgstr "mois"
-
 msgid "more actions"
 msgstr "plus d'actions"
 
@@ -3283,6 +3338,9 @@
 msgid "new"
 msgstr "nouveau"
 
+msgid "next page"
+msgstr "page suivante"
+
 msgid "next_results"
 msgstr "résultats suivants"
 
@@ -3486,6 +3544,9 @@
 msgid "preferences"
 msgstr "préférences"
 
+msgid "previous page"
+msgstr "page précédente"
+
 msgid "previous_results"
 msgstr "résultats précédents"
 
@@ -4077,10 +4138,14 @@
 msgstr "la valeur \"%s\" est déjà utilisée, veuillez utiliser une autre valeur"
 
 msgid "there is no next page"
-msgstr "il n'y a pas de page suivante"
+msgstr "Il n'y a pas de page suivante"
 
 msgid "there is no previous page"
-msgstr "il n'y a pas de page précédente"
+msgstr "Il n'y a pas de page précédente"
+
+#, python-format
+msgid "there is no transaction #%s"
+msgstr "Il n'y a pas de transaction #%s"
 
 msgid "this action is not reversible!"
 msgstr ""
@@ -4166,9 +4231,6 @@
 msgid "to_state_object"
 msgstr "transition vers cet état"
 
-msgid "today"
-msgstr "aujourd'hui"
-
 msgid "todo_by"
 msgstr "à faire par"
 
@@ -4293,6 +4355,9 @@
 msgid "unauthorized value"
 msgstr "valeur non autorisée"
 
+msgid "undefined user"
+msgstr "utilisateur inconnu"
+
 msgid "undo"
 msgstr "annuler"
 
@@ -4320,6 +4385,9 @@
 msgid "unknown vocabulary:"
 msgstr "vocabulaire inconnu : "
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "mot de passe"
 
@@ -4526,9 +4594,6 @@
 msgid "wednesday"
 msgstr "mercredi"
 
-msgid "week"
-msgstr "semaine"
-
 #, python-format
 msgid "welcome %s !"
 msgstr "bienvenue %s !"
@@ -4628,27 +4693,23 @@
 "vous devriez enlevé la mise en ligne de la relation %s qui est supportée et "
 "peut-être croisée"
 
-#~ msgid "(loading ...)"
-#~ msgstr "(chargement ...)"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "suivez ce lien si javascript est désactivé"
-
-#~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
-#~ msgstr ""
-#~ "comment formater la date dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "comment formater la date dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "comment formater l'heure dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
+#~ msgid "Action"
+#~ msgstr "Action"
+
+#~ msgid "day"
+#~ msgstr "jour"
+
+#~ msgid "log out first"
+#~ msgstr "déconnecter vous d'abord"
+
+#~ msgid "month"
+#~ msgstr "mois"
+
+#~ msgid "today"
+#~ msgstr "aujourd'hui"
+
+#~ msgid "undo last change"
+#~ msgstr "annuler dernier changement"
+
+#~ msgid "week"
+#~ msgstr "semaine"
--- a/mail.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/mail.py	Thu Mar 21 18:13:31 2013 +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,7 +15,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/>.
-"""Common utilies to format / semd emails."""
+"""Common utilies to format / send emails."""
 
 __docformat__ = "restructuredtext en"
 
--- a/migration.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/migration.py	Thu Mar 21 18:13:31 2013 +0100
@@ -514,7 +514,9 @@
                     elif op == None:
                         continue
                     else:
-                        print 'unable to handle this case', oper, version, op, ver
+                        print ('unable to handle %s in %s, set to `%s %s` '
+                               'but currently up to `%s %s`' %
+                               (cube, source, oper, version, op, ver))
             # "solve" constraint satisfaction problem
             if cube not in self.cubes:
                 self.errors.append( ('add', cube, version, source) )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.0_Any.py	Thu Mar 21 18:13:31 2013 +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/migration/3.15.0_common.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,7 @@
+import ConfigParser
+try:
+    undo_actions = config.cfgfile_parser.get('MAIN', 'undo-support', False)
+except ConfigParser.NoOptionError:
+    pass # this conf. file was probably already migrated
+else:
+    config.global_set_option('undo-enabled', bool(undo_actions))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.4_Any.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,11 @@
+from logilab.common.shellutils import generate_password
+from cubicweb.server.utils import crypt_password
+
+for user in rql('CWUser U WHERE U cw_source S, S name "system", U upassword P, U login L').entities():
+    salt = user.upassword.getvalue()
+    if crypt_password('', salt) == salt:
+        passwd = generate_password()
+        print 'setting random password for user %s' % user.login
+        user.set_attributes(upassword=passwd)
+
+commit()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.9_Any.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,2 @@
+sync_schema_props_perms(('State', 'state_of', 'Workflow'), commit=False)
+sync_schema_props_perms(('State', 'name', 'String'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/ldapuser2ldapfeed.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,99 @@
+"""turn a pyro source into a datafeed source
+
+Once this script is run, execute c-c db-check to cleanup relation tables.
+"""
+import sys
+from collections import defaultdict
+from logilab.common.shellutils import generate_password
+
+try:
+    source_name, = __args__
+    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 = defaultdict(list)
+extids = set()
+duplicates = []
+for entity in rql('Any X WHERE X cw_source S, S eid %(s)s', {'s': source.eid}).entities():
+    etype = entity.__regid__
+    if not source.support_entity(etype):
+        print "source doesn't support %s, delete %s" % (etype, entity.eid)
+        todelete[etype].append(entity)
+        continue
+    try:
+        entity.complete()
+    except Exception:
+        print '%s %s much probably deleted, delete it (extid %s)' % (
+            etype, entity.eid, entity.cw_metainformation()['extid'])
+        todelete[etype].append(entity)
+        continue
+    print 'get back', etype, entity.eid
+    entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
+    if not entity.creation_date:
+        entity.cw_edited['creation_date'] = datetime.now()
+    if not entity.modification_date:
+        entity.cw_edited['modification_date'] = datetime.now()
+    if not entity.upassword:
+        entity.cw_edited['upassword'] = generate_password()
+    extid = entity.cw_metainformation()['extid']
+    if not entity.cwuri:
+        entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
+            source.urls[0], extid.decode('utf-8', 'ignore'))
+    print entity.cw_edited
+    if extid in extids:
+        duplicates.append(extid)
+        continue
+    extids.add(extid)
+    system_source.add_entity(session, entity)
+    sql("UPDATE entities SET source='system' "
+        "WHERE eid=%(eid)s", {'eid': entity.eid})
+
+# only cleanup entities table, remaining stuff should be cleaned by a c-c
+# db-check to be run after this script
+if duplicates:
+    print 'found %s duplicate entries' % len(duplicates)
+    from pprint import pprint
+    pprint(duplicates)
+
+print len(todelete), 'entities will be deleted'
+for etype, entities in todelete.iteritems():
+    print 'deleting', etype, [e.login for e in entities]
+    system_source.delete_info_multi(session, entities, source_name)
+
+
+
+source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0)
+source_ent.set_attributes(type=u"ldapfeed", parser=u"ldapfeed")
+
+
+if raw_input('Commit ?') in 'yY':
+    print 'committing'
+    commit()
+else:
+    rollback()
+    print 'rollbacked'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/repair_splitbrain_ldapuser_source.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,108 @@
+"""
+CAUTION: READ THIS CAREFULLY
+
+Sometimes it happens that ldap (specifically ldapuser type) source
+yield "ghost" users. The reasons may vary (server upgrade while some
+instances are still running & syncing with the ldap source, unmanaged
+updates to the upstream ldap, etc.).
+
+This script was written and refined enough times that we are confident
+in that it does something reasonnable (at least it did for the
+target application).
+
+However you should really REALLY understand what it does before
+deciding to apply it for you. And then ADAPT it tou your needs.
+
+"""
+
+import base64
+from collections import defaultdict
+
+from cubicweb.server.session import hooks_control
+
+try:
+    source_name, = __args__
+    source = repo.sources_by_uri[source_name]
+except ValueError:
+    print('you should specify the source name as script argument (i.e. after --'
+          ' on the command line)')
+    sys.exit(1)
+except KeyError:
+    print '%s is not an active source' % source_name
+    sys.exit(1)
+
+# check source is reachable before doing anything
+if not source.get_connection().cnx:
+    print '%s is not reachable. Fix this before running this script' % source_name
+    sys.exit(1)
+
+def find_dupes():
+    # XXX this retrieves entities from a source name "ldap"
+    #     you will want to adjust
+    rset = sql("SELECT eid, extid FROM entities WHERE source='%s'" % source_name)
+    extid2eids = defaultdict(list)
+    for eid, extid in rset:
+        extid2eids[extid].append(eid)
+    return dict((base64.b64decode(extid).lower(), eids)
+                for extid, eids in extid2eids.items()
+                if len(eids) > 1)
+
+def merge_dupes(dupes, docommit=False):
+    gone_eids = []
+    CWUser = schema['CWUser']
+    for extid, eids in dupes.items():
+        newest = eids.pop() # we merge everything on the newest
+        print 'merging ghosts of', extid, 'into', newest
+        # now we merge pairwise into the newest
+        for old in eids:
+            subst = {'old': old, 'new': newest}
+            print '  merging', old
+            gone_eids.append(old)
+            for rschema in CWUser.subject_relations():
+                if rschema.final or rschema == 'identity':
+                    continue
+                if CWUser.rdef(rschema, 'subject').composite == 'subject':
+                    # old 'composite' property is wiped ...
+                    # think about email addresses, excel preferences
+                    for eschema in rschema.objects():
+                        rql('DELETE %s X WHERE U %s X, U eid %%(old)s' % (eschema, rschema), subst)
+                else:
+                    # relink the new user to its old relations
+                    rql('SET NU %s X WHERE NU eid %%(new)s, NOT NU %s X, OU %s X, OU eid %%(old)s' %
+                        (rschema, rschema, rschema), subst)
+                    # delete the old relations
+                    rql('DELETE U %s X WHERE U eid %%(old)s' % rschema, subst)
+            # same thing ...
+            for rschema in CWUser.object_relations():
+                if rschema.final or rschema == 'identity':
+                    continue
+                rql('SET X %s NU WHERE NU eid %%(new)s, NOT X %s NU, X %s OU, OU eid %%(old)s' %
+                    (rschema, rschema, rschema), subst)
+                rql('DELETE X %s U WHERE U eid %%(old)s' % rschema, subst)
+    if not docommit:
+        rollback()
+        return
+    commit() # XXX flushing operations is wanted rather than really committing
+    print 'clean up entities table'
+    sql('DELETE FROM entities WHERE eid IN (%s)' % (', '.join(str(x) for x in gone_eids)))
+    commit()
+
+def main():
+    dupes = find_dupes()
+    if not dupes:
+        print 'No duplicate user'
+        return
+
+    print 'Found %s duplicate user instances' % len(dupes)
+
+    while True:
+        print 'Fix or dry-run? (f/d)  ... or Ctrl-C to break out'
+        answer = raw_input('> ')
+        if answer.lower() not in 'fd':
+            continue
+        print 'Please STOP THE APPLICATION INSTANCES (service or interactive), and press Return when done.'
+        raw_input('<I swear all running instances and workers of the application are stopped>')
+        with hooks_control(session, session.HOOKS_DENY_ALL):
+            merge_dupes(dupes, docommit=answer=='f')
+
+main()
--- a/mixins.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/mixins.py	Thu Mar 21 18:13:31 2013 +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 Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,1570 @@
+# 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,
+                 entity=None, **kwargs):
+        if not rset and entity is None:
+            return 0
+        score = 0
+        if entity is not None:
+            score = self.score_entity(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:
+        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 len(rset) == 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 len(rset) == 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(len(rset)))
+
+
+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. Also don't use rset.rows so
+        # this selector will work if rset is a simple list of list.
+        return rset and self.match_expected(len(rset[0])) or 0
+
+
+class paginated_rset(Predicate):
+    """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 len(rset) <= (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, entity=None, **kwargs):
+        if entity is not None:
+            return self.score_entity(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	Wed Aug 01 10:30:48 2012 +0200
+++ b/pylintext.py	Thu Mar 21 18:13:31 2013 +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/req.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/req.py	Thu Mar 21 18:13:31 2013 +0100
@@ -75,6 +75,9 @@
         self.local_perm_cache = {}
         self._ = unicode
 
+    def get_option_value(self, option, foreid=None):
+        raise NotImplementedError
+
     def property_value(self, key):
         """return value of the property with the given key, giving priority to
         user specific value if any, else using site value
@@ -204,6 +207,9 @@
         parameters. Values are automatically URL quoted, and the
         publishing method to use may be specified or will be guessed.
 
+        if ``__secure__`` argument is True, the request will try to build a
+        https url.
+
         raises :exc:`ValueError` if None is found in arguments
         """
         # use *args since we don't want first argument to be "anonymous" to
@@ -222,7 +228,8 @@
                 method = 'view'
         base_url = kwargs.pop('base_url', None)
         if base_url is None:
-            base_url = self.base_url()
+            secure = kwargs.pop('__secure__', None)
+            base_url = self.base_url(secure=secure)
         if '_restpath' in kwargs:
             assert method == 'view', method
             path = kwargs.pop('_restpath')
@@ -415,8 +422,11 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
-    def base_url(self):
-        """return the root url of the instance"""
+    def base_url(self, secure=None):
+        """return the root url of the instance
+        """
+        if secure:
+            raise NotImplementedError()
         return self.vreg.config['base-url']
 
     # abstract methods to override according to the web front-end #############
--- a/schema.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/schema.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/schemas/base.py	Thu Mar 21 18:13:31 2013 +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/schemas/workflow.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/schemas/workflow.py	Thu Mar 21 18:13:31 2013 +0100
@@ -66,7 +66,7 @@
     name = String(required=True, indexed=True, internationalizable=True,
                   maxsize=256,
                   constraints=[RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N', 'Y',
-                                                   _('workflow already have a state of that name'))])
+                                                   _('workflow already has a state of that name'))])
     description = RichString(default_format='text/rest',
                              description=_('semantic description of this state'))
 
@@ -79,7 +79,7 @@
     state_of = SubjectRelation('Workflow', cardinality='1*', composite='object',
                                description=_('workflow to which this state belongs'),
                                constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N', 'Y',
-                                                                _('workflow already have a state of that name'))])
+                                                                _('workflow already has a state of that name'))])
 
 
 class BaseTransition(EntityType):
--- a/selectors.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/selectors.py	Thu Mar 21 18:13:31 2013 +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,1604 +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. Also don't use rset.rows so
-        # this selector will work if rset is a simple list of list.
-        return rset and self.match_expected(len(rset[0])) or 0
-
-
-class paginated_rset(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):
@@ -1621,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)
 
@@ -1655,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
     """
@@ -1675,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/__init__.py	Thu Mar 21 18:13:31 2013 +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.
@@ -31,10 +31,12 @@
 
 from logilab.common.modutils import LazyObject
 from logilab.common.textutils import splitstrip
+from logilab.common.registry import yes
 
 from yams import BASE_GROUPS
 
 from cubicweb import CW_SOFTWARE_ROOT
+from cubicweb.appobject import AppObject
 
 class ShuttingDown(BaseException):
     """raised when trying to access some resources while the repository is
@@ -42,7 +44,26 @@
     catch it.
     """
 
-# server-side debugging #########################################################
+# server-side services #########################################################
+
+class Service(AppObject):
+    """Base class for services.
+
+    A service is a selectable object that performs an action server-side.
+    Use :class:`cubicweb.dbapi.Connection.call_service` to call them from
+    the web-side.
+
+    When inheriting this class, do not forget to define at least the __regid__
+    attribute (and probably __select__ too).
+    """
+    __registry__ = 'services'
+    __select__ = yes()
+
+    def call(self, **kwargs):
+        raise NotImplementedError
+
+
+# server-side debugging ########################################################
 
 # server debugging flags. They may be combined using binary operators.
 
@@ -271,7 +292,9 @@
 
 # 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'),
+                'zmqrql': LazyObject('cubicweb.server.sources.zmqrql', 'ZMQRQLSource'),
                 }
--- a/server/checkintegrity.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/checkintegrity.py	Thu Mar 21 18:13:31 2013 +0100
@@ -134,10 +134,12 @@
     # attribute to their current value
     source = repo.system_source
     for eschema in etypes:
-        rset = session.execute('Any X WHERE X is %s' % eschema)
-        source.fti_index_entities(session, rset.entities())
-        # clear entity cache to avoid high memory consumption on big tables
-        session.drop_entity_cache()
+        etype_class = session.vreg['etypes'].etype_class(str(eschema))
+        for fti_rql in etype_class.cw_fti_index_rql_queries(session):
+            rset = session.execute(fti_rql)
+            source.fti_index_entities(session, rset.entities())
+            # clear entity cache to avoid high memory consumption on big tables
+            session.drop_entity_cache()
         if withpb:
             pb.update()
 
@@ -315,7 +317,7 @@
     print 'Checking mandatory relations'
     msg = '%s #%s is missing mandatory %s relation %s (autofix will delete the entity)'
     for rschema in schema.relations():
-        if rschema.final or rschema.type in PURE_VIRTUAL_RTYPES:
+        if rschema.final or rschema in PURE_VIRTUAL_RTYPES or rschema in ('is', 'is_instance_of'):
             continue
         smandatory = set()
         omandatory = set()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/cwzmq.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,257 @@
+# -*- 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 cPickle
+import traceback
+
+import zmq
+from zmq.eventloop import ioloop
+import zmq.eventloop.zmqstream
+
+from logging import getLogger
+from cubicweb import set_log_methods
+from cubicweb.server.server import QuitEvent
+
+ctx = zmq.Context()
+
+class ZMQComm(object):
+    """
+    A simple ZMQ-based notification bus.
+
+    There should at most one instance of this class attached to a
+    Repository. A typical usage may be something like::
+
+        def callback(msg):
+            self.info('received message: %s', ' '.join(msg))
+        repo.app_instances_bus.subscribe('hello', callback)
+
+    to subsribe to the 'hello' kind of message. On the other side, to
+    emit a notification, call::
+
+       repo.app_instances_bus.publish(['hello', 'world'])
+
+    See http://docs.cubicweb.org for more details.
+    """
+    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))
+
+
+class ZMQRepositoryServer(object):
+
+    def __init__(self, repository):
+        """make the repository available as a PyRO object"""
+        self.address = None
+        self.repo = repository
+        self.socket = None
+        self.stream = None
+        self.loop = ioloop.IOLoop()
+
+        # event queue
+        self.events = []
+
+    def connect(self, address):
+        self.address = address
+
+    def run(self):
+        """enter the service loop"""
+        # start repository looping tasks
+        self.socket = ctx.socket(zmq.REP)
+        self.stream = zmq.eventloop.zmqstream.ZMQStream(self.socket, io_loop=self.loop)
+        self.stream.bind(self.address)
+        self.info('ZMQ server bound on: %s', self.address)
+
+        self.stream.on_recv(self.process_cmds)
+
+        try:
+            self.loop.start()
+        except zmq.ZMQError:
+            self.warning('ZMQ event loop killed')
+        self.quit()
+
+    def trigger_events(self):
+        """trigger ready events"""
+        for event in self.events[:]:
+            if event.is_ready():
+                self.info('starting event %s', event)
+                event.fire(self)
+                try:
+                    event.update()
+                except Finished:
+                    self.events.remove(event)
+
+    def process_cmd(self, cmd):
+        """Delegate the given command to the repository.
+
+        ``cmd`` is a list of (method_name, args, kwargs)
+        where ``args`` is a list of positional arguments
+        and ``kwargs`` is a dictionnary of named arguments.
+
+        >>> rset = delegate_to_repo(["execute", [sessionid], {'rql': rql}])
+
+        :note1: ``kwargs`` may be ommited
+
+            >>> rset = delegate_to_repo(["execute", [sessionid, rql]])
+
+        :note2: both ``args`` and ``kwargs`` may be omitted
+
+            >>> schema = delegate_to_repo(["get_schema"])
+            >>> schema = delegate_to_repo("get_schema") # also allowed
+
+        """
+        cmd = cPickle.loads(cmd)
+        if not cmd:
+            raise AttributeError('function name required')
+        if isinstance(cmd, basestring):
+            cmd = [cmd]
+        if len(cmd) < 2:
+            cmd.append(())
+        if len(cmd) < 3:
+            cmd.append({})
+        cmd  = list(cmd) + [(), {}]
+        funcname, args, kwargs = cmd[:3]
+        result = getattr(self.repo, funcname)(*args, **kwargs)
+        return result
+
+    def process_cmds(self, cmds):
+        """Callback intended to be used with ``on_recv``.
+
+        Call ``delegate_to_repo`` on each command and send a pickled of
+        each result recursively.
+
+        Any exception are catched, pickled and sent.
+        """
+        try:
+            for cmd in cmds:
+                result = self.process_cmd(cmd)
+                self.send_data(result)
+        except Exception, exc:
+            traceback.print_exc()
+            self.send_data(exc)
+
+    def send_data(self, data):
+        self.socket.send_pyobj(data)
+
+    def quit(self, shutdown_repo=False):
+        """stop the server"""
+        self.info('Quitting ZMQ server')
+        try:
+            self.loop.add_callback(self.loop.stop)
+            self.stream.on_recv(None)
+            self.stream.close()
+        except Exception, e:
+            print e
+            pass
+        if shutdown_repo and not self.repo.shutting_down:
+            event = QuitEvent()
+            event.fire(self)
+
+    # server utilitities ######################################################
+
+    def install_sig_handlers(self):
+        """install signal handlers"""
+        import signal
+        self.info('installing signal handlers')
+        signal.signal(signal.SIGINT, lambda x, y, s=self: s.quit(shutdown_repo=True))
+        signal.signal(signal.SIGTERM, lambda x, y, s=self: s.quit(shutdown_repo=True))
+
+
+    # these are overridden by set_log_methods below
+    # only defining here to prevent pylint from complaining
+    @classmethod
+    def info(cls, msg, *a, **kw):
+        pass
+
+
+set_log_methods(Publisher, getLogger('cubicweb.zmq.pub'))
+set_log_methods(Subscriber, getLogger('cubicweb.zmq.sub'))
+set_log_methods(ZMQRepositoryServer, getLogger('cubicweb.zmq.repo'))
--- a/server/hook.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/hook.py	Thu Mar 21 18:13:31 2013 +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.
@@ -174,14 +174,17 @@
 Non data events
 ~~~~~~~~~~~~~~~
 
-Hooks called on server start/maintenance/stop event (eg `server_startup`,
-`server_maintenance`, `server_shutdown`) have a `repo` attribute, but *their
-`_cw` attribute is None*.  The `server_startup` is called on regular startup,
-while `server_maintenance` is called on cubicweb-ctl upgrade or shell
-commands. `server_shutdown` is called anyway.
+Hooks called on server start/maintenance/stop event (e.g.
+`server_startup`, `server_maintenance`, `before_server_shutdown`,
+`server_shutdown`) have a `repo` attribute, but *their `_cw` attribute
+is None*.  The `server_startup` is called on regular startup, while
+`server_maintenance` is called on cubicweb-ctl upgrade or shell
+commands. `server_shutdown` is called anyway but connections to the
+native source is impossible; `before_server_shutdown` handles that.
 
-Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have
-a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*.
+Hooks called on backup/restore event (eg `server_backup`,
+`server_restore`) have a `repo` and a `timestamp` attributes, but
+*their `_cw` attribute is None*.
 
 Hooks called on session event (eg `session_open`, `session_close`) have no
 special attribute.
@@ -233,8 +236,8 @@
 or rollback() will restore the hooks.
 
 
-Hooks specific selector
-~~~~~~~~~~~~~~~~~~~~~~~
+Hooks specific predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. autoclass:: cubicweb.server.hook.match_rtype
 .. autoclass:: cubicweb.server.hook.match_rtype_sets
 
@@ -258,13 +261,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',
@@ -273,7 +276,8 @@
 RELATIONS_HOOKS = set(('before_add_relation',   'after_add_relation' ,
                        'before_delete_relation','after_delete_relation'))
 SYSTEM_HOOKS = set(('server_backup', 'server_restore',
-                    'server_startup', 'server_maintenance', 'server_shutdown',
+                    'server_startup', 'server_maintenance',
+                    'server_shutdown', 'before_server_shutdown',
                     'session_open', 'session_close'))
 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
 
@@ -328,7 +332,7 @@
                                    key=lambda x: x.order)
                     with security_enabled(session, write=False):
                         for hook in hooks:
-                           hook()
+                            hook()
 
     def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs):
         """return a set of hooks that should not be considered by filtered_possible objects
@@ -338,14 +342,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 +415,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 +443,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 +456,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,21 +468,23 @@
         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()
+    .. sourcecode:: python
+
+      MYSET = set()
 
-    class Hook1(Hook):
-        __regid__ = 'hook1'
-        __select__ = Hook.__select__ & match_rtype_sets(MYSET)
-        ...
+      class Hook1(Hook):
+          __regid__ = 'hook1'
+          __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+          ...
 
-    class Hook2(Hook):
-        __regid__ = 'hook2'
-        __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+      class Hook2(Hook):
+          __regid__ = 'hook2'
+          __select__ = Hook.__select__ & match_rtype_sets(MYSET)
 
     Client code can now change `MYSET`, this will changes the selection criteria
     of :class:`Hook1` and :class:`Hook1`.
@@ -489,7 +493,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 +538,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))
@@ -554,9 +557,14 @@
             raise Exception('bad .events attribute %s on %s.%s' % (
                 cls.events, cls.__module__, cls.__name__))
 
+    @classmethod
+    def __registered__(cls, reg):
+        cls.check_events()
+
     @classproperty
     def __registries__(cls):
-        cls.check_events()
+        if cls.events is None:
+            return []
         return ['%s_hooks' % ev for ev in cls.events]
 
     known_args = set(('entity', 'rtype', 'eidfrom', 'eidto', 'repo', 'timestamp'))
@@ -583,7 +591,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 +1075,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 Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,355 @@
+# 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, Binary
+from cubicweb.server import utils
+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', 'userPassword': 'upassword'},
+          'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
+          'group': 'ldap-source', 'level': 1,
+          }),
+
+    )
+
+    _conn = None
+
+    def _entity_update(self, source_entity):
+        if self.urls:
+            if len(self.urls) > 1:
+                raise ValidationError(source_entity.eid, {'url': _('can only have one url')})
+            try:
+                protocol, hostport = self.urls[0].split('://')
+            except ValueError:
+                raise ValidationError(source_entity.eid, {'url': _('badly formatted url')})
+            if protocol not in PROTO_PORT:
+                raise ValidationError(source_entity.eid, {'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, ldap.SERVER_DOWN):
+            # 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 _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
+        # Required for AD
+        try:
+           conn.set_option(ldap.OPT_REFERRALS, 0)
+        except ldap.LDAPError: # Cannot set referrals, so do nothing
+           pass
+        #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
+        #conn.timeout = op_timeout
+        # Now bind with the credentials given. Let exceptions propagate out.
+        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 self.user_attrs.get(key) == 'upassword': # XXx better password detection
+                value = value[0].encode('utf-8')
+                # we only support ldap_salted_sha1 for ldap sources, see: server/utils.py
+                if not value.startswith('{SSHA}'):
+                    value = utils.crypt_password(value)
+                itemdict[key] = Binary(value)
+            else:
+                for i, val in enumerate(value):
+                    value[i] = unicode(val, 'utf-8', 'replace')
+                if isinstance(value, list) and len(value) == 1:
+                    itemdict[key] = value = value[0]
+        return itemdict
+
+    def _process_no_such_object(self, session, dn):
+        """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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/migractions.py	Thu Mar 21 18:13:31 2013 +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(
@@ -1058,7 +1061,7 @@
                         rdef = copy(rschema.rdef(rschema.subjects(objtype)[0], objtype))
                         rdef.subject = etype
                         rdef.rtype = self.repo.schema.rschema(rschema)
-                        rdef.object = self.repo.schema.rschema(objtype)
+                        rdef.object = self.repo.schema.eschema(objtype)
                         ss.execschemarql(execute, rdef,
                                          ss.rdef2rql(rdef, cmap, gmap))
         if commit:
@@ -1072,7 +1075,7 @@
         if commit:
             self.commit()
 
-    def cmd_rename_relation(self, oldname, newname, commit=True):
+    def cmd_rename_relation_type(self, oldname, newname, commit=True):
         """rename an existing relation
 
         `oldname` is a string giving the name of the existing relation
@@ -1465,7 +1468,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
@@ -1523,6 +1526,10 @@
     def cmd_reactivate_verification_hooks(self):
         self.session.enable_hook_categories('integrity')
 
+    @deprecated("[3.15] use rename_relation_type(oldname, newname)")
+    def cmd_rename_relation(self, oldname, newname, commit=True):
+        self.cmd_rename_relation_type(oldname, newname, commit)
+
 
 class ForRqlIterator:
     """specific rql iterator to make the loop skipable"""
--- a/server/mssteps.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/mssteps.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/repository.py	Thu Mar 21 18:13:31 2013 +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.
@@ -65,7 +65,7 @@
                            ('cw_source', 'object'),
                            ])
 
-def prefill_entity_caches(entity, relations):
+def prefill_entity_caches(entity):
     session = entity._cw
     # prefill entity relation caches
     for rschema in entity.e_schema.subject_relations():
@@ -120,6 +120,41 @@
                             {'x': eidfrom, 'y': eidto})
 
 
+def preprocess_inlined_relations(session, entity):
+    """when an entity is added, check if it has some inlined relation which
+    requires to be extrated for proper call hooks
+    """
+    relations = []
+    activeintegrity = session.is_hook_category_activated('activeintegrity')
+    eschema = entity.e_schema
+    for attr in entity.cw_edited.iterkeys():
+        rschema = eschema.subjrels[attr]
+        if not rschema.final: # inlined relation
+            value = entity.cw_edited[attr]
+            relations.append((attr, value))
+            session.update_rel_cache_add(entity.eid, attr, value)
+            rdef = session.rtype_eids_rdef(attr, entity.eid, value)
+            if rdef.cardinality[1] in '1?' and activeintegrity:
+                with security_enabled(session, read=False):
+                    session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
+                                    {'x': entity.eid, 'y': value})
+    return relations
+
+
+class NullEventBus(object):
+    def publish(self, msg):
+        pass
+
+    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
@@ -127,18 +162,22 @@
     XXX protect pyro access
     """
 
-    def __init__(self, config, vreg=None):
+    def __init__(self, config, tasks_manager=None, vreg=None):
         self.config = config
         if vreg is None:
-            vreg = cwvreg.CubicWebVRegistry(config)
+            vreg = cwvreg.CWRegistryStore(config)
         self.vreg = vreg
+        self._tasks_manager = tasks_manager
+
         self.pyro_registered = False
         self.pyro_uri = None
+        self.app_instances_bus = NullEventBus()
         self.info('starting repository from %s', self.config.apphome)
         # dictionary of opened sessions
         self._sessions = {}
+
+
         # list of functions to be called at regular interval
-        self._looping_tasks = []
         # list of running threads
         self._running_threads = []
         # initial schema, should be build or replaced latter
@@ -162,6 +201,9 @@
             self.init_cnxset_pool()
         @onevent('after-registry-reload', self)
         def fix_user_classes(self):
+            # After registery reload the 'CWUser' class used for CWEtype
+            # changed.  To any existing user object have a different class than
+            # the new loaded one. We are hot fixing this.
             usercls = self.vreg['etypes'].etype_class('CWUser')
             for session in self._sessions.values():
                 if not isinstance(session.user, InternalManager):
@@ -232,8 +274,7 @@
                or not 'CWSource' in self.schema: # # 3.10 migration
             self.system_source.init_creating()
             return
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             # FIXME: sources should be ordered (add_entity priority)
             for sourceent in session.execute(
                 'Any S, SN, SA, SC WHERE S is_instance_of CWSource, '
@@ -244,8 +285,6 @@
                     self.system_source.init(True, sourceent)
                     continue
                 self.add_source(sourceent, add_to_cnxsets=False)
-        finally:
-            session.close()
 
     def _clear_planning_caches(self):
         for cache in ('source_defs', 'is_multi_sources_relation',
@@ -309,14 +348,13 @@
         self.schema = schema
 
     def fill_schema(self):
-        """lod schema from the repository"""
+        """load schema from the repository"""
         from cubicweb.server.schemaserial import deserialize_schema
         self.info('loading schema from the repository')
         appschema = schema.CubicWebSchema(self.config.appid)
         self.set_schema(self.config.load_bootstrap_schema(), resetvreg=False)
         self.debug('deserializing db schema into %s %#x', appschema.name, id(appschema))
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             try:
                 deserialize_schema(appschema, session)
             except BadSchemaDefinition:
@@ -327,11 +365,15 @@
                 raise Exception('Is the database initialised ? (cause: %s)' %
                                 (ex.args and ex.args[0].strip() or 'unknown')), \
                                 None, sys.exc_info()[-1]
-        finally:
-            session.close()
         self.set_schema(appschema)
 
-    def start_looping_tasks(self):
+
+    def _prepare_startup(self):
+        """Prepare "Repository as a server" for startup.
+
+        * trigger server startup hook,
+        * register session clean up task.
+        """
         if not (self.config.creating or self.config.repairing
                 or self.config.quick_start):
             # call instance level initialisation hooks
@@ -340,15 +382,23 @@
             self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24
             assert self.cleanup_session_time > 0
             cleanup_session_interval = min(60*60, self.cleanup_session_time / 3)
-            self.looping_task(cleanup_session_interval, self.clean_sessions)
-        assert isinstance(self._looping_tasks, list), 'already started'
-        for i, (interval, func, args) in enumerate(self._looping_tasks):
-            self._looping_tasks[i] = task = utils.LoopTask(self, interval, func, args)
-            self.info('starting task %s with interval %.2fs', task.name,
-                      interval)
-            task.start()
-        # ensure no tasks will be further added
-        self._looping_tasks = tuple(self._looping_tasks)
+            assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
+            self._tasks_manager.add_looping_task(cleanup_session_interval,
+                                                 self.clean_sessions)
+
+    def start_looping_tasks(self):
+        """Actual "Repository as a server" startup.
+
+        * trigger server startup hook,
+        * register session clean up task,
+        * start all tasks.
+
+        XXX Other startup related stuffs are done elsewhere. In Repository
+        XXX __init__ or in external codes (various server managers).
+        """
+        self._prepare_startup()
+        assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
+        self._tasks_manager.start()
 
     def looping_task(self, interval, func, *args):
         """register a function to be called every `interval` seconds.
@@ -356,15 +406,12 @@
         looping tasks can only be registered during repository initialization,
         once done this method will fail.
         """
-        try:
-            self._looping_tasks.append( (interval, func, args) )
-        except AttributeError:
-            raise RuntimeError("can't add looping task once the repository is started")
+        assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
+        self._tasks_manager.add_looping_task(interval, func, *args)
 
     def threaded_task(self, func):
         """start function in a separated thread"""
-        t = utils.RepoThread(func, self._running_threads)
-        t.start()
+        utils.RepoThread(func, self._running_threads).start()
 
     #@locked
     def _get_cnxset(self):
@@ -392,21 +439,21 @@
         connections
         """
         assert not self.shutting_down, 'already shutting down'
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
+            # then, the system source is still available
+            self.hm.call_hooks('before_server_shutdown', repo=self)
         self.shutting_down = True
         self.system_source.shutdown()
-        if isinstance(self._looping_tasks, tuple): # if tasks have been started
-            for looptask in self._looping_tasks:
-                self.info('canceling task %s...', looptask.name)
-                looptask.cancel()
-                looptask.join()
-                self.info('task %s finished', looptask.name)
+        if self._tasks_manager is not None:
+            self._tasks_manager.stop()
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
+            self.hm.call_hooks('server_shutdown', repo=self)
         for thread in self._running_threads:
             self.info('waiting thread %s...', thread.getName())
             thread.join()
             self.info('thread %s finished', thread.getName())
-        if not (self.config.creating or self.config.repairing
-                or self.config.quick_start):
-            self.hm.call_hooks('server_shutdown', repo=self)
         self.close_sessions()
         while not self._cnxsets_pool.empty():
             cnxset = self._cnxsets_pool.get_nowait()
@@ -436,8 +483,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:
@@ -497,7 +546,8 @@
         results['sql_no_cache'] = self.system_source.no_cache
         results['nb_open_sessions'] = len(self._sessions)
         results['nb_active_threads'] = threading.activeCount()
-        results['looping_tasks'] = ', '.join(str(t) for t in self._looping_tasks)
+        looping_tasks = self._tasks_manager._looping_tasks
+        results['looping_tasks'] = ', '.join(str(t) for t in looping_tasks)
         results['available_cnxsets'] = self._cnxsets_pool.qsize()
         results['threads'] = ', '.join(sorted(str(t) for t in threading.enumerate()))
         return results
@@ -595,8 +645,7 @@
         """
         from logilab.common.changelog import Version
         vcconf = {}
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             for pk, version in session.execute(
                 'Any K,V WHERE P is CWProperty, P value V, P pkey K, '
                 'P pkey ~="system.version.%"', build_descr=False):
@@ -615,8 +664,6 @@
                         msg = ('instance has %s version %s but %s '
                                'is installed. Run "cubicweb-ctl upgrade".')
                         raise ExecutionError(msg % (cube, version, fsversion))
-        finally:
-            session.close()
         return vcconf
 
     @cached
@@ -637,14 +684,11 @@
 
         This is a public method, not requiring a session id.
         """
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             # don't use session.execute, we don't want rset.req set
             return self.querier.execute(session, 'Any K,V WHERE P is CWProperty,'
                                         'P pkey K, P value V, NOT P for_user U',
                                         build_descr=False)
-        finally:
-            session.close()
 
     # XXX protect this method: anonymous should be allowed and registration
     # plugged
@@ -653,10 +697,9 @@
         given password. This method is designed to be used for anonymous
         registration on public web site.
         """
-        session = self.internal_session()
-        # for consistency, keep same error as unique check hook (although not required)
-        errmsg = session._('the value "%s" is already used, use another one')
-        try:
+        with self.internal_session() as session:
+            # for consistency, keep same error as unique check hook (although not required)
+            errmsg = session._('the value "%s" is already used, use another one')
             if (session.execute('CWUser X WHERE X login %(login)s', {'login': login},
                                 build_descr=False)
                 or session.execute('CWUser X WHERE X use_email C, C address %(login)s',
@@ -683,8 +726,6 @@
                                 'U primary_email X, U use_email X '
                                 'WHERE U login %(login)s', d, build_descr=False)
             session.commit()
-        finally:
-            session.close()
         return True
 
     def find_users(self, fetch_attrs, **query_attrs):
@@ -707,8 +748,7 @@
         for k in chain(fetch_attrs, query_attrs.iterkeys()):
             if k not in cwuserattrs:
                 raise Exception('bad input for find_user')
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             varmaker = rqlvar_maker()
             vars = [(attr, varmaker.next()) for attr in fetch_attrs]
             rql = 'Any %s WHERE X is CWUser, ' % ','.join(var[1] for var in vars)
@@ -717,8 +757,6 @@
                                                   for attr in query_attrs.iterkeys()),
                                    query_attrs)
             return rset.rows
-        finally:
-            session.close()
 
     def connect(self, login, **kwargs):
         """open a connection for a given user
@@ -730,13 +768,10 @@
         raise `ConnectionError` if we can't open a connection
         """
         # use an internal connection
-        session = self.internal_session()
-        # try to get a user object
-        cnxprops = kwargs.pop('cnxprops', None)
-        try:
+        with self.internal_session() as session:
+            # try to get a user object
+            cnxprops = kwargs.pop('cnxprops', None)
             user = self.authenticate_user(session, login, **kwargs)
-        finally:
-            session.close()
         session = Session(user, self, cnxprops)
         user._cw = user.cw_rset.req = session
         user.cw_clear_relation_cache()
@@ -861,6 +896,25 @@
         del self._sessions[sessionid]
         self.info('closed session %s for user %s', sessionid, session.user.login)
 
+    def call_service(self, sessionid, regid, async, **kwargs):
+        """
+        See :class:`cubicweb.dbapi.Connection.call_service`
+        and :class:`cubicweb.server.Service`
+        """
+        def task():
+            session = self._get_session(sessionid, setcnxset=True)
+            service = session.vreg['services'].select(regid, session, **kwargs)
+            try:
+                return service.call(**kwargs)
+            finally:
+                session.rollback() # free cnxset
+        if async:
+            self.info('calling service %s asynchronously', regid)
+            self.threaded_task(task)
+        else:
+            self.info('calling service %s synchronously', regid)
+            return task()
+
     def user_info(self, sessionid, props=None):
         """this method should be used by client to:
         * check session id validity
@@ -930,14 +984,11 @@
         * list of (etype, eid) of entities of the given types which have been
           deleted since the given timestamp
         """
-        session = self.internal_session()
-        updatetime = datetime.utcnow()
-        try:
+        with self.internal_session() as session:
+            updatetime = datetime.utcnow()
             modentities, delentities = self.system_source.modified_entities(
                 session, etypes, mtime)
             return updatetime, modentities, delentities
-        finally:
-            session.close()
 
     # session handling ########################################################
 
@@ -1322,7 +1373,6 @@
         entity._cw_is_saved = False # entity has an eid but is not yet saved
         # init edited_attributes before calling before_add_entity hooks
         entity.cw_edited = edited
-        eschema = entity.e_schema
         source = self.locate_etype_source(entity.__regid__)
         # allocate an eid to the entity before calling hooks
         entity.eid = self.system_source.create_eid(session)
@@ -1330,22 +1380,10 @@
         extid = self.init_entity_caches(session, entity, source)
         if server.DEBUG & server.DBG_REPO:
             print 'ADD entity', self, entity.__regid__, entity.eid, edited
-        relations = []
-        prefill_entity_caches(entity, relations)
+        prefill_entity_caches(entity)
         if source.should_call_hooks:
             self.hm.call_hooks('before_add_entity', session, entity=entity)
-        activintegrity = session.is_hook_category_activated('activeintegrity')
-        for attr in edited.iterkeys():
-            rschema = eschema.subjrels[attr]
-            if not rschema.final: # inlined relation
-                value = edited[attr]
-                relations.append((attr, value))
-                session.update_rel_cache_add(entity.eid, attr, value)
-                rdef = session.rtype_eids_rdef(attr, entity.eid, value)
-                if rdef.cardinality[1] in '1?' and activintegrity:
-                    with security_enabled(session, read=False):
-                        session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
-                                        {'x': entity.eid, 'y': value})
+        relations = preprocess_inlined_relations(session, entity)
         edited.set_defaults()
         if session.is_hook_category_activated('integrity'):
             edited.check(creation=True)
@@ -1508,7 +1546,7 @@
         activintegrity = session.is_hook_category_activated('activeintegrity')
         for rtype, eids_subj_obj in relations.iteritems():
             if server.DEBUG & server.DBG_REPO:
-                for subjeid, objeid in relations:
+                for subjeid, objeid in eids_subj_obj:
                     print 'ADD relation', subjeid, rtype, objeid
             for subjeid, objeid in eids_subj_obj:
                 source = self.locate_relation_source(session, subjeid, rtype, objeid)
@@ -1654,6 +1692,7 @@
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
 
+
 def pyro_unregister(config):
     """unregister the repository from the pyro name server"""
     from logilab.common.pyro_ext import ns_unregister
--- a/server/server.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/server.py	Thu Mar 21 18:13:31 2013 +0100
@@ -26,6 +26,7 @@
 from time import localtime, mktime
 
 from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.server.utils import TasksManager
 from cubicweb.server.repository import Repository
 
 class Finished(Exception):
@@ -77,7 +78,7 @@
     def __init__(self, config):
         """make the repository available as a PyRO object"""
         self.config = config
-        self.repo = Repository(config)
+        self.repo = Repository(config, TasksManager())
         self.ns = None
         self.quiting = None
         # event queue
--- a/server/serverconfig.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/serverconfig.py	Thu Mar 21 18:13:31 2013 +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.
@@ -140,12 +140,9 @@
           'help': 'size of the parsed rql cache size.',
           'group': 'main', 'level': 3,
           }),
-        ('undo-support',
-         {'type' : 'string', 'default': '',
-          'help': 'string defining actions that will have undo support: \
-[C]reate [U]pdate [D]elete entities / [A]dd [R]emove relation. Leave it empty \
-for no undo support, set it to CUDAR for full undo support, or to DR for \
-support undoing of deletion only.',
+        ('undo-enabled',
+         {'type' : 'yn', 'default': False,
+          'help': 'enable undo support',
           'group': 'main', 'level': 3,
           }),
         ('keep-transaction-lifetime',
@@ -207,6 +204,25 @@
 and if not set, it will be choosen randomly',
           'group': 'pyro', 'level': 3,
           }),
+        # zmq services config
+        ('zmq-repository-address',
+         {'type' : 'string',
+          'default': None,
+          'help': 'ZMQ URI on which the repository will be bound to.',
+          'group': 'zmq', 'level': 3,
+          }),
+         ('zmq-address-sub',
+          {'type' : 'csv',
+           'default' : None,
+           '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
@@ -276,7 +292,7 @@
                 return True
             return source.uri in self.sources_mode
         if self.quick_start:
-            return False
+            return source.uri == 'system'
         return (not source.disabled and (
             not self.enabled_sources or source.uri in self.enabled_sources))
 
--- a/server/serverctl.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/serverctl.py	Thu Mar 21 18:13:31 2013 +0100
@@ -35,6 +35,7 @@
 from cubicweb.toolsutils import Command, CommandHandler, underline_title
 from cubicweb.cwctl import CWCTL, check_options_consistency
 from cubicweb.server import SOURCE_TYPES
+from cubicweb.server.repository import Repository
 from cubicweb.server.serverconfig import (
     USER_OPTIONS, ServerConfiguration, SourceConfiguration,
     ask_source_config, generate_source_config)
@@ -446,7 +447,7 @@
             get_connection(
                 system['db-driver'], database=system['db-name'],
                 host=system.get('db-host'), port=system.get('db-port'),
-                user=system.get('db-user'), password=system.get('db-password'),
+                user=system.get('db-user') or '', password=system.get('db-password') or '',
                 **extra)
         except Exception, ex:
             raise ConfigurationError(
@@ -633,7 +634,7 @@
 class StartRepositoryCommand(Command):
     """Start a CubicWeb RQL server for a given instance.
 
-    The server will be accessible through pyro
+    The server will be remotely accessible through pyro or ZMQ
 
     <instance>
       the identifier of the instance to initialize.
@@ -650,12 +651,30 @@
           'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
           'help': 'debug if -D is set, error otherwise',
           }),
+        ('address',
+         {'short': 'a', 'type': 'string', 'metavar': '<protocol>://<host>:<port>',
+          'default': '',
+          'help': ('specify a ZMQ URI on which to bind, or use "pyro://"'
+                   'to create a pyro-based repository'),
+          }),
         )
 
+    def create_repo(self, config):
+        address = self['address']
+        if not address:
+            address = config.get('zmq-repository-address') or 'pyro://'
+        if address.startswith('pyro://'):
+            from cubicweb.server.server import RepositoryServer
+            return RepositoryServer(config), config['host']
+        else:
+            from cubicweb.server.utils import TasksManager
+            from cubicweb.server.cwzmq import ZMQRepositoryServer
+            repo = Repository(config, TasksManager())
+            return ZMQRepositoryServer(repo), address
+
     def run(self, args):
         from logilab.common.daemon import daemonize, setugid
         from cubicweb.cwctl import init_cmdline_log_threshold
-        from cubicweb.server.server import RepositoryServer
         appid = args[0]
         debug = self['debug']
         if sys.platform == 'win32' and not debug:
@@ -665,7 +684,7 @@
         config = ServerConfiguration.config_for(appid, debugmode=debug)
         init_cmdline_log_threshold(config, self['loglevel'])
         # create the server
-        server = RepositoryServer(config)
+        server, address = self.create_repo(config)
         # ensure the directory where the pid-file should be set exists (for
         # instance /var/run/cubicweb may be deleted on computer restart)
         pidfile = config['pid-file']
@@ -679,7 +698,7 @@
         if uid is not None:
             setugid(uid)
         server.install_sig_handlers()
-        server.connect(config['host'], 0)
+        server.connect(address)
         server.run()
 
 
@@ -974,20 +993,24 @@
 class RebuildFTICommand(Command):
     """Rebuild the full-text index of the system database of an instance.
 
-    <instance>
+    <instance> [etype(s)]
       the identifier of the instance to rebuild
+
+    If no etype is specified, cubicweb will reindex everything, otherwise
+    only specified etypes will be considered.
     """
     name = 'db-rebuild-fti'
     arguments = '<instance>'
-    min_args = max_args = 1
+    min_args = 1
 
     def run(self, args):
         from cubicweb.server.checkintegrity import reindex_entities
-        appid = args[0]
+        appid = args.pop(0)
+        etypes = args or None
         config = ServerConfiguration.config_for(appid)
         repo, cnx = repo_cnx(config)
         session = repo._get_session(cnx.sessionid, setcnxset=True)
-        reindex_entities(repo.schema, session)
+        reindex_entities(repo.schema, session, etypes=etypes)
         cnx.commit()
 
 
--- a/server/session.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/session.py	Thu Mar 21 18:13:31 2013 +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
@@ -106,7 +106,8 @@
         self.free_cnxset = free_cnxset
 
     def __enter__(self):
-        pass
+        # ensure session has a cnxset
+        self.session.set_cnxset()
 
     def __exit__(self, exctype, exc, traceback):
         if exctype:
@@ -252,13 +253,11 @@
         self.cnxtype = cnxprops.cnxtype
         self.timestamp = time()
         self.default_mode = 'read'
-        # support undo for Create Update Delete entity / Add Remove relation
+        # undo support
         if repo.config.creating or repo.config.repairing or self.is_internal_session:
-            self.undo_actions = ()
+            self.undo_actions = False
         else:
-            self.undo_actions = set(repo.config['undo-support'].upper())
-            if self.undo_actions - set('CUDAR'):
-                raise Exception('bad undo-support string in configuration')
+            self.undo_actions = repo.config['undo-enabled']
         # short cut to querier .execute method
         self._execute = repo.querier.execute
         # shared data, used to communicate extra information between the client
@@ -303,6 +302,8 @@
             self.set_tx_data()
             return self.__threaddata.txdata
 
+    def get_option_value(self, option, foreid=None):
+        return self.repo.get_option_value(option, foreid)
 
     def hijack_user(self, user):
         """return a fake request/session using specified user"""
@@ -847,6 +848,12 @@
         else:
             self.data[key] = value
 
+    # server-side service call #################################################
+
+    def call_service(self, regid, async=False, **kwargs):
+        return self.repo.call_service(self.id, regid, async, **kwargs)
+
+
     # request interface #######################################################
 
     @property
@@ -890,7 +897,7 @@
         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
         metas = self.repo.type_and_source_from_eid(eid, self)
         if asdict:
-            return dict(zip(('type', 'source', 'extid', 'asource'), metas)) 
+            return dict(zip(('type', 'source', 'extid', 'asource'), metas))
        # XXX :-1 for cw compat, use asdict=True for full information
         return metas[:-1]
 
@@ -1118,9 +1125,8 @@
 
     # undo support ############################################################
 
-    def undoable_action(self, action, ertype):
-        return action in self.undo_actions and not ertype in NO_UNDO_TYPES
-        # XXX elif transaction on mark it partial
+    def ertype_supports_undo(self, ertype):
+        return self.undo_actions  and ertype not in NO_UNDO_TYPES
 
     def transaction_uuid(self, set=True):
         try:
@@ -1272,6 +1278,12 @@
         if not safe:
             self.disable_hook_categories('integrity')
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exctype, excvalue, tb):
+        self.close()
+
     @property
     def cnxset(self):
         """connections set, set according to transaction mode for each query"""
--- a/server/sources/datafeed.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/sources/datafeed.py	Thu Mar 21 18:13:31 2013 +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,14 +22,15 @@
 
 import urllib2
 import StringIO
+from os.path import exists
 from datetime import datetime, timedelta
 from base64 import b64decode
 from cookielib import CookieJar
 
 from lxml import etree
-from logilab.mtconverter import xml_escape
 
 from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError, UnknownEid
+from cubicweb.server.repository import preprocess_inlined_relations
 from cubicweb.server.sources import AbstractSource
 from cubicweb.appobject import AppObject
 
@@ -67,7 +68,7 @@
           }),
         ('delete-entities',
          {'type' : 'yn',
-          'default': True,
+          'default': False,
           'help': ('Should already imported entities not found anymore on the '
                    'external source be deleted?'),
           'group': 'datafeed-source', 'level': 2,
@@ -79,6 +80,7 @@
           'group': 'datafeed-source', 'level': 2,
           }),
         )
+
     def __init__(self, repo, source_config, eid=None):
         AbstractSource.__init__(self, repo, source_config, eid)
         self.update_config(None, self.check_conf_dict(eid, source_config,
@@ -102,6 +104,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
@@ -150,21 +153,24 @@
 
     def update_latest_retrieval(self, session):
         self.latest_retrieval = datetime.utcnow()
+        session.set_cnxset()
         session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
                         {'x': self.eid, 'date': self.latest_retrieval})
+        session.commit()
 
     def acquire_synchronization_lock(self, session):
         # XXX race condition until WHERE of SET queries is executed using
         # 'SELECT FOR UPDATE'
         now = datetime.utcnow()
+        session.set_cnxset()
         if not session.execute(
             'SET X in_synchronization %(now)s WHERE X eid %(x)s, '
             'X in_synchronization NULL OR X in_synchronization < %(maxdt)s',
             {'x': self.eid, 'now': now, 'maxdt': now - self.max_lock_lifetime}):
             self.error('concurrent synchronization detected, skip pull')
-            session.commit(free_cnxset=False)
+            session.commit()
             return False
-        session.commit(free_cnxset=False)
+        session.commit()
         return True
 
     def release_synchronization_lock(self, session):
@@ -190,29 +196,22 @@
             self.release_synchronization_lock(session)
 
     def _pull_data(self, session, force=False, raise_on_error=False):
-        if self.config['delete-entities']:
-            myuris = self.source_cwuris(session)
-        else:
-            myuris = None
         importlog = self.init_import_log(session)
+        myuris = self.source_cwuris(session)
         parser = self._get_parser(session, sourceuris=myuris, import_log=importlog)
         if self.process_urls(parser, self.urls, raise_on_error):
             self.warning("some error occured, don't attempt to delete entities")
-        elif self.config['delete-entities'] and myuris:
-            byetype = {}
-            for eid, etype in myuris.values():
-                byetype.setdefault(etype, []).append(str(eid))
-            self.error('delete %s entities %s', self.uri, byetype)
-            for etype, eids in byetype.iteritems():
-                session.execute('DELETE %s X WHERE X eid IN (%s)'
-                                % (etype, ','.join(eids)))
+        else:
+            parser.handle_deletion(self.config, session, myuris)
         self.update_latest_retrieval(session)
         stats = parser.stats
         if stats.get('created'):
             importlog.record_info('added %s entities' % len(stats['created']))
         if stats.get('updated'):
             importlog.record_info('updated %s entities' % len(stats['updated']))
+        session.set_cnxset()
         importlog.write_log(session, end_timestamp=self.latest_retrieval)
+        session.commit()
         return stats
 
     def process_urls(self, parser, urls, raise_on_error=False):
@@ -256,18 +255,27 @@
         """called by the repository after an entity stored here has been
         inserted in the system table.
         """
+        relations = preprocess_inlined_relations(session, entity)
         if session.is_hook_category_activated('integrity'):
             entity.cw_edited.check(creation=True)
         self.repo.system_source.add_entity(session, entity)
         entity.cw_edited.saved = entity._cw_is_saved = True
         sourceparams['parser'].after_entity_copy(entity, sourceparams)
+        # call hooks for inlined relations
+        call_hooks = self.repo.hm.call_hooks
+        if self.should_call_hooks:
+            for attr, value in relations:
+                call_hooks('before_add_relation', session,
+                           eidfrom=entity.eid, rtype=attr, eidto=value)
+                call_hooks('after_add_relation', session,
+                           eidfrom=entity.eid, rtype=attr, eidto=value)
 
     def source_cwuris(self, session):
         sql = ('SELECT extid, eid, type FROM entities, cw_source_relation '
                'WHERE entities.eid=cw_source_relation.eid_from '
                'AND cw_source_relation.eid_to=%s' % self.eid)
         return dict((b64decode(uri), (eid, type))
-                    for uri, eid, type in session.system_sql(sql))
+                    for uri, eid, type in session.system_sql(sql).fetchall())
 
     def init_import_log(self, session, **kwargs):
         dataimport = session.create_entity('CWDataImport', cw_import_of=self,
@@ -276,6 +284,7 @@
         dataimport.init()
         return dataimport
 
+
 class DataFeedParser(AppObject):
     __registry__ = 'parsers'
 
@@ -284,8 +293,14 @@
         self.source = source
         self.sourceuris = sourceuris
         self.import_log = import_log
-        self.stats = {'created': set(),
-                      'updated': set()}
+        self.stats = {'created': set(), 'updated': set(), 'checked': set()}
+
+    def normalize_url(self, url):
+        from cubicweb.sobjects import URL_MAPPING # available after registration
+        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"""
@@ -339,7 +354,7 @@
             self.sourceuris.pop(str(uri), None)
         return session.entity_from_eid(eid, etype)
 
-    def process(self, url, partialcommit=True):
+    def process(self, url, raise_on_error=False):
         """main callback: process the url"""
         raise NotImplementedError
 
@@ -358,10 +373,46 @@
     def notify_updated(self, entity):
         return self.stats['updated'].add(entity.eid)
 
+    def notify_checked(self, entity):
+        return self.stats['checked'].add(entity.eid)
+
+    def is_deleted(self, extid, etype, eid):
+        """return True if the entity of given external id, entity type and eid
+        is actually deleted. Always return True by default, put more sensible
+        stuff in sub-classes.
+        """
+        return True
+
+    def handle_deletion(self, config, session, myuris):
+        if config['delete-entities'] and myuris:
+            byetype = {}
+            for extid, (eid, etype) in myuris.iteritems():
+                if self.is_deleted(extid, etype, eid):
+                    byetype.setdefault(etype, []).append(str(eid))
+            for etype, eids in byetype.iteritems():
+                self.warning('delete %s %s entities', len(eids), etype)
+                session.set_cnxset()
+                session.execute('DELETE %s X WHERE X eid IN (%s)'
+                                % (etype, ','.join(eids)))
+                session.commit()
+
+    def update_if_necessary(self, entity, attrs):
+        entity.complete(tuple(attrs))
+        # check modification date and compare attribute values to only update
+        # what's actually needed
+        self.notify_checked(entity)
+        mdate = attrs.get('modification_date')
+        if not mdate or mdate > entity.modification_date:
+            attrs = dict( (k, v) for k, v in attrs.iteritems()
+                          if v != getattr(entity, k))
+            if attrs:
+                entity.set_attributes(**attrs)
+                self.notify_updated(entity)
+
 
 class DataFeedXMLParser(DataFeedParser):
 
-    def process(self, url, raise_on_error=False, partialcommit=True):
+    def process(self, url, raise_on_error=False):
         """IDataFeedParser main entry point"""
         try:
             parsed = self.parse(url)
@@ -383,30 +434,23 @@
         for args in parsed:
             try:
                 self.process_item(*args)
-                if partialcommit:
-                    # commit+set_cnxset instead of commit(free_cnxset=False) to let
-                    # other a chance to get our connections set
-                    commit()
-                    set_cnxset()
+                # commit+set_cnxset instead of commit(free_cnxset=False) to let
+                # other a chance to get our connections set
+                commit()
+                set_cnxset()
             except ValidationError, exc:
                 if raise_on_error:
                     raise
-                if partialcommit:
-                    self.source.error('Skipping %s because of validation error %s' % (args, exc))
-                    rollback()
-                    set_cnxset()
-                    error = True
-                else:
-                    raise
+                self.source.error('Skipping %s because of validation error %s'
+                                  % (args, exc))
+                rollback()
+                set_cnxset()
+                error = True
         return error
 
     def parse(self, url):
         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://'):
@@ -421,6 +465,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 Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,44 @@
+# 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"""
+
+from cubicweb.server.sources import datafeed
+from cubicweb.server import ldaputils
+
+
+class LDAPFeedSource(ldaputils.LDAPSourceMixIn,
+                     datafeed.DataFeedSource):
+    """LDAP feed source: unlike ldapuser source, this source is copy based and
+    will import ldap content (beside passwords for authentication) into the
+    system source.
+    """
+    support_entities = {'CWUser': False}
+    use_cwuri_as_url = False
+
+    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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/sources/ldapuser.py	Thu Mar 21 18:13:31 2013 +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 __future__ import division, with_statement
 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,114 +105,66 @@
         """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):
+        with self.repo.internal_session() as session:
+            self.pull_data(session)
+
+    def pull_data(self, session, force=False, raise_on_error=False):
         """synchronize content known by this repository with content in the
         external repository
         """
         self.info('synchronizing ldap source %s', self.uri)
-        try:
-            ldap_emailattr = self.user_rev_attrs['email']
-        except KeyError:
-            return # no email in ldap, we're done
-        session = self.repo.internal_session()
+        ldap_emailattr = self.user_rev_attrs['email']
+        assert ldap_emailattr
         execute = session.execute
-        try:
-            cursor = session.system_sql("SELECT eid, extid FROM entities WHERE "
-                                        "source='%s'" % self.uri)
-            for eid, b64extid in cursor.fetchall():
-                extid = b64decode(b64extid)
-                self.debug('ldap eid %s', eid)
-                # if no result found, _search automatically delete entity information
-                res = self._search(session, extid, BASE)
-                self.debug('ldap search %s', res)
-                if res:
-                    ldapemailaddr = res[0].get(ldap_emailattr)
-                    if ldapemailaddr:
-                        if isinstance(ldapemailaddr, list):
-                            ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list
-                        rset = execute('Any X,A WHERE '
-                                       'X address A, U use_email X, U eid %(u)s',
-                                       {'u': eid})
-                        ldapemailaddr = unicode(ldapemailaddr)
-                        for emaileid, emailaddr, in rset:
-                            if emailaddr == ldapemailaddr:
-                                break
+        cursor = session.system_sql("SELECT eid, extid FROM entities WHERE "
+                                    "source='%s'" % self.uri)
+        for eid, b64extid in cursor.fetchall():
+            extid = b64decode(b64extid)
+            self.debug('ldap eid %s', eid)
+            # if no result found, _search automatically delete entity information
+            res = self._search(session, extid, BASE)
+            self.debug('ldap search %s', res)
+            if res:
+                ldapemailaddr = res[0].get(ldap_emailattr)
+                if ldapemailaddr:
+                    if isinstance(ldapemailaddr, list):
+                        ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list
+                    rset = execute('Any X,A WHERE '
+                                   'X address A, U use_email X, U eid %(u)s',
+                                   {'u': eid})
+                    ldapemailaddr = unicode(ldapemailaddr)
+                    for emaileid, emailaddr, in rset:
+                        if emailaddr == ldapemailaddr:
+                            break
+                    else:
+                        self.debug('updating email address of user %s to %s',
+                                  extid, ldapemailaddr)
+                        emailrset = execute('EmailAddress A WHERE A address %(addr)s',
+                                            {'addr': ldapemailaddr})
+                        if emailrset:
+                            execute('SET U use_email X WHERE '
+                                    'X eid %(x)s, U eid %(u)s',
+                                    {'x': emailrset[0][0], 'u': eid})
+                        elif rset:
+                            if not execute('SET X address %(addr)s WHERE '
+                                           'U primary_email X, U eid %(u)s',
+                                           {'addr': ldapemailaddr, 'u': eid}):
+                                execute('SET X address %(addr)s WHERE '
+                                        'X eid %(x)s',
+                                        {'addr': ldapemailaddr, 'x': rset[0][0]})
                         else:
-                            self.debug('updating email address of user %s to %s',
-                                      extid, ldapemailaddr)
-                            emailrset = execute('EmailAddress A WHERE A address %(addr)s',
-                                                {'addr': ldapemailaddr})
-                            if emailrset:
-                                execute('SET U use_email X WHERE '
-                                        'X eid %(x)s, U eid %(u)s',
-                                        {'x': emailrset[0][0], 'u': eid})
-                            elif rset:
-                                if not execute('SET X address %(addr)s WHERE '
-                                               'U primary_email X, U eid %(u)s',
-                                               {'addr': ldapemailaddr, 'u': eid}):
-                                    execute('SET X address %(addr)s WHERE '
-                                            'X eid %(x)s',
-                                            {'addr': ldapemailaddr, 'x': rset[0][0]})
-                            else:
-                                # no email found, create it
-                                _insert_email(session, ldapemailaddr, eid)
-        finally:
-            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
+                            # no email found, create it
+                            _insert_email(session, ldapemailaddr, eid)
+        session.commit()
 
     def ldap_name(self, var):
         if var.stinfo['relations']:
@@ -383,7 +233,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 +309,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 +345,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 +373,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/native.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/sources/native.py	Thu Mar 21 18:13:31 2013 +0100
@@ -55,17 +55,16 @@
 from yams.schema import role_name
 
 from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary,
-                      UniqueTogetherError)
+                      UniqueTogetherError, QueryError, UndoTransactionException)
 from cubicweb import transaction as tx, server, neg_role
 from cubicweb.utils import QueryCache
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
 from cubicweb.server import hook
-from cubicweb.server.utils import crypt_password, eschema_eid
+from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update
 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
 from cubicweb.server.rqlannotation import set_qdata
 from cubicweb.server.hook import CleanupDeletedEidsCacheOp
-from cubicweb.server.session import hooks_control, security_enabled
 from cubicweb.server.edition import EditedEntity
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
@@ -162,24 +161,24 @@
     allownull = rdef.cardinality[0] != '1'
     return coltype, allownull
 
-class UndoException(Exception):
+
+class _UndoException(Exception):
     """something went wrong during undoing"""
 
     def __unicode__(self):
         """Called by the unicode builtin; should return a Unicode object
 
-        Type of UndoException message must be `unicode` by design in CubicWeb.
+        Type of _UndoException message must be `unicode` by design in CubicWeb.
+        """
+        assert isinstance(self.args[0], unicode)
+        return self.args[0]
 
-        .. warning::
-            This method is not available in python2.5"""
-        assert isinstance(self.message, unicode)
-        return self.message
 
 def _undo_check_relation_target(tentity, rdef, role):
     """check linked entity has not been redirected for this relation"""
     card = rdef.role_cardinality(role)
     if card in '?1' and tentity.related(rdef.rtype, role):
-        raise UndoException(tentity._cw._(
+        raise _UndoException(tentity._cw._(
             "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
             "is already linked using this relation.")
                             % {'role': neg_role(role),
@@ -192,7 +191,7 @@
         try:
             entities.append(session.entity_from_eid(eid))
         except UnknownEid:
-            raise UndoException(session._(
+            raise _UndoException(session._(
                 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
                 " doesn't exist anymore.")
                                 % {'role': session._(role),
@@ -203,7 +202,7 @@
         rschema = session.vreg.schema.rschema(rtype)
         rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)]
     except KeyError:
-        raise UndoException(session._(
+        raise _UndoException(session._(
             "Can't restore relation %(rtype)s between %(subj)s and "
             "%(obj)s, that relation does not exists anymore in the "
             "schema.")
@@ -635,7 +634,7 @@
             attrs = self.preprocess_entity(entity)
             sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
             self.doexec(session, sql, attrs)
-            if session.undoable_action('C', entity.__regid__):
+            if session.ertype_supports_undo(entity.__regid__):
                 self._record_tx_action(session, 'tx_entity_actions', 'C',
                                        etype=entity.__regid__, eid=entity.eid)
 
@@ -643,7 +642,7 @@
         """replace an entity in the source"""
         with self._storage_handler(entity, 'updated'):
             attrs = self.preprocess_entity(entity)
-            if session.undoable_action('U', entity.__regid__):
+            if session.ertype_supports_undo(entity.__regid__):
                 changes = self._save_attrs(session, entity, attrs)
                 self._record_tx_action(session, 'tx_entity_actions', 'U',
                                        etype=entity.__regid__, eid=entity.eid,
@@ -655,7 +654,7 @@
     def delete_entity(self, session, entity):
         """delete an entity from the source"""
         with self._storage_handler(entity, 'deleted'):
-            if session.undoable_action('D', entity.__regid__):
+            if session.ertype_supports_undo(entity.__regid__):
                 attrs = [SQL_PREFIX + r.type
                          for r in entity.e_schema.subject_relations()
                          if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
@@ -670,14 +669,14 @@
     def add_relation(self, session, subject, rtype, object, inlined=False):
         """add a relation to the source"""
         self._add_relations(session,  rtype, [(subject, object)], inlined)
-        if session.undoable_action('A', rtype):
+        if session.ertype_supports_undo(rtype):
             self._record_tx_action(session, 'tx_relation_actions', 'A',
                                    eid_from=subject, rtype=rtype, eid_to=object)
 
     def add_relations(self, session,  rtype, subj_obj_list, inlined=False):
         """add a relations to the source"""
         self._add_relations(session, rtype, subj_obj_list, inlined)
-        if session.undoable_action('A', rtype):
+        if session.ertype_supports_undo(rtype):
             for subject, object in subj_obj_list:
                 self._record_tx_action(session, 'tx_relation_actions', 'A',
                                        eid_from=subject, rtype=rtype, eid_to=object)
@@ -710,7 +709,7 @@
         """delete a relation from the source"""
         rschema = self.schema.rschema(rtype)
         self._delete_relation(session, subject, rtype, object, rschema.inlined)
-        if session.undoable_action('R', rtype):
+        if session.ertype_supports_undo(rtype):
             self._record_tx_action(session, 'tx_relation_actions', 'R',
                                    eid_from=subject, rtype=rtype, eid_to=object)
 
@@ -1155,16 +1154,18 @@
         session.mode = 'write'
         errors = []
         session.transaction_data['undoing_uuid'] = txuuid
-        with hooks_control(session, session.HOOKS_DENY_ALL,
-                           'integrity', 'activeintegrity', 'undo'):
-            with security_enabled(session, read=False):
+        with session.deny_all_hooks_but('integrity', 'activeintegrity', 'undo'):
+            with session.security_enabled(read=False):
                 for action in reversed(self.tx_actions(session, txuuid, False)):
                     undomethod = getattr(self, '_undo_%s' % action.action.lower())
                     errors += undomethod(session, action)
         # remove the transactions record
         self.doexec(session,
                     "DELETE FROM transactions WHERE tx_uuid='%s'" % txuuid)
-        return errors
+        if errors:
+            raise UndoTransactionException(txuuid, errors)
+        else:
+            return
 
     def start_undoable_transaction(self, session, uuid):
         """session callback to insert a transaction record in the transactions
@@ -1217,12 +1218,53 @@
         try:
             time, ueid = cu.fetchone()
         except TypeError:
-            raise tx.NoSuchTransaction()
+            raise tx.NoSuchTransaction(txuuid)
         if not (session.user.is_in_group('managers')
                 or session.user.eid == ueid):
-            raise tx.NoSuchTransaction()
+            raise tx.NoSuchTransaction(txuuid)
         return time, ueid
 
+    def _reedit_entity(self, entity, changes, err):
+        session = entity._cw
+        eid = entity.eid
+        entity.cw_edited = edited = EditedEntity(entity)
+        # check for schema changes, entities linked through inlined relation
+        # still exists, rewrap binary values
+        eschema = entity.e_schema
+        getrschema = eschema.subjrels
+        for column, value in changes.items():
+            rtype = column[len(SQL_PREFIX):]
+            if rtype == "eid":
+                continue # XXX should even `eid` be stored in action changes?
+            try:
+                rschema = getrschema[rtype]
+            except KeyError:
+                err(session._("can't restore relation %(rtype)s of entity %(eid)s, "
+                              "this relation does not exist in the schema anymore.")
+                    % {'rtype': rtype, 'eid': eid})
+            if not rschema.final:
+                if not rschema.inlined:
+                    assert value is None
+                # rschema is an inlined relation
+                elif value is not None:
+                    # not a deletion: we must put something in edited
+                    try:
+                        entity._cw.entity_from_eid(value) # check target exists
+                        edited[rtype] = value
+                    except UnknownEid:
+                        err(session._("can't restore entity %(eid)s of type %(eschema)s, "
+                                      "target of %(rtype)s (eid %(value)s) does not exist any longer")
+                            % locals())
+            elif eschema.destination(rtype) in ('Bytes', 'Password'):
+                changes[column] = self._binary(value)
+                edited[rtype] = Binary(value)
+            elif isinstance(value, str):
+                edited[rtype] = unicode(value, session.encoding, 'replace')
+            else:
+                edited[rtype] = value
+        # This must only be done after init_entitiy_caches : defered in calling functions
+        # edited.check()
+
     def _undo_d(self, session, action):
         """undo an entity deletion"""
         errors = []
@@ -1237,31 +1279,10 @@
             err("can't restore entity %s of type %s, type no more supported"
                 % (eid, etype))
             return errors
-        entity.cw_edited = edited = EditedEntity(entity)
-        # check for schema changes, entities linked through inlined relation
-        # still exists, rewrap binary values
-        eschema = entity.e_schema
-        getrschema = eschema.subjrels
-        for column, value in action.changes.items():
-            rtype = column[3:] # remove cw_ prefix
-            try:
-                rschema = getrschema[rtype]
-            except KeyError:
-                err(_("Can't restore relation %(rtype)s of entity %(eid)s, "
-                      "this relation does not exists anymore in the schema.")
-                    % {'rtype': rtype, 'eid': eid})
-            if not rschema.final:
-                assert value is None
-            elif eschema.destination(rtype) in ('Bytes', 'Password'):
-                action.changes[column] = self._binary(value)
-                edited[rtype] = Binary(value)
-            elif isinstance(value, str):
-                edited[rtype] = unicode(value, session.encoding, 'replace')
-            else:
-                edited[rtype] = value
+        self._reedit_entity(entity, action.changes, err)
         entity.eid = eid
         session.repo.init_entity_caches(session, entity, self)
-        edited.check()
+        entity.cw_edited.check()
         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
         # restore the entity
         action.changes['cw_eid'] = eid
@@ -1282,14 +1303,14 @@
         subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
         try:
             sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
-        except UndoException, ex:
+        except _UndoException, ex:
             errors.append(unicode(ex))
         else:
             for role, entity in (('subject', sentity),
                                  ('object', oentity)):
                 try:
                     _undo_check_relation_target(entity, rdef, role)
-                except UndoException, ex:
+                except _UndoException, ex:
                     errors.append(unicode(ex))
                     continue
         if not errors:
@@ -1342,7 +1363,22 @@
 
     def _undo_u(self, session, action):
         """undo an entity update"""
-        return ['undoing of entity updating not yet supported.']
+        errors = []
+        err = errors.append
+        try:
+            entity = session.entity_from_eid(action.eid)
+        except UnknownEid:
+            err(session._("can't restore state of entity %s, it has been "
+                          "deleted inbetween") % action.eid)
+            return errors
+        self._reedit_entity(entity, action.changes, err)
+        entity.cw_edited.check()
+        self.repo.hm.call_hooks('before_update_entity', session, entity=entity)
+        sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, action.changes,
+                                 ['cw_eid'])
+        self.doexec(session, sql, action.changes)
+        self.repo.hm.call_hooks('after_update_entity', session, entity=entity)
+        return errors
 
     def _undo_a(self, session, action):
         """undo a relation addition"""
@@ -1350,7 +1386,7 @@
         subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
         try:
             sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
-        except UndoException, ex:
+        except _UndoException, ex:
             errors.append(unicode(ex))
         else:
             rschema = rdef.rtype
@@ -1559,9 +1595,10 @@
         pass
 
 class LoginPasswordAuthentifier(BaseAuthentifier):
-    passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
-    auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
-    _sols = ({'X': 'CWUser', 'P': 'Password'},)
+    passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
+    auth_rql = ('Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
+                'X cw_source S, S name "system"')
+    _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
 
     def set_schema(self, schema):
         """set the instance'schema"""
@@ -1592,7 +1629,22 @@
         # get eid from login and (crypted) password
         rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
         try:
-            return rset[0][0]
+            user = rset[0][0]
+            # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used
+            # before 3.14.7), update with a fresh one
+            if pwd.getvalue():
+                verify, newhash = verify_and_update(password, pwd.getvalue())
+                if not verify: # should not happen, but...
+                    raise AuthenticationError('bad password')
+                if newhash:
+                    session.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s" % (
+                                        SQL_PREFIX + 'CWUser',
+                                        SQL_PREFIX + 'upassword',
+                                        SQL_PREFIX + 'login'),
+                                       {'newhash': self.source._binary(newhash),
+                                        'login': login})
+                    session.commit(free_cnxset=False)
+            return user
         except IndexError:
             raise AuthenticationError('bad password')
 
--- a/server/sources/pyrorql.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/sources/pyrorql.py	Thu Mar 21 18:13:31 2013 +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,298 +21,56 @@
 _ = unicode
 
 import threading
-from os.path import join
-from time import mktime
-from datetime import datetime
-from base64 import b64decode
-
 from Pyro.errors import PyroError, ConnectionClosedError
 
 from logilab.common.configuration import REQUIRED
-from logilab.common.optik_ext import check_yn
-
-from yams.schema import role_name
-
-from rql.nodes import Constant
-from rql.utils import rqlvar_maker
 
-from cubicweb import dbapi, server
-from cubicweb import ValidationError, BadConnectionId, UnknownEid, ConnectionError
-from cubicweb.schema import VIRTUAL_RTYPES
-from cubicweb.cwconfig import register_persistent_options
-from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
-                                     TimedCache, dbg_st_search, dbg_results)
-from cubicweb.server.msplanner import neged_relation
+from cubicweb import dbapi
+from cubicweb import ConnectionError
+from cubicweb.server.sources import ConnectionWrapper
 
-def uidtype(union, col, etype, args):
-    select, col = union.locate_subquery(col, etype, args)
-    return getattr(select.selection[col], 'uidtype', None)
-
+from cubicweb.server.sources.remoterql import RemoteSource
 
-class ReplaceByInOperator(Exception):
-    def __init__(self, eids):
-        self.eids = eids
-
-class PyroRQLSource(AbstractSource):
+class PyroRQLSource(RemoteSource):
     """External repository source, using Pyro connection"""
 
-    # boolean telling if modification hooks should be called when something is
-    # modified in this source
-    should_call_hooks = False
-    # boolean telling if the repository should connect to this source during
-    # migration
-    connect_for_migration = False
+    CNX_TYPE = 'pyro'
 
-    options = (
+    options = RemoteSource.options + (
         # XXX pyro-ns host/port
         ('pyro-ns-id',
          {'type' : 'string',
           'default': REQUIRED,
           'help': 'identifier of the repository in the pyro name server',
-          'group': 'pyro-source', 'level': 0,
-          }),
-        ('cubicweb-user',
-         {'type' : 'string',
-          'default': REQUIRED,
-          'help': 'user to use for connection on the distant repository',
-          'group': 'pyro-source', 'level': 0,
-          }),
-        ('cubicweb-password',
-         {'type' : 'password',
-          'default': '',
-          'help': 'user to use for connection on the distant repository',
-          'group': 'pyro-source', 'level': 0,
-          }),
-        ('base-url',
-         {'type' : 'string',
-          'default': '',
-          'help': 'url of the web site for the distant repository, if you want '
-          'to generate external link to entities from this repository',
-          'group': 'pyro-source', 'level': 1,
-          }),
-        ('skip-external-entities',
-         {'type' : 'yn',
-          'default': False,
-          'help': 'should entities not local to the source be considered or not',
-          'group': 'pyro-source', 'level': 0,
+          'group': 'remote-source', 'level': 0,
           }),
         ('pyro-ns-host',
          {'type' : 'string',
           'default': None,
           'help': 'Pyro name server\'s host. If not set, default to the value \
 from all_in_one.conf. It may contains port information using <host>:<port> notation.',
-          'group': 'pyro-source', 'level': 1,
+          'group': 'remote-source', 'level': 1,
           }),
         ('pyro-ns-group',
          {'type' : 'string',
           'default': None,
           'help': 'Pyro name server\'s group where the repository will be \
 registered. If not set, default to the value from all_in_one.conf.',
-          'group': 'pyro-source', 'level': 2,
+          'group': 'remote-source', 'level': 2,
           }),
-        ('synchronization-interval',
-         {'type' : 'time',
-          'default': '5min',
-          'help': 'interval between synchronization with the external \
-repository (default to 5 minutes).',
-          'group': 'pyro-source', 'level': 2,
-          }),
-
     )
 
-    PUBLIC_KEYS = AbstractSource.PUBLIC_KEYS + ('base-url',)
-    _conn = None
-
-    def __init__(self, repo, source_config, eid=None):
-        AbstractSource.__init__(self, repo, source_config, eid)
-        self.update_config(None, self.check_conf_dict(eid, source_config,
-                                                      fail_if_unknown=False))
-        self._query_cache = TimedCache(1800)
-
-    def update_config(self, source_entity, processed_config):
-        """update configuration from source entity"""
-        # XXX get it through pyro if unset
-        baseurl = processed_config.get('base-url')
-        if baseurl and not baseurl.endswith('/'):
-            processed_config['base-url'] += '/'
-        self.config = processed_config
-        self._skip_externals = processed_config['skip-external-entities']
-        if source_entity is not None:
-            self.latest_retrieval = source_entity.latest_retrieval
-
-    def reset_caches(self):
-        """method called during test to reset potential source caches"""
-        self._query_cache = TimedCache(1800)
-
-    def init(self, activated, source_entity):
-        """method called by the repository once ready to handle request"""
-        self.load_mapping(source_entity._cw)
-        if activated:
-            interval = self.config['synchronization-interval']
-            self.repo.looping_task(interval, self.synchronize)
-            self.repo.looping_task(self._query_cache.ttl.seconds/10,
-                                   self._query_cache.clear_expired)
-            self.latest_retrieval = source_entity.latest_retrieval
-
-    def load_mapping(self, session=None):
-        self.support_entities = {}
-        self.support_relations = {}
-        self.dont_cross_relations = set(('owned_by', 'created_by'))
-        self.cross_relations = set()
-        assert self.eid is not None
-        self._schemacfg_idx = {}
-        self._load_mapping(session)
-
-    etype_options = set(('write',))
-    rtype_options = set(('maycross', 'dontcross', 'write',))
-
-    def _check_options(self, schemacfg, allowedoptions):
-        if schemacfg.options:
-            options = set(w.strip() for w in schemacfg.options.split(':'))
-        else:
-            options = set()
-        if options - allowedoptions:
-            options = ', '.join(sorted(options - allowedoptions))
-            msg = _('unknown option(s): %s' % options)
-            raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
-        return options
-
-    def add_schema_config(self, schemacfg, checkonly=False):
-        """added CWSourceSchemaConfig, modify mapping accordingly"""
-        try:
-            ertype = schemacfg.schema.name
-        except AttributeError:
-            msg = schemacfg._cw._("attribute/relation can't be mapped, only "
-                                  "entity and relation types")
-            raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
-        if schemacfg.schema.__regid__ == 'CWEType':
-            options = self._check_options(schemacfg, self.etype_options)
-            if not checkonly:
-                self.support_entities[ertype] = 'write' in options
-        else: # CWRType
-            if ertype in ('is', 'is_instance_of', 'cw_source') or ertype in VIRTUAL_RTYPES:
-                msg = schemacfg._cw._('%s relation should not be in mapped') % ertype
-                raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
-            options = self._check_options(schemacfg, self.rtype_options)
-            if 'dontcross' in options:
-                if 'maycross' in options:
-                    msg = schemacfg._("can't mix dontcross and maycross options")
-                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
-                if 'write' in options:
-                    msg = schemacfg._("can't mix dontcross and write options")
-                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
-                if not checkonly:
-                    self.dont_cross_relations.add(ertype)
-            elif not checkonly:
-                self.support_relations[ertype] = 'write' in options
-                if 'maycross' in options:
-                    self.cross_relations.add(ertype)
-        if not checkonly:
-            # add to an index to ease deletion handling
-            self._schemacfg_idx[schemacfg.eid] = ertype
-
-    def del_schema_config(self, schemacfg, checkonly=False):
-        """deleted CWSourceSchemaConfig, modify mapping accordingly"""
-        if checkonly:
-            return
-        try:
-            ertype = self._schemacfg_idx[schemacfg.eid]
-            if ertype[0].isupper():
-                del self.support_entities[ertype]
-            else:
-                if ertype in self.support_relations:
-                    del self.support_relations[ertype]
-                    if ertype in self.cross_relations:
-                        self.cross_relations.remove(ertype)
-                else:
-                    self.dont_cross_relations.remove(ertype)
-        except Exception:
-            self.error('while updating mapping consequently to removal of %s',
-                       schemacfg)
-
-    def local_eid(self, cnx, extid, session):
-        etype, dexturi, dextid = cnx.describe(extid)
-        if dexturi == 'system' or not (
-            dexturi in self.repo.sources_by_uri or self._skip_externals):
-            assert etype in self.support_entities, etype
-            eid = self.repo.extid2eid(self, str(extid), etype, session)
-            if eid > 0:
-                return eid, True
-        elif dexturi in self.repo.sources_by_uri:
-            source = self.repo.sources_by_uri[dexturi]
-            cnx = session.cnxset.connection(source.uri)
-            eid = source.local_eid(cnx, dextid, session)[0]
-            return eid, False
-        return None, None
-
-    def synchronize(self, mtime=None):
-        """synchronize content known by this repository with content in the
-        external repository
-        """
-        self.info('synchronizing pyro source %s', self.uri)
-        cnx = self.get_connection()
-        try:
-            extrepo = cnx._repo
-        except AttributeError:
-            # fake connection wrapper returned when we can't connect to the
-            # external source (hence we've no chance to synchronize...)
-            return
-        etypes = self.support_entities.keys()
-        if mtime is None:
-            mtime = self.latest_retrieval
-        updatetime, modified, deleted = extrepo.entities_modified_since(
-            etypes, mtime)
-        self._query_cache.clear()
-        repo = self.repo
-        session = repo.internal_session()
-        source = repo.system_source
-        try:
-            for etype, extid in modified:
-                try:
-                    eid = self.local_eid(cnx, extid, session)[0]
-                    if eid is not None:
-                        rset = session.eid_rset(eid, etype)
-                        entity = rset.get_entity(0, 0)
-                        entity.complete(entity.e_schema.indexable_attributes())
-                        source.index_entity(session, entity)
-                except Exception:
-                    self.exception('while updating %s with external id %s of source %s',
-                                   etype, extid, self.uri)
-                    continue
-            for etype, extid in deleted:
-                try:
-                    eid = self.repo.extid2eid(self, str(extid), etype, session,
-                                              insert=False)
-                    # entity has been deleted from external repository but is not known here
-                    if eid is not None:
-                        entity = session.entity_from_eid(eid, etype)
-                        repo.delete_info(session, entity, self.uri,
-                                         scleanup=self.eid)
-                except Exception:
-                    if self.repo.config.mode == 'test':
-                        raise
-                    self.exception('while updating %s with external id %s of source %s',
-                                   etype, extid, self.uri)
-                    continue
-            self.latest_retrieval = updatetime
-            session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
-                            {'x': self.eid, 'date': self.latest_retrieval})
-            session.commit()
-        finally:
-            session.close()
-
     def _get_connection(self):
         """open and return a connection to the source"""
         nshost = self.config.get('pyro-ns-host') or self.repo.config['pyro-ns-host']
         nsgroup = self.config.get('pyro-ns-group') or self.repo.config['pyro-ns-group']
         self.info('connecting to instance :%s.%s for user %s',
                   nsgroup, self.config['pyro-ns-id'], self.config['cubicweb-user'])
-        #cnxprops = ConnectionProperties(cnxtype=self.config['cnx-type'])
         return dbapi.connect(database=self.config['pyro-ns-id'],
                              login=self.config['cubicweb-user'],
                              password=self.config['cubicweb-password'],
                              host=nshost, group=nsgroup,
-                             setvreg=False) #cnxprops=cnxprops)
+                             setvreg=False)
 
     def get_connection(self):
         try:
@@ -333,373 +91,9 @@
         except AttributeError:
             # inmemory connection
             pass
-        if not isinstance(cnx, ConnectionWrapper):
-            try:
-                cnx.check()
-                return # ok
-            except (BadConnectionId, ConnectionClosedError):
-                pass
-        # try to reconnect
-        return self.get_connection()
-
-    def syntax_tree_search(self, session, union, args=None, cachekey=None,
-                           varmap=None):
-        assert dbg_st_search(self.uri, union, varmap, args, cachekey)
-        rqlkey = union.as_string(kwargs=args)
         try:
-            results = self._query_cache[rqlkey]
-        except KeyError:
-            results = self._syntax_tree_search(session, union, args)
-            self._query_cache[rqlkey] = results
-        assert dbg_results(results)
-        return results
-
-    def _syntax_tree_search(self, session, union, args):
-        """return result from this source for a rql query (actually from a rql
-        syntax tree and a solution dictionary mapping each used variable to a
-        possible type). If cachekey is given, the query necessary to fetch the
-        results (but not the results themselves) may be cached using this key.
-        """
-        if not args is None:
-            args = args.copy()
-        # get cached cursor anyway
-        cu = session.cnxset[self.uri]
-        if cu is None:
-            # this is a ConnectionWrapper instance
-            msg = session._("can't connect to source %s, some data may be missing")
-            session.set_shared_data('sources_error', msg % self.uri)
-            return []
-        translator = RQL2RQL(self)
-        try:
-            rql = translator.generate(session, union, args)
-        except UnknownEid, ex:
-            if server.DEBUG:
-                print '  unknown eid', ex, 'no results'
-            return []
-        if server.DEBUG & server.DBG_RQL:
-            print '  translated rql', rql
-        try:
-            rset = cu.execute(rql, args)
-        except Exception, ex:
-            self.exception(str(ex))
-            msg = session._("error while querying source %s, some data may be missing")
-            session.set_shared_data('sources_error', msg % self.uri)
-            return []
-        descr = rset.description
-        if rset:
-            needtranslation = []
-            rows = rset.rows
-            for i, etype in enumerate(descr[0]):
-                if (etype is None or not self.schema.eschema(etype).final
-                    or uidtype(union, i, etype, args)):
-                    needtranslation.append(i)
-            if needtranslation:
-                cnx = session.cnxset.connection(self.uri)
-                for rowindex in xrange(rset.rowcount - 1, -1, -1):
-                    row = rows[rowindex]
-                    localrow = False
-                    for colindex in needtranslation:
-                        if row[colindex] is not None: # optional variable
-                            eid, local = self.local_eid(cnx, row[colindex], session)
-                            if local:
-                                localrow = True
-                            if eid is not None:
-                                row[colindex] = eid
-                            else:
-                                # skip this row
-                                del rows[rowindex]
-                                del descr[rowindex]
-                                break
-                    else:
-                        # skip row if it only contains eids of entities which
-                        # are actually from a source we also know locally,
-                        # except if some args specified (XXX should actually
-                        # check if there are some args local to the source)
-                        if not (translator.has_local_eid or localrow):
-                            del rows[rowindex]
-                            del descr[rowindex]
-            results = rows
-        else:
-            results = []
-        return results
-
-    def _entity_relations_and_kwargs(self, session, entity):
-        relations = []
-        kwargs = {'x': self.repo.eid2extid(self, entity.eid, session)}
-        for key, val in entity.cw_attr_cache.iteritems():
-            relations.append('X %s %%(%s)s' % (key, key))
-            kwargs[key] = val
-        return relations, kwargs
-
-    def add_entity(self, session, entity):
-        """add a new entity to the source"""
-        raise NotImplementedError()
-
-    def update_entity(self, session, entity):
-        """update an entity in the source"""
-        relations, kwargs = self._entity_relations_and_kwargs(session, entity)
-        cu = session.cnxset[self.uri]
-        cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs)
-        self._query_cache.clear()
-        entity.cw_clear_all_caches()
-
-    def delete_entity(self, session, entity):
-        """delete an entity from the source"""
-        if session.deleted_in_transaction(self.eid):
-            # source is being deleted, don't propagate
-            self._query_cache.clear()
-            return
-        cu = session.cnxset[self.uri]
-        cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
-                   {'x': self.repo.eid2extid(self, entity.eid, session)})
-        self._query_cache.clear()
-
-    def add_relation(self, session, subject, rtype, object):
-        """add a relation to the source"""
-        cu = session.cnxset[self.uri]
-        cu.execute('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                   {'x': self.repo.eid2extid(self, subject, session),
-                    'y': self.repo.eid2extid(self, object, session)})
-        self._query_cache.clear()
-        session.entity_from_eid(subject).cw_clear_all_caches()
-        session.entity_from_eid(object).cw_clear_all_caches()
-
-    def delete_relation(self, session, subject, rtype, object):
-        """delete a relation from the source"""
-        if session.deleted_in_transaction(self.eid):
-            # source is being deleted, don't propagate
-            self._query_cache.clear()
-            return
-        cu = session.cnxset[self.uri]
-        cu.execute('DELETE X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                   {'x': self.repo.eid2extid(self, subject, session),
-                    'y': self.repo.eid2extid(self, object, session)})
-        self._query_cache.clear()
-        session.entity_from_eid(subject).cw_clear_all_caches()
-        session.entity_from_eid(object).cw_clear_all_caches()
-
-
-class RQL2RQL(object):
-    """translate a local rql query to be executed on a distant repository"""
-    def __init__(self, source):
-        self.source = source
-        self.repo = source.repo
-        self.current_operator = None
-
-    def _accept_children(self, node):
-        res = []
-        for child in node.children:
-            rql = child.accept(self)
-            if rql is not None:
-                res.append(rql)
-        return res
-
-    def generate(self, session, rqlst, args):
-        self._session = session
-        self.kwargs = args
-        self.need_translation = False
-        self.has_local_eid = False
-        return self.visit_union(rqlst)
-
-    def visit_union(self, node):
-        s = self._accept_children(node)
-        if len(s) > 1:
-            return ' UNION '.join('(%s)' % q for q in s)
-        return s[0]
+            return super(PyroRQLSource, self).check_connection(cnx)
+        except ConnectionClosedError:
+            # try to reconnect
+            return self.get_connection()
 
-    def visit_select(self, node):
-        """return the tree as an encoded rql string"""
-        self._varmaker = rqlvar_maker(defined=node.defined_vars.copy())
-        self._const_var = {}
-        if node.distinct:
-            base = 'DISTINCT Any'
-        else:
-            base = 'Any'
-        s = ['%s %s' % (base, ','.join(v.accept(self) for v in node.selection))]
-        if node.groupby:
-            s.append('GROUPBY %s' % ', '.join(group.accept(self)
-                                              for group in node.groupby))
-        if node.orderby:
-            s.append('ORDERBY %s' % ', '.join(self.visit_sortterm(term)
-                                              for term in node.orderby))
-        if node.limit is not None:
-            s.append('LIMIT %s' % node.limit)
-        if node.offset:
-            s.append('OFFSET %s' % node.offset)
-        restrictions = []
-        if node.where is not None:
-            nr = node.where.accept(self)
-            if nr is not None:
-                restrictions.append(nr)
-        if restrictions:
-            s.append('WHERE %s' % ','.join(restrictions))
-
-        if node.having:
-            s.append('HAVING %s' % ', '.join(term.accept(self)
-                                             for term in node.having))
-        subqueries = []
-        for subquery in node.with_:
-            subqueries.append('%s BEING (%s)' % (','.join(ca.name for ca in subquery.aliases),
-                                                 self.visit_union(subquery.query)))
-        if subqueries:
-            s.append('WITH %s' % (','.join(subqueries)))
-        return ' '.join(s)
-
-    def visit_and(self, node):
-        res = self._accept_children(node)
-        if res:
-            return ', '.join(res)
-        return
-
-    def visit_or(self, node):
-        res = self._accept_children(node)
-        if len(res) > 1:
-            return ' OR '.join('(%s)' % rql for rql in res)
-        elif res:
-            return res[0]
-        return
-
-    def visit_not(self, node):
-        rql = node.children[0].accept(self)
-        if rql:
-            return 'NOT (%s)' % rql
-        return
-
-    def visit_exists(self, node):
-        rql = node.children[0].accept(self)
-        if rql:
-            return 'EXISTS(%s)' % rql
-        return
-
-    def visit_relation(self, node):
-        try:
-            if isinstance(node.children[0], Constant):
-                # simplified rqlst, reintroduce eid relation
-                try:
-                    restr, lhs = self.process_eid_const(node.children[0])
-                except UnknownEid:
-                    # can safely skip not relation with an unsupported eid
-                    if neged_relation(node):
-                        return
-                    raise
-            else:
-                lhs = node.children[0].accept(self)
-                restr = None
-        except UnknownEid:
-            # can safely skip not relation with an unsupported eid
-            if neged_relation(node):
-                return
-            # XXX what about optional relation or outer NOT EXISTS()
-            raise
-        if node.optional in ('left', 'both'):
-            lhs += '?'
-        if node.r_type == 'eid' or not self.source.schema.rschema(node.r_type).final:
-            self.need_translation = True
-            self.current_operator = node.operator()
-            if isinstance(node.children[0], Constant):
-                self.current_etypes = (node.children[0].uidtype,)
-            else:
-                self.current_etypes = node.children[0].variable.stinfo['possibletypes']
-        try:
-            rhs = node.children[1].accept(self)
-        except UnknownEid:
-            # can safely skip not relation with an unsupported eid
-            if neged_relation(node):
-                return
-            # XXX what about optional relation or outer NOT EXISTS()
-            raise
-        except ReplaceByInOperator, ex:
-            rhs = 'IN (%s)' % ','.join(eid for eid in ex.eids)
-        self.need_translation = False
-        self.current_operator = None
-        if node.optional in ('right', 'both'):
-            rhs += '?'
-        if restr is not None:
-            return '%s %s %s, %s' % (lhs, node.r_type, rhs, restr)
-        return '%s %s %s' % (lhs, node.r_type, rhs)
-
-    def visit_comparison(self, node):
-        if node.operator in ('=', 'IS'):
-            return node.children[0].accept(self)
-        return '%s %s' % (node.operator.encode(),
-                          node.children[0].accept(self))
-
-    def visit_mathexpression(self, node):
-        return '(%s %s %s)' % (node.children[0].accept(self),
-                               node.operator.encode(),
-                               node.children[1].accept(self))
-
-    def visit_function(self, node):
-        #if node.name == 'IN':
-        res = []
-        for child in node.children:
-            try:
-                rql = child.accept(self)
-            except UnknownEid, ex:
-                continue
-            res.append(rql)
-        if not res:
-            raise ex
-        return '%s(%s)' % (node.name, ', '.join(res))
-
-    def visit_constant(self, node):
-        if self.need_translation or node.uidtype:
-            if node.type == 'Int':
-                self.has_local_eid = True
-                return str(self.eid2extid(node.value))
-            if node.type == 'Substitute':
-                key = node.value
-                # ensure we have not yet translated the value...
-                if not key in self._const_var:
-                    self.kwargs[key] = self.eid2extid(self.kwargs[key])
-                    self._const_var[key] = None
-                    self.has_local_eid = True
-        return node.as_string()
-
-    def visit_variableref(self, node):
-        """get the sql name for a variable reference"""
-        return node.name
-
-    def visit_sortterm(self, node):
-        if node.asc:
-            return node.term.accept(self)
-        return '%s DESC' % node.term.accept(self)
-
-    def process_eid_const(self, const):
-        value = const.eval(self.kwargs)
-        try:
-            return None, self._const_var[value]
-        except Exception:
-            var = self._varmaker.next()
-            self.need_translation = True
-            restr = '%s eid %s' % (var, self.visit_constant(const))
-            self.need_translation = False
-            self._const_var[value] = var
-            return restr, var
-
-    def eid2extid(self, eid):
-        try:
-            return self.repo.eid2extid(self.source, eid, self._session)
-        except UnknownEid:
-            operator = self.current_operator
-            if operator is not None and operator != '=':
-                # deal with query like "X eid > 12"
-                #
-                # The problem is that eid order in the external source may
-                # differ from the local source
-                #
-                # So search for all eids from this source matching the condition
-                # locally and then to replace the "> 12" branch by "IN (eids)"
-                #
-                # XXX we may have to insert a huge number of eids...)
-                sql = "SELECT extid FROM entities WHERE source='%s' AND type IN (%s) AND eid%s%s"
-                etypes = ','.join("'%s'" % etype for etype in self.current_etypes)
-                cu = self._session.system_sql(sql % (self.source.uri, etypes,
-                                                      operator, eid))
-                # XXX buggy cu.rowcount which may be zero while there are some
-                # results
-                rows = cu.fetchall()
-                if rows:
-                    raise ReplaceByInOperator((b64decode(r[0]) for r in rows))
-            raise
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/remoterql.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,670 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Source to query another RQL remote repository"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from os.path import join
+from base64 import b64decode
+
+from logilab.common.configuration import REQUIRED
+
+from yams.schema import role_name
+
+from rql.nodes import Constant
+from rql.utils import rqlvar_maker
+
+from cubicweb import dbapi, server
+from cubicweb import ValidationError, BadConnectionId, UnknownEid
+from cubicweb.schema import VIRTUAL_RTYPES
+from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
+                                     TimedCache, dbg_st_search, dbg_results)
+from cubicweb.server.msplanner import neged_relation
+
+def uidtype(union, col, etype, args):
+    select, col = union.locate_subquery(col, etype, args)
+    return getattr(select.selection[col], 'uidtype', None)
+
+
+class ReplaceByInOperator(Exception):
+    def __init__(self, eids):
+        self.eids = eids
+
+class RemoteSource(AbstractSource):
+    """Generic external repository source"""
+
+    CNX_TYPE = None # Must be ovewritted !
+
+    # boolean telling if modification hooks should be called when something is
+    # modified in this source
+    should_call_hooks = False
+    # boolean telling if the repository should connect to this source during
+    # migration
+    connect_for_migration = False
+
+    options = (
+
+        ('cubicweb-user',
+         {'type' : 'string',
+          'default': REQUIRED,
+          'help': 'user to use for connection on the distant repository',
+          'group': 'remote-source', 'level': 0,
+          }),
+        ('cubicweb-password',
+         {'type' : 'password',
+          'default': '',
+          'help': 'user to use for connection on the distant repository',
+          'group': 'remote-source', 'level': 0,
+          }),
+        ('base-url',
+         {'type' : 'string',
+          'default': '',
+          'help': 'url of the web site for the distant repository, if you want '
+          'to generate external link to entities from this repository',
+          'group': 'remote-source', 'level': 1,
+          }),
+        ('skip-external-entities',
+         {'type' : 'yn',
+          'default': False,
+          'help': 'should entities not local to the source be considered or not',
+          'group': 'remote-source', 'level': 0,
+          }),
+        ('synchronization-interval',
+         {'type' : 'time',
+          'default': '5min',
+          'help': 'interval between synchronization with the external \
+repository (default to 5 minutes).',
+          'group': 'remote-source', 'level': 2,
+          }))
+
+    PUBLIC_KEYS = AbstractSource.PUBLIC_KEYS + ('base-url',)
+
+    _conn = None
+
+    def __init__(self, repo, source_config, eid=None):
+        super(RemoteSource, self).__init__(repo, source_config, eid)
+        self.update_config(None, self.check_conf_dict(eid, source_config,
+                                                      fail_if_unknown=False))
+        self._query_cache = TimedCache(1800)
+
+    def update_config(self, source_entity, processed_config):
+        """update configuration from source entity"""
+        baseurl = processed_config.get('base-url')
+        if baseurl and not baseurl.endswith('/'):
+            processed_config['base-url'] += '/'
+        self.config = processed_config
+        self._skip_externals = processed_config['skip-external-entities']
+        if source_entity is not None:
+            self.latest_retrieval = source_entity.latest_retrieval
+
+    def _get_connection(self):
+        """open and return a connection to the source"""
+        self.info('connecting to source %(base-url)s with user %(cubicweb-user)s',
+                  self.config)
+        cnxprops = ConnectionProperties(cnxtype=self.CNX_TYPE)
+        return dbapi.connect(login=self.config['cubicweb-user'],
+                             password=self.config['cubicweb-password'],
+                             cnxprops=cnxprops)
+
+    def get_connection(self):
+        try:
+            return self._get_connection()
+        except ConnectionError, ex:
+            self.critical("can't get connection to source %s: %s", self.uri, ex)
+            return ConnectionWrapper()
+
+
+    def reset_caches(self):
+        """method called during test to reset potential source caches"""
+        self._query_cache = TimedCache(1800)
+
+    def init(self, activated, source_entity):
+        """method called by the repository once ready to handle request"""
+        self.load_mapping(source_entity._cw)
+        if activated:
+            interval = self.config['synchronization-interval']
+            self.repo.looping_task(interval, self.synchronize)
+            self.repo.looping_task(self._query_cache.ttl.seconds/10,
+                                   self._query_cache.clear_expired)
+            self.latest_retrieval = source_entity.latest_retrieval
+
+    def load_mapping(self, session=None):
+        self.support_entities = {}
+        self.support_relations = {}
+        self.dont_cross_relations = set(('owned_by', 'created_by'))
+        self.cross_relations = set()
+        assert self.eid is not None
+        self._schemacfg_idx = {}
+        self._load_mapping(session)
+
+    etype_options = set(('write',))
+    rtype_options = set(('maycross', 'dontcross', 'write',))
+
+    def _check_options(self, schemacfg, allowedoptions):
+        if schemacfg.options:
+            options = set(w.strip() for w in schemacfg.options.split(':'))
+        else:
+            options = set()
+        if options - allowedoptions:
+            options = ', '.join(sorted(options - allowedoptions))
+            msg = _('unknown option(s): %s' % options)
+            raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+        return options
+
+    def add_schema_config(self, schemacfg, checkonly=False):
+        """added CWSourceSchemaConfig, modify mapping accordingly"""
+        try:
+            ertype = schemacfg.schema.name
+        except AttributeError:
+            msg = schemacfg._cw._("attribute/relation can't be mapped, only "
+                                  "entity and relation types")
+            raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
+        if schemacfg.schema.__regid__ == 'CWEType':
+            options = self._check_options(schemacfg, self.etype_options)
+            if not checkonly:
+                self.support_entities[ertype] = 'write' in options
+        else: # CWRType
+            if ertype in ('is', 'is_instance_of', 'cw_source') or ertype in VIRTUAL_RTYPES:
+                msg = schemacfg._cw._('%s relation should not be in mapped') % ertype
+                raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
+            options = self._check_options(schemacfg, self.rtype_options)
+            if 'dontcross' in options:
+                if 'maycross' in options:
+                    msg = schemacfg._("can't mix dontcross and maycross options")
+                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+                if 'write' in options:
+                    msg = schemacfg._("can't mix dontcross and write options")
+                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+                if not checkonly:
+                    self.dont_cross_relations.add(ertype)
+            elif not checkonly:
+                self.support_relations[ertype] = 'write' in options
+                if 'maycross' in options:
+                    self.cross_relations.add(ertype)
+        if not checkonly:
+            # add to an index to ease deletion handling
+            self._schemacfg_idx[schemacfg.eid] = ertype
+
+    def del_schema_config(self, schemacfg, checkonly=False):
+        """deleted CWSourceSchemaConfig, modify mapping accordingly"""
+        if checkonly:
+            return
+        try:
+            ertype = self._schemacfg_idx[schemacfg.eid]
+            if ertype[0].isupper():
+                del self.support_entities[ertype]
+            else:
+                if ertype in self.support_relations:
+                    del self.support_relations[ertype]
+                    if ertype in self.cross_relations:
+                        self.cross_relations.remove(ertype)
+                else:
+                    self.dont_cross_relations.remove(ertype)
+        except Exception:
+            self.error('while updating mapping consequently to removal of %s',
+                       schemacfg)
+
+    def local_eid(self, cnx, extid, session):
+        etype, dexturi, dextid = cnx.describe(extid)
+        if dexturi == 'system' or not (
+            dexturi in self.repo.sources_by_uri or self._skip_externals):
+            assert etype in self.support_entities, etype
+            eid = self.repo.extid2eid(self, str(extid), etype, session)
+            if eid > 0:
+                return eid, True
+        elif dexturi in self.repo.sources_by_uri:
+            source = self.repo.sources_by_uri[dexturi]
+            cnx = session.cnxset.connection(source.uri)
+            eid = source.local_eid(cnx, dextid, session)[0]
+            return eid, False
+        return None, None
+
+    def synchronize(self, mtime=None):
+        """synchronize content known by this repository with content in the
+        external repository
+        """
+        self.info('synchronizing remote %s source %s', (self.CNX_TYPE, self.uri))
+        cnx = self.get_connection()
+        try:
+            extrepo = cnx._repo
+        except AttributeError:
+            # fake connection wrapper returned when we can't connect to the
+            # external source (hence we've no chance to synchronize...)
+            return
+        etypes = self.support_entities.keys()
+        if mtime is None:
+            mtime = self.latest_retrieval
+        updatetime, modified, deleted = extrepo.entities_modified_since(
+            etypes, mtime)
+        self._query_cache.clear()
+        repo = self.repo
+        session = repo.internal_session()
+        source = repo.system_source
+        try:
+            for etype, extid in modified:
+                try:
+                    eid = self.local_eid(cnx, extid, session)[0]
+                    if eid is not None:
+                        rset = session.eid_rset(eid, etype)
+                        entity = rset.get_entity(0, 0)
+                        entity.complete(entity.e_schema.indexable_attributes())
+                        source.index_entity(session, entity)
+                except Exception:
+                    self.exception('while updating %s with external id %s of source %s',
+                                   etype, extid, self.uri)
+                    continue
+            for etype, extid in deleted:
+                try:
+                    eid = self.repo.extid2eid(self, str(extid), etype, session,
+                                              insert=False)
+                    # entity has been deleted from external repository but is not known here
+                    if eid is not None:
+                        entity = session.entity_from_eid(eid, etype)
+                        repo.delete_info(session, entity, self.uri,
+                                         scleanup=self.eid)
+                except Exception:
+                    if self.repo.config.mode == 'test':
+                        raise
+                    self.exception('while updating %s with external id %s of source %s',
+                                   etype, extid, self.uri)
+                    continue
+            self.latest_retrieval = updatetime
+            session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
+                            {'x': self.eid, 'date': self.latest_retrieval})
+            session.commit()
+        finally:
+            session.close()
+
+    def get_connection(self):
+        raise NotImplementedError()
+
+    def check_connection(self, cnx):
+        """check connection validity, return None if the connection is still valid
+        else a new connection
+        """
+        if not isinstance(cnx, ConnectionWrapper):
+            try:
+                cnx.check()
+                return # ok
+            except BadConnectionId:
+                pass
+        # try to reconnect
+        return self.get_connection()
+
+    def syntax_tree_search(self, session, union, args=None, cachekey=None,
+                           varmap=None):
+        assert dbg_st_search(self.uri, union, varmap, args, cachekey)
+        rqlkey = union.as_string(kwargs=args)
+        try:
+            results = self._query_cache[rqlkey]
+        except KeyError:
+            results = self._syntax_tree_search(session, union, args)
+            self._query_cache[rqlkey] = results
+        assert dbg_results(results)
+        return results
+
+    def _syntax_tree_search(self, session, union, args):
+        """return result from this source for a rql query (actually from a rql
+        syntax tree and a solution dictionary mapping each used variable to a
+        possible type). If cachekey is given, the query necessary to fetch the
+        results (but not the results themselves) may be cached using this key.
+        """
+        if not args is None:
+            args = args.copy()
+        # get cached cursor anyway
+        cu = session.cnxset[self.uri]
+        if cu is None:
+            # this is a ConnectionWrapper instance
+            msg = session._("can't connect to source %s, some data may be missing")
+            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
+            return []
+        translator = RQL2RQL(self)
+        try:
+            rql = translator.generate(session, union, args)
+        except UnknownEid, ex:
+            if server.DEBUG:
+                print '  unknown eid', ex, 'no results'
+            return []
+        if server.DEBUG & server.DBG_RQL:
+            print '  translated rql', rql
+        try:
+            rset = cu.execute(rql, args)
+        except Exception, ex:
+            self.exception(str(ex))
+            msg = session._("error while querying source %s, some data may be missing")
+            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
+            return []
+        descr = rset.description
+        if rset:
+            needtranslation = []
+            rows = rset.rows
+            for i, etype in enumerate(descr[0]):
+                if (etype is None or not self.schema.eschema(etype).final
+                    or uidtype(union, i, etype, args)):
+                    needtranslation.append(i)
+            if needtranslation:
+                cnx = session.cnxset.connection(self.uri)
+                for rowindex in xrange(rset.rowcount - 1, -1, -1):
+                    row = rows[rowindex]
+                    localrow = False
+                    for colindex in needtranslation:
+                        if row[colindex] is not None: # optional variable
+                            eid, local = self.local_eid(cnx, row[colindex], session)
+                            if local:
+                                localrow = True
+                            if eid is not None:
+                                row[colindex] = eid
+                            else:
+                                # skip this row
+                                del rows[rowindex]
+                                del descr[rowindex]
+                                break
+                    else:
+                        # skip row if it only contains eids of entities which
+                        # are actually from a source we also know locally,
+                        # except if some args specified (XXX should actually
+                        # check if there are some args local to the source)
+                        if not (translator.has_local_eid or localrow):
+                            del rows[rowindex]
+                            del descr[rowindex]
+            results = rows
+        else:
+            results = []
+        return results
+
+    def _entity_relations_and_kwargs(self, session, entity):
+        relations = []
+        kwargs = {'x': self.repo.eid2extid(self, entity.eid, session)}
+        for key, val in entity.cw_attr_cache.iteritems():
+            relations.append('X %s %%(%s)s' % (key, key))
+            kwargs[key] = val
+        return relations, kwargs
+
+    def add_entity(self, session, entity):
+        """add a new entity to the source"""
+        raise NotImplementedError()
+
+    def update_entity(self, session, entity):
+        """update an entity in the source"""
+        relations, kwargs = self._entity_relations_and_kwargs(session, entity)
+        cu = session.cnxset[self.uri]
+        cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs)
+        self._query_cache.clear()
+        entity.cw_clear_all_caches()
+
+    def delete_entity(self, session, entity):
+        """delete an entity from the source"""
+        if session.deleted_in_transaction(self.eid):
+            # source is being deleted, don't propagate
+            self._query_cache.clear()
+            return
+        cu = session.cnxset[self.uri]
+        cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
+                   {'x': self.repo.eid2extid(self, entity.eid, session)})
+        self._query_cache.clear()
+
+    def add_relation(self, session, subject, rtype, object):
+        """add a relation to the source"""
+        cu = session.cnxset[self.uri]
+        cu.execute('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+                   {'x': self.repo.eid2extid(self, subject, session),
+                    'y': self.repo.eid2extid(self, object, session)})
+        self._query_cache.clear()
+        session.entity_from_eid(subject).cw_clear_all_caches()
+        session.entity_from_eid(object).cw_clear_all_caches()
+
+    def delete_relation(self, session, subject, rtype, object):
+        """delete a relation from the source"""
+        if session.deleted_in_transaction(self.eid):
+            # source is being deleted, don't propagate
+            self._query_cache.clear()
+            return
+        cu = session.cnxset[self.uri]
+        cu.execute('DELETE X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+                   {'x': self.repo.eid2extid(self, subject, session),
+                    'y': self.repo.eid2extid(self, object, session)})
+        self._query_cache.clear()
+        session.entity_from_eid(subject).cw_clear_all_caches()
+        session.entity_from_eid(object).cw_clear_all_caches()
+
+
+class RQL2RQL(object):
+    """translate a local rql query to be executed on a distant repository"""
+    def __init__(self, source):
+        self.source = source
+        self.repo = source.repo
+        self.current_operator = None
+
+    def _accept_children(self, node):
+        res = []
+        for child in node.children:
+            rql = child.accept(self)
+            if rql is not None:
+                res.append(rql)
+        return res
+
+    def generate(self, session, rqlst, args):
+        self._session = session
+        self.kwargs = args
+        self.need_translation = False
+        self.has_local_eid = False
+        return self.visit_union(rqlst)
+
+    def visit_union(self, node):
+        s = self._accept_children(node)
+        if len(s) > 1:
+            return ' UNION '.join('(%s)' % q for q in s)
+        return s[0]
+
+    def visit_select(self, node):
+        """return the tree as an encoded rql string"""
+        self._varmaker = rqlvar_maker(defined=node.defined_vars.copy())
+        self._const_var = {}
+        if node.distinct:
+            base = 'DISTINCT Any'
+        else:
+            base = 'Any'
+        s = ['%s %s' % (base, ','.join(v.accept(self) for v in node.selection))]
+        if node.groupby:
+            s.append('GROUPBY %s' % ', '.join(group.accept(self)
+                                              for group in node.groupby))
+        if node.orderby:
+            s.append('ORDERBY %s' % ', '.join(self.visit_sortterm(term)
+                                              for term in node.orderby))
+        if node.limit is not None:
+            s.append('LIMIT %s' % node.limit)
+        if node.offset:
+            s.append('OFFSET %s' % node.offset)
+        restrictions = []
+        if node.where is not None:
+            nr = node.where.accept(self)
+            if nr is not None:
+                restrictions.append(nr)
+        if restrictions:
+            s.append('WHERE %s' % ','.join(restrictions))
+
+        if node.having:
+            s.append('HAVING %s' % ', '.join(term.accept(self)
+                                             for term in node.having))
+        subqueries = []
+        for subquery in node.with_:
+            subqueries.append('%s BEING (%s)' % (','.join(ca.name for ca in subquery.aliases),
+                                                 self.visit_union(subquery.query)))
+        if subqueries:
+            s.append('WITH %s' % (','.join(subqueries)))
+        return ' '.join(s)
+
+    def visit_and(self, node):
+        res = self._accept_children(node)
+        if res:
+            return ', '.join(res)
+        return
+
+    def visit_or(self, node):
+        res = self._accept_children(node)
+        if len(res) > 1:
+            return ' OR '.join('(%s)' % rql for rql in res)
+        elif res:
+            return res[0]
+        return
+
+    def visit_not(self, node):
+        rql = node.children[0].accept(self)
+        if rql:
+            return 'NOT (%s)' % rql
+        return
+
+    def visit_exists(self, node):
+        rql = node.children[0].accept(self)
+        if rql:
+            return 'EXISTS(%s)' % rql
+        return
+
+    def visit_relation(self, node):
+        try:
+            if isinstance(node.children[0], Constant):
+                # simplified rqlst, reintroduce eid relation
+                try:
+                    restr, lhs = self.process_eid_const(node.children[0])
+                except UnknownEid:
+                    # can safely skip not relation with an unsupported eid
+                    if neged_relation(node):
+                        return
+                    raise
+            else:
+                lhs = node.children[0].accept(self)
+                restr = None
+        except UnknownEid:
+            # can safely skip not relation with an unsupported eid
+            if neged_relation(node):
+                return
+            # XXX what about optional relation or outer NOT EXISTS()
+            raise
+        if node.optional in ('left', 'both'):
+            lhs += '?'
+        if node.r_type == 'eid' or not self.source.schema.rschema(node.r_type).final:
+            self.need_translation = True
+            self.current_operator = node.operator()
+            if isinstance(node.children[0], Constant):
+                self.current_etypes = (node.children[0].uidtype,)
+            else:
+                self.current_etypes = node.children[0].variable.stinfo['possibletypes']
+        try:
+            rhs = node.children[1].accept(self)
+        except UnknownEid:
+            # can safely skip not relation with an unsupported eid
+            if neged_relation(node):
+                return
+            # XXX what about optional relation or outer NOT EXISTS()
+            raise
+        except ReplaceByInOperator, ex:
+            rhs = 'IN (%s)' % ','.join(eid for eid in ex.eids)
+        self.need_translation = False
+        self.current_operator = None
+        if node.optional in ('right', 'both'):
+            rhs += '?'
+        if restr is not None:
+            return '%s %s %s, %s' % (lhs, node.r_type, rhs, restr)
+        return '%s %s %s' % (lhs, node.r_type, rhs)
+
+    def visit_comparison(self, node):
+        if node.operator in ('=', 'IS'):
+            return node.children[0].accept(self)
+        return '%s %s' % (node.operator.encode(),
+                          node.children[0].accept(self))
+
+    def visit_mathexpression(self, node):
+        return '(%s %s %s)' % (node.children[0].accept(self),
+                               node.operator.encode(),
+                               node.children[1].accept(self))
+
+    def visit_function(self, node):
+        #if node.name == 'IN':
+        res = []
+        for child in node.children:
+            try:
+                rql = child.accept(self)
+            except UnknownEid, ex:
+                continue
+            res.append(rql)
+        if not res:
+            raise ex
+        return '%s(%s)' % (node.name, ', '.join(res))
+
+    def visit_constant(self, node):
+        if self.need_translation or node.uidtype:
+            if node.type == 'Int':
+                self.has_local_eid = True
+                return str(self.eid2extid(node.value))
+            if node.type == 'Substitute':
+                key = node.value
+                # ensure we have not yet translated the value...
+                if not key in self._const_var:
+                    self.kwargs[key] = self.eid2extid(self.kwargs[key])
+                    self._const_var[key] = None
+                    self.has_local_eid = True
+        return node.as_string()
+
+    def visit_variableref(self, node):
+        """get the sql name for a variable reference"""
+        return node.name
+
+    def visit_sortterm(self, node):
+        if node.asc:
+            return node.term.accept(self)
+        return '%s DESC' % node.term.accept(self)
+
+    def process_eid_const(self, const):
+        value = const.eval(self.kwargs)
+        try:
+            return None, self._const_var[value]
+        except Exception:
+            var = self._varmaker.next()
+            self.need_translation = True
+            restr = '%s eid %s' % (var, self.visit_constant(const))
+            self.need_translation = False
+            self._const_var[value] = var
+            return restr, var
+
+    def eid2extid(self, eid):
+        try:
+            return self.repo.eid2extid(self.source, eid, self._session)
+        except UnknownEid:
+            operator = self.current_operator
+            if operator is not None and operator != '=':
+                # deal with query like "X eid > 12"
+                #
+                # The problem is that eid order in the external source may
+                # differ from the local source
+                #
+                # So search for all eids from this source matching the condition
+                # locally and then to replace the "> 12" branch by "IN (eids)"
+                #
+                # XXX we may have to insert a huge number of eids...)
+                sql = "SELECT extid FROM entities WHERE source='%s' AND type IN (%s) AND eid%s%s"
+                etypes = ','.join("'%s'" % etype for etype in self.current_etypes)
+                cu = self._session.system_sql(sql % (self.source.uri, etypes,
+                                                      operator, eid))
+                # XXX buggy cu.rowcount which may be zero while there are some
+                # results
+                rows = cu.fetchall()
+                if rows:
+                    raise ReplaceByInOperator((b64decode(r[0]) for r in rows))
+            raise
+
--- a/server/sources/rql2sql.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/sources/rql2sql.py	Thu Mar 21 18:13:31 2013 +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.
@@ -1284,10 +1284,10 @@
     def _visit_var_attr_relation(self, relation, rhs_vars):
         """visit an attribute relation with variable(s) in the RHS
 
-        attribute variables are used either in the selection or for
-        unification (eg X attr1 A, Y attr2 A). In case of selection,
-        nothing to do here.
+        attribute variables are used either in the selection or for unification
+        (eg X attr1 A, Y attr2 A). In case of selection, nothing to do here.
         """
+        ored = relation.ored()
         for vref in rhs_vars:
             var = vref.variable
             if var.name in self._varmap:
@@ -1298,10 +1298,21 @@
                 principal = 1
             else:
                 principal = var.stinfo.get('principal')
-            if principal is not None and principal is not relation:
+            # we've to return some sql if:
+            # 1. visited relation is ored
+            # 2. variable's principal is not this relation and not 1.
+            if ored or (principal is not None and principal is not relation
+                        and not getattr(principal, 'ored', lambda : 0)()):
                 # we have to generate unification expression
-                lhssql = self._inlined_var_sql(relation.children[0].variable,
-                                               relation.r_type)
+                if principal is relation:
+                    # take care if ored case and principal is the relation to
+                    # use the right relation in the unification term
+                    _rel = [rel for rel in var.stinfo['rhsrelations']
+                            if not rel is principal][0]
+                else:
+                    _rel = relation
+                lhssql = self._inlined_var_sql(_rel.children[0].variable,
+                                               _rel.r_type)
                 try:
                     self._state.ignore_varmap = True
                     sql = lhssql + relation.children[1].accept(self)
--- a/server/sources/storages.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/sources/storages.py	Thu Mar 21 18:13:31 2013 +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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/zmqrql.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,27 @@
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Source to query another RQL repository using pyro"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from cubicweb.server.sources.remoterql import RemoteSource
+
+class ZMQRQLSource(RemoteSource):
+    """External repository source, using ZMQ sockets"""
+    CNX_TYPE = 'zmq'
--- a/server/sqlutils.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/sqlutils.py	Thu Mar 21 18:13:31 2013 +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/data/ldap_test.ldif	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/data/ldap_test.ldif	Thu Mar 21 18:13:31 2013 +0100
@@ -31,7 +31,7 @@
 gecos: Sylvain Thenault
 mail: sylvain.thenault@logilab.fr
 mail: syt@logilab.fr
-userPassword: {SSHA}v/8xJQP3uoaTBZz1T7Y0B3qOxRN1cj7D
+userPassword: syt
 
 dn: uid=adim,ou=People,dc=cubicweb,dc=test
 loginShell: /bin/bash
@@ -53,5 +53,5 @@
 gecos: Adrien Di Mascio
 mail: adim@logilab.fr
 mail: adrien.dimascio@logilab.fr
-userPassword: {SSHA}cPQOWqkkLDlfWFwxcl1m8V2JdySQBHfS
+userPassword: adim
 
--- a/server/test/data/slapd.conf.in	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/data/slapd.conf.in	Thu Mar 21 18:13:31 2013 +0100
@@ -45,9 +45,9 @@
 suffix          "dc=cubicweb,dc=test"
 
 # rootdn directive for specifying a superuser on the database. This is needed
-# for syncrepl.
-#rootdn          "cn=admin,dc=cubicweb,dc=test"
-#rootpw          "cubicwebrocks"
+# for syncrepl. and ldapdelete easyness
+rootdn          "cn=admin,dc=cubicweb,dc=test"
+rootpw          "cw"
 # Where the database file are physically stored for database #1
-directory       "%(apphome)s/ldapdb"
+directory       "%(testdir)s"
 
--- a/server/test/unittest_datafeed.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_datafeed.py	Thu Mar 21 18:13:31 2013 +0100
@@ -54,7 +54,7 @@
             stats = dfsource.pull_data(session, force=True)
             self.commit()
             # test import stats
-            self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
+            self.assertEqual(sorted(stats.keys()), ['checked', 'created', 'updated'])
             self.assertEqual(len(stats['created']), 1)
             entity = self.execute('Card X').get_entity(0, 0)
             self.assertIn(entity.eid, stats['created'])
--- a/server/test/unittest_ldapuser.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_ldapuser.py	Thu Mar 21 18:13:31 2013 +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.
@@ -16,100 +16,271 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb.server.sources.ldapusers unit and functional tests"""
+from __future__ import with_statement
 
 import os
 import shutil
 import time
-from os.path import abspath, join, exists
+from os.path import join, exists
 import subprocess
-from socket import socket, error as socketerror
+import tempfile
 
 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
 from cubicweb.devtools import get_test_db_handler
 
-from cubicweb.server.sources.ldapuser import *
+from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter
 
-CONFIG = u'''host=%s
-user-base-dn=ou=People,dc=cubicweb,dc=test
-user-scope=ONELEVEL
-user-classes=top,posixAccount
-user-login-attr=uid
-user-default-group=users
-user-attrs-map=gecos:email,uid:login
-'''
+CONFIG = u'user-base-dn=ou=People,dc=cubicweb,dc=test'
+URL = None
 
-
-def setUpModule(*args):
-    create_slapd_configuration(LDAPUserSourceTC.config)
-
-def tearDownModule(*args):
-    terminate_slapd()
-
-def create_slapd_configuration(config):
-    global slapd_process, CONFIG
-    basedir = join(config.apphome, "ldapdb")
+def create_slapd_configuration(cls):
+    global URL
+    slapddir = tempfile.mkdtemp('cw-unittest-ldap')
+    config = cls.config
     slapdconf = join(config.apphome, "slapd.conf")
     confin = file(join(config.apphome, "slapd.conf.in")).read()
     confstream = file(slapdconf, 'w')
-    confstream.write(confin % {'apphome': config.apphome})
+    confstream.write(confin % {'apphome': config.apphome, 'testdir': slapddir})
     confstream.close()
-    if not exists(basedir):
-        os.makedirs(basedir)
-        # fill ldap server with some data
-        ldiffile = join(config.apphome, "ldap_test.ldif")
-        print "Initing ldap database"
-        cmdline = "/usr/sbin/slapadd -f %s -l %s -c" % (slapdconf, ldiffile)
-        subprocess.call(cmdline, shell=True)
-
+    # fill ldap server with some data
+    ldiffile = join(config.apphome, "ldap_test.ldif")
+    config.info('Initing ldap database')
+    cmdline = "/usr/sbin/slapadd -f %s -l %s -c" % (slapdconf, ldiffile)
+    subprocess.call(cmdline, shell=True)
 
     #ldapuri = 'ldapi://' + join(basedir, "ldapi").replace('/', '%2f')
     port = get_available_port(xrange(9000, 9100))
     host = 'localhost:%s' % port
     ldapuri = 'ldap://%s' % host
     cmdline = ["/usr/sbin/slapd", "-f",  slapdconf,  "-h",  ldapuri, "-d", "0"]
-    print 'Starting slapd:', ' '.join(cmdline)
-    slapd_process = subprocess.Popen(cmdline)
+    config.info('Starting slapd:', ' '.join(cmdline))
+    cls.slapd_process = subprocess.Popen(cmdline)
     time.sleep(0.2)
-    if slapd_process.poll() is None:
-        print "slapd started with pid %s" % slapd_process.pid
+    if cls.slapd_process.poll() is None:
+        config.info('slapd started with pid %s' % cls.slapd_process.pid)
     else:
         raise EnvironmentError('Cannot start slapd with cmdline="%s" (from directory "%s")' %
                                (" ".join(cmdline), os.getcwd()))
-    CONFIG = CONFIG % host
+    URL = u'ldap://%s' % host
+    return slapddir
 
-def terminate_slapd():
-    global slapd_process
-    if slapd_process.returncode is None:
-        print "terminating slapd"
-        if hasattr(slapd_process, 'terminate'):
-            slapd_process.terminate()
+def terminate_slapd(cls):
+    config = cls.config
+    if cls.slapd_process and cls.slapd_process.returncode is None:
+        config.info('terminating slapd')
+        if hasattr(cls.slapd_process, 'terminate'):
+            cls.slapd_process.terminate()
         else:
             import os, signal
-            os.kill(slapd_process.pid, signal.SIGTERM)
-        slapd_process.wait()
-        print "DONE"
-    del slapd_process
+            os.kill(cls.slapd_process.pid, signal.SIGTERM)
+        cls.slapd_process.wait()
+        config.info('DONE')
+
+class LDAPTestBase(CubicWebTC):
+    loglevel = 'ERROR'
+
+    @classmethod
+    def setUpClass(cls):
+        from cubicweb.cwctl import init_cmdline_log_threshold
+        init_cmdline_log_threshold(cls.config, cls.loglevel)
+        cls._tmpdir = create_slapd_configuration(cls)
+
+    @classmethod
+    def tearDownClass(cls):
+        terminate_slapd(cls)
+        try:
+            shutil.rmtree(cls._tmpdir)
+        except:
+            pass
+
+class CheckWrongGroup(LDAPTestBase):
+
+    def test_wrong_group(self):
+        self.session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
+                                   url=URL, config=CONFIG)
+        self.commit()
+        with self.session.repo.internal_session(safe=True) as session:
+            source = self.session.execute('CWSource S WHERE S type="ldapfeed"').get_entity(0,0)
+            config = source.repo_source.check_config(source)
+            # inject a bogus group here, along with at least a valid one
+            config['user-default-group'] = ('thisgroupdoesnotexists','users')
+            source.repo_source.update_config(source, config)
+            session.commit(free_cnxset=False)
+            # here we emitted an error log entry
+            stats = source.repo_source.pull_data(session, force=True, raise_on_error=True)
+            session.commit()
+
+class DeleteStuffFromLDAPFeedSourceTC(LDAPTestBase):
+    test_db_id = 'ldap-feed'
+
+    @classmethod
+    def pre_setup_database(cls, session, config):
+        session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
+                              url=URL, config=CONFIG)
+        session.commit()
+        with session.repo.internal_session(safe=True) as isession:
+            lfsource = isession.repo.sources_by_uri['ldapuser']
+            stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+
+    def _pull(self):
+        with self.session.repo.internal_session() as isession:
+            lfsource = isession.repo.sources_by_uri['ldapuser']
+            stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+            isession.commit()
+
+    def test_a_filter_inactivate(self):
+        """ filtered out people should be deactivated, unable to authenticate """
+        source = self.session.execute('CWSource S WHERE S type="ldapfeed"').get_entity(0,0)
+        config = source.repo_source.check_config(source)
+        # filter with adim's phone number
+        config['user-filter'] = u'(%s=%s)' % ('telephoneNumber', '109')
+        source.repo_source.update_config(source, config)
+        self.commit()
+        self._pull()
+        self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
+        self.assertEqual(self.execute('Any N WHERE U login "syt", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'deactivated')
+        self.assertEqual(self.execute('Any N WHERE U login "adim", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'activated')
+        # unfilter, syt should be activated again
+        config['user-filter'] = u''
+        source.repo_source.update_config(source, config)
+        self.commit()
+        self._pull()
+        self.assertEqual(self.execute('Any N WHERE U login "syt", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'activated')
+        self.assertEqual(self.execute('Any N WHERE U login "adim", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'activated')
 
-class LDAPUserSourceTC(CubicWebTC):
+    def test_delete(self):
+        """ delete syt, pull, check deactivation, repull,
+        readd syt, pull, check activation
+        """
+        uri = self.repo.sources_by_uri['ldapuser'].urls[0]
+        deletecmd = ("ldapdelete -H %s 'uid=syt,ou=People,dc=cubicweb,dc=test' "
+                     "-v -x -D cn=admin,dc=cubicweb,dc=test -w'cw'" % uri)
+        os.system(deletecmd)
+        self._pull()
+        self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
+        self.assertEqual(self.execute('Any N WHERE U login "syt", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'deactivated')
+        # check that it doesn't choke
+        self._pull()
+        # reset the fscking ldap thing
+        self.tearDownClass()
+        self.setUpClass()
+        self._pull()
+        self.assertEqual(self.execute('Any N WHERE U login "syt", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'activated')
+        # test reactivating the user isn't enough to authenticate, as the native source
+        # refuse to authenticate user from other sources
+        os.system(deletecmd)
+        self._pull()
+        user = self.execute('CWUser U WHERE U login "syt"').get_entity(0, 0)
+        user.cw_adapt_to('IWorkflowable').fire_transition('activate')
+        self.commit()
+        self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
+
+class LDAPFeedSourceTC(LDAPTestBase):
+    test_db_id = 'ldap-feed'
+
+    @classmethod
+    def pre_setup_database(cls, session, config):
+        session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
+                              url=URL, config=CONFIG)
+        session.commit()
+        isession = session.repo.internal_session(safe=True)
+        lfsource = isession.repo.sources_by_uri['ldapuser']
+        stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+
+    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)
+        source.pull_data(self.session)
+        rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
+        self.assertEqual(len(rset), 1)
+        self.assertTrue(self.repo.system_source.authenticate(
+                self.session, 'syt', password='syt'))
+
+
+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 +292,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 +517,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 +552,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 +586,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_msplanner.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_postgres.py	Thu Mar 21 18:13:31 2013 +0100
@@ -25,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
@@ -50,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):
@@ -67,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):
@@ -80,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_querier.py	Thu Mar 21 18:13:31 2013 +0100
@@ -122,7 +122,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],
@@ -147,7 +147,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'},
@@ -168,7 +168,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'},
@@ -189,12 +188,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)')
@@ -204,7 +205,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)'])
 
@@ -1118,13 +1119,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_repository.py	Thu Mar 21 18:13:31 2013 +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,9 +34,9 @@
 
 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.dbapi import connect, multiple_connections_unfix, ConnectionProperties
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import tuplify
 from cubicweb.server import repository, hook
@@ -113,6 +113,8 @@
         self.assertRaises(AuthenticationError,
                           self.repo.connect, self.admlogin, password='nimportnawak')
         self.assertRaises(AuthenticationError,
+                          self.repo.connect, self.admlogin, password='')
+        self.assertRaises(AuthenticationError,
                           self.repo.connect, self.admlogin, password=None)
         self.assertRaises(AuthenticationError,
                           self.repo.connect, None, password=None)
@@ -379,6 +381,65 @@
             # connect monkey patch some method by default, remove them
             multiple_connections_unfix()
 
+
+    def test_zmq(self):
+        try:
+            import zmq
+        except ImportError:
+            self.skipTest("zmq in not available")
+        done = []
+        from cubicweb.devtools import TestServerConfiguration as ServerConfiguration
+        from cubicweb.server.cwzmq import ZMQRepositoryServer
+        # the client part has to be in a thread due to sqlite limitations
+        t = threading.Thread(target=self._zmq_client, args=(done,))
+        t.start()
+
+        zmq_server = ZMQRepositoryServer(self.repo)
+        zmq_server.connect('tcp://127.0.0.1:41415')
+
+        t2 = threading.Thread(target=self._zmq_quit, args=(done, zmq_server,))
+        t2.start()
+
+        zmq_server.run()
+
+        t2.join(1)
+        t.join(1)
+
+        if t.isAlive():
+            self.fail('something went wrong, thread still alive')
+
+    def _zmq_quit(self, done, srv):
+        while not done:
+            time.sleep(0.1)
+        srv.quit()
+
+    def _zmq_client(self, done):
+        cnxprops = ConnectionProperties('zmq')
+        try:
+            cnx = connect('tcp://127.0.0.1:41415', u'admin', password=u'gingkow',
+                          cnxprops=cnxprops,
+                          initlog=False) # don't reset logging configuration
+            try:
+                cnx.load_appobjects(subpath=('entities',))
+                # check we can get the schema
+                schema = cnx.get_schema()
+                self.assertTrue(cnx.vreg)
+                self.assertTrue('etypes'in cnx.vreg)
+                cu = cnx.cursor()
+                rset = cu.execute('Any U,G WHERE U in_group G')
+                user = iter(rset.entities()).next()
+                self.assertTrue(user._cw)
+                self.assertTrue(user._cw.vreg)
+                from cubicweb.entities import authobjs
+                self.assertIsInstance(user._cw.user, authobjs.CWUser)
+                cnx.close()
+                done.append(True)
+            finally:
+                # connect monkey patch some method by default, remove them
+                multiple_connections_unfix()
+        finally:
+            done.append(False)
+
     def test_internal_api(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
--- a/server/test/unittest_rql2sql.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_rql2sql.py	Thu Mar 21 18:13:31 2013 +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.
@@ -219,6 +219,11 @@
 
 
 ADVANCED = [
+    ("Societe S WHERE S2 is Societe, S2 nom SN, S nom 'Logilab' OR S nom SN",
+     '''SELECT _S.cw_eid
+FROM cw_Societe AS _S, cw_Societe AS _S2
+WHERE ((_S.cw_nom=Logilab) OR (_S2.cw_nom=_S.cw_nom))'''),
+
     ("Societe S WHERE S nom 'Logilab' OR S nom 'Caesium'",
      '''SELECT _S.cw_eid
 FROM cw_Societe AS _S
--- a/server/test/unittest_security.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_security.py	Thu Mar 21 18:13:31 2013 +0100
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """functional tests for server'security"""
+from __future__ import with_statement
 
 import sys
 
@@ -24,9 +25,10 @@
 from rql import RQLException
 
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb import Unauthorized, ValidationError, QueryError
+from cubicweb import Unauthorized, ValidationError, QueryError, Binary
 from cubicweb.schema import ERQLExpression
 from cubicweb.server.querier import check_read_access
+from cubicweb.server.utils import _CRYPTO_CTX
 
 
 class BaseSecurityTC(CubicWebTC):
@@ -34,7 +36,8 @@
     def setup_database(self):
         super(BaseSecurityTC, self).setup_database()
         self.create_user(self.request(), 'iaminusersgrouponly')
-
+        hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
+        self.create_user(self.request(), 'oldpassword', password=Binary(hash))
 
 class LowLevelSecurityFunctionTC(BaseSecurityTC):
 
@@ -59,6 +62,18 @@
             self.assertRaises(Unauthorized,
                               cu.execute, 'Any X,P WHERE X is CWUser, X upassword P')
 
+    def test_update_password(self):
+        """Ensure that if a user's password is stored with a deprecated hash, it will be updated on next login"""
+        oldhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+        with self.login('oldpassword') as cu:
+            pass
+        newhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+        self.assertNotEqual(oldhash, newhash)
+        self.assertTrue(newhash.startswith('$6$'))
+        with self.login('oldpassword') as cu:
+            pass
+        self.assertEqual(newhash, str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0]))
+
 
 class SecurityRewritingTC(BaseSecurityTC):
     def hijack_source_execute(self):
@@ -590,5 +605,23 @@
                           self.execute, 'SET TI to_state S WHERE TI eid %(ti)s, S name "pitetre"',
                           {'ti': trinfo.eid})
 
+    def test_emailaddress_security(self):
+        # check for prexisting email adresse
+        if self.execute('Any X WHERE X is EmailAddress'):
+            rset = self.execute('Any X, U WHERE X is EmailAddress, U use_email X')
+            msg = ['Preexisting email readable by anon found!']
+            tmpl = '  - "%s" used by user "%s"'
+            for i in xrange(len(rset)):
+                email, user = rset.get_entity(i, 0), rset.get_entity(i, 1)
+                msg.append(tmpl % (email.dc_title(), user.dc_title()))
+            raise RuntimeError('\n'.join(msg))
+        # actual test
+        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	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_storage.py	Thu Mar 21 18:13:31 2013 +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/server/test/unittest_undo.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/test/unittest_undo.py	Thu Mar 21 18:13:31 2013 +0100
@@ -22,14 +22,14 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.transaction import *
 
-from cubicweb.server.sources.native import UndoException
+from cubicweb.server.sources.native import UndoTransactionException, _UndoException
 
 
 class UndoableTransactionTC(CubicWebTC):
 
     def setup_database(self):
         req = self.request()
-        self.session.undo_actions = set('CUDAR')
+        self.session.undo_actions = True
         self.toto = self.create_user(req, 'toto', password='toto', groups=('users',),
                                      commit=False)
         self.txuuid = self.commit()
@@ -48,6 +48,17 @@
             "SELECT * from tx_relation_actions WHERE tx_uuid='%s'" % txuuid)
         self.assertFalse(cu.fetchall())
 
+    def assertUndoTransaction(self, txuuid, expected_errors=None):
+        if expected_errors is None :
+            expected_errors = []
+        try:
+            self.cnx.undo_transaction(txuuid)
+        except UndoTransactionException, exn:
+            errors = exn.errors
+        else:
+            errors = []
+        self.assertEqual(errors, expected_errors)
+
     def test_undo_api(self):
         self.assertTrue(self.txuuid)
         # test transaction api
@@ -69,12 +80,14 @@
         self.assertEqual(a1.action, 'C')
         self.assertEqual(a1.eid, self.toto.eid)
         self.assertEqual(a1.etype,'CWUser')
+        self.assertEqual(a1.ertype, 'CWUser')
         self.assertEqual(a1.changes, None)
         self.assertEqual(a1.public, True)
         self.assertEqual(a1.order, 1)
         a4 = actions[3]
         self.assertEqual(a4.action, 'A')
         self.assertEqual(a4.rtype, 'in_group')
+        self.assertEqual(a4.ertype, 'in_group')
         self.assertEqual(a4.eid_from, self.toto.eid)
         self.assertEqual(a4.eid_to, self.toto.in_group[0].eid)
         self.assertEqual(a4.order, 4)
@@ -155,10 +168,9 @@
         self.assertEqual(len(actions), 1)
         toto.cw_clear_all_caches()
         e.cw_clear_all_caches()
-        errors = self.cnx.undo_transaction(txuuid)
+        self.assertUndoTransaction(txuuid)
         undotxuuid = self.commit()
         self.assertEqual(undotxuuid, None) # undo not undoable
-        self.assertEqual(errors, [])
         self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid}))
         self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid}))
         self.assertTrue(self.execute('Any X WHERE X has_text "toto@logilab"'))
@@ -193,14 +205,12 @@
         c2 = session.create_entity('Card', title=u'hip', content=u'hip')
         p.set_relations(fiche=c2)
         self.commit()
-        errors = self.cnx.undo_transaction(txuuid)
+        self.assertUndoTransaction(txuuid, [
+            "Can't restore object relation fiche to entity "
+            "%s which is already linked using this relation." % p.eid])
         self.commit()
         p.cw_clear_all_caches()
         self.assertEqual(p.fiche[0].eid, c2.eid)
-        self.assertEqual(len(errors), 1)
-        self.assertEqual(errors[0],
-                          "Can't restore object relation fiche to entity "
-                          "%s which is already linked using this relation." % p.eid)
 
     def test_undo_deletion_integrity_2(self):
         # test validation error raised if we can't restore a required relation
@@ -213,10 +223,9 @@
         txuuid = self.commit()
         g.cw_delete()
         self.commit()
-        errors = self.cnx.undo_transaction(txuuid)
-        self.assertEqual(errors,
-                          [u"Can't restore relation in_group, object entity "
-                          "%s doesn't exist anymore." % g.eid])
+        self.assertUndoTransaction(txuuid, [
+            u"Can't restore relation in_group, object entity "
+            "%s doesn't exist anymore." % g.eid])
         with self.assertRaises(ValidationError) as cm:
             self.commit()
         self.assertEqual(cm.exception.entity, self.toto.eid)
@@ -229,9 +238,8 @@
         c = session.create_entity('Card', title=u'hop', content=u'hop')
         p = session.create_entity('Personne', nom=u'louis', fiche=c)
         txuuid = self.commit()
-        errors = self.cnx.undo_transaction(txuuid)
+        self.assertUndoTransaction(txuuid)
         self.commit()
-        self.assertFalse(errors)
         self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': c.eid}))
         self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': p.eid}))
         self.assertFalse(self.execute('Any X,Y WHERE X fiche Y'))
@@ -288,12 +296,135 @@
 
     # test implicit 'replacement' of an inlined relation
 
+    def test_undo_inline_rel_remove_ok(self):
+        """Undo remove relation  Personne (?) fiche (?) Card
+
+        NB: processed by `_undo_r` as expected"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c)
+        self.commit()
+        p.set_relations(fiche=None)
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertEqual(p.fiche[0].eid, c.eid)
+
+    def test_undo_inline_rel_remove_ko(self):
+        """Restore an inlined relation to a deleted entity, with an error.
+
+        NB: processed by `_undo_r` as expected"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c)
+        self.commit()
+        p.set_relations(fiche=None)
+        txuuid = self.commit()
+        c.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid, [
+            "Can't restore relation fiche, object entity %d doesn't exist anymore." % c.eid])
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertFalse(p.fiche)
+        self.assertIsNone(session.system_sql(
+            'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0])
+
+    def test_undo_inline_rel_add_ok(self):
+        """Undo add relation  Personne (?) fiche (?) Card
+
+        Caution processed by `_undo_u`, not `_undo_a` !"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis')
+        self.commit()
+        p.set_relations(fiche=c)
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertFalse(p.fiche)
+
+    def test_undo_inline_rel_add_ko(self):
+        """Undo add relation  Personne (?) fiche (?) Card
+
+        Caution processed by `_undo_u`, not `_undo_a` !"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis')
+        self.commit()
+        p.set_relations(fiche=c)
+        txuuid = self.commit()
+        c.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid)
+
+    def test_undo_inline_rel_replace_ok(self):
+        """Undo changing relation  Personne (?) fiche (?) Card
+
+        Caution processed by `_undo_u` """
+        session = self.session
+        c1 = session.create_entity('Card', title=u'hop', content=u'hop')
+        c2 = session.create_entity('Card', title=u'hip', content=u'hip')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c1)
+        self.commit()
+        p.set_relations(fiche=c2)
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertEqual(p.fiche[0].eid, c1.eid)
+
+    def test_undo_inline_rel_replace_ko(self):
+        """Undo changing relation  Personne (?) fiche (?) Card, with an error
+
+        Caution processed by `_undo_u` """
+        session = self.session
+        c1 = session.create_entity('Card', title=u'hop', content=u'hop')
+        c2 = session.create_entity('Card', title=u'hip', content=u'hip')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c1)
+        self.commit()
+        p.set_relations(fiche=c2)
+        txuuid = self.commit()
+        c1.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid, [
+            "can't restore entity %s of type Personne, target of fiche (eid %s)"
+            " does not exist any longer" % (p.eid, c1.eid)])
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertFalse(p.fiche)
+
+    def test_undo_attr_update_ok(self):
+        session = self.session
+        p = session.create_entity('Personne', nom=u'toto')
+        session.commit()
+        self.session.set_cnxset()
+        p.set_attributes(nom=u'titi')
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        p.cw_clear_all_caches()
+        self.assertEqual(p.nom, u'toto')
+
+    def test_undo_attr_update_ko(self):
+        session = self.session
+        p = session.create_entity('Personne', nom=u'toto')
+        session.commit()
+        self.session.set_cnxset()
+        p.set_attributes(nom=u'titi')
+        txuuid = self.commit()
+        p.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid, [
+            u"can't restore state of entity %s, it has been deleted inbetween" % p.eid])
+
 
 class UndoExceptionInUnicode(CubicWebTC):
 
     # problem occurs in string manipulation for python < 2.6
     def test___unicode__method(self):
-        u = UndoException(u"voilà")
+        u = _UndoException(u"voilà")
         self.assertIsInstance(unicode(u), unicode)
 
 
--- a/server/utils.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/server/utils.py	Thu Mar 21 18:13:31 2013 +0100
@@ -52,7 +52,9 @@
         return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8')
     _calc_checksum = calc_checksum
 
-_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt'])
+_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'],
+                           deprecated=['cubicwebmd5crypt', 'des_crypt'])
+verify_and_update = _CRYPTO_CTX.verify_and_update
 
 def crypt_password(passwd, salt=None):
     """return the encrypted password using the given salt or a generated one
@@ -62,8 +64,11 @@
     # empty hash, accept any password for backwards compat
     if salt == '':
         return salt
-    if _CRYPTO_CTX.verify(passwd, salt):
-        return salt
+    try:
+        if _CRYPTO_CTX.verify(passwd, salt):
+            return salt
+    except ValueError: # e.g. couldn't identify hash
+        pass
     # wrong password
     return ''
 
@@ -139,12 +144,12 @@
 
 class LoopTask(object):
     """threaded task restarting itself once executed"""
-    def __init__(self, repo, interval, func, args):
+    def __init__(self, tasks_manager, interval, func, args):
         if interval <= 0:
             raise ValueError('Loop task interval must be > 0 '
                              '(current value: %f for %s)' % \
                              (interval, func_name(func)))
-        self.repo = repo
+        self._tasks_manager = tasks_manager
         self.interval = interval
         def auto_restart_func(self=self, func=func, args=args):
             restart = True
@@ -157,7 +162,7 @@
             except BaseException:
                 restart = False
             finally:
-                if restart and not self.repo.shutting_down:
+                if restart and tasks_manager.running:
                     self.start()
         self.func = auto_restart_func
         self.name = func_name(func)
@@ -203,3 +208,54 @@
 
     def getName(self):
         return '%s(%s)' % (self._name, Thread.getName(self))
+
+class TasksManager(object):
+    """Object dedicated manage background task"""
+
+    def __init__(self):
+        self.running = False
+        self._tasks = []
+        self._looping_tasks = []
+
+    def add_looping_task(self, interval, func, *args):
+        """register a function to be called every `interval` seconds.
+
+        looping tasks can only be registered during repository initialization,
+        once done this method will fail.
+        """
+        task = LoopTask(self, interval, func, args)
+        if self.running:
+            self._start_task(task)
+        else:
+            self._tasks.append(task)
+
+    def _start_task(self, task):
+        self._looping_tasks.append(task)
+        self.info('starting task %s with interval %.2fs', task.name,
+                  task.interval)
+        task.start()
+
+    def start(self):
+        """Start running looping task"""
+        assert self.running == False # bw compat purpose maintly
+        while self._tasks:
+            task = self._tasks.pop()
+            self._start_task(task)
+        self.running = True
+
+    def stop(self):
+        """Stop all running task.
+
+        returns when all task have been cancel and none are running anymore"""
+        if self.running:
+            while self._looping_tasks:
+                looptask = self._looping_tasks.pop()
+                self.info('canceling task %s...', looptask.name)
+                looptask.cancel()
+                looptask.join()
+                self.info('task %s finished', looptask.name)
+
+from logging import getLogger
+from cubicweb import set_log_methods
+set_log_methods(TasksManager, getLogger('cubicweb.repository'))
+
--- a/skeleton/__pkginfo__.py.tmpl	Wed Aug 01 10:30:48 2012 +0200
+++ b/skeleton/__pkginfo__.py.tmpl	Thu Mar 21 18:13:31 2013 +0100
@@ -16,6 +16,12 @@
 __depends__ =  %(dependencies)s
 __recommends__ = {}
 
+classifiers = [
+    'Environment :: Web Environment',
+    'Framework :: CubicWeb',
+    'Programming Language :: Python',
+    'Programming Language :: JavaScript',
+    ]
 
 from os import listdir as _listdir
 from os.path import join, isdir
--- a/skeleton/debian/DISTNAME.prerm.tmpl	Wed Aug 01 10:30:48 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-#!/bin/sh -e
-
-delete_pyo_pyc () {
-  find /usr/share/cubicweb/cubes/%(cubename)s -name "*.pyc" | xargs rm -f
-  find /usr/share/cubicweb/cubes/%(cubename)s -name "*.pyo" | xargs rm -f
-}
-
-
-case "$1" in
-    failed-upgrade|abort-install|abort-upgrade|disappear)
-    ;;
-    upgrade)
-    delete_pyo_pyc
-    ;;
-    remove)
-    delete_pyo_pyc
-    ;;
-    purge)
-    ;;
-
-    *)
-        echo "postrm called with unknown argument \`$1'" >&2
-        exit 1
-
-esac
-
-#DEBHELPER#
--- a/skeleton/debian/compat	Wed Aug 01 10:30:48 2012 +0200
+++ b/skeleton/debian/compat	Thu Mar 21 18:13:31 2013 +0100
@@ -1,1 +1,1 @@
-5
+7
--- a/skeleton/debian/control.tmpl	Wed Aug 01 10:30:48 2012 +0200
+++ b/skeleton/debian/control.tmpl	Thu Mar 21 18:13:31 2013 +0100
@@ -2,13 +2,13 @@
 Section: web
 Priority: optional
 Maintainer: %(author)s <%(author-email)s>
-Build-Depends: debhelper (>= 5.0.37.1), python (>=2.4), python-support
-Standards-Version: 3.8.0
-
+Build-Depends: debhelper (>= 7), python (>=2.5), python-support
+Standards-Version: 3.9.3
+XS-Python-Version: >= 2.5
 
 Package: %(distname)s
 Architecture: all
-Depends: cubicweb-common (>= %(version)s)
+Depends: cubicweb-common (>= %(version)s), ${python:Depends}
 Description: %(shortdesc)s
  CubicWeb is a semantic web application framework.
  .
--- a/skeleton/debian/copyright.tmpl	Wed Aug 01 10:30:48 2012 +0200
+++ b/skeleton/debian/copyright.tmpl	Thu Mar 21 18:13:31 2013 +0100
@@ -1,4 +1,4 @@
-Upstream Author: 
+Upstream Author:
 
   %(author)s <%(author-email)s>
 
--- a/skeleton/debian/rules.tmpl	Wed Aug 01 10:30:48 2012 +0200
+++ b/skeleton/debian/rules.tmpl	Thu Mar 21 18:13:31 2013 +0100
@@ -4,7 +4,10 @@
 
 # Uncomment this to turn on verbose mode.
 #export DH_VERBOSE=1
-build: build-stamp
+build: build-arch build-indep
+build-arch:
+	# Nothing to do
+build-indep: build-stamp
 build-stamp:
 	dh_testdir
 	NO_SETUPTOOLS=1 python setup.py -q build
--- a/skeleton/setup.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/skeleton/setup.py	Thu Mar 21 18:13:31 2013 +0100
@@ -41,7 +41,7 @@
 
 # import required features
 from __pkginfo__ import modname, version, license, description, web, \
-     author, author_email
+     author, author_email, classifiers
 
 if exists('README'):
     long_description = file('README').read()
@@ -193,6 +193,7 @@
                  data_files = data_files,
                  ext_modules = ext_modules,
                  cmdclass = cmdclass,
+                 classifiers = classifiers,
                  **kwargs
                  )
 
--- a/sobjects/__init__.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/sobjects/__init__.py	Thu Mar 21 18:13:31 2013 +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 Mar 21 18:13:31 2013 +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):
+        """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)
+
+    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.startswith('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 Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,184 @@
+# 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 __future__ import with_statement
+
+from logilab.common.decorators import cached, cachedproperty
+from logilab.common.shellutils import generate_password
+
+from cubicweb import Binary, ConfigurationError
+from cubicweb.server.utils import crypt_password
+from cubicweb.server.sources import datafeed
+
+
+class DataFeedLDAPAdapter(datafeed.DataFeedParser):
+    __regid__ = 'ldapfeed'
+    # attributes that may appears in source user_attrs dict which are not
+    # attributes of the cw user
+    non_attribute_keys = set(('email',))
+
+    @cachedproperty
+    def searchfilterstr(self):
+        """ ldap search string, including user-filter """
+        return '(&%s)' % ''.join(self.source.base_filters)
+
+    @cachedproperty
+    def source_entities_by_extid(self):
+        source = self.source
+        return dict((userdict['dn'], userdict)
+                    for userdict in source._search(self._cw,
+                                                   source.user_base_dn,
+                                                   source.user_base_scope,
+                                                   self.searchfilterstr))
+
+    def process(self, url, raise_on_error=False):
+        """IDataFeedParser main entry point"""
+        self.debug('processing ldapfeed source %s %s', self.source, self.searchfilterstr)
+        for userdict in self.source_entities_by_extid.itervalues():
+            self.warning('fetched user %s', userdict)
+            extid = userdict['dn']
+            entity = self.extid2entity(extid, 'CWUser', **userdict)
+            if entity is not None and not self.created_during_pull(entity):
+                self.notify_updated(entity)
+                attrs = self.ldap2cwattrs(userdict)
+                self.update_if_necessary(entity, attrs)
+                self._process_email(entity, userdict)
+
+
+    def handle_deletion(self, config, session, myuris):
+        if config['delete-entities']:
+            super(DataFeedLDAPAdapter, self).handle_deletion(config, session, myuris)
+            return
+        if myuris:
+            byetype = {}
+            for extid, (eid, etype) in myuris.iteritems():
+                if self.is_deleted(extid, etype, eid):
+                    byetype.setdefault(etype, []).append(str(eid))
+
+            for etype, eids in byetype.iteritems():
+                if etype != 'CWUser':
+                    continue
+                self.warning('deactivate %s %s entities', len(eids), etype)
+                for eid in eids:
+                    wf = session.entity_from_eid(eid).cw_adapt_to('IWorkflowable')
+                    wf.fire_transition_if_possible('deactivate')
+        session.commit(free_cnxset=False)
+
+    def update_if_necessary(self, entity, attrs):
+        # disable read security to allow password selection
+        with entity._cw.security_enabled(read=False):
+            entity.complete(tuple(attrs))
+        if entity.__regid__ == 'CWUser':
+            wf = entity.cw_adapt_to('IWorkflowable')
+            if wf.state == 'deactivated':
+                wf.fire_transition('activate')
+                self.warning('user %s reactivated', entity.login)
+        mdate = attrs.get('modification_date')
+        if not mdate or mdate > entity.modification_date:
+            attrs = dict( (k, v) for k, v in attrs.iteritems()
+                          if v != getattr(entity, k))
+            if attrs:
+                entity.set_attributes(**attrs)
+                self.notify_updated(entity)
+
+    def ldap2cwattrs(self, sdict, tdict=None):
+        if tdict is None:
+            tdict = {}
+        for sattr, tattr in self.source.user_attrs.iteritems():
+            if tattr not in self.non_attribute_keys:
+                try:
+                    tdict[tattr] = sdict[sattr]
+                except KeyError:
+                    raise ConfigurationError('source attribute %s is not present '
+                                             'in the source, please check the '
+                                             'user-attrs-map field' % sattr)
+        return tdict
+
+    def before_entity_copy(self, entity, sourceparams):
+        if entity.__regid__ == 'EmailAddress':
+            entity.cw_edited['address'] = sourceparams['address']
+        else:
+            self.ldap2cwattrs(sourceparams, entity.cw_edited)
+            pwd = entity.cw_edited.get('upassword')
+            if not pwd:
+                # generate a dumb password if not fetched from ldap (see
+                # userPassword)
+                pwd = crypt_password(generate_password())
+                entity.cw_edited['upassword'] = Binary(pwd)
+        return entity
+
+    def after_entity_copy(self, entity, sourceparams):
+        super(DataFeedLDAPAdapter, self).after_entity_copy(entity, sourceparams)
+        if entity.__regid__ == 'EmailAddress':
+            return
+        groups = filter(None, [self._get_group(name)
+                               for name in self.source.user_default_groups])
+        if groups:
+            entity.set_relations(in_group=groups)
+        self._process_email(entity, sourceparams)
+
+    def is_deleted(self, extidplus, etype, eid):
+        try:
+            extid, _ = extidplus.rsplit('@@', 1)
+        except ValueError:
+            # for some reason extids here tend to come in both forms, e.g:
+            # dn, dn@@Babar
+            extid = extidplus
+        return extid not in self.source_entities_by_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)
+            elif self.sourceuris:
+                # pop from sourceuris anyway, else email may be removed by the
+                # source once import is finished
+                uri = userdict['dn'] + '@@' + emailaddr.encode('utf-8')
+                self.sourceuris.pop(uri, None)
+            # XXX else check use_email relation?
+
+    @cached
+    def _get_group(self, name):
+        try:
+            return self._cw.execute('Any X WHERE X is CWGroup, X name %(name)s',
+                                    {'name': name}).get_entity(0, 0)
+        except IndexError:
+            self.error('group %r referenced by source configuration %r does not exist'
+                       % (name, self.source.uri))
+            return None
+
--- a/sobjects/notification.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/sobjects/notification.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ /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	Wed Aug 01 10:30:48 2012 +0200
+++ b/sobjects/supervising.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/sobjects/test/data/sobjects/__init__.py	Thu Mar 21 18:13:31 2013 +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 Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,329 @@
+# 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()), ['checked', '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']), 0)
+        self.assertEqual(len(stats['checked']), 2)
+        self.repo._type_source_cache.clear()
+        self.repo._extid_cache.clear()
+        session.set_cnxset()
+        with session.security_enabled(read=False): # avoid Unauthorized due to password selection
+            stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+        self.assertEqual(stats['created'], set())
+        self.assertEqual(len(stats['updated']), 0)
+        self.assertEqual(len(stats['checked']), 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	Wed Aug 01 10:30:48 2012 +0200
+++ /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/sobjects/test/unittest_supervising.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/sobjects/test/unittest_supervising.py	Thu Mar 21 18:13:31 2013 +0100
@@ -72,7 +72,7 @@
 
 * added relation bookmarked_by from bookmark #EID to cwuser #EID
 
-* updated comment #EID (#EID)
+* updated comment #EID (duh?)
   http://testing.fr/cubicweb/comment/EID
 
 * deleted relation comments from comment #EID to card #EID''',
--- a/test/data/views.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/test/data/views.py	Thu Mar 21 18:13:31 2013 +0100
@@ -17,3 +17,17 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 from cubicweb.web.views import xmlrss
 xmlrss.RSSIconBox.visible = True
+
+
+from cubicweb.predicates import match_user_groups
+from cubicweb.server import Service
+
+
+class TestService(Service):
+    __regid__ = 'test_service'
+    __select__ = Service.__select__ & match_user_groups('managers')
+    passed_here = []
+
+    def call(self, msg):
+        self.passed_here.append(msg)
+        return 'babar'
--- a/test/unittest_dbapi.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/test/unittest_dbapi.py	Thu Mar 21 18:13:31 2013 +0100
@@ -23,10 +23,11 @@
 
 from logilab.common import tempattr
 
-from cubicweb import ConnectionError, cwconfig
+from cubicweb import ConnectionError, cwconfig, NoSelectableObject
 from cubicweb.dbapi import ProgrammingError
 from cubicweb.devtools.testlib import CubicWebTC
 
+
 class DBAPITC(CubicWebTC):
 
     def test_public_repo_api(self):
@@ -82,6 +83,20 @@
             req.ajax_replace_url('domid') # don't crash
             req.user.cw_adapt_to('IBreadCrumbs') # don't crash
 
+    def test_call_service(self):
+        ServiceClass = self.vreg['services']['test_service'][0]
+        for _cw in (self.request(), self.session):
+            ret_value = _cw.call_service('test_service', msg='coucou')
+            self.assertEqual('coucou', ServiceClass.passed_here.pop())
+            self.assertEqual('babar', ret_value)
+        with self.login('anon') as ctm:
+            for _cw in (self.request(), self.session):
+                with self.assertRaises(NoSelectableObject):
+                    _cw.call_service('test_service', msg='toto')
+                self.rollback()
+                self.assertEqual([], ServiceClass.passed_here)
+
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/test/unittest_entity.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/test/unittest_entity.py	Thu Mar 21 18:13:31 2013 +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.
@@ -18,14 +18,19 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for cubicweb.web.views.entities module"""
 
+from __future__ import with_statement
+
 from datetime import datetime
+
 from logilab.common import tempattr
+from logilab.common.decorators import clear_cache
+
 from cubicweb import Binary, Unauthorized
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.mttransforms import HAS_TAL
 from cubicweb.entities import fetch_config
 from cubicweb.uilib import soup2xhtml
-from cubicweb.schema import RQLVocabularyConstraint
+from cubicweb.schema import RQLVocabularyConstraint, RRQLExpression
 
 class EntityTC(CubicWebTC):
 
@@ -316,12 +321,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')
@@ -348,6 +363,18 @@
             'NOT (S connait AD, AD nom "toto"), AD is Personne, '
             'EXISTS(S travaille AE, AE nom "tutu")')
 
+    def test_unrelated_rql_security_rel_perms(self):
+        '''check `connait` add permission has no effect for a new entity on the
+        unrelated rql'''
+        rdef = self.schema['Personne'].rdef('connait')
+        perm_rrqle = RRQLExpression('U has_update_permission S')
+        with self.temporary_permissions((rdef, {'add': (perm_rrqle,)})):
+            person = self.vreg['etypes'].etype_class('Personne')(self.request())
+            rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
+        self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
+                         'O is Personne, O nom AA, O prenom AB, '
+                         'O modification_date AC')
+
     def test_unrelated_rql_constraints_edition_subject(self):
         person = self.request().create_entity('Personne', nom=u'sylvain')
         rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
@@ -459,31 +486,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 Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/test/unittest_req.py	Thu Mar 21 18:13:31 2013 +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.
@@ -36,7 +36,7 @@
         req = RequestSessionBase(None)
         req.from_controller = lambda : 'view'
         req.relative_path = lambda includeparams=True: None
-        req.base_url = lambda : 'http://testing.fr/cubicweb/'
+        req.base_url = lambda secure=None: 'http://testing.fr/cubicweb/'
         self.assertEqual(req.build_url(), u'http://testing.fr/cubicweb/view')
         self.assertEqual(req.build_url(None), u'http://testing.fr/cubicweb/view')
         self.assertEqual(req.build_url('one'), u'http://testing.fr/cubicweb/one')
--- a/test/unittest_selectors.py	Wed Aug 01 10:30:48 2012 +0200
+++ /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	Wed Aug 01 10:30:48 2012 +0200
+++ b/test/unittest_vregistry.py	Thu Mar 21 18:13:31 2013 +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/transaction.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/transaction.py	Thu Mar 21 18:13:31 2013 +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,6 @@
 This module is in the cubicweb package and not in cubicweb.server because those
 objects should be accessible to client through pyro, where the cubicweb.server
 package may not be installed.
-
 """
 __docformat__ = "restructuredtext en"
 _ = unicode
@@ -39,8 +38,12 @@
 
 
 class NoSuchTransaction(RepositoryError):
-    pass
+    # Used by CubicWebException
+    msg = _("there is no transaction #%s")
 
+    def __init__(self, txuuid):
+        super(RepositoryError, self).__init__(txuuid)
+        self.txuuid = txuuid
 
 class Transaction(object):
     """an undoable transaction"""
@@ -82,6 +85,11 @@
     def label(self):
         return ACTION_LABELS[self.action]
 
+    @property
+    def ertype(self):
+        """ Return the entity or relation type this action is related to"""
+        raise NotImplementedError(self)
+
 
 class EntityAction(AbstractAction):
     def __init__(self, action, public, order, etype, eid, changes):
@@ -95,6 +103,11 @@
             self.label, self.eid, self.changes,
             self.public and 'dbapi' or 'hook')
 
+    @property
+    def ertype(self):
+        """ Return the entity or relation type this action is related to"""
+        return self.etype
+
 
 class RelationAction(AbstractAction):
     def __init__(self, action, public, order, rtype, eidfrom, eidto):
@@ -107,3 +120,8 @@
         return '<%s: %s %s %s (%s)>' % (
             self.label, self.eid_from, self.rtype, self.eid_to,
             self.public and 'dbapi' or 'hook')
+
+    @property
+    def ertype(self):
+        """ Return the entity or relation type this action is related to"""
+        return self.rtype
--- a/view.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/view.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/vregistry.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/__init__.py	Thu Mar 21 18:13:31 2013 +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/_exceptions.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/_exceptions.py	Thu Mar 21 18:13:31 2013 +0100
@@ -20,59 +20,90 @@
 
 __docformat__ = "restructuredtext en"
 
+import httplib
+
 from cubicweb._exceptions import *
 from cubicweb.utils import json_dumps
 
+
+class DirectResponse(Exception):
+    """Used to supply a twitted HTTP Response directly"""
+    def __init__(self, response):
+        self.response = response
+
+class InvalidSession(CubicWebException):
+    """raised when a session id is found but associated session is not found or
+    invalid"""
+
+# Publish related exception
+
 class PublishException(CubicWebException):
     """base class for publishing related exception"""
 
+    def __init__(self, *args, **kwargs):
+        self.status = kwargs.pop('status', httplib.OK)
+        super(PublishException, self).__init__(*args, **kwargs)
+
+class LogOut(PublishException):
+    """raised to ask for deauthentication of a logged in user"""
+    def __init__(self, url=None):
+        super(LogOut, self).__init__()
+        self.url = url
+
+class Redirect(PublishException):
+    """raised to redirect the http request"""
+    def __init__(self, location, status=httplib.SEE_OTHER):
+        super(Redirect, self).__init__(status=status)
+        self.location = location
+
+class StatusResponse(PublishException):
+
+    def __init__(self, status, content=''):
+        super(StatusResponse, self).__init__(status=status)
+        self.content = content
+
+    def __repr__(self):
+        return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
+        self.url = url
+
+# Publish related error
+
 class RequestError(PublishException):
     """raised when a request can't be served because of a bad input"""
 
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(RequestError, self).__init__(*args, **kwargs)
+
+
 class NothingToEdit(RequestError):
     """raised when an edit request doesn't specify any eid to edit"""
 
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(NothingToEdit, self).__init__(*args, **kwargs)
+
 class ProcessFormError(RequestError):
     """raised when posted data can't be processed by the corresponding field
     """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(ProcessFormError, self).__init__(*args, **kwargs)
 
 class NotFound(RequestError):
-    """raised when a 404 error should be returned"""
-
-class Redirect(PublishException):
-    """raised to redirect the http request"""
-    def __init__(self, location):
-        self.location = location
-
-class DirectResponse(Exception):
-    def __init__(self, response):
-        self.response = response
+    """raised when something was not found. In most case,
+       a 404 error should be returned"""
 
-class StatusResponse(Exception):
-    def __init__(self, status, content=''):
-        self.status = int(status)
-        self.content = content
-
-    def __repr__(self):
-        return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
-
-class InvalidSession(CubicWebException):
-    """raised when a session id is found but associated session is not found or
-    invalid
-    """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.NOT_FOUND)
+        super(NotFound, self).__init__(*args, **kwargs)
 
 class RemoteCallFailed(RequestError):
     """raised when a json remote call fails
     """
-    def __init__(self, reason=''):
-        super(RemoteCallFailed, self).__init__()
+    def __init__(self, reason='', status=httplib.INTERNAL_SERVER_ERROR):
+        super(RemoteCallFailed, self).__init__(status=status)
         self.reason = reason
 
     def dumps(self):
         return json_dumps({'reason': self.reason})
-
-class LogOut(PublishException):
-    """raised to ask for deauthentication of a logged in user"""
-    def __init__(self, url):
-        super(LogOut, self).__init__()
-        self.url = url
--- a/web/action.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/action.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/application.py	Thu Mar 21 18:13:31 2013 +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,9 @@
 import sys
 from time import clock, time
 from contextlib import contextmanager
+from warnings import warn
+
+import httplib
 
 from logilab.common.deprecation import deprecated
 
@@ -31,7 +34,8 @@
 
 from cubicweb import set_log_methods, cwvreg
 from cubicweb import (
-    ValidationError, Unauthorized, AuthenticationError, NoSelectableObject,
+    ValidationError, Unauthorized, Forbidden,
+    AuthenticationError, NoSelectableObject,
     BadConnectionId, CW_EVENT_MANAGER)
 from cubicweb.dbapi import DBAPISession, anonymous_session
 from cubicweb.web import LOGGER, component
@@ -39,6 +43,8 @@
     StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
     RemoteCallFailed, InvalidSession, RequestError)
 
+from cubicweb.web.request import CubicWebRequestBase
+
 # make session manager available through a global variable so the debug view can
 # print information about web session
 SESSION_MANAGER = None
@@ -276,7 +282,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)
@@ -288,11 +294,11 @@
         if config['query-log-file']:
             from threading import Lock
             self._query_log = open(config['query-log-file'], 'a')
-            self.publish = self.log_publish
+            self.handle_request = self.log_handle_request
             self._logfile_lock = Lock()
         else:
             self._query_log = None
-            self.publish = self.main_publish
+            self.handle_request = self.main_handle_request
         # instantiate session and url resolving helpers
         self.session_handler = session_handler_fact(self)
         self.set_urlresolver()
@@ -311,12 +317,12 @@
 
     # publish methods #########################################################
 
-    def log_publish(self, path, req):
+    def log_handle_request(self, req, path):
         """wrapper around _publish to log all queries executed for a given
         accessed path
         """
         try:
-            return self.main_publish(path, req)
+            return self.main_handle_request(req, path)
         finally:
             cnx = req.cnx
             if cnx:
@@ -332,7 +338,79 @@
                     except Exception:
                         self.exception('error while logging queries')
 
-    def main_publish(self, path, req):
+
+
+    def main_handle_request(self, req, path):
+        if not isinstance(req, CubicWebRequestBase):
+            warn('[3.15] Application entry poin arguments are now (req, path) '
+                 'not (path, req)', DeprecationWarning, 2)
+            req, path = path, req
+        if req.authmode == 'http':
+            # activate realm-based auth
+            realm = self.vreg.config['realm']
+            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
+        content = ''
+        try:
+            self.connect(req)
+            # DENY https acces for anonymous_user
+            if (req.https
+                and req.session.anonymous_session
+                and self.vreg.config['https-deny-anonymous']):
+                # don't allow anonymous on https connection
+                raise AuthenticationError()
+            # nested try to allow LogOut to delegate logic to AuthenticationError
+            # handler
+            try:
+                ### Try to generate the actual request content
+                content = self.core_handle(req, path)
+            # Handle user log-out
+            except LogOut, ex:
+                # When authentification is handled by cookie the code that
+                # raised LogOut must has invalidated the cookie. We can just
+                # reload the original url without authentification
+                if self.vreg.config['auth-mode'] == 'cookie' and ex.url:
+                    req.headers_out.setHeader('location', str(ex.url))
+                if ex.status is not None:
+                    req.status_out = httplib.SEE_OTHER
+                # When the authentification is handled by http we must
+                # explicitly ask for authentification to flush current http
+                # authentification information
+                else:
+                    # Render "logged out" content.
+                    # assignement to ``content`` prevent standard
+                    # AuthenticationError code to overwrite it.
+                    content = self.loggedout_content(req)
+                    # let the explicitly reset http credential
+                    raise AuthenticationError()
+        except Redirect, ex:
+            # authentication needs redirection (eg openid)
+            content = self.redirect_handler(req, ex)
+        # Wrong, absent or Reseted credential
+        except AuthenticationError:
+            # If there is an https url configured and
+            # the request do not used https, redirect to login form
+            https_url = self.vreg.config['https-url']
+            if https_url and req.base_url() != https_url:
+                req.status_out = httplib.SEE_OTHER
+                req.headers_out.setHeader('location', https_url + 'login')
+            else:
+                # We assume here that in http auth mode the user *May* provide
+                # Authentification Credential if asked kindly.
+                if self.vreg.config['auth-mode'] == 'http':
+                    req.status_out = httplib.UNAUTHORIZED
+                # In the other case (coky auth) we assume that there is no way
+                # for the user to provide them...
+                # XXX But WHY ?
+                else:
+                    req.status_out = httplib.FORBIDDEN
+                # If previous error handling already generated a custom content
+                # do not overwrite it. This is used by LogOut Except
+                # XXX ensure we don't actually serve content
+                if not content:
+                    content = self.need_login_content(req)
+        return content
+
+    def core_handle(self, req, path):
         """method called by the main publisher to process <path>
 
         should return a string containing the resulting page or raise a
@@ -347,87 +425,97 @@
         :rtype: str
         :return: the result of the pusblished url
         """
-        path = path or 'view'
         # don't log form values they may contains sensitive information
-        self.info('publish "%s" (%s, form params: %s)',
-                  path, req.session.sessionid, req.form.keys())
+        self.debug('publish "%s" (%s, form params: %s)',
+                   path, req.session.sessionid, req.form.keys())
         # remove user callbacks on a new request (except for json controllers
         # to avoid callbacks being unregistered before they could be called)
         tstart = clock()
         commited = False
         try:
+            ### standard processing of the request
             try:
                 ctrlid, rset = self.url_resolver.process(req, path)
                 try:
                     controller = self.vreg['controllers'].select(ctrlid, req,
                                                                  appli=self)
                 except NoSelectableObject:
-                    if ctrlid == 'login':
-                        raise Unauthorized(req._('log out first'))
                     raise Unauthorized(req._('not authorized'))
                 req.update_search_state()
                 result = controller.publish(rset=rset)
-                if req.cnx:
-                    # no req.cnx if anonymous aren't allowed and we are
-                    # displaying some anonymous enabled view such as the cookie
-                    # authentication form
-                    req.cnx.commit()
-                    commited = True
-            except (StatusResponse, DirectResponse):
-                if req.cnx:
-                    req.cnx.commit()
-                raise
-            except (AuthenticationError, LogOut):
-                raise
-            except Redirect:
-                # redirect is raised by edit controller when everything went fine,
-                # so try to commit
-                try:
-                    if req.cnx:
-                        txuuid = req.cnx.commit()
-                        if txuuid is not None:
-                            msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
-                                req.build_url('undo', txuuid=txuuid), req._('undo'))
-                            req.append_to_redirect_message(msg)
-                except ValidationError, ex:
-                    self.validation_error_handler(req, ex)
-                except Unauthorized, ex:
-                    req.data['errmsg'] = req._('You\'re not authorized to access this page. '
-                                               'If you think you should, please contact the site administrator.')
-                    self.error_handler(req, ex, tb=False)
-                except Exception, ex:
-                    self.error_handler(req, ex, tb=True)
-                else:
-                    # delete validation errors which may have been previously set
-                    if '__errorurl' in req.form:
-                        req.session.data.pop(req.form['__errorurl'], None)
-                    raise
-            except RemoteCallFailed, ex:
-                req.set_header('content-type', 'application/json')
-                raise StatusResponse(500, ex.dumps())
-            except NotFound:
-                raise StatusResponse(404, self.notfound_content(req))
-            except ValidationError, ex:
-                self.validation_error_handler(req, ex)
-            except Unauthorized, ex:
-                self.error_handler(req, ex, tb=False, code=403)
-            except (BadRQLQuery, RequestError), ex:
-                self.error_handler(req, ex, tb=False)
-            except BaseException, ex:
-                self.error_handler(req, ex, tb=True)
-            except:
-                self.critical('Catch all triggered!!!')
-                self.exception('this is what happened')
-                result = 'oops'
+            except StatusResponse, ex:
+                warn('StatusResponse is deprecated use req.status_out',
+                     DeprecationWarning)
+                result = ex.content
+                req.status_out = ex.status
+            except Redirect, ex:
+                # Redirect may be raised by edit controller when everything went
+                # fine, so attempt to commit
+                result = self.redirect_handler(req, ex)
+            if req.cnx:
+                txuuid = req.cnx.commit()
+                commited = True
+                if txuuid is not None:
+                    req.data['last_undoable_transaction'] = txuuid
+        ### error case
+        except NotFound, ex:
+            result = self.notfound_content(req)
+            req.status_out = ex.status
+        except ValidationError, ex:
+            req.status_out = httplib.CONFLICT
+            result = self.validation_error_handler(req, ex)
+        except RemoteCallFailed, ex:
+            result = self.ajax_error_handler(req, ex)
+        except Unauthorized, ex:
+            req.data['errmsg'] = req._('You\'re not authorized to access this page. '
+                                       'If you think you should, please contact the site administrator.')
+            req.status_out = httplib.UNAUTHORIZED
+            result = self.error_handler(req, ex, tb=False)
+        except Forbidden, ex:
+            req.data['errmsg'] = req._('This action is forbidden. '
+                                       'If you think it should be allowed, please contact the site administrator.')
+            req.status_out = httplib.FORBIDDEN
+            result = self.error_handler(req, ex, tb=False)
+        except (BadRQLQuery, RequestError), ex:
+            result = self.error_handler(req, ex, tb=False)
+        ### pass through exception
+        except DirectResponse:
+            if req.cnx:
+                req.cnx.commit()
+            raise
+        except (AuthenticationError, LogOut):
+            # the rollback is handled in the finally
+            raise
+        ### Last defence line
+        except BaseException, ex:
+            result = self.error_handler(req, ex, tb=True)
         finally:
             if req.cnx and not commited:
                 try:
                     req.cnx.rollback()
                 except Exception:
                     pass # ignore rollback error at this point
-        self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
+            # request may be referenced by "onetime callback", so clear its entity
+            # cache to avoid memory usage
+            req.drop_entity_cache()
+        self.add_undo_link_to_msg(req)
+        self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart)
         return result
 
+    # Error handlers
+
+    def redirect_handler(self, req, ex):
+        """handle redirect
+        - comply to ex status
+        - set header field
+        - return empty content
+        """
+        self.debug('redirecting to %s', str(ex.location))
+        req.headers_out.setHeader('location', str(ex.location))
+        assert 300 <= ex.status < 400
+        req.status_out = ex.status
+        return ''
+
     def validation_error_handler(self, req, ex):
         ex.errors = dict((k, v) for k, v in ex.errors.items())
         if '__errorurl' in req.form:
@@ -440,18 +528,22 @@
             # session key is 'url + #<form dom id', though we usually don't want
             # the browser to move to the form since it hides the global
             # messages.
-            raise Redirect(req.form['__errorurl'].rsplit('#', 1)[0])
-        self.error_handler(req, ex, tb=False)
+            location = req.form['__errorurl'].rsplit('#', 1)[0]
+            req.headers_out.setHeader('location', str(location))
+            req.status_out = httplib.SEE_OTHER
+            return ''
+        return self.error_handler(req, ex, tb=False)
 
-    def error_handler(self, req, ex, tb=False, code=500):
+    def error_handler(self, req, ex, tb=False):
         excinfo = sys.exc_info()
-        self.exception(repr(ex))
+        if tb:
+            self.exception(repr(ex))
         req.set_header('Cache-Control', 'no-cache')
         req.remove_header('Etag')
         req.reset_message()
         req.reset_headers()
-        if req.json_request:
-            raise RemoteCallFailed(unicode(ex))
+        if req.ajax_request:
+            return self.ajax_error_handler(req, ex)
         try:
             req.data['ex'] = ex
             if tb:
@@ -462,7 +554,27 @@
             content = self.vreg['views'].main_template(req, template, view=errview)
         except Exception:
             content = self.vreg['views'].main_template(req, 'error-template')
-        raise StatusResponse(code, content)
+        if getattr(ex, 'status', None) is not None:
+            req.status_out = ex.status
+        return content
+
+    def add_undo_link_to_msg(self, req):
+        txuuid = req.data.get('last_undoable_transaction')
+        if txuuid is not None:
+            msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
+            req.build_url('undo', txuuid=txuuid), req._('undo'))
+            req.append_to_redirect_message(msg)
+
+    def ajax_error_handler(self, req, ex):
+        req.set_header('content-type', 'application/json')
+        status = ex.status
+        if status is None:
+            status = httplib.INTERNAL_SERVER_ERROR
+        json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+        req.status_out = status
+        return json_dumper()
+
+    # special case handling
 
     def need_login_content(self, req):
         return self.vreg['views'].main_template(req, 'login')
@@ -476,6 +588,8 @@
         template = self.main_template_id(req)
         return self.vreg['views'].main_template(req, template, view=view)
 
+    # template stuff
+
     def main_template_id(self, req):
         template = req.form.get('__template', req.property_value('ui.main-template'))
         if template not in self.vreg['views']:
--- a/web/box.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/box.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/component.py	Thu Mar 21 18:13:31 2013 +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
@@ -108,7 +108,9 @@
         view = self.cw_extra_kwargs.get('view')
         if view is not None and hasattr(view, 'page_navigation_url'):
             url = view.page_navigation_url(self, path, params)
-        elif path == 'json':
+        elif path in ('json', 'ajax'):
+            # 'ajax' is the new correct controller, but the old 'json'
+            # controller should still be supported
             url = self.ajax_page_url(**params)
         else:
             url = self._cw.build_url(path, **params)
@@ -121,7 +123,7 @@
     def ajax_page_url(self, **params):
         divid = params.setdefault('divid', 'pageContent')
         params['rql'] = self.cw_rset.printable_rql()
-        return js_href("$(%s).loadxhtml('json', %s, 'get', 'swap')" % (
+        return js_href("$(%s).loadxhtml(AJAX_PREFIX_URL, %s, 'get', 'swap')" % (
             json_dumps('#'+divid), js.ajaxFuncArgs('view', params)))
 
     def page_link(self, path, params, start, stop, content):
--- a/web/controller.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/controller.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.ajax.box.js	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.ajax.js	Thu Mar 21 18:13:31 2013 +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, {
@@ -180,8 +181,17 @@
                 // compute concat-like url for missing resources and append <link>
                 // element to $head
                 if (missingStylesheetsUrl) {
-                    $srcnode.attr('href', missingStylesheetsUrl);
-                    $srcnode.appendTo($head);
+                    // IE has problems with dynamic CSS insertions. One symptom (among others)
+                    // is a "1 item remaining" message in the status bar. (cf. #2356261)
+                    // document.createStyleSheet needs to be used for this, although it seems
+                    // that IE can't create more than 31 additional stylesheets with
+                    // document.createStyleSheet.
+                    if ($.browser.msie) {
+                        document.createStyleSheet(missingStylesheetsUrl);
+                    } else {
+                        $srcnode.attr('href', missingStylesheetsUrl);
+                        $srcnode.appendTo($head);
+                    }
                 }
             }
         });
@@ -439,7 +449,7 @@
  * emulation of gettext's _ shortcut
  */
 function _(message) {
-    return loadRemote('json', ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
+    return loadRemote(AJAX_BASE_URL, ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
 }
 
 /**
@@ -495,19 +505,19 @@
         }
         extraparams['rql'] = rql;
         extraparams['vid'] = vid;
-        $fragment.loadxhtml('json', ajaxFuncArgs('view', extraparams));
+        $fragment.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams));
     }
 }
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    loadRemote('json', ajaxFuncArgs('unload_page_data'), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'GET', true);
 }
 
 function removeBookmark(beid) {
-    var d = loadRemote('json', ajaxFuncArgs('delete_bookmark', null, beid));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('delete_bookmark', null, beid));
     d.addCallback(function(boxcontent) {
-        $('#bookmarks_box').loadxhtml('json',
+        $('#bookmarks_box').loadxhtml(AJAX_BASE_URL,
                                       ajaxFuncArgs('render', null, 'ctxcomponents',
                                                    'bookmarks_box'));
         document.location.hash = '#header';
@@ -517,7 +527,7 @@
 
 function userCallback(cbname) {
     setProgressCursor();
-    var d = loadRemote('json', ajaxFuncArgs('user_callback', null, cbname));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('user_callback', null, cbname));
     d.addCallback(resetCursor);
     d.addErrback(resetCursor);
     d.addErrback(remoteCallFailed);
@@ -527,7 +537,7 @@
 function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
     var d = userCallback(cbname);
     d.addCallback(function() {
-        $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {'rql': rql},
+        $('#' + nodeid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {'rql': rql},
                                                        registry, compid));
         if (msg) {
             updateMessage(msg);
@@ -553,7 +563,7 @@
  */
 function unregisterUserCallback(cbname) {
     setProgressCursor();
-    var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unregister_user_callback',
                                             null, cbname));
     d.addCallback(resetCursor);
     d.addErrback(resetCursor);
@@ -679,14 +689,14 @@
 	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);
 }
 
 
 /**
- * .. function:: reload(domid, registry, formparams, *render_args)
+ * .. function:: reload(domid, compid, registry, formparams, *render_args)
  *
  * `js_render` based reloading of views and components.
  */
@@ -694,7 +704,7 @@
     var ajaxArgs = ['render', formparams, registry, compid];
     ajaxArgs = ajaxArgs.concat(cw.utils.sliceList(arguments, 4));
     var params = ajaxFuncArgs.apply(null, ajaxArgs);
-    return $('#'+domid).loadxhtml('json', params, null, 'swap');
+    return $('#'+domid).loadxhtml(AJAX_BASE_URL, params, null, 'swap');
 }
 
 /* ajax tabs ******************************************************************/
@@ -738,8 +748,8 @@
         nodeid = nodeid || (compid + 'Component');
         extraargs = extraargs || {};
         var node = cw.jqNode(nodeid);
-        return node.loadxhtml('json', ajaxFuncArgs('component', null, compid,
-                                                   rql, registry, extraargs));
+        return node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('component', null, compid,
+                                                          rql, registry, extraargs));
     }
 );
 
@@ -775,7 +785,7 @@
             // passing `props` directly to loadxml because replacePageChunk
             // is sometimes called (abusively) with some extra parameters in `vid`
             var mode = swap ? 'swap': 'replace';
-            var url = JSON_BASE_URL + asURL(props);
+            var url = AJAX_BASE_URL + asURL(props);
             jQuery(node).loadxhtml(url, params, 'get', mode);
         } else {
             cw.log('Node', nodeId, 'not found');
@@ -798,7 +808,7 @@
         arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
     };
     var result = jQuery.ajax({
-        url: JSON_BASE_URL,
+        url: AJAX_BASE_URL,
         data: props,
         async: false,
         traditional: true
@@ -818,7 +828,7 @@
         arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
     };
     // XXX we should inline the content of loadRemote here
-    var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
+    var deferred = loadRemote(AJAX_BASE_URL, props, 'POST');
     deferred = deferred.addErrback(remoteCallFailed);
     deferred = deferred.addErrback(resetCursor);
     deferred = deferred.addCallback(resetCursor);
--- a/web/data/cubicweb.css	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.css	Thu Mar 21 18:13:31 2013 +0100
@@ -82,7 +82,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;
 }
 
@@ -242,13 +242,21 @@
 
 table#header td#header-right {
   padding-top: 1em;
-  float: right;
+  white-space: nowrap;
 }
 
 table#header img#logo{
   vertical-align: middle;
 }
 
+table#header td#headtext {
+  white-space: nowrap;
+}
+
+table#header td#header-center{
+ width: 100%;
+}
+
 span#appliName {
   font-weight: bold;
   color: %(defaultColor)s;
@@ -575,7 +583,7 @@
   vertical-align: bottom;
 }
 
-input#norql{
+input.norql{
   width:155px;
   margin-right: 2px;
 }
@@ -811,7 +819,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.edition.js	Thu Mar 21 18:13:31 2013 +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);
@@ -591,7 +591,7 @@
     try {
         var zipped = cw.utils.formContents(formid);
         var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
-        var d = loadRemote('json', args, 'POST');
+        var d = loadRemote(AJAX_BASE_URL, args, 'POST');
     } catch(ex) {
         cw.log('got exception', ex);
         return false;
--- a/web/data/cubicweb.facets.js	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.facets.js	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.iprogress.css	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.js	Thu Mar 21 18:13:31 2013 +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.old.css	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.old.css	Thu Mar 21 18:13:31 2013 +0100
@@ -275,10 +275,16 @@
 table#header a {
   color: #000;
 }
+table#header td#headtext {
+  white-space: nowrap;
+}
 
 table#header td#header-right {
   padding-top: 1em;
-  float: right;
+  white-space: nowrap;
+}
+table#header td#header-center{
+  width: 100%;
 }
 
 span#appliName {
--- a/web/data/cubicweb.reledit.js	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.reledit.js	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/cubicweb.widgets.js	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/data/uiprops.py	Thu Mar 21 18:13:31 2013 +0100
@@ -148,7 +148,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/facet.py	Thu Mar 21 18:13:31 2013 +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
 
@@ -1387,11 +1388,16 @@
             return
         if isinstance(value, list):
             value = reduce(lambda x, y: int(x) | int(y), value)
+        else:
+            value = int(value)
         attr_var = self.select.make_variable()
         self.select.add_relation(self.filtered_variable, self.rtype, attr_var)
         comp = nodes.Comparison('=', nodes.Constant(value, 'Int'))
-        comp.append(nodes.MathExpression('&', nodes.variable_ref(attr_var),
-                                         nodes.Constant(value, 'Int')))
+        if value == 0:
+            comp.append(nodes.variable_ref(attr_var))
+        else:
+            comp.append(nodes.MathExpression('&', nodes.variable_ref(attr_var),
+                                             nodes.Constant(value, 'Int')))
         having = self.select.having
         if having:
             self.select.replace(having[0], nodes.And(having[0], comp))
@@ -1401,7 +1407,7 @@
     def rset_vocabulary(self, rset):
         mask = reduce(lambda x, y: x | (y[0] or 0), rset, 0)
         return sorted([(self._cw._(label), val) for label, val in self.choices
-                       if val & mask])
+                       if not val or val & mask])
 
     def possible_values(self):
         return [unicode(val) for label, val in self.vocabulary()]
--- a/web/form.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/form.py	Thu Mar 21 18:13:31 2013 +0100
@@ -88,23 +88,19 @@
 
     def __init__(self, req, rset=None, row=None, col=None,
                  submitmsg=None, mainform=True, **kwargs):
-        super(Form, self).__init__(req, rset=rset, row=row, col=col)
+        # process kwargs first so we can properly pass them to Form and match
+        # order expectation (ie cw_extra_kwargs populated almost first)
+        hiddens, extrakw = self._process_kwargs(kwargs)
+        # now call ancestor init
+        super(Form, self).__init__(req, rset=rset, row=row, col=col, **extrakw)
+        # then continue with further specific initialization
         self.fields = list(self.__class__._fields_)
+        for key, val in hiddens:
+            self.add_hidden(key, val)
         if mainform:
             formid = kwargs.pop('formvid', self.__regid__)
             self.add_hidden(u'__form_id', formid)
             self._posting = self._cw.form.get('__form_id') == formid
-        for key, val in kwargs.iteritems():
-            if key in controller.NAV_FORM_PARAMETERS:
-                self.add_hidden(key, val)
-            elif key == 'redirect_path':
-                self.add_hidden(u'__redirectpath', val)
-            elif hasattr(self.__class__, key) and not key[0] == '_':
-                setattr(self, key, val)
-            else:
-                self.cw_extra_kwargs[key] = val
-            # skip other parameters, usually given for selection
-            # (else write a custom class to handle them)
         if mainform:
             self.add_hidden(u'__errorurl', self.session_key())
             self.add_hidden(u'__domid', self.domid)
@@ -119,6 +115,22 @@
         if submitmsg is not None:
             self.set_message(submitmsg)
 
+    def _process_kwargs(self, kwargs):
+        hiddens = []
+        extrakw = {}
+        # search for navigation parameters and customization of existing
+        # attributes; remaining stuff goes in extrakwargs
+        for key, val in kwargs.iteritems():
+            if key in controller.NAV_FORM_PARAMETERS:
+                hiddens.append( (key, val) )
+            elif key == 'redirect_path':
+                hiddens.append( (u'__redirectpath', val) )
+            elif hasattr(self.__class__, key) and not key[0] == '_':
+                setattr(self, key, val)
+            else:
+                extrakw[key] = val
+        return hiddens, extrakw
+
     def set_message(self, submitmsg):
         """sets a submitmsg if exists, using _cwmsgid mechanism """
         cwmsgid = self._cw.set_redirect_message(submitmsg)
--- a/web/formfields.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/formfields.py	Thu Mar 21 18:13:31 2013 +0100
@@ -313,6 +313,7 @@
 
     def role_name(self):
         """return <field.name>-<field.role> if role is specified, else field.name"""
+        assert self.name, 'field without a name (give it to constructor for explicitly built fields)'
         if self.role is not None:
             return role_name(self.name, self.role)
         return self.name
@@ -360,7 +361,7 @@
         if self.eidparam and self.role is not None:
             if form._cw.vreg.schema.rschema(self.name).final:
                 return form.edited_entity.e_schema.default(self.name)
-            return ()
+            return form.linked_to.get((self.name, self.role), ())
         return None
 
     def example_format(self, req):
--- a/web/formwidgets.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/formwidgets.py	Thu Mar 21 18:13:31 2013 +0100
@@ -672,10 +672,11 @@
     """
     needs_js = ('jquery.ui.js', )
     needs_css = ('jquery.ui.css',)
+    default_size = 10
 
     def __init__(self, datestr=None, **kwargs):
         super(JQueryDatePicker, self).__init__(**kwargs)
-        self.datestr = datestr
+        self.value = datestr
 
     def _render(self, form, field, renderer):
         req = form._cw
@@ -689,44 +690,36 @@
                        '{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
                        ' showOn: "button", buttonImageOnly: true})' % (
                            domid, req.uiprops['CALENDAR_ICON'], fmt))
-        if self.datestr is None:
+        return self._render_input(form, field, domid)
+
+    def _render_input(self, form, field, domid):
+        if self.value is None:
             value = self.values(form, field)[0]
         else:
-            value = self.datestr
-        attrs = {}
-        if self.settabindex:
-            attrs['tabindex'] = req.next_tabindex()
-        return tags.input(id=domid, name=domid, value=value,
-                          type='text', size='10', **attrs)
+            value = self.value
+        attrs = self.attributes(form, field)
+        attrs.setdefault('size', unicode(self.default_size))
+        return tags.input(name=domid, value=value, type='text', **attrs)
 
 
-class JQueryTimePicker(FieldWidget):
+class JQueryTimePicker(JQueryDatePicker):
     """Use jquery.timePicker to define a time picker. Will return the time as an
     unicode string.
     """
     needs_js = ('jquery.timePicker.js',)
     needs_css = ('jquery.timepicker.css',)
+    default_size = 5
 
     def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
-        super(JQueryTimePicker, self).__init__(**kwargs)
-        self.timestr = timestr
+        super(JQueryTimePicker, self).__init__(timestr, **kwargs)
         self.timesteps = timesteps
         self.separator = separator
 
     def _render(self, form, field, renderer):
-        req = form._cw
         domid = field.dom_id(form, self.suffix)
-        req.add_onload(u'cw.jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
-            domid, self.timestr, self.timesteps, self.separator))
-        if self.timestr is None:
-            value = self.values(form, field)[0]
-        else:
-            value = self.timestr
-        attrs = {}
-        if self.settabindex:
-            attrs['tabindex'] = req.next_tabindex()
-        return tags.input(id=domid, name=domid, value=value,
-                          type='text', size='5')
+        form._cw.add_onload(u'cw.jqNode("%s").timePicker({step: %s, separator: "%s"})' % (
+                domid, self.timesteps, self.separator))
+        return self._render_input(form, field, domid)
 
 
 class JQueryDateTimePicker(FieldWidget):
@@ -849,7 +842,7 @@
 
     def _get_url(self, entity, field):
         fname = self.autocomplete_initfunc
-        return entity._cw.build_url('json', fname=fname, mode='remote',
+        return entity._cw.build_url('ajax', fname=fname, mode='remote',
                                     pageid=entity._cw.pageid)
 
 
--- a/web/http_headers.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/http_headers.py	Thu Mar 21 18:13:31 2013 +0100
@@ -1290,11 +1290,13 @@
             self._raw_headers[name] = r
         return r
 
-    def hasHeader(self, name):
+    def __contains__(self, name):
         """Does a header with the given name exist?"""
         name=name.lower()
         return self._raw_headers.has_key(name)
 
+    hasHeader = __contains__
+
     def getRawHeaders(self, name, default=None):
         """Returns a list of headers matching the given name as the raw string given."""
 
--- a/web/httpcache.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/httpcache.py	Thu Mar 21 18:13:31 2013 +0100
@@ -147,3 +147,39 @@
 
 viewmod.StartupView.http_cache_manager = MaxAgeHTTPCacheManager
 viewmod.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
+
+
+### HTTP Cache validator ############################################
+
+
+
+def get_validators(headers_in):
+    """return a list of http condition validator relevant to this request
+    """
+    result = []
+    for header, func in VALIDATORS:
+        value = headers_in.getHeader(header)
+        if value is not None:
+            result.append((func, value))
+    return result
+
+
+def if_modified_since(ref_date, headers_out):
+    last_modified = headers_out.getHeader('last-modified')
+    if last_modified is None:
+        return True
+    return ref_date < last_modified
+
+def if_none_match(tags, headers_out):
+    etag = headers_out.getHeader('etag')
+    if etag is None:
+        return True
+    return not ((etag in tags) or ('*' in tags))
+
+VALIDATORS = [
+    ('if-modified-since', if_modified_since),
+    #('if-unmodified-since', if_unmodified_since),
+    ('if-none-match', if_none_match),
+    #('if-modified-since', if_modified_since),
+]
+
--- a/web/request.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/request.py	Thu Mar 21 18:13:31 2013 +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,11 +22,13 @@
 import time
 import random
 import base64
+import urllib
 from hashlib import sha1 # pylint: disable=E0611
 from Cookie import SimpleCookie
 from calendar import timegm
-from datetime import date
+from datetime import date, datetime
 from urlparse import urlsplit
+import httplib
 from itertools import count
 from warnings import warn
 
@@ -37,14 +39,13 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.dbapi import DBAPIRequest
-from cubicweb.mail import header
 from cubicweb.uilib import remove_html_tags, js
 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
-from cubicweb.web.httpcache import GMTOFFSET
-from cubicweb.web.http_headers import Headers, Cookie
+from cubicweb.web.httpcache import GMTOFFSET, get_validators
+from cubicweb.web.http_headers import Headers, Cookie, parseDateTime
 
 _MARKER = object()
 
@@ -81,34 +82,53 @@
 
 
 class CubicWebRequestBase(DBAPIRequest):
-    """abstract HTTP request, should be extended according to the HTTP backend"""
-    json_request = False # to be set to True by json controllers
+    """abstract HTTP request, should be extended according to the HTTP backend
+    Immutable attributes that describe the received query and generic configuration
+    """
+    ajax_request = False # to be set to True by ajax controllers
 
-    def __init__(self, vreg, https, form=None):
+    def __init__(self, vreg, https=False, form=None, headers={}):
+        """
+        :vreg: Vregistry,
+        :https: boolean, s this a https request
+        :form: Forms value
+        """
         super(CubicWebRequestBase, self).__init__(vreg)
+        #: (Boolean) Is this an https request.
         self.https = https
+        #: User interface property (vary with https) (see :ref:`uiprops`)
+        self.uiprops = None
+        #: url for serving datadir (vary with https) (see :ref:`resources`)
+        self.datadir_url = None
         if https:
             self.uiprops = vreg.config.https_uiprops
             self.datadir_url = vreg.config.https_datadir_url
         else:
             self.uiprops = vreg.config.uiprops
             self.datadir_url = vreg.config.datadir_url
-        # raw html headers that can be added from any view
+        #: raw html headers that can be added from any view
         self.html_headers = HTMLHead(self)
-        # form parameters
+        #: received headers
+        self._headers_in = Headers()
+        for k, v in headers.iteritems():
+            self._headers_in.addRawHeader(k, v)
+        #: form parameters
         self.setup_params(form)
-        # dictionary that may be used to store request data that has to be
-        # shared among various components used to publish the request (views,
-        # controller, application...)
+        #: dictionary that may be used to store request data that has to be
+        #: shared among various components used to publish the request (views,
+        #: controller, application...)
         self.data = {}
-        # search state: 'normal' or 'linksearch' (eg searching for an object
-        # to create a relation with another)
+        #:  search state: 'normal' or 'linksearch' (eg searching for an object
+        #:  to create a relation with another)
         self.search_state = ('normal',)
-        # page id, set by htmlheader template
+        #: page id, set by htmlheader template
         self.pageid = None
         self._set_pageid()
         # prepare output header
+        #: Header used for the final response
         self.headers_out = Headers()
+        #: HTTP status use by the final response
+        self.status_out  = 200
 
     def _set_pageid(self):
         """initialize self.pageid
@@ -121,10 +141,41 @@
             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)
+
+    def base_url(self, secure=None):
+        """return the root url of the instance
+
+        secure = False -> base-url
+        secure = None  -> https-url if req.https
+        secure = True  -> https if it exist
+        """
+        if secure is None:
+            secure = self.https
+        base_url = None
+        if secure:
+            base_url = self.vreg.config.get('https-url')
+        if base_url is None:
+            base_url = super(CubicWebRequestBase, self).base_url()
+        return base_url
+
     @property
     def authmode(self):
+        """Authentification mode of the instance
+
+        (see :ref:`WebServerConfig`)"""
         return self.vreg.config['auth-mode']
 
+    # Various variable generator.
+
     @property
     def varmaker(self):
         """the rql varmaker is exposed both as a property and as the
@@ -259,7 +310,6 @@
             form = self.form
         return list_form_param(form, param, pop)
 
-
     def reset_headers(self):
         """used by AutomaticWebTest to clear html headers between tests on
         the same resultset
@@ -560,18 +610,36 @@
             name = bwcompat
         self.set_cookie(name, '', maxage=0, expires=date(2000, 1, 1))
 
-    def set_content_type(self, content_type, filename=None, encoding=None):
+    def set_content_type(self, content_type, filename=None, encoding=None,
+                         disposition='inline'):
         """set output content type for this request. An optional filename
-        may be given
+        may be given.
+
+        The disposition argument may be `attachement` or `inline` as specified
+        for the Content-disposition HTTP header. The disposition parameter have
+        no effect if no filename are specified.
         """
         if content_type.startswith('text/') and ';charset=' not in content_type:
             content_type += ';charset=' + (encoding or self.encoding)
         self.set_header('content-type', content_type)
         if filename:
-            if isinstance(filename, unicode):
-                filename = header(filename).encode()
-            self.set_header('content-disposition', 'inline; filename=%s'
-                            % filename)
+            header = [disposition]
+            unicode_filename = None
+            try:
+                ascii_filename = filename.encode('ascii')
+            except UnicodeEncodeError:
+                # fallback filename for very old browser
+                unicode_filename = filename
+                ascii_filename = filename.encode('ascii', 'ignore')
+            # escape " and \
+            # see http://greenbytes.de/tech/tc2231/#attwithfilenameandextparamescaped
+            ascii_filename = ascii_filename.replace('\x5c', r'\\').replace('"', r'\"')
+            header.append('filename="%s"' % ascii_filename)
+            if unicode_filename is not None:
+                # encoded filename according RFC5987
+                urlquoted_filename = urllib.quote(unicode_filename.encode('utf-8'), '')
+                header.append("filename*=utf-8''" + urlquoted_filename)
+            self.set_header('content-disposition', ';'.join(header))
 
     # high level methods for HTML headers management ##########################
 
@@ -646,7 +714,10 @@
         # after having url unescaping the content. This may make appear some
         # quote or other special characters that will break the js expression.
         extraparams.setdefault('fname', 'view')
-        url = self.build_url('json', **extraparams)
+        # remove pageid from the generated URL as it's forced as a parameter
+        # to the loadxhtml call below.
+        extraparams.pop('pageid', None)
+        url = self.build_url('ajax', **extraparams)
         cbname = build_cb_uid(url[:50])
         # think to propagate pageid. XXX see https://www.cubicweb.org/ticket/1753121
         jscode = u'function %s() { $("#%s").%s; }' % (
@@ -701,14 +772,33 @@
         return 'view'
 
     def validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
+        """raise a `StatusResponse` exception if a cached page along the way
         exists and is still usable.
 
         calls the client-dependant implementation of `_validate_cache`
         """
-        self._validate_cache()
-        if self.http_method() == 'HEAD':
-            raise StatusResponse(200, '')
+        modified = True
+        if self.get_header('Cache-Control') not in ('max-age=0', 'no-cache'):
+            # Here, we search for any invalid 'not modified' condition
+            # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3
+            validators = get_validators(self._headers_in)
+            if validators: # if we have no
+                modified = any(func(val, self.headers_out) for func, val in validators)
+        # Forge expected response
+        if modified:
+            if 'Expires' not in self.headers_out:
+                # Expires header seems to be required by IE7 -- Are you sure ?
+                self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
+            if self.http_method() == 'HEAD':
+                raise StatusResponse(200, '')
+            # /!\ no raise, the function returns and we keep processing the request)
+        else:
+            # overwrite headers_out to forge a brand new not-modified response
+            self.headers_out = self._forge_cached_headers()
+            if self.http_method() in ('HEAD', 'GET'):
+                raise StatusResponse(httplib.NOT_MODIFIED)
+            else:
+                raise StatusResponse(httplib.PRECONDITION_FAILED)
 
     # abstract methods to override according to the web front-end #############
 
@@ -716,11 +806,19 @@
         """returns 'POST', 'GET', 'HEAD', etc."""
         raise NotImplementedError()
 
-    def _validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
-        exists and is still usable
-        """
-        raise NotImplementedError()
+    def _forge_cached_headers(self):
+        # overwrite headers_out to forge a brand new not-modified response
+        headers = Headers()
+        for header in (
+            # Required from sec 10.3.5:
+            'date', 'etag', 'content-location', 'expires',
+            'cache-control', 'vary',
+            # Others:
+            'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
+            value = self._headers_in.getRawHeaders(header)
+            if value is not None:
+                headers.setRawHeaders(header, value)
+        return headers
 
     def relative_path(self, includeparams=True):
         """return the normalized path of the request (ie at least relative
@@ -732,12 +830,37 @@
         """
         raise NotImplementedError()
 
-    def get_header(self, header, default=None):
-        """return the value associated with the given input HTTP header,
-        raise KeyError if the header is not set
+    # http headers ############################################################
+
+    ### incoming headers
+
+    def get_header(self, header, default=None, raw=True):
+        """return the value associated with the given input header, raise
+        KeyError if the header is not set
         """
-        raise NotImplementedError()
+        if raw:
+            return self._headers_in.getRawHeaders(header, [default])[0]
+        return self._headers_in.getHeader(header, default)
+
+    def header_accept_language(self):
+        """returns an ordered list of preferred languages"""
+        acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
+        for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
+                              reverse=True):
+            lang = lang.split('-')[0]
+            yield lang
 
+    def header_if_modified_since(self):
+        """If the HTTP header If-modified-since is set, return the equivalent
+        date time value (GMT), else return None
+        """
+        mtime = self.get_header('If-modified-since', raw=False)
+        if mtime:
+            # :/ twisted is returned a localized time stamp
+            return datetime.fromtimestamp(mtime) + GMTOFFSET
+        return None
+
+    ### outcoming headers
     def set_header(self, header, value, raw=True):
         """set an output HTTP header"""
         if raw:
@@ -785,12 +908,6 @@
         values = _parse_accept_header(accepteds, value_parser, value_sort_key)
         return (raw_value for (raw_value, parsed_value, score) in values)
 
-    def header_if_modified_since(self):
-        """If the HTTP header If-modified-since is set, return the equivalent
-        mx date time value (GMT), else return None
-        """
-        raise NotImplementedError()
-
     def demote_to_html(self):
         """helper method to dynamically set request content type to text/html
 
@@ -805,6 +922,8 @@
             self.set_content_type('text/html')
             self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT)
 
+    # xml doctype #############################################################
+
     def set_doctype(self, doctype, reset_xmldecl=True):
         """helper method to dynamically change page doctype
 
--- a/web/test/data/views.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/data/views.py	Thu Mar 21 18:13:31 2013 +0100
@@ -21,12 +21,12 @@
 from cubicweb.web import Redirect
 from cubicweb.web.application import CubicWebPublisher
 
-# proof of concept : monkey patch publish method so that if we are in an
+# proof of concept : monkey patch handle method so that if we are in an
 # anonymous session and __fblogin is found is req.form, the user with the
 # given login is created if necessary and then a session is opened for that
 # user
 # NOTE: this require "cookie" authentication mode
-def auto_login_publish(self, path, req):
+def auto_login_handle_request(self, req, path):
     if (not req.cnx or req.cnx.anonymous_connection) and req.form.get('__fblogin'):
         login = password = req.form.pop('__fblogin')
         self.repo.register_user(login, password)
@@ -40,7 +40,7 @@
         except Redirect:
             pass
         assert req.user.login == login
-    return orig_publish(self, path, req)
+    return orig_handle(self, req, path)
 
-orig_publish = CubicWebPublisher.main_publish
-CubicWebPublisher.main_publish = auto_login_publish
+orig_handle = CubicWebPublisher.main_handle_request
+CubicWebPublisher.main_handle_request = auto_login_handle_request
--- a/web/test/unittest_application.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_application.py	Thu Mar 21 18:13:31 2013 +0100
@@ -154,11 +154,6 @@
 
 
 class ApplicationTC(CubicWebTC):
-    def setUp(self):
-        super(ApplicationTC, self).setUp()
-        def raise_hdlr(*args, **kwargs):
-            raise
-        self.app.error_handler = raise_hdlr
 
     @classproperty
     def config(cls):
@@ -184,12 +179,12 @@
 
     def test_nonregr_publish1(self):
         req = self.request(u'CWEType X WHERE X final FALSE, X meta FALSE')
-        self.app.publish('view', req)
+        self.app.handle_request(req, 'view')
 
     def test_nonregr_publish2(self):
         req = self.request(u'Any count(N) WHERE N todo_by U, N is Note, U eid %s'
                            % self.user().eid)
-        self.app.publish('view', req)
+        self.app.handle_request(req, 'view')
 
     def test_publish_validation_error(self):
         req = self.request()
@@ -202,7 +197,7 @@
              # just a sample, missing some necessary information for real life
             '__errorurl': 'view?vid=edition...'
             }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         eidmap = forminfo['eidmap']
         self.assertEqual(eidmap, {})
@@ -232,7 +227,7 @@
                     # necessary to get validation error handling
                     '__errorurl': 'view?vid=edition...',
                     }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         self.assertEqual(set(forminfo['eidmap']), set('XY'))
         self.assertEqual(forminfo['eidmap']['X'], None)
@@ -261,7 +256,7 @@
                     # necessary to get validation error handling
                     '__errorurl': 'view?vid=edition...',
                     }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         self.assertEqual(set(forminfo['eidmap']), set('XY'))
         self.assertIsInstance(forminfo['eidmap']['X'], int)
@@ -274,7 +269,7 @@
 
     def _test_cleaned(self, kwargs, injected, cleaned):
         req = self.request(**kwargs)
-        page = self.app.publish('view', req)
+        page = self.app.handle_request(req, 'view')
         self.assertFalse(injected in page, (kwargs, injected))
         self.assertTrue(cleaned in page, (kwargs, cleaned))
 
@@ -308,12 +303,6 @@
         self.commit()
         self.assertEqual(vreg.property_value('ui.language'), 'en')
 
-    def test_login_not_available_to_authenticated(self):
-        req = self.request()
-        with self.assertRaises(Unauthorized) as cm:
-            self.app_publish(req, 'login')
-        self.assertEqual(str(cm.exception), 'log out first')
-
     def test_fb_login_concept(self):
         """see data/views.py"""
         self.set_auth_mode('cookie', 'anon')
@@ -321,7 +310,7 @@
         req = self.request()
         origcnx = req.cnx
         req.form['__fblogin'] = u'turlututu'
-        page = self.app_publish(req)
+        page = self.app.handle_request(req, '')
         self.assertFalse(req.cnx is origcnx)
         self.assertEqual(req.user.login, 'turlututu')
         self.assertTrue('turlututu' in page, page)
@@ -332,25 +321,28 @@
     def test_http_auth_no_anon(self):
         req, origsession = self.init_authentication('http')
         self.assertAuthFailure(req)
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'login')
+        self.assertRaises(AuthenticationError, self.app_handle_request, req, 'login')
         self.assertEqual(req.cnx, None)
         authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
         req.set_request_header('Authorization', 'basic %s' % authstr)
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_no_anon(self):
         req, origsession = self.init_authentication('cookie')
         self.assertAuthFailure(req)
-        form = self.app_publish(req, 'login')
+        try:
+            form = self.app_handle_request(req, 'login')
+        except Redirect, redir:
+            self.fail('anonymous user should get login form')
         self.assertTrue('__login' in form)
         self.assertTrue('__password' in form)
         self.assertEqual(req.cnx, None)
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_login_by_email(self):
@@ -370,7 +362,7 @@
         req.form['__login'] = address
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def _reset_cookie(self, req):
@@ -410,7 +402,7 @@
         authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
         req.set_request_header('Authorization', 'basic %s' % authstr)
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_anon_allowed(self):
@@ -422,7 +414,7 @@
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_anonymized_request(self):
@@ -441,7 +433,7 @@
         req = self.request()
         # expect a rset with None in [0][0]
         req.form['rql'] = 'rql:Any OV1, X WHERE X custom_workflow OV1?'
-        self.app_publish(req)
+        self.app_handle_request(req)
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_facet.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_facet.py	Thu Mar 21 18:13:31 2013 +0100
@@ -221,6 +221,25 @@
         self.assertEqual(f.select.as_string(),
                           "DISTINCT Any  WHERE X ordernum XO, X is CWAttribute, X ordernum C HAVING 3 = (C & 3)")
 
+    def test_bitfield_0_value(self):
+        req, rset, rqlst, filtered_variable = self.prepare_rqlst(
+            'CWAttribute X WHERE X ordernum XO',
+            expected_baserql='Any X WHERE X ordernum XO, X is CWAttribute',
+            expected_preparedrql='DISTINCT Any  WHERE X ordernum XO, X is CWAttribute')
+        f = facet.BitFieldFacet(req, rset=rset,
+                                select=rqlst.children[0],
+                                filtered_variable=filtered_variable)
+        f.choices = [('zero', 0,), ('un', 1,), ('deux', 2,)]
+        f.rtype = 'ordernum'
+        self.assertEqual(f.vocabulary(),
+                          [(u'deux', 2), (u'un', 1), (u'zero', 0)])
+        self.assertEqual(f.possible_values(),
+                          ['2', '1', '0'])
+        req.form[f.__regid__] = '0'
+        f.add_rql_restrictions()
+        self.assertEqual(f.select.as_string(),
+                          "DISTINCT Any  WHERE X ordernum XO, X is CWAttribute, X ordernum C HAVING 0 = C")
+
     def test_rql_path_eid(self):
         req, rset, rqlst, filtered_variable = self.prepare_rqlst()
         class RPF(facet.RQLPathFacet):
--- a/web/test/unittest_formfields.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_formfields.py	Thu Mar 21 18:13:31 2013 +0100
@@ -147,7 +147,7 @@
     def test_property_key_field(self):
         from cubicweb.web.views.cwproperties import PropertyKeyField
         req = self.request()
-        field = PropertyKeyField()
+        field = PropertyKeyField(name='test')
         e = self.vreg['etypes'].etype_class('CWProperty')(req)
         renderer = self.vreg['formrenderers'].select('base', req)
         form = EntityFieldsForm(req, entity=e)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_http.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,282 @@
+from logilab.common.testlib import TestCase, unittest_main, tag, Tags
+
+from cubicweb.web import StatusResponse
+from cubicweb.devtools.fake import FakeRequest
+
+
+def _test_cache(hin, hout, method='GET'):
+    """forge and process a request
+
+    return status code and the request object
+
+    status is None is no cache is involved
+    """
+    # forge request
+    req = FakeRequest(method=method)
+    for key, value in hin:
+        req._headers_in.addRawHeader(key, str(value))
+    for key, value in hout:
+        req.headers_out.addRawHeader(key, str(value))
+    # process
+    status = None
+    try:
+        req.validate_cache()
+    except StatusResponse, ex:
+        status = ex.status
+    return status, req
+
+class HTTPCache(TestCase):
+    """Check that the http cache logiac work as expected
+    (as far as we understood the RFC)
+
+    """
+    tags = TestCase.tags | Tags('http', 'cache')
+
+
+    def assertCache(self, expected, status, situation=''):
+        """simple assert for nicer message"""
+        if expected != status:
+            if expected is None:
+                expected = "MODIFIED"
+            if status is None:
+                status = "MODIFIED"
+            msg = 'expected %r got %r' % (expected, status)
+            if situation:
+                msg = "%s - when: %s" % (msg, situation)
+            self.fail(msg)
+
+    def test_IN_none_OUT_none(self):
+        #: test that no caching is requested when not data is available
+        #: on any side
+        status, req =_test_cache((),())
+        self.assertIsNone(status)
+
+    def test_IN_Some_OUT_none(self):
+        #: test that no caching is requested when no data is available
+        #: server (origin) side
+        hin = [('if-modified-since','Sat, 14 Apr 2012 14:39:32 GM'),
+              ]
+        status, req = _test_cache(hin, ())
+        self.assertIsNone(status)
+        hin = [('if-none-match','babar/huitre'),
+              ]
+        status, req = _test_cache(hin, ())
+        self.assertIsNone(status)
+        hin = [('if-modified-since','Sat, 14 Apr 2012 14:39:32 GM'),
+               ('if-none-match','babar/huitre'),
+              ]
+        status, req = _test_cache(hin, ())
+        self.assertIsNone(status)
+
+    def test_IN_none_OUT_Some(self):
+        #: test that no caching is requested when no data is provided
+        #: by the client
+        hout = [('last-modified','Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache((), hout)
+        self.assertIsNone(status)
+        hout = [('etag','babar/huitre'),
+               ]
+        status, req = _test_cache((), hout)
+        self.assertIsNone(status)
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+                ('etag','babar/huitre'),
+               ]
+        status, req = _test_cache((), hout)
+        self.assertIsNone(status)
+
+    @tag('last_modified')
+    def test_last_modified_newer(self):
+        #: test the proper behavior of modification date only
+        # newer
+        hin  = [('if-modified-since', 'Sat, 13 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'origin is newer than client')
+
+    @tag('last_modified')
+    def test_last_modified_older(self):
+        # older
+        hin  = [('if-modified-since', 'Sat, 15 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'origin is older than client')
+
+    @tag('last_modified')
+    def test_last_modified_same(self):
+        # same
+        hin  = [('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'origin is equal to client')
+
+    @tag('etag')
+    def test_etag_mismatch(self):
+        #: test the proper behavior of etag only
+        # etag mismatch
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'celestine'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'etag mismatch')
+
+    @tag('etag')
+    def test_etag_match(self):
+        # etag match
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'etag match')
+        # etag match in multiple
+        hin  = [('if-none-match', 'loutre'),
+                ('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'etag match in multiple')
+        # client use "*" as etag
+        hin  = [('if-none-match', '*'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'client use "*" as etag')
+
+    @tag('etag', 'last_modified')
+    def test_both(self):
+        #: test the proper behavior of etag only
+        # both wrong
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'loutre'),
+                ('last-modified', 'Sat, 15 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'both wrong')
+
+    @tag('etag', 'last_modified')
+    def test_both_etag_mismatch(self):
+        # both etag mismatch
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'loutre'),
+                ('last-modified', 'Sat, 13 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'both  but etag mismatch')
+
+    @tag('etag', 'last_modified')
+    def test_both_but_modified(self):
+        # both but modified
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'babar'),
+                ('last-modified', 'Sat, 15 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'both  but modified')
+
+    @tag('etag', 'last_modified')
+    def test_both_ok(self):
+        # both ok
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'babar'),
+                ('last-modified', 'Sat, 13 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'both ok')
+
+    @tag('etag', 'HEAD')
+    def test_head_verb(self):
+        #: check than FOUND 200 is properly raise without content on HEAD request
+        #: This logic does not really belong here :-/
+        # modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'rhino/really-not-babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='HEAD')
+        self.assertCache(200, status, 'modifier HEAD verb')
+        # not modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='HEAD')
+        self.assertCache(304, status, 'not modifier HEAD verb')
+
+    @tag('etag', 'POST')
+    def test_post_verb(self):
+        # modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'rhino/really-not-babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='POST')
+        self.assertCache(None, status, 'modifier HEAD verb')
+        # not modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='POST')
+        self.assertCache(412, status, 'not modifier HEAD verb')
+
+    @tag('expires')
+    def test_expires_added(self):
+        #: Check that Expires header is added:
+        #: - when the page is modified
+        #: - when none was already present
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'rhino/really-not-babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'modifier HEAD verb')
+        value = req.headers_out.getHeader('expires')
+        self.assertIsNotNone(value)
+
+    @tag('expires')
+    def test_expires_not_added(self):
+        #: Check that Expires header is not added if NOT-MODIFIED
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'not modifier HEAD verb')
+        value = req.headers_out.getHeader('expires')
+        self.assertIsNone(value)
+
+    @tag('expires')
+    def test_expires_no_overwrite(self):
+        #: Check that cache does not overwrite existing Expires header
+        hin  = [('if-none-match', 'babar'),
+               ]
+        DATE = 'Sat, 13 Apr 2012 14:39:32 GM'
+        hout = [('etag', 'rhino/really-not-babar'),
+                ('expires', DATE),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'not modifier HEAD verb')
+        value = req.headers_out.getRawHeaders('expires')
+        self.assertEqual(value, [DATE])
+
+
+if __name__ == '__main__':
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_idownloadable.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,127 @@
+# -*- 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 __future__ import with_statement
+
+from functools import partial
+
+from logilab.common.testlib import unittest_main
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb import view
+from cubicweb.predicates import is_instance
+
+
+class IDownloadableTC(CubicWebTC):
+
+    def setUp(self):
+        super(IDownloadableTC, self).setUp()
+        class IDownloadableUser(view.EntityAdapter):
+            __regid__ = 'IDownloadable'
+            __select__ = is_instance('CWUser')
+
+            def download_content_type(self):
+                """return MIME type of the downloadable content"""
+                return 'text/plain'
+
+            def download_encoding(self):
+                """return encoding of the downloadable content"""
+                return 'ascii'
+
+            def download_file_name(self):
+                """return file name of the downloadable content"""
+                return  self.entity.name() + '.txt'
+
+            def download_data(self):
+                return 'Babar is not dead!'
+        self.vreg.register(IDownloadableUser)
+        self.addCleanup(partial(self.vreg.unregister, IDownloadableUser))
+
+    def test_header_simple_case(self):
+        req = self.request()
+        req.form['vid'] = 'download'
+        req.form['eid'] = str(req.user.eid)
+        data = self.ctrl_publish(req,'view')
+        get = req.headers_out.getRawHeaders
+        self.assertEqual(['attachment;filename="admin.txt"'],
+                         get('content-disposition'))
+        self.assertEqual(['text/plain;charset=ascii'],
+                         get('content-type'))
+        self.assertEqual('Babar is not dead!', data)
+
+    def test_header_with_space(self):
+        req = self.request()
+        self.create_user(req, login=u'c c l a', password='babar')
+        self.commit()
+        with self.login(u'c c l a', password='babar'):
+            req = self.request()
+            req.form['vid'] = 'download'
+            req.form['eid'] = str(req.user.eid)
+            data = self.ctrl_publish(req,'view')
+            get = req.headers_out.getRawHeaders
+            self.assertEqual(['attachment;filename="c c l a.txt"'],
+                             get('content-disposition'))
+            self.assertEqual(['text/plain;charset=ascii'],
+                             get('content-type'))
+            self.assertEqual('Babar is not dead!', data)
+
+    def test_header_with_space_and_comma(self):
+        req = self.request()
+        self.create_user(req, login=ur'c " l\ a', password='babar')
+        self.commit()
+        with self.login(ur'c " l\ a', password='babar'):
+            req = self.request()
+            req.form['vid'] = 'download'
+            req.form['eid'] = str(req.user.eid)
+            data = self.ctrl_publish(req,'view')
+            get = req.headers_out.getRawHeaders
+            self.assertEqual([r'attachment;filename="c \" l\\ a.txt"'],
+                             get('content-disposition'))
+            self.assertEqual(['text/plain;charset=ascii'],
+                             get('content-type'))
+            self.assertEqual('Babar is not dead!', data)
+
+    def test_header_unicode_filename(self):
+        req = self.request()
+        self.create_user(req, login=u'cécilia', password='babar')
+        self.commit()
+        with self.login(u'cécilia', password='babar'):
+            req = self.request()
+            req.form['vid'] = 'download'
+            req.form['eid'] = str(req.user.eid)
+            self.ctrl_publish(req,'view')
+            get = req.headers_out.getRawHeaders
+            self.assertEqual(['''attachment;filename="ccilia.txt";filename*=utf-8''c%C3%A9cilia.txt'''],
+                             get('content-disposition'))
+
+    def test_header_unicode_long_filename(self):
+        req = self.request()
+        name = u'Bèrte_hô_grand_nôm_ça_va_totallement_déborder_de_la_limite_là'
+        self.create_user(req, login=name, password='babar')
+        self.commit()
+        with self.login(name, password='babar'):
+            req = self.request()
+            req.form['vid'] = 'download'
+            req.form['eid'] = str(req.user.eid)
+            self.ctrl_publish(req,'view')
+            get = req.headers_out.getRawHeaders
+            self.assertEqual(["""attachment;filename="Brte_h_grand_nm_a_va_totallement_dborder_de_la_limite_l.txt";filename*=utf-8''B%C3%A8rte_h%C3%B4_grand_n%C3%B4m_%C3%A7a_va_totallement_d%C3%A9border_de_la_limite_l%C3%A0.txt"""],
+                             get('content-disposition'))
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/test/unittest_request.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_request.py	Thu Mar 21 18:13:31 2013 +0100
@@ -5,7 +5,9 @@
 
 from functools import partial
 
-from cubicweb.web.request import (_parse_accept_header,
+from cubicweb.devtools.fake import FakeConfig
+
+from cubicweb.web.request import (CubicWebRequestBase, _parse_accept_header,
                                   _mimetype_sort_key, _mimetype_parser, _charset_sort_key)
 
 
@@ -65,5 +67,23 @@
                           ('utf-8', 'utf-8', 0.7),
                           ('*', '*', 0.7)])
 
+    def test_base_url(self):
+        dummy_vreg = type('DummyVreg', (object,), {})()
+        dummy_vreg.config = FakeConfig()
+        dummy_vreg.config['base-url'] = 'http://babar.com/'
+        dummy_vreg.config['https-url'] = 'https://toto.com/'
+
+        req = CubicWebRequestBase(dummy_vreg, https=False)
+        self.assertEqual('http://babar.com/', req.base_url())
+        self.assertEqual('http://babar.com/', req.base_url(False))
+        self.assertEqual('https://toto.com/', req.base_url(True))
+
+        req = CubicWebRequestBase(dummy_vreg, https=True)
+        self.assertEqual('https://toto.com/', req.base_url())
+        self.assertEqual('http://babar.com/', req.base_url(False))
+        self.assertEqual('https://toto.com/', req.base_url(True))
+
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_views_basecontrollers.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Thu Mar 21 18:13:31 2013 +0100
@@ -19,16 +19,27 @@
 
 from __future__ import with_statement
 
+from urlparse import urlsplit, urlunsplit, urljoin
+# parse_qs is deprecated in cgi and has been moved to urlparse in Python 2.6
+try:
+    from urlparse import parse_qs as url_parse_query
+except ImportError:
+    from cgi import parse_qs as url_parse_query
 from logilab.common.testlib import unittest_main, mock_object
+from logilab.common.decorators import monkeypatch
 
 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
+import cubicweb.transaction as tx
+
 u = unicode
 
 def req_form(user):
@@ -85,7 +96,7 @@
             'firstname-subject:'+eid:   u'Sylvain',
             'in_group-subject:'+eid:  groups,
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
         self.assertEqual(e.firstname, u'Sylvain')
         self.assertEqual(e.surname, u'Th\xe9nault')
@@ -104,7 +115,7 @@
             'upassword-subject:'+eid: 'tournicoton',
             'upassword-subject-confirm:'+eid: 'tournicoton',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         cnx.commit() # commit to check we don't get late validation error for instance
         self.assertEqual(path, 'cwuser/user')
         self.assertFalse('vid' in params)
@@ -125,7 +136,7 @@
             'firstname-subject:'+eid: u'Th\xe9nault',
             'surname-subject:'+eid:   u'Sylvain',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
         self.assertEqual(e.login, user.login)
         self.assertEqual(e.firstname, u'Th\xe9nault')
@@ -151,7 +162,7 @@
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': 'X',
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created person
         self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Mascio"').get_entity(0, 0)
@@ -173,7 +184,7 @@
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': peid,
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created person
         self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Masci"').get_entity(0, 0)
@@ -193,7 +204,7 @@
                     'address-subject:'+emaileid: u'adim@logilab.fr',
                     'use_email-object:'+emaileid: peid,
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         email.cw_clear_all_caches()
         self.assertEqual(email.address, 'adim@logilab.fr')
 
@@ -256,7 +267,7 @@
                     'amount-subject:X': u'10',
                     'described_by_test-subject:X': u(feid),
                     }
-        self.expect_redirect_publish(req, 'edit')
+        self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created
         #eid = params['rql'].split()[-1]
         e = self.execute('Salesterm X').get_entity(0, 0)
@@ -268,7 +279,7 @@
         user = self.user()
         req = self.request(**req_form(user))
         req.session.data['pending_insert'] = set([(user.eid, 'in_group', tmpgroup.eid)])
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertItemsEqual(usergroups, ['managers', 'test'])
@@ -287,7 +298,7 @@
         # now try to delete the relation
         req = self.request(**req_form(user))
         req.session.data['pending_delete'] = set([(user.eid, 'in_group', groupeid)])
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertItemsEqual(usergroups, ['managers'])
@@ -307,7 +318,7 @@
             '__form_id': 'edition',
             '__action_apply': '',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertTrue(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         self.assertEqual(params['vid'], 'edition')
@@ -329,7 +340,7 @@
             '__redirectparams': 'toto=tutu&tata=titi',
             '__form_id': 'edition',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'view')
         self.assertEqual(params['rql'], redirectrql)
         self.assertEqual(params['vid'], 'primary')
@@ -341,7 +352,7 @@
         eid = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
         req.form = {'eid': u(eid), '__type:%s'%eid: 'BlogEntry',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'blogentry')
         self.assertIn('_cwmsgid', params)
         eid = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
@@ -351,7 +362,7 @@
         req = req
         req.form = {'eid': u(eid), '__type:%s'%eid: 'EmailAddress',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'cwuser/admin')
         self.assertIn('_cwmsgid', params)
         eid1 = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
@@ -361,7 +372,7 @@
                     '__type:%s'%eid1: 'BlogEntry',
                     '__type:%s'%eid2: 'EmailAddress',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'view')
         self.assertIn('_cwmsgid', params)
 
@@ -377,7 +388,7 @@
                     'title-subject:X': u'entry1-copy',
                     'content-subject:X': u'content1',
                     }
-        self.expect_redirect_publish(req, 'edit')
+        self.expect_redirect_handle_request(req, 'edit')
         blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
         self.assertEqual(blogentry2.entry_of[0].eid, blog.eid)
 
@@ -395,7 +406,7 @@
                         'title-subject:X': u'entry1-copy',
                         'content-subject:X': u'content1',
                         }
-            self.expect_redirect_publish(req, 'edit')
+            self.expect_redirect_handle_request(req, 'edit')
             blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
             # entry_of should not be copied
             self.assertEqual(len(blogentry2.entry_of), 0)
@@ -421,7 +432,7 @@
             'read_permission-subject:'+cwetypeeid:  groups,
             }
         try:
-            path, params = self.expect_redirect_publish(req, 'edit')
+            path, params = self.expect_redirect_handle_request(req, 'edit')
             e = self.execute('Any X WHERE X eid %(x)s', {'x': cwetypeeid}).get_entity(0, 0)
             self.assertEqual(e.name, 'CWEType')
             self.assertEqual(sorted(g.eid for g in e.read_permission), groupeids)
@@ -441,7 +452,7 @@
             '__type:A': 'BlogEntry', '_cw_entity_fields:A': 'title-subject,content-subject',
             'title-subject:A': u'"13:03:40"',
             'content-subject:A': u'"13:03:43"',}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertTrue(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         e = self.execute('Any C, T WHERE C eid %(x)s, C content T', {'x': eid}).get_entity(0, 0)
@@ -479,7 +490,7 @@
                     'login-subject:X': u'toto',
                     'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'cwuser/toto')
         e = self.execute('Any X WHERE X is CWUser, X login "toto"').get_entity(0, 0)
         self.assertEqual(e.login, 'toto')
@@ -509,12 +520,12 @@
             #    which fires a Redirect
             # 2/ When re-publishing the copy form, the publisher implicitly commits
             try:
-                self.app_publish(req, 'edit')
+                self.app_handle_request(req, 'edit')
             except Redirect:
                 req = self.request()
                 req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
                 req.form['vid'] = 'copy'
-                self.app_publish(req, 'view')
+                self.app_handle_request(req, 'view')
             rset = self.execute('CWUser P WHERE P surname "Boom"')
             self.assertEqual(len(rset), 0)
         finally:
@@ -557,11 +568,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 +691,176 @@
         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.assertEqual(foo(object, 1, 2), 'hello')
+        appobject = foo.__appobject__
+        self.assertTrue(issubclass(appobject, AjaxFunction))
+        self.assertEqual(appobject.__regid__, 'foo')
+        self.assertEqual(appobject.check_pageid, False)
+        self.assertEqual(appobject.output_type, None)
+        req = self.request()
+        f = appobject(req)
+        self.assertEqual(f(12, 13), 'hello')
+
+    def test_ajaxfunc_checkpageid(self):
+        @ajaxfunc(check_pageid=True)
+        def foo(self, x, y):
+            return 'hello'
+        self.assertEqual(foo(object, 1, 2), 'hello')
+        appobject = foo.__appobject__
+        self.assertTrue(issubclass(appobject, AjaxFunction))
+        self.assertEqual(appobject.__regid__, 'foo')
+        self.assertEqual(appobject.check_pageid, True)
+        self.assertEqual(appobject.output_type, None)
+        # no pageid
+        req = self.request()
+        f = appobject(req)
+        self.assertRaises(RemoteCallFailed, f, 12, 13)
+
+    def test_ajaxfunc_json(self):
+        @ajaxfunc(output_type='json')
+        def foo(self, x, y):
+            return x + y
+        self.assertEqual(foo(object, 1, 2), 3)
+        appobject = foo.__appobject__
+        self.assertTrue(issubclass(appobject, AjaxFunction))
+        self.assertEqual(appobject.__regid__, 'foo')
+        self.assertEqual(appobject.check_pageid, False)
+        self.assertEqual(appobject.output_type, 'json')
+        # no pageid
+        req = self.request()
+        f = appobject(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')
+
+    def test_monkeypatch_jsoncontroller_stdfunc(self):
+        @monkeypatch(JSonController)
+        @jsonize
+        def js_reledit_form(self):
+            return 12
+        res, req = self.remote_call('reledit_form')
+        self.assertEqual(res, '12')
+
+
+class UndoControllerTC(CubicWebTC):
+
+    def setup_database(self):
+        req = self.request()
+        self.session.undo_actions = True
+        self.toto = self.create_user(req, 'toto', password='toto', groups=('users',),
+                                     commit=False)
+        self.txuuid_toto = self.commit()
+        self.toto_email = self.session.create_entity('EmailAddress',
+                                       address=u'toto@logilab.org',
+                                       reverse_use_email=self.toto)
+        self.txuuid_toto_email = self.commit()
+
+    def test_no_such_transaction(self):
+        req = self.request()
+        txuuid = u"12345acbd"
+        req.form['txuuid'] = txuuid
+        controller = self.vreg['controllers'].select('undo', req)
+        with self.assertRaises(tx.NoSuchTransaction) as cm:
+            result = controller.publish(rset=None)
+        self.assertEqual(cm.exception.txuuid, txuuid)
+
+    def assertURLPath(self, url, expected_path, expected_params=None):
+        """ This assert that the path part of `url` matches  expected path
+
+        TODO : implement assertion on the expected_params too
+        """
+        req = self.request()
+        scheme, netloc, path, query, fragment = urlsplit(url)
+        query_dict = url_parse_query(query)
+        expected_url = urljoin(req.base_url(), expected_path)
+        self.assertEqual( urlunsplit((scheme, netloc, path, None, None)), expected_url)
+
+    def test_redirect_redirectpath(self):
+        "Check that the potential __redirectpath is honored"
+        req = self.request()
+        txuuid = self.txuuid_toto_email
+        req.form['txuuid'] = txuuid
+        rpath = "toto"
+        req.form['__redirectpath'] = rpath
+        controller = self.vreg['controllers'].select('undo', req)
+        with self.assertRaises(Redirect) as cm:
+            result = controller.publish(rset=None)
+        self.assertURLPath(cm.exception.location, rpath)
+
+    def test_redirect_default(self):
+        req = self.request()
+        txuuid = self.txuuid_toto_email
+        req.form['txuuid'] = txuuid
+        req.session.data['breadcrumbs'] = [ urljoin(req.base_url(), path)
+                                            for path in ('tata', 'toto',)]
+        controller = self.vreg['controllers'].select('undo', req)
+        with self.assertRaises(Redirect) as cm:
+            result = controller.publish(rset=None)
+        self.assertURLPath(cm.exception.location, 'toto')
+
+
+class LoginControllerTC(CubicWebTC):
+
+    def test_login_with_dest(self):
+        req = self.request()
+        req.form = {'postlogin_path': 'elephants/babar'}
+        with self.assertRaises(Redirect) as cm:
+            self.ctrl_publish(req, ctrl='login')
+        self.assertEqual(req.build_url('elephants/babar'), cm.exception.location)
+
+    def test_login_no_dest(self):
+        req = self.request()
+        with self.assertRaises(Redirect) as cm:
+            self.ctrl_publish(req, ctrl='login')
+        self.assertEqual(req.base_url(), cm.exception.location)
 
 if __name__ == '__main__':
     unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_errorform.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,101 @@
+# 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/>.
+
+from __future__ import with_statement
+
+from logilab.common.testlib import unittest_main
+from logilab.mtconverter import html_unescape
+
+from cubicweb import Forbidden, ValidationError
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.utils import json
+from cubicweb.view import StartupView, TRANSITIONAL_DOCTYPE_NOEXT
+from cubicweb.web import Redirect
+from cubicweb.web.htmlwidgets import TableWidget
+from cubicweb.web.views import vid_from_rset
+
+import re
+import hmac
+
+class ErrorViewTC(CubicWebTC):
+    def setUp(self):
+        super(ErrorViewTC, self).setUp()
+        self.req = self.request()
+        self.vreg.config['submit-mail'] = "test@logilab.fr"
+        self.vreg.config['print-traceback'] = "yes"
+
+    def test_error_generation(self):
+        """
+        tests
+        """
+
+        class MyWrongView(StartupView):
+            __regid__ = 'my-view'
+            def call(self):
+                raise ValueError('This is wrong')
+
+        with self.temporary_appobjects(MyWrongView):
+            try:
+                self.view('my-view')
+            except Exception, e:
+                import sys
+                self.req.data['excinfo'] = sys.exc_info()
+                self.req.data['ex'] = e
+                html = self.view('error', req=self.req)
+                self.failUnless(re.search(r'^<input name="__signature" type="hidden" value="[0-9a-f]{32}" />$',
+                                          html.source, re.M))
+
+
+    def test_error_submit_nosig(self):
+        """
+        tests that the reportbug controller refuses submission if
+        there is not content signature
+        """
+
+        self.req.form = {'description': u'toto',
+                         }
+        with self.assertRaises(Forbidden) as cm:
+            self.ctrl_publish(self.req, 'reportbug')
+
+    def test_error_submit_wrongsig(self):
+        """
+        tests that the reportbug controller refuses submission if the
+        content signature is invalid
+        """
+
+        self.req.form = {'__signature': 'X',
+                         'description': u'toto',
+                         }
+        with self.assertRaises(Forbidden) as cm:
+            self.ctrl_publish(self.req, 'reportbug')
+
+    def test_error_submit_ok(self):
+        """
+        tests that the reportbug controller accept the email submission if the
+        content signature is valid
+        """
+
+        sign = self.vreg.config.sign_text('toto')
+        self.req.form = {'__signature': sign,
+                         'description': u'toto',
+                         }
+        with self.assertRaises(Redirect) as cm:
+            self.ctrl_publish(self.req, 'reportbug')
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/test/unittest_views_json.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_views_json.py	Thu Mar 21 18:13:31 2013 +0100
@@ -1,8 +1,34 @@
+# -*- 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.devtools.testlib import CubicWebTC
 
 from cubicweb.utils import json
 
+from cubicweb.web.application import anonymized_request
+
 class JsonViewsTC(CubicWebTC):
+    anonymize = True
+    res_jsonp_data = '[["guests", 1]]'
+
+    def setUp(self):
+        super(JsonViewsTC, self).setUp()
+        self.config.global_set_option('anonymize-jsonp-queries', self.anonymize)
 
     def test_json_rsetexport(self):
         req = self.request()
@@ -11,6 +37,13 @@
         self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json'])
         self.assertEqual(data, '[["guests", 1], ["managers", 1]]')
 
+    def test_json_rsetexport_empty_rset(self):
+        req = self.request()
+        rset = req.execute('Any X WHERE X is CWUser, X login "foobarbaz"')
+        data = self.view('jsonexport', rset)
+        self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json'])
+        self.assertEqual(data, '[]')
+
     def test_json_rsetexport_with_jsonp(self):
         req = self.request()
         req.form.update({'callback': 'foo',
@@ -19,7 +52,7 @@
         data = self.ctrl_publish(req, ctrl='jsonp')
         self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/javascript'])
         # because jsonp anonymizes data, only 'guests' group should be found
-        self.assertEqual(data, 'foo([["guests", 1]])')
+        self.assertEqual(data, 'foo(%s)' % self.res_jsonp_data)
 
     def test_json_rsetexport_with_jsonp_and_bad_vid(self):
         req = self.request()
@@ -30,7 +63,7 @@
         data = self.ctrl_publish(req, ctrl='jsonp')
         self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/javascript'])
         # result should be plain json, not the table view
-        self.assertEqual(data, 'foo([["guests", 1]])')
+        self.assertEqual(data, 'foo(%s)' % self.res_jsonp_data)
 
     def test_json_ersetexport(self):
         req = self.request()
@@ -41,6 +74,10 @@
         self.assertEqual(data[1]['name'], 'managers')
 
 
+class NotAnonymousJsonViewsTC(JsonViewsTC):
+    anonymize = False
+    res_jsonp_data = '[["guests", 1], ["managers", 1]]'
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_staticcontrollers.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,97 @@
+from __future__ import with_statement
+
+from logilab.common.testlib import tag, Tags
+from cubicweb.devtools.testlib import CubicWebTC
+
+import os
+import os.path as osp
+import glob
+
+from cubicweb.utils import HTMLHead
+from cubicweb.web.views.staticcontrollers import ConcatFilesHandler
+
+class StaticControllerCacheTC(CubicWebTC):
+
+    tags = CubicWebTC.tags | Tags('static_controller', 'cache', 'http')
+
+
+    def _publish_static_files(self, url, header={}):
+        req = self.request(headers=header)
+        req._url = url
+        return self.app_handle_request(req, url), req
+
+    def test_static_file_are_cached(self):
+        _, req = self._publish_static_files('data/cubicweb.css')
+        self.assertEqual(200, req.status_out)
+        self.assertIn('last-modified', req.headers_out)
+        next_headers = {
+            'if-modified-since': req.get_response_header('last-modified', raw=True),
+        }
+        _, req = self._publish_static_files('data/cubicweb.css', next_headers)
+        self.assertEqual(304, req.status_out)
+
+
+class ConcatFilesTC(CubicWebTC):
+
+    tags = CubicWebTC.tags | Tags('static_controller', 'concat')
+
+    def tearDown(self):
+        super(ConcatFilesTC, self).tearDown()
+        self._cleanup_concat_cache()
+
+    def _cleanup_concat_cache(self):
+        uicachedir = osp.join(self.config.apphome, 'uicache')
+        for fname in glob.glob(osp.join(uicachedir, 'cache_concat_*')):
+            os.unlink(osp.join(uicachedir, fname))
+
+    def _publish_js_files(self, js_files):
+        req = self.request()
+        head = HTMLHead(req)
+        url = head.concat_urls([req.data_url(js_file) for js_file in js_files])[len(req.base_url()):]
+        req._url = url
+        return self.app_handle_request(req, url), req
+
+    def expected_content(self, js_files):
+        content = u''
+        for js_file in js_files:
+            dirpath, rid = self.config.locate_resource(js_file)
+            if dirpath is not None: # ignore resources not found
+                with open(osp.join(dirpath, rid)) as f:
+                    content += f.read() + '\n'
+        return content
+
+    def test_cache(self):
+        js_files = ('cubicweb.ajax.js', 'jquery.js')
+        result, req = self._publish_js_files(js_files)
+        self.assertNotEqual(404, req.status_out)
+        # check result content
+        self.assertEqual(result, self.expected_content(js_files))
+        # make sure we kept a cached version on filesystem
+        concat_hander = ConcatFilesHandler(self.config)
+        filepath = concat_hander.build_filepath(js_files)
+        self.assertTrue(osp.isfile(filepath))
+
+
+    def test_invalid_file_in_debug_mode(self):
+        js_files = ('cubicweb.ajax.js', 'dummy.js')
+        # in debug mode, an error is raised
+        self.config.debugmode = True
+        try:
+            result, req = self._publish_js_files(js_files)
+            #print result
+            self.assertEqual(404, req.status_out)
+        finally:
+            self.config.debugmode = False
+
+    def test_invalid_file_in_production_mode(self):
+        js_files = ('cubicweb.ajax.js', 'dummy.js')
+        result, req = self._publish_js_files(js_files)
+        self.assertNotEqual(404, req.status_out)
+        # check result content
+        self.assertEqual(result, self.expected_content(js_files))
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
+
--- a/web/test/unittest_viewselector.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_viewselector.py	Thu Mar 21 18:13:31 2013 +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,16 +22,15 @@
 
 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 (
     primary, baseviews, tableview, editforms, calendar, management, embedding,
     actions, startup, cwuser, schema, xbel, vcard, owl, treeview, idownloadable,
     wdoc, debug, cwuser, cwproperties, cwsources, workflow, xmlrss, rdf,
-    csvexport, json)
+    csvexport, json, undohistory)
 
 from cubes.folder import views as folderviews
 
@@ -54,6 +53,11 @@
 
 assert RDFVIEWS
 
+if hasattr(rdf, 'RDFView'): # not available if rdflib not installed
+    RDFVIEWS = [('rdf', rdf.RDFView)]
+else:
+    RDFVIEWS = []
+
 class ViewSelectorTC(CubicWebTC):
 
     def setup_database(self):
@@ -103,13 +107,14 @@
                               ('siteinfo', debug.SiteInfoView),
                               ('systempropertiesform', cwproperties.SystemCWPropertiesForm),
                               ('tree', folderviews.FolderTreeView),
+                              ('undohistory', undohistory.UndoHistoryView),
                               ])
 
     def test_possible_views_noresult(self):
         req = self.request()
         rset = req.execute('Any X WHERE X eid 999999')
-        self.assertListEqual(self.pviews(req, rset),
-                             [])
+        self.assertListEqual([('jsonexport', json.JsonRsetView)],
+                             self.pviews(req, rset))
 
     def test_possible_views_one_egroup(self):
         req = self.request()
--- a/web/test/unittest_web.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/test/unittest_web.py	Thu Mar 21 18:13:31 2013 +0100
@@ -38,7 +38,9 @@
         self.assertTrue(url.endswith('()'))
         cbname = url.split()[1][:-2]
         self.assertMultiLineEqual(
-            'function %s() { $("#foo").loadxhtml("http://testing.fr/cubicweb/json?%s",{"pageid": "%s"},"get","replace"); }' % (cbname, qs, req.pageid),
+            'function %s() { $("#foo").loadxhtml("http://testing.fr/cubicweb/ajax?%s",'
+            '{"pageid": "%s"},"get","replace"); }' %
+            (cbname, qs, req.pageid),
             req.html_headers.post_inlined_scripts[0])
 
 if __name__ == '__main__':
--- a/web/views/actions.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/actions.py	Thu Mar 21 18:13:31 2013 +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()
@@ -82,6 +82,18 @@
                 return 1
     return 0
 
+class has_undoable_transactions(EntityPredicate):
+    "Select entities having public (i.e. end-user) undoable transactions."
+
+    def score_entity(self, entity):
+        if not entity._cw.vreg.config['undo-enabled']:
+            return 0
+        if entity._cw.cnx.undoable_transactions(eid=entity.eid):
+            return 1
+        else:
+            return 0
+
+
 # generic 'main' actions #######################################################
 
 class SelectAction(action.Action):
@@ -130,7 +142,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()}
@@ -420,6 +432,7 @@
         self._cw.add_js('cubicweb.rhythm.js')
         return 'rhythm'
 
+
 ## default actions ui configuration ###########################################
 
 addmenu = uicfg.actionbox_appearsin_addmenu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/ajaxcontroller.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,461 @@
+# 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-func`` 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 :func:`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 warnings import warn
+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')
+        # 1/ check first for old-style (JSonController) ajax func for bw compat
+        try:
+            func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+            func = partial(func, self)
+        except AttributeError:
+            # 2/ check for new-style (AjaxController) ajax func
+            try:
+                func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+            except ObjectNotFound:
+                raise RemoteCallFailed('no %s method' % fname)
+        else:
+            warn('[3.15] remote function %s found on JSonController, '
+                 'use AjaxFunction / @ajaxfunc instead' % fname,
+                 DeprecationWarning, stacklevel=2)
+        # no <arg> attribute means the callback takes no argument
+        args = self._cw.form.get('arg', ())
+        if not isinstance(args, (list, tuple)):
+            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__
+    # relate the ``implementation`` object to its wrapper appobject
+    # will be used by e.g.:
+    #   import base_module
+    #   @ajaxfunc
+    #   def foo(self):
+    #       return 42
+    #   assert foo(object) == 42
+    #   vreg.register_and_replace(foo, base_module.older_foo)
+    implementation.__appobject__ = AnAjaxFunc
+    return implementation
+
+
+def ajaxfunc(implementation=None, selector=yes(), output_type=None,
+             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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/ajaxedit.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/autoform.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/basecomponents.py	Thu Mar 21 18:13:31 2013 +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
@@ -76,10 +77,10 @@
     __abstract__ = True
     cw_property_defs = component.override_ctx(
         component.CtxComponent,
-        vocabulary=['header-left', 'header-right'])
+        vocabulary=['header-center', 'header-left', 'header-right', ])
     # don't want user to hide this component using an cwproperty
     site_wide = True
-    context = _('header-left')
+    context = _('header-center')
 
 
 class ApplLogo(HeaderComponent):
@@ -87,6 +88,7 @@
     __regid__ = 'logo'
     __select__ = yes() # no need for a cnx
     order = -1
+    context = _('header-left')
 
     def render(self, w):
         w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
@@ -187,7 +189,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/basecontrollers.py	Thu Mar 21 18:13:31 2013 +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,22 @@
 __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.web.controller import Controller
-from cubicweb.web.views import vid_from_rset, formrenderers
+                      AuthenticationError, typed_eid, UndoTransactionException,
+                      Forbidden)
+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, append_url_params
+from cubicweb.web.views import vid_from_rset
+import cubicweb.transaction as tx
 
-
+@deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead')
 def jsonize(func):
     """decorator to sets correct content_type and calls `json_dumps` on
     results
@@ -46,6 +48,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 +59,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
@@ -81,6 +85,17 @@
             # Cookie authentication
             return self.appli.need_login_content(self._cw)
 
+class LoginControllerForAuthed(Controller):
+    __regid__ = 'login'
+    __select__ = ~anonymous_user()
+
+    def publish(self, rset=None):
+        """log in the instance"""
+        path = self._cw.form.get('postlogin_path', '')
+        # redirect expect an url, not a path. Also path may contains a query
+        # string, hence should not be given to _cw.build_url()
+        raise Redirect(self._cw.base_url() + path)
+
 
 class LogoutController(Controller):
     __regid__ = 'logout'
@@ -200,7 +215,7 @@
         return (False, _validation_error(req, ex), ctrl._edited_entity)
     except Redirect, ex:
         try:
-            req.cnx.commit() # ValidationError may be raise on commit
+            txuuid = req.cnx.commit() # ValidationError may be raised on commit
         except ValidationError, ex:
             return (False, _validation_error(req, ex), ctrl._edited_entity)
         except Exception, ex:
@@ -208,6 +223,8 @@
             req.exception('unexpected error while validating form')
             return (False, str(ex).decode('utf-8'), ctrl._edited_entity)
         else:
+            if txuuid is not None:
+                req.data['last_undoable_transaction'] = txuuid
             # complete entity: it can be used in js callbacks where we might
             # want every possible information
             if ctrl._edited_entity:
@@ -234,7 +251,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,348 +259,33 @@
             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')
 
     def publish(self, rset=None):
         req = self._cw
+        desc = req.form['description']
+        # The description is generated and signed by cubicweb itself, check
+        # description's signature so we don't want to send spam here
+        sign = req.form.get('__signature', '')
+        if not (sign and req.vreg.config.check_text_sign(desc, sign)):
+            raise Forbidden('Invalid content')
         self.sendmail(req.vreg.config['submit-mail'],
                       req._('%s error report') % req.vreg.config.appid,
-                      req.form['description'])
+                      desc)
         raise Redirect(req.build_url(__message=req._('bug report sent')))
 
 
@@ -593,17 +295,17 @@
 
     def publish(self, rset=None):
         txuuid = self._cw.form['txuuid']
-        errors = self._cw.cnx.undo_transaction(txuuid)
-        if not errors:
-            self.redirect()
-        raise ValidationError(None, {None: '\n'.join(errors)})
+        try:
+            self._cw.cnx.undo_transaction(txuuid)
+        except UndoTransactionException, exc:
+            errors = exc.errors
+            #This will cause a rollback in main_publish
+            raise ValidationError(None, {None: '\n'.join(errors)})
+        else :
+            self.redirect() # Will raise Redirect
 
     def redirect(self, msg=None):
         req = self._cw
         msg = msg or req._("transaction undone")
-        breadcrumbs = req.session.data.get('breadcrumbs', None)
-        if breadcrumbs is not None and len(breadcrumbs) > 1:
-            url = req.rebuild_url(breadcrumbs[-2], __message=msg)
-        else:
-            url = req.build_url(__message=msg)
-        raise Redirect(url)
+        self._return_to_lastpage( dict(_cwmsgid= req.set_redirect_message(msg)) )
+
--- a/web/views/basetemplates.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/basetemplates.py	Thu Mar 21 18:13:31 2013 +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,10 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import class_renamed
+from logilab.common.registry import objectify_predicate
+from logilab.common.decorators import classproperty
 
-from cubicweb.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 +85,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:
@@ -328,7 +329,9 @@
     __regid__ = 'header'
     main_cell_components = ('appliname', 'breadcrumbs')
     headers = (('headtext', 'header-left'),
-               ('header-right', 'header-right'))
+               ('header-center', 'header-center'),
+               ('header-right', 'header-right')
+               )
 
     def call(self, view, **kwargs):
         self.main_header(view)
@@ -417,26 +420,56 @@
                 comp.render(w=self.w, view=view)
             self.w(u'</div>')
 
+class BaseLogForm(forms.FieldsForm):
+    """Abstract Base login form to be used by any login form
+    """
+    __abstract__ = True
 
-class LogForm(forms.FieldsForm):
     __regid__ = 'logform'
     domid = 'loginForm'
     needs_css = ('cubicweb.login.css',)
-    onclick = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
+
+    onclick_base = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
+    onclick_args = (None, None)
+
+    @classproperty
+    def form_buttons(cls):
+        # we use a property because sub class will need to define their own onclick_args.
+        # Therefor we can't juste make the string formating when instanciating this class
+        onclick = cls.onclick_base % cls.onclick_args
+        form_buttons = [fw.SubmitButton(label=_('log in'),
+                                    attrs={'class': 'loginButton'}),
+                        fw.ResetButton(label=_('cancel'),
+                                       attrs={'class': 'loginButton',
+                                              'onclick': onclick}),]
+        ## Can't shortcut next access because __dict__ is a "dictproxy" which 
+        ## does not support items assignement.
+        # cls.__dict__['form_buttons'] = form_buttons
+        return form_buttons
+
+    def form_action(self):
+        if self.action is None:
+            # reuse existing redirection if it exists
+            target = self._cw.form.get('postlogin_path',
+                                       self._cw.relative_path())
+            url_args = {}
+            if target and target != '/':
+                url_args['postlogin_path'] = target
+            return self._cw.build_url('login', __secure__=True, **url_args)
+        return super(LogForm, self).form_action()
+
+class LogForm(BaseLogForm):
+    """Simple login form that send username and password
+    """
+    __regid__ = 'logform'
+    domid = 'loginForm'
+    needs_css = ('cubicweb.login.css',)
     # XXX have to recall fields name since python is mangling __login/__password
     __login = ff.StringField('__login', widget=fw.TextInput({'class': 'data'}))
     __password = ff.StringField('__password', label=_('password'),
                                 widget=fw.PasswordSingleInput({'class': 'data'}))
-    form_buttons = [fw.SubmitButton(label=_('log in'),
-                                    attrs={'class': 'loginButton'}),
-                    fw.ResetButton(label=_('cancel'),
-                                   attrs={'class': 'loginButton',
-                                          'onclick': onclick % ('popupLoginBox', '__login')}),]
 
-    def form_action(self):
-        if self.action is None:
-            return login_form_url(self._cw)
-        return super(LogForm, self).form_action()
+    onclick_args =  ('popupLoginBox', '__login')
 
 
 class LogFormView(View):
@@ -482,12 +515,3 @@
         cw.html_headers.add_onload('jQuery("#__login:visible").focus()')
 
 LogFormTemplate = class_renamed('LogFormTemplate', LogFormView)
-
-
-def login_form_url(req):
-    if req.https:
-        return req.url()
-    httpsurl = req.vreg.config.get('https-url')
-    if httpsurl:
-        return req.url().replace(req.base_url(), httpsurl)
-    return req.url()
--- a/web/views/baseviews.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/baseviews.py	Thu Mar 21 18:13:31 2013 +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,30 +166,25 @@
     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 when the entity should be considered as displayed out of
-    its context. By default it produces the result of `textoutofcontext` wrapped
-    in a link leading to the primary view of the entity.
+    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'
 
     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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/bookmark.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/boxes.py	Thu Mar 21 18:13:31 2013 +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
@@ -48,17 +48,17 @@
 BoxTemplate = box.BoxTemplate
 BoxHtml = htmlwidgets.BoxHtml
 
-class EditBox(component.CtxComponent): # XXX rename to ActionsBox
+class EditBox(component.CtxComponent):
     """
     box with all actions impacting the entity displayed: edit, copy, delete
     change state, add related entities...
     """
     __regid__ = 'edit_box'
-    __select__ = component.CtxComponent.__select__ & non_final_entity()
 
     title = _('actions')
     order = 2
     contextual = True
+    __select__ = component.CtxComponent.__select__ & non_final_entity()
 
     def init_rendering(self):
         super(EditBox, self).init_rendering()
@@ -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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/calendar.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/cwproperties.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/cwsources.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/cwuser.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/debug.py	Thu Mar 21 18:13:31 2013 +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
 
@@ -131,10 +131,13 @@
             sessions = SESSION_MANAGER.current_sessions()
             w(u'<h3>%s</h3>' % _('opened web sessions'))
             if sessions:
+                n_no_cnx_sessions = 0
                 w(u'<ul>')
                 for session in sessions:
                     if not session.cnx:
-                        w(u'<li>%s (NO CNX)</li>' % session.sessionid)
+                        # We do not want to list all sessions without cnx
+                        # Their session ID are useless, hence we just count them
+                        n_no_cnx_sessions += 1
                         continue
                     try:
                         last_usage_time = session.cnx.check()
@@ -148,6 +151,9 @@
                     dict_to_html(w, session.data)
                     w(u'</li>')
                 w(u'</ul>')
+                if n_no_cnx_sessions > 0:
+                    w(u'<h3>%s %s</h3>' % (n_no_cnx_sessions,
+                                           _('web sessions without CNX')))
             else:
                 w(u'<p>%s</p>' % _('no web sessions found'))
 
--- a/web/views/editcontroller.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/editcontroller.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/editforms.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/editviews.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/emailaddress.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/embedding.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/facets.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/formrenderers.py	Thu Mar 21 18:13:31 2013 +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
 
@@ -116,7 +117,9 @@
         errormsg = self.error_message(form)
         if errormsg:
             data.insert(0, errormsg)
-        w(''.join(data))
+        # NOTE: we call unicode because `tag` objects may be found within data
+        #       e.g. from the cwtags library
+        w(''.join(unicode(x) for x in data))
 
     def render_content(self, w, form, values):
         if self.display_progress_div:
@@ -491,17 +494,30 @@
     entity's form.
     """
     __regid__ = 'inline'
+    fieldset_css_class = 'subentity'
 
     def render_title(self, w, form, values):
+        w(u'<div class="iformTitle">')
         w(u'<span>%(title)s</span> '
           '#<span class="icounter">%(counter)s</span> ' % values)
         if values['removejs']:
             values['removemsg'] = self._cw._('remove-inlined-entity-form')
             w(u'[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]'
               % values)
+        w(u'</div>')
 
     def render(self, w, form, values):
         form.add_media()
+        self.open_form(w, form, values)
+        self.render_title(w, form, values)
+        # XXX that stinks
+        # cleanup values
+        for key in ('title', 'removejs', 'removemsg'):
+            values.pop(key, None)
+        self.render_fields(w, form, values)
+        self.close_form(w, form, values)
+
+    def open_form(self, w, form, values):
         try:
             w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values)
         except KeyError:
@@ -510,22 +526,15 @@
             w(u'<div id="notice-%s" class="notice">%s</div>' % (
                 values['divid'], self._cw._('click on the box to cancel the deletion')))
         w(u'<div class="iformBody">')
-        eschema = form.edited_entity.e_schema
-        w(u'<div class="iformTitle">')
-        self.render_title(w, form, values)
-        w(u'</div>')
-        # XXX that stinks
-        # cleanup values
-        for key in ('title', 'removejs', 'removemsg'):
-            values.pop(key, None)
-        self.render_fields(w, form, values)
+
+    def close_form(self, w, form, values):
         w(u'</div></div>')
 
     def render_fields(self, w, form, values):
         w(u'<fieldset id="fs-%(divid)s">' % values)
         fields = self._render_hidden_fields(w, form)
         w(u'</fieldset>')
-        w(u'<fieldset class="subentity">')
+        w(u'<fieldset class="%s">' % self.fieldset_css_class)
         if fields:
             self._render_fields(fields, w, form)
         self.render_child_forms(w, form, values)
--- a/web/views/forms.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/forms.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/ibreadcrumbs.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/idownloadable.py	Thu Mar 21 18:13:31 2013 +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
@@ -100,7 +100,8 @@
             contenttype = adapter.download_content_type()
         self._cw.set_content_type(contenttype or self.content_type,
                                   filename=adapter.download_file_name(),
-                                  encoding=encoding)
+                                  encoding=encoding,
+                                  disposition='attachment')
 
     def call(self):
         entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
--- a/web/views/igeocodable.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/igeocodable.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/iprogress.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/isioc.py	Thu Mar 21 18:13:31 2013 +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/json.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/json.py	Thu Mar 21 18:13:31 2013 +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,6 +23,7 @@
 _ = unicode
 
 from cubicweb.utils import json_dumps
+from cubicweb.predicates import any_rset
 from cubicweb.view import EntityView, AnyRsetView
 from cubicweb.web.application import anonymized_request
 from cubicweb.web.views import basecontrollers
@@ -50,14 +51,20 @@
                 self._cw.form['vid'] = 'jsonexport'
         else: # if no vid is specified, use jsonexport
             self._cw.form['vid'] = 'jsonexport'
-        with anonymized_request(self._cw):
-            json_data = super(JsonpController, self).publish(rset)
-            if 'callback' in self._cw.form: # jsonp
-                json_padding = self._cw.form['callback']
-                # use ``application/javascript`` is ``callback`` parameter is
-                # provided, let ``application/json`` otherwise
-                self._cw.set_content_type('application/javascript')
-                json_data = '%s(%s)' % (json_padding, json_data)
+        if self._cw.vreg.config['anonymize-jsonp-queries']:
+            with anonymized_request(self._cw):
+                return self._get_json_data(rset)
+        else:
+            return self._get_json_data(rset)
+
+    def _get_json_data(self, rset):
+        json_data = super(JsonpController, self).publish(rset)
+        if 'callback' in self._cw.form: # jsonp
+            json_padding = self._cw.form['callback']
+            # use ``application/javascript`` is ``callback`` parameter is
+            # provided, let ``application/json`` otherwise
+            self._cw.set_content_type('application/javascript')
+            json_data = '%s(%s)' % (json_padding, json_data)
         return json_data
 
 
@@ -84,6 +91,7 @@
 class JsonRsetView(JsonMixIn, AnyRsetView):
     """dumps raw result set in JSON format"""
     __regid__ = 'jsonexport'
+    __select__ = any_rset() # means rset might be empty or have any shape
     title = _('json-export-view')
 
     def call(self):
--- a/web/views/management.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/management.py	Thu Mar 21 18:13:31 2013 +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,9 +20,11 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import yes, none_rset, match_user_groups, authenticated_user
+from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
+
+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
@@ -147,6 +149,8 @@
             form.add_hidden('description', binfo,
                             # we must use a text area to keep line breaks
                             widget=wdgs.TextArea({'class': 'hidden'}))
+            # add a signature so one can't send arbitrary text
+            form.add_hidden('__signature', req.vreg.config.sign_text(binfo))
             form.add_hidden('__bugreporting', '1')
             form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)]
             form.action = req.build_url('reportbug')
@@ -170,7 +174,7 @@
     """A textual stats output for monitoring tools such as munin """
 
     __regid__ = 'processinfo'
-    content_type = 'text/txt'
+    content_type = 'text/plain'
     templatable = False
     __select__ = none_rset() & match_user_groups('users', 'managers')
 
--- a/web/views/massmailing.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/massmailing.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/navigation.py	Thu Mar 21 18:13:31 2013 +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
@@ -364,11 +364,13 @@
 
     @property
     def prev_icon(self):
-        return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_prev.png'))
+        return '<img src="%s" alt="%s" />' % (
+            xml_escape(self._cw.data_url('go_prev.png')), self._cw._('previous page'))
 
     @property
     def next_icon(self):
-        return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_next.png'))
+        return '<img src="%s" alt="%s" />' % (
+            xml_escape(self._cw.data_url('go_next.png')), self._cw._('next page'))
 
     def init_rendering(self):
         adapter = self.entity.cw_adapt_to('IPrevNext')
--- a/web/views/owl.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/owl.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/plots.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/primary.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/pyviews.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/reledit.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/schema.py	Thu Mar 21 18:13:31 2013 +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/sessions.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/sessions.py	Thu Mar 21 18:13:31 2013 +0100
@@ -95,18 +95,6 @@
         #      reopening. Is it actually a problem?
         if 'last_login_time' in req.vreg.schema:
             self._update_last_login_time(req)
-        args = req.form
-        for forminternal_key in ('__form_id', '__domid', '__errorurl'):
-            args.pop(forminternal_key, None)
-        path = req.relative_path(False)
-        if path in ('login', 'logout') or req.form.get('vid') == 'loggedout':
-            path = 'view'
-            args['__message'] = req._('welcome %s !') % req.user.login
-            if 'vid' in req.form and req.form['vid'] != 'loggedout':
-                args['vid'] = req.form['vid']
-            if 'rql' in req.form:
-                args['rql'] = req.form['rql']
-            raise Redirect(req.build_url(path, **args))
         req.set_message(req._('welcome %s !') % req.user.login)
 
     def _update_last_login_time(self, req):
--- a/web/views/startup.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/startup.py	Thu Mar 21 18:13:31 2013 +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
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/staticcontrollers.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,250 @@
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Set of static resources controllers for :
+
+- /data/...
+- /static/...
+- /fckeditor/...
+
+"""
+from __future__ import with_statement
+
+import os
+import os.path as osp
+import hashlib
+import mimetypes
+from time import mktime
+from datetime import datetime, timedelta
+from logging import getLogger
+
+from cubicweb import Unauthorized
+from cubicweb.web import NotFound
+from cubicweb.web.http_headers import generateDateTime
+from cubicweb.web.controller import Controller
+from cubicweb.web.views.urlrewrite import URLRewriter
+
+
+
+class StaticFileController(Controller):
+    """an abtract class to serve static file
+
+    Make sure to add your subclass to the STATIC_CONTROLLERS list"""
+    __abstract__ = True
+    directory_listing_allowed = False
+
+    def max_age(self, path):
+        """max cache TTL"""
+        return 60*60*24*7
+
+    def static_file(self, path):
+        """Return full content of a static file.
+
+        XXX iterable content would be better
+        """
+        debugmode = self._cw.vreg.config.debugmode
+        if osp.isdir(path):
+            if self.directory_listing_allowed:
+                return u''
+            raise Unauthorized(path)
+        if not osp.isfile(path):
+            raise NotFound()
+        if not debugmode:
+            # XXX: Don't provide additional resource information to error responses
+            #
+            # the HTTP RFC recommands not going further than 1 year ahead
+            expires = datetime.now() + timedelta(days=6*30)
+            self._cw.set_header('Expires', generateDateTime(mktime(expires.timetuple())))
+
+        # XXX system call to os.stats could be cached once and for all in
+        # production mode (where static files are not expected to change)
+        #
+        # Note that: we do a osp.isdir + osp.isfile before and a potential
+        # os.read after. Improving this specific call will not help
+        #
+        # Real production environment should use dedicated static file serving.
+        self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime))
+        self._cw.validate_cache()
+        # XXX elif uri.startswith('/https/'): uri = uri[6:]
+        mimetype, encoding = mimetypes.guess_type(path)
+        if mimetype is None:
+            mimetype = 'application/octet-stream'
+        self._cw.set_content_type(mimetype, osp.basename(path), encoding)
+        with open(path, 'rb') as resource:
+            return resource.read()
+
+    @property
+    def relpath(self):
+        """path of a requested file relative to the controller"""
+        path = self._cw.form.get('static_relative_path')
+        if path is None:
+            path = self._cw.relative_path(includeparams=True)
+        return path
+
+
+class ConcatFilesHandler(object):
+    """Emulating the behavior of modconcat
+
+    this serve multiple file as a single one.
+    """
+
+    def __init__(self, config):
+        self._resources = {}
+        self.config = config
+        self.logger = getLogger('cubicweb.web')
+
+    def _resource(self, path):
+        """get the resouce"""
+        try:
+            return self._resources[path]
+        except KeyError:
+            self._resources[path] = self.config.locate_resource(path)
+            return self._resources[path]
+
+    def _up_to_date(self, filepath, paths):
+        """
+        The concat-file is considered up-to-date if it exists.
+        In debug mode, an additional check is performed to make sure that
+        concat-file is more recent than all concatenated files
+        """
+        if not osp.isfile(filepath):
+            return False
+        if self.config.debugmode:
+            concat_lastmod = os.stat(filepath).st_mtime
+            for path in paths:
+                dirpath, rid = self._resource(path)
+                if rid is None:
+                    raise NotFound(path)
+                path = osp.join(dirpath, rid)
+                if os.stat(path).st_mtime > concat_lastmod:
+                    return False
+        return True
+
+    def build_filepath(self, paths):
+        """return the filepath that will be used to cache concatenation of `paths`
+        """
+        _, ext = osp.splitext(paths[0])
+        fname = 'cache_concat_' + hashlib.md5(';'.join(paths)).hexdigest() + ext
+        return osp.join(self.config.appdatahome, 'uicache', fname)
+
+    def concat_cached_filepath(self, paths):
+        filepath = self.build_filepath(paths)
+        if not self._up_to_date(filepath, paths):
+            with open(filepath, 'wb') as f:
+                for path in paths:
+                    dirpath, rid = self._resource(path)
+                    if rid is None:
+                        # In production mode log an error, do not return a 404
+                        # XXX the erroneous content is cached anyway
+                        self.logger.error('concatenated data url error: %r file '
+                                          'does not exist', path)
+                        if self.config.debugmode:
+                            raise NotFound(path)
+                    else:
+                        with open(osp.join(dirpath, rid), 'rb') as source:
+                            for line in source:
+                                f.write(line)
+                        f.write('\n')
+        return filepath
+
+
+class DataController(StaticFileController):
+    """Controller in charge of serving static file in /data/
+
+    Handle modeconcat like url.
+    """
+
+    __regid__ = 'data'
+
+    def __init__(self, *args, **kwargs):
+        super(DataController, self).__init__(*args, **kwargs)
+        config = self._cw.vreg.config
+        md5_version = config.instance_md5_version()
+        self.base_datapath = config.data_relpath()
+        self.data_modconcat_basepath = '%s??' % self.base_datapath
+        self.concat_files_registry = ConcatFilesHandler(config)
+
+    def publish(self, rset=None):
+        config = self._cw.vreg.config
+        # includeparams=True for modconcat-like urls
+        relpath = self.relpath
+        if relpath.startswith(self.data_modconcat_basepath):
+            paths = relpath[len(self.data_modconcat_basepath):].split(',')
+            filepath = self.concat_files_registry.concat_cached_filepath(paths)
+        else:
+            # skip leading '/data/' and url params
+            relpath = relpath[len(self.base_datapath):].split('?', 1)[0]
+            dirpath, rid = config.locate_resource(relpath)
+            if dirpath is None:
+                raise NotFound()
+            filepath = osp.join(dirpath, rid)
+        return self.static_file(filepath)
+
+
+class FCKEditorController(StaticFileController):
+    """Controller in charge of serving FCKEditor related file
+
+    The motivational for a dedicated controller have been lost.
+    """
+
+    __regid__ = 'fckeditor'
+
+    def publish(self, rset=None):
+        config = self._cw.vreg.config
+        if self._cw.https:
+            uiprops = config.https_uiprops
+        else:
+            uiprops = config.uiprops
+        relpath = self.relpath
+        if relpath.startswith('fckeditor/'):
+            relpath = relpath[len('fckeditor/'):]
+        relpath = relpath.split('?', 1)[0]
+        return self.static_file(osp.join(uiprops['FCKEDITOR_PATH'], relpath))
+
+
+class StaticDirectoryController(StaticFileController):
+    """Controller in charge of serving static file in /static/
+    """
+    __regid__ = 'static'
+
+    def publish(self, rset=None):
+        staticdir = self._cw.vreg.config.static_directory
+        relpath = self.relpath
+        return self.static_file(osp.join(staticdir, relpath))
+
+STATIC_CONTROLLERS = [DataController, FCKEditorController,
+                      StaticDirectoryController]
+
+class StaticControlerRewriter(URLRewriter):
+    """a quick and dirty rewritter in charge of server static file.
+
+    This is a work around the flatness of url handling in cubicweb."""
+
+    __regid__ = 'static'
+
+    priority = 10
+
+    def rewrite(self, req, uri):
+        for ctrl in STATIC_CONTROLLERS:
+            if uri.startswith('/%s/' % ctrl.__regid__):
+                break
+        else:
+            self.debug("not a static file uri: %s", uri)
+            raise KeyError(uri)
+        relpath = self._cw.relative_path(includeparams=False)
+        self._cw.form['static_relative_path'] = self._cw.relative_path(includeparams=True)
+        return ctrl.__regid__, None
--- a/web/views/tableview.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/tableview.py	Thu Mar 21 18:13:31 2013 +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,
@@ -173,8 +174,8 @@
 
     @cachedproperty
     def initial_load(self):
-        """We detect a bit heuristically if we are built for the first time.
-        or from subsequent calls by the form filter or by the pagination hooks.
+        """We detect a bit heuristically if we are built for the first time or
+        from subsequent calls by the form filter or by the pagination hooks.
         """
         form = self._cw.form
         return 'fromformfilter' not in form and '__fromnavigation' not in form
@@ -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):
@@ -866,7 +864,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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/tabs.py	Thu Mar 21 18:13:31 2013 +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 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
 
@@ -47,18 +47,26 @@
         """a lazy version of wview"""
         w = w or self.w
         self._cw.add_js('cubicweb.ajax.js')
+        # the form is copied into urlparams to please the inner views
+        # that might want to take params from it
+        # beware of already present rql or eid elements
+        # to be safe of collision a proper argument passing protocol
+        # (with namespaces) should be used instead of the current
+        # ad-hockery
         urlparams = self._cw.form.copy()
+        urlparams.pop('rql', None)
+        urlparams.pop('eid', None)
         urlparams.update({'vid' : vid, 'fname' : 'view'})
         if rql:
             urlparams['rql'] = rql
         elif eid:
-            urlparams['rql'] = uilib.rql_for_eid(eid)
+            urlparams['eid'] = eid
         elif rset:
             urlparams['rql'] = rset.printable_rql()
         if tabid is None:
             tabid = uilib.domid(vid)
         w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
-            tabid, xml_escape(self._cw.build_url('json', **urlparams))))
+            tabid, xml_escape(self._cw.build_url('ajax', **urlparams))))
         if show_spinbox:
             # Don't use ``alt`` since image is a *visual* helper for ajax
             w(u'<img style="display: none" src="%s" alt="" id="%s-hole"/>'
@@ -82,6 +90,7 @@
 
 class TabsMixin(LazyViewMixin):
     """a tab mixin to easily get jQuery based, lazy, ajax tabs"""
+    lazy = True
 
     @property
     def cookie_name(self):
@@ -114,7 +123,7 @@
             vid = tabkwargs.get('vid', tabid)
             domid = uilib.domid(tabid)
             try:
-                viewsvreg.select(vid, self._cw, **tabkwargs)
+                viewsvreg.select(vid, self._cw, tabid=domid, **tabkwargs)
             except NoSelectableObject:
                 continue
             selected_tabs.append((tabid, domid, tabkwargs))
@@ -149,17 +158,20 @@
         w(u'</ul>')
         for tabid, domid, tabkwargs in tabs:
             w(u'<div id="%s">' % domid)
-            tabkwargs.setdefault('tabid', domid)
-            tabkwargs.setdefault('vid', tabid)
-            tabkwargs.setdefault('rset', self.cw_rset)
-            self.lazyview(**tabkwargs)
+            if self.lazy:
+                tabkwargs.setdefault('tabid', domid)
+                tabkwargs.setdefault('vid', tabid)
+                self.lazyview(**tabkwargs)
+            else:
+                self._cw.view(tabid, w=self.w, **tabkwargs)
             w(u'</div>')
         w(u'</div>')
         # call the setTab() JS function *after* each tab is generated
         # because the callback binding needs to be done before
         # XXX make work history: true
-        self._cw.add_onload(u"""
-  jQuery('#entity-tabs-%(eeid)s').tabs(
+        if self.lazy:
+            self._cw.add_onload(u"""
+  jQuery('#entity-tabs-%(uid)s').tabs(
     { selected: %(tabindex)s,
       select: function(event, ui) {
         setTab(ui.panel.id, '%(cookiename)s');
@@ -167,9 +179,13 @@
     });
   setTab('%(domid)s', '%(cookiename)s');
 """ % {'tabindex'   : active_tab_idx,
-       'domid'        : active_tab,
-       'eeid'       : (entity and entity.eid or uid),
+       'domid'      : active_tab,
+       'uid'        : uid,
        'cookiename' : self.cookie_name})
+        else:
+            self._cw.add_onload(
+                u"jQuery('#entity-tabs-%(uid)s').tabs({selected: %(tabindex)s});"
+                % {'tabindex': active_tab_idx, 'uid': uid})
 
 
 class EntityRelationView(EntityView):
@@ -210,8 +226,7 @@
     tabs = [_('main_tab')]
     default_tab = 'main_tab'
 
-    def cell_call(self, row, col):
-        entity = self.cw_rset.complete_entity(row, col)
+    def render_entity(self, entity):
         self.render_entity_toolbox(entity)
         self.w(u'<div class="tabbedprimary"></div>')
         self.render_entity_title(entity)
--- a/web/views/timeline.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/timeline.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/timetable.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/treeview.py	Thu Mar 21 18:13:31 2013 +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)
@@ -238,7 +239,7 @@
             w(u'<li class="%s">' % u' '.join(liclasses))
         else:
             rql = itree.children_rql() % {'x': entity.eid}
-            url = xml_escape(self._cw.build_url('json', rql=rql, vid=parentvid,
+            url = xml_escape(self._cw.build_url('ajax', rql=rql, vid=parentvid,
                                                 pageid=self._cw.pageid,
                                                 treeid=treeid,
                                                 fname='view',
@@ -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))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/undohistory.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,224 @@
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+
+from logilab.common.registry import Predicate
+
+from cubicweb import UnknownEid, tags, transaction as tx
+from cubicweb.view import View, StartupView
+from cubicweb.predicates import match_kwargs, ExpectedValuePredicate
+from cubicweb.schema import display_name
+
+
+class undoable_action(Predicate):
+    """Select only undoable actions depending on filters provided. Undo Action
+    is expected to be specified by the `tx_action` argument.
+
+    Currently the only implemented filter is:
+
+    :param action_type: chars among CUDAR (standing for Create, Update, Delete,
+                        Add, Remove)
+    """
+
+    # XXX FIXME : this selector should be completed to allow selection on the
+    # entity or relation types and public / private.
+    def __init__(self, action_type='CUDAR'):
+        assert not set(action_type) - set('CUDAR')
+        self.action_type = action_type
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__, ', '.join(
+            "%s=%v" % (str(k), str(v)) for k, v in kwargs.iteritems() ))
+
+    def __call__(self, cls, req, tx_action=None, **kwargs):
+        # tx_action is expected to be a transaction.AbstractAction
+        if not isinstance(tx_action, tx.AbstractAction):
+            return 0
+        # Filter according to action type
+        return int(tx_action.action in self.action_type)
+
+
+class UndoHistoryView(StartupView):
+    __regid__ = 'undohistory'
+    title = _('Undoing')
+    item_vid = 'undoable-transaction-view'
+    cache_max_age = 0
+
+    redirect_path = 'view' #TODO
+    redirect_params = dict(vid='undohistory') #TODO
+    public_actions_only = True
+
+    # TODO Allow to choose if if want all actions or only the public ones
+    # (default)
+
+    def call(self, **kwargs):
+        txs = self._cw.cnx.undoable_transactions()
+        if txs :
+            self.w(u"<ul class='undo-transactions'>")
+            for tx in txs:
+                self.cell_call(tx)
+            self.w(u"</ul>")
+
+    def cell_call(self, tx):
+        self.w(u'<li>')
+        self.wview(self.item_vid, None, txuuid=tx.uuid,
+                   public=self.public_actions_only,
+                   redirect_path=self.redirect_path,
+                   redirect_params=self.redirect_params)
+        self.w(u'</li>\n')
+
+
+class UndoableTransactionView(View):
+    __regid__ = 'undoable-transaction-view'
+    __select__ = View.__select__ & match_kwargs('txuuid')
+
+    item_vid = 'undoable-action-list-view'
+    cache_max_age = 0
+
+    def build_undo_link(self, txuuid,
+                        redirect_path=None, redirect_params=None):
+        """ the kwargs are passed to build_url"""
+        _ = self._cw._
+        redirect = {}
+        if redirect_path:
+            redirect['__redirectpath'] = redirect_path
+        if redirect_params:
+            if isinstance(redirect_params, dict):
+                redirect['__redirectparams'] = self._cw.build_url_params(**redirect_params)
+            else:
+                redirect['__redirectparams'] = redirect_params
+        link_url = self._cw.build_url('undo', txuuid=txuuid, **redirect)
+        msg = u"<span class='undo'>%s</span>" % tags.a( _('undo'), href=link_url)
+        return msg
+
+    def call(self, txuuid, public=True,
+             redirect_path=None, redirect_params=None):
+        _ = self._cw._
+        txinfo = self._cw.cnx.transaction_info(txuuid)
+        try:
+            #XXX Under some unknown circumstances txinfo.user_eid=-1
+            user = self._cw.entity_from_eid(txinfo.user_eid)
+        except UnknownEid:
+            user = None
+        undo_url = self.build_undo_link(txuuid,
+                                        redirect_path=redirect_path,
+                                        redirect_params=redirect_params)
+        txinfo_dict = dict( dt = self._cw.format_date(txinfo.datetime, time=True),
+                            user_eid = txinfo.user_eid,
+                            user = user and user.view('outofcontext') or _("undefined user"),
+                            txuuid = txuuid,
+                            undo_link = undo_url)
+        self.w( _("By %(user)s on %(dt)s [%(undo_link)s]") % txinfo_dict)
+
+        tx_actions = txinfo.actions_list(public=public)
+        if tx_actions :
+            self.wview(self.item_vid, None, tx_actions=tx_actions)
+
+
+class UndoableActionListView(View):
+    __regid__ = 'undoable-action-list-view'
+    __select__ = View.__select__ & match_kwargs('tx_actions')
+    title = _('Undoable actions')
+    item_vid = 'undoable-action-view'
+    cache_max_age = 0
+
+    def call(self, tx_actions):
+        if tx_actions :
+            self.w(u"<ol class='undo-actions'>")
+            for action in tx_actions:
+                self.cell_call(action)
+            self.w(u"</ol>")
+
+    def cell_call(self, action):
+        self.w(u'<li>')
+        self.wview(self.item_vid, None, tx_action=action)
+        self.w(u'</li>\n')
+
+
+class UndoableActionBaseView(View):
+    __regid__ = 'undoable-action-view'
+    __abstract__ = True
+
+    def call(self, tx_action):
+        raise NotImplementedError(self)
+
+    def _build_entity_link(self, eid):
+        try:
+            entity = self._cw.entity_from_eid(eid)
+            return entity.view('outofcontext')
+        except UnknownEid:
+            return _("(suppressed) entity #%d") % eid
+
+    def _build_relation_info(self, rtype, eid_from,  eid_to):
+        return dict( rtype=display_name(self._cw, rtype),
+                     entity_from=self._build_entity_link(eid_from),
+                     entity_to=self._build_entity_link(eid_to) )
+
+    def _build_entity_info(self, etype, eid, changes):
+        return dict( etype=display_name(self._cw, etype),
+                     entity=self._build_entity_link(eid),
+                     eid=eid,
+                     changes=changes)
+
+
+class UndoableAddActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='A')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Added relation : %(entity_from)s %(rtype)s %(entity_to)s") %
+               self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to))
+
+
+class UndoableRemoveActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='R')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Delete relation : %(entity_from)s %(rtype)s %(entity_to)s") %
+               self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to))
+
+
+class UndoableCreateActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='C')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Created %(etype)s : %(entity)s") % #  : %(changes)s
+               self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes) )
+
+
+class UndoableDeleteActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='D')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Deleted %(etype)s : %(entity)s") %
+               self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes))
+
+
+class UndoableUpdateActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='U')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Updated %(etype)s : %(entity)s") %
+               self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes))
--- a/web/views/urlpublishing.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/urlpublishing.py	Thu Mar 21 18:13:31 2013 +0100
@@ -106,7 +106,8 @@
         :param req: the request object
 
         :type path: str
-        :param path: the path of the resource to publish
+        :param path: the path of the resource to publish. If empty, None or "/"
+                     "view" is used as the default path.
 
         :rtype: tuple(str, `cubicweb.rset.ResultSet` or None)
         :return: the publishing method identifier and an optional result set
--- a/web/views/vcard.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/vcard.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/wdoc.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/workflow.py	Thu Mar 21 18:13:31 2013 +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
@@ -315,7 +315,7 @@
     wf = req.entity_from_eid(wfeid)
     rschema = req.vreg.schema[field.name]
     param = 'toeid' if field.role == 'subject' else 'fromeid'
-    return sorted((e.view('combobox'), e.eid)
+    return sorted((e.view('combobox'), unicode(e.eid))
                   for e in getattr(wf, 'reverse_%s' % wfrelation)
                   if rschema.has_perm(req, 'add', **{param: e.eid}))
 
@@ -330,12 +330,14 @@
 
 def transition_states_vocabulary(form, field):
     entity = form.edited_entity
-    if not entity.has_eid():
+    if entity.has_eid():
+        wfeid = entity.transition_of[0].eid
+    else:
         eids = form.linked_to.get(('transition_of', 'subject'))
         if not eids:
             return []
-        return _wf_items_for_relation(form._cw, eids[0], 'state_of', field)
-    return field.relvoc_unrelated(form)
+        wfeid = eids[0]
+    return _wf_items_for_relation(form._cw, wfeid, 'state_of', field)
 
 _afs.tag_subject_of(('*', 'destination_state', '*'), 'main', 'attributes')
 _affk.tag_subject_of(('*', 'destination_state', '*'),
@@ -348,12 +350,14 @@
 
 def state_transitions_vocabulary(form, field):
     entity = form.edited_entity
-    if not entity.has_eid():
+    if entity.has_eid():
+        wfeid = entity.state_of[0].eid
+    else :
         eids = form.linked_to.get(('state_of', 'subject'))
-        if eids:
-            return _wf_items_for_relation(form._cw, eids[0], 'transition_of', field)
-        return []
-    return field.relvoc_unrelated(form)
+        if not eids:
+            return []
+        wfeid = eids[0]
+    return _wf_items_for_relation(form._cw, wfeid, 'transition_of', field)
 
 _afs.tag_subject_of(('State', 'allowed_transition', '*'), 'main', 'attributes')
 _affk.tag_subject_of(('State', 'allowed_transition', '*'),
--- a/web/views/xbel.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/xbel.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/views/xmlrss.py	Thu Mar 21 18:13:31 2013 +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	Wed Aug 01 10:30:48 2012 +0200
+++ b/web/webconfig.py	Thu Mar 21 18:13:31 2013 +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,10 +21,12 @@
 _ = unicode
 
 import os
+import hmac
+from uuid import uuid4
 from os.path import join, exists, split, isdir
 from warnings import warn
 
-from logilab.common.decorators import cached
+from logilab.common.decorators import cached, cachedproperty
 from logilab.common.deprecation import deprecated
 
 from cubicweb import ConfigurationError
@@ -219,6 +221,12 @@
           'help': 'use modconcat-like URLS to concat and serve JS / CSS files',
           'group': 'web', 'level': 2,
           }),
+        ('anonymize-jsonp-queries',
+         {'type': 'yn',
+          'default': True,
+          'help': 'anonymize the connection before executing any jsonp query.',
+          'group': 'web', 'level': 1
+          }),
         ))
 
     def fckeditor_installed(self):
@@ -251,7 +259,7 @@
 
     def anonymous_user(self):
         """return a login and password to use for anonymous users.
-        
+
         None may be returned for both if anonymous connection is not
         allowed or if an empty login is used in configuration
         """
@@ -266,6 +274,25 @@
             raise ConfigurationError("anonymous information should only contains ascii")
         return user, passwd
 
+    @cachedproperty
+    def _instance_salt(self):
+        """This random key/salt is used to sign content to be sent back by
+        browsers, eg. in the error report form.
+        """
+        return str(uuid4())
+
+    def sign_text(self, text):
+        """sign some text for later checking"""
+        # replace \r\n so we do not depend on whether a browser "reencode"
+        # original message using \r\n or not
+        return hmac.new(self._instance_salt,
+                        text.strip().replace('\r\n', '\n')).hexdigest()
+
+    def check_text_sign(self, text, signature):
+        """check the text signature is equal to the given signature"""
+        return self.sign_text(text) == signature
+
+
     def locate_resource(self, rid):
         """return the (directory, filename) where the given resource
         may be found
@@ -321,17 +348,19 @@
         if not (self.repairing or self.creating):
             self.global_set_option('base-url', baseurl)
         httpsurl = self['https-url']
-        if (self.debugmode or self.mode == 'test'):
-            datadir_path = 'data/'
-        else:
-            datadir_path = 'data/%s/' % self.instance_md5_version()
+        data_relpath = self.data_relpath()
         if httpsurl:
             if httpsurl[-1] != '/':
                 httpsurl += '/'
                 if not self.repairing:
                     self.global_set_option('https-url', httpsurl)
-            self.https_datadir_url = httpsurl + datadir_path
-        self.datadir_url = baseurl + datadir_path
+            self.https_datadir_url = httpsurl + data_relpath
+        self.datadir_url = baseurl + data_relpath
+
+    def data_relpath(self):
+        if self.mode == 'test':
+            return 'data/'
+        return 'data/%s/' % self.instance_md5_version()
 
     def _build_ui_properties(self):
         # self.datadir_url[:-1] to remove trailing /
--- a/wsgi/handler.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/wsgi/handler.py	Thu Mar 21 18:13:31 2013 +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,14 +15,16 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""WSGI request handler for cubicweb
+"""WSGI request handler for cubicweb"""
 
-"""
+
 
 __docformat__ = "restructuredtext en"
 
+from itertools import chain, repeat, izip
+
 from cubicweb import AuthenticationError
-from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
+from cubicweb.web import DirectResponse
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.wsgi.request import CubicWebWsgiRequest
 
@@ -71,7 +73,6 @@
     505: 'HTTP VERSION NOT SUPPORTED',
 }
 
-
 class WSGIResponse(object):
     """encapsulates the wsgi response parameters
     (code, headers and body if there is one)
@@ -79,7 +80,9 @@
     def __init__(self, code, req, body=None):
         text = STATUS_CODE_TEXT.get(code, 'UNKNOWN STATUS CODE')
         self.status =  '%s %s' % (code, text)
-        self.headers = [(str(k), str(v)) for k, v in req.headers_out.items()]
+        self.headers = list(chain(*[izip(repeat(k), v)
+                                    for k, v in req.headers_out.getAllRawHeaders()]))
+        self.headers = [(str(k), str(v)) for k, v in self.headers]
         if body:
             self.body = [body]
         else:
@@ -103,95 +106,31 @@
     def __init__(self, config, vreg=None):
         self.appli = CubicWebPublisher(config, vreg=vreg)
         self.config = config
-        self.base_url = None
-#         self.base_url = config['base-url'] or config.default_base_url()
-#         assert self.base_url[-1] == '/'
-#         self.https_url = config['https-url']
-#         assert not self.https_url or self.https_url[-1] == '/'
+        self.base_url = config['base-url']
+        self.https_url = config['https-url']
         self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter')
 
     def _render(self, req):
         """this function performs the actual rendering
-        XXX missing: https handling, url rewriting, cache management,
-                     authentication
         """
         if self.base_url is None:
             self.base_url = self.config._base_url = req.base_url()
-        # XXX https handling needs to be implemented
-        if req.authmode == 'http':
-            # activate realm-based auth
-            realm = self.config['realm']
-            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
         try:
-            self.appli.connect(req)
-        except Redirect, ex:
-            return self.redirect(req, ex.location)
-        path = req.path
-        if not path or path == "/":
-            path = 'view'
-        try:
-            result = self.appli.publish(path, req)
+            path = req.path
+            result = self.appli.handle_request(req, path)
         except DirectResponse, ex:
-            return WSGIResponse(200, req, ex.response)
-        except StatusResponse, ex:
-            return WSGIResponse(ex.status, req, ex.content)
-        except AuthenticationError:  # must be before AuthenticationError
-            return self.request_auth(req)
-        except LogOut:
-            if self.config['auth-mode'] == 'cookie':
-                # in cookie mode redirecting to the index view is enough :
-                # either anonymous connection is allowed and the page will
-                # be displayed or we'll be redirected to the login form
-                msg = req._('you have been logged out')
-#                 if req.https:
-#                     req._base_url =  self.base_url
-#                     req.https = False
-                url = req.build_url('view', vid='index', __message=msg)
-                return self.redirect(req, url)
-            else:
-                # in http we have to request auth to flush current http auth
-                # information
-                return self.request_auth(req, loggedout=True)
-        except Redirect, ex:
-            return self.redirect(req, ex.location)
-        if not result:
-            # no result, something went wrong...
-            self.error('no data (%s)', req)
-            # 500 Internal server error
-            return self.redirect(req, req.build_url('error'))
-        return WSGIResponse(200, req, result)
+            return ex.response
+        return WSGIResponse(req.status_out, req, result)
 
 
     def __call__(self, environ, start_response):
         """WSGI protocol entry point"""
-        req = CubicWebWsgiRequest(environ, self.appli.vreg, self.base_url)
+        req = CubicWebWsgiRequest(environ, self.appli.vreg)
         response = self._render(req)
         start_response(response.status, response.headers)
         return response.body
 
-    def redirect(self, req, location):
-        """convenience function which builds a redirect WSGIResponse"""
-        self.debug('redirecting to %s', location)
-        req.set_header('location', str(location))
-        return WSGIResponse(303, req)
 
-    def request_auth(self, req, loggedout=False):
-        """returns the appropriate WSGIResponse to require the user to log in
-        """
-#         if self.https_url and req.base_url() != self.https_url:
-#             return self.redirect(self.https_url + 'login')
-        if self.config['auth-mode'] == 'http':
-            code = 401 # UNAUTHORIZED
-        else:
-            code = 403 # FORBIDDEN
-        if loggedout:
-#             if req.https:
-#                 req._base_url =  self.base_url
-#                 req.https = False
-            content = self.appli.loggedout_content(req)
-        else:
-            content = self.appli.need_login_content(req)
-        return WSGIResponse(code, req, content)
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
--- a/wsgi/request.py	Wed Aug 01 10:30:48 2012 +0200
+++ b/wsgi/request.py	Thu Mar 21 18:13:31 2013 +0100
@@ -32,7 +32,8 @@
 
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.wsgi import (pformat, qs2dict, safe_copyfileobj, parse_file_upload,
-                        normalize_header)
+                           normalize_header)
+from cubicweb.web.http_headers import Headers
 
 
 
@@ -40,22 +41,23 @@
     """most of this code COMES FROM DJANO
     """
 
-    def __init__(self, environ, vreg, base_url=None):
+    def __init__(self, environ, vreg):
         self.environ = environ
         self.path = environ['PATH_INFO']
         self.method = environ['REQUEST_METHOD'].upper()
-        self._headers = dict([(normalize_header(k[5:]), v) for k, v in self.environ.items()
-                              if k.startswith('HTTP_')])
+
+        headers_in = dict((normalize_header(k[5:]), v) for k, v in self.environ.items()
+                          if k.startswith('HTTP_'))
         https = environ.get("HTTPS") in ('yes', 'on', '1')
-        self._base_url = base_url or self.instance_uri()
         post, files = self.get_posted_data()
-        super(CubicWebWsgiRequest, self).__init__(vreg, https, post)
+
+        super(CubicWebWsgiRequest, self).__init__(vreg, https, post,
+                                                  headers= headers_in)
         if files is not None:
             for key, (name, _, stream) in files.iteritems():
-                name = unicode(name, self.encoding)
+                if name is not None:
+                    name = unicode(name, self.encoding)
                 self.form[key] = (name, stream)
-        # prepare output headers
-        self.headers_out = {}
 
     def __repr__(self):
         # Since this is called as part of error handling, we need to be very
@@ -67,9 +69,6 @@
 
     ## cubicweb request interface ################################################
 
-    def base_url(self):
-        return self._base_url
-
     def http_method(self):
         """returns 'POST', 'GET', 'HEAD', etc."""
         return self.method
@@ -91,31 +90,6 @@
 
         return path
 
-    def get_header(self, header, default=None):
-        """return the value associated with the given input HTTP header,
-        raise KeyError if the header is not set
-        """
-        return self._headers.get(normalize_header(header), default)
-
-    def set_header(self, header, value, raw=True):
-        """set an output HTTP header"""
-        assert raw, "don't know anything about non-raw headers for wsgi requests"
-        self.headers_out[header] = value
-
-    def add_header(self, header, value):
-        """add an output HTTP header"""
-        self.headers_out[header] = value
-
-    def remove_header(self, header):
-        """remove an output HTTP header"""
-        self.headers_out.pop(header, None)
-
-    def header_if_modified_since(self):
-        """If the HTTP header If-modified-since is set, return the equivalent
-        mx date time value (GMT), else return None
-        """
-        return None
-
     ## wsgi request helpers ###################################################
 
     def instance_uri(self):
@@ -146,6 +120,8 @@
             and self.environ['wsgi.url_scheme'] == 'https'
 
     def get_posted_data(self):
+        # The WSGI spec says 'QUERY_STRING' may be absent.
+        post = qs2dict(self.environ.get('QUERY_STRING', ''))
         files = None
         if self.method == 'POST':
             if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
@@ -153,12 +129,10 @@
                                    for k, v in self.environ.items()
                                    if k.startswith('HTTP_'))
                 header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '')
-                post, files = parse_file_upload(header_dict, self.raw_post_data)
+                post_, files = parse_file_upload(header_dict, self.raw_post_data)
+                post.update(post_)
             else:
-                post = qs2dict(self.raw_post_data)
-        else:
-            # The WSGI spec says 'QUERY_STRING' may be absent.
-            post = qs2dict(self.environ.get('QUERY_STRING', ''))
+                post.update(qs2dict(self.raw_post_data))
         return post, files
 
     @property
@@ -176,20 +150,3 @@
         postdata = buf.getvalue()
         buf.close()
         return postdata
-
-    def _validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
-        exists and is still usable
-        """
-        # XXX
-#         if self.get_header('Cache-Control') in ('max-age=0', 'no-cache'):
-#             # Expires header seems to be required by IE7
-#             self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-#             return
-#         try:
-#             http.checkPreconditions(self._twreq, _PreResponse(self))
-#         except http.HTTPError, ex:
-#             self.info('valid http cache, no actual rendering')
-#             raise DirectResponse(ex.response)
-        # Expires header seems to be required by IE7
-        self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zmqclient.py	Thu Mar 21 18:13:31 2013 +0100
@@ -0,0 +1,58 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Source to query another RQL repository using pyro"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from functools import partial
+import zmq
+
+
+# XXX hack to overpass old zmq limitation that force to have
+# only one context per python process
+try:
+    from cubicweb.server.cwzmq import ctx
+except ImportError:
+    ctx = zmq.Context()
+
+class ZMQRepositoryClient(object):
+    """
+    This class delegate the overall repository stuff to a remote source.
+
+    So calling a method of this repository will results on calling the
+    corresponding method of the remote source repository.
+
+    Any raised exception on the remote source is propagated locally.
+
+    ZMQ is used as the transport layer and cPickle is used to serialize data.
+    """
+
+    def __init__(self, zmq_address):
+        self.socket = ctx.socket(zmq.REQ)
+        self.socket.connect(zmq_address)
+
+    def __zmqcall__(self, name, *args, **kwargs):
+         self.socket.send_pyobj([name, args, kwargs])
+         result = self.socket.recv_pyobj()
+         if isinstance(result, BaseException):
+             raise result
+         return result
+
+    def __getattr__(self, name):
+        return partial(self.__zmqcall__, name)