backport default into stable. STABLE IS NOW 3.9, default 3.10 stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 19 Jul 2010 15:37:02 +0200
branchstable
changeset 5994 97c55baefa0c
parent 5976 00b1b6b906cf (current diff)
parent 5992 5f9a9086c171 (diff)
child 5995 b9c612274af7
backport default into stable. STABLE IS NOW 3.9, default 3.10
devtools/test/data/dbfill.conf
doc/book/en/devrepo/entityclasses/interfaces.rst
skeleton/data/external_resources.tmpl
skeleton/test/test_CUBENAME.py
web/data/cubicweb.bookmarks.js
web/data/cubicweb.massmailing.js
web/data/external_resources
web/data/mail.gif
web/data/nomail.gif
web/test/jstest_python.jst
--- a/.hgtags	Thu Jul 15 12:03:13 2010 +0200
+++ b/.hgtags	Mon Jul 19 15:37:02 2010 +0200
@@ -135,5 +135,11 @@
 5d05b08adeab1ea301e49ed8537e35ede6db92f6 cubicweb-debian-version-3.8.5-1
 1a24c62aefc5e57f61be3d04affd415288e81904 cubicweb-version-3.8.6
 607a90073911b6bb941a49b5ec0b0d2a9cd479af cubicweb-debian-version-3.8.6-1
+d9936c39d478b6701a4adef17bc28888ffa011c6 cubicweb-version-3.9.0
+eda4940ffef8b7d36127e68de63a52388374a489 cubicweb-debian-version-3.9.0-1
 a1a334d934390043a4293a4ee42bdceb1343246e cubicweb-version-3.8.7
 1cccf88d6dfe42986e1091de4c364b7b5814c54f cubicweb-debian-version-3.8.7-1
+4d75f743ed49dd7baf8bde7b0e475244933fa08e cubicweb-version-3.9.1
+9bd75af3dca36d7be5d25fc5ab1b89b34c811456 cubicweb-debian-version-3.9.1-1
+e51796b9caf389c224c6f66dcb8aa75bf1b82eff cubicweb-version-3.9.2
+8a23821dc1383e14a7e92a931b91bc6eed4d0af7 cubicweb-debian-version-3.9.2-1
--- a/MANIFEST.in	Thu Jul 15 12:03:13 2010 +0200
+++ b/MANIFEST.in	Mon Jul 19 15:37:02 2010 +0200
@@ -5,13 +5,14 @@
 include bin/cubicweb-*
 include man/cubicweb-ctl.1
 
-recursive-include doc README makefile *.conf *.py *.rst *.txt *.html *.png *.svg *.zargo *.dia
+recursive-include doc README makefile *.conf *.css *.py *.rst *.txt *.html *.png *.svg *.zargo *.dia
 
 recursive-include misc *.py *.png *.display
 
 include web/views/*.pt
 recursive-include web/data external_resources *.js *.css *.py *.png *.gif *.ico *.ttf
 recursive-include web/wdoc *.rst *.png *.xml ChangeLog*
+recursive-include devtools/data *.js *.css
 
 recursive-include i18n *.pot *.po
 recursive-include schemas *.py *.sql
@@ -21,10 +22,15 @@
 recursive-include sobjects/test/data bootstrap_cubes *.py
 recursive-include hooks/test/data bootstrap_cubes *.py
 recursive-include server/test/data bootstrap_cubes *.py source*
-recursive-include web/test/data bootstrap_cubes *.py
-recursive-include devtools/test/data bootstrap_cubes *.py *.txt
+recursive-include devtools/test/data bootstrap_cubes *.py *.txt *.js
+recursive-include web/test/data bootstrap_cubes pouet.css *.py
+
+recursive-include web/test/jstests *.js *.html *.css *.json
+recursive-include web/test/windmill *.py
 
 recursive-include skeleton *.py *.css *.js *.po compat *.in *.tmpl
 
+prune doc/book/en/.static/
+prune doc/book/fr/.static/
 prune misc/cwfs
 prune goa
--- a/__init__.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/__init__.py	Mon Jul 19 15:37:02 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """CubicWeb is a generic framework to quickly build applications which describes
 relations between entitites.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 # ignore the pygments UserWarnings
--- a/__pkginfo__.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/__pkginfo__.py	Mon Jul 19 15:37:02 2010 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 8, 7)
+numversion = (3, 9, 2)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -41,9 +41,9 @@
 
 __depends__ = {
     'logilab-common': '>= 0.50.2',
-    'logilab-mtconverter': '>= 0.6.0',
+    'logilab-mtconverter': '>= 0.8.0',
     'rql': '>= 0.26.2',
-    'yams': '>= 0.28.1',
+    'yams': '>= 0.29.1',
     'docutils': '>= 0.6',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependancies
@@ -52,7 +52,7 @@
     'Twisted': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '>= 1.0.5',
+    'logilab-database': '>= 1.1.0',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
     }
 
@@ -77,6 +77,7 @@
                 join('server', 'test', 'data'),
                 join('hooks', 'test', 'data'),
                 join('web', 'test', 'data'),
+                join('devtools', 'data'),
                 join('devtools', 'test', 'data'),
                 'schemas', 'skeleton']
 
--- a/appobject.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/appobject.py	Mon Jul 19 15:37:02 2010 +0200
@@ -39,6 +39,92 @@
 from logilab.common.decorators import classproperty
 from logilab.common.logging_ext import set_log_methods
 
+from cubicweb.cwconfig import CubicWebConfiguration
+
+def class_regid(cls):
+    """returns a unique identifier for an appobject class"""
+    if 'id' in cls.__dict__:
+        warn('[3.6] %s.%s: id is deprecated, use __regid__'
+             % (cls.__module__, cls.__name__), DeprecationWarning)
+        cls.__regid__ = cls.id
+    if hasattr(cls, 'id') and not isinstance(cls.id, property):
+        return cls.id
+    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 ########################################
 
@@ -175,6 +261,7 @@
 
 class AndSelector(MultiSelector):
     """and-chained selectors (formerly known as chainall)"""
+    @lltrace
     def __call__(self, cls, *args, **kwargs):
         score = 0
         for selector in self.selectors:
@@ -187,6 +274,7 @@
 
 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)
@@ -199,6 +287,7 @@
     def __init__(self, selector):
         self.selector = selector
 
+    @lltrace
     def __call__(self, cls, *args, **kwargs):
         score = self.selector(cls, *args, **kwargs)
         return int(not score)
--- a/cwconfig.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/cwconfig.py	Mon Jul 19 15:37:02 2010 +0200
@@ -296,8 +296,6 @@
     # log_format = '%(asctime)s - [%(threadName)s] (%(name)s) %(levelname)s: %(message)s'
     # nor remove appobjects based on unused interface [???]
     cleanup_interface_sobjects = True
-    # debug mode
-    debugmode = False
 
 
     if (CWDEV and _forced_mode != 'system'):
@@ -499,6 +497,13 @@
             deps = dict((key, None) for key in deps)
             warn('[3.8] cube %s should define %s as a dict' % (cube, key),
                  DeprecationWarning)
+        for depcube in deps:
+            try:
+                newname = CW_MIGRATION_MAP[depcube]
+            except KeyError:
+                pass
+            else:
+                deps[newname] = deps.pop(depcube)
         return deps
 
     @classmethod
@@ -518,17 +523,17 @@
         """
         cubes = list(cubes)
         todo = cubes[:]
+        if with_recommends:
+            available = set(cls.available_cubes())
         while todo:
             cube = todo.pop(0)
             for depcube in cls.cube_dependencies(cube):
                 if depcube not in cubes:
-                    depcube = CW_MIGRATION_MAP.get(depcube, depcube)
                     cubes.append(depcube)
                     todo.append(depcube)
             if with_recommends:
                 for depcube in cls.cube_recommends(cube):
-                    if depcube not in cubes:
-                        depcube = CW_MIGRATION_MAP.get(depcube, depcube)
+                    if depcube not in cubes and depcube in available:
                         cubes.append(depcube)
                         todo.append(depcube)
         return cubes
@@ -663,12 +668,14 @@
                     vregpath.append(path + '.py')
         return vregpath
 
-    def __init__(self):
+    def __init__(self, debugmode=False):
         register_stored_procedures()
         ConfigurationMixIn.__init__(self)
+        self.debugmode = debugmode
         self.adjust_sys_path()
         self.load_defaults()
-        self.translations = {}
+        # will be properly initialized later by _gettext_init
+        self.translations = {'en': (unicode, lambda ctx, msgid: unicode(msgid) )}
         self._site_loaded = set()
         # don't register ReStructured Text directives by simple import, avoid pb
         # with eg sphinx.
@@ -684,25 +691,23 @@
         # overriden in CubicWebConfiguration
         self.cls_adjust_sys_path()
 
-    def init_log(self, logthreshold=None, debug=False,
-                 logfile=None, syslog=False):
+    def init_log(self, logthreshold=None, logfile=None, syslog=False):
         """init the log service"""
         if logthreshold is None:
-            if debug:
+            if self.debugmode:
                 logthreshold = 'DEBUG'
             else:
                 logthreshold = self['log-threshold']
-        self.debugmode = debug
         if sys.platform == 'win32':
             # no logrotate on win32, so use logging rotation facilities
             # for now, hard code weekly rotation every sunday, and 52 weeks kept
             # idea: make this configurable?
-            init_log(debug, syslog, logthreshold, logfile, self.log_format,
+            init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format,
                      rotation_parameters={'when': 'W6', # every sunday
                                           'interval': 1,
                                           'backupCount': 52})
         else:
-            init_log(debug, syslog, logthreshold, logfile, self.log_format)
+            init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format)
         # configure simpleTal logger
         logging.getLogger('simpleTAL').setLevel(logging.ERROR)
 
@@ -844,12 +849,12 @@
         return mdir
 
     @classmethod
-    def config_for(cls, appid, config=None):
+    def config_for(cls, appid, config=None, debugmode=False):
         """return a configuration instance for the given instance identifier
         """
         config = config or guess_configuration(cls.instance_home(appid))
         configcls = configuration_cls(config)
-        return configcls(appid)
+        return configcls(appid, debugmode)
 
     @classmethod
     def possible_configurations(cls, appid):
@@ -909,17 +914,21 @@
         """return default path to the pid file of the instance'server"""
         if self.mode == 'system':
             # XXX not under _INSTALL_PREFIX, right?
-            rtdir = env_path('CW_RUNTIME_DIR', '/var/run/cubicweb/', 'run time')
+            default = '/var/run/cubicweb/'
         else:
             import tempfile
-            rtdir = env_path('CW_RUNTIME_DIR', tempfile.gettempdir(), 'run time')
+            default = tempfile.gettempdir()
+        # runtime directory created on startup if necessary, don't check it
+        # exists
+        rtdir = env_path('CW_RUNTIME_DIR', default, 'run time',
+                         checkexists=False)
         return join(rtdir, '%s-%s.pid' % (self.appid, self.name))
 
     # instance methods used to get instance specific resources #############
 
-    def __init__(self, appid):
+    def __init__(self, appid, debugmode=False):
         self.appid = appid
-        CubicWebNoAppConfiguration.__init__(self)
+        CubicWebNoAppConfiguration.__init__(self, debugmode)
         self._cubes = None
         self.load_file_configuration(self.main_config_file())
 
@@ -986,6 +995,29 @@
         """write down current configuration"""
         self.generate_config(open(self.main_config_file(), 'w'))
 
+    def check_writeable_uid_directory(self, path):
+        """check given directory path exists, belongs to the user running the
+        server process and is writeable.
+
+        If not, try to fix this, leting exception propagate when not possible.
+        """
+        if not exists(path):
+            os.makedirs(path)
+        if self['uid']:
+            try:
+                uid = int(self['uid'])
+            except ValueError:
+                from pwd import getpwnam
+                uid = getpwnam(self['uid']).pw_uid
+        else:
+            uid = os.getuid()
+        fstat = os.stat(path)
+        if fstat.st_uid != uid:
+            os.chown(path, uid, os.getgid())
+        import stat
+        if not (fstat.st_mode & stat.S_IWUSR):
+            os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+
     @cached
     def instance_md5_version(self):
         import hashlib
@@ -1000,7 +1032,7 @@
         super(CubicWebConfiguration, self).load_configuration()
         if self.apphome and self.set_language:
             # init gettext
-            self._set_language()
+            self._gettext_init()
 
     def _load_site_cubicweb(self, sitefile):
         # overriden to register cube specific options
@@ -1009,12 +1041,12 @@
             self.register_options(mod.options)
             self.load_defaults()
 
-    def init_log(self, logthreshold=None, debug=False, force=False):
+    def init_log(self, logthreshold=None, force=False):
         """init the log service"""
         if not force and hasattr(self, '_logging_initialized'):
             return
         self._logging_initialized = True
-        CubicWebNoAppConfiguration.init_log(self, logthreshold, debug,
+        CubicWebNoAppConfiguration.init_log(self, logthreshold,
                                             logfile=self.get('log-file'))
         # read a config file if it exists
         logconfig = join(self.apphome, 'logging.conf')
@@ -1035,7 +1067,7 @@
             if lang != 'en':
                 yield lang
 
-    def _set_language(self):
+    def _gettext_init(self):
         """set language for gettext"""
         from gettext import translation
         path = join(self.apphome, 'i18n')
@@ -1115,6 +1147,7 @@
 def register_stored_procedures():
     from logilab.database import FunctionDescr
     from rql.utils import register_function, iter_funcnode_variables
+    from rql.nodes import SortTerm, Constant, VariableRef
 
     global _EXT_REGISTERED
     if _EXT_REGISTERED:
@@ -1160,6 +1193,34 @@
     register_function(TEXT_LIMIT_SIZE)
 
 
+    class FTIRANK(FunctionDescr):
+        """return ranking of a variable that must be used as some has_text
+        relation subject in the query's restriction. Usually used to sort result
+        of full-text search by ranking.
+        """
+        supported_backends = ('postgres',)
+        rtype = 'Float'
+
+        def st_check_backend(self, backend, funcnode):
+            """overriden so that on backend not supporting fti ranking, the
+            function is removed when in an orderby clause, or replaced by a 1.0
+            constant.
+            """
+            if not self.supports(backend):
+                parent = funcnode.parent
+                while parent is not None and not isinstance(parent, SortTerm):
+                    parent = parent.parent
+                if isinstance(parent, SortTerm):
+                    parent.parent.remove(parent)
+                else:
+                    funcnode.parent.replace(funcnode, Constant(1.0, 'Float'))
+                    parent = funcnode
+                for vref in parent.iget_nodes(VariableRef):
+                    vref.unregister_reference()
+
+    register_function(FTIRANK)
+
+
     class FSPATH(FunctionDescr):
         """return path of some bytes attribute stored using the Bytes
         File-System Storage (bfss)
--- a/cwctl.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/cwctl.py	Mon Jul 19 15:37:02 2010 +0200
@@ -17,9 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """the cubicweb-ctl tool, based on logilab.common.clcommands to
 provide a pluggable commands system.
-
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 # *ctl module should limit the number of import to be imported as quickly as
@@ -477,23 +476,23 @@
 
     def start_instance(self, appid):
         """start the instance's server"""
-        debug = self['debug']
-        force = self['force']
-        loglevel = self['loglevel']
-        config = cwcfg.config_for(appid)
-        if loglevel is not None:
-            loglevel = 'LOG_%s' % loglevel.upper()
-            config.global_set_option('log-threshold', loglevel)
-            config.init_log(loglevel, debug=debug, force=True)
+        config = cwcfg.config_for(appid, debugmode=self['debug'])
+        init_cmdline_log_threshold(config, self['loglevel'])
         if self['profile']:
             config.global_set_option('profile', self.config.profile)
         helper = self.config_helper(config, cmdname='start')
         pidf = config['pid-file']
-        if exists(pidf) and not force:
+        if exists(pidf) and not self['force']:
             msg = "%s seems to be running. Remove %s by hand if necessary or use \
 the --force option."
             raise ExecutionError(msg % (appid, pidf))
-        helper.start_server(config, debug)
+        helper.start_server(config)
+
+
+def init_cmdline_log_threshold(config, loglevel):
+    if loglevel is not None:
+        config.global_set_option('log-threshold', loglevel.upper())
+        config.init_log(config['log-threshold'], force=True)
 
 
 class StopInstanceCommand(InstanceCommand):
@@ -788,11 +787,15 @@
     repository internals (session, etc...) so most migration commands won't be
     available.
 
+    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
+    them in '__args__' afterwards.
+
     <instance>
       the identifier of the instance to connect.
     """
     name = 'shell'
-    arguments = '<instance> [batch command file]'
+    arguments = '<instance> [batch command file(s)] [-- <script arguments>]'
     options = (
         ('system-only',
          {'short': 'S', 'action' : 'store_true',
@@ -868,8 +871,11 @@
             mih = config.migration_handler()
         try:
             if args:
-                for arg in args:
-                    mih.cmd_process_script(arg)
+                # use cmdline parser to access left/right attributes only
+                # remember that usage requires instance appid as first argument
+                scripts, args = self.cmdline_parser.largs[1:], self.cmdline_parser.rargs
+                for script in scripts:
+                    mih.cmd_process_script(script, scriptargs=args)
             else:
                 mih.interactive_shell()
         finally:
--- a/cwvreg.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/cwvreg.py	Mon Jul 19 15:37:02 2010 +0200
@@ -82,7 +82,6 @@
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_if_interface_found
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
 
 Examples:
@@ -194,6 +193,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import  deprecated
 from logilab.common.modutils import cleanup_sys_modules
@@ -202,7 +203,7 @@
 
 from cubicweb import (ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid,
                       ObjectNotFound, NoSelectableObject, RegistryNotFound,
-                      CW_EVENT_MANAGER, onevent)
+                      CW_EVENT_MANAGER)
 from cubicweb.utils import dump_class
 from cubicweb.vregistry import VRegistry, Registry, class_regid
 from cubicweb.rtags import RTAGS
@@ -213,23 +214,23 @@
 
 def use_interfaces(obj):
     """return interfaces used by the given object by searching for implements
-    selectors, with a bw compat fallback to accepts_interfaces attribute
+    selectors
     """
     from cubicweb.selectors import implements
-    try:
-        # XXX deprecated
-        return sorted(obj.accepts_interfaces)
-    except AttributeError:
-        try:
-            impl = obj.__select__.search_selector(implements)
-            if impl:
-                return sorted(impl.expected_ifaces)
-        except AttributeError:
-            pass # old-style appobject classes with no accepts_interfaces
-        except:
-            print 'bad selector %s on %s' % (obj.__select__, obj)
-            raise
-        return ()
+    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
+    """
+    from cubicweb.selectors import appobject_selectable
+    impl = obj.__select__.search_selector(appobject_selectable)
+    if impl:
+        return (impl.registry, impl.regids)
+    return None
 
 
 class CWRegistry(Registry):
@@ -444,14 +445,13 @@
     * contentnavigation XXX to merge with components? to kill?
     """
 
-    def __init__(self, config, debug=None, initlog=True):
+    def __init__(self, config, initlog=True):
         if initlog:
             # first init log service
-            config.init_log(debug=debug)
+            config.init_log()
         super(CubicWebVRegistry, self).__init__(config)
         self.schema = None
         self.initialized = False
-        self.reset()
         # XXX give force_reload (or refactor [re]loading...)
         if self.config.mode != 'test':
             # don't clear rtags during test, this may cause breakage with
@@ -478,8 +478,10 @@
         return (value for key, value in self.items())
 
     def reset(self):
+        CW_EVENT_MANAGER.emit('before-registry-reset', self)
         super(CubicWebVRegistry, self).reset()
         self._needs_iface = {}
+        self._needs_appobject = {}
         # two special registries, propertydefs which care all the property
         # definitions, and propertyvals which contains values for those
         # properties
@@ -488,6 +490,7 @@
             self['propertyvalues'] = self.eprop_values = {}
             for key, propdef in self.config.eproperty_definitions():
                 self.register_property(key, **propdef)
+        CW_EVENT_MANAGER.emit('after-registry-reset', self)
 
     def set_schema(self, schema):
         """set instance'schema and load application objects"""
@@ -521,7 +524,6 @@
                 if not cube in cubes:
                     cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
                     cleanup_sys_modules(cpath)
-        self.reset()
         self.register_objects(path)
         CW_EVENT_MANAGER.emit('after-registry-reload')
 
@@ -540,6 +542,7 @@
                 for obj in objects:
                     obj.schema = schema
 
+    @deprecated('[3.9] use .register instead')
     def register_if_interface_found(self, obj, ifaces, **kwargs):
         """register `obj` but remove it if no entity class implements one of
         the given `ifaces` interfaces at the end of the registration process.
@@ -565,7 +568,15 @@
         # XXX bw compat
         ifaces = use_interfaces(obj)
         if ifaces:
+            if not obj.__name__.endswith('Adapter') and \
+                   any(iface for iface in ifaces if not isinstance(iface, basestring)):
+                warn('[3.9] %s: interfaces in implements selector are '
+                     'deprecated in favor of adapters / adaptable '
+                     'selector' % obj.__name__, DeprecationWarning)
             self._needs_iface[obj] = ifaces
+        depends_on = require_appobject(obj)
+        if depends_on is not None:
+            self._needs_appobject[obj] = depends_on
 
     def register_objects(self, path):
         """overriden to give cubicweb's extrapath (eg cubes package's __path__)
@@ -583,13 +594,18 @@
         # we may want to keep interface dependent objects (e.g.for i18n
         # catalog generation)
         if self.config.cleanup_interface_sobjects:
-            # remove appobjects that don't support any available interface
+            # XXX deprecated with cw 3.9: remove appobjects that don't support
+            # any available interface
             implemented_interfaces = set()
             if 'Any' in self.get('etypes', ()):
                 for etype in self.schema.entities():
                     if etype.final:
                         continue
                     cls = self['etypes'].etype_class(etype)
+                    if cls.__implements__:
+                        warn('[3.9] %s: using __implements__/interfaces are '
+                             'deprecated in favor of adapters' % cls.__name__,
+                             DeprecationWarning)
                     for iface in cls.__implements__:
                         implemented_interfaces.update(iface.__mro__)
                     implemented_interfaces.update(cls.__mro__)
@@ -603,15 +619,27 @@
                     self.debug('kicking appobject %s (no implemented '
                                'interface among %s)', obj, ifaces)
                     self.unregister(obj)
-        # clear needs_iface so we don't try to remove some not-anymore-in
-        # objects on automatic reloading
-        self._needs_iface.clear()
+            # since 3.9: remove appobjects which depending on other, unexistant
+            # appobjects
+            for obj, (regname, regids) in self._needs_appobject.items():
+                try:
+                    registry = self[regname]
+                except RegistryNotFound:
+                    self.debug('kicking %s (no registry %s)', obj, regname)
+                    self.unregister(obj)
+                    continue
+                for regid in regids:
+                    if registry.get(regid):
+                        break
+                else:
+                    self.debug('kicking %s (no %s object in registry %s)',
+                               obj, ' or '.join(regids), regname)
+                    self.unregister(obj)
         super(CubicWebVRegistry, 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)
 
-
     # rql parsing utilities ####################################################
 
     @property
--- a/dataimport.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/dataimport.py	Mon Jul 19 15:37:02 2010 +0200
@@ -584,7 +584,7 @@
             kwargs[k] = getattr(v, 'eid', v)
         entity, rels = self.metagen.base_etype_dicts(etype)
         entity = copy(entity)
-        entity._related_cache = {}
+        entity.cw_clear_relation_cache()
         self.metagen.init_entity(entity)
         entity.update(kwargs)
         entity.edited_attributes = set(entity)
--- a/dbapi.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/dbapi.py	Mon Jul 19 15:37:02 2010 +0200
@@ -255,9 +255,6 @@
             self.session = None
             self.cnx = self.user = _NeedAuthAccessMock()
 
-    def base_url(self):
-        return self.vreg.config['base-url']
-
     def from_controller(self):
         return 'view'
 
@@ -582,6 +579,7 @@
         """
         from cubicweb.web.request import CubicWebRequestBase as cwrb
         DBAPIRequest.build_ajax_replace_url = cwrb.build_ajax_replace_url.im_func
+        DBAPIRequest.ajax_replace_url = cwrb.ajax_replace_url.im_func
         DBAPIRequest.list_form_param = cwrb.list_form_param.im_func
         DBAPIRequest.property_value = _fake_property_value
         DBAPIRequest.next_tabindex = count().next
--- a/debian/changelog	Thu Jul 15 12:03:13 2010 +0200
+++ b/debian/changelog	Mon Jul 19 15:37:02 2010 +0200
@@ -1,3 +1,21 @@
+cubicweb (3.9.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 16 Jul 2010 12:40:59 +0200
+
+cubicweb (3.9.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 12 Jul 2010 13:25:10 +0200
+
+cubicweb (3.9.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 07 Jul 2010 13:01:06 +0200
+
 cubicweb (3.8.7-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Thu Jul 15 12:03:13 2010 +0200
+++ b/debian/control	Mon Jul 19 15:37:02 2010 +0200
@@ -10,7 +10,7 @@
 Build-Depends: debhelper (>= 5), python-dev (>=2.5), python-central (>= 0.5)
 Standards-Version: 3.8.0
 Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.5, << 2.6
+XS-Python-Version: >= 2.5, << 2.7
 
 Package: cubicweb
 Architecture: all
@@ -33,7 +33,7 @@
 Conflicts: cubicweb-multisources
 Replaces: cubicweb-multisources
 Provides: cubicweb-multisources
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.0.5), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.1.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
 Recommends: pyro, cubicweb-documentation (= ${source:Version})
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -97,7 +97,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.50.2), python-yams (>= 0.29.0), python-rql (>= 0.26.3), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.50.2), python-yams (>= 0.29.1), python-rql (>= 0.26.3), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/devtools/__init__.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/__init__.py	Mon Jul 19 15:37:02 2010 +0200
@@ -21,6 +21,7 @@
 __docformat__ = "restructuredtext en"
 
 import os
+import sys
 import logging
 from datetime import timedelta
 from os.path import (abspath, join, exists, basename, dirname, normpath, split,
@@ -181,12 +182,9 @@
     def available_languages(self, *args):
         return ('en', 'fr', 'de')
 
-    def ext_resources_file(self):
-        """return instance's external resources file"""
-        return join(self.apphome, 'data', 'external_resources')
-
     def pyro_enabled(self):
-        # but export PYRO_MULTITHREAD=0 or you get problems with sqlite and threads
+        # but export PYRO_MULTITHREAD=0 or you get problems with sqlite and
+        # threads
         return True
 
 
@@ -210,8 +208,6 @@
         init_test_database_sqlite(config)
     elif driver == 'postgres':
         init_test_database_postgres(config)
-    elif driver == 'sqlserver2005':
-        init_test_database_sqlserver2005(config)
     else:
         raise ValueError('no initialization function for driver %r' % driver)
     config._cubes = None # avoid assertion error
@@ -227,10 +223,8 @@
     driver = config.sources()['system']['db-driver']
     if driver == 'sqlite':
         reset_test_database_sqlite(config)
-    elif driver in ('sqlserver2005', 'postgres'):
-        # XXX do something with dump/restore ?
-        print 'resetting the database is not done for', driver
-        print 'you should handle it manually'
+    elif driver == 'postgres':
+        init_test_database_postgres(config)
     else:
         raise ValueError('no reset function for driver %r' % driver)
 
@@ -239,11 +233,46 @@
 
 def init_test_database_postgres(config):
     """initialize a fresh postgresql databse used for testing purpose"""
-    if config.init_repository:
-        from cubicweb.server import init_repository
-        init_repository(config, interactive=False, drop=True)
+    from logilab.database import get_db_helper
+    from cubicweb.server import init_repository
+    from cubicweb.server.serverctl import (createdb, system_source_cnx,
+                                           _db_sys_cnx)
+    source = config.sources()['system']
+    dbname = source['db-name']
+    templdbname = dbname + '_template'
+    helper = get_db_helper('postgres')
+    # connect on the dbms system base to create our base
+    dbcnx = _db_sys_cnx(source, 'CREATE DATABASE and / or USER', verbose=0)
+    cursor = dbcnx.cursor()
+    try:
+        if dbname in helper.list_databases(cursor):
+            cursor.execute('DROP DATABASE %s' % dbname)
+        if not templdbname in helper.list_databases(cursor):
+            source['db-name'] = templdbname
+            createdb(helper, source, dbcnx, cursor)
+            dbcnx.commit()
+            cnx = system_source_cnx(source, special_privs='LANGUAGE C', verbose=0)
+            templcursor = cnx.cursor()
+            # XXX factorize with db-create code
+            helper.init_fti_extensions(templcursor)
+            # install plpythonu/plpgsql language if not installed by the cube
+            langs = sys.platform == 'win32' and ('plpgsql',) or ('plpythonu', 'plpgsql')
+            for extlang in langs:
+                helper.create_language(templcursor, extlang)
+            cnx.commit()
+            templcursor.close()
+            cnx.close()
+            init_repository(config, interactive=False)
+            source['db-name'] = dbname
+    except:
+        dbcnx.rollback()
+        # XXX drop template
+        raise
+    createdb(helper, source, dbcnx, cursor, template=templdbname)
+    dbcnx.commit()
+    dbcnx.close()
 
-### sqlserver2005 test database handling ############################################
+### sqlserver2005 test database handling #######################################
 
 def init_test_database_sqlserver2005(config):
     """initialize a fresh sqlserver databse used for testing purpose"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/cwwindmill.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,70 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""this module contains base classes for windmill integration"""
+
+import os, os.path as osp
+
+# imported by default to simplify further import statements
+from logilab.common.testlib import unittest_main
+
+from windmill.authoring import unit
+from windmill.dep import functest
+
+from cubicweb.devtools.httptest import CubicWebServerTC
+
+
+class UnitTestReporter(functest.reports.FunctestReportInterface):
+    def summary(self, test_list, totals_dict, stdout_capture):
+        self.test_list = test_list
+
+unittestreporter = UnitTestReporter()
+functest.reports.register_reporter(unittestreporter)
+
+class CubicWebWindmillUseCase(CubicWebServerTC, unit.WindmillUnitTestCase):
+    """basic class for Windmill use case tests
+
+    :param browser: browser identification string (firefox|ie|safari|chrome) (firefox by default)
+    :param test_dir: testing file path or directory (./windmill by default)
+    """
+    browser = 'firefox'
+    test_dir = osp.join(os.getcwd(), 'windmill')
+
+    def setUp(self):
+        # reduce log output
+        from logging import getLogger, ERROR
+        getLogger('cubicweb').setLevel(ERROR)
+        getLogger('logilab').setLevel(ERROR)
+        getLogger('windmill').setLevel(ERROR)
+        # Start CubicWeb session before running the server to populate self.vreg
+        CubicWebServerTC.setUp(self)
+        assert os.path.exists(self.test_dir), "provide 'test_dir' as the given test file/dir"
+        unit.WindmillUnitTestCase.setUp(self)
+
+    def tearDown(self):
+        unit.WindmillUnitTestCase.tearDown(self)
+        CubicWebServerTC.tearDown(self)
+
+    def testWindmill(self):
+        self.windmill_shell_objects['start_' + self.browser]()
+        self.windmill_shell_objects['do_test'](self.test_dir, threaded=False)
+        for test in unittestreporter.test_list:
+            self._testMethodDoc = getattr(test, "__doc__", None)
+            self._testMethodName = test.__name__
+            self.assertEquals(test.result, True)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/cwmock.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,11 @@
+/*
+ * cubicweb js mock for unit tests
+ * This module defines variables and functions used in quite a few places
+ * in cw js framework that can't be used or guessed without a real CW server
+ */
+
+var pageid = 'my-page-id';
+
+function _(message) {
+    return message;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/qunit.css	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,119 @@
+
+ol#qunit-tests {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	margin:0;
+	padding:0;
+	list-style-position:inside;
+
+	font-size: smaller;
+}
+ol#qunit-tests li{
+	padding:0.4em 0.5em 0.4em 2.5em;
+	border-bottom:1px solid #fff;
+	font-size:small;
+	list-style-position:inside;
+}
+ol#qunit-tests li ol{
+	box-shadow: inset 0px 2px 13px #999;
+	-moz-box-shadow: inset 0px 2px 13px #999;
+	-webkit-box-shadow: inset 0px 2px 13px #999;
+	margin-top:0.5em;
+	margin-left:0;
+	padding:0.5em;
+	background-color:#fff;
+	border-radius:15px;
+	-moz-border-radius: 15px;
+	-webkit-border-radius: 15px;
+}
+ol#qunit-tests li li{
+	border-bottom:none;
+	margin:0.5em;
+	background-color:#fff;
+	list-style-position: inside;
+	padding:0.4em 0.5em 0.4em 0.5em;
+}
+
+ol#qunit-tests li li.pass{
+	border-left:26px solid #C6E746;
+	background-color:#fff;
+	color:#5E740B;
+	}
+ol#qunit-tests li li.fail{
+	border-left:26px solid #EE5757;
+	background-color:#fff;
+	color:#710909;
+}
+ol#qunit-tests li.pass{
+	background-color:#D2E0E6;
+	color:#528CE0;
+}
+ol#qunit-tests li.fail{
+	background-color:#EE5757;
+	color:#000;
+}
+ol#qunit-tests li strong {
+	cursor:pointer;
+}
+h1#qunit-header{
+	background-color:#0d3349;
+	margin:0;
+	padding:0.5em 0 0.5em 1em;
+	color:#fff;
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	border-top-right-radius:15px;
+	border-top-left-radius:15px;
+	-moz-border-radius-topright:15px;
+	-moz-border-radius-topleft:15px;
+	-webkit-border-top-right-radius:15px;
+	-webkit-border-top-left-radius:15px;
+	text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px;
+}
+h2#qunit-banner{
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	height:5px;
+	margin:0;
+	padding:0;
+}
+h2#qunit-banner.qunit-pass{
+	background-color:#C6E746;
+}
+h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar {
+	background-color:#EE5757;
+}
+#qunit-testrunner-toolbar {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	padding:0;
+	/*width:80%;*/
+	padding:0em 0 0.5em 2em;
+	font-size: small;
+}
+h2#qunit-userAgent {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	background-color:#2b81af;
+	margin:0;
+	padding:0;
+	color:#fff;
+	font-size: small;
+	padding:0.5em 0 0.5em 2.5em;
+	text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+p#qunit-testresult{
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	margin:0;
+	font-size: small;
+	color:#2b81af;
+	border-bottom-right-radius:15px;
+	border-bottom-left-radius:15px;
+	-moz-border-radius-bottomright:15px;
+	-moz-border-radius-bottomleft:15px;
+	-webkit-border-bottom-right-radius:15px;
+	-webkit-border-bottom-left-radius:15px;
+	background-color:#D2E0E6;
+	padding:0.5em 0.5em 0.5em 2.5em;
+}
+strong b.fail{
+	color:#710909;
+	}
+strong b.pass{
+	color:#5E740B;
+	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/qunit.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,1069 @@
+/*
+ * QUnit - A JavaScript Unit Testing Framework
+ * 
+ * http://docs.jquery.com/QUnit
+ *
+ * Copyright (c) 2009 John Resig, Jörn Zaefferer
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ */
+
+(function(window) {
+
+var QUnit = {
+
+	// Initialize the configuration options
+	init: function() {
+		config = {
+			stats: { all: 0, bad: 0 },
+			moduleStats: { all: 0, bad: 0 },
+			started: +new Date,
+			updateRate: 1000,
+			blocking: false,
+			autorun: false,
+			assertions: [],
+			filters: [],
+			queue: []
+		};
+
+		var tests = id("qunit-tests"),
+			banner = id("qunit-banner"),
+			result = id("qunit-testresult");
+
+		if ( tests ) {
+			tests.innerHTML = "";
+		}
+
+		if ( banner ) {
+			banner.className = "";
+		}
+
+		if ( result ) {
+			result.parentNode.removeChild( result );
+		}
+	},
+	
+	// call on start of module test to prepend name to all tests
+	module: function(name, testEnvironment) {
+		config.currentModule = name;
+
+		synchronize(function() {
+			if ( config.currentModule ) {
+				QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+			}
+
+			config.currentModule = name;
+			config.moduleTestEnvironment = testEnvironment;
+			config.moduleStats = { all: 0, bad: 0 };
+
+			QUnit.moduleStart( name, testEnvironment );
+		});
+	},
+
+	asyncTest: function(testName, expected, callback) {
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = 0;
+		}
+
+		QUnit.test(testName, expected, callback, true);
+	},
+	
+	test: function(testName, expected, callback, async) {
+		var name = testName, testEnvironment, testEnvironmentArg;
+
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = null;
+		}
+		// is 2nd argument a testEnvironment?
+		if ( expected && typeof expected === 'object') {
+			testEnvironmentArg =  expected;
+			expected = null;
+		}
+
+		if ( config.currentModule ) {
+			name = config.currentModule + " module: " + name;
+		}
+
+		if ( !validTest(name) ) {
+			return;
+		}
+
+		synchronize(function() {
+			QUnit.testStart( testName );
+
+			testEnvironment = extend({
+				setup: function() {},
+				teardown: function() {}
+			}, config.moduleTestEnvironment);
+			if (testEnvironmentArg) {
+				extend(testEnvironment,testEnvironmentArg);
+			}
+
+			// allow utility functions to access the current test environment
+			QUnit.current_testEnvironment = testEnvironment;
+			
+			config.assertions = [];
+			config.expected = expected;
+
+			try {
+				if ( !config.pollution ) {
+					saveGlobal();
+				}
+
+				testEnvironment.setup.call(testEnvironment);
+			} catch(e) {
+				QUnit.ok( false, "Setup failed on " + name + ": " + e.message );
+			}
+
+			if ( async ) {
+				QUnit.stop();
+			}
+
+			try {
+				callback.call(testEnvironment);
+			} catch(e) {
+				fail("Test " + name + " died, exception and test follows", e, callback);
+				QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message );
+				// else next test will carry the responsibility
+				saveGlobal();
+
+				// Restart the tests if they're blocking
+				if ( config.blocking ) {
+					start();
+				}
+			}
+		});
+
+		synchronize(function() {
+			try {
+				checkPollution();
+				testEnvironment.teardown.call(testEnvironment);
+			} catch(e) {
+				QUnit.ok( false, "Teardown failed on " + name + ": " + e.message );
+			}
+
+			try {
+				QUnit.reset();
+			} catch(e) {
+				fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset);
+			}
+
+			if ( config.expected && config.expected != config.assertions.length ) {
+				QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" );
+			}
+
+			var good = 0, bad = 0,
+				tests = id("qunit-tests");
+
+			config.stats.all += config.assertions.length;
+			config.moduleStats.all += config.assertions.length;
+
+			if ( tests ) {
+				var ol  = document.createElement("ol");
+				ol.style.display = "none";
+
+				for ( var i = 0; i < config.assertions.length; i++ ) {
+					var assertion = config.assertions[i];
+
+					var li = document.createElement("li");
+					li.className = assertion.result ? "pass" : "fail";
+					li.appendChild(document.createTextNode(assertion.message || "(no message)"));
+					ol.appendChild( li );
+
+					if ( assertion.result ) {
+						good++;
+					} else {
+						bad++;
+						config.stats.bad++;
+						config.moduleStats.bad++;
+					}
+				}
+
+				var b = document.createElement("strong");
+				b.innerHTML = name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>";
+				
+				addEvent(b, "click", function() {
+					var next = b.nextSibling, display = next.style.display;
+					next.style.display = display === "none" ? "block" : "none";
+				});
+				
+				addEvent(b, "dblclick", function(e) {
+					var target = e && e.target ? e.target : window.event.srcElement;
+					if ( target.nodeName.toLowerCase() === "strong" ) {
+						var text = "", node = target.firstChild;
+
+						while ( node.nodeType === 3 ) {
+							text += node.nodeValue;
+							node = node.nextSibling;
+						}
+
+						text = text.replace(/(^\s*|\s*$)/g, "");
+
+						if ( window.location ) {
+							window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text);
+						}
+					}
+				});
+
+				var li = document.createElement("li");
+				li.className = bad ? "fail" : "pass";
+				li.appendChild( b );
+				li.appendChild( ol );
+				tests.appendChild( li );
+
+				if ( bad ) {
+					var toolbar = id("qunit-testrunner-toolbar");
+					if ( toolbar ) {
+						toolbar.style.display = "block";
+						id("qunit-filter-pass").disabled = null;
+						id("qunit-filter-missing").disabled = null;
+					}
+				}
+
+			} else {
+				for ( var i = 0; i < config.assertions.length; i++ ) {
+					if ( !config.assertions[i].result ) {
+						bad++;
+						config.stats.bad++;
+						config.moduleStats.bad++;
+					}
+				}
+			}
+
+			QUnit.testDone( testName, bad, config.assertions.length );
+
+			if ( !window.setTimeout && !config.queue.length ) {
+				done();
+			}
+		});
+
+		if ( window.setTimeout && !config.doneTimer ) {
+			config.doneTimer = window.setTimeout(function(){
+				if ( !config.queue.length ) {
+					done();
+				} else {
+					synchronize( done );
+				}
+			}, 13);
+		}
+	},
+	
+	/**
+	 * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
+	 */
+	expect: function(asserts) {
+		config.expected = asserts;
+	},
+
+	/**
+	 * Asserts true.
+	 * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
+	 */
+	ok: function(a, msg) {
+		QUnit.log(a, msg);
+
+		config.assertions.push({
+			result: !!a,
+			message: msg
+		});
+	},
+
+	/**
+	 * Checks that the first two arguments are equal, with an optional message.
+	 * Prints out both actual and expected values.
+	 *
+	 * Prefered to ok( actual == expected, message )
+	 *
+	 * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
+	 *
+	 * @param Object actual
+	 * @param Object expected
+	 * @param String message (optional)
+	 */
+	equal: function(actual, expected, message) {
+		push(expected == actual, actual, expected, message);
+	},
+
+	notEqual: function(actual, expected, message) {
+		push(expected != actual, actual, expected, message);
+	},
+	
+	deepEqual: function(a, b, message) {
+		push(QUnit.equiv(a, b), a, b, message);
+	},
+
+	notDeepEqual: function(a, b, message) {
+		push(!QUnit.equiv(a, b), a, b, message);
+	},
+
+	strictEqual: function(actual, expected, message) {
+		push(expected === actual, actual, expected, message);
+	},
+
+	notStrictEqual: function(actual, expected, message) {
+		push(expected !== actual, actual, expected, message);
+	},
+	
+	start: function() {
+		// A slight delay, to avoid any current callbacks
+		if ( window.setTimeout ) {
+			window.setTimeout(function() {
+				if ( config.timeout ) {
+					clearTimeout(config.timeout);
+				}
+
+				config.blocking = false;
+				process();
+			}, 13);
+		} else {
+			config.blocking = false;
+			process();
+		}
+	},
+	
+	stop: function(timeout) {
+		config.blocking = true;
+
+		if ( timeout && window.setTimeout ) {
+			config.timeout = window.setTimeout(function() {
+				QUnit.ok( false, "Test timed out" );
+				QUnit.start();
+			}, timeout);
+		}
+	},
+	
+	/**
+	 * Resets the test setup. Useful for tests that modify the DOM.
+	 */
+	reset: function() {
+		if ( window.jQuery ) {
+			jQuery("#main").html( config.fixture );
+			jQuery.event.global = {};
+			jQuery.ajaxSettings = extend({}, config.ajaxSettings);
+		}
+	},
+	
+	/**
+	 * Trigger an event on an element.
+	 *
+	 * @example triggerEvent( document.body, "click" );
+	 *
+	 * @param DOMElement elem
+	 * @param String type
+	 */
+	triggerEvent: function( elem, type, event ) {
+		if ( document.createEvent ) {
+			event = document.createEvent("MouseEvents");
+			event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
+				0, 0, 0, 0, 0, false, false, false, false, 0, null);
+			elem.dispatchEvent( event );
+
+		} else if ( elem.fireEvent ) {
+			elem.fireEvent("on"+type);
+		}
+	},
+	
+	// Safe object type checking
+	is: function( type, obj ) {
+		return Object.prototype.toString.call( obj ) === "[object "+ type +"]";
+	},
+	
+	// Logging callbacks
+	done: function(failures, total) {},
+	log: function(result, message) {},
+	testStart: function(name) {},
+	testDone: function(name, failures, total) {},
+	moduleStart: function(name, testEnvironment) {},
+	moduleDone: function(name, failures, total) {}
+};
+
+// Backwards compatibility, deprecated
+QUnit.equals = QUnit.equal;
+QUnit.same = QUnit.deepEqual;
+
+// Maintain internal state
+var config = {
+	// The queue of tests to run
+	queue: [],
+
+	// block until document ready
+	blocking: true
+};
+
+// Load paramaters
+(function() {
+	var location = window.location || { search: "", protocol: "file:" },
+		GETParams = location.search.slice(1).split('&');
+
+	for ( var i = 0; i < GETParams.length; i++ ) {
+		GETParams[i] = decodeURIComponent( GETParams[i] );
+		if ( GETParams[i] === "noglobals" ) {
+			GETParams.splice( i, 1 );
+			i--;
+			config.noglobals = true;
+		} else if ( GETParams[i].search('=') > -1 ) {
+			GETParams.splice( i, 1 );
+			i--;
+		}
+	}
+	
+	// restrict modules/tests by get parameters
+	config.filters = GETParams;
+	
+	// Figure out if we're running the tests from a server or not
+	QUnit.isLocal = !!(location.protocol === 'file:');
+})();
+
+// Expose the API as global variables, unless an 'exports'
+// object exists, in that case we assume we're in CommonJS
+if ( typeof exports === "undefined" || typeof require === "undefined" ) {
+	extend(window, QUnit);
+	window.QUnit = QUnit;
+} else {
+	extend(exports, QUnit);
+	exports.QUnit = QUnit;
+}
+
+if ( typeof document === "undefined" || document.readyState === "complete" ) {
+	config.autorun = true;
+}
+
+addEvent(window, "load", function() {
+	// Initialize the config, saving the execution queue
+	var oldconfig = extend({}, config);
+	QUnit.init();
+	extend(config, oldconfig);
+
+	config.blocking = false;
+
+	var userAgent = id("qunit-userAgent");
+	if ( userAgent ) {
+		userAgent.innerHTML = navigator.userAgent;
+	}
+	
+	var toolbar = id("qunit-testrunner-toolbar");
+	if ( toolbar ) {
+		toolbar.style.display = "none";
+		
+		var filter = document.createElement("input");
+		filter.type = "checkbox";
+		filter.id = "qunit-filter-pass";
+		filter.disabled = true;
+		addEvent( filter, "click", function() {
+			var li = document.getElementsByTagName("li");
+			for ( var i = 0; i < li.length; i++ ) {
+				if ( li[i].className.indexOf("pass") > -1 ) {
+					li[i].style.display = filter.checked ? "none" : "";
+				}
+			}
+		});
+		toolbar.appendChild( filter );
+
+		var label = document.createElement("label");
+		label.setAttribute("for", "qunit-filter-pass");
+		label.innerHTML = "Hide passed tests";
+		toolbar.appendChild( label );
+
+		var missing = document.createElement("input");
+		missing.type = "checkbox";
+		missing.id = "qunit-filter-missing";
+		missing.disabled = true;
+		addEvent( missing, "click", function() {
+			var li = document.getElementsByTagName("li");
+			for ( var i = 0; i < li.length; i++ ) {
+				if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) {
+					li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block";
+				}
+			}
+		});
+		toolbar.appendChild( missing );
+
+		label = document.createElement("label");
+		label.setAttribute("for", "qunit-filter-missing");
+		label.innerHTML = "Hide missing tests (untested code is broken code)";
+		toolbar.appendChild( label );
+	}
+
+	var main = id('main');
+	if ( main ) {
+		config.fixture = main.innerHTML;
+	}
+
+	if ( window.jQuery ) {
+		config.ajaxSettings = window.jQuery.ajaxSettings;
+	}
+
+	QUnit.start();
+});
+
+function done() {
+	if ( config.doneTimer && window.clearTimeout ) {
+		window.clearTimeout( config.doneTimer );
+		config.doneTimer = null;
+	}
+
+	if ( config.queue.length ) {
+		config.doneTimer = window.setTimeout(function(){
+			if ( !config.queue.length ) {
+				done();
+			} else {
+				synchronize( done );
+			}
+		}, 13);
+
+		return;
+	}
+
+	config.autorun = true;
+
+	// Log the last module results
+	if ( config.currentModule ) {
+		QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+	}
+
+	var banner = id("qunit-banner"),
+		tests = id("qunit-tests"),
+		html = ['Tests completed in ',
+		+new Date - config.started, ' milliseconds.<br/>',
+		'<span class="passed">', config.stats.all - config.stats.bad, '</span> tests of <span class="total">', config.stats.all, '</span> passed, <span class="failed">', config.stats.bad,'</span> failed.'].join('');
+
+	if ( banner ) {
+		banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
+	}
+
+	if ( tests ) {	
+		var result = id("qunit-testresult");
+
+		if ( !result ) {
+			result = document.createElement("p");
+			result.id = "qunit-testresult";
+			result.className = "result";
+			tests.parentNode.insertBefore( result, tests.nextSibling );
+		}
+
+		result.innerHTML = html;
+	}
+
+	QUnit.done( config.stats.bad, config.stats.all );
+}
+
+function validTest( name ) {
+	var i = config.filters.length,
+		run = false;
+
+	if ( !i ) {
+		return true;
+	}
+	
+	while ( i-- ) {
+		var filter = config.filters[i],
+			not = filter.charAt(0) == '!';
+
+		if ( not ) {
+			filter = filter.slice(1);
+		}
+
+		if ( name.indexOf(filter) !== -1 ) {
+			return !not;
+		}
+
+		if ( not ) {
+			run = true;
+		}
+	}
+
+	return run;
+}
+
+function push(result, actual, expected, message) {
+	message = message || (result ? "okay" : "failed");
+	QUnit.ok( result, result ? message + ": " + QUnit.jsDump.parse(expected) : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) );
+}
+
+function synchronize( callback ) {
+	config.queue.push( callback );
+
+	if ( config.autorun && !config.blocking ) {
+		process();
+	}
+}
+
+function process() {
+	var start = (new Date()).getTime();
+
+	while ( config.queue.length && !config.blocking ) {
+		if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
+			config.queue.shift()();
+
+		} else {
+			setTimeout( process, 13 );
+			break;
+		}
+	}
+}
+
+function saveGlobal() {
+	config.pollution = [];
+	
+	if ( config.noglobals ) {
+		for ( var key in window ) {
+			config.pollution.push( key );
+		}
+	}
+}
+
+function checkPollution( name ) {
+	var old = config.pollution;
+	saveGlobal();
+	
+	var newGlobals = diff( old, config.pollution );
+	if ( newGlobals.length > 0 ) {
+		ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
+		config.expected++;
+	}
+
+	var deletedGlobals = diff( config.pollution, old );
+	if ( deletedGlobals.length > 0 ) {
+		ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
+		config.expected++;
+	}
+}
+
+// returns a new Array with the elements that are in a but not in b
+function diff( a, b ) {
+	var result = a.slice();
+	for ( var i = 0; i < result.length; i++ ) {
+		for ( var j = 0; j < b.length; j++ ) {
+			if ( result[i] === b[j] ) {
+				result.splice(i, 1);
+				i--;
+				break;
+			}
+		}
+	}
+	return result;
+}
+
+function fail(message, exception, callback) {
+	if ( typeof console !== "undefined" && console.error && console.warn ) {
+		console.error(message);
+		console.error(exception);
+		console.warn(callback.toString());
+
+	} else if ( window.opera && opera.postError ) {
+		opera.postError(message, exception, callback.toString);
+	}
+}
+
+function extend(a, b) {
+	for ( var prop in b ) {
+		a[prop] = b[prop];
+	}
+
+	return a;
+}
+
+function addEvent(elem, type, fn) {
+	if ( elem.addEventListener ) {
+		elem.addEventListener( type, fn, false );
+	} else if ( elem.attachEvent ) {
+		elem.attachEvent( "on" + type, fn );
+	} else {
+		fn();
+	}
+}
+
+function id(name) {
+	return !!(typeof document !== "undefined" && document && document.getElementById) &&
+		document.getElementById( name );
+}
+
+// Test for equality any JavaScript type.
+// Discussions and reference: http://philrathe.com/articles/equiv
+// Test suites: http://philrathe.com/tests/equiv
+// Author: Philippe Rathé <prathe@gmail.com>
+QUnit.equiv = function () {
+
+    var innerEquiv; // the real equiv function
+    var callers = []; // stack to decide between skip/abort functions
+    var parents = []; // stack to avoiding loops from circular referencing
+
+
+    // Determine what is o.
+    function hoozit(o) {
+        if (QUnit.is("String", o)) {
+            return "string";
+            
+        } else if (QUnit.is("Boolean", o)) {
+            return "boolean";
+
+        } else if (QUnit.is("Number", o)) {
+
+            if (isNaN(o)) {
+                return "nan";
+            } else {
+                return "number";
+            }
+
+        } else if (typeof o === "undefined") {
+            return "undefined";
+
+        // consider: typeof null === object
+        } else if (o === null) {
+            return "null";
+
+        // consider: typeof [] === object
+        } else if (QUnit.is( "Array", o)) {
+            return "array";
+        
+        // consider: typeof new Date() === object
+        } else if (QUnit.is( "Date", o)) {
+            return "date";
+
+        // consider: /./ instanceof Object;
+        //           /./ instanceof RegExp;
+        //          typeof /./ === "function"; // => false in IE and Opera,
+        //                                          true in FF and Safari
+        } else if (QUnit.is( "RegExp", o)) {
+            return "regexp";
+
+        } else if (typeof o === "object") {
+            return "object";
+
+        } else if (QUnit.is( "Function", o)) {
+            return "function";
+        } else {
+            return undefined;
+        }
+    }
+
+    // Call the o related callback with the given arguments.
+    function bindCallbacks(o, callbacks, args) {
+        var prop = hoozit(o);
+        if (prop) {
+            if (hoozit(callbacks[prop]) === "function") {
+                return callbacks[prop].apply(callbacks, args);
+            } else {
+                return callbacks[prop]; // or undefined
+            }
+        }
+    }
+    
+    var callbacks = function () {
+
+        // for string, boolean, number and null
+        function useStrictEquality(b, a) {
+            if (b instanceof a.constructor || a instanceof b.constructor) {
+                // to catch short annotaion VS 'new' annotation of a declaration
+                // e.g. var i = 1;
+                //      var j = new Number(1);
+                return a == b;
+            } else {
+                return a === b;
+            }
+        }
+
+        return {
+            "string": useStrictEquality,
+            "boolean": useStrictEquality,
+            "number": useStrictEquality,
+            "null": useStrictEquality,
+            "undefined": useStrictEquality,
+
+            "nan": function (b) {
+                return isNaN(b);
+            },
+
+            "date": function (b, a) {
+                return hoozit(b) === "date" && a.valueOf() === b.valueOf();
+            },
+
+            "regexp": function (b, a) {
+                return hoozit(b) === "regexp" &&
+                    a.source === b.source && // the regex itself
+                    a.global === b.global && // and its modifers (gmi) ...
+                    a.ignoreCase === b.ignoreCase &&
+                    a.multiline === b.multiline;
+            },
+
+            // - skip when the property is a method of an instance (OOP)
+            // - abort otherwise,
+            //   initial === would have catch identical references anyway
+            "function": function () {
+                var caller = callers[callers.length - 1];
+                return caller !== Object &&
+                        typeof caller !== "undefined";
+            },
+
+            "array": function (b, a) {
+                var i, j, loop;
+                var len;
+
+                // b could be an object literal here
+                if ( ! (hoozit(b) === "array")) {
+                    return false;
+                }   
+                
+                len = a.length;
+                if (len !== b.length) { // safe and faster
+                    return false;
+                }
+                
+                //track reference to avoid circular references
+                parents.push(a);
+                for (i = 0; i < len; i++) {
+                    loop = false;
+                    for(j=0;j<parents.length;j++){
+                        if(parents[j] === a[i]){
+                            loop = true;//dont rewalk array
+                        }
+                    }
+                    if (!loop && ! innerEquiv(a[i], b[i])) {
+                        parents.pop();
+                        return false;
+                    }
+                }
+                parents.pop();
+                return true;
+            },
+
+            "object": function (b, a) {
+                var i, j, loop;
+                var eq = true; // unless we can proove it
+                var aProperties = [], bProperties = []; // collection of strings
+
+                // comparing constructors is more strict than using instanceof
+                if ( a.constructor !== b.constructor) {
+                    return false;
+                }
+
+                // stack constructor before traversing properties
+                callers.push(a.constructor);
+                //track reference to avoid circular references
+                parents.push(a);
+                
+                for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
+                    loop = false;
+                    for(j=0;j<parents.length;j++){
+                        if(parents[j] === a[i])
+                            loop = true; //don't go down the same path twice
+                    }
+                    aProperties.push(i); // collect a's properties
+
+                    if (!loop && ! innerEquiv(a[i], b[i])) {
+                        eq = false;
+                        break;
+                    }
+                }
+
+                callers.pop(); // unstack, we are done
+                parents.pop();
+
+                for (i in b) {
+                    bProperties.push(i); // collect b's properties
+                }
+
+                // Ensures identical properties name
+                return eq && innerEquiv(aProperties.sort(), bProperties.sort());
+            }
+        };
+    }();
+
+    innerEquiv = function () { // can take multiple arguments
+        var args = Array.prototype.slice.apply(arguments);
+        if (args.length < 2) {
+            return true; // end transition
+        }
+
+        return (function (a, b) {
+            if (a === b) {
+                return true; // catch the most you can
+            } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) {
+                return false; // don't lose time with error prone cases
+            } else {
+                return bindCallbacks(a, callbacks, [b, a]);
+            }
+
+        // apply transition with (1..n) arguments
+        })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
+    };
+
+    return innerEquiv;
+
+}();
+
+/**
+ * jsDump
+ * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
+ * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
+ * Date: 5/15/2008
+ * @projectDescription Advanced and extensible data dumping for Javascript.
+ * @version 1.0.0
+ * @author Ariel Flesler
+ * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ */
+QUnit.jsDump = (function() {
+	function quote( str ) {
+		return '"' + str.toString().replace(/"/g, '\\"') + '"';
+	};
+	function literal( o ) {
+		return o + '';	
+	};
+	function join( pre, arr, post ) {
+		var s = jsDump.separator(),
+			base = jsDump.indent(),
+			inner = jsDump.indent(1);
+		if ( arr.join )
+			arr = arr.join( ',' + s + inner );
+		if ( !arr )
+			return pre + post;
+		return [ pre, inner + arr, base + post ].join(s);
+	};
+	function array( arr ) {
+		var i = arr.length,	ret = Array(i);					
+		this.up();
+		while ( i-- )
+			ret[i] = this.parse( arr[i] );				
+		this.down();
+		return join( '[', ret, ']' );
+	};
+	
+	var reName = /^function (\w+)/;
+	
+	var jsDump = {
+		parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance
+			var	parser = this.parsers[ type || this.typeOf(obj) ];
+			type = typeof parser;			
+			
+			return type == 'function' ? parser.call( this, obj ) :
+				   type == 'string' ? parser :
+				   this.parsers.error;
+		},
+		typeOf:function( obj ) {
+			var type;
+			if ( obj === null ) {
+				type = "null";
+			} else if (typeof obj === "undefined") {
+				type = "undefined";
+			} else if (QUnit.is("RegExp", obj)) {
+				type = "regexp";
+			} else if (QUnit.is("Date", obj)) {
+				type = "date";
+			} else if (QUnit.is("Function", obj)) {
+				type = "function";
+			} else if (obj.setInterval && obj.document && !obj.nodeType) {
+				type = "window";
+			} else if (obj.nodeType === 9) {
+				type = "document";
+			} else if (obj.nodeType) {
+				type = "node";
+			} else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) {
+				type = "array";
+			} else {
+				type = typeof obj;
+			}
+			return type;
+		},
+		separator:function() {
+			return this.multiline ?	this.HTML ? '<br />' : '\n' : this.HTML ? '&nbsp;' : ' ';
+		},
+		indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
+			if ( !this.multiline )
+				return '';
+			var chr = this.indentChar;
+			if ( this.HTML )
+				chr = chr.replace(/\t/g,'   ').replace(/ /g,'&nbsp;');
+			return Array( this._depth_ + (extra||0) ).join(chr);
+		},
+		up:function( a ) {
+			this._depth_ += a || 1;
+		},
+		down:function( a ) {
+			this._depth_ -= a || 1;
+		},
+		setParser:function( name, parser ) {
+			this.parsers[name] = parser;
+		},
+		// The next 3 are exposed so you can use them
+		quote:quote, 
+		literal:literal,
+		join:join,
+		//
+		_depth_: 1,
+		// This is the list of parsers, to modify them, use jsDump.setParser
+		parsers:{
+			window: '[Window]',
+			document: '[Document]',
+			error:'[ERROR]', //when no parser is found, shouldn't happen
+			unknown: '[Unknown]',
+			'null':'null',
+			undefined:'undefined',
+			'function':function( fn ) {
+				var ret = 'function',
+					name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
+				if ( name )
+					ret += ' ' + name;
+				ret += '(';
+				
+				ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
+				return join( ret, this.parse(fn,'functionCode'), '}' );
+			},
+			array: array,
+			nodelist: array,
+			arguments: array,
+			object:function( map ) {
+				var ret = [ ];
+				this.up();
+				for ( var key in map )
+					ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
+				this.down();
+				return join( '{', ret, '}' );
+			},
+			node:function( node ) {
+				var open = this.HTML ? '&lt;' : '<',
+					close = this.HTML ? '&gt;' : '>';
+					
+				var tag = node.nodeName.toLowerCase(),
+					ret = open + tag;
+					
+				for ( var a in this.DOMAttrs ) {
+					var val = node[this.DOMAttrs[a]];
+					if ( val )
+						ret += ' ' + a + '=' + this.parse( val, 'attribute' );
+				}
+				return ret + close + open + '/' + tag + close;
+			},
+			functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
+				var l = fn.length;
+				if ( !l ) return '';				
+				
+				var args = Array(l);
+				while ( l-- )
+					args[l] = String.fromCharCode(97+l);//97 is 'a'
+				return ' ' + args.join(', ') + ' ';
+			},
+			key:quote, //object calls it internally, the key part of an item in a map
+			functionCode:'[code]', //function calls it internally, it's the content of the function
+			attribute:quote, //node calls it internally, it's an html attribute value
+			string:quote,
+			date:quote,
+			regexp:literal, //regex
+			number:literal,
+			'boolean':literal
+		},
+		DOMAttrs:{//attributes to dump from nodes, name=>realName
+			id:'id',
+			name:'name',
+			'class':'className'
+		},
+		HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
+		indentChar:'   ',//indentation unit
+		multiline:false //if true, items in a collection, are separated by a \n, else just a space.
+	};
+
+	return jsDump;
+})();
+
+})(this);
--- a/devtools/devctl.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/devctl.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,10 +15,10 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""additional cubicweb-ctl commands and command handlers for cubicweb and cubicweb's
-cubes development
+"""additional cubicweb-ctl commands and command handlers for cubicweb and
+cubicweb's cubes development
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 # *ctl module should limit the number of import to be imported as quickly as
@@ -67,7 +67,7 @@
         return None
     def main_config_file(self):
         return None
-    def init_log(self, debug=None):
+    def init_log(self):
         pass
     def load_configuration(self):
         pass
@@ -603,7 +603,7 @@
         exclude = SKEL_EXCLUDE
         if self['layout'] == 'simple':
             exclude += ('sobjects.py*', 'precreate.py*', 'realdb_test*',
-                        'cubes.*', 'external_resources*')
+                        'cubes.*', 'uiprops.py*')
         copy_skeleton(skeldir, cubedir, context, exclude=exclude)
 
     def _ask_for_dependencies(self):
@@ -741,10 +741,21 @@
             p = Popen((viewer, out))
             p.wait()
 
+
+class GenerateQUnitHTML(Command):
+    """Generate a QUnit html file to see test in your browser"""
+    name = "qunit-html"
+    arguments = '<test file> [<dependancy js file>...]'
+
+    def run(self, args):
+        from cubicweb.devtools.qunit import make_qunit_html
+        print make_qunit_html(args[0], args[1:])
+
 register_commands((UpdateCubicWebCatalogCommand,
                    UpdateTemplateCatalogCommand,
                    #LiveServerCommand,
                    NewCubeCommand,
                    ExamineLogCommand,
                    GenerateSchema,
+                   GenerateQUnitHTML,
                    ))
--- a/devtools/fake.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/fake.py	Mon Jul 19 15:37:02 2010 +0200
@@ -16,8 +16,8 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Fake objects to ease testing of cubicweb without a fully working environment
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.database import get_db_helper
@@ -30,6 +30,7 @@
 
 class FakeConfig(dict, BaseApptestConfiguration):
     translations = {}
+    uiprops = {}
     apphome = None
     def __init__(self, appid='data', apphome=None, cubes=()):
         self.appid = appid
@@ -39,12 +40,13 @@
         self['uid'] = None
         self['base-url'] = BASE_URL
         self['rql-cache-size'] = 100
+        self.datadir_url = BASE_URL + 'data/'
 
     def cubes(self, expand=False):
         return self._cubes
 
     def sources(self):
-        return {}
+        return {'system': {'db-driver': 'sqlite'}}
 
 
 class FakeRequest(CubicWebRequestBase):
@@ -66,10 +68,6 @@
     def header_if_modified_since(self):
         return None
 
-    def base_url(self):
-        """return the root url of the instance"""
-        return BASE_URL
-
     def relative_path(self, includeparams=True):
         """return the normalized path of the request (ie at least relative
         to the instance's root, but some other normalization may be needed
--- a/devtools/fill.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/fill.py	Mon Jul 19 15:37:02 2010 +0200
@@ -216,7 +216,7 @@
 
     # XXX nothing to do here
     def generate_Any_data_format(self, entity, index, **kwargs):
-        # data_format attribute of Image/File has no vocabulary constraint, we
+        # data_format attribute of File has no vocabulary constraint, we
         # need this method else stupid values will be set which make mtconverter
         # raise exception
         return u'application/octet-stream'
@@ -227,12 +227,6 @@
         # raise exception
         return u'text/plain'
 
-    def generate_Image_data_format(self, entity, index, **kwargs):
-        # data_format attribute of Image/File has no vocabulary constraint, we
-        # need this method else stupid values will be set which make mtconverter
-        # raise exception
-        return u'image/png'
-
 
 class autoextend(type):
     def __new__(mcs, name, bases, classdict):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/httptest.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,180 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""this module contains base classes and utilities for integration with running
+http server
+"""
+from __future__ import with_statement
+
+__docformat__ = "restructuredtext en"
+
+import threading
+import socket
+import httplib
+
+from twisted.internet import reactor, error
+
+from cubicweb.etwist.server import run
+from cubicweb.devtools.testlib import CubicWebTC
+
+
+def get_available_port(ports_scan):
+    """return the first available port from the given ports range
+
+    Try to connect port by looking for refused connection (111) or transport
+    endpoint already connected (106) errors
+
+    Raise a RuntimeError if no port can be found
+
+    :type ports_range: list
+    :param ports_range: range of ports to test
+    :rtype: int
+    """
+    for port in ports_scan:
+        try:
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            sock = s.connect(("localhost", port))
+        except socket.error, err:
+            if err.args[0] in (111, 106):
+                return port
+        finally:
+            s.close()
+    raise RuntimeError('get_available_port([ports_range]) cannot find an available port')
+
+class CubicWebServerTC(CubicWebTC):
+    """basic class for running test server
+
+    :param ports_range: range of http ports to test (range(7000, 8000) by default)
+    :type ports_range: iterable
+    :param anonymous_logged: is anonymous user logged by default ? (True by default)
+    :type anonymous_logged: bool
+    :param test_url: base url used by server
+    :param test_host: server host
+    :param test_port: server port
+
+    The first port found as available in `ports_range` will be used to launch
+    the test server
+    """
+    ports_range = range(7000, 8000)
+    # anonymous is logged by default in cubicweb test cases
+    anonymous_logged = True
+    test_host='127.0.0.1'
+
+
+
+    @property
+    def test_url(self):
+        return 'http://%s:%d/' % (self.test_host, self.test_port)
+
+    def init_server(self):
+        self.test_port = get_available_port(self.ports_range)
+        self.config['port'] = self.test_port
+        self.config['base-url'] = self.test_url
+        self.config['force-html-content-type'] = True
+        self.config['pyro-server'] = False
+
+    def start_server(self):
+        self.config.pyro_enabled = lambda : False
+        # use a semaphore to avoid starting test while the http server isn't
+        # fully initilialized
+        semaphore = threading.Semaphore(0)
+        def safe_run(*args, **kwargs):
+            try:
+                run(*args, **kwargs)
+            finally:
+                semaphore.release()
+
+        reactor.addSystemEventTrigger('after', 'startup', semaphore.release)
+        t = threading.Thread(target=safe_run, name='cubicweb_test_web_server',
+                             args=(self.config, self.vreg, True))
+        self.web_thread = t
+        if not self.anonymous_logged:
+                self.config.global_set_option('anonymous-user', None)
+        t.start()
+        semaphore.acquire()
+        if not self.web_thread.isAlive():
+            # XXX race condition with actual thread death
+            raise RuntimeError('Could not start the web server')
+        #pre init utils connection
+        self._web_test_cnx = httplib.HTTPConnection(self.test_host, self.test_port)
+        self._ident_cookie = None
+
+    def stop_server(self, timeout=15):
+        """Stop the webserver, waiting for the thread to return"""
+        if self._web_test_cnx is None:
+            self.web_logout()
+            self._web_test_cnx.close()
+        try:
+            reactor.stop()
+            self.web_thread.join(timeout)
+            assert not self.web_thread.isAlive()
+
+        finally:
+            reactor.__init__()
+
+    def web_login(self, user=None, passwd=None):
+        """Log the current http session for the provided credential
+
+        If no user is provided, admin connection are used.
+        """
+        if user is None:
+            user  = self.admlogin
+            passwd = self.admpassword
+        if passwd is None:
+            passwd = user
+        self.login(user)
+        response = self.web_get("?__login=%s&__password=%s" %
+                                (user, passwd))
+        assert response.status == httplib.SEE_OTHER, response.status
+        self._ident_cookie = response.getheader('Set-Cookie')
+        return True
+
+    def web_logout(self, user='admin', pwd=None):
+        """Log out current http user"""
+        if self._ident_cookie is not None:
+            response = self.web_get('logout')
+        self._ident_cookie = None
+
+    def web_get(self, path='', headers=None):
+        """Return an httplib.HTTPResponse object for the specified path
+
+        Use available credential if available.
+        """
+        if headers is None:
+            headers = {}
+        if self._ident_cookie is not None:
+            assert 'Cookie' not in headers
+            headers['Cookie'] = self._ident_cookie
+        self._web_test_cnx.request("GET", '/' + path, headers=headers)
+        response = self._web_test_cnx.getresponse()
+        response.body = response.read() # to chain request
+        response.read = lambda : response.body
+        return response
+
+    def setUp(self):
+        CubicWebTC.setUp(self)
+        self.init_server()
+        self.start_server()
+
+    def tearDown(self):
+        try:
+            self.stop_server()
+        except error.ReactorNotRunning, err:
+            # Server could be launched manually
+            print err
+        CubicWebTC.tearDown(self)
+
--- a/devtools/livetest.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/livetest.py	Mon Jul 19 15:37:02 2010 +0200
@@ -55,7 +55,7 @@
         """Indicate which resource to use to process down the URL's path"""
         if len(segments) and segments[0] == 'data':
             # Anything in data/ is treated as static files
-            datadir = self.config.locate_resource(segments[1])
+            datadir = self.config.locate_resource(segments[1])[0]
             if datadir:
                 return static.File(str(datadir), segments[1:])
         # Otherwise we use this single resource
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/qunit.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,311 @@
+import os, os.path as osp
+import signal
+from tempfile import mkdtemp, NamedTemporaryFile, TemporaryFile
+import tempfile
+from Queue import Queue, Empty
+from subprocess import Popen, check_call, CalledProcessError
+from shutil import rmtree, copy as copyfile
+from uuid import uuid4 
+
+# imported by default to simplify further import statements
+from logilab.common.testlib import unittest_main, with_tempdir, InnerTest
+
+import os
+import cubicweb
+from cubicweb.view import StartupView
+from cubicweb.web.controller import Controller
+from cubicweb.devtools.httptest import CubicWebServerTC
+
+
+class VerboseCalledProcessError(CalledProcessError):
+
+    def __init__(self, returncode, command, stdout, stderr):
+        super(VerboseCalledProcessError, self).__init__(returncode, command)
+        self.stdout = stdout
+        self.stderr = stderr
+
+    def __str__(self):
+        str = [ super(VerboseCalledProcessError, self).__str__()]
+        if self.stdout.strip():
+            str.append('******************')
+            str.append('* process stdout *')
+            str.append('******************')
+            str.append(self.stdout)
+        if self.stderr.strip():
+            str.append('******************')
+            str.append('* process stderr *')
+            str.append('******************')
+            str.append(self.stderr)
+        return '\n'.join(str)
+
+
+
+class FirefoxHelper(object):
+
+    profile_name_mask = 'PYTEST_PROFILE_%(uid)s'
+
+    def __init__(self, url=None):
+        self._process = None
+        self._tmp_dir = mkdtemp()
+        self._profile_data = {'uid': uuid4()}
+        self._profile_name = self.profile_name_mask % self._profile_data
+        fnull = open(os.devnull, 'w')
+        stdout = TemporaryFile()
+        stderr = TemporaryFile()
+        try:
+          check_call(['firefox', '-no-remote', '-CreateProfile',
+                      '%s %s' % (self._profile_name, self._tmp_dir)],
+                                stdout=stdout, stderr=stderr)
+        except CalledProcessError, cpe:
+            stdout.seek(0)
+            stderr.seek(0)
+            raise VerboseCalledProcessError(cpe.returncode, cpe.cmd, stdout.read(), stderr.read())
+
+
+    def start(self, url):
+        self.stop()
+        fnull = open(os.devnull, 'w')
+        self._process = Popen(['firefox', '-no-remote', '-P', self._profile_name, url],
+                              stdout=fnull, stderr=fnull)
+
+    def stop(self):
+        if self._process is not None:
+            assert self._process.returncode is None,  self._process.returncode
+            os.kill(self._process.pid, signal.SIGTERM)
+            self._process.wait()
+            self._process = None
+
+    def __del__(self):
+        self.stop()
+        rmtree(self._tmp_dir)
+
+
+class QUnitTestCase(CubicWebServerTC):
+
+    # testfile, (dep_a, dep_b)
+    all_js_tests = ()
+
+    def setUp(self):
+        super(QUnitTestCase, self).setUp()
+        self.test_queue = Queue()
+        class MyQUnitResultController(QUnitResultController):
+            tc = self
+            test_queue = self.test_queue
+        self._qunit_controller = MyQUnitResultController
+        self.vreg.register(MyQUnitResultController)
+
+    def tearDown(self):
+        super(QUnitTestCase, self).tearDown()
+        self.vreg.unregister(self._qunit_controller)
+
+
+    def abspath(self, path):
+        """use self.__module__ to build absolute path if necessary"""
+        if not osp.isabs(path):
+           dirname = osp.dirname(__import__(self.__module__).__file__)
+           return osp.abspath(osp.join(dirname,path))
+        return path
+
+
+
+    def test_javascripts(self):
+        for args in self.all_js_tests:
+            test_file = self.abspath(args[0])
+            if len(args) > 1:
+                depends   = [self.abspath(dep) for dep in args[1]]
+            else:
+                depends = ()
+            if len(args) > 2:
+                data   = [self.abspath(data) for data in args[2]]
+            else:
+                data = ()
+            for js_test in self._test_qunit(test_file, depends, data):
+                yield js_test
+
+    @with_tempdir
+    def _test_qunit(self, test_file, depends=(), data_files=(), timeout=30):
+        assert osp.exists(test_file), test_file
+        for dep in depends:
+            assert osp.exists(dep), dep
+        for data in data_files:
+            assert osp.exists(data), data
+
+
+        # generate html test file
+        jquery_dir = 'file://' + self.config.locate_resource('jquery.js')[0]
+        html_test_file = NamedTemporaryFile(suffix='.html')
+        html_test_file.write(make_qunit_html(test_file, depends,
+                             server_data=(self.test_host, self.test_port),
+                             web_data_path=jquery_dir))
+        html_test_file.flush()
+        # copying data file
+        for data in data_files:
+            copyfile(data, tempfile.tempdir)
+
+        while not self.test_queue.empty():
+            self.test_queue.get(False)
+
+        browser = FirefoxHelper()
+        browser.start(html_test_file.name)
+        test_count = 0
+        error = False
+        def raise_exception(cls, *data):
+            raise cls(*data)
+        while not error:
+            try:
+                result, test_name, msg = self.test_queue.get(timeout=timeout)
+                test_name = '%s (%s)' % (test_name, test_file)
+                self.set_description(test_name)
+                if result is None:
+                    break
+                test_count += 1
+                if result:
+                    yield InnerTest(test_name, lambda : 1)
+                else:
+                    yield InnerTest(test_name, self.fail, msg)
+            except Empty:
+                error = True
+                yield InnerTest(test_file, raise_exception, RuntimeError, "%s did not report execution end. %i test processed so far." % (test_file, test_count))
+
+        browser.stop()
+        if test_count <= 0 and not error:
+            yield InnerTest(test_name, raise_exception, RuntimeError, 'No test yielded by qunit for %s' % test_file)
+
+class QUnitResultController(Controller):
+
+    __regid__ = 'qunit_result'
+
+
+    # Class variables to circumvent the instantiation of a new Controller for each request.
+    _log_stack = [] # store QUnit log messages
+    _current_module_name = '' # store the current QUnit module name
+
+    def publish(self, rset=None):
+        event = self._cw.form['event']
+        getattr(self, 'handle_%s' % event)()
+
+    def handle_module_start(self):
+        self.__class__._current_module_name = self._cw.form.get('name', '')
+
+    def handle_test_done(self):
+        name = '%s // %s' %  (self._current_module_name, self._cw.form.get('name', ''))
+        failures = int(self._cw.form.get('failures', 0))
+        total = int(self._cw.form.get('total', 0))
+
+        self._log_stack.append('%i/%i assertions failed' % (failures, total))
+        msg = '\n'.join(self._log_stack)
+
+        if failures:
+            self.tc.test_queue.put((False, name, msg))
+        else:
+            self.tc.test_queue.put((True, name, msg))
+        self._log_stack[:] = []
+
+    def handle_done(self):
+        self.tc.test_queue.put((None, None, None))
+
+    def handle_log(self):
+        result = self._cw.form['result']
+        message = self._cw.form['message']
+        self._log_stack.append('%s: %s' % (result, message))
+
+
+
+def cw_path(*paths):
+  return file_path(osp.join(cubicweb.CW_SOFTWARE_ROOT, *paths))
+
+def file_path(path):
+    return 'file://' + osp.abspath(path)
+
+def build_js_script( host, port):
+    return """
+    var host = '%s';
+    var port = '%s';
+
+    QUnit.moduleStart = function (name) {
+      jQuery.ajax({
+                  url: 'http://'+host+':'+port+'/qunit_result',
+                 data: {"event": "module_start",
+                        "name": name},
+                 async: false});
+    }
+
+    QUnit.testDone = function (name, failures, total) {
+      jQuery.ajax({
+                  url: 'http://'+host+':'+port+'/qunit_result',
+                 data: {"event": "test_done",
+                        "name": name,
+                        "failures": failures,
+                        "total":total},
+                 async: false});
+    }
+
+    QUnit.done = function (failures, total) {
+      jQuery.ajax({
+                   url: 'http://'+host+':'+port+'/qunit_result',
+                   data: {"event": "done",
+                          "failures": failures,
+                          "total":total},
+                   async: false});
+      window.close();
+    }
+
+    QUnit.log = function (result, message) {
+      jQuery.ajax({
+                   url: 'http://'+host+':'+port+'/qunit_result',
+                   data: {"event": "log",
+                          "result": result,
+                          "message": message},
+                   async: false});
+    }
+    """ % (host, port)
+
+def make_qunit_html(test_file, depends=(), server_data=None,
+                    web_data_path=cw_path('web', 'data')):
+    """"""
+    data = {
+            'web_data': web_data_path,
+            'web_test': cw_path('devtools', 'data'),
+        }
+
+    html = ['''<html>
+  <head>
+    <!-- JS lib used as testing framework -->
+    <link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
+    <script src="%(web_data)s/jquery.js" type="text/javascript"></script>
+    <script src="%(web_test)s/cwmock.js" type="text/javascript"></script>
+    <script src="%(web_test)s/qunit.js" type="text/javascript"></script>'''
+    % data]
+    if server_data is not None:
+        host, port = server_data
+        html.append('<!-- result report tools -->')
+        html.append('<script type="text/javascript">')
+        html.append(build_js_script(host, port))
+        html.append('</script>')
+    html.append('<!-- Test script dependencies (tested code for example) -->')
+
+    for dep in depends:
+        html.append('    <script src="%s" type="text/javascript"></script>' % file_path(dep))
+
+    html.append('    <!-- Test script itself -->')
+    html.append('    <script src="%s" type="text/javascript"></script>'% (file_path(test_file),))
+    html.append('''  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">QUnit example</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>''')
+    return u'\n'.join(html)
+
+
+
+
+
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/devtools/repotest.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/repotest.py	Mon Jul 19 15:37:02 2010 +0200
@@ -18,8 +18,8 @@
 """some utilities to ease repository testing
 
 This module contains functions to initialize a new repository.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from pprint import pprint
@@ -134,24 +134,32 @@
             schema._eid_index[rdef.eid] = rdef
 
 
-from logilab.common.testlib import TestCase
+from logilab.common.testlib import TestCase, mock_object
+from logilab.database import get_db_helper
+
 from rql import RQLHelper
+
 from cubicweb.devtools.fake import FakeRepo, FakeSession
 from cubicweb.server import set_debug
 from cubicweb.server.querier import QuerierHelper
 from cubicweb.server.session import Session
-from cubicweb.server.sources.rql2sql import remove_unused_solutions
+from cubicweb.server.sources.rql2sql import SQLGenerator, remove_unused_solutions
 
 class RQLGeneratorTC(TestCase):
-    schema = None # set this in concret test
+    schema = backend = None # set this in concret test
 
     def setUp(self):
         self.repo = FakeRepo(self.schema)
+        self.repo.system_source = mock_object(dbdriver=self.backend)
         self.rqlhelper = RQLHelper(self.schema, special_relations={'eid': 'uid',
-                                                                   'has_text': 'fti'})
+                                                                   'has_text': 'fti'},
+                                   backend=self.backend)
         self.qhelper = QuerierHelper(self.repo, self.schema)
         ExecutionPlan._check_permissions = _dummy_check_permissions
         rqlannotation._select_principal = _select_principal
+        if self.backend is not None:
+            dbhelper = get_db_helper(self.backend)
+            self.o = SQLGenerator(self.schema, dbhelper)
 
     def tearDown(self):
         ExecutionPlan._check_permissions = _orig_check_permissions
@@ -270,6 +278,7 @@
         self.system = self.sources[-1]
         do_monkey_patch()
         self._dumb_sessions = [] # by hi-jacked parent setup
+        self.repo.vreg.rqlhelper.backend = 'postgres' # so FTIRANK is considered
 
     def add_source(self, sourcecls, uri):
         self.sources.append(sourcecls(self.repo, self.o.schema,
--- a/devtools/test/data/dbfill.conf	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-[BASE]
-APPLICATION_SCHEMA = /home/adim/cvs_work/soft_prive/ginco/applications/crm/schema
-APPLICATION_HOME = /home/adim/etc/erudi.d/crmadim # ???
-FAKEDB_NAME = crmtest
-ENCODING = UTF-8
-HOST = crater
-USER = adim
-PASSWORD = adim
-
-
-[ENTITIES]
-default = 20 #means default is 20 entities
-Person = 10 # means 10 Persons
-Company = 5# means 5 companies
-
-
-[RELATIONS]
-Person works_for Company = 4
-Division subsidiary_of Company = 3
-
-[DEFAULT_VALUES]
-Person.firstname = data/firstnames.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/dep_1.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,1 @@
+a = 4;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/deps_2.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,1 @@
+b = a +2;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_simple_failure.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,18 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(2, 4);
+  });
+
+  test("test 2", function() {
+      equals('', '45');
+      equals('1024', '32');
+  });
+
+  module("able");
+  test("test 3", function() {
+      same(1, 1);
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_simple_success.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,17 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(2, 2);
+  });
+
+  test("test 2", function() {
+      equals('45', '45');
+  });
+
+  module("able");
+  test("test 3", function() {
+      same(1, 1);
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_with_dep.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,9 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(a, 4);
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_with_ordered_deps.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,9 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(b, 6);
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/utils.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,29 @@
+function datetuple(d) {
+    return [d.getFullYear(), d.getMonth()+1, d.getDate(), 
+	    d.getHours(), d.getMinutes()];
+}
+    
+function pprint(obj) {
+    print('{');
+    for(k in obj) {
+	print('  ' + k + ' = ' + obj[k]);
+    }
+    print('}');
+}
+
+function arrayrepr(array) {
+    return '[' + array.join(', ') + ']';
+}
+    
+function assertArrayEquals(array1, array2) {
+    if (array1.length != array2.length) {
+	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
+    }
+    for (var i=0; i<array1.length; i++) {
+	if (array1[i] != array2[i]) {
+	    
+	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
+						 + ' differs at index ' + i);
+	}
+    }
+}
--- a/devtools/test/data/views.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/test/data/views.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,12 +15,10 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""only for unit tests !
-
-"""
+"""only for unit tests !"""
 
 from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 
 HTML_PAGE = u"""<html>
   <body>
@@ -31,7 +29,7 @@
 
 class SimpleView(EntityView):
     __regid__ = 'simple'
-    __select__ = implements('Bug',)
+    __select__ = is_instance('Bug',)
 
     def call(self, **kwargs):
         self.cell_call(0, 0)
@@ -41,7 +39,7 @@
 
 class RaisingView(EntityView):
     __regid__ = 'raising'
-    __select__ = implements('Bug',)
+    __select__ = is_instance('Bug',)
 
     def cell_call(self, row, col):
         raise ValueError()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/unittest_httptest.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,51 @@
+from logilab.common.testlib import TestCase, unittest_main, tag
+from cubicweb.devtools.httptest import CubicWebServerTC
+
+import httplib
+from os import path as osp
+
+
+class TwistedCWAnonTC(CubicWebServerTC):
+
+    def test_response(self):
+        try:
+            response = self.web_get()
+        except httplib.NotConnected, ex:
+            self.fail("Can't connection to test server: %s" % ex)
+
+    def test_response_anon(self):
+        response = self.web_get()
+        self.assertEquals(response.status, httplib.OK)
+
+
+    def test_base_url(self):
+        if self.test_url not in self.web_get().read():
+            self.fail('no mention of base url in retrieved page')
+
+
+class TwistedCWIdentTC(CubicWebServerTC):
+
+    anonymous_logged = False
+
+    def test_response_denied(self):
+        response = self.web_get()
+        self.assertEquals(response.status, httplib.FORBIDDEN)
+
+    def test_login(self):
+        response = self.web_get()
+        if response.status != httplib.FORBIDDEN:
+             self.skip('Already authenticated')
+        # login
+        self.web_login(self.admlogin, self.admpassword)
+        response = self.web_get()
+        self.assertEquals(response.status, httplib.OK)
+        # logout
+        self.web_logout()
+        response = self.web_get()
+        self.assertEquals(response.status, httplib.FORBIDDEN)
+
+
+
+
+if __name__ == '__main__':
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/unittest_qunit.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,31 @@
+from logilab.common.testlib import unittest_main
+from cubicweb.devtools.qunit import make_qunit_html, QUnitTestCase
+
+from os import path as osp
+
+JSTESTDIR = osp.abspath(osp.join(osp.dirname(__file__), 'data', 'js_examples'))
+
+
+def js(name):
+    return osp.join(JSTESTDIR, name)
+
+class QUnitTestCaseTC(QUnitTestCase):
+
+    all_js_tests = (
+                    (js('test_simple_success.js'),),
+                    (js('test_with_dep.js'), (js('dep_1.js'),)),
+                    (js('test_with_ordered_deps.js'), (js('dep_1.js'), js('deps_2.js'),)),
+                   )
+
+
+    def test_simple_failure(self):
+        js_tests = list(self._test_qunit(js('test_simple_failure.js')))
+        self.assertEquals(len(js_tests), 3)
+        test_1, test_2, test_3 = js_tests
+        self.assertRaises(self.failureException, test_1[0], *test_1[1:])
+        self.assertRaises(self.failureException, test_2[0], *test_2[1:])
+        test_3[0](*test_3[1:])
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/devtools/testlib.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/devtools/testlib.py	Mon Jul 19 15:37:02 2010 +0200
@@ -31,7 +31,7 @@
 
 import yams.schema
 
-from logilab.common.testlib import TestCase, InnerTest
+from logilab.common.testlib import TestCase, InnerTest, Tags
 from logilab.common.pytest import nocoverage, pause_tracing, resume_tracing
 from logilab.common.debugger import Debugger
 from logilab.common.umessage import message_from_string
@@ -163,6 +163,7 @@
     appid = 'data'
     configcls = devtools.ApptestConfiguration
     reset_schema = reset_vreg = False # reset schema / vreg between tests
+    tags= TestCase.tags | Tags('cubicweb', 'cw_repo')
 
     @classproperty
     def config(cls):
@@ -313,7 +314,7 @@
         req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
                     % ','.join(repr(g) for g in groups),
                     {'x': user.eid})
-        user.clear_related_cache('in_group', 'subject')
+        user.cw_clear_relation_cache('in_group', 'subject')
         if commit:
             req.cnx.commit()
         return user
@@ -633,10 +634,10 @@
         view = viewsreg.select(vid, req, **kwargs)
         # set explicit test description
         if rset is not None:
-            self.set_description("testing %s, mod=%s (%s)" % (
+            self.set_description("testing vid=%s defined in %s with (%s)" % (
                 vid, view.__module__, rset.printable_rql()))
         else:
-            self.set_description("testing %s, mod=%s (no rset)" % (
+            self.set_description("testing vid=%s defined in %s without rset" % (
                 vid, view.__module__))
         if template is None: # raw view testing, no template
             viewfunc = view.render
@@ -704,7 +705,7 @@
             validatorclass = self.content_type_validators.get(view.content_type,
                                                               default_validator)
         if validatorclass is None:
-            return None
+            return output.strip()
         validator = validatorclass()
         if isinstance(validator, htmlparser.DTDValidator):
             # XXX remove <canvas> used in progress widget, unknown in html dtd
@@ -786,6 +787,8 @@
     """base class for test with auto-populating of the database"""
     __abstract__ = True
 
+    tags = CubicWebTC.tags | Tags('autopopulated')
+
     pdbclass = CubicWebDebugger
     # this is a hook to be able to define a list of rql queries
     # that are application dependent and cannot be guessed automatically
@@ -911,6 +914,9 @@
 
 class AutomaticWebTest(AutoPopulateTest):
     """import this if you wan automatic tests to be ran"""
+
+    tags = AutoPopulateTest.tags | Tags('web', 'generated')
+
     def setUp(self):
         AutoPopulateTest.setUp(self)
         # access to self.app for proper initialization of the authentication
--- a/doc/book/en/annexes/depends.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/annexes/depends.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -27,6 +27,9 @@
 * logilab-common - http://www.logilab.org/project/logilab-common -
   http://pypi.python.org/pypi/logilab-common/ - included in the forest
 
+* logilab-database - http://www.logilab.org/project/logilab-database -
+  http://pypi.python.org/pypi/logilab-database/ - included in the forest
+
 * logilab-constraint - http://www.logilab.org/project/logilab-constraint -
   http://pypi.python.org/pypi/constraint/ - included in the forest
 
@@ -44,7 +47,7 @@
 
 To use network communication between cubicweb instances / clients:
 
-* Pyro - http://pyro.sourceforge.net/ - http://pypi.python.org/pypi/Pyro
+* Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro
 
 If you're using a Postgres database (recommended):
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/annexes/docstrings-conventions.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,106 @@
+Javascript docstrings
+=====================
+
+Whereas in Python source code we only need to include a module docstrings 
+using the directive `.. automodule:: mypythonmodule`, we will have to 
+explicitely define Javascript modules and functions in the doctrings since
+there is no native directive to include Javascript files.
+
+Rest generation
+---------------
+
+`pyjsrest` is a small utility parsing Javascript doctrings and generating the
+corresponding Restructured file used by Sphinx to generate HTML documentation.
+This script will have the following structure::
+
+  ===========
+  filename.js
+  ===========
+  .. module:: filename.js
+
+We use the `.. module::` directive to register a javascript library
+as a Python module for Sphinx. This provides an entry in the module index.
+
+The contents of the docstring found in the javascript file will be added as is
+following the module declaration. No treatment will be done on the doctring.
+All the documentation structure will be in the docstrings and will comply
+with the following rules.
+
+Docstring structure
+-------------------
+
+Basically we document javascript with RestructuredText docstring
+following the same convention as documenting Python code.
+
+The doctring in Javascript files must be contained in standard 
+Javascript comment signs, starting with `/**` and ending with `*/`,
+such as::
+
+ /**
+  * My comment starts here.
+  * This is the second line prefixed with a `*`.
+  * ...
+  * ...
+  * All the follwing line will be prefixed with a `*` followed by a space.
+  * ...
+  * ...
+  */ 
+
+
+Comments line prefixed by `//` will be ignored. They are reserved for source
+code comments dedicated to developers.
+
+
+Javscript functions docstring
+-----------------------------
+
+By default, the `function` directive describes a module-level function.
+
+`function` directive
+~~~~~~~~~~~~~~~~~~~~
+
+Its purpose is to define the function prototype such as::
+
+    .. function:: loadxhtml(url, data, reqtype, mode) 
+
+If any namespace is used, we should add it in the prototype for now, 
+until we define an appropriate directive.
+::
+    .. function:: jQuery.fn.loadxhtml(url, data, reqtype, mode) 
+
+Function parameters
+~~~~~~~~~~~~~~~~~~~
+
+We will define function parameters as a bulleted list, where the
+parameter name will be backquoted and followed by its description.
+
+Example of a javascript function docstring::
+
+    .. function:: loadxhtml(url, data, reqtype, mode) 
+
+    cubicweb loadxhtml plugin to make jquery handle xhtml response
+
+    fetches `url` and replaces this's content with the result
+
+    Its arguments are:
+
+    * `url`
+
+    * `mode`, how the replacement should be done (default is 'replace')
+       Possible values are :
+           - 'replace' to replace the node's content with the generated HTML
+           - 'swap' to replace the node itself with the generated HTML
+           - 'append' to append the generated HTML to the node's content
+
+
+Optional parameter specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Javascript functions handle arguments not listed in the function signature.
+In the javascript code, they will be flagged using `/* ... */`. In the docstring,
+we flag those optional arguments the same way we would define it in
+Python::
+
+    .. function:: asyncRemoteExec(fname, arg1=None, arg2=None)
+
+
--- a/doc/book/en/annexes/faq.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/annexes/faq.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -115,7 +115,7 @@
 
     from cubicweb import dbapi
 
-    cnx = dbapi.connection(database='instance-id', user='admin', password='admin')
+    cnx = dbapi.connect(database='instance-id', user='admin', password='admin')
     cur = cnx.cursor()
     for name in ('Personal', 'Professional', 'Computers'):
         cur.execute('INSERT Blog B: B name %s', name)
@@ -302,10 +302,10 @@
     import pwd
     import sys
 
-    from logilab.common.db import get_connection
+    from logilab.database import get_connection
 
     def getlogin():
-        """avoid usinng os.getlogin() because of strange tty / stdin problems
+        """avoid using os.getlogin() because of strange tty/stdin problems
         (man 3 getlogin)
         Another solution would be to use $LOGNAME, $USER or $USERNAME
         """
@@ -402,6 +402,20 @@
     mydb=> update cw_cwuser set cw_upassword='qHO8282QN5Utg' where cw_login='joe';
     UPDATE 1
 
+You can prefer use a migration script similar to this shell invocation instead::
+
+    $ cubicweb-ctl shell <instance>
+    >>> from cubicweb.server.utils import crypt_password
+    >>> crypted = crypt_password('joepass')
+    >>> rset = rql('Any U WHERE U is CWUser, U login "joe"')
+    >>> joe = rset.get_entity(0,0)
+    >>> joe.set_attributes(upassword=crypted)
+
+The more experimented people would use RQL request directly::
+
+    >>> rql('SET X upassword %(a)s WHERE X is CWUser, X login "joe"',
+    ...     {'a': crypted})
+
 I've just created a user in a group and it doesn't work !
 ---------------------------------------------------------
 
--- a/doc/book/en/annexes/index.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/annexes/index.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -17,3 +17,5 @@
    rql/index
    mercurial
    depends
+   javascript-api
+   docstrings-conventions
Binary file doc/book/en/annexes/rql/Graph-ex.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/annexes/rql/debugging.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,56 @@
+.. -*- coding: utf-8 -*-
+
+.. _DEBUGGING:
+
+Debugging RQL
+-------------
+
+Available levels
+~~~~~~~~~~~~~~~~
+
+:DBG_NONE:
+    no debug information (current mode)
+
+:DBG_RQL:
+    rql execution information
+
+:DBG_SQL:
+    executed sql
+
+:DBG_REPO:
+    repository events
+
+:DBG_MS:
+    multi-sources
+
+:DBG_MORE:
+    more verbosity
+
+:DBG_ALL:
+    all level enabled
+
+
+Enable verbose output
+~~~~~~~~~~~~~~~~~~~~~
+
+It may be interested to enable a verboser output to debug your RQL statements:
+
+.. sourcecode:: python
+
+    from cubicweb import server
+    server.set_debug(server.DBG_RQL|server.DBG_SQL|server.DBG_ALL)
+
+
+Detect largest RQL queries
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+See `Profiling and performance` chapter (see :ref:`PROFILING`).
+
+
+API
+~~~
+
+.. autofunction:: cubicweb.server.set_debug
+
+.. autoclass:: cubicweb.server.debugged
+
--- a/doc/book/en/annexes/rql/index.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/annexes/rql/index.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -4,8 +4,9 @@
 This chapter describes the Relation Query Language syntax and its implementation in CubicWeb.
 
 .. toctree::
-   :maxdepth: 1
+   :maxdepth: 2
 
    intro
    language
+   debugging
    implementation
--- a/doc/book/en/annexes/rql/intro.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/annexes/rql/intro.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -7,8 +7,13 @@
 Goals of RQL
 ~~~~~~~~~~~~
 
-The goal is to have a language making relations browsing easy. As
-such, attributes will be regarded as cases of special relations (in
+The goal is to have a semantic language in order to:
+
+- query relations in a clear syntax
+- empowers access to data repository manipulation
+- making attributes/relations browsing easy
+
+As such, attributes will be regarded as cases of special relations (in
 terms of usage, the user should see no syntactic difference between an
 attribute and a relation).
 
@@ -40,6 +45,13 @@
 conversion and basic types manipulation, which we may want to look at one time
 or another.  Finally, the syntax is a little esoteric.
 
+Datalog
+```````
+
+Datalog_ is a prolog derived query langage which applies to relational
+databases. It is more expressive than RQL in that it accepts either
+extensional_ and intensional_ predicates (or relations). As of now,
+RQL only deals with intensional relations.
 
 The different types of queries
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -59,7 +71,91 @@
    Remove entities or relations existing in the database.
 
 
+RQL relation expressions
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+RQL expressions apply to a live database defined by a
+:ref:`datamodel_definition`. Apart from the main type, or head, of the
+expression (search, insert, etc.) the most common constituent of an
+RQL expression is a (set of) relation expression(s).
+
+An RQL relation expression contains three components:
+
+* the subject, which is an entity type
+* the predicate, which is a relation definition (an arc of the schema)
+* the object, which is either an attribute or a relation to another entity
+
+.. image:: Graph-ex.gif
+    :alt: <subject> <predicate> <object>
+    :align: center
+
+.. warning::
+
+ A relation is always expressed in the order: ``subject``,
+ ``predicate``, ``object``.
+
+ It is important to determine if the entity type is subject or object
+ to construct a valid expression. Inverting the subject/object is an
+ error since the relation cannot be found in the schema.
+
+ If one does not have access to the code, one can find the order by
+ looking at the schema image in manager views (the subject is located
+ at the beginning of the arrow).
+
+An example of two related relation expressions::
+
+  P works_for C, P name N
+
+RQL variables represent typed entities. The type of entities is
+either automatically inferred (by looking at the possible relation
+definitions, see :ref:`RelationDefinition`) or explicitely constrained
+using the ``is`` meta relation.
+
+In the example above, we barely need to look at the schema. If
+variable names (in the RQL expression) and relation type names (in the
+schema) are expresssively designed, the human reader can infer as much
+as the |cubicweb| querier.
+
+The ``P`` variable is used twice but it always represent the same set
+of entities. Hence ``P works_for C`` and ``P name N`` must be
+compatible in the sense that all the Ps (which *can* refer to
+different entity types) must accept the ``works_for`` and ``name``
+relation types. This does restrict the set of possible values of P.
+
+Adding another relation expression::
+
+  P works_for C, P name N, C name "logilab"
+
+This further restricts the possible values of P through an indirect
+constraint on the possible values of ``C``. The RQL-level unification_
+happening there is translated to one (or several) joins_ at the
+database level.
+
+.. note::
+
+ In |cubicweb|, the term `relation` is often found without ambiguity
+ instead of `predicate`.  This predicate is also known as the
+ `property` of the triple in `RDF concepts`_
 
 
-.. _Versa: http://uche.ogbuji.net/tech/rdf/versa/
+RQL Operators
+~~~~~~~~~~~~~
+
+An RQL expression's head can be completed using various operators such
+as ``ORDERBY``, ``GROUPBY``, ``HAVING``, ``LIMIT`` etc.
+
+RQL relation expressions can be grouped with ``UNION`` or
+``WITH``. Predicate oriented keywords such as ``EXISTS``, ``OR``,
+``NOT`` are available.
+
+The complete zoo of RQL operators is described extensively in the
+following chapter (:ref:`RQL`).
+
+.. _RDF concepts: http://www.w3.org/TR/rdf-concepts/
+.. _Versa: http://wiki.xml3k.org/Versa
 .. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
+.. _unification: http://en.wikipedia.org/wiki/Unification_(computing)
+.. _joins: http://en.wikipedia.org/wiki/Join_(SQL)
+.. _Datalog: http://en.wikipedia.org/wiki/Datalog
+.. _intensional: http://en.wikipedia.org/wiki/Intensional_definition
+.. _extensional: http://en.wikipedia.org/wiki/Extension_(predicate_logic)
--- a/doc/book/en/annexes/rql/language.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/annexes/rql/language.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -15,6 +15,7 @@
   HAVING, ILIKE, IN, INSERT, LIKE, LIMIT, NOT, NOW, NULL, OFFSET,
   OR, ORDERBY, SET, TODAY, TRUE, UNION, WHERE, WITH
 
+
 Variables and Typing
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -29,10 +30,11 @@
 There is a special type **Any**, referring to a non specific type.
 
 We can restrict the possible types for a variable using the
-special relation **is**.
+special relation **is** in the constraints.
+
 The possible type(s) for each variable is derived from the schema
-according to the constraints expressed above and thanks to the relations between
-each variable.
+according to the constraints expressed above and thanks to the relations
+between each variable.
 
 Built-in types
 ``````````````
@@ -63,7 +65,7 @@
   of logical operators (see :ref:`PriorityOperators`).
 
 Mathematical Operators
-```````````````````````
+``````````````````````
 ::
 
      +, -, *, /
@@ -74,7 +76,13 @@
 
      =, <, <=, >=, >, ~=, IN, LIKE, ILIKE
 
-* The operator `=` is the default operator.
+* Syntax to use comparison operator:
+
+    `VARIABLE relation operator VALUE`
+
+* The operator `=` is the default operator and can be omitted.
+
+* `relation` name is always attended
 
 * The operator `LIKE` equivalent to `~=` can be used with the
   special character `%` in a string to indicate that the chain
@@ -89,7 +97,7 @@
 * The operator `IN` provides a list of possible values:
   ::
 
-    Any X WHERE X name IN ( 'chauvat', 'fayolle', 'di mascio', 'thenault')
+    Any X WHERE X name IN ('chauvat', 'fayolle', 'di mascio', 'thenault')
 
 
 .. XXX nico: "A trick <> 'bar'" wouldn't it be more convenient than "NOT A trick 'bar'" ?
@@ -100,16 +108,11 @@
 ``````````````````
 
 1. '*', '/'
-
 2. '+', '-'
-
-3. 'not'
-
-4 'and'
-
-5 'or'
-
-6 ','
+3. 'NOT'
+4. 'AND'
+5. 'OR'
+6. ','
 
 
 Search Query
@@ -141,16 +144,39 @@
 ``````````````````
 
 - For grouped queries (e.g. with a GROUPBY clause), all
-  selected variables should be grouped.
+  selected variables should be grouped at the right of the keyword.
 
-- To group and/or sort by attributes, we can do: "X,L user U, U
-  login L GROUPBY L, X ORDERBY L"
+- To group and/or sort by attributes, we can do::
+
+  X,L user U, U login L GROUPBY L, X ORDERBY L
 
 - If the sorting method (SORT_METHOD) is not specified, then the sorting is
-  ascendant.
+  ascendant (`ASC`).
 
 - Aggregate Functions: COUNT, MIN, MAX, AVG, SUM
 
+Having
+``````
+
+The HAVING clause, as in SQL, has been originally introduced to restrict a query according to value returned by an aggregate function, e.g.::
+
+    Any X GROUPBY X WHERE X relation Y HAVING COUNT(Y) > 10
+
+It may however be used for something else...
+
+In the WHERE clause, we are limited to 3-expression_, such thing can't be expressed directly as in the SQL's way. But this can be expressed using HAVING comparison expression.
+
+For instance, let's say you want to get people whose uppercased first name equals to another person uppercased first name::
+
+    Person X WHERE X firstname XFN, Y firstname YFN HAVING X > Y, UPPER(XFN) = UPPER(YFN)
+
+This open some new possibilities. Another example::
+
+    Person X WHERE X birthday XB HAVING YEAR(XB) = 2000
+
+That lets you use transformation functions not only in selection but for restriction as well and to by-pass limitation of the WHERE clause, which was the major flaw in the RQL language.
+
+Notice that while we would like this to work without the HAVING clause, this can't be currently be done because it introduces an ambiguity in RQL's grammar that can't be handled by Yapps_, the parser's generator we're using.
 
 Negation
 ````````
@@ -170,9 +196,8 @@
 
    Any A WHERE A comments B, A identity B
 
-return all objects that comment themselves. The relation
-`identity` is especially useful when defining the rules for securities
-with `RQLExpressions`.
+return all objects that comment themselves. The relation `identity` is
+especially useful when defining the rules for securities with `RQLExpressions`.
 
 
 Limit / offset
@@ -181,13 +206,6 @@
 
     Any P ORDERBY N LIMIT 5 OFFSET 10 WHERE P is Person, P firstname N
 
-Function calls
-``````````````
-::
-
-    Any UPPER(N) WHERE P firstname N
-
-Functions on string: UPPER, LOWER
 
 Exists
 ``````
@@ -199,8 +217,14 @@
           OR EXISTS(T tags X, T name "priority")
 
 
-Optional relations (Left outer join)
-````````````````````````````````````
+Optional relations
+``````````````````
+
+It is a similar concept that the `Left outer join`_:
+
+    the result of a left outer join (or simply left join) for table A and B
+    always contains all records of the "left" table (A), even if the
+    join-condition does not find any matching record in the "right" table (B).
 
 * They allow you to select entities related or not to another.
 
@@ -218,12 +242,6 @@
     Any T,P,V WHERE T is Ticket, T concerns P, T done_in V?
 
 
-Having
-``````
-::
-
-    Any X GROUPBY X WHERE X knows Y HAVING COUNT(Y) > 10
-
 Subqueries
 ``````````
 ::
@@ -234,16 +252,29 @@
      DISTINCT Any W, REF
         WITH W, REF BEING
             (
-	      (Any W, REF WHERE W is Workcase, W ref REF,
+              (Any W, REF WHERE W is Workcase, W ref REF,
                                  W concerned_by D, D name "Logilab")
                UNION
               (Any W, REF WHERE W is Workcase, W ref REF, '
                                 W split_into WP, WP name "WP1")
             )
 
+Function calls
+``````````````
+::
+
+    Any UPPER(N) WHERE P firstname N
+    Any LOWER(N) WHERE P firstname N
+
+Functions available on string: `UPPER`, `LOWER`
+
+.. XXX retrieve available function automatically
+
+For a performance issue, you can enrich the RQL dialect by RDMS (Relational database management system) functions.
+
 
 Examples
-````````
+~~~~~~~~
 
 - *Search for the object of identifier 53*
   ::
@@ -280,11 +311,11 @@
         P is Person, (P interested_by T, T name 'training') OR
         (P city 'Paris')
 
-- *The name and surname of all people*
+- *The surname and firstname of all people*
   ::
 
         Any N, P WHERE
-        X is Person, X name N, X first_name P
+        X is Person, X name N, X firstname P
 
   Note that the selection of several entities generally force
   the use of "Any" because the type specification applies otherwise
@@ -304,7 +335,7 @@
 
 
 Insertion query
-~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~
 
     `INSERT` <entity type> V1 (, <entity type> V2) \ * `:` <assignments>
     [ `WHERE` <restriction>]
@@ -336,6 +367,7 @@
 
 Update and relation creation queries
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
     `SET` <assignements>
     [ `WHERE` <restriction>]
 
@@ -345,7 +377,7 @@
 - *Renaming of the person named 'foo' to 'bar' with the first name changed*
   ::
 
-        SET X name 'bar', X first_name 'original' WHERE X is Person, X name 'foo'
+        SET X name 'bar', X firstname 'original' WHERE X is Person, X name 'foo'
 
 - *Insert a relation of type 'know' between objects linked by
   the relation of type 'friend'*
@@ -356,6 +388,7 @@
 
 Deletion query
 ~~~~~~~~~~~~~~
+
     `DELETE` (<entity type> V) | (V1 relation v2 ),...
     [ `WHERE` <restriction>]
 
@@ -372,6 +405,7 @@
 
         DELETE X friend Y WHERE X is Person, X name 'foo'
 
+
 Virtual RQL relations
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -381,6 +415,13 @@
 * `has_text`: relation to use to query the full text index (only for
   entities having fulltextindexed attributes).
 
-* `identity`: relation to use to tell that a RQL variable should be
+* `identity`: `Identity`_ relation to use to tell that a RQL variable should be
   the same as another (but you've to use two different rql variables
   for querying purpose)
+
+* `is`: relation to enforce possible types for a variable
+
+
+
+.. _Yapps: http://theory.stanford.edu/~amitp/yapps/
+.. _Left outer join: http://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join
--- a/doc/book/en/devrepo/datamodel/definition.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -1,5 +1,7 @@
  .. -*- coding: utf-8 -*-
 
+.. _datamodel_definition:
+
 Yams *schema*
 -------------
 
@@ -11,6 +13,8 @@
 
 .. _`Yams`: http://www.logilab.org/project/yams
 
+.. _datamodel_overview:
+
 Overview
 ~~~~~~~~
 
@@ -408,7 +412,7 @@
 * special relations "has_<ACTION>_permission" can not be used
 
 
-
+.. _yams_example:
 
 Defining your schema using yams
 -------------------------------
@@ -494,15 +498,15 @@
 means that you need two separate entities that implement the `ITree` interface and
 get the result from `.children()` which ever entity is concerned.
 
-Inheritance
-```````````
-XXX feed me
+.. Inheritance
+.. ```````````
+.. XXX feed me
 
 
 Definition of relations
 ~~~~~~~~~~~~~~~~~~~~~~~
 
-XXX add note about defining relation type / definition
+.. XXX add note about defining relation type / definition
 
 A relation is defined by a Python class heriting `RelationType`. The name
 of the class corresponds to the name of the type. The class then contains
@@ -546,7 +550,7 @@
 :Historical note:
 
    It has been historically possible to use `ObjectRelation` which
-   defines a relation in the opposite direction. This feature is soon to be
+   defines a relation in the opposite direction. This feature is
    deprecated and therefore should not be used in newly written code.
 
 :Future deprecation note:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devrepo/entityclasses/adapters.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,170 @@
+.. _adapters:
+
+Interfaces and Adapters
+-----------------------
+
+Interfaces are the same thing as object-oriented programming
+`interfaces`_. Adapter refers to a well-known `adapter`_ design
+pattern that helps separating concerns in object oriented
+applications.
+
+.. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
+.. _`adapter`: http://en.wikipedia.org/wiki/Adapter_pattern
+
+In |cubicweb| adapters provide logical functionalities
+to entity types. They are introduced in version `3.9`. Before that one
+had to implements Interfaces in entity classes to achieve a similar goal. However,
+hte problem with this approch is that is clutters the entity class's namespace, exposing
+name collision risks with schema attributes/relations or even methods names
+(different interfaces may define the same method with not necessarily the same
+behaviour expected).
+
+Definition of an adapter is quite trivial. An excerpt from cubicweb
+itself (found in :mod:`cubicweb.entities.adapters`):
+
+.. sourcecode:: python
+
+
+    class ITreeAdapter(EntityAdapter):
+        """This adapter has to be overriden to be configured using the
+        tree_relation, child_role and parent_role class attributes to
+        benefit from this default implementation
+        """
+        __regid__ = 'ITree'
+
+        child_role = 'subject'
+        parent_role = 'object'
+
+        def children_rql(self):
+            """returns RQL to get children """
+            return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
+
+The adapter object has ``self.entity`` attribute which represents the
+entity being adapted.
+
+Specializing and binding an adapter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
+
+  from cubicweb.entities.adapters import ITreeAdapter
+
+  class MyEntityITreeAdapter(ITreeAdapter):
+      __select__ = is_instance('MyEntity')
+      tree_relation = 'filed_under'
+
+The ITreeAdapter here provides a default implementation. The
+tree_relation class attribute is actually used by this implementation
+to help implement correct behaviour.
+
+Here we provide a specific implementation which will be bound for
+``MyEntity`` entity type (the `adaptee`).
+
+
+Selecting on an adapter
+~~~~~~~~~~~~~~~~~~~~~~~
+
+There is an ``adaptable`` selector which can be used instead of
+``implements``.
+
+.. _interfaces_to_adapters:
+
+Converting code from Interfaces/Mixins to Adapters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here we go with a small example. Before:
+
+.. sourcecode:: python
+
+    from cubicweb.selectors import implements
+    from cubicweb.interfaces import ITree
+    from cubicweb.mixins import ITreeMixIn
+
+    class MyEntity(ITreeMixIn, AnyEntity):
+        __implements__ = AnyEntity.__implements__ + (ITree,)
+
+
+    class ITreeView(EntityView):
+        __select__ = implements('ITree')
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            children = entity.children()
+
+After:
+
+.. sourcecode:: python
+
+    from cubicweb.selectors import adaptable, implements
+    from cubicweb.entities.adapters import ITreeAdapter
+
+    class MyEntityITreeAdapter(ITreeAdapter):
+        __select__ = implements('MyEntity')
+
+    class ITreeView(EntityView):
+        __select__ = adaptable('ITree')
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            itree = entity.cw_adapt_to('ITree')
+            children = itree.children()
+
+As we can see, the interface/mixin duality disappears and the entity
+class itself is completely freed from these concerns. When you want
+to use the ITree interface of an entity, call its `cw_adapt_to` method
+to get an adapter for this interface, then access to members of the
+interface on the adapter
+
+Let's look at an example where we defined everything ourselves. We
+start from:
+
+.. sourcecode:: python
+
+    class IFoo(Interface):
+        def bar(self, *args):
+            raise NotImplementedError
+
+    class MyEntity(AnyEntity):
+        __regid__ = 'MyEntity'
+	__implements__ = AnyEntity.__implements__ + (IFoo,)
+
+        def bar(self, *args):
+            return sum(captain.age for captain in self.captains)
+
+    class FooView(EntityView):
+       __regid__ = 'mycube.fooview'
+       __select__ = implements('IFoo')
+
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            self.w('bar: %s' % entity.bar())
+
+Converting to:
+
+.. sourcecode:: python
+
+   class IFooAdapter(EntityAdapter):
+       __regid__ = 'IFoo'
+       __select__ = is_instance('MyEntity')
+
+       def bar(self, *args):
+           return sum(captain.age for captain in self.entity.captains)
+
+   class FooView(EntityView):
+      __regid__ = 'mycube.fooview'
+      __select__ = adaptable('IFoo')
+
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            self.w('bar: %s' % entity.cw_adapt_to('IFoo').bar())
+
+.. note::
+
+   When migrating an entity method to an adapter, the code can be moved as is
+   except for the `self` of the entity class, which in the adapter must become `self.entity`.
+
+Adapters defined in the library
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: cubicweb.entities.adapters
+   :members:
+
+More are defined in web/views.
--- a/doc/book/en/devrepo/entityclasses/application-logic.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devrepo/entityclasses/application-logic.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -1,5 +1,5 @@
-How to use entities objects
----------------------------
+How to use entities objects and adapters
+----------------------------------------
 
 The previous chapters detailed the classes and methods available to
 the developper at the so-called `ORM`_ level. However they say little
@@ -7,9 +7,9 @@
 
 .. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
 
-Entities objects are used in the repository and web sides of
-CubicWeb. On the repository side of things, one should manipulate them
-in Hooks and Operations.
+Entities objects (and their adapters) are used in the repository and
+web sides of CubicWeb. On the repository side of things, one should
+manipulate them in Hooks and Operations.
 
 Hooks and Operations provide support for the implementation of rules
 such as computed attributes, coherency invariants, etc (they play the
@@ -32,21 +32,22 @@
 wire. There is no way state can be shared between these processes
 (there is a specific API for that). Hence, it is not possible to use
 entity objects as messengers between these components of an
-application. It means that an attribute set as in `obj.x = 42`,
+application. It means that an attribute set as in ``obj.x = 42``,
 whether or not x is actually an entity schema attribute, has a short
 life span, limited to the hook, operation or view within which the
 object was built.
 
 Setting an attribute or relation value can be done in the context of a
-Hook/Operation, using the obj.set_attributes(x=42) notation or a plain
+Hook/Operation, using the obj.set_relations(x=42) notation or a plain
 RQL SET expression.
 
 In views, it would be preferable to encapsulate the necessary logic in
-a method of the concerned entity class(es). But of course, this advice
-is also reasonnable for Hooks/Operations, though the separation of
-concerns here is less stringent than in the case of views.
+a method of an adapter for the concerned entity class(es). But of
+course, this advice is also reasonnable for Hooks/Operations, though
+the separation of concerns here is less stringent than in the case of
+views.
 
-This leads to the practical role of entity objects: it's where an
+This leads to the practical role of objects adapters: it's where an
 important part of the application logic lie (the other part being
 located in the Hook/Operations).
 
@@ -58,26 +59,31 @@
 
 .. sourcecode:: python
 
-    class Project(TreeMixIn, AnyEntity):
+    from cubicweb.entities.adapters import ITreeAdapter
+
+    class ProjectAdapter(ITreeAdapter):
+        __select__ = implements('Project')
+        tree_relation = 'subproject_of'
+
+    class Project(AnyEntity):
         __regid__ = 'Project'
-        __implements__ = AnyEntity.__implements__ + (ITree,)
         fetch_attrs, fetch_order = fetch_config(('name', 'description',
                                                  'description_format', 'summary'))
 
         TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
 
-        tree_attribute = 'subproject_of'
-        parent_target = 'subject'
-        children_target = 'object'
-
         def dc_title(self):
             return self.name
 
-First we see that it uses an ITree interface and the TreeMixIn default
-implementation. The attributes `tree_attribute`, `parent_target` and
-`children_target` are used by the TreeMixIn code. This is typically
-used in views concerned with the representation of tree-like
-structures (CubicWeb provides several such views).
+The fact that the `Project` entity type implements an ``ITree``
+interface is materialized by the ``ProjectAdapter`` class (inheriting
+the pre-defined ``ITreeAdapter`` whose __regid__ is of course
+``ITree``), which will be selected on `Project` entity types because
+of its selector. On this adapter, we redefine the ``tree_relation``
+attribute of the ITreeAdapter class.
+
+This is typically used in views concerned with the representation of
+tree-like structures (CubicWeb provides several such views).
 
 It is important that the views themselves try not to implement this
 logic, not only because such views would be hardly applyable to other
@@ -89,7 +95,17 @@
 about the transitive closure of the child relation). This is a further
 argument to implement it at entity class level.
 
-The `dc_title` method provides a (unicode string) value likely to be
+The fetch_attrs, fetch_order class attributes are parameters of the
+`ORM`_ layer. They tell which attributes should be loaded at once on
+entity object instantiation (by default, only the eid is known, other
+attributes are loaded on demand), and which attribute is to be used to
+order the .related() and .unrelated() methods output.
+
+We can observe the big TICKET_DEFAULT_STATE_RESTR is a pure
+application domain piece of data. There is, of course, no limitation
+to the amount of class attributes of this kind.
+
+The ``dc_title`` method provides a (unicode string) value likely to be
 consummed by views, but note that here we do not care about output
 encodings. We care about providing data in the most universal format
 possible, because the data could be used by a web view (which would be
@@ -97,17 +113,14 @@
 oriented output (which would have the necessary context about the
 needed byte stream encoding).
 
-The fetch_attrs, fetch_order class attributes are parameters of the
-`ORM`_ layer. They tell which attributes should be loaded at once on
-entity object instantiation (by default, only the eid is known, other
-attributes are loaded on demand), and which attribute is to be used to
-order the .related() and .unrelated() methods output.
+.. note::
 
-Finally, we can observe the big TICKET_DEFAULT_STATE_RESTR is a pure
-application domain piece of data. There is, of course, no limitation
-to the amount of class attributes of this kind.
+  The dublin code `dc_xxx` methods are not moved to an adapter as they
+  are extremely prevalent in cubicweb and assorted cubes and should be
+  available for all entity types.
 
-Let us now dig into more substantial pieces of code.
+Let us now dig into more substantial pieces of code, continuing the
+Project class.
 
 .. sourcecode:: python
 
@@ -151,7 +164,7 @@
 * it is NOT concerned with database coherency (this is the realm of
   Hooks/Operations); in other words, it assumes a coherent world
 
-* it is NOT concerned with end-user interfaces
+* it is NOT (directly) concerned with end-user interfaces
 
 * however it can be used in both contexts
 
--- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devrepo/entityclasses/data-as-objects.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -4,23 +4,22 @@
 Python-level access to persistent data is provided by the
 :class:`Entity <cubicweb.entity>` class.
 
-An entity class is bound to a schema entity type.  Descriptors are added when
+.. XXX this part is not clear. refactor it.
+
+An entity class is bound to a schema entity type. Descriptors are added when
 classes are registered in order to initialize the class according to its schema:
 
-* we can access the defined attributes in the schema thanks to the attributes of
-  the same name on instances (typed value)
+* the attributes defined in the schema appear as attributes of these classes
 
-* we can access the defined relations in the schema thanks to the relations of
-  the same name on instances (entities instances list)
-
+* the relations defined in the schema appear as attributes of these classes,
+  but are lists of instances
 
 `Formatting and output generation`:
 
 * `view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
   (and returns an unicode string)
 
-* `absolute_url(*args, **kwargs)`, returns an absolute URL to access the primary view
-  of an entity
+* `absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
 
 * `rest_path()`, returns a relative REST URL to get the entity
 
@@ -31,7 +30,7 @@
 `Data handling`:
 
 * `as_rset()`, converts the entity into an equivalent result set simulating the
-   request `Any X WHERE X eid _eid_`
+  request `Any X WHERE X eid _eid_`
 
 * `complete(skip_bytes=True)`, executes a request that recovers at
   once all the missing attributes of an entity
@@ -52,10 +51,10 @@
   values given named parameters
 
 * `set_relations(**kwargs)`, add relations to the given object. To
-   set a relation where this entity is the object of the relation,
-   use `reverse_<relation>` as argument name.  Values may be an
-   entity, a list of entities, or None (meaning that all relations of
-   the given type from or to this object should be deleted).
+  set a relation where this entity is the object of the relation,
+  use `reverse_<relation>` as argument name.  Values may be an
+  entity, a list of entities, or None (meaning that all relations of
+  the given type from or to this object should be deleted).
 
 * `copy_relations(ceid)`, copies the relations of the entities having the eid
   given in the parameters on the current entity
@@ -66,7 +65,7 @@
 The :class:`AnyEntity` class
 ----------------------------
 
-To provide a specific behavior for each entity, we have to define a class
+To provide a specific behavior for each entity, we can define a class
 inheriting from `cubicweb.entities.AnyEntity`. In general, we define this class
 in `mycube.entities` module (or in a submodule if we want to split code among
 multiple files) so that it will be available on both server and client side.
@@ -111,7 +110,7 @@
 `Misc methods`:
 
 * `after_deletion_path`, return (path, parameters) which should be
-   used as redirect information when this entity is being deleted
+  used as redirect information when this entity is being deleted
 
 * `pre_web_edit`, callback called by the web editcontroller when an
   entity will be created/modified, to let a chance to do some entity
@@ -139,5 +138,18 @@
 one in OTHER_CUBE. These types are stored in the `etype` section of
 the `vregistry`.
 
-Notice this is different than yams schema inheritance.
+Notice this is different than yams schema inheritance, which is an
+experimental undocumented feature.
+
+
+Application logic
+-----------------
 
+While a lot of custom behaviour and application logic can be
+implemented using entity classes, the programmer must be aware that
+adding new attributes and method on an entity class adds may shadow
+schema-level attribute or relation definitions.
+
+To keep entities clean (mostly data structures plus a few universal
+methods such as listed above), one should use `adapters` (see
+:ref:`adapters`).
--- a/doc/book/en/devrepo/entityclasses/index.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devrepo/entityclasses/index.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -9,5 +9,5 @@
 
    data-as-objects
    load-sort
-   interfaces
+   adapters
    application-logic
--- a/doc/book/en/devrepo/entityclasses/interfaces.rst	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-Interfaces
-----------
-
-This is the same thing as object-oriented programming `interfaces`_.
-
-.. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
-
-Definition of an interface is quite trivial. An example from cubicweb
-itself (found in cubicweb/interfaces.py):
-
-.. sourcecode:: python
-
-    class ITree(Interface):
-
-        def parent(self):
-            """returns the parent entity"""
-
-        def children(self):
-            """returns the item's children"""
-
-        def children_rql(self):
-            """returns RQL to get children"""
-
-        def iterchildren(self):
-            """iterates over the item's children"""
-
-        def is_leaf(self):
-            """returns true if this node as no child"""
-
-        def is_root(self):
-            """returns true if this node has no parent"""
-
-        def root(self):
-            """returns the root object"""
-
-
-Declaration of interfaces implemented by a class
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. sourcecode:: python
-
-  from cubicweb.interfaces import ITree
-  from cubicweb.mixins import TreeMixIn
-
-  class MyEntity(TreeMixIn, AnyEntity):
-      __regid__ = 'MyEntity'
-      __implements__ = AnyEntity.__implements__ + ('ITree',)
-
-      tree_attribute = 'filed_under'
-
-The TreeMixIn here provides a default implementation for the
-interface. The tree_attribute class attribute is actually used by this
-implementation to help implement correct behaviour.
-
-Interfaces (and some implementations as mixins) defined in the library
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.interfaces
-   :members:
-
-.. automodule:: cubicweb.mixins
-   :members:
-
-
-
--- a/doc/book/en/devrepo/profiling.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devrepo/profiling.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -1,3 +1,5 @@
+.. _PROFILING:
+
 Profiling and performance
 =========================
 
--- a/doc/book/en/devrepo/vreg.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devrepo/vreg.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -37,6 +37,7 @@
 .. autoclass:: cubicweb.appobject.yes
 .. autoclass:: cubicweb.selectors.match_kwargs
 .. autoclass:: cubicweb.selectors.appobject_selectable
+.. autoclass:: cubicweb.selectors.adaptable
 
 
 Result set selectors
@@ -66,7 +67,7 @@
 match or not according to entity's (instance or class) properties.
 
 .. autoclass:: cubicweb.selectors.non_final_entity
-.. autoclass:: cubicweb.selectors.implements
+.. autoclass:: cubicweb.selectors.is_instance
 .. autoclass:: cubicweb.selectors.score_entity
 .. autoclass:: cubicweb.selectors.rql_condition
 .. autoclass:: cubicweb.selectors.relation_possible
@@ -75,6 +76,8 @@
 .. 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.implements
 
 
 Logged user selectors
--- a/doc/book/en/devweb/edition/form.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devweb/edition/form.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -1,3 +1,5 @@
+.. _webform:
+
 HTML form construction
 ----------------------
 
--- a/doc/book/en/devweb/js.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devweb/js.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -350,3 +350,47 @@
 There is also javascript support for massmailing, gmap (google maps),
 fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over
 AppEngine), flot (charts drawing), tabs and bookmarks.
+
+API
+~~~
+
+.. 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
+inherit from ``QUnitTestCase`` and register your javascript test file in the
+``all_js_tests`` lclass attribut. This  ``all_js_tests`` is a sequence a
+3-tuple (<test_file, [<dependencies> ,] [<data_files>]):
+
+The <test_file> should contains the qunit test. <dependencies> defines the list
+of javascript file that must be imported before the test script.  Dependencies
+are included their definition order. <data_files> are additional files copied in the
+test directory. both <dependencies> and <data_files> are optionnal.
+``jquery.js`` is preincluded in for all test.
+
+.. sourcecode:: python
+
+    from cubicweb.qunit import QUnitTestCase
+
+    class MyQUnitTest(QUnitTestCase):
+
+        all_js_tests = (
+            ("relative/path/to/my_simple_testcase.js",)
+            ("relative/path/to/my_qunit_testcase.js",(
+                "rel/path/to/dependency_1.js",
+                "rel/path/to/dependency_2.js",)),
+            ("relative/path/to/my_complexe_qunit_testcase.js",(
+                 "rel/path/to/dependency_1.js",
+                 "rel/path/to/dependency_2.js",
+               ),(
+                 "rel/path/file_dependency.html",
+                 "path/file_dependency.json")
+                ),
+            )
--- a/doc/book/en/devweb/views/breadcrumbs.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devweb/views/breadcrumbs.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -8,11 +8,11 @@
 ~~~~~~~
 
 Breadcrumbs are displayed by default in the header section (see
-:ref:`the_main_template_sections`).  With the default main
-template, the header section is composed by the logo, the application
-name, breadcrumbs and, at the most right, the login box. Breadcrumbs
-are displayed just next to the application name, thus breadcrumbs
-begin with a separator.
+:ref:`the_main_template_sections`).  With the default main template,
+the header section is composed by the logo, the application name,
+breadcrumbs and, at the most right, the login box. Breadcrumbs are
+displayed just next to the application name, thus they begin with a
+separator.
 
 Here is the header section of the CubicWeb's forge:
 
@@ -22,29 +22,31 @@
 :mod:`cubicweb.web.views.ibreadcrumbs`:
 
 - `BreadCrumbEntityVComponent`: displayed for a result set with one line
-  if the entity implements the ``IBreadCrumbs`` interface.
+  if the entity is adaptable to ``IBreadCrumbsAdapter``.
 - `BreadCrumbETypeVComponent`: displayed for a result set with more than
-  one line, but with all entities of the same type which implement the
-  ``IBreadCrumbs`` interface.
+  one line, but with all entities of the same type which can adapt to
+  ``IBreadCrumbsAdapter``.
 - `BreadCrumbAnyRSetVComponent`: displayed for any other result set.
 
 Building breadcrumbs
 ~~~~~~~~~~~~~~~~~~~~
 
-The ``IBreadCrumbs`` interface is defined in the
-:mod:`cubicweb.interfaces` module. It specifies that an entity which
-implements this interface must have a ``breadcrumbs`` method.
+The ``IBreadCrumbsAdapter`` adapter is defined in the
+:mod:`cubicweb.web.views.ibreadcrumbs` module. It specifies that an
+entity which implements this interface must have a ``breadcrumbs`` and
+a ``parent_entity`` method. A default implementation for each is
+provided. This implementation expoits the ITreeAdapter.
 
 .. note::
 
    Redefining the breadcrumbs is the hammer way to do it. Another way
-   is to define the `parent` method on an entity (as defined in the
-   `ITree` interface). If available, it will be used to compute
-   breadcrumbs.
+   is to define an `ITreeAdapter` adapter on an entity type. If
+   available, it will be used to compute breadcrumbs.
 
-Here is the API of the ``breadcrumbs`` method:
+Here is the API of the ``IBreadCrumbsAdapter`` class:
 
-.. automethod:: cubicweb.interfaces.IBreadCrumbs.breadcrumbs
+.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbs.parent_entity
+.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbs.breadcrumbs
 
 If the breadcrumbs method return a list of entities, the
 ``cubicweb.web.views.ibreadcrumbs.BreadCrumbView`` is used to display
--- a/doc/book/en/devweb/views/index.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/devweb/views/index.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -12,6 +12,7 @@
    views
    basetemplates
    primary
+   reledit
    baseviews
    startup
    boxes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/views/reledit.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,113 @@
+.. _reledit:
+
+The "Click and Edit" (also `reledit`) View
+------------------------------------------
+
+The principal way to update data through the Web UI is through the
+`modify` action on entities, which brings a full form. This is
+described in the :ref:`webform` chapter.
+
+There is however another way to perform piecewise edition of entities
+and relations, using a specific `reledit` (for *relation edition*)
+view from the :mod:`cubicweb.web.views.reledit` module.
+
+This is typically applied from the default Primary View (see
+:ref:`primary_view`) on the attributes and relation section. It makes
+small editions more convenient.
+
+Of course, this can be used customely in any other view. Here come
+some explanation about its capabilities and instructions on the way to
+use it.
+
+Using `reledit`
+***************
+
+Let's start again with a simple example:
+
+.. sourcecode:: python
+
+   class Company(EntityType):
+        name = String(required=True, unique=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+        status = SubjectRelation('File', cardinality='?*', composite='subject')
+
+In some view code we might want to show these attributes/relations and
+allow the user to edit each of them in turn without having to leave
+the current page. We would write code as below:
+
+.. sourcecode:: python
+
+   company.view('reledit', rtype='name', default_value='<name>') # editable name attribute
+   company.view('reledit', rtype='boss') # editable boss relation
+   company.view('reledit', rtype='status') # editable attribute-like relation
+
+If one wanted to edit the company from a boss's point of view, one
+would have to indicate the proper relation's role. By default the role
+is `subject`.
+
+.. sourcecode:: python
+
+   person.view('reledit', rtype='boss', role='object')
+
+Each of these will provide with a different editing widget. The `name`
+attribute will obviously get a text input field. The `boss` relation
+will be edited through a selection box, allowing to pick another
+`Person` as boss. The `status` relation, given that it defines Company
+as a composite entity with one file inside, will provide additional actions
+
+* to `add` a `File` when there is one
+* to `delete` the `File` (if the cardinality allows it)
+
+Moreover, editing the relation or using the `add` action leads to an
+embedded edition/creation form allowing edition of the target entity
+(which is `File` in our example) instead of merely allowing to choose
+amongst existing files.
+
+The `reledit_ctrl` rtag
+***********************
+
+The behaviour of reledited attributes/relations can be finely
+controlled using the reledit_ctrl rtag, defined in
+:mod:`cubicweb.web.uicfg`.
+
+This rtag provides three control variables:
+
+* ``default_value``
+* ``reload``, to specificy if edition of the relation entails a full page
+  reload, which defaults to False
+* ``noedit``, to explicitly inhibit edition
+
+Let's see how to use these controls.
+
+.. sourcecode:: python
+
+    from logilab.mtconverter import xml_escape
+    from cubicweb.web.uicfg import reledit_ctrl
+    reledit_ctrl.tag_attribute(('Company', 'name'),
+                               {'reload': lambda x:x.eid,
+                                'default_value': xml_escape(u'<logilab tastes better>')})
+    reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'noedit': True})
+
+The `default_value` needs to be an xml escaped unicode string.
+
+The `noedit` attribute is convenient to programmatically disable some
+relation edition on views that apply it systematically (the prime
+example being the primary view). Here we use it to forbid changing the
+`boss` relation from a `Person` side (as it could have unwanted
+effects).
+
+Finally, the `reload` key accepts either a boolean, an eid or an
+unicode string representing an url. If an eid is provided, it will be
+internally transformed into an url. The eid/url case helps when one
+needs to reload and the current url is inappropriate. A common case is
+edition of a key attribute, which is part of the current url. If one
+user changed the Company's name from `lozilab` to `logilab`, reloading
+on http://myapp/company/lozilab would fail. Providing the entity's
+eid, then, forces to reload on something like http://myapp/company/42,
+which always work.
+
+
+
+
+
+
--- a/doc/book/en/makefile	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/makefile	Mon Jul 19 15:37:02 2010 +0200
@@ -11,6 +11,10 @@
 PAPER         =
 #BUILDDIR      = build
 BUILDDIR      = ~/tmp/cwdoc
+CWDIR         = ../../..
+JSDIR         = ${CWDIR}/web/data
+JSTORST       = ${CWDIR}/doc/tools/pyjsrest.py
+BUILDJS       = devweb/js_api
 
 # Internal variables for sphinx
 PAPEROPT_a4     = -D latex_paper_size=a4
@@ -18,6 +22,7 @@
 ALLSPHINXOPTS   = -d ${BUILDDIR}/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
 
 
+
 .PHONY: help clean html web pickle htmlhelp latex changes linkcheck
 
 help:
@@ -36,6 +41,7 @@
 	rm -rf apidoc/
 	rm -f *.html
 	-rm -rf ${BUILDDIR}/*
+	-rm -rf ${BUILDJS}
 
 all: ${TARGET} apidoc html
 
@@ -48,12 +54,16 @@
 	epydoc --html -o apidoc -n "cubicweb" --exclude=setup --exclude=__pkginfo__ ../../../
 
 # run sphinx ###
-html:
+html: js
 	mkdir -p ${BUILDDIR}/html ${BUILDDIR}/doctrees
 	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) ${BUILDDIR}/html
 	@echo
 	@echo "Build finished. The HTML pages are in ${BUILDDIR}/html."
 
+js:
+	mkdir -p ${BUILDJS}
+	$(JSTORST) -p ${JSDIR} -o ${BUILDJS}
+
 pickle:
 	mkdir -p ${BUILDDIR}/pickle ${BUILDDIR}/doctrees
 	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) ${BUILDDIR}/pickle
--- a/doc/book/en/tutorials/base/maintemplate.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/tutorials/base/maintemplate.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -123,8 +123,8 @@
 
 .. image:: ../../images/lax-book_06-simple-main-template_en.png
 
-XXX
-[WRITE ME]
+.. XXX
+.. [WRITE ME]
 
 * customize MainTemplate and show that everything in the user
   interface can be changed
--- a/doc/book/en/tutorials/index.rst	Thu Jul 15 12:03:13 2010 +0200
+++ b/doc/book/en/tutorials/index.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -17,3 +17,4 @@
 
    base/index
    advanced/index
+   tools/windmill.rst
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/tutorials/tools/windmill.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,152 @@
+==========================
+Use Windmill with CubicWeb
+==========================
+
+Windmill_ implements cross browser testing, in-browser recording and playback,
+and functionality for fast accurate debugging and test environment integration.
+
+.. _Windmill: http://www.getwindmill.com/
+
+`Online features list <http://www.getwindmill.com/features>`_ is available.
+
+
+Installation
+============
+
+Windmill
+--------
+
+You have to install Windmill manually for now. If you're using Debian, there is
+no binary package (`yet <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=579109>`_).
+
+The simplest solution is to use a *setuptools/pip* command (for a clean
+environment, take a look to the `virtualenv
+<http://pypi.python.org/pypi/virtualenv>`_ project as well)::
+
+    pip install windmill
+    curl -O http://github.com/windmill/windmill/tarball/master
+
+However, the Windmill project doesn't release frequently. Our recommandation is
+to used the last snapshot of the Git repository:
+
+.. sourcecode:: shell
+
+    git clone git://github.com/windmill/windmill.git HEAD
+    cd windmill
+    python setup.py develop
+
+Install instructions are `available <http://wiki.github.com/windmill/windmill/installing>`_.
+
+Be sure to have the windmill module in your PYTHONPATH afterwards::
+
+    python -c "import windmill"
+
+X dummy
+-------
+
+In order to reduce unecessary system load from your test machines, It's
+recommended to use X dummy server for testing the Unix web clients, you need a
+dummy video X driver (as xserver-xorg-video-dummy package in Debian) coupled
+with a light X server as `Xvfb <http://en.wikipedia.org/wiki/Xvfb>`_.
+
+    The dummy driver is a special driver available with the XFree86 DDX. To use
+    the dummy driver, simply substitue it for your normal card driver in the
+    Device section of your xorg.conf configuration file. For example, if you
+    normally uses an ati driver, then you will have a Device section with
+    Driver "ati" to let the X server know that you want it to load and use the
+    ati driver; however, for these conformance tests, you would change that
+    line to Driver "dummy" and remove any other ati specific options from the
+    Device section.
+
+    *From: http://www.x.org/wiki/XorgTesting*
+
+Then, you can run the X server with the following command :
+
+    /usr/bin/X11/Xvfb :1 -ac -screen 0 1280x1024x8 -fbdir /tmp
+
+
+Windmill usage
+==============
+
+Record your use case
+--------------------
+
+- start your instance manually
+- start Windmill_ with url site as last argument (read Usage_ or use *'-h'*
+  option to find required command line arguments)
+- use the record button
+- click on save to obtain python code of your use case
+- copy the content to a new file in a *windmill* directory
+
+.. _Usage: http://wiki.github.com/windmill/windmill/running-tests
+
+If you are using firefox as client, consider the "firebug" option.
+
+You can refine the test by the *loadtest* windmill option:
+
+    windmill -m firebug loadtest=<test_file.py> <instance url>
+
+But use the internal windmill shell to explore available commands:
+
+    windmill -m firebug shell <instance url>
+
+.. sourcecode:: python
+
+    >>> load_test(<your test file>)
+    >>> run_test(<your test file>)
+
+
+
+Integrate Windmill tests into CubicWeb
+======================================
+
+Run your tests
+--------------
+
+You can easily run your windmill test suite through `pytest` or :mod:`unittest`.
+You have to copy a *test_windmill.py* file from :mod:`web.test`.
+
+By default, CubicWeb will use **firefox** as the default browser and will try
+to run test instance server on localhost. In the general case, You've no need
+to change anything.
+
+Check the :class:`cubicweb.devtools.cwwindmill.CubicWebServerTC` class for server
+parameters and :class:`cubicweb.devtools.cwwindmill.CubicWebWindmillUseCase` for
+Windmill configuration.
+
+Best practises
+--------------
+
+Don't run another instance on the same port. You risk to silence some
+regressions (test runner will automatically fail in further versions).
+
+Start your use case by using an assert on the expected primary url page.
+Otherwise all your tests could fail without clear explanation of the used
+navigation.
+
+In the same location of the *test_windmill.py*, create a *windmill/* with your
+windmill recorded use cases.
+
+Then, you can launch the test series with::
+
+    % pytest test/test_windmill.py
+
+For instance, you can start CubicWeb framework use tests by::
+
+    % pytest web/test/test_windmill.py
+
+
+Preferences
+===========
+
+A *.windmill/prefs.py* could be used to redefine default configuration values.
+
+.. define CubicWeb preferences in the parent test case instead with a dedicated firefox profile
+
+For managing browser extensions, read `advanced topic chapter
+<http://wiki.github.com/windmill/windmill/advanced-topics>`_.
+
+More configuration examples could be seen in *windmill/conf/global_settings.py*
+as template.
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/refactoring-the-css-with-uiprops.rst	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,73 @@
+=========================================
+Refactoring the CSSs with UI properties
+=========================================
+
+Overview
+=========
+
+Managing styles progressively became difficult in CubicWeb. The
+introduction of uiprops is an attempt to fix this problem.
+
+The goal is to make it possible to use variables in our CSSs.
+
+These variables are defined or computed in the uiprops.py python file
+and inserted in the CSS using the Python string interpolation syntax.
+
+A quick example, put in ``uiprops.py``::
+
+  defaultBgColor = '#eee'
+
+and in your css::
+
+  body { background-color: %(defaultBgColor)s; }
+
+
+The good practices are:
+
+- define a variable in uiprops to avoid repetitions in the CSS
+  (colors, borders, fonts, etc.)
+
+- define a variable in uiprops when you need to compute values
+  (compute a color palette, etc.)
+
+The algorithm implemented in CubicWeb is the following:
+
+- read uiprops file while walk up the chain of cube dependencies: if
+  cube myblog depends on cube comment, the variables defined in myblog
+  will have precedence over the ones in comment
+
+- replace the %(varname)s in all the CSSs of all the cubes
+
+Keep in mind that the browser will then interpret the CSSs and apply
+the standard cascading mechanism.
+
+FAQ
+====
+
+- How do I keep the old style?
+
+  Put ``STYLESHEET = [data('cubicweb.old.css')]`` in your uiprops.py
+  file and think about something else.
+
+- What are the changes in cubicweb.css?
+
+  Version 3.9.0 of cubicweb changed the following in the default html
+  markup and css:
+
+  ===============  ==================================
+   old              new
+  ===============  ==================================
+   .navcol          #navColumnLeft, #navColumnRight
+   #contentcol      #contentColumn
+   .footer          #footer
+   .logo	    #logo
+   .simpleMessage   .loginMessage
+   .appMsg	    (styles are removed from css)
+   .searchMessage   (styles are removed from css)
+  ===============  ==================================
+
+  Introduction of the new cubicweb.reset.css based on Eric Meyer's
+  reset css.
+
+  Lots of margin, padding, etc.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tools/pyjsrest.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+"""
+Parser for Javascript comments.
+"""
+from __future__ import with_statement
+
+import sys, os, getopt, re
+
+def clean_comment(match):
+    comment = match.group()
+    comment = strip_stars(comment)
+    return comment
+
+# Rest utilities
+def rest_title(title, level, level_markups=['=', '=', '-', '~', '+', '`']):
+    size = len(title)
+    if level == 0:
+        return '\n'.join((level_markups[level] * size, title, level_markups[0] * size)) + '\n'
+    return '\n'.join(('\n' + title, level_markups[level] * size)) + '\n'
+
+def get_doc_comments(text):
+    """
+    Return a list of all documentation comments in the file text.  Each
+    comment is a pair, with the first element being the comment text and
+    the second element being the line after it, which may be needed to
+    guess function & arguments.
+
+    >>> get_doc_comments(read_file('examples/module.js'))[0][0][:40]
+    '/**\n * This is the module documentation.'
+    >>> get_doc_comments(read_file('examples/module.js'))[1][0][7:50]
+    'This is documentation for the first method.'
+    >>> get_doc_comments(read_file('examples/module.js'))[1][1]
+    'function the_first_function(arg1, arg2) '
+    >>> get_doc_comments(read_file('examples/module.js'))[2][0]
+    '/** This is the documentation for the second function. */'
+
+    """
+    return [clean_comment(match) for match in re.finditer('/\*\*.*?\*/',
+            text, re.DOTALL|re.MULTILINE)]
+
+RE_STARS = re.compile('^\s*?\* ?', re.MULTILINE)
+
+
+def strip_stars(doc_comment):
+    """
+    Strip leading stars from a doc comment.
+
+    >>> strip_stars('/** This is a comment. */')
+    'This is a comment.'
+    >>> strip_stars('/**\n * This is a\n * multiline comment. */')
+    'This is a\n multiline comment.'
+    >>> strip_stars('/** \n\t * This is a\n\t * multiline comment. \n*/')
+    'This is a\n multiline comment.'
+
+    """
+    return RE_STARS.sub('', doc_comment[3:-2]).strip()
+
+def parse_js_files(args=sys.argv):
+    """
+    Main command-line invocation.
+    """
+    try:
+        opts, args = getopt.gnu_getopt(args[1:], 'p:o:h', [
+            'jspath=', 'output=', 'help'])
+        opts = dict(opts)
+    except getopt.GetoptError:
+        usage()
+        sys.exit(2)
+
+    rst_dir = opts.get('--output') or opts.get('-o')
+    if rst_dir is None and len(args) != 1:
+        rst_dir = 'apidocs'
+    js_dir = opts.get('--jspath') or opts.get('-p')
+    if not os.path.exists(os.path.join(rst_dir)):
+        os.makedirs(os.path.join(rst_dir))
+
+    f_index = open(os.path.join(rst_dir, 'index.rst'), 'wb')
+    f_index.write('''
+.. toctree::
+    :maxdepth: 1
+
+'''
+)
+    for js_path, js_dirs, js_files in os.walk(js_dir):
+        rst_path = re.sub('%s%s*' % (js_dir, os.path.sep), '', js_path)
+        for js_file in js_files:
+            if not js_file.endswith('.js'):
+                continue
+            if not os.path.exists(os.path.join(rst_dir, rst_path)):
+                os.makedirs(os.path.join(rst_dir, rst_path))
+            rst_content =  extract_rest(js_path, js_file)
+            filename = os.path.join(rst_path, js_file[:-3])
+            # add to index
+            f_index.write('    %s\n' % filename)
+            # save rst file
+            with open(os.path.join(rst_dir, filename) + '.rst', 'wb') as f_rst:
+                f_rst.write(rst_content)
+    f_index.close()
+
+def extract_rest(js_dir, js_file):
+    js_filepath = os.path.join(js_dir, js_file)
+    filecontent = open(js_filepath, 'U').read()
+    comments = get_doc_comments(filecontent)
+    rst = rest_title(js_file, 0)
+    rst += '.. module:: %s\n\n' % js_file
+    rst += '\n\n'.join(comments)
+    return rst
+
+if __name__ == '__main__':
+    parse_js_files()
--- a/entities/__init__.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entities/__init__.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""base application's entities class implementation: `AnyEntity`
+"""base application's entities class implementation: `AnyEntity`"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -28,33 +27,13 @@
 from cubicweb import Unauthorized, typed_eid
 from cubicweb.entity import Entity
 
-from cubicweb.interfaces import IBreadCrumbs, IFeed
-
 
 class AnyEntity(Entity):
     """an entity instance has e_schema automagically set on the class and
     instances have access to their issuing cursor
     """
     __regid__ = 'Any'
-    __implements__ = (IBreadCrumbs, IFeed)
-
-    fetch_attrs = ('modification_date',)
-    @classmethod
-    def fetch_order(cls, attr, var):
-        """class method used to control sort order when multiple entities of
-        this type are fetched
-        """
-        return cls.fetch_unrelated_order(attr, var)
-
-    @classmethod
-    def fetch_unrelated_order(cls, attr, var):
-        """class method used to control sort order when multiple entities of
-        this type are fetched to use in edition (eg propose them to create a
-        new relation on an edited entity).
-        """
-        if attr == 'modification_date':
-            return '%s DESC' % var
-        return None
+    __implements__ = ()
 
     # meta data api ###########################################################
 
@@ -63,7 +42,7 @@
         for rschema, attrschema in self.e_schema.attribute_definitions():
             if rschema.meta:
                 continue
-            value = self.get_value(rschema.type)
+            value = self.cw_attr_value(rschema.type)
             if value:
                 # make the value printable (dates, floats, bytes, etc.)
                 return self.printable_value(rschema.type, value, attrschema.type,
@@ -120,32 +99,6 @@
         except (Unauthorized, IndexError):
             return None
 
-    def breadcrumbs(self, view=None, recurs=False):
-        path = [self]
-        if hasattr(self, 'parent'):
-            parent = self.parent()
-            if parent is not None:
-                try:
-                    path = parent.breadcrumbs(view, True) + [self]
-                except TypeError:
-                    warn("breadcrumbs method's now takes two arguments "
-                         "(view=None, recurs=False), please update",
-                         DeprecationWarning)
-                    path = parent.breadcrumbs(view) + [self]
-        if not recurs:
-            if view is None:
-                if 'vtitle' in self._cw.form:
-                    # embeding for instance
-                    path.append( self._cw.form['vtitle'] )
-            elif view.__regid__ != 'primary' and hasattr(view, 'title'):
-                path.append( self._cw._(view.title) )
-        return path
-
-    ## IFeed interface ########################################################
-
-    def rss_feed_url(self):
-        return self.absolute_url(vid='rss')
-
     # abstractions making the whole things (well, some at least) working ######
 
     def sortvalue(self, rtype=None):
@@ -154,7 +107,7 @@
         """
         if rtype is None:
             return self.dc_title().lower()
-        value = self.get_value(rtype)
+        value = self.cw_attr_value(rtype)
         # do not restrict to `unicode` because Bytes will return a `str` value
         if isinstance(value, basestring):
             return self.printable_value(rtype, format='text/plain').lower()
@@ -189,35 +142,8 @@
         self.__linkto[(rtype, role)] = linkedto
         return linkedto
 
-    # edit controller callbacks ###############################################
-
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if hasattr(self, 'parent') and self.parent():
-            return self.parent().rest_path(), {}
-        return str(self.e_schema).lower(), {}
-
-    def pre_web_edit(self):
-        """callback called by the web editcontroller when an entity will be
-        created/modified, to let a chance to do some entity specific stuff.
-
-        Do nothing by default.
-        """
-        pass
-
     # server side helpers #####################################################
 
-    def notification_references(self, view):
-        """used to control References field of email send on notification
-        for this entity. `view` is the notification view.
-
-        Should return a list of eids which can be used to generate message ids
-        of previously sent email
-        """
-        return ()
-
 # XXX:  store a reference to the AnyEntity class since it is hijacked in goa
 #       configuration and we need the actual reference to avoid infinite loops
 #       in mro
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/adapters.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,443 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""some basic entity adapter implementations, for interfaces used in the
+framework itself.
+"""
+
+__docformat__ = "restructuredtext en"
+
+from itertools import chain
+from warnings import warn
+
+from logilab.mtconverter import TransformError
+from logilab.common.decorators import cached
+
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, is_instance, relation_possible
+from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
+
+
+class IEmailableAdapter(EntityAdapter):
+    __regid__ = 'IEmailable'
+    __select__ = relation_possible('primary_email') | relation_possible('use_email')
+
+    def get_email(self):
+        if getattr(self.entity, 'primary_email', None):
+            return self.entity.primary_email[0].address
+        if getattr(self.entity, 'use_email', None):
+            return self.entity.use_email[0].address
+        return None
+
+    def allowed_massmail_keys(self):
+        """returns a set of allowed email substitution keys
+
+        The default is to return the entity's attribute list but you might
+        override this method to allow extra keys.  For instance, a Person
+        class might want to return a `companyname` key.
+        """
+        return set(rschema.type
+                   for rschema, attrtype in self.entity.e_schema.attribute_definitions()
+                   if attrtype.type not in ('Password', 'Bytes'))
+
+    def as_email_context(self):
+        """returns the dictionary as used by the sendmail controller to
+        build email bodies.
+
+        NOTE: the dictionary keys should match the list returned by the
+        `allowed_massmail_keys` method.
+        """
+        return dict( (attr, getattr(self.entity, attr))
+                     for attr in self.allowed_massmail_keys() )
+
+
+class INotifiableAdapter(EntityAdapter):
+    __regid__ = 'INotifiable'
+    __select__ = is_instance('Any')
+
+    @implements_adapter_compat('INotifiableAdapter')
+    def notification_references(self, view):
+        """used to control References field of email send on notification
+        for this entity. `view` is the notification view.
+
+        Should return a list of eids which can be used to generate message
+        identifiers of previously sent email(s)
+        """
+        itree = self.entity.cw_adapt_to('ITree')
+        if itree is not None:
+            return itree.path()[:-1]
+        return ()
+
+
+class IFTIndexableAdapter(EntityAdapter):
+    __regid__ = 'IFTIndexable'
+    __select__ = is_instance('Any')
+
+    def fti_containers(self, _done=None):
+        if _done is None:
+            _done = set()
+        entity = self.entity
+        _done.add(entity.eid)
+        containers = tuple(entity.e_schema.fulltext_containers())
+        if containers:
+            for rschema, target in containers:
+                if target == 'object':
+                    targets = getattr(entity, rschema.type)
+                else:
+                    targets = getattr(entity, 'reverse_%s' % rschema)
+                for entity in targets:
+                    if entity.eid in _done:
+                        continue
+                    for container in entity.cw_adapt_to('IFTIndexable').fti_containers(_done):
+                        yield container
+                        yielded = True
+        else:
+            yield entity
+
+    # weight in ABCD
+    entity_weight = 1.0
+    attr_weight = {}
+
+    def get_words(self):
+        """used by the full text indexer to get words to index
+
+        this method should only be used on the repository side since it depends
+        on the logilab.database package
+
+        :rtype: list
+        :return: the list of indexable word of this entity
+        """
+        from logilab.database.fti import tokenize
+        # take care to cases where we're modyfying the schema
+        entity = self.entity
+        pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
+        words = {}
+        for rschema in entity.e_schema.indexable_attributes():
+            if (entity.e_schema, rschema) in pending:
+                continue
+            weight = self.attr_weight.get(rschema, 'C')
+            try:
+                value = entity.printable_value(rschema, format='text/plain')
+            except TransformError:
+                continue
+            except:
+                self.exception("can't add value of %s to text index for entity %s",
+                               rschema, entity.eid)
+                continue
+            if value:
+                words.setdefault(weight, []).extend(tokenize(value))
+        for rschema, role in entity.e_schema.fulltext_relations():
+            if role == 'subject':
+                for entity_ in getattr(entity, rschema.type):
+                    merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
+            else: # if role == 'object':
+                for entity_ in getattr(entity, 'reverse_%s' % rschema.type):
+                    merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
+        return words
+
+def merge_weight_dict(maindict, newdict):
+    for weight, words in newdict.iteritems():
+        maindict.setdefault(weight, []).extend(words)
+
+class IDownloadableAdapter(EntityAdapter):
+    """interface for downloadable entities"""
+    __regid__ = 'IDownloadable'
+    __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract
+
+    @implements_adapter_compat('IDownloadable')
+    def download_url(self, **kwargs): # XXX not really part of this interface
+        """return an url to download entity's content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_content_type(self):
+        """return MIME type of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_encoding(self):
+        """return encoding of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_file_name(self):
+        """return file name of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_data(self):
+        """return actual data of the downloadable content"""
+        raise NotImplementedError
+
+
+class ITreeAdapter(EntityAdapter):
+    """This adapter has to be overriden to be configured using the
+    tree_relation, child_role and parent_role class attributes to
+    benefit from this default implementation
+    """
+    __regid__ = 'ITree'
+    __select__ = implements(ITree, warn=False) # XXX for bw compat, else should be abstract
+
+    child_role = 'subject'
+    parent_role = 'object'
+
+    @property
+    def tree_relation(self):
+        warn('[3.9] tree_attribute is deprecated, define tree_relation on a custom '
+             'ITree for %s instead' % (self.entity.__class__),
+             DeprecationWarning)
+        return self.entity.tree_attribute
+
+    @implements_adapter_compat('ITree')
+    def children_rql(self):
+        """returns RQL to get children
+
+        XXX should be removed from the public interface
+        """
+        return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
+
+    @implements_adapter_compat('ITree')
+    def different_type_children(self, entities=True):
+        """return children entities of different type as this entity.
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        res = self.entity.related(self.tree_relation, self.parent_role,
+                                  entities=entities)
+        eschema = self.entity.e_schema
+        if entities:
+            return [e for e in res if e.e_schema != eschema]
+        return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
+
+    @implements_adapter_compat('ITree')
+    def same_type_children(self, entities=True):
+        """return children entities of the same type as this entity.
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        res = self.entity.related(self.tree_relation, self.parent_role,
+                                  entities=entities)
+        eschema = self.entity.e_schema
+        if entities:
+            return [e for e in res if e.e_schema == eschema]
+        return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
+
+    @implements_adapter_compat('ITree')
+    def is_leaf(self):
+        """returns true if this node as no child"""
+        return len(self.children()) == 0
+
+    @implements_adapter_compat('ITree')
+    def is_root(self):
+        """returns true if this node has no parent"""
+        return self.parent() is None
+
+    @implements_adapter_compat('ITree')
+    def root(self):
+        """return the root object"""
+        return self._cw.entity_from_eid(self.path()[0])
+
+    @implements_adapter_compat('ITree')
+    def parent(self):
+        """return the parent entity if any, else None (e.g. if we are on the
+        root)
+        """
+        try:
+            return self.entity.related(self.tree_relation, self.child_role,
+                                       entities=True)[0]
+        except (KeyError, IndexError):
+            return None
+
+    @implements_adapter_compat('ITree')
+    def children(self, entities=True, sametype=False):
+        """return children entities
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        if sametype:
+            return self.same_type_children(entities)
+        else:
+            return self.entity.related(self.tree_relation, self.parent_role,
+                                       entities=entities)
+
+    @implements_adapter_compat('ITree')
+    def iterparents(self, strict=True):
+        def _uptoroot(self):
+            curr = self
+            while True:
+                curr = curr.parent()
+                if curr is None:
+                    break
+                yield curr
+                curr = curr.cw_adapt_to('ITree')
+        if not strict:
+            return chain([self.entity], _uptoroot(self))
+        return _uptoroot(self)
+
+    @implements_adapter_compat('ITree')
+    def iterchildren(self, _done=None):
+        """iterates over the item's children"""
+        if _done is None:
+            _done = set()
+        for child in self.children():
+            if child.eid in _done:
+                self.error('loop in %s tree', child.__regid__.lower())
+                continue
+            yield child
+            _done.add(child.eid)
+
+    @implements_adapter_compat('ITree')
+    def prefixiter(self, _done=None):
+        if _done is None:
+            _done = set()
+        if self.entity.eid in _done:
+            return
+        _done.add(self.entity.eid)
+        yield self.entity
+        for child in self.same_type_children():
+            for entity in child.cw_adapt_to('ITree').prefixiter(_done):
+                yield entity
+
+    @cached
+    @implements_adapter_compat('ITree')
+    def path(self):
+        """returns the list of eids from the root object to this object"""
+        path = []
+        adapter = self
+        entity = adapter.entity
+        while entity is not None:
+            if entity.eid in path:
+                self.error('loop in %s tree', entity.__regid__.lower())
+                break
+            path.append(entity.eid)
+            try:
+                # check we are not jumping to another tree
+                if (adapter.tree_relation != self.tree_relation or
+                    adapter.child_role != self.child_role):
+                    break
+                entity = adapter.parent()
+                adapter = entity.cw_adapt_to('ITree')
+            except AttributeError:
+                break
+        path.reverse()
+        return path
+
+
+class IProgressAdapter(EntityAdapter):
+    """something that has a cost, a state and a progression.
+
+    You should at least override progress_info an in_progress methods on concret
+    implementations.
+    """
+    __regid__ = 'IProgress'
+    __select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def cost(self):
+        """the total cost"""
+        return self.progress_info()['estimated']
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def revised_cost(self):
+        return self.progress_info().get('estimatedcorrected', self.cost)
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def done(self):
+        """what is already done"""
+        return self.progress_info()['done']
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def todo(self):
+        """what remains to be done"""
+        return self.progress_info()['todo']
+
+    @implements_adapter_compat('IProgress')
+    def progress_info(self):
+        """returns a dictionary describing progress/estimated cost of the
+        version.
+
+        - mandatory keys are (''estimated', 'done', 'todo')
+
+        - optional keys are ('notestimated', 'notestimatedcorrected',
+          'estimatedcorrected')
+
+        'noestimated' and 'notestimatedcorrected' should default to 0
+        'estimatedcorrected' should default to 'estimated'
+        """
+        raise NotImplementedError
+
+    @implements_adapter_compat('IProgress')
+    def finished(self):
+        """returns True if status is finished"""
+        return not self.in_progress()
+
+    @implements_adapter_compat('IProgress')
+    def in_progress(self):
+        """returns True if status is not finished"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IProgress')
+    def progress(self):
+        """returns the % progress of the task item"""
+        try:
+            return 100. * self.done / self.revised_cost
+        except ZeroDivisionError:
+            # total cost is 0 : if everything was estimated, task is completed
+            if self.progress_info().get('notestimated'):
+                return 0.
+            return 100
+
+    @implements_adapter_compat('IProgress')
+    def progress_class(self):
+        return ''
+
+
+class IMileStoneAdapter(IProgressAdapter):
+    __regid__ = 'IMileStone'
+    __select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract
+
+    parent_type = None # specify main task's type
+
+    @implements_adapter_compat('IMileStone')
+    def get_main_task(self):
+        """returns the main ITask entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def initial_prevision_date(self):
+        """returns the initial expected end of the milestone"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def eta_date(self):
+        """returns expected date of completion based on what remains
+        to be done
+        """
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def completion_date(self):
+        """returns date on which the subtask has been completed"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def contractors(self):
+        """returns the list of persons supposed to work on this task"""
+        raise NotImplementedError
--- a/entities/authobjs.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entities/authobjs.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""entity classes user and group entities
+"""entity classes user and group entities"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.common.decorators import cached
--- a/entities/lib.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entities/lib.py	Mon Jul 19 15:37:02 2010 +0200
@@ -48,13 +48,13 @@
 
     @property
     def email_of(self):
-        return self.reverse_use_email and self.reverse_use_email[0]
+        return self.reverse_use_email and self.reverse_use_email[0] or None
 
     @property
     def prefered(self):
         return self.prefered_form and self.prefered_form[0] or self
 
-    @deprecated('use .prefered')
+    @deprecated('[3.6] use .prefered')
     def canonical_form(self):
         return self.prefered_form and self.prefered_form[0] or self
 
@@ -89,14 +89,6 @@
             return self.display_address()
         return super(EmailAddress, self).printable_value(attr, value, attrtype, format)
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.email_of:
-            return self.email_of.rest_path(), {}
-        return super(EmailAddress, self).after_deletion_path()
-
 
 class Bookmark(AnyEntity):
     """customized class for Bookmark entities"""
@@ -133,12 +125,6 @@
         except UnknownProperty:
             return u''
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        return 'view', {}
-
 
 class CWCache(AnyEntity):
     """Cache"""
--- a/entities/schemaobjs.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entities/schemaobjs.py	Mon Jul 19 15:37:02 2010 +0200
@@ -115,14 +115,6 @@
             scard, self.relation_type[0].name, ocard,
             self.to_entity[0].name)
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.relation_type:
-            return self.relation_type[0].rest_path(), {}
-        return super(CWRelation, self).after_deletion_path()
-
     @property
     def rtype(self):
         return self.relation_type[0]
@@ -139,6 +131,7 @@
         rschema = self._cw.vreg.schema.rschema(self.rtype.name)
         return rschema.rdefs[(self.stype.name, self.otype.name)]
 
+
 class CWAttribute(CWRelation):
     __regid__ = 'CWAttribute'
 
@@ -160,14 +153,6 @@
     def dc_title(self):
         return '%s(%s)' % (self.cstrtype[0].name, self.value or u'')
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.reverse_constrained_by:
-            return self.reverse_constrained_by[0].rest_path(), {}
-        return super(CWConstraint, self).after_deletion_path()
-
     @property
     def type(self):
         return self.cstrtype[0].name
@@ -201,14 +186,6 @@
     def check_expression(self, *args, **kwargs):
         return self._rqlexpr().check(*args, **kwargs)
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.expression_of:
-            return self.expression_of.rest_path(), {}
-        return super(RQLExpression, self).after_deletion_path()
-
 
 class CWPermission(AnyEntity):
     __regid__ = 'CWPermission'
@@ -218,12 +195,3 @@
         if self.label:
             return '%s (%s)' % (self._cw._(self.name), self.label)
         return self._cw._(self.name)
-
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        permissionof = getattr(self, 'reverse_require_permission', ())
-        if len(permissionof) == 1:
-            return permissionof[0].rest_path(), {}
-        return super(CWPermission, self).after_deletion_path()
--- a/entities/test/unittest_base.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entities/test/unittest_base.py	Mon Jul 19 15:37:02 2010 +0200
@@ -27,7 +27,7 @@
 from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb import ValidationError
-from cubicweb.interfaces import IMileStone, IWorkflowable
+from cubicweb.interfaces import IMileStone, ICalendarable
 from cubicweb.entities import AnyEntity
 
 
@@ -106,7 +106,7 @@
     def test_allowed_massmail_keys(self):
         e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0)
         # Bytes/Password attributes should be omited
-        self.assertEquals(e.allowed_massmail_keys(),
+        self.assertEquals(e.cw_adapt_to('IEmailable').allowed_massmail_keys(),
                           set(('surname', 'firstname', 'login', 'last_login_time',
                                'creation_date', 'modification_date', 'cwuri', 'eid'))
                           )
@@ -115,8 +115,9 @@
 class InterfaceTC(CubicWebTC):
 
     def test_nonregr_subclasses_and_mixins_interfaces(self):
+        from cubicweb.entities.wfobjs import WorkflowableMixIn
+        WorkflowableMixIn.__implements__ = (ICalendarable,)
         CWUser = self.vreg['etypes'].etype_class('CWUser')
-        self.failUnless(implements(CWUser, IWorkflowable))
         class MyUser(CWUser):
             __implements__ = (IMileStone,)
         self.vreg._loadedmods[__name__] = {}
@@ -126,10 +127,10 @@
         # a copy is done systematically
         self.failUnless(issubclass(MyUser_, MyUser))
         self.failUnless(implements(MyUser_, IMileStone))
-        self.failUnless(implements(MyUser_, IWorkflowable))
+        self.failUnless(implements(MyUser_, ICalendarable))
         # original class should not have beed modified, only the copy
         self.failUnless(implements(MyUser, IMileStone))
-        self.failIf(implements(MyUser, IWorkflowable))
+        self.failIf(implements(MyUser, ICalendarable))
 
 
 class SpecializedEntityClassesTC(CubicWebTC):
--- a/entities/test/unittest_wfobjs.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entities/test/unittest_wfobjs.py	Mon Jul 19 15:37:02 2010 +0200
@@ -100,35 +100,38 @@
 
     def test_workflow_base(self):
         e = self.create_user('toto')
-        self.assertEquals(e.state, 'activated')
-        e.change_state('deactivated', u'deactivate 1')
+        iworkflowable = e.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'activated')
+        iworkflowable.change_state('deactivated', u'deactivate 1')
         self.commit()
-        e.change_state('activated', u'activate 1')
+        iworkflowable.change_state('activated', u'activate 1')
         self.commit()
-        e.change_state('deactivated', u'deactivate 2')
+        iworkflowable.change_state('deactivated', u'deactivate 2')
         self.commit()
-        e.clear_related_cache('wf_info_for', 'object')
+        e.cw_clear_relation_cache('wf_info_for', 'object')
         self.assertEquals([tr.comment for tr in e.reverse_wf_info_for],
                           ['deactivate 1', 'activate 1', 'deactivate 2'])
-        self.assertEquals(e.latest_trinfo().comment, 'deactivate 2')
+        self.assertEquals(iworkflowable.latest_trinfo().comment, 'deactivate 2')
 
     def test_possible_transitions(self):
         user = self.execute('CWUser X').get_entity(0, 0)
-        trs = list(user.possible_transitions())
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        trs = list(iworkflowable.possible_transitions())
         self.assertEquals(len(trs), 1)
         self.assertEquals(trs[0].name, u'deactivate')
         self.assertEquals(trs[0].destination(None).name, u'deactivated')
         # test a std user get no possible transition
         cnx = self.login('member')
         # fetch the entity using the new session
-        trs = list(cnx.user().possible_transitions())
+        trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions())
         self.assertEquals(len(trs), 0)
 
     def _test_manager_deactivate(self, user):
-        user.clear_related_cache('in_state', 'subject')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        user.cw_clear_relation_cache('in_state', 'subject')
         self.assertEquals(len(user.in_state), 1)
-        self.assertEquals(user.state, 'deactivated')
-        trinfo = user.latest_trinfo()
+        self.assertEquals(iworkflowable.state, 'deactivated')
+        trinfo = iworkflowable.latest_trinfo()
         self.assertEquals(trinfo.previous_state.name, 'activated')
         self.assertEquals(trinfo.new_state.name, 'deactivated')
         self.assertEquals(trinfo.comment, 'deactivate user')
@@ -137,7 +140,8 @@
 
     def test_change_state(self):
         user = self.user()
-        user.change_state('deactivated', comment=u'deactivate user')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.change_state('deactivated', comment=u'deactivate user')
         trinfo = self._test_manager_deactivate(user)
         self.assertEquals(trinfo.transition, None)
 
@@ -154,33 +158,36 @@
 
     def test_fire_transition(self):
         user = self.user()
-        user.fire_transition('deactivate', comment=u'deactivate user')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate', comment=u'deactivate user')
         user.clear_all_caches()
-        self.assertEquals(user.state, 'deactivated')
+        self.assertEquals(iworkflowable.state, 'deactivated')
         self._test_manager_deactivate(user)
         trinfo = self._test_manager_deactivate(user)
         self.assertEquals(trinfo.transition.name, 'deactivate')
 
     def test_goback_transition(self):
-        wf = self.session.user.current_workflow
+        wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
         asleep = wf.add_state('asleep')
-        wf.add_transition('rest', (wf.state_by_name('activated'), wf.state_by_name('deactivated')),
-                               asleep)
+        wf.add_transition('rest', (wf.state_by_name('activated'),
+                                   wf.state_by_name('deactivated')),
+                          asleep)
         wf.add_transition('wake up', asleep)
         user = self.create_user('stduser')
-        user.fire_transition('rest')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('rest')
         self.commit()
-        user.fire_transition('wake up')
+        iworkflowable.fire_transition('wake up')
         self.commit()
-        self.assertEquals(user.state, 'activated')
-        user.fire_transition('deactivate')
+        self.assertEquals(iworkflowable.state, 'activated')
+        iworkflowable.fire_transition('deactivate')
         self.commit()
-        user.fire_transition('rest')
+        iworkflowable.fire_transition('rest')
         self.commit()
-        user.fire_transition('wake up')
+        iworkflowable.fire_transition('wake up')
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'deactivated')
+        self.assertEquals(iworkflowable.state, 'deactivated')
 
     # XXX test managers can change state without matching transition
 
@@ -189,18 +196,18 @@
         self.create_user('tutu')
         cnx = self.login('tutu')
         req = self.request()
-        member = req.entity_from_eid(self.member.eid)
+        iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
         ex = self.assertRaises(ValidationError,
-                               member.fire_transition, 'deactivate')
+                               iworkflowable.fire_transition, 'deactivate')
         self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
         cnx.close()
         cnx = self.login('member')
         req = self.request()
-        member = req.entity_from_eid(self.member.eid)
-        member.fire_transition('deactivate')
+        iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         cnx.commit()
         ex = self.assertRaises(ValidationError,
-                               member.fire_transition, 'activate')
+                               iworkflowable.fire_transition, 'activate')
         self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
 
     def test_fire_transition_owned_by(self):
@@ -250,43 +257,44 @@
                                       [(swfstate2, state2), (swfstate3, state3)])
         self.assertEquals(swftr1.destination(None).eid, swfstate1.eid)
         # workflows built, begin test
-        self.group = self.request().create_entity('CWGroup', name=u'grp1')
+        group = self.request().create_entity('CWGroup', name=u'grp1')
         self.commit()
-        self.assertEquals(self.group.current_state.eid, state1.eid)
-        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.subworkflow_input_transition(), None)
-        self.group.fire_transition('swftr1', u'go')
+        iworkflowable = group.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.current_state.eid, state1.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.subworkflow_input_transition(), None)
+        iworkflowable.fire_transition('swftr1', u'go')
         self.commit()
-        self.group.clear_all_caches()
-        self.assertEquals(self.group.current_state.eid, swfstate1.eid)
-        self.assertEquals(self.group.current_workflow.eid, swf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid)
-        self.group.fire_transition('tr1', u'go')
+        group.clear_all_caches()
+        self.assertEquals(iworkflowable.current_state.eid, swfstate1.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, swf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.subworkflow_input_transition().eid, swftr1.eid)
+        iworkflowable.fire_transition('tr1', u'go')
         self.commit()
-        self.group.clear_all_caches()
-        self.assertEquals(self.group.current_state.eid, state2.eid)
-        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        group.clear_all_caches()
+        self.assertEquals(iworkflowable.current_state.eid, state2.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.subworkflow_input_transition(), None)
         # force back to swfstate1 is impossible since we can't any more find
         # subworkflow input transition
         ex = self.assertRaises(ValidationError,
-                               self.group.change_state, swfstate1, u'gadget')
+                               iworkflowable.change_state, swfstate1, u'gadget')
         self.assertEquals(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
         self.rollback()
         # force back to state1
-        self.group.change_state('state1', u'gadget')
-        self.group.fire_transition('swftr1', u'au')
-        self.group.clear_all_caches()
-        self.group.fire_transition('tr2', u'chapeau')
+        iworkflowable.change_state('state1', u'gadget')
+        iworkflowable.fire_transition('swftr1', u'au')
+        group.clear_all_caches()
+        iworkflowable.fire_transition('tr2', u'chapeau')
         self.commit()
-        self.group.clear_all_caches()
-        self.assertEquals(self.group.current_state.eid, state3.eid)
-        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertListEquals(parse_hist(self.group.workflow_history),
+        group.clear_all_caches()
+        self.assertEquals(iworkflowable.current_state.eid, state3.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertListEquals(parse_hist(iworkflowable.workflow_history),
                               [('state1', 'swfstate1', 'swftr1', 'go'),
                                ('swfstate1', 'swfstate2', 'tr1', 'go'),
                                ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'),
@@ -337,8 +345,9 @@
         self.commit()
         group = self.request().create_entity('CWGroup', name=u'grp1')
         self.commit()
+        iworkflowable = group.cw_adapt_to('IWorkflowable')
         for trans in ('identify', 'release', 'close'):
-            group.fire_transition(trans)
+            iworkflowable.fire_transition(trans)
             self.commit()
 
 
@@ -362,6 +371,7 @@
         self.commit()
         group = self.request().create_entity('CWGroup', name=u'grp1')
         self.commit()
+        iworkflowable = group.cw_adapt_to('IWorkflowable')
         for trans, nextstate in (('identify', 'xsigning'),
                                  ('xabort', 'created'),
                                  ('identify', 'xsigning'),
@@ -369,10 +379,10 @@
                                  ('release', 'xsigning'),
                                  ('xabort', 'identified')
                                  ):
-            group.fire_transition(trans)
+            iworkflowable.fire_transition(trans)
             self.commit()
             group.clear_all_caches()
-            self.assertEquals(group.state, nextstate)
+            self.assertEquals(iworkflowable.state, nextstate)
 
 
 class CustomWorkflowTC(CubicWebTC):
@@ -389,35 +399,38 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
         self.member.clear_all_caches()
-        self.assertEquals(self.member.state, 'activated')# no change before commit
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'activated')# no change before commit
         self.commit()
         self.member.clear_all_caches()
-        self.assertEquals(self.member.current_workflow.eid, wf.eid)
-        self.assertEquals(self.member.state, 'asleep')
-        self.assertEquals(self.member.workflow_history, ())
+        self.assertEquals(iworkflowable.current_workflow.eid, wf.eid)
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals(iworkflowable.workflow_history, ())
 
     def test_custom_wf_replace_state_keep_history(self):
         """member in inital state with some history, state is redirected and
         state change is recorded to history
         """
-        self.member.fire_transition('deactivate')
-        self.member.fire_transition('activate')
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
+        iworkflowable.fire_transition('activate')
         wf = add_wf(self, 'CWUser')
         wf.add_state('asleep', initial=True)
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
         self.commit()
         self.member.clear_all_caches()
-        self.assertEquals(self.member.current_workflow.eid, wf.eid)
-        self.assertEquals(self.member.state, 'asleep')
-        self.assertEquals(parse_hist(self.member.workflow_history),
+        self.assertEquals(iworkflowable.current_workflow.eid, wf.eid)
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('activated', 'deactivated', 'deactivate', None),
                            ('deactivated', 'activated', 'activate', None),
                            ('activated', 'asleep', None, 'workflow changed to "CWUser"')])
 
     def test_custom_wf_no_initial_state(self):
         """try to set a custom workflow which has no initial state"""
-        self.member.fire_transition('deactivate')
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         wf = add_wf(self, 'CWUser')
         wf.add_state('asleep')
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
@@ -438,7 +451,8 @@
         """member in some state shared by the new workflow, nothing has to be
         done
         """
-        self.member.fire_transition('deactivate')
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         wf = add_wf(self, 'CWUser')
         wf.add_state('asleep', initial=True)
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
@@ -447,12 +461,12 @@
         self.execute('DELETE X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
         self.member.clear_all_caches()
-        self.assertEquals(self.member.state, 'asleep')# no change before commit
+        self.assertEquals(iworkflowable.state, 'asleep')# no change before commit
         self.commit()
         self.member.clear_all_caches()
-        self.assertEquals(self.member.current_workflow.name, "default user workflow")
-        self.assertEquals(self.member.state, 'activated')
-        self.assertEquals(parse_hist(self.member.workflow_history),
+        self.assertEquals(iworkflowable.current_workflow.name, "default user workflow")
+        self.assertEquals(iworkflowable.state, 'activated')
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('activated', 'deactivated', 'deactivate', None),
                            ('deactivated', 'asleep', None, 'workflow changed to "CWUser"'),
                            ('asleep', 'activated', None, 'workflow changed to "default user workflow"'),])
@@ -473,28 +487,29 @@
     def test_auto_transition_fired(self):
         wf = self.setup_custom_wf()
         user = self.create_user('member')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': user.eid})
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'asleep')
-        self.assertEquals([t.name for t in user.possible_transitions()],
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals([t.name for t in iworkflowable.possible_transitions()],
                           ['rest'])
-        user.fire_transition('rest')
+        iworkflowable.fire_transition('rest')
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'asleep')
-        self.assertEquals([t.name for t in user.possible_transitions()],
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals([t.name for t in iworkflowable.possible_transitions()],
                           ['rest'])
-        self.assertEquals(parse_hist(user.workflow_history),
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('asleep', 'asleep', 'rest', None)])
         user.set_attributes(surname=u'toto') # fulfill condition
         self.commit()
-        user.fire_transition('rest')
+        iworkflowable.fire_transition('rest')
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'dead')
-        self.assertEquals(parse_hist(user.workflow_history),
+        self.assertEquals(iworkflowable.state, 'dead')
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('asleep', 'asleep', 'rest', None),
                            ('asleep', 'asleep', 'rest', None),
                            ('asleep', 'dead', 'sick', None),])
@@ -505,7 +520,8 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': user.eid})
         self.commit()
-        self.assertEquals(user.state, 'dead')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'dead')
 
     def test_auto_transition_initial_state_fired(self):
         wf = self.execute('Any WF WHERE ET default_workflow WF, '
@@ -517,14 +533,15 @@
         self.commit()
         user = self.create_user('member', surname=u'toto')
         self.commit()
-        self.assertEquals(user.state, 'dead')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'dead')
 
 
 class WorkflowHooksTC(CubicWebTC):
 
     def setUp(self):
         CubicWebTC.setUp(self)
-        self.wf = self.session.user.current_workflow
+        self.wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
         self.session.set_pool()
         self.s_activated = self.wf.state_by_name('activated').eid
         self.s_deactivated = self.wf.state_by_name('deactivated').eid
@@ -572,8 +589,9 @@
     def test_transition_checking1(self):
         cnx = self.login('stduser')
         user = cnx.user(self.session)
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
         ex = self.assertRaises(ValidationError,
-                               user.fire_transition, 'activate')
+                               iworkflowable.fire_transition, 'activate')
         self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
@@ -581,8 +599,9 @@
     def test_transition_checking2(self):
         cnx = self.login('stduser')
         user = cnx.user(self.session)
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
         ex = self.assertRaises(ValidationError,
-                               user.fire_transition, 'dummy')
+                               iworkflowable.fire_transition, 'dummy')
         self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
@@ -591,15 +610,16 @@
         cnx = self.login('stduser')
         session = self.session
         user = cnx.user(session)
-        user.fire_transition('deactivate')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         cnx.commit()
         session.set_pool()
         ex = self.assertRaises(ValidationError,
-                               user.fire_transition, 'deactivate')
+                               iworkflowable.fire_transition, 'deactivate')
         self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                                             u"transition isn't allowed from")
         # get back now
-        user.fire_transition('activate')
+        iworkflowable.fire_transition('activate')
         cnx.commit()
         cnx.close()
 
--- a/entities/wfobjs.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entities/wfobjs.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,13 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""workflow definition and history related entities
+"""workflow handling:
 
+* entity types defining workflow (Workflow, State, Transition...)
+* workflow history (TrInfo)
+* adapter for workflowable entities (IWorkflowableAdapter)
 """
+
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -27,7 +31,8 @@
 from logilab.common.compat import any
 
 from cubicweb.entities import AnyEntity, fetch_config
-from cubicweb.interfaces import IWorkflowable
+from cubicweb.view import EntityAdapter
+from cubicweb.selectors import relation_possible
 from cubicweb.mixins import MI_REL_TRIGGERS
 
 class WorkflowException(Exception): pass
@@ -47,15 +52,6 @@
         return any(et for et in self.reverse_default_workflow
                    if et.name == etype)
 
-    # XXX define parent() instead? what if workflow of multiple types?
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.workflow_of:
-            return self.workflow_of[0].rest_path(), {'vid': 'workflow'}
-        return super(Workflow, self).after_deletion_path()
-
     def iter_workflows(self, _done=None):
         """return an iterator on actual workflows, eg this workflow and its
         subworkflows
@@ -177,7 +173,7 @@
                 {'os': todelstate.eid, 'ns': replacement.eid})
         execute('SET X to_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s',
                 {'os': todelstate.eid, 'ns': replacement.eid})
-        todelstate.delete()
+        todelstate.cw_delete()
 
 
 class BaseTransition(AnyEntity):
@@ -226,14 +222,6 @@
             return False
         return True
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.transition_of:
-            return self.transition_of[0].rest_path(), {}
-        return super(BaseTransition, self).after_deletion_path()
-
     def set_permissions(self, requiredgroups=(), conditions=(), reset=True):
         """set or add (if `reset` is False) groups and conditions for this
         transition
@@ -277,7 +265,7 @@
         try:
             return self.destination_state[0]
         except IndexError:
-            return entity.latest_trinfo().previous_state
+            return entity.cw_adapt_to('IWorkflowable').latest_trinfo().previous_state
 
     def potential_destinations(self):
         try:
@@ -288,9 +276,6 @@
                     for previousstate in tr.reverse_allowed_transition:
                         yield previousstate
 
-    def parent(self):
-        return self.workflow
-
 
 class WorkflowTransition(BaseTransition):
     """customized class for WorkflowTransition entities"""
@@ -331,7 +316,7 @@
             return None
         if tostateeid is None:
             # go back to state from which we've entered the subworkflow
-            return entity.subworkflow_input_trinfo().previous_state
+            return entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo().previous_state
         return self._cw.entity_from_eid(tostateeid)
 
     @cached
@@ -358,9 +343,6 @@
     def destination(self):
         return self.destination_state and self.destination_state[0] or None
 
-    def parent(self):
-        return self.reverse_subworkflow_exit[0]
-
 
 class State(AnyEntity):
     """customized class for State entities"""
@@ -371,10 +353,7 @@
     @property
     def workflow(self):
         # take care, may be missing in multi-sources configuration
-        return self.state_of and self.state_of[0]
-
-    def parent(self):
-        return self.workflow
+        return self.state_of and self.state_of[0] or None
 
 
 class TrInfo(AnyEntity):
@@ -399,22 +378,99 @@
     def transition(self):
         return self.by_transition and self.by_transition[0] or None
 
-    def parent(self):
-        return self.for_entity
-
 
 class WorkflowableMixIn(object):
     """base mixin providing workflow helper methods for workflowable entities.
     This mixin will be automatically set on class supporting the 'in_state'
     relation (which implies supporting 'wf_info_for' as well)
     """
-    __implements__ = (IWorkflowable,)
+
+    @property
+    @deprecated('[3.5] use printable_state')
+    def displayable_state(self):
+        return self._cw._(self.state)
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').main_workflow")
+    def main_workflow(self):
+        return self.cw_adapt_to('IWorkflowable').main_workflow
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_workflow")
+    def current_workflow(self):
+        return self.cw_adapt_to('IWorkflowable').current_workflow
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_state")
+    def current_state(self):
+        return self.cw_adapt_to('IWorkflowable').current_state
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').state")
+    def state(self):
+        return self.cw_adapt_to('IWorkflowable').state
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').printable_state")
+    def printable_state(self):
+        return self.cw_adapt_to('IWorkflowable').printable_state
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').workflow_history")
+    def workflow_history(self):
+        return self.cw_adapt_to('IWorkflowable').workflow_history
+
+    @deprecated('[3.5] get transition from current workflow and use its may_be_fired method')
+    def can_pass_transition(self, trname):
+        """return the Transition instance if the current user can fire the
+        transition with the given name, else None
+        """
+        tr = self.current_workflow and self.current_workflow.transition_by_name(trname)
+        if tr and tr.may_be_fired(self.eid):
+            return tr
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').cwetype_workflow()")
+    def cwetype_workflow(self):
+        return self.cw_adapt_to('IWorkflowable').main_workflow()
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').latest_trinfo()")
+    def latest_trinfo(self):
+        return self.cw_adapt_to('IWorkflowable').latest_trinfo()
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').possible_transitions()")
+    def possible_transitions(self, type='normal'):
+        return self.cw_adapt_to('IWorkflowable').possible_transitions(type)
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').fire_transition()")
+    def fire_transition(self, tr, comment=None, commentformat=None):
+        return self.cw_adapt_to('IWorkflowable').fire_transition(tr, comment, commentformat)
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').change_state()")
+    def change_state(self, statename, comment=None, commentformat=None, tr=None):
+        return self.cw_adapt_to('IWorkflowable').change_state(statename, comment, commentformat, tr)
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()")
+    def subworkflow_input_trinfo(self):
+        return self.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_transition()")
+    def subworkflow_input_transition(self):
+        return self.cw_adapt_to('IWorkflowable').subworkflow_input_transition()
+
+
+MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn
+
+
+
+class IWorkflowableAdapter(WorkflowableMixIn, EntityAdapter):
+    """base adapter providing workflow helper methods for workflowable entities.
+    """
+    __regid__ = 'IWorkflowable'
+    __select__ = relation_possible('in_state')
+
+    @cached
+    def cwetype_workflow(self):
+        """return the default workflow for entities of this type"""
+        # XXX CWEType method
+        wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
+                                  'ET name %(et)s', {'et': self.entity.__regid__})
+        if wfrset:
+            return wfrset.get_entity(0, 0)
+        self.warning("can't find any workflow for %s", self.entity.__regid__)
+        return None
 
     @property
     def main_workflow(self):
         """return current workflow applied to this entity"""
-        if self.custom_workflow:
-            return self.custom_workflow[0]
+        if self.entity.custom_workflow:
+            return self.entity.custom_workflow[0]
         return self.cwetype_workflow()
 
     @property
@@ -425,14 +481,14 @@
     @property
     def current_state(self):
         """return current state entity"""
-        return self.in_state and self.in_state[0] or None
+        return self.entity.in_state and self.entity.in_state[0] or None
 
     @property
     def state(self):
         """return current state name"""
         try:
-            return self.in_state[0].name
-        except IndexError:
+            return self.current_state.name
+        except AttributeError:
             self.warning('entity %s has no state', self)
             return None
 
@@ -449,26 +505,15 @@
         """return the workflow history for this entity (eg ordered list of
         TrInfo entities)
         """
-        return self.reverse_wf_info_for
+        return self.entity.reverse_wf_info_for
 
     def latest_trinfo(self):
         """return the latest transition information for this entity"""
         try:
-            return self.reverse_wf_info_for[-1]
+            return self.workflow_history[-1]
         except IndexError:
             return None
 
-    @cached
-    def cwetype_workflow(self):
-        """return the default workflow for entities of this type"""
-        # XXX CWEType method
-        wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
-                                  'ET name %(et)s', {'et': self.__regid__})
-        if wfrset:
-            return wfrset.get_entity(0, 0)
-        self.warning("can't find any workflow for %s", self.__regid__)
-        return None
-
     def possible_transitions(self, type='normal'):
         """generates transition that MAY be fired for the given entity,
         expected to be in this state
@@ -483,16 +528,44 @@
             {'x': self.current_state.eid, 'type': type,
              'wfeid': self.current_workflow.eid})
         for tr in rset.entities():
-            if tr.may_be_fired(self.eid):
+            if tr.may_be_fired(self.entity.eid):
                 yield tr
 
+    def subworkflow_input_trinfo(self):
+        """return the TrInfo which has be recorded when this entity went into
+        the current sub-workflow
+        """
+        if self.main_workflow.eid == self.current_workflow.eid:
+            return # doesn't make sense
+        subwfentries = []
+        for trinfo in self.workflow_history:
+            if (trinfo.transition and
+                trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
+                # entering or leaving a subworkflow
+                if (subwfentries and
+                    subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
+                    subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
+                    # leave
+                    del subwfentries[-1]
+                else:
+                    # enter
+                    subwfentries.append(trinfo)
+        if not subwfentries:
+            return None
+        return subwfentries[-1]
+
+    def subworkflow_input_transition(self):
+        """return the transition which has went through the current sub-workflow
+        """
+        return getattr(self.subworkflow_input_trinfo(), 'transition', None)
+
     def _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
         kwargs = {}
         if comment is not None:
             kwargs['comment'] = comment
             if commentformat is not None:
                 kwargs['comment_format'] = commentformat
-        kwargs['wf_info_for'] = self
+        kwargs['wf_info_for'] = self.entity
         if treid is not None:
             kwargs['by_transition'] = self._cw.entity_from_eid(treid)
         if tseid is not None:
@@ -532,51 +605,3 @@
             stateeid = state.eid
         # XXX try to find matching transition?
         return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
-
-    def subworkflow_input_trinfo(self):
-        """return the TrInfo which has be recorded when this entity went into
-        the current sub-workflow
-        """
-        if self.main_workflow.eid == self.current_workflow.eid:
-            return # doesn't make sense
-        subwfentries = []
-        for trinfo in self.workflow_history:
-            if (trinfo.transition and
-                trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
-                # entering or leaving a subworkflow
-                if (subwfentries and
-                    subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
-                    subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
-                    # leave
-                    del subwfentries[-1]
-                else:
-                    # enter
-                    subwfentries.append(trinfo)
-        if not subwfentries:
-            return None
-        return subwfentries[-1]
-
-    def subworkflow_input_transition(self):
-        """return the transition which has went through the current sub-workflow
-        """
-        return getattr(self.subworkflow_input_trinfo(), 'transition', None)
-
-    def clear_all_caches(self):
-        super(WorkflowableMixIn, self).clear_all_caches()
-        clear_cache(self, 'cwetype_workflow')
-
-    @deprecated('[3.5] get transition from current workflow and use its may_be_fired method')
-    def can_pass_transition(self, trname):
-        """return the Transition instance if the current user can fire the
-        transition with the given name, else None
-        """
-        tr = self.current_workflow and self.current_workflow.transition_by_name(trname)
-        if tr and tr.may_be_fired(self.eid):
-            return tr
-
-    @property
-    @deprecated('[3.5] use printable_state')
-    def displayable_state(self):
-        return self._cw._(self.state)
-
-MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn
--- a/entity.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/entity.py	Mon Jul 19 15:37:02 2010 +0200
@@ -19,11 +19,12 @@
 
 __docformat__ = "restructuredtext en"
 
+from copy import copy
 from warnings import warn
 
 from logilab.common import interface
-from logilab.common.compat import all
 from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
 from logilab.mtconverter import TransformData, TransformError, xml_escape
 
 from rql.utils import rqlvar_maker
@@ -51,7 +52,7 @@
     return '1'
 
 
-class Entity(AppObject, dict):
+class Entity(AppObject):
     """an entity instance has e_schema automagically set on
     the class and instances has access to their issuing cursor.
 
@@ -106,10 +107,10 @@
                     if not interface.implements(cls, iface):
                         interface.extend(cls, iface)
             if role == 'subject':
-                setattr(cls, rschema.type, SubjectRelation(rschema))
+                attr = rschema.type
             else:
                 attr = 'reverse_%s' % rschema.type
-                setattr(cls, attr, ObjectRelation(rschema))
+            setattr(cls, attr, Relation(rschema, role))
         if mixins:
             # see etype class instantation in cwvreg.ETypeRegistry.etype_class method:
             # due to class dumping, cls is the generated top level class with actual
@@ -124,6 +125,24 @@
             cls.__bases__ = tuple(mixins)
             cls.info('plugged %s mixins on %s', mixins, cls)
 
+    fetch_attrs = ('modification_date',)
+    @classmethod
+    def fetch_order(cls, attr, var):
+        """class method used to control sort order when multiple entities of
+        this type are fetched
+        """
+        return cls.fetch_unrelated_order(attr, var)
+
+    @classmethod
+    def fetch_unrelated_order(cls, attr, var):
+        """class method used to control sort order when multiple entities of
+        this type are fetched to use in edition (eg propose them to create a
+        new relation on an edited entity).
+        """
+        if attr == 'modification_date':
+            return '%s DESC' % var
+        return None
+
     @classmethod
     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
                   settype=True, ordermethod='fetch_order'):
@@ -269,17 +288,17 @@
 
     def __init__(self, req, rset=None, row=None, col=0):
         AppObject.__init__(self, req, rset=rset, row=row, col=col)
-        dict.__init__(self)
-        self._related_cache = {}
+        self._cw_related_cache = {}
         if rset is not None:
             self.eid = rset[row][col]
         else:
             self.eid = None
-        self._is_saved = True
+        self._cw_is_saved = True
+        self.cw_attr_cache = {}
 
     def __repr__(self):
         return '<Entity %s %s %s at %s>' % (
-            self.e_schema, self.eid, self.keys(), id(self))
+            self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
 
     def __json_encode__(self):
         """custom json dumps hook to dump the entity's eid
@@ -298,12 +317,18 @@
     def __cmp__(self, other):
         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
 
+    def __contains__(self, key):
+        return key in self.cw_attr_cache
+
+    def __iter__(self):
+        return iter(self.cw_attr_cache)
+
     def __getitem__(self, key):
         if key == 'eid':
             warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
                  DeprecationWarning, stacklevel=2)
             return self.eid
-        return super(Entity, self).__getitem__(key)
+        return self.cw_attr_cache[key]
 
     def __setitem__(self, attr, value):
         """override __setitem__ to update self.edited_attributes.
@@ -321,13 +346,13 @@
                  DeprecationWarning, stacklevel=2)
             self.eid = value
         else:
-            super(Entity, self).__setitem__(attr, value)
+            self.cw_attr_cache[attr] = value
             # don't add attribute into skip_security if already in edited
             # attributes, else we may accidentaly skip a desired security check
             if hasattr(self, 'edited_attributes') and \
                    attr not in self.edited_attributes:
                 self.edited_attributes.add(attr)
-                self.skip_security_attributes.add(attr)
+                self._cw_skip_security_attributes.add(attr)
 
     def __delitem__(self, attr):
         """override __delitem__ to update self.edited_attributes on cleanup of
@@ -345,28 +370,35 @@
                 del self.entity['load_left']
 
         """
-        super(Entity, self).__delitem__(attr)
+        del self.cw_attr_cache[attr]
         if hasattr(self, 'edited_attributes'):
             self.edited_attributes.remove(attr)
 
+    def clear(self):
+        self.cw_attr_cache.clear()
+
+    def get(self, key, default=None):
+        return self.cw_attr_cache.get(key, default)
+
     def setdefault(self, attr, default):
         """override setdefault to update self.edited_attributes"""
-        super(Entity, self).setdefault(attr, default)
+        value = self.cw_attr_cache.setdefault(attr, default)
         # don't add attribute into skip_security if already in edited
         # attributes, else we may accidentaly skip a desired security check
         if hasattr(self, 'edited_attributes') and \
                attr not in self.edited_attributes:
             self.edited_attributes.add(attr)
-            self.skip_security_attributes.add(attr)
+            self._cw_skip_security_attributes.add(attr)
+        return value
 
     def pop(self, attr, default=_marker):
         """override pop to update self.edited_attributes on cleanup of
         undesired changes introduced in the entity's dict. See `__delitem__`
         """
         if default is _marker:
-            value = super(Entity, self).pop(attr)
+            value = self.cw_attr_cache.pop(attr)
         else:
-            value = super(Entity, self).pop(attr, default)
+            value = self.cw_attr_cache.pop(attr, default)
         if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
             self.edited_attributes.remove(attr)
         return value
@@ -377,27 +409,24 @@
         for attr, value in values.items():
             self[attr] = value # use self.__setitem__ implementation
 
-    def rql_set_value(self, attr, value):
-        """call by rql execution plan when some attribute is modified
-
-        don't use dict api in such case since we don't want attribute to be
-        added to skip_security_attributes.
-        """
-        super(Entity, self).__setitem__(attr, value)
+    def cw_adapt_to(self, interface):
+        """return an adapter the entity to the given interface name.
 
-    def pre_add_hook(self):
-        """hook called by the repository before doing anything to add the entity
-        (before_add entity hooks have not been called yet). This give the
-        occasion to do weird stuff such as autocast (File -> Image for instance).
-
-        This method must return the actual entity to be added.
+        return None if it can not be adapted.
         """
-        return self
+        try:
+            cache = self._cw_adapters_cache
+        except AttributeError:
+            self._cw_adapters_cache = cache = {}
+        try:
+            return cache[interface]
+        except KeyError:
+            adapter = self._cw.vreg['adapters'].select_or_none(
+                interface, self._cw, entity=self)
+            cache[interface] = adapter
+            return adapter
 
-    def set_eid(self, eid):
-        self.eid = eid
-
-    def has_eid(self):
+    def has_eid(self): # XXX cw_has_eid
         """return True if the entity has an attributed eid (False
         meaning that the entity has to be created
         """
@@ -407,38 +436,34 @@
         except (ValueError, TypeError):
             return False
 
-    def is_saved(self):
+    def cw_is_saved(self):
         """during entity creation, there is some time during which the entity
-        has an eid attributed though it's not saved (eg during before_add_entity
-        hooks). You can use this method to ensure the entity has an eid *and* is
-        saved in its source.
+        has an eid attributed though it's not saved (eg during
+        'before_add_entity' hooks). You can use this method to ensure the entity
+        has an eid *and* is saved in its source.
         """
-        return self.has_eid() and self._is_saved
+        return self.has_eid() and self._cw_is_saved
 
     @cached
-    def metainformation(self):
+    def cw_metainformation(self):
         res = dict(zip(('type', 'source', 'extid'), self._cw.describe(self.eid)))
         res['source'] = self._cw.source_defs()[res['source']]
         return res
 
-    def clear_local_perm_cache(self, action):
-        for rqlexpr in self.e_schema.get_rqlexprs(action):
-            self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
-
-    def check_perm(self, action):
+    def cw_check_perm(self, action):
         self.e_schema.check_perm(self._cw, action, eid=self.eid)
 
-    def has_perm(self, action):
+    def cw_has_perm(self, action):
         return self.e_schema.has_perm(self._cw, action, eid=self.eid)
 
-    def view(self, __vid, __registry='views', w=None, **kwargs):
+    def view(self, __vid, __registry='views', w=None, **kwargs): # XXX cw_view
         """shortcut to apply a view on this entity"""
         view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
                                                 row=self.cw_row, col=self.cw_col,
                                                 **kwargs)
         return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
 
-    def absolute_url(self, *args, **kwargs):
+    def absolute_url(self, *args, **kwargs): # XXX cw_url
         """return an absolute url to view this entity"""
         # use *args since we don't want first argument to be "anonymous" to
         # avoid potential clash with kwargs
@@ -451,7 +476,7 @@
         # the object for use in the relation is tricky
         # XXX search_state is web specific
         if getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
-            kwargs['base_url'] = self.metainformation()['source'].get('base-url')
+            kwargs['base_url'] = self.cw_metainformation()['source'].get('base-url')
         if method in (None, 'view'):
             try:
                 kwargs['_restpath'] = self.rest_path(kwargs.get('base_url'))
@@ -463,7 +488,7 @@
             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
         return self._cw.build_url(method, **kwargs)
 
-    def rest_path(self, use_ext_eid=False):
+    def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
         """returns a REST-like (relative) path for this entity"""
         mainattr, needcheck = self._rest_attr_info()
         etype = str(self.e_schema)
@@ -486,12 +511,12 @@
                     path += '/eid'
         if mainattr == 'eid':
             if use_ext_eid:
-                value = self.metainformation()['extid']
+                value = self.cw_metainformation()['extid']
             else:
                 value = self.eid
         return '%s/%s' % (path, self._cw.url_quote(value))
 
-    def attr_metadata(self, attr, metadata):
+    def cw_attr_metadata(self, attr, metadata):
         """return a metadata for an attribute (None if unspecified)"""
         value = getattr(self, '%s_%s' % (attr, metadata), None)
         if value is None and metadata == 'encoding':
@@ -499,7 +524,7 @@
         return value
 
     def printable_value(self, attr, value=_marker, attrtype=None,
-                        format='text/html', displaytime=True):
+                        format='text/html', displaytime=True): # XXX cw_printable_value
         """return a displayable value (i.e. unicode string) which may contains
         html tags
         """
@@ -518,16 +543,16 @@
             # description...
             if props.internationalizable:
                 value = self._cw._(value)
-            attrformat = self.attr_metadata(attr, 'format')
+            attrformat = self.cw_attr_metadata(attr, 'format')
             if attrformat:
-                return self.mtc_transform(value, attrformat, format,
-                                          self._cw.encoding)
+                return self._cw_mtc_transform(value, attrformat, format,
+                                              self._cw.encoding)
         elif attrtype == 'Bytes':
-            attrformat = self.attr_metadata(attr, 'format')
+            attrformat = self.cw_attr_metadata(attr, 'format')
             if attrformat:
-                encoding = self.attr_metadata(attr, 'encoding')
-                return self.mtc_transform(value.getvalue(), attrformat, format,
-                                          encoding)
+                encoding = self.cw_attr_metadata(attr, 'encoding')
+                return self._cw_mtc_transform(value.getvalue(), attrformat, format,
+                                              encoding)
             return u''
         value = printable_value(self._cw, attrtype, value, props,
                                 displaytime=displaytime)
@@ -535,8 +560,8 @@
             value = xml_escape(value)
         return value
 
-    def mtc_transform(self, data, format, target_format, encoding,
-                      _engine=ENGINE):
+    def _cw_mtc_transform(self, data, format, target_format, encoding,
+                          _engine=ENGINE):
         trdata = TransformData(data, format, encoding, appobject=self)
         data = _engine.convert(trdata, target_format).decode()
         if format == 'text/html':
@@ -545,7 +570,13 @@
 
     # entity cloning ##########################################################
 
-    def copy_relations(self, ceid):
+    def cw_copy(self):
+        thecopy = copy(self)
+        thecopy.cw_attr_cache = copy(self.cw_attr_cache)
+        thecopy._cw_related_cache = {}
+        return thecopy
+
+    def copy_relations(self, ceid): # XXX cw_copy_relations
         """copy relations of the object with the given eid on this
         object (this method is called on the newly created copy, and
         ceid designates the original entity).
@@ -574,7 +605,7 @@
             rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
                 rschema.type, rschema.type)
             execute(rql, {'x': self.eid, 'y': ceid})
-            self.clear_related_cache(rschema.type, 'subject')
+            self.cw_clear_relation_cache(rschema.type, 'subject')
         for rschema in self.e_schema.object_relations():
             if rschema.meta:
                 continue
@@ -592,36 +623,32 @@
             rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
                 rschema.type, rschema.type)
             execute(rql, {'x': self.eid, 'y': ceid})
-            self.clear_related_cache(rschema.type, 'object')
+            self.cw_clear_relation_cache(rschema.type, 'object')
 
     # data fetching methods ###################################################
 
     @cached
-    def as_rset(self):
+    def as_rset(self): # XXX .cw_as_rset
         """returns a resultset containing `self` information"""
         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
                          {'x': self.eid}, [(self.__regid__,)])
         rset.req = self._cw
         return rset
 
-    def to_complete_relations(self):
+    def _cw_to_complete_relations(self):
         """by default complete final relations to when calling .complete()"""
         for rschema in self.e_schema.subject_relations():
             if rschema.final:
                 continue
             targets = rschema.objects(self.e_schema)
-            if len(targets) > 1:
-                # ambigous relations, the querier doesn't handle
-                # outer join correctly in this case
-                continue
             if rschema.inlined:
                 matching_groups = self._cw.user.matching_groups
-                rdef = rschema.rdef(self.e_schema, targets[0])
-                if matching_groups(rdef.get_groups('read')) and \
-                   all(matching_groups(e.get_groups('read')) for e in targets):
+                if all(matching_groups(e.get_groups('read')) and
+                       rschema.rdef(self.e_schema, e).get_groups('read')
+                       for e in targets):
                     yield rschema, 'subject'
 
-    def to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
+    def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
         for rschema, attrschema in self.e_schema.attribute_definitions():
             # skip binary data by default
             if skip_bytes and attrschema.type == 'Bytes':
@@ -638,7 +665,7 @@
             yield attr
 
     _cw_completed = False
-    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True):
+    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
         """complete this entity by adding missing attributes (i.e. query the
         repository to fill the entity)
 
@@ -655,9 +682,9 @@
         V = varmaker.next()
         rql = ['WHERE %s eid %%(x)s' % V]
         selected = []
-        for attr in (attributes or self.to_complete_attributes(skip_bytes, skip_pwd)):
+        for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
             # if attribute already in entity, nothing to do
-            if self.has_key(attr):
+            if self.cw_attr_cache.has_key(attr):
                 continue
             # case where attribute must be completed, but is not yet in entity
             var = varmaker.next()
@@ -667,26 +694,20 @@
         lastattr = len(selected) + 1
         if attributes is None:
             # fetch additional relations (restricted to 0..1 relations)
-            for rschema, role in self.to_complete_relations():
+            for rschema, role in self._cw_to_complete_relations():
                 rtype = rschema.type
-                if self.relation_cached(rtype, role):
+                if self.cw_relation_cached(rtype, role):
                     continue
+                # at this point we suppose that:
+                # * this is a inlined relation
+                # * entity (self) is the subject
+                # * user has read perm on the relation and on the target entity
+                assert rschema.inlined
+                assert role == 'subject'
                 var = varmaker.next()
-                targettype = rschema.targets(self.e_schema, role)[0]
-                rdef = rschema.role_rdef(self.e_schema, targettype, role)
-                card = rdef.role_cardinality(role)
-                assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
-                                                      role, card)
-                if role == 'subject':
-                    if card == '1':
-                        rql.append('%s %s %s' % (V, rtype, var))
-                    else:
-                        rql.append('%s %s %s?' % (V, rtype, var))
-                else:
-                    if card == '1':
-                        rql.append('%s %s %s' % (var, rtype, V))
-                    else:
-                        rql.append('%s? %s %s' % (var, rtype, V))
+                # keep outer join anyway, we don't want .complete to crash on
+                # missing mandatory relation (see #1058267)
+                rql.append('%s %s %s?' % (V, rtype, var))
                 selected.append(((rtype, role), var))
         if selected:
             # select V, we need it as the left most selected variable
@@ -706,9 +727,9 @@
                     rrset.req = self._cw
                 else:
                     rrset = self._cw.eid_rset(value)
-                self.set_related_cache(rtype, role, rrset)
+                self.cw_set_relation_cache(rtype, role, rrset)
 
-    def get_value(self, name):
+    def cw_attr_value(self, name):
         """get value for the attribute relation <name>, query the repository
         to get the value if necessary.
 
@@ -716,9 +737,9 @@
         :param name: name of the attribute to get
         """
         try:
-            value = self[name]
+            value = self.cw_attr_cache[name]
         except KeyError:
-            if not self.is_saved():
+            if not self.cw_is_saved():
                 return None
             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
             try:
@@ -740,7 +761,7 @@
                         self[name] = value = None
         return value
 
-    def related(self, rtype, role='subject', limit=None, entities=False):
+    def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
         """returns a resultset of related entities
 
         :param role: is the role played by 'self' in the relation ('subject' or 'object')
@@ -748,19 +769,19 @@
         :param entities: if True, the entites are returned; if False, a result set is returned
         """
         try:
-            return self.related_cache(rtype, role, entities, limit)
+            return self._cw_relation_cache(rtype, role, entities, limit)
         except KeyError:
             pass
         if not self.has_eid():
             if entities:
                 return []
             return self.empty_rset()
-        rql = self.related_rql(rtype, role)
+        rql = self.cw_related_rql(rtype, role)
         rset = self._cw.execute(rql, {'x': self.eid})
-        self.set_related_cache(rtype, role, rset)
+        self.cw_set_relation_cache(rtype, role, rset)
         return self.related(rtype, role, limit, entities)
 
-    def related_rql(self, rtype, role='subject', targettypes=None):
+    def cw_related_rql(self, rtype, role='subject', targettypes=None):
         rschema = self._cw.vreg.schema[rtype]
         if role == 'subject':
             restriction = 'E eid %%(x)s, E %s X' % rtype
@@ -809,7 +830,7 @@
 
     # generic vocabulary methods ##############################################
 
-    def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
+    def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
                       vocabconstraints=True):
         """build a rql to fetch `targettype` entities unrelated to this entity
         using (rtype, role) relation.
@@ -871,12 +892,12 @@
         return rql, args
 
     def unrelated(self, rtype, targettype, role='subject', limit=None,
-                  ordermethod=None):
+                  ordermethod=None): # XXX .cw_unrelated
         """return a result set of target type objects that may be related
         by a given relation, with self as subject or object
         """
         try:
-            rql, args = self.unrelated_rql(rtype, targettype, role, ordermethod)
+            rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
         except Unauthorized:
             return self._cw.empty_rset()
         if limit is not None:
@@ -884,18 +905,19 @@
             rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
         return self._cw.execute(rql, args)
 
-    # relations cache handling ################################################
+    # relations cache handling #################################################
 
-    def relation_cached(self, rtype, role):
-        """return true if the given relation is already cached on the instance
+    def cw_relation_cached(self, rtype, role):
+        """return None if the given relation isn't already cached on the
+        instance, else the content of the cache (a 2-uple (rset, entities)).
         """
-        return self._related_cache.get('%s_%s' % (rtype, role))
+        return self._cw_related_cache.get('%s_%s' % (rtype, role))
 
-    def related_cache(self, rtype, role, entities=True, limit=None):
+    def _cw_relation_cache(self, rtype, role, entities=True, limit=None):
         """return values for the given relation if it's cached on the instance,
         else raise `KeyError`
         """
-        res = self._related_cache['%s_%s' % (rtype, role)][entities]
+        res = self._cw_related_cache['%s_%s' % (rtype, role)][entities]
         if limit is not None and limit < len(res):
             if entities:
                 res = res[:limit]
@@ -903,10 +925,10 @@
                 res = res.limit(limit)
         return res
 
-    def set_related_cache(self, rtype, role, rset, col=0):
+    def cw_set_relation_cache(self, rtype, role, rset):
         """set cached values for the given relation"""
         if rset:
-            related = list(rset.entities(col))
+            related = list(rset.entities(0))
             rschema = self._cw.vreg.schema.rschema(rtype)
             if role == 'subject':
                 rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
@@ -916,23 +938,24 @@
                 target = 'subject'
             if rcard in '?1':
                 for rentity in related:
-                    rentity._related_cache['%s_%s' % (rtype, target)] = (
+                    rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
                         self.as_rset(), (self,))
         else:
             related = ()
-        self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
+        self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
 
-    def clear_related_cache(self, rtype=None, role=None):
+    def cw_clear_relation_cache(self, rtype=None, role=None):
         """clear cached values for the given relation or the entire cache if
         no relation is given
         """
         if rtype is None:
-            self._related_cache = {}
+            self._cw_related_cache = {}
+            self._cw_adapters_cache = {}
         else:
             assert role
-            self._related_cache.pop('%s_%s' % (rtype, role), None)
+            self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
 
-    def clear_all_caches(self):
+    def clear_all_caches(self): # XXX cw_clear_all_caches
         """flush all caches on this entity. Further attributes/relations access
         will triggers new database queries to get back values.
 
@@ -942,10 +965,9 @@
         # clear attributes cache
         haseid = 'eid' in self
         self._cw_completed = False
-        self.clear()
+        self.cw_attr_cache.clear()
         # clear relations cache
-        for rschema, _, role in self.e_schema.relation_definitions():
-            self.clear_related_cache(rschema.type, role)
+        self.cw_clear_relation_cache()
         # rest path unique cache
         try:
             del self.__unique
@@ -954,10 +976,10 @@
 
     # raw edition utilities ###################################################
 
-    def set_attributes(self, **kwargs):
+    def set_attributes(self, **kwargs): # XXX cw_set_attributes
         _check_cw_unsafe(kwargs)
         assert kwargs
-        assert self._is_saved, "should not call set_attributes while entity "\
+        assert self.cw_is_saved(), "should not call set_attributes while entity "\
                "hasn't been saved yet"
         relations = []
         for key in kwargs:
@@ -972,7 +994,7 @@
         # edited_attributes / skip_security_attributes machinery
         self.update(kwargs)
 
-    def set_relations(self, **kwargs):
+    def set_relations(self, **kwargs): # XXX cw_set_relations
         """add relations to the given object. To set a relation where this entity
         is the object of the relation, use 'reverse_'<relation> as argument name.
 
@@ -996,28 +1018,42 @@
                 restr, ','.join(str(r.eid) for r in values)),
                              {'x': self.eid})
 
-    def delete(self, **kwargs):
+    def cw_delete(self, **kwargs):
         assert self.has_eid(), self.eid
         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
                          {'x': self.eid}, **kwargs)
 
     # server side utilities ###################################################
 
+    def _cw_rql_set_value(self, attr, value):
+        """call by rql execution plan when some attribute is modified
+
+        don't use dict api in such case since we don't want attribute to be
+        added to skip_security_attributes.
+
+        This method is for internal use, you should not use it.
+        """
+        self.cw_attr_cache[attr] = value
+
+    def _cw_clear_local_perm_cache(self, action):
+        for rqlexpr in self.e_schema.get_rqlexprs(action):
+            self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
+
     @property
-    def skip_security_attributes(self):
+    def _cw_skip_security_attributes(self):
         try:
-            return self._skip_security_attributes
+            return self.__cw_skip_security_attributes
         except:
-            self._skip_security_attributes = set()
-            return self._skip_security_attributes
+            self.__cw_skip_security_attributes = set()
+            return self.__cw_skip_security_attributes
 
-    def set_defaults(self):
+    def _cw_set_defaults(self):
         """set default values according to the schema"""
         for attr, value in self.e_schema.defaults():
-            if not self.has_key(attr):
+            if not self.cw_attr_cache.has_key(attr):
                 self[str(attr)] = value
 
-    def check(self, creation=False):
+    def _cw_check(self, creation=False):
         """check this entity against its schema. Only final relation
         are checked here, constraint on actual relations are checked in hooks
         """
@@ -1040,60 +1076,33 @@
         self.e_schema.check(self, creation=creation, _=_,
                             relations=relations)
 
-    def fti_containers(self, _done=None):
-        if _done is None:
-            _done = set()
-        _done.add(self.eid)
-        containers = tuple(self.e_schema.fulltext_containers())
-        if containers:
-            for rschema, target in containers:
-                if target == 'object':
-                    targets = getattr(self, rschema.type)
-                else:
-                    targets = getattr(self, 'reverse_%s' % rschema)
-                for entity in targets:
-                    if entity.eid in _done:
-                        continue
-                    for container in entity.fti_containers(_done):
-                        yield container
-                        yielded = True
-        else:
-            yield self
+    @deprecated('[3.9] use entity.cw_attr_value(attr)')
+    def get_value(self, name):
+        return self.cw_attr_value(name)
 
-    def get_words(self):
-        """used by the full text indexer to get words to index
+    @deprecated('[3.9] use entity.cw_delete()')
+    def delete(self, **kwargs):
+        return self.cw_delete(**kwargs)
 
-        this method should only be used on the repository side since it depends
-        on the logilab.database package
+    @deprecated('[3.9] use entity.cw_attr_metadata(attr, metadata)')
+    def attr_metadata(self, attr, metadata):
+        return self.cw_attr_metadata(attr, metadata)
 
-        :rtype: list
-        :return: the list of indexable word of this entity
-        """
-        from logilab.database.fti import tokenize
-        # take care to cases where we're modyfying the schema
-        pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
-        words = []
-        for rschema in self.e_schema.indexable_attributes():
-            if (self.e_schema, rschema) in pending:
-                continue
-            try:
-                value = self.printable_value(rschema, format='text/plain')
-            except TransformError:
-                continue
-            except:
-                self.exception("can't add value of %s to text index for entity %s",
-                               rschema, self.eid)
-                continue
-            if value:
-                words += tokenize(value)
-        for rschema, role in self.e_schema.fulltext_relations():
-            if role == 'subject':
-                for entity in getattr(self, rschema.type):
-                    words += entity.get_words()
-            else: # if role == 'object':
-                for entity in getattr(self, 'reverse_%s' % rschema.type):
-                    words += entity.get_words()
-        return words
+    @deprecated('[3.9] use entity.cw_has_perm(action)')
+    def has_perm(self, action):
+        return self.cw_has_perm(action)
+
+    @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)')
+    def set_related_cache(self, rtype, role, rset):
+        self.cw_set_relation_cache(rtype, role, rset)
+
+    @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role, rset)')
+    def clear_related_cache(self, rtype=None, role=None):
+        self.cw_clear_relation_cache(rtype, role)
+
+    @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
+    def related_rql(self, rtype, role='subject', targettypes=None):
+        return self.cw_related_rql(rtype, role, targettypes)
 
 
 # attribute and relation descriptors ##########################################
@@ -1108,18 +1117,18 @@
     def __get__(self, eobj, eclass):
         if eobj is None:
             return self
-        return eobj.get_value(self._attrname)
+        return eobj.cw_attr_value(self._attrname)
 
     def __set__(self, eobj, value):
         eobj[self._attrname] = value
 
+
 class Relation(object):
     """descriptor that controls schema relation access"""
-    _role = None # for pylint
 
-    def __init__(self, rschema):
-        self._rschema = rschema
+    def __init__(self, rschema, role):
         self._rtype = rschema.type
+        self._role = role
 
     def __get__(self, eobj, eclass):
         if eobj is None:
@@ -1131,14 +1140,6 @@
         raise NotImplementedError
 
 
-class SubjectRelation(Relation):
-    """descriptor that controls schema relation access"""
-    _role = 'subject'
-
-class ObjectRelation(Relation):
-    """descriptor that controls schema relation access"""
-    _role = 'object'
-
 from logging import getLogger
 from cubicweb import set_log_methods
 set_log_methods(Entity, getLogger('cubicweb.entity'))
--- a/etwist/request.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/etwist/request.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Twisted request handler for CubicWeb
+"""Twisted request handler for CubicWeb"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from datetime import datetime
@@ -55,9 +54,9 @@
         return self._twreq.method
 
     def relative_path(self, includeparams=True):
-        """return the normalized path of the request (ie at least relative
-        to the instance's root, but some other normalization may be needed
-        so that the returned path may be used to compare to generated urls
+        """return the normalized path of the request (ie at least relative to
+        the instance's root, but some other normalization may be needed so that
+        the returned path may be used to compare to generated urls
 
         :param includeparams:
            boolean indicating if GET form parameters should be kept in the path
@@ -68,8 +67,8 @@
         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
+        """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]
--- a/etwist/server.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/etwist/server.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""twisted server for CubicWeb web instances
+"""twisted server for CubicWeb web instances"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -39,11 +38,11 @@
 from twisted.web import static, resource
 from twisted.web.server import NOT_DONE_YET
 
-from cubicweb.web import dumps
 
 from logilab.common.decorators import monkeypatch
 
 from cubicweb import AuthenticationError, ConfigurationError, CW_EVENT_MANAGER
+from cubicweb.utils import json_dumps
 from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.web.http_headers import generateDateTime
@@ -99,12 +98,11 @@
 
 
 class CubicWebRootResource(resource.Resource):
-    def __init__(self, config, debug=None):
-        self.debugmode = debug
+    def __init__(self, config, vreg=None):
         self.config = config
         # instantiate publisher here and not in init_publisher to get some
         # checks done before daemonization (eg versions consistency)
-        self.appli = CubicWebPublisher(config, debug=self.debugmode)
+        self.appli = CubicWebPublisher(config, vreg=vreg)
         self.base_url = config['base-url']
         self.https_url = config['https-url']
         self.children = {}
@@ -118,8 +116,6 @@
         # when we have an in-memory repository, clean unused sessions every XX
         # seconds and properly shutdown the server
         if config.repo_method == 'inmemory':
-            reactor.addSystemEventTrigger('before', 'shutdown',
-                                          self.shutdown_event)
             if config.pyro_enabled():
                 # if pyro is enabled, we have to register to the pyro name
                 # server, create a pyro daemon, and create a task to handle pyro
@@ -127,7 +123,10 @@
                 self.pyro_daemon = self.appli.repo.pyro_register()
                 self.pyro_listen_timeout = 0.02
                 self.appli.repo.looping_task(1, self.pyro_loop_event)
-            self.appli.repo.start_looping_tasks()
+            if config.mode != 'test':
+                reactor.addSystemEventTrigger('before', 'shutdown',
+                                              self.shutdown_event)
+                self.appli.repo.start_looping_tasks()
         self.set_url_rewriter()
         CW_EVENT_MANAGER.bind('after-registry-reload', self.set_url_rewriter)
 
@@ -156,6 +155,9 @@
         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
         directory = pre_path[0]
         # Anything in data/, static/, fckeditor/ and the generated versioned
         # data directory is treated as static files
@@ -165,7 +167,7 @@
             if directory == 'static':
                 return File(self.config.static_directory)
             if directory == 'fckeditor':
-                return File(self.config.ext_resources['FCKEDITOR_PATH'])
+                return File(uiprops['FCKEDITOR_PATH'])
             if directory != 'data':
                 # versioned directory, use specific file with http cache
                 # headers so their are cached for a very long time
@@ -173,10 +175,10 @@
             else:
                 cls = File
             if path == 'fckeditor':
-                return cls(self.config.ext_resources['FCKEDITOR_PATH'])
+                return cls(uiprops['FCKEDITOR_PATH'])
             if path == directory: # recurse
                 return self
-            datadir = self.config.locate_resource(path)
+            datadir, path = self.config.locate_resource(path)
             if datadir is None:
                 return self # recurse
             self.debug('static file %s from %s', path, datadir)
@@ -187,7 +189,10 @@
     def render(self, request):
         """Render a page from the root resource"""
         # reload modified files in debug mode
-        if self.debugmode:
+        if self.config.debugmode:
+            self.config.uiprops.reload_if_needed()
+            if self.https_url:
+                self.config.https_uiprops.reload_if_needed()
             self.appli.vreg.reload_if_needed()
         if self.config['profile']: # default profiler don't trace threads
             return self.render_request(request)
@@ -312,12 +317,12 @@
         self.setResponseCode(http.BAD_REQUEST)
         if path in JSON_PATHS: # XXX better json path detection
             self.setHeader('content-type',"application/json")
-            body = dumps({'reason': 'request max size exceeded'})
+            body = json_dumps({'reason': 'request max size exceeded'})
         elif path in FRAME_POST_PATHS: # XXX better frame post path detection
             self.setHeader('content-type',"text/html")
             body = ('<script type="text/javascript">'
                     'window.parent.handleFormValidationResponse(null, null, null, %s, null);'
-                    '</script>' % dumps( (False, 'request max size exceeded', None) ))
+                    '</script>' % json_dumps( (False, 'request max size exceeded', None) ))
         else:
             self.setHeader('content-type',"text/html")
             body = ("<html><head><title>Processing Failed</title></head><body>"
@@ -394,20 +399,22 @@
 LOGGER = getLogger('cubicweb.twisted')
 set_log_methods(CubicWebRootResource, LOGGER)
 
-def run(config, debug):
+def run(config, vreg=None, debug=None):
+    if debug is not None:
+        config.debugmode = debug
+    config.check_writeable_uid_directory(config.appdatahome)
     # create the site
-    root_resource = CubicWebRootResource(config, debug)
+    root_resource = CubicWebRootResource(config, vreg=vreg)
     website = server.Site(root_resource)
     # serve it via standard HTTP on port set in the configuration
     port = config['port'] or 8080
     reactor.listenTCP(port, website)
-    logger = getLogger('cubicweb.twisted')
-    if not debug:
+    if not config.debugmode:
         if sys.platform == 'win32':
             raise ConfigurationError("Under windows, you must use the service management "
                                      "commands (e.g : 'net start my_instance)'")
         from logilab.common.daemon import daemonize
-        print 'instance starting in the background'
+        LOGGER.info('instance started in the background on %s', root_resource.base_url)
         if daemonize(config['pid-file']):
             return # child process
     root_resource.init_publisher() # before changing uid
@@ -419,7 +426,7 @@
             uid = getpwnam(config['uid']).pw_uid
         os.setuid(uid)
     root_resource.start_service()
-    logger.info('instance started on %s', root_resource.base_url)
+    LOGGER.info('instance started on %s', root_resource.base_url)
     # avoid annoying warnign if not in Main Thread
     signals = threading.currentThread().getName() == 'MainThread'
     if config['profile']:
--- a/etwist/twctl.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/etwist/twctl.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +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/>.
-"""cubicweb-clt handlers for twisted
-
-"""
+"""cubicweb-clt handlers for twisted"""
 
 from cubicweb.toolsutils import CommandHandler
 from cubicweb.web.webctl import WebCreateHandler
@@ -32,9 +30,9 @@
     cmdname = 'start'
     cfgname = 'twisted'
 
-    def start_server(self, config, debug):
+    def start_server(self, config):
         from cubicweb.etwist import server
-        server.run(config, debug)
+        server.run(config)
 
 class TWStopHandler(CommandHandler):
     cmdname = 'stop'
--- a/goa/appobjects/components.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/goa/appobjects/components.py	Mon Jul 19 15:37:02 2010 +0200
@@ -98,7 +98,7 @@
 def sendmail(self, recipient, subject, body):
     sender = '%s <%s>' % (
         self.req.user.dc_title() or self.config['sender-name'],
-        self.req.user.get_email() or self.config['sender-addr'])
+        self.req.user.cw_adapt_to('IEmailable').get_email() or self.config['sender-addr'])
     mail.send_mail(sender=sender, to=recipient,
                    subject=subject, body=body)
 
--- a/goa/db.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/goa/db.py	Mon Jul 19 15:37:02 2010 +0200
@@ -233,7 +233,7 @@
                 return self.req.datastore_get(self.eid)
             except AttributeError: # self.req is not a server session
                 return Get(self.eid)
-        self.set_defaults()
+        self._cw_set_defaults()
         values = self._to_gae_dict(convert=False)
         parent = key_name = _app = None
         if self._gaeinitargs is not None:
@@ -343,7 +343,7 @@
             self.req = req
         dbmodel = self.to_gae_model()
         key = Put(dbmodel)
-        self.set_eid(str(key))
+        self.eid = str(key)
         if self.req is not None and self.rset is None:
             self.rset = rset_from_objs(self.req, dbmodel, ('eid',),
                                        'Any X WHERE X eid %(x)s', {'x': self.eid})
@@ -409,7 +409,7 @@
     def dynamic_properties(self):
         raise NotImplementedError('use eschema')
 
-    def is_saved(self):
+    def cw_is_saved(self):
         return self.has_eid()
 
     def parent(self):
--- a/goa/gaesource.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/goa/gaesource.py	Mon Jul 19 15:37:02 2010 +0200
@@ -49,15 +49,15 @@
         except KeyError:
             pass
         else:
-            entity.clear_related_cache(rtype, role)
+            entity.cw_clear_relation_cache(rtype, role)
     if gaesubject.kind() == 'CWUser':
         for asession in session.repo._sessions.itervalues():
             if asession.user.eid == subject:
-                asession.user.clear_related_cache(rtype, 'subject')
+                asession.user.cw_clear_relation_cache(rtype, 'subject')
     if gaeobject.kind() == 'CWUser':
         for asession in session.repo._sessions.itervalues():
             if asession.user.eid == object:
-                asession.user.clear_related_cache(rtype, 'object')
+                asession.user.cw_clear_relation_cache(rtype, 'object')
 
 def _mark_modified(session, gaeentity):
     modified = session.transaction_data.setdefault('modifiedentities', {})
--- a/goa/skel/loader.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/goa/skel/loader.py	Mon Jul 19 15:37:02 2010 +0200
@@ -30,7 +30,7 @@
     # apply monkey patches first
     goa.do_monkey_patch()
     # get instance's configuration (will be loaded from app.conf file)
-    GAEConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
+    GAEConfiguration.uiprops['JAVASCRIPTS'].append('DATADIR/goa.js')
     config = GAEConfiguration('toto', APPLROOT)
     # create default groups
     create_groups()
--- a/goa/skel/main.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/goa/skel/main.py	Mon Jul 19 15:37:02 2010 +0200
@@ -31,7 +31,7 @@
 
 # get instance's configuration (will be loaded from app.conf file)
 from cubicweb.goa.goaconfig import GAEConfiguration
-GAEConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
+GAEConfiguration.uiprops['JAVASCRIPTS'].append('DATADIR/goa.js')
 config = GAEConfiguration('toto', APPLROOT)
 
 # dynamic objects registry
--- a/goa/test/unittest_rql.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/goa/test/unittest_rql.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
 from cubicweb.goa.testlib import *
 
 from cubicweb import Binary
@@ -612,7 +609,7 @@
     def test_error_unknown_eid(self):
         rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': '1234'})
         self.assertEquals(len(rset), 0)
-        self.blog.delete()
+        self.blog.cw_delete()
         rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': self.blog.eid})
         self.assertEquals(len(rset), 0)
 
--- a/goa/tools/laxctl.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/goa/tools/laxctl.py	Mon Jul 19 15:37:02 2010 +0200
@@ -43,7 +43,7 @@
     do_monkey_patch()
     from cubicweb.goa.goavreg import GAEVregistry
     from cubicweb.goa.goaconfig import GAEConfiguration
-    #WebConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
+    #WebConfiguration.uiprops['JAVASCRIPTS'].append('DATADIR/goa.js')
     config = GAEConfiguration('toto', applroot)
     vreg = GAEVregistry(config)
     vreg.set_schema(config.load_schema())
--- a/hooks/bookmark.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/bookmark.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""bookmark related hooks
+"""bookmark related hooks"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.server import hook
@@ -28,7 +27,7 @@
     def precommit_event(self):
         if not self.session.deleted_in_transaction(self.bookmark.eid):
             if not self.bookmark.bookmarked_by:
-                self.bookmark.delete()
+                self.bookmark.cw_delete()
 
 
 class DelBookmarkedByHook(hook.Hook):
--- a/hooks/integrity.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/integrity.py	Mon Jul 19 15:37:02 2010 +0200
@@ -27,7 +27,7 @@
 
 from cubicweb import ValidationError
 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.uilib import soup2xhtml
 from cubicweb.server import hook
 from cubicweb.server.hook import set_operation
@@ -253,7 +253,7 @@
     """delete the composed of a composite relation when this relation is deleted
     """
     __regid__ = 'checkownersgroup'
-    __select__ = IntegrityHook.__select__ & implements('CWGroup')
+    __select__ = IntegrityHook.__select__ & is_instance('CWGroup')
     events = ('before_delete_entity', 'before_update_entity')
 
     def __call__(self):
@@ -293,7 +293,7 @@
 class StripCWUserLoginHook(IntegrityHook):
     """ensure user logins are stripped"""
     __regid__ = 'stripuserlogin'
-    __select__ = IntegrityHook.__select__ & implements('CWUser')
+    __select__ = IntegrityHook.__select__ & is_instance('CWUser')
     events = ('before_add_entity', 'before_update_entity',)
 
     def __call__(self):
--- a/hooks/metadata.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/metadata.py	Mon Jul 19 15:37:02 2010 +0200
@@ -21,7 +21,7 @@
 
 from datetime import datetime
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server import hook
 from cubicweb.server.utils import eschema_eid
 
@@ -140,7 +140,7 @@
 class FixUserOwnershipHook(MetaDataHook):
     """when a user has been created, add owned_by relation on itself"""
     __regid__ = 'fixuserowner'
-    __select__ = MetaDataHook.__select__ & implements('CWUser')
+    __select__ = MetaDataHook.__select__ & is_instance('CWUser')
     events = ('after_add_entity',)
 
     def __call__(self):
--- a/hooks/notification.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/notification.py	Mon Jul 19 15:37:02 2010 +0200
@@ -22,7 +22,7 @@
 
 from logilab.common.textutils import normalize_text
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server import hook
 from cubicweb.sobjects.supervising import SupervisionMailOp
 
@@ -49,7 +49,7 @@
 class StatusChangeHook(NotificationHook):
     """notify when a workflowable entity has its state modified"""
     __regid__ = 'notifystatuschange'
-    __select__ = NotificationHook.__select__ & implements('TrInfo')
+    __select__ = NotificationHook.__select__ & is_instance('TrInfo')
     events = ('after_add_entity',)
 
     def __call__(self):
--- a/hooks/security.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/security.py	Mon Jul 19 15:37:02 2010 +0200
@@ -29,9 +29,9 @@
 def check_entity_attributes(session, entity, editedattrs=None, creation=False):
     eid = entity.eid
     eschema = entity.e_schema
-    # .skip_security_attributes is there to bypass security for attributes
+    # ._cw_skip_security_attributes is there to bypass security for attributes
     # set by hooks by modifying the entity's dictionnary
-    dontcheck = entity.skip_security_attributes
+    dontcheck = entity._cw_skip_security_attributes
     if editedattrs is None:
         try:
             editedattrs = entity.edited_attributes
@@ -59,7 +59,7 @@
         for values in session.transaction_data.pop('check_entity_perm_op'):
             entity = session.entity_from_eid(values[0])
             action = values[1]
-            entity.check_perm(action)
+            entity.cw_check_perm(action)
             check_entity_attributes(session, entity, values[2:],
                                     creation=self.creation)
 
@@ -110,10 +110,10 @@
     def __call__(self):
         try:
             # check user has permission right now, if not retry at commit time
-            self.entity.check_perm('update')
+            self.entity.cw_check_perm('update')
             check_entity_attributes(self._cw, self.entity)
         except Unauthorized:
-            self.entity.clear_local_perm_cache('update')
+            self.entity._cw_clear_local_perm_cache('update')
             # save back editedattrs in case the entity is reedited later in the
             # same transaction, which will lead to edited_attributes being
             # overwritten
@@ -127,7 +127,7 @@
     events = ('before_delete_entity',)
 
     def __call__(self):
-        self.entity.check_perm('delete')
+        self.entity.cw_check_perm('delete')
 
 
 class BeforeAddRelationSecurityHook(SecurityHook):
--- a/hooks/syncschema.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/syncschema.py	Mon Jul 19 15:37:02 2010 +0200
@@ -33,8 +33,9 @@
 from logilab.common.testlib import mock_object
 
 from cubicweb import ValidationError
-from cubicweb.selectors import implements
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS, display_name
+from cubicweb.selectors import is_instance
+from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS,
+                             ETYPE_NAME_MAP, display_name)
 from cubicweb.server import hook, schemaserial as ss
 from cubicweb.server.sqlutils import SQL_PREFIX
 
@@ -80,6 +81,11 @@
 
 def add_inline_relation_column(session, etype, rtype):
     """add necessary column and index for an inlined relation"""
+    attrkey = '%s.%s' % (etype, rtype)
+    createdattrs = session.transaction_data.setdefault('createdattrs', set())
+    if attrkey in createdattrs:
+        return
+    createdattrs.add(attrkey)
     table = SQL_PREFIX + etype
     column = SQL_PREFIX + rtype
     try:
@@ -96,8 +102,6 @@
     # is done by the dbhelper)
     session.pool.source('system').create_index(session, table, column)
     session.info('added index on %s(%s)', table, column)
-    session.transaction_data.setdefault('createdattrs', []).append(
-        '%s.%s' % (etype, rtype))
 
 
 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
@@ -115,6 +119,14 @@
         raise ValidationError(entity.eid, errors)
 
 
+class SyncSchemaHook(hook.Hook):
+    """abstract class for schema synchronization hooks (in the `syncschema`
+    category)
+    """
+    __abstract__ = True
+    category = 'syncschema'
+
+
 # operations for low-level database alteration  ################################
 
 class DropTable(hook.Operation):
@@ -129,6 +141,8 @@
         self.session.system_sql('DROP TABLE %s' % self.table)
         self.info('dropped table %s', self.table)
 
+    # XXX revertprecommit_event
+
 
 class DropRelationTable(DropTable):
     def __init__(self, session, rtype):
@@ -156,6 +170,8 @@
             self.error('dropping column not supported by the backend, handle '
                        'it yourself (%s.%s)', table, column)
 
+    # XXX revertprecommit_event
+
 
 # base operations for in-memory schema synchronization  ########################
 
@@ -175,7 +191,7 @@
             if not eschema.final:
                 clear_cache(eschema, 'ordered_relations')
 
-    def commit_event(self):
+    def postcommit_event(self):
         rebuildinfered = self.session.data.get('rebuild-infered', True)
         repo = self.session.repo
         # commit event should not raise error, while set_schema has chances to
@@ -195,60 +211,88 @@
 
 class MemSchemaOperation(hook.Operation):
     """base class for schema operations"""
-    def __init__(self, session, kobj=None, **kwargs):
-        self.kobj = kobj
-        # once Operation.__init__ has been called, event may be triggered, so
-        # do this last !
+    def __init__(self, session, **kwargs):
         hook.Operation.__init__(self, session, **kwargs)
         # every schema operation is triggering a schema update
         MemSchemaNotifyChanges(session)
 
-    def prepare_constraints(self, rdef):
-        # if constraints is already a list, reuse it (we're updating multiple
-        # constraints of the same rdef in the same transactions)
-        if not isinstance(rdef.constraints, list):
-            rdef.constraints = list(rdef.constraints)
-        self.constraints = rdef.constraints
-
-
-class MemSchemaEarlyOperation(MemSchemaOperation):
-    def insert_index(self):
-        """schema operation which are inserted at the begining of the queue
-        (typically to add/remove entity or relation types)
-        """
-        i = -1
-        for i, op in enumerate(self.session.pending_operations):
-            if not isinstance(op, MemSchemaEarlyOperation):
-                return i
-        return i + 1
-
 
 # operations for high-level source database alteration  ########################
 
-class SourceDbCWETypeRename(hook.Operation):
+class CWETypeAddOp(MemSchemaOperation):
+    """after adding a CWEType entity:
+    * add it to the instance's schema
+    * 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
+    """
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        schema = session.vreg.schema
+        etype = ybo.EntityType(eid=entity.eid, name=entity.name,
+                               description=entity.description)
+        eschema = schema.add_entity_type(etype)
+        # create the necessary table
+        tablesql = y2sql.eschema2sql(session.pool.source('system').dbhelper,
+                                     eschema, prefix=SQL_PREFIX)
+        for sql in tablesql.split(';'):
+            if sql.strip():
+                session.system_sql(sql)
+        # add meta relations
+        gmap = group_mapping(session)
+        cmap = ss.cstrtype_mapping(session)
+        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
+            rschema = schema[rtype]
+            sampletype = rschema.subjects()[0]
+            desttype = rschema.objects()[0]
+            rdef = copy(rschema.rdef(sampletype, desttype))
+            rdef.subject = mock_object(eid=entity.eid)
+            mock = mock_object(eid=None)
+            ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.session.vreg.schema.del_entity_type(self.entity.name)
+        # revert changes on database
+        self.session.system_sql('DROP TABLE %s%s' % (SQL_PREFIX, self.entity.name))
+
+
+class CWETypeRenameOp(MemSchemaOperation):
     """this operation updates physical storage accordingly"""
     oldname = newname = None # make pylint happy
 
-    def precommit_event(self):
+    def rename(self, oldname, newname):
+        self.session.vreg.schema.rename_entity_type(oldname, newname)
         # we need sql to operate physical changes on the system database
         sqlexec = self.session.system_sql
-        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
-                                                     SQL_PREFIX, self.newname))
-        self.info('renamed table %s to %s', self.oldname, self.newname)
+        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, oldname,
+                                                     SQL_PREFIX, newname))
+        self.info('renamed table %s to %s', oldname, newname)
         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
+                (newname, oldname))
         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
+                (newname, oldname))
+        # XXX transaction records
+
+    def precommit_event(self):
+        self.rename(self.oldname, self.newname)
+
+    def revertprecommit_event(self):
+        self.rename(self.newname, self.oldname)
 
 
-class SourceDbCWRTypeUpdate(hook.Operation):
+class CWRTypeUpdateOp(MemSchemaOperation):
     """actually update some properties of a relation definition"""
     rschema = entity = values = None # make pylint happy
+    oldvalus = None
 
     def precommit_event(self):
         rschema = self.rschema
         if rschema.final:
-            return
+            return # watched changes to final relation type are unexpected
         session = self.session
         if 'fulltext_container' in self.values:
             for subjtype, objtype in rschema.rdefs:
@@ -256,10 +300,14 @@
                                    UpdateFTIndexOp)
                 hook.set_operation(session, 'fti_update_etypes', objtype,
                                    UpdateFTIndexOp)
+        # update the in-memory schema first
+        self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
+        self.rschema.__dict__.update(self.values)
+        # then make necessary changes to the system source database
         if not 'inlined' in self.values:
             return # nothing to do
         inlined = self.values['inlined']
-        # check in-lining is necessary / possible
+        # check in-lining is possible when inlined
         if inlined:
             self.entity.check_inlined_allowed()
         # inlined changed, make necessary physical changes!
@@ -295,7 +343,7 @@
                 except Exception, ex:
                     # the column probably already exists. this occurs when the
                     # entity's type has just been added or if the column has not
-                    # been previously dropped
+                    # been previously dropped (eg sqlite)
                     self.error('error while altering table %s: %s', etype, ex)
                 # copy existant data.
                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
@@ -315,8 +363,13 @@
                 # drop existant table
                 DropRelationTable(session, rtype)
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.rschema.__dict__.update(self.oldvalues)
+        # XXX revert changes on database
 
-class SourceDbCWAttributeAdd(hook.Operation):
+
+class CWAttributeAddOp(MemSchemaOperation):
     """an attribute relation (CWAttribute) has been added:
     * add the necessary column
     * set default on this column if any and possible
@@ -330,24 +383,18 @@
     def init_rdef(self, **kwargs):
         entity = self.entity
         fromentity = entity.stype
+        rdefdef = self.rdefdef = ybo.RelationDefinition(
+            str(fromentity.name), entity.rtype.name, str(entity.otype.name),
+            description=entity.description, cardinality=entity.cardinality,
+            constraints=get_constraints(self.session, entity),
+            order=entity.ordernum, eid=entity.eid, **kwargs)
+        self.session.vreg.schema.add_relation_def(rdefdef)
         self.session.execute('SET X ordernum Y+1 '
                              'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
                              'X ordernum >= %(order)s, NOT X eid %(x)s',
                              {'x': entity.eid, 'se': fromentity.eid,
                               'order': entity.ordernum or 0})
-        subj = str(fromentity.name)
-        rtype = entity.rtype.name
-        obj = str(entity.otype.name)
-        constraints = get_constraints(self.session, entity)
-        rdef = ybo.RelationDefinition(subj, rtype, obj,
-                                      description=entity.description,
-                                      cardinality=entity.cardinality,
-                                      constraints=constraints,
-                                      order=entity.ordernum,
-                                      eid=entity.eid,
-                                      **kwargs)
-        MemSchemaRDefAdd(self.session, rdef)
-        return rdef
+        return rdefdef
 
     def precommit_event(self):
         session = self.session
@@ -361,22 +408,24 @@
                  'indexed': entity.indexed,
                  'fulltextindexed': entity.fulltextindexed,
                  'internationalizable': entity.internationalizable}
-        rdef = self.init_rdef(**props)
-        sysource = session.pool.source('system')
+        # update the in-memory schema first
+        rdefdef = self.init_rdef(**props)
+        # then make necessary changes to the system source database
+        syssource = session.pool.source('system')
         attrtype = y2sql.type_from_constraints(
-            sysource.dbhelper, rdef.object, rdef.constraints)
+            syssource.dbhelper, rdefdef.object, rdefdef.constraints)
         # XXX should be moved somehow into lgdb: sqlite doesn't support to
         # add a new column with UNIQUE, it should be added after the ALTER TABLE
         # using ADD INDEX
-        if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
+        if syssource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
             extra_unique_index = True
             attrtype = attrtype.replace(' UNIQUE', '')
         else:
             extra_unique_index = False
         # added some str() wrapping query since some backend (eg psycopg) don't
         # allow unicode queries
-        table = SQL_PREFIX + rdef.subject
-        column = SQL_PREFIX + rdef.name
+        table = SQL_PREFIX + rdefdef.subject
+        column = SQL_PREFIX + rdefdef.name
         try:
             session.system_sql(str('ALTER TABLE %s ADD %s %s'
                                    % (table, column, attrtype)),
@@ -389,7 +438,7 @@
             self.error('error while altering table %s: %s', table, ex)
         if extra_unique_index or entity.indexed:
             try:
-                sysource.create_index(session, table, column,
+                syssource.create_index(session, table, column,
                                       unique=extra_unique_index)
             except Exception, ex:
                 self.error('error while creating index for %s.%s: %s',
@@ -397,28 +446,28 @@
         # final relations are not infered, propagate
         schema = session.vreg.schema
         try:
-            eschema = schema.eschema(rdef.subject)
+            eschema = schema.eschema(rdefdef.subject)
         except KeyError:
             return # entity type currently being added
         # propagate attribute to children classes
-        rschema = schema.rschema(rdef.name)
+        rschema = schema.rschema(rdefdef.name)
         # if relation type has been inserted in the same transaction, its final
         # attribute is still set to False, so we've to ensure it's False
         rschema.final = True
         # XXX 'infered': True/False, not clear actually
-        props.update({'constraints': rdef.constraints,
-                      'description': rdef.description,
-                      'cardinality': rdef.cardinality,
-                      'constraints': rdef.constraints,
-                      'permissions': rdef.get_permissions(),
-                      'order': rdef.order,
+        props.update({'constraints': rdefdef.constraints,
+                      'description': rdefdef.description,
+                      'cardinality': rdefdef.cardinality,
+                      'constraints': rdefdef.constraints,
+                      'permissions': rdefdef.get_permissions(),
+                      'order': rdefdef.order,
                       'infered': False, 'eid': None
                       })
         cstrtypemap = ss.cstrtype_mapping(session)
         groupmap = group_mapping(session)
-        object = schema.eschema(rdef.object)
+        object = schema.eschema(rdefdef.object)
         for specialization in eschema.specialized_by(False):
-            if (specialization, rdef.object) in rschema.rdefs:
+            if (specialization, rdefdef.object) in rschema.rdefs:
                 continue
             sperdef = RelationDefinitionSchema(specialization, rschema,
                                                object, props)
@@ -430,14 +479,21 @@
             session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
                                {'default': default})
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.session.vreg.schema.del_relation_def(
+            self.rdefdef.subject, self.rdefdef.name, self.rdefdef.object)
+        # XXX revert changes on database
 
-class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
+
+class CWRelationAddOp(CWAttributeAddOp):
     """an actual relation has been added:
-    * if this is an inlined relation, add the necessary column
-      else if it's the first instance of this relation type, add the
-      necessary table and set default permissions
-    * register an operation to add the relation definition to the
-      instance's schema on commit
+
+    * add the relation definition to the instance's schema
+
+    * if this is an inlined relation, add the necessary column else if it's the
+      first instance of this relation type, add the necessary table and set
+      default permissions
 
     constraints are handled by specific hooks
     """
@@ -446,280 +502,229 @@
     def precommit_event(self):
         session = self.session
         entity = self.entity
-        rdef = self.init_rdef(composite=entity.composite)
+        # update the in-memory schema first
+        rdefdef = self.init_rdef(composite=entity.composite)
+        # then make necessary changes to the system source database
         schema = session.vreg.schema
-        rtype = rdef.name
+        rtype = rdefdef.name
         rschema = schema.rschema(rtype)
         # this have to be done before permissions setting
         if rschema.inlined:
             # need to add a column if the relation is inlined and if this is the
             # first occurence of "Subject relation Something" whatever Something
-            # and if it has not been added during other event of the same
-            # transaction
-            key = '%s.%s' % (rdef.subject, rtype)
-            try:
-                alreadythere = bool(rschema.objects(rdef.subject))
-            except KeyError:
-                alreadythere = False
-            if not (alreadythere or
-                    key in session.transaction_data.get('createdattrs', ())):
-                add_inline_relation_column(session, rdef.subject, rtype)
+            if len(rschema.objects(rdefdef.subject)) == 1:
+                add_inline_relation_column(session, rdefdef.subject, rtype)
         else:
             # need to create the relation if no relation definition in the
             # schema and if it has not been added during other event of the same
             # transaction
-            if not (rschema.subjects() or
+            if not (len(rschema.rdefs) > 1 or
                     rtype in session.transaction_data.get('createdtables', ())):
-                try:
-                    rschema = schema.rschema(rtype)
-                    tablesql = y2sql.rschema2sql(rschema)
-                except KeyError:
-                    # fake we add it to the schema now to get a correctly
-                    # initialized schema but remove it before doing anything
-                    # more dangerous...
-                    rschema = schema.add_relation_type(rdef)
-                    tablesql = y2sql.rschema2sql(rschema)
-                    schema.del_relation_type(rtype)
+                rschema = schema.rschema(rtype)
                 # create the necessary table
-                for sql in tablesql.split(';'):
+                for sql in y2sql.rschema2sql(rschema).split(';'):
                     if sql.strip():
                         session.system_sql(sql)
                 session.transaction_data.setdefault('createdtables', []).append(
                     rtype)
 
+    # XXX revertprecommit_event
 
-class SourceDbRDefUpdate(hook.Operation):
+
+class RDefDelOp(MemSchemaOperation):
+    """an actual relation has been removed"""
+    rdef = None # make pylint happy
+
+    def precommit_event(self):
+        session = self.session
+        rdef = self.rdef
+        rschema = rdef.rtype
+        # make necessary changes to the system source database first
+        rdeftype = rschema.final and 'CWAttribute' or 'CWRelation'
+        execute = session.execute
+        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
+                       'R eid %%(x)s' % rdeftype, {'x': rschema.eid})
+        lastrel = rset[0][0] == 0
+        # we have to update physical schema systematically for final and inlined
+        # relations, but only if it's the last instance for this relation type
+        # for other relations
+        if (rschema.final or rschema.inlined):
+            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
+                           'R eid %%(r)s, X from_entity E, E eid %%(e)s'
+                           % rdeftype,
+                           {'r': rschema.eid, 'e': rdef.subject.eid})
+            if rset[0][0] == 0 and not session.deleted_in_transaction(rdef.subject.eid):
+                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
+                ptypes.add(rschema.type)
+                DropColumn(session, table=SQL_PREFIX + str(rdef.subject),
+                           column=SQL_PREFIX + str(rschema))
+        elif lastrel:
+            DropRelationTable(session, str(rschema))
+        # then update the in-memory schema
+        rschema.del_relation_def(rdef.subject, rdef.object)
+        # if this is the last relation definition of this type, drop associated
+        # relation type
+        if lastrel and not session.deleted_in_transaction(rschema.eid):
+            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rschema.eid})
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        #
+        # Note: add_relation_def takes a RelationDefinition, not a
+        # RelationDefinitionSchema, needs to fake it
+        self.rdef.name = str(self.rdef.rtype)
+        self.session.vreg.schema.add_relation_def(self.rdef)
+
+
+
+class RDefUpdateOp(MemSchemaOperation):
     """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
+    rschema = rdefkey = values = None # make pylint happy
+    oldvalues = None
+    indexed_changed = null_allowed_changed = False
 
     def precommit_event(self):
         session = self.session
-        etype = self.kobj[0]
-        table = SQL_PREFIX + etype
-        column = SQL_PREFIX + self.rschema.type
+        rdef = self.rdef = self.rschema.rdefs[self.rdefkey]
+        # update the in-memory schema first
+        self.oldvalues = dict( (attr, getattr(rdef, attr)) for attr in self.values)
+        rdef.update(self.values)
+        # then make necessary changes to the system source database
+        syssource = session.pool.source('system')
         if 'indexed' in self.values:
-            sysource = session.pool.source('system')
-            if self.values['indexed']:
-                sysource.create_index(session, table, column)
-            else:
-                sysource.drop_index(session, table, column)
-        if 'cardinality' in self.values and self.rschema.final:
-            syssource = session.pool.source('system')
-            if not syssource.dbhelper.alter_column_support:
-                # not supported (and NOT NULL not set by yams in that case, so
-                # no worry) XXX (syt) then should we set NOT NULL below ??
-                return
-            atype = self.rschema.objects(etype)[0]
-            constraints = self.rschema.rdef(etype, atype).constraints
-            coltype = y2sql.type_from_constraints(syssource.dbhelper, atype, constraints,
-                                                  creating=False)
-            # XXX check self.values['cardinality'][0] actually changed?
-            syssource.set_null_allowed(self.session, table, column, coltype,
-                                       self.values['cardinality'][0] != '1')
+            syssource.update_rdef_indexed(session, rdef)
+            self.indexed_changed = True
+        if 'cardinality' in self.values and (rdef.rtype.final or
+                                             rdef.rtype.inlined) \
+              and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
+            syssource.update_rdef_null_allowed(self.session, rdef)
+            self.null_allowed_changed = True
         if 'fulltextindexed' in self.values:
-            hook.set_operation(session, 'fti_update_etypes', etype,
+            hook.set_operation(session, 'fti_update_etypes', rdef.subject,
                                UpdateFTIndexOp)
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.rdef.update(self.oldvalues)
+        # revert changes on database
+        syssource = self.session.pool.source('system')
+        if self.indexed_changed:
+            syssource.update_rdef_indexed(self.session, self.rdef)
+        if self.null_allowed_changed:
+            syssource.update_rdef_null_allowed(self.session, self.rdef)
 
-class SourceDbCWConstraintAdd(hook.Operation):
+
+def _set_modifiable_constraints(rdef):
+    # for proper in-place modification of in-memory schema: if rdef.constraints
+    # is already a list, reuse it (we're updating multiple constraints of the
+    # same rdef in the same transactions)
+    if not isinstance(rdef.constraints, list):
+        rdef.constraints = list(rdef.constraints)
+
+
+class CWConstraintDelOp(MemSchemaOperation):
+    """actually remove a constraint of a relation definition"""
+    rdef = oldcstr = newcstr = None # make pylint happy
+    size_cstr_changed = unique_changed = False
+
+    def precommit_event(self):
+        session = self.session
+        rdef = self.rdef
+        # in-place modification of in-memory schema first
+        _set_modifiable_constraints(rdef)
+        rdef.constraints.remove(self.oldcstr)
+        # then update database: alter the physical schema on size/unique
+        # constraint changes
+        syssource = session.pool.source('system')
+        cstrtype = self.oldcstr.type()
+        if cstrtype == 'SizeConstraint':
+            syssource.update_rdef_column(session, rdef)
+            self.size_cstr_changed = True
+        elif cstrtype == 'UniqueConstraint':
+            syssource.update_rdef_unique(session, rdef)
+            self.unique_changed = True
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        if self.newcstr is not None:
+            self.rdef.constraints.remove(self.newcstr)
+        if self.oldcstr is not None:
+            self.rdef.constraints.append(self.oldcstr)
+        # revert changes on database
+        syssource = self.session.pool.source('system')
+        if self.size_cstr_changed:
+            syssource.update_rdef_column(self.session, self.rdef)
+        if self.unique_changed:
+            syssource.update_rdef_unique(self.session, self.rdef)
+
+
+class CWConstraintAddOp(CWConstraintDelOp):
     """actually update constraint of a relation definition"""
     entity = None # make pylint happy
-    cancelled = False
 
     def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
         session = self.session
+        rdefentity = self.entity.reverse_constrained_by[0]
         # when the relation is added in the same transaction, the constraint
         # object is created by the operation adding the attribute or relation,
         # so there is nothing to do here
-        if session.added_in_transaction(rdef.eid):
+        if session.added_in_transaction(rdefentity.eid):
             return
-        rdefschema = session.vreg.schema.schema_by_eid(rdef.eid)
-        subjtype, rtype, objtype = rdefschema.as_triple()
+        rdef = self.rdef = session.vreg.schema.schema_by_eid(rdefentity.eid)
         cstrtype = self.entity.type
-        oldcstr = rtype.rdef(subjtype, objtype).constraint_by_type(cstrtype)
-        newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        table = SQL_PREFIX + str(subjtype)
-        column = SQL_PREFIX + str(rtype)
-        # alter the physical schema on size constraint changes
-        if newcstr.type() == 'SizeConstraint' and (
-            oldcstr is None or oldcstr.max != newcstr.max):
-            syssource = self.session.pool.source('system')
-            card = rtype.rdef(subjtype, objtype).cardinality
-            coltype = y2sql.type_from_constraints(syssource.dbhelper, objtype,
-                                                  [newcstr], creating=False)
-            try:
-                syssource.change_col_type(session, table, column, coltype, card[0] != '1')
-                self.info('altered column %s of table %s: now %s',
-                          column, table, coltype)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
+        oldcstr = self.oldcstr = rdef.constraint_by_type(cstrtype)
+        newcstr = self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+        # in-place modification of in-memory schema first
+        _set_modifiable_constraints(rdef)
+        newcstr.eid = self.entity.eid
+        if oldcstr is not None:
+            rdef.constraints.remove(oldcstr)
+        rdef.constraints.append(newcstr)
+        # then update database: alter the physical schema on size/unique
+        # constraint changes
+        syssource = session.pool.source('system')
+        if cstrtype == 'SizeConstraint' and (oldcstr is None or
+                                             oldcstr.max != newcstr.max):
+            syssource.update_rdef_column(session, rdef)
+            self.size_cstr_changed = True
         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
-            session.pool.source('system').create_index(
-                self.session, table, column, unique=True)
-
-
-class SourceDbCWConstraintDel(hook.Operation):
-    """actually remove a constraint of a relation definition"""
-    rtype = subjtype = None # make pylint happy
-
-    def precommit_event(self):
-        cstrtype = self.cstr.type()
-        table = SQL_PREFIX + str(self.rdef.subject)
-        column = SQL_PREFIX + str(self.rdef.rtype)
-        # alter the physical schema on size/unique constraint changes
-        if cstrtype == 'SizeConstraint':
-            syssource = self.session.pool.source('system')
-            coltype = y2sql.type_from_constraints(syssource.dbhelper,
-                                                  self.rdef.object, [],
-                                                  creating=False)
-            try:
-                syssource.change_col_type(session, table, column, coltype,
-                                          self.rdef.cardinality[0] != '1')
-                self.info('altered column %s of table %s: now %s',
-                          column, table, coltype)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
-        elif cstrtype == 'UniqueConstraint':
-            self.session.pool.source('system').drop_index(
-                self.session, table, column, unique=True)
+            syssource.update_rdef_unique(session, rdef)
+            self.unique_changed = True
 
 
 # operations for in-memory schema synchronization  #############################
 
-class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
-    """actually add the entity type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.session.vreg.schema.add_entity_type(self.kobj)
-
-
-class MemSchemaCWETypeRename(MemSchemaOperation):
-    """this operation updates physical storage accordingly"""
-    oldname = newname = None # make pylint happy
-
-    def commit_event(self):
-        self.session.vreg.schema.rename_entity_type(self.oldname, self.newname)
-
-
 class MemSchemaCWETypeDel(MemSchemaOperation):
     """actually remove the entity type from the instance's schema"""
-    def commit_event(self):
-        try:
-            # del_entity_type also removes entity's relations
-            self.session.vreg.schema.del_entity_type(self.kobj)
-        except KeyError:
-            # s/o entity type have already been deleted
-            pass
+    def postcommit_event(self):
+        # del_entity_type also removes entity's relations
+        self.session.vreg.schema.del_entity_type(self.etype)
 
 
-class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
+class MemSchemaCWRTypeAdd(MemSchemaOperation):
     """actually add the relation type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.session.vreg.schema.add_relation_type(self.kobj)
-
+    def precommit_event(self):
+        self.session.vreg.schema.add_relation_type(self.rtypedef)
 
-class MemSchemaCWRTypeUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.__dict__.update(self.values)
+    def revertprecommit_event(self):
+        self.session.vreg.schema.del_relation_type(self.rtypedef.name)
 
 
 class MemSchemaCWRTypeDel(MemSchemaOperation):
     """actually remove the relation type from the instance's schema"""
-    def commit_event(self):
+    def postcommit_event(self):
         try:
-            self.session.vreg.schema.del_relation_type(self.kobj)
+            self.session.vreg.schema.del_relation_type(self.rtype)
         except KeyError:
             # s/o entity type have already been deleted
             pass
 
 
-class MemSchemaRDefAdd(MemSchemaEarlyOperation):
-    """actually add the attribute relation definition to the instance's
-    schema
-    """
-    def commit_event(self):
-        self.session.vreg.schema.add_relation_def(self.kobj)
-
-
-class MemSchemaRDefUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.rdefs[self.kobj].update(self.values)
-
-
-class MemSchemaRDefDel(MemSchemaOperation):
-    """actually remove the relation definition from the instance's schema"""
-    def commit_event(self):
-        subjtype, rtype, objtype = self.kobj
-        try:
-            self.session.vreg.schema.del_relation_def(subjtype, rtype, objtype)
-        except KeyError:
-            # relation type may have been already deleted
-            pass
-
-
-class MemSchemaCWConstraintAdd(MemSchemaOperation):
-    """actually update constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintAdd
-    """
-    cancelled = False
-
-    def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
-        # when the relation is added in the same transaction, the constraint
-        # object is created by the operation adding the attribute or relation,
-        # so there is nothing to do here
-        if self.session.added_in_transaction(rdef.eid):
-            self.cancelled = True
-            return
-        rdef = self.session.vreg.schema.schema_by_eid(rdef.eid)
-        self.prepare_constraints(rdef)
-        cstrtype = self.entity.type
-        self.cstr = rdef.constraint_by_type(cstrtype)
-        self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        self.newcstr.eid = self.entity.eid
-
-    def commit_event(self):
-        if self.cancelled:
-            return
-        # in-place modification
-        if not self.cstr is None:
-            self.constraints.remove(self.cstr)
-        self.constraints.append(self.newcstr)
-
-
-class MemSchemaCWConstraintDel(MemSchemaOperation):
-    """actually remove a constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintDel
-    """
-    rtype = subjtype = objtype = None # make pylint happy
-    def precommit_event(self):
-        self.prepare_constraints(self.rdef)
-
-    def commit_event(self):
-        self.constraints.remove(self.cstr)
-
-
 class MemSchemaPermissionAdd(MemSchemaOperation):
     """synchronize schema when a *_permission relation has been added on a group
     """
 
-    def commit_event(self):
+    def precommit_event(self):
         """the observed connections pool has been commited"""
         try:
             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
@@ -740,13 +745,15 @@
             perms.append(perm)
             erschema.set_action_permissions(self.action, perms)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaPermissionDel(MemSchemaPermissionAdd):
     """synchronize schema when a *_permission relation has been deleted from a
     group
     """
 
-    def commit_event(self):
+    def precommit_event(self):
         """the observed connections pool has been commited"""
         try:
             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
@@ -771,19 +778,23 @@
             self.error('can\'t remove permission %s for %s on %s',
                        perm, self.action, erschema)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaSpecializesAdd(MemSchemaOperation):
 
-    def commit_event(self):
+    def precommit_event(self):
         eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
         parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
         eschema._specialized_type = parenteschema.type
         parenteschema._specialized_by.append(eschema.type)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaSpecializesDel(MemSchemaOperation):
 
-    def commit_event(self):
+    def precommit_event(self):
         try:
             eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
             parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
@@ -793,10 +804,7 @@
         eschema._specialized_type = None
         parenteschema._specialized_by.remove(eschema.type)
 
-
-class SyncSchemaHook(hook.Hook):
-    __abstract__ = True
-    category = 'syncschema'
+    # XXX revertprecommit_event
 
 
 # CWEType hooks ################################################################
@@ -808,7 +816,7 @@
     * instantiate an operation to delete the entity type on commit
     """
     __regid__ = 'syncdelcwetype'
-    __select__ = SyncSchemaHook.__select__ & implements('CWEType')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWEType')
     events = ('before_delete_entity',)
 
     def __call__(self):
@@ -817,9 +825,10 @@
         if name in CORE_ETYPES:
             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
         # delete every entities of this type
-        self._cw.execute('DELETE %s X' % name)
+        if not name in ETYPE_NAME_MAP:
+            self._cw.execute('DELETE %s X' % name)
+            MemSchemaCWETypeDel(self._cw, etype=name)
         DropTable(self._cw, table=SQL_PREFIX + name)
-        MemSchemaCWETypeDel(self._cw, name)
 
 
 class AfterDelCWETypeHook(DelCWETypeHook):
@@ -847,42 +856,7 @@
         entity = self.entity
         if entity.get('final'):
             return
-        schema = self._cw.vreg.schema
-        name = entity['name']
-        etype = ybo.EntityType(name=name, description=entity.get('description'),
-                               meta=entity.get('meta')) # don't care about final
-        # fake we add it to the schema now to get a correctly initialized schema
-        # but remove it before doing anything more dangerous...
-        schema = self._cw.vreg.schema
-        eschema = schema.add_entity_type(etype)
-        # generate table sql and rql to add metadata
-        tablesql = y2sql.eschema2sql(self._cw.pool.source('system').dbhelper,
-                                     eschema, prefix=SQL_PREFIX)
-        rdefrqls = []
-        gmap = group_mapping(self._cw)
-        cmap = ss.cstrtype_mapping(self._cw)
-        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
-            rschema = schema[rtype]
-            sampletype = rschema.subjects()[0]
-            desttype = rschema.objects()[0]
-            rdef = copy(rschema.rdef(sampletype, desttype))
-            rdef.subject = mock_object(eid=entity.eid)
-            mock = mock_object(eid=None)
-            rdefrqls.append( (mock, tuple(ss.rdef2rql(rdef, cmap, gmap))) )
-        # now remove it !
-        schema.del_entity_type(name)
-        # create the necessary table
-        for sql in tablesql.split(';'):
-            if sql.strip():
-                self._cw.system_sql(sql)
-        # register operation to modify the schema on commit
-        # this have to be done before adding other relations definitions
-        # or permission settings
-        etype.eid = entity.eid
-        MemSchemaCWETypeAdd(self._cw, etype)
-        # add meta relations
-        for rdef, relrqls in rdefrqls:
-            ss.execschemarql(self._cw.execute, rdef, relrqls)
+        CWETypeAddOp(self._cw, entity=entity)
 
 
 class BeforeUpdateCWETypeHook(DelCWETypeHook):
@@ -895,12 +869,9 @@
         check_valid_changes(self._cw, entity, ro_attrs=('final',))
         # don't use getattr(entity, attr), we would get the modified value if any
         if 'name' in entity.edited_attributes:
-            newname = entity.pop('name')
-            oldname = entity.name
+            oldname, newname = hook.entity_oldnewvalue(entity, 'name')
             if newname.lower() != oldname.lower():
-                SourceDbCWETypeRename(self._cw, oldname=oldname, newname=newname)
-                MemSchemaCWETypeRename(self._cw, oldname=oldname, newname=newname)
-            entity['name'] = newname
+                CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
 
 
 # CWRType hooks ################################################################
@@ -912,7 +883,7 @@
     * instantiate an operation to delete the relation type on commit
     """
     __regid__ = 'syncdelcwrtype'
-    __select__ = SyncSchemaHook.__select__ & implements('CWRType')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
     events = ('before_delete_entity',)
 
     def __call__(self):
@@ -924,7 +895,7 @@
                         {'x': self.entity.eid})
         self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
                         {'x': self.entity.eid})
-        MemSchemaCWRTypeDel(self._cw, name)
+        MemSchemaCWRTypeDel(self._cw, rtype=name)
 
 
 class AfterAddCWRTypeHook(DelCWRTypeHook):
@@ -939,13 +910,12 @@
 
     def __call__(self):
         entity = self.entity
-        rtype = ybo.RelationType(name=entity.name,
-                                 description=entity.get('description'),
-                                 meta=entity.get('meta', False),
-                                 inlined=entity.get('inlined', False),
-                                 symmetric=entity.get('symmetric', False),
-                                 eid=entity.eid)
-        MemSchemaCWRTypeAdd(self._cw, rtype)
+        rtypedef = ybo.RelationType(name=entity.name,
+                                    description=entity.description,
+                                    inlined=entity.get('inlined', False),
+                                    symmetric=entity.get('symmetric', False),
+                                    eid=entity.eid)
+        MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
 
 
 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
@@ -964,9 +934,8 @@
                     newvalues[prop] = entity[prop]
         if newvalues:
             rschema = self._cw.vreg.schema.rschema(entity.name)
-            SourceDbCWRTypeUpdate(self._cw, rschema=rschema, entity=entity,
-                                  values=newvalues)
-            MemSchemaCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues)
+            CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
+                            values=newvalues)
 
 
 class AfterDelRelationTypeHook(SyncSchemaHook):
@@ -984,9 +953,12 @@
 
     def __call__(self):
         session = self._cw
-        rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
+        try:
+            rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
+        except KeyError:
+            self.critical('cant get schema rdef associated to %s', self.eidfrom)
+            return
         subjschema, rschema, objschema = rdef.as_triple()
-        pendings = session.transaction_data.get('pendingeids', ())
         pendingrdefs = session.transaction_data.setdefault('pendingrdefs', set())
         # first delete existing relation if necessary
         if rschema.final:
@@ -995,93 +967,73 @@
         else:
             rdeftype = 'CWRelation'
             pendingrdefs.add((subjschema, rschema, objschema))
-            if not (subjschema.eid in pendings or objschema.eid in pendings):
+            if not (session.deleted_in_transaction(subjschema.eid) or
+                    session.deleted_in_transaction(objschema.eid)):
                 session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
                                 % (rschema, subjschema, objschema))
-        execute = session.execute
-        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
-                       'R eid %%(x)s' % rdeftype, {'x': self.eidto})
-        lastrel = rset[0][0] == 0
-        # we have to update physical schema systematically for final and inlined
-        # relations, but only if it's the last instance for this relation type
-        # for other relations
-
-        if (rschema.final or rschema.inlined):
-            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
-                           'R eid %%(x)s, X from_entity E, E name %%(name)s'
-                           % rdeftype, {'x': self.eidto, 'name': str(subjschema)})
-            if rset[0][0] == 0 and not subjschema.eid in pendings:
-                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
-                ptypes.add(rschema.type)
-                DropColumn(session, table=SQL_PREFIX + subjschema.type,
-                           column=SQL_PREFIX + rschema.type)
-        elif lastrel:
-            DropRelationTable(session, rschema.type)
-        # if this is the last instance, drop associated relation type
-        if lastrel and not self.eidto in pendings:
-            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': self.eidto})
-        MemSchemaRDefDel(session, (subjschema, rschema, objschema))
+        RDefDelOp(session, rdef=rdef)
 
 
 # CWAttribute / CWRelation hooks ###############################################
 
 class AfterAddCWAttributeHook(SyncSchemaHook):
     __regid__ = 'syncaddcwattribute'
-    __select__ = SyncSchemaHook.__select__ & implements('CWAttribute')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute')
     events = ('after_add_entity',)
 
     def __call__(self):
-        SourceDbCWAttributeAdd(self._cw, entity=self.entity)
+        CWAttributeAddOp(self._cw, entity=self.entity)
 
 
 class AfterAddCWRelationHook(AfterAddCWAttributeHook):
     __regid__ = 'syncaddcwrelation'
-    __select__ = SyncSchemaHook.__select__ & implements('CWRelation')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWRelation')
 
     def __call__(self):
-        SourceDbCWRelationAdd(self._cw, entity=self.entity)
+        CWRelationAddOp(self._cw, entity=self.entity)
 
 
 class AfterUpdateCWRDefHook(SyncSchemaHook):
     __regid__ = 'syncaddcwattribute'
-    __select__ = SyncSchemaHook.__select__ & implements('CWAttribute',
-                                                        'CWRelation')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute',
+                                                         'CWRelation')
     events = ('before_update_entity',)
 
     def __call__(self):
         entity = self.entity
         if self._cw.deleted_in_transaction(entity.eid):
             return
-        desttype = entity.otype.name
+        subjtype = entity.stype.name
+        objtype = entity.otype.name
         rschema = self._cw.vreg.schema[entity.rtype.name]
+        # note: do not access schema rdef here, it may be added later by an
+        # operation
         newvalues = {}
-        for prop in RelationDefinitionSchema.rproperty_defs(desttype):
+        for prop in RelationDefinitionSchema.rproperty_defs(objtype):
             if prop == 'constraints':
                 continue
             if prop == 'order':
-                prop = 'ordernum'
-            if prop in entity.edited_attributes:
-                old, new = hook.entity_oldnewvalue(entity, prop)
+                attr = 'ordernum'
+            else:
+                attr = prop
+            if attr in entity.edited_attributes:
+                old, new = hook.entity_oldnewvalue(entity, attr)
                 if old != new:
-                    newvalues[prop] = entity[prop]
+                    newvalues[prop] = new
         if newvalues:
-            subjtype = entity.stype.name
-            MemSchemaRDefUpdate(self._cw, kobj=(subjtype, desttype),
-                                rschema=rschema, values=newvalues)
-            SourceDbRDefUpdate(self._cw, kobj=(subjtype, desttype),
-                               rschema=rschema, values=newvalues)
+            RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
+                         values=newvalues)
 
 
 # constraints synchronization hooks ############################################
 
 class AfterAddCWConstraintHook(SyncSchemaHook):
     __regid__ = 'syncaddcwconstraint'
-    __select__ = SyncSchemaHook.__select__ & implements('CWConstraint')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
     events = ('after_add_entity', 'after_update_entity')
 
     def __call__(self):
-        MemSchemaCWConstraintAdd(self._cw, entity=self.entity)
-        SourceDbCWConstraintAdd(self._cw, entity=self.entity)
+        CWConstraintAddOp(self._cw, entity=self.entity)
 
 
 class AfterAddConstrainedByHook(SyncSchemaHook):
@@ -1109,8 +1061,7 @@
         except IndexError:
             self._cw.critical('constraint type no more accessible')
         else:
-            SourceDbCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
-            MemSchemaCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
+            CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr)
 
 
 # permissions synchronization hooks ############################################
@@ -1176,7 +1127,7 @@
             still_fti = list(schema[etype].indexable_attributes())
             for entity in rset.entities():
                 source.fti_unindex_entity(session, entity.eid)
-                for container in entity.fti_containers():
+                for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
                     if still_fti or container is not entity:
                         source.fti_unindex_entity(session, container.eid)
                         source.fti_index_entity(session, container)
--- a/hooks/syncsession.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/syncsession.py	Mon Jul 19 15:37:02 2010 +0200
@@ -22,7 +22,7 @@
 
 from yams.schema import role_name
 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server import hook
 
 
@@ -108,7 +108,7 @@
 
 class CloseDeletedUserSessionsHook(SyncSessionHook):
     __regid__ = 'closession'
-    __select__ = SyncSessionHook.__select__ & implements('CWUser')
+    __select__ = SyncSessionHook.__select__ & is_instance('CWUser')
     events = ('after_delete_entity',)
 
     def __call__(self):
@@ -152,7 +152,7 @@
 
 class AddCWPropertyHook(SyncSessionHook):
     __regid__ = 'addcwprop'
-    __select__ = SyncSessionHook.__select__ & implements('CWProperty')
+    __select__ = SyncSessionHook.__select__ & is_instance('CWProperty')
     events = ('after_add_entity',)
 
     def __call__(self):
--- a/hooks/test/unittest_hooks.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/test/unittest_hooks.py	Mon Jul 19 15:37:02 2010 +0200
@@ -114,13 +114,10 @@
         self.assertEquals(rset.get_entity(0, 0).reverse_parts[0].messageid, '<2345>')
 
     def test_unsatisfied_constraints(self):
-        releid = self.execute('INSERT CWRelation X: X from_entity FE, X relation_type RT, X to_entity TE '
-                              'WHERE FE name "CWUser", RT name "in_group", TE name "String"')[0][0]
-        self.execute('SET X read_permission Y WHERE X eid %(x)s, Y name "managers"',
-                     {'x': releid}, 'x')
+        releid = self.execute('SET U in_group G WHERE G name "owners", U login "admin"')[0][0]
         ex = self.assertRaises(ValidationError, self.commit)
         self.assertEquals(ex.errors,
-                          {'to_entity-object': 'RQLConstraint O final FALSE failed'})
+                          {'in_group-object': u'RQLConstraint NOT O name "owners" failed'})
 
     def test_html_tidy_hook(self):
         req = self.request()
--- a/hooks/test/unittest_syncschema.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/test/unittest_syncschema.py	Mon Jul 19 15:37:02 2010 +0200
@@ -188,6 +188,9 @@
             self.failIf(self.index_exists('State', 'state_of'))
             rset = self.execute('Any X, Y WHERE X state_of Y')
             self.assertEquals(len(rset), 2) # user states
+        except:
+            import traceback
+            traceback.print_exc()
         finally:
             self.execute('SET X inlined TRUE WHERE X name "state_of"')
             self.failIf(self.schema['state_of'].inlined)
--- a/hooks/workflow.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/hooks/workflow.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Core hooks: workflow related hooks
+"""Core hooks: workflow related hooks"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from datetime import datetime
@@ -25,8 +24,7 @@
 from yams.schema import role_name
 
 from cubicweb import RepositoryError, ValidationError
-from cubicweb.interfaces import IWorkflowable
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance, adaptable
 from cubicweb.server import hook
 
 
@@ -51,11 +49,12 @@
     def precommit_event(self):
         session = self.session
         entity = self.entity
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         # if there is an initial state and the entity's state is not set,
         # use the initial state as a default state
         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
-               and entity.current_workflow:
-            state = entity.current_workflow.initial
+               and iworkflowable.current_workflow:
+            state = iworkflowable.current_workflow.initial
             if state:
                 session.add_relation(entity.eid, 'in_state', state.eid)
                 _FireAutotransitionOp(session, entity=entity)
@@ -65,10 +64,11 @@
 
     def precommit_event(self):
         entity = self.entity
-        autotrs = list(entity.possible_transitions('auto'))
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
+        autotrs = list(iworkflowable.possible_transitions('auto'))
         if autotrs:
             assert len(autotrs) == 1
-            entity.fire_transition(autotrs[0])
+            iworkflowable.fire_transition(autotrs[0])
 
 
 class _WorkflowChangedOp(hook.Operation):
@@ -82,29 +82,30 @@
         if self.eid in pendingeids:
             return
         entity = session.entity_from_eid(self.eid)
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         # check custom workflow has not been rechanged to another one in the same
         # transaction
-        mainwf = entity.main_workflow
+        mainwf = iworkflowable.main_workflow
         if mainwf.eid == self.wfeid:
             deststate = mainwf.initial
             if not deststate:
                 qname = role_name('custom_workflow', 'subject')
                 msg = session._('workflow has no initial state')
                 raise ValidationError(entity.eid, {qname: msg})
-            if mainwf.state_by_eid(entity.current_state.eid):
+            if mainwf.state_by_eid(iworkflowable.current_state.eid):
                 # nothing to do
                 return
             # if there are no history, simply go to new workflow's initial state
-            if not entity.workflow_history:
-                if entity.current_state.eid != deststate.eid:
+            if not iworkflowable.workflow_history:
+                if iworkflowable.current_state.eid != deststate.eid:
                     _change_state(session, entity.eid,
-                                  entity.current_state.eid, deststate.eid)
+                                  iworkflowable.current_state.eid, deststate.eid)
                     _FireAutotransitionOp(session, entity=entity)
                 return
             msg = session._('workflow changed to "%s"')
             msg %= session._(mainwf.name)
             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
-            entity.change_state(deststate, msg, u'text/plain')
+            iworkflowable.change_state(deststate, msg, u'text/plain')
 
 
 class _CheckTrExitPoint(hook.Operation):
@@ -125,9 +126,10 @@
     def precommit_event(self):
         session = self.session
         forentity = self.forentity
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
         trinfo = self.trinfo
         # we're in a subworkflow, check if we've reached an exit point
-        wftr = forentity.subworkflow_input_transition()
+        wftr = iworkflowable.subworkflow_input_transition()
         if wftr is None:
             # inconsistency detected
             qname = role_name('to_state', 'subject')
@@ -137,9 +139,9 @@
         if tostate is not None:
             # reached an exit point
             msg = session._('exiting from subworkflow %s')
-            msg %= session._(forentity.current_workflow.name)
+            msg %= session._(iworkflowable.current_workflow.name)
             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
-            forentity.change_state(tostate, msg, u'text/plain', tr=wftr)
+            iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
 
 
 # hooks ########################################################################
@@ -151,7 +153,7 @@
 
 class SetInitialStateHook(WorkflowHook):
     __regid__ = 'wfsetinitial'
-    __select__ = WorkflowHook.__select__ & implements(IWorkflowable)
+    __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -175,7 +177,7 @@
     * by_transition or to_state (managers only) inlined relation is set
     """
     __regid__ = 'wffiretransition'
-    __select__ = WorkflowHook.__select__ & implements('TrInfo')
+    __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
     events = ('before_add_entity',)
 
     def __call__(self):
@@ -189,18 +191,19 @@
             msg = session._('mandatory relation')
             raise ValidationError(entity.eid, {qname: msg})
         forentity = session.entity_from_eid(foreid)
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
         # then check it has a workflow set, unless we're in the process of changing
         # entity's workflow
         if session.transaction_data.get((forentity.eid, 'customwf')):
             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
             wf = session.entity_from_eid(wfeid)
         else:
-            wf = forentity.current_workflow
+            wf = iworkflowable.current_workflow
         if wf is None:
             msg = session._('related entity has no workflow set')
             raise ValidationError(entity.eid, {None: msg})
         # then check it has a state set
-        fromstate = forentity.current_state
+        fromstate = iworkflowable.current_state
         if fromstate is None:
             msg = session._('related entity has no state')
             raise ValidationError(entity.eid, {None: msg})
@@ -270,7 +273,7 @@
 class FiredTransitionHook(WorkflowHook):
     """change related entity state"""
     __regid__ = 'wffiretransition'
-    __select__ = WorkflowHook.__select__ & implements('TrInfo')
+    __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -278,8 +281,9 @@
         _change_state(self._cw, trinfo['wf_info_for'],
                       trinfo['from_state'], trinfo['to_state'])
         forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
-        assert forentity.current_state.eid == trinfo['to_state']
-        if forentity.main_workflow.eid != forentity.current_workflow.eid:
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
+        assert iworkflowable.current_state.eid == trinfo['to_state']
+        if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
             _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
 
 
@@ -297,7 +301,8 @@
             # state changed through TrInfo insertion, so we already know it's ok
             return
         entity = session.entity_from_eid(self.eidfrom)
-        mainwf = entity.main_workflow
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
+        mainwf = iworkflowable.main_workflow
         if mainwf is None:
             msg = session._('entity has no workflow set')
             raise ValidationError(entity.eid, {None: msg})
@@ -309,7 +314,7 @@
             msg = session._("state doesn't belong to entity's workflow. You may "
                             "want to set a custom workflow for this entity first.")
             raise ValidationError(self.eidfrom, {qname: msg})
-        if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+        if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
             qname = role_name('in_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
             raise ValidationError(self.eidfrom, {qname: msg})
@@ -359,7 +364,7 @@
 
     def __call__(self):
         entity = self._cw.entity_from_eid(self.eidfrom)
-        typewf = entity.cwetype_workflow()
+        typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow()
         if typewf is not None:
             _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
 
--- a/i18n/en.po	Thu Jul 15 12:03:13 2010 +0200
+++ b/i18n/en.po	Mon Jul 19 15:37:02 2010 +0200
@@ -235,6 +235,9 @@
 msgid "Browse by category"
 msgstr ""
 
+msgid "Browse by entity type"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -419,6 +422,9 @@
 msgid "Garbage collection information"
 msgstr ""
 
+msgid "Got rhythm?"
+msgstr ""
+
 msgid "Help"
 msgstr ""
 
@@ -624,9 +630,6 @@
 msgid "Submit bug report by mail"
 msgstr ""
 
-msgid "The repository holds the following entities"
-msgstr ""
-
 #, python-format
 msgid "The view %s can not be applied to this query"
 msgstr ""
@@ -955,6 +958,9 @@
 msgid "add_permission_object"
 msgstr "has permission to add"
 
+msgid "add_relation"
+msgstr "add"
+
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
 msgstr ""
@@ -1282,6 +1288,12 @@
 msgid "click on the box to cancel the deletion"
 msgstr ""
 
+msgid "click to add a value"
+msgstr ""
+
+msgid "click to delete this value"
+msgstr ""
+
 msgid "click to edit this field"
 msgstr ""
 
@@ -2271,11 +2283,20 @@
 msgid "granted to groups"
 msgstr ""
 
-msgid "graphical representation of the instance'schema"
+#, python-format
+msgid "graphical representation of %(appid)s data model"
 msgstr ""
 
 #, python-format
-msgid "graphical schema for %s"
+msgid ""
+"graphical representation of the %(etype)s entity type from %(appid)s data "
+"model"
+msgstr ""
+
+#, python-format
+msgid ""
+"graphical representation of the %(rtype)s relation type from %(appid)s data "
+"model"
 msgstr ""
 
 #, python-format
@@ -2801,6 +2822,9 @@
 msgid "no edited fields specified for entity %s"
 msgstr ""
 
+msgid "no related entity"
+msgstr ""
+
 msgid "no related project"
 msgstr ""
 
@@ -3711,6 +3735,9 @@
 msgid "update_permission_object"
 msgstr "has permission to update"
 
+msgid "update_relation"
+msgstr "update"
+
 msgid "updated"
 msgstr ""
 
--- a/i18n/es.po	Thu Jul 15 12:03:13 2010 +0200
+++ b/i18n/es.po	Mon Jul 19 15:37:02 2010 +0200
@@ -243,6 +243,9 @@
 msgid "Browse by category"
 msgstr "Busca por categoría"
 
+msgid "Browse by entity type"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -427,6 +430,9 @@
 msgid "Garbage collection information"
 msgstr ""
 
+msgid "Got rhythm?"
+msgstr ""
+
 msgid "Help"
 msgstr ""
 
@@ -632,9 +638,6 @@
 msgid "Submit bug report by mail"
 msgstr "Enviar este reporte por email"
 
-msgid "The repository holds the following entities"
-msgstr "El repositorio contiene las entidades siguientes"
-
 #, python-format
 msgid "The view %s can not be applied to this query"
 msgstr "La vista %s no puede ser aplicada a esta búsqueda"
@@ -978,6 +981,9 @@
 msgid "add_permission_object"
 msgstr "tiene la autorización para agregar"
 
+msgid "add_relation"
+msgstr ""
+
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
 msgstr "Agregado %(etype)s #%(eid)s (%(title)s)"
@@ -1310,6 +1316,12 @@
 msgid "click on the box to cancel the deletion"
 msgstr "Seleccione la zona de edición para cancelar la eliminación"
 
+msgid "click to add a value"
+msgstr ""
+
+msgid "click to delete this value"
+msgstr ""
+
 msgid "click to edit this field"
 msgstr ""
 
@@ -2321,12 +2333,21 @@
 msgid "granted to groups"
 msgstr "Otorgado a los grupos"
 
-msgid "graphical representation of the instance'schema"
+#, python-format
+msgid "graphical representation of %(appid)s data model"
 msgstr ""
 
 #, python-format
-msgid "graphical schema for %s"
-msgstr "Gráfica del esquema por %s"
+msgid ""
+"graphical representation of the %(etype)s entity type from %(appid)s data "
+"model"
+msgstr ""
+
+#, python-format
+msgid ""
+"graphical representation of the %(rtype)s relation type from %(appid)s data "
+"model"
+msgstr ""
 
 #, python-format
 msgid "graphical workflow for %s"
@@ -2875,6 +2896,9 @@
 msgid "no edited fields specified for entity %s"
 msgstr ""
 
+msgid "no related entity"
+msgstr ""
+
 msgid "no related project"
 msgstr "no hay proyecto relacionado"
 
@@ -3792,6 +3816,9 @@
 msgid "update_permission_object"
 msgstr "objeto de autorización de modificaciones"
 
+msgid "update_relation"
+msgstr ""
+
 msgid "updated"
 msgstr ""
 
@@ -4039,5 +4066,11 @@
 msgid "you should probably delete that property"
 msgstr "deberia probablamente suprimir esta propriedad"
 
+#~ msgid "The repository holds the following entities"
+#~ msgstr "El repositorio contiene las entidades siguientes"
+
+#~ msgid "graphical schema for %s"
+#~ msgstr "Gráfica del esquema por %s"
+
 #~ msgid "schema-image"
 #~ msgstr "esquema imagen"
--- a/i18n/fr.po	Thu Jul 15 12:03:13 2010 +0200
+++ b/i18n/fr.po	Mon Jul 19 15:37:02 2010 +0200
@@ -242,6 +242,9 @@
 msgid "Browse by category"
 msgstr "Naviguer par catégorie"
 
+msgid "Browse by entity type"
+msgstr "Naviguer par type d'entité"
+
 msgid "Bytes"
 msgstr "Donnée binaires"
 
@@ -438,6 +441,9 @@
 msgid "Garbage collection information"
 msgstr "Information sur le ramasse-miette"
 
+msgid "Got rhythm?"
+msgstr ""
+
 msgid "Help"
 msgstr "Aide"
 
@@ -535,7 +541,7 @@
 msgstr "Nouvelle transition workflow"
 
 msgid "No result matching query"
-msgstr "aucun résultat"
+msgstr "Aucun résultat ne correspond à la requête"
 
 msgid "Non exhaustive list of views that may apply to entities of this type"
 msgstr "Liste non exhausite des vues s'appliquant à ce type d'entité"
@@ -643,9 +649,6 @@
 msgid "Submit bug report by mail"
 msgstr "Soumettre ce rapport par email"
 
-msgid "The repository holds the following entities"
-msgstr "Le dépot contient les entités suivantes"
-
 #, python-format
 msgid "The view %s can not be applied to this query"
 msgstr "La vue %s ne peut être appliquée à cette requête"
@@ -995,6 +998,9 @@
 msgid "add_permission_object"
 msgstr "a la permission d'ajouter"
 
+msgid "add_relation"
+msgstr "ajouter"
+
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
 msgstr "ajout de l'entité %(etype)s #%(eid)s (%(title)s)"
@@ -1330,6 +1336,12 @@
 msgid "click on the box to cancel the deletion"
 msgstr "cliquez dans la zone d'édition pour annuler la suppression"
 
+msgid "click to add a value"
+msgstr "cliquer pour ajouter une valeur"
+
+msgid "click to delete this value"
+msgstr "cliquer pour supprimer cette valeur"
+
 msgid "click to edit this field"
 msgstr "cliquez pour éditer ce champ"
 
@@ -2357,12 +2369,25 @@
 msgid "granted to groups"
 msgstr "accordée aux groupes"
 
-msgid "graphical representation of the instance'schema"
-msgstr "représentation graphique du schéma de l'instance"
+#, python-format
+msgid "graphical representation of %(appid)s data model"
+msgstr "réprésentation graphique du modèle de données de %(appid)s"
 
 #, python-format
-msgid "graphical schema for %s"
-msgstr "graphique du schéma pour %s"
+msgid ""
+"graphical representation of the %(etype)s entity type from %(appid)s data "
+"model"
+msgstr ""
+"réprésentation graphique du modèle de données pour le type d'entité %(etype)"
+"s de %(appid)s"
+
+#, python-format
+msgid ""
+"graphical representation of the %(rtype)s relation type from %(appid)s data "
+"model"
+msgstr ""
+"réprésentation graphique du modèle de données pour le type de relation %"
+"(etype)s de %(appid)s"
 
 #, python-format
 msgid "graphical workflow for %s"
@@ -2909,6 +2934,9 @@
 msgid "no edited fields specified for entity %s"
 msgstr "aucun champ à éditer spécifié pour l'entité %s"
 
+msgid "no related entity"
+msgstr "pas d'entité liée"
+
 msgid "no related project"
 msgstr "pas de projet rattaché"
 
@@ -3834,6 +3862,9 @@
 msgid "update_permission_object"
 msgstr "a la permission de modifier"
 
+msgid "update_relation"
+msgstr "modifier"
+
 msgid "updated"
 msgstr "mis à jour"
 
--- a/interfaces.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/interfaces.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,68 +15,24 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-Standard interfaces.
+"""Standard interfaces. Deprecated in favor of adapters.
 
 .. note::
 
-  The `implements` selector matches not only entity classes but also
-  their interfaces. Writing __select__ = implements('IGeocodable') is
-  a perfectly fine thing to do.
+  The `implements` selector used to match not only entity classes but also their
+  interfaces. This will disappear in a future version. You should define an
+  adapter for that interface and use `adaptable('MyIFace')` selector on appobjects
+  that require that interface.
+
 """
 __docformat__ = "restructuredtext en"
 
 from logilab.common.interface import Interface
 
-class IEmailable(Interface):
-    """interface for emailable entities"""
 
-    def get_email(self):
-        """return email address"""
-
-    @classmethod
-    def allowed_massmail_keys(cls):
-        """returns a set of allowed email substitution keys
-
-        The default is to return the entity's attribute list but an
-        entity class might override this method to allow extra keys.
-        For instance, the Person class might want to return a `companyname`
-        key.
-        """
-
-    def as_email_context(self):
-        """returns the dictionary as used by the sendmail controller to
-        build email bodies.
-
-        NOTE: the dictionary keys should match the list returned by the
-        `allowed_massmail_keys` method.
-        """
-
-
-class IWorkflowable(Interface):
-    """interface for entities dealing with a specific workflow"""
-    # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn
-
-    @property
-    def state(self):
-        """return current state name"""
-
-    def change_state(self, stateeid, trcomment=None, trcommentformat=None):
-        """change the entity's state to the state of the given name in entity's
-        workflow
-        """
-
-    def latest_trinfo(self):
-        """return the latest transition information for this entity
-        """
-
-
+# XXX deprecates in favor of IProgressAdapter
 class IProgress(Interface):
-    """something that has a cost, a state and a progression
-
-    Take a look at cubicweb.mixins.ProgressMixIn for some
-    default implementations
-    """
+    """something that has a cost, a state and a progression"""
 
     @property
     def cost(self):
@@ -112,7 +68,7 @@
     def progress(self):
         """returns the % progress of the task item"""
 
-
+# XXX deprecates in favor of IMileStoneAdapter
 class IMileStone(IProgress):
     """represents an ITask's item"""
 
@@ -135,7 +91,132 @@
     def contractors(self):
         """returns the list of persons supposed to work on this task"""
 
+# XXX deprecates in favor of IEmbedableAdapter
+class IEmbedable(Interface):
+    """interface for embedable entities"""
 
+    def embeded_url(self):
+        """embed action interface"""
+
+# XXX deprecates in favor of ICalendarViewsAdapter
+class ICalendarViews(Interface):
+    """calendar views interface"""
+    def matching_dates(self, begin, end):
+        """
+        :param begin: day considered as begin of the range (`DateTime`)
+        :param end: day considered as end of the range (`DateTime`)
+
+        :return:
+          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+          this entity apply
+        """
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ICalendarable(Interface):
+    """interface for items that do have a begin date 'start' and an end date 'stop'
+    """
+
+    @property
+    def start(self):
+        """return start date"""
+
+    @property
+    def stop(self):
+        """return stop state"""
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ITimetableViews(Interface):
+    """timetable views interface"""
+    def timetable_date(self):
+        """XXX explain
+
+        :return: date (`DateTime`)
+        """
+
+# XXX deprecates in favor of IGeocodableAdapter
+class IGeocodable(Interface):
+    """interface required by geocoding views such as gmap-view"""
+
+    @property
+    def latitude(self):
+        """returns the latitude of the entity"""
+
+    @property
+    def longitude(self):
+        """returns the longitude of the entity"""
+
+    def marker_icon(self):
+        """returns the icon that should be used as the marker"""
+
+# XXX deprecates in favor of ISIOCItemAdapter
+class ISiocItem(Interface):
+    """interface for entities which may be represented as an ISIOC item"""
+
+    def isioc_content(self):
+        """return item's content"""
+
+    def isioc_container(self):
+        """return container entity"""
+
+    def isioc_type(self):
+        """return container type (post, BlogPost, MailMessage)"""
+
+    def isioc_replies(self):
+        """return replies items"""
+
+    def isioc_topics(self):
+        """return topics items"""
+
+# XXX deprecates in favor of ISIOCContainerAdapter
+class ISiocContainer(Interface):
+    """interface for entities which may be represented as an ISIOC container"""
+
+    def isioc_type(self):
+        """return container type (forum, Weblog, MailingList)"""
+
+    def isioc_items(self):
+        """return contained items"""
+
+# XXX deprecates in favor of IEmailableAdapter
+class IFeed(Interface):
+    """interface for entities with rss flux"""
+
+    def rss_feed_url(self):
+        """"""
+
+# XXX deprecates in favor of IDownloadableAdapter
+class IDownloadable(Interface):
+    """interface for downloadable entities"""
+
+    def download_url(self): # XXX not really part of this interface
+        """return an url to download entity's content"""
+    def download_content_type(self):
+        """return MIME type of the downloadable content"""
+    def download_encoding(self):
+        """return encoding of the downloadable content"""
+    def download_file_name(self):
+        """return file name of the downloadable content"""
+    def download_data(self):
+        """return actual data of the downloadable content"""
+
+# XXX deprecates in favor of IPrevNextAdapter
+class IPrevNext(Interface):
+    """interface for entities which can be linked to a previous and/or next
+    entity
+    """
+
+    def next_entity(self):
+        """return the 'next' entity"""
+    def previous_entity(self):
+        """return the 'previous' entity"""
+
+# XXX deprecates in favor of IBreadCrumbsAdapter
+class IBreadCrumbs(Interface):
+
+    def breadcrumbs(self, view, recurs=False):
+        pass
+
+# XXX deprecates in favor of ITreeAdapter
 class ITree(Interface):
 
     def parent(self):
@@ -159,141 +240,3 @@
     def root(self):
         """returns the root object"""
 
-
-## web specific interfaces ####################################################
-
-
-class IPrevNext(Interface):
-    """interface for entities which can be linked to a previous and/or next
-    entity
-    """
-
-    def next_entity(self):
-        """return the 'next' entity"""
-    def previous_entity(self):
-        """return the 'previous' entity"""
-
-
-class IBreadCrumbs(Interface):
-    """interface for entities which can be "located" on some path"""
-
-    # XXX fix recurs !
-    def breadcrumbs(self, view, recurs=False):
-        """return a list containing some:
-
-        * tuple (url, label)
-        * entity
-        * simple label string
-
-        defining path from a root to the current view
-
-        the main view is given as argument so breadcrumbs may vary according
-        to displayed view (may be None). When recursing on a parent entity,
-        the `recurs` argument should be set to True.
-        """
-
-
-class IDownloadable(Interface):
-    """interface for downloadable entities"""
-
-    def download_url(self): # XXX not really part of this interface
-        """return an url to download entity's content"""
-    def download_content_type(self):
-        """return MIME type of the downloadable content"""
-    def download_encoding(self):
-        """return encoding of the downloadable content"""
-    def download_file_name(self):
-        """return file name of the downloadable content"""
-    def download_data(self):
-        """return actual data of the downloadable content"""
-
-
-class IEmbedable(Interface):
-    """interface for embedable entities"""
-
-    def embeded_url(self):
-        """embed action interface"""
-
-class ICalendarable(Interface):
-    """interface for items that do have a begin date 'start' and an end date 'stop'
-    """
-
-    @property
-    def start(self):
-        """return start date"""
-
-    @property
-    def stop(self):
-        """return stop state"""
-
-class ICalendarViews(Interface):
-    """calendar views interface"""
-    def matching_dates(self, begin, end):
-        """
-        :param begin: day considered as begin of the range (`DateTime`)
-        :param end: day considered as end of the range (`DateTime`)
-
-        :return:
-          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
-          this entity apply
-        """
-
-class ITimetableViews(Interface):
-    """timetable views interface"""
-    def timetable_date(self):
-        """XXX explain
-
-        :return: date (`DateTime`)
-        """
-
-class IGeocodable(Interface):
-    """interface required by geocoding views such as gmap-view"""
-
-    @property
-    def latitude(self):
-        """returns the latitude of the entity"""
-
-    @property
-    def longitude(self):
-        """returns the longitude of the entity"""
-
-    def marker_icon(self):
-        """returns the icon that should be used as the marker
-        (returns None for default)
-        """
-
-class IFeed(Interface):
-    """interface for entities with rss flux"""
-
-    def rss_feed_url(self):
-        """return an url which layout sub-entities item
-        """
-
-class ISiocItem(Interface):
-    """interface for entities (which are item
-    in sioc specification) with sioc views"""
-
-    def isioc_content(self):
-        """return content entity"""
-
-    def isioc_container(self):
-        """return container entity"""
-
-    def isioc_type(self):
-        """return container type (post, BlogPost, MailMessage)"""
-
-    def isioc_replies(self):
-        """return replies items"""
-
-    def isioc_topics(self):
-        """return topics items"""
-
-class ISiocContainer(Interface):
-    """interface for entities (which are container
-    in sioc specification) with sioc views"""
-
-    def isioc_type(self):
-        """return container type (forum, Weblog, MailingList)"""
-
-    def isioc_items(self):
-        """return contained items"""
--- a/mail.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/mail.py	Mon Jul 19 15:37:02 2010 +0200
@@ -184,7 +184,7 @@
             # previous email
             if not self.msgid_timestamp:
                 refs = [self.construct_message_id(eid)
-                        for eid in entity.notification_references(self)]
+                        for eid in entity.cw_adapt_to('INotifiable').notification_references(self)]
             else:
                 refs = ()
             msgid = self.construct_message_id(entity.eid)
@@ -198,7 +198,7 @@
             if isinstance(something, Entity):
                 # hi-jack self._cw to get a session for the returned user
                 self._cw = self._cw.hijack_user(something)
-                emailaddr = something.get_email()
+                emailaddr = something.cw_adapt_to('IEmailable').get_email()
             else:
                 emailaddr, lang = something
                 self._cw.set_language(lang)
@@ -246,7 +246,8 @@
     # email generation helpers #################################################
 
     def construct_message_id(self, eid):
-        return construct_message_id(self._cw.vreg.config.appid, eid, self.msgid_timestamp)
+        return construct_message_id(self._cw.vreg.config.appid, eid,
+                                    self.msgid_timestamp)
 
     def format_field(self, attr, value):
         return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value}
--- a/migration.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/migration.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""utilities for instances migration
+"""utilities for instances migration"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -111,7 +110,7 @@
         self.config = config
         if config:
             # no config on shell to a remote instance
-            self.config.init_log(logthreshold=logging.ERROR, debug=True)
+            self.config.init_log(logthreshold=logging.ERROR)
         # 0: no confirmation, 1: only main commands confirmed, 2 ask for everything
         self.verbosity = verbosity
         self.need_wrap = True
@@ -281,14 +280,25 @@
         return context
 
     def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
-        """execute a migration script
-        in interactive mode,  display the migration script path, ask for
-        confirmation and execute it if confirmed
+        """execute a migration script in interactive mode
+
+        Display the migration script path, ask for confirmation and execute it
+        if confirmed
+
+        Context environment can have these variables defined:
+        - __name__ : will be determine by funcname parameter
+        - __file__ : is the name of the script if it exists
+        - __args__ : script arguments coming from command-line
+
+        :param migrscript: name of the script
+        :param funcname: defines __name__ inside the shell (or use __main__)
+        :params args: optional arguments for funcname
+        :keyword scriptargs: optional arguments of the script
         """
         migrscript = os.path.normpath(migrscript)
         if migrscript.endswith('.py'):
             script_mode = 'python'
-        elif migrscript.endswith('.txt') or migrscript.endswith('.rst'):
+        elif migrscript.endswith(('.txt', '.rst')):
             script_mode = 'doctest'
         else:
             raise Exception('This is not a valid cubicweb shell input')
@@ -300,7 +310,8 @@
                 pyname = '__main__'
             else:
                 pyname = splitext(basename(migrscript))[0]
-            scriptlocals.update({'__file__': migrscript, '__name__': pyname})
+            scriptlocals.update({'__file__': migrscript, '__name__': pyname,
+                                 '__args__': kwargs.pop("scriptargs", [])})
             execfile(migrscript, scriptlocals)
             if funcname is not None:
                 try:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.9.0_Any.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,3 @@
+if repo.system_source.dbdriver == 'postgres':
+    sql('ALTER TABLE appears ADD COLUMN weight float')
+    sql('UPDATE appears SET weight=1.0 ')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/ldap_change_base_dn.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,24 @@
+from base64 import b64decode, b64encode
+try:
+    uri, newdn = __args__
+except ValueError:
+    print 'USAGE: cubicweb-ctl shell <instance> ldap_change_base_dn.py -- <ldap source uri> <new dn>'
+    print
+    print 'you should not have updated your sources file yet'
+
+olddn = repo.config.sources()[uri]['user-base-dn']
+
+assert olddn != newdn
+
+raw_input("Ensure you've stopped the instance, type enter when done.")
+
+for eid, extid in sql("SELECT eid, extid FROM entities WHERE source='%s'" % uri):
+    olduserdn = b64decode(extid)
+    newuserdn = olduserdn.replace(olddn, newdn)
+    if newuserdn != olduserdn:
+        print olduserdn, '->', newuserdn
+        sql("UPDATE entities SET extid='%s' WHERE eid=%s" % (b64encode(newuserdn), eid))
+
+commit()
+
+print 'you can now update the sources file to the new dn and restart the instance'
--- a/mixins.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/mixins.py	Mon Jul 19 15:37:02 2010 +0200
@@ -21,9 +21,10 @@
 from itertools import chain
 
 from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated, class_deprecated
 
 from cubicweb.selectors import implements
-from cubicweb.interfaces import IEmailable, ITree
+from cubicweb.interfaces import ITree
 
 
 class TreeMixIn(object):
@@ -33,6 +34,9 @@
     tree_attribute, parent_target and children_target class attribute to
     benefit from this default implementation
     """
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead'
+
     tree_attribute = None
     # XXX misnamed
     parent_target = 'subject'
@@ -117,16 +121,6 @@
             return chain([self], _uptoroot(self))
         return _uptoroot(self)
 
-    def notification_references(self, view):
-        """used to control References field of email send on notification
-        for this entity. `view` is the notification view.
-
-        Should return a list of eids which can be used to generate message ids
-        of previously sent email
-        """
-        return self.path()[:-1]
-
-
     ## ITree interface ########################################################
     def parent(self):
         """return the parent entity if any, else None (e.g. if we are on the
@@ -151,7 +145,7 @@
                                 entities=entities)
 
     def children_rql(self):
-        return self.related_rql(self.tree_attribute, self.children_target)
+        return self.cw_related_rql(self.tree_attribute, self.children_target)
 
     def is_leaf(self):
         return len(self.children()) == 0
@@ -171,8 +165,7 @@
     NOTE: The default implementation is based on the
     primary_email / use_email scheme
     """
-    __implements__ = (IEmailable,)
-
+    @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()")
     def get_email(self):
         if getattr(self, 'primary_email', None):
             return self.primary_email[0].address
@@ -180,28 +173,6 @@
             return self.use_email[0].address
         return None
 
-    @classmethod
-    def allowed_massmail_keys(cls):
-        """returns a set of allowed email substitution keys
-
-        The default is to return the entity's attribute list but an
-        entity class might override this method to allow extra keys.
-        For instance, the Person class might want to return a `companyname`
-        key.
-        """
-        return set(rschema.type
-                   for rschema, attrtype in cls.e_schema.attribute_definitions()
-                   if attrtype.type not in ('Password', 'Bytes'))
-
-    def as_email_context(self):
-        """returns the dictionary as used by the sendmail controller to
-        build email bodies.
-
-        NOTE: the dictionary keys should match the list returned by the
-        `allowed_massmail_keys` method.
-        """
-        return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
-
 
 """pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
 classes which have the relation described by the dict's key.
@@ -215,7 +186,7 @@
     }
 
 
-
+# XXX move to cubicweb.web.views.treeview once we delete usage from this file
 def _done_init(done, view, row, col):
     """handle an infinite recursion safety belt"""
     if done is None:
@@ -223,7 +194,7 @@
     entity = view.cw_rset.get_entity(row, col)
     if entity.eid in done:
         msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
-            'rel': entity.tree_attribute,
+            'rel': entity.cw_adapt_to('ITree').tree_relation,
             'eid': entity.eid
             }
         return None, msg
@@ -233,16 +204,20 @@
 
 class TreeViewMixIn(object):
     """a recursive tree view"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead'
+
     __regid__ = 'tree'
+    __select__ = implements(ITree, warn=False)
     item_vid = 'treeitem'
-    __select__ = implements(ITree)
 
     def call(self, done=None, **kwargs):
         if done is None:
             done = set()
         super(TreeViewMixIn, self).call(done=done, **kwargs)
 
-    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+    def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs):
+        assert maxlevel is None or maxlevel > 0
         done, entity = _done_init(done, self, row, col)
         if done is None:
             # entity is actually an error message
@@ -250,8 +225,14 @@
             return
         self.open_item(entity)
         entity.view(vid or self.item_vid, w=self.w, **kwargs)
+        if maxlevel is not None:
+            maxlevel -= 1
+            if maxlevel == 0:
+                self.close_item(entity)
+                return
         relatedrset = entity.children(entities=False)
-        self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
+        self.wview(self.__regid__, relatedrset, 'null', done=done,
+                   maxlevel=maxlevel, **kwargs)
         self.close_item(entity)
 
     def open_item(self, entity):
@@ -262,6 +243,8 @@
 
 class TreePathMixIn(object):
     """a recursive path view"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead'
     __regid__ = 'path'
     item_vid = 'oneline'
     separator = u'&#160;&gt;&#160;'
@@ -286,6 +269,8 @@
 
 class ProgressMixIn(object):
     """provide a default implementations for IProgress interface methods"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead'
 
     @property
     def cost(self):
--- a/mttransforms.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/mttransforms.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""mime type transformation engine for cubicweb, based on mtconverter
+"""mime type transformation engine for cubicweb, based on mtconverter"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab import mtconverter
--- a/req.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/req.py	Mon Jul 19 15:37:02 2010 +0200
@@ -133,7 +133,7 @@
         Example (in a shell session):
 
         >>> c = create_entity('Company', name=u'Logilab')
-        >>> create_entity('Person', firstname=u'John', lastname=u'Doe',
+        >>> create_entity('Person', firstname=u'John', surname=u'Doe',
         ...               works_for=c)
 
         """
@@ -279,7 +279,7 @@
         user = self.user
         userinfo['login'] = user.login
         userinfo['name'] = user.name()
-        userinfo['email'] = user.get_email()
+        userinfo['email'] = user.cw_adapt_to('IEmailable').get_email()
         return userinfo
 
     def is_internal_session(self):
@@ -373,11 +373,11 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
-    # abstract methods to override according to the web front-end #############
-
     def base_url(self):
         """return the root url of the instance"""
-        raise NotImplementedError
+        return self.vreg.config['base-url']
+
+    # abstract methods to override according to the web front-end #############
 
     def describe(self, eid):
         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
--- a/rqlrewrite.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/rqlrewrite.py	Mon Jul 19 15:37:02 2010 +0200
@@ -19,8 +19,8 @@
 tree.
 
 This is used for instance for read security checking in the repository.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from rql import nodes as n, stmts, TypeResolverException
--- a/rset.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/rset.py	Mon Jul 19 15:37:02 2010 +0200
@@ -77,10 +77,16 @@
         rows = self.rows
         if len(rows) > 10:
             rows = rows[:10] + ['...']
+        if len(rows) > 1:
+            # add a line break before first entity if more that one.
+            pattern = '<resultset %r (%s rows):\n%s>' 
+        else:
+            pattern = '<resultset %r (%s rows): %s>'
+
         if not self.description:
-            return '<resultset %r (%s rows): %s>' % (self.rql, len(self.rows),
+            return pattern % (self.rql, len(self.rows),
                                                      '\n'.join(str(r) for r in rows))
-        return '<resultset %r (%s rows): %s>' % (self.rql, len(self.rows),
+        return pattern % (self.rql, len(self.rows),
                                                  '\n'.join('%s (%s)' % (r, d)
                                                            for r, d in zip(rows, self.description)))
 
@@ -453,7 +459,7 @@
         etype = self.description[row][col]
         entity = self.req.vreg['etypes'].etype_class(etype)(req, rset=self,
                                                             row=row, col=col)
-        entity.set_eid(eid)
+        entity.eid = eid
         # cache entity
         req.set_entity_cache(entity)
         eschema = entity.e_schema
@@ -494,7 +500,7 @@
                         rrset.req = req
                     else:
                         rrset = self._build_entity(row, outerselidx).as_rset()
-                    entity.set_related_cache(attr, role, rrset)
+                    entity.cw_set_relation_cache(attr, role, rrset)
         return entity
 
     @cached
--- a/schema.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/schema.py	Mon Jul 19 15:37:02 2010 +0200
@@ -417,7 +417,7 @@
             # avoid deleting the relation type accidentally...
             self.schema['has_text'].del_relation_def(self, self.schema['String'])
 
-    def schema_entity(self):
+    def schema_entity(self): # XXX @property for consistency with meta
         """return True if this entity type is used to build the schema"""
         return self.type in SCHEMA_TYPES
 
@@ -441,7 +441,7 @@
     def meta(self):
         return self.type in META_RTYPES
 
-    def schema_relation(self):
+    def schema_relation(self): # XXX @property for consistency with meta
         """return True if this relation type is used to build the schema"""
         return self.type in SCHEMA_TYPES
 
@@ -572,7 +572,13 @@
         rdef.name = rdef.name.lower()
         rdef.subject = bw_normalize_etype(rdef.subject)
         rdef.object = bw_normalize_etype(rdef.object)
-        rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
+        try:
+            rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
+        except BadSchemaDefinition:
+            reversed_etype_map = dict( (v, k) for k, v in ETYPE_NAME_MAP.iteritems() )
+            if rdef.subject in reversed_etype_map or rdef.object in reversed_etype_map:
+                return
+            raise
         if rdefs:
             try:
                 self._eid_index[rdef.eid] = rdefs
--- a/schemas/base.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/schemas/base.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""core CubicWeb schema, but not necessary at bootstrap time
+"""core CubicWeb schema, but not necessary at bootstrap time"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
--- a/selectors.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/selectors.py	Mon Jul 19 15:37:02 2010 +0200
@@ -169,7 +169,7 @@
 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.selectors.lltrace
+.. autofunction:: cubicweb.appobject.lltrace
 
 .. note::
   Selectors __call__ should *always* return a positive integer, and shall never
@@ -183,10 +183,10 @@
 
 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.selectors.traced_selection` context
+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.selectors.traced_selection
+.. autoclass:: cubicweb.appobject.traced_selection
 
 
 .. |cubicweb| replace:: *CubicWeb*
@@ -202,90 +202,15 @@
 from logilab.common.interface import implements as implements_iface
 
 from yams import BASE_TYPES
+from rql.nodes import Function
 
-from cubicweb import Unauthorized, NoSelectableObject, NotAnEntity, role
+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.appobject import Selector, objectify_selector, yes
-from cubicweb.vregistry import class_regid
-from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.appobject import Selector, objectify_selector, lltrace, yes
 from cubicweb.schema import split_expression
 
-# helpers for debugging selectors
-SELECTOR_LOGGER = logging.getLogger('cubicweb.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
-
+from cubicweb.appobject import traced_selection # XXX for bw compat
 
 def score_interface(etypesreg, cls_or_inst, cls, iface):
     """Return XXX if the give object (maybe an instance or class) implements
@@ -302,6 +227,7 @@
             if iface is basecls:
                 return index + 3
         return 0
+    # XXX iface in implements deprecated in 3.9
     if implements_iface(cls_or_inst, iface):
         # implenting an interface takes precedence other special Any interface
         return 2
@@ -321,31 +247,6 @@
         return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
 
 
-class ImplementsMixIn(object):
-    """mix-in class for selectors checking implemented interfaces of something
-    """
-    def __init__(self, *expected_ifaces, **kwargs):
-        super(ImplementsMixIn, self).__init__(**kwargs)
-        self.expected_ifaces = expected_ifaces
-
-    def __str__(self):
-        return '%s(%s)' % (self.__class__.__name__,
-                           ','.join(str(s) for s in self.expected_ifaces))
-
-    def score_interfaces(self, req, cls_or_inst, cls):
-        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, cls_or_inst, cls, iface)
-        return score
-
-
 class EClassSelector(Selector):
     """abstract class for selectors working on *entity class(es)* specified
     explicitly or found of the result set.
@@ -375,14 +276,17 @@
         self.accept_none = accept_none
 
     @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+    def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+                 **kwargs):
         if kwargs.get('entity'):
             return self.score_class(kwargs['entity'].__class__, req)
         if not rset:
             return 0
         score = 0
         if row is None:
-            if not self.accept_none:
+            if accept_none is None:
+                accept_none = self.accept_none
+            if not accept_none:
                 if any(rset[i][col] is None for i in xrange(len(rset))):
                     return 0
             for etype in rset.column_types(col):
@@ -442,7 +346,8 @@
     """
 
     @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+    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
@@ -450,9 +355,11 @@
             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 self.accept_none:
+                    if not accept_none:
                         return 0
                     continue
                 escore = self.score(req, rset, row, col)
@@ -482,7 +389,7 @@
     """Take a list of expected values as initializer argument and store them
     into the :attr:`expected` set attribute.
 
-    You should implements the :meth:`_get_value(cls, req, **kwargs)` method
+    You should implement the :meth:`_get_value(cls, req, **kwargs)` method
     which should return the value for the given context. The selector will then
     return 1 if the value is expected, else 0.
     """
@@ -528,19 +435,42 @@
 
     * `registry`, a registry name
 
-    * `regid`, an object identifier in this registry
+    * `regids`, object identifiers in this registry, one of them should be
+      selectable.
     """
-    def __init__(self, registry, regid):
+    selectable_score = 1
+    def __init__(self, registry, *regids):
         self.registry = registry
-        self.regid = regid
+        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:
+                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.
+    """
+    # being adaptable to an interface takes precedence other is_instance('Any'),
+    # hence return 2 (is_instance('Any') score is 1)
+    selectable_score = 2
+    def __init__(self, *regids):
+        super(adaptable, self).__init__('adapters', *regids)
 
     def __call__(self, cls, req, **kwargs):
-        try:
-            req.vreg[self.registry].select(self.regid, req, **kwargs)
-            return 1
-        except NoSelectableObject:
-            return 0
-
+        kwargs.setdefault('accept_none', False)
+        return super(adaptable, self).__call__(cls, req, **kwargs)
 
 # rset selectors ##############################################################
 
@@ -586,8 +516,8 @@
 @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 if a specific row in the
-    result set is specified ('row' argument).
+    """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 not None and (row is not None or rset.rowcount == 1):
         return 1
@@ -595,7 +525,7 @@
 
 
 class multi_lines_rset(Selector):
-    """If `nb`is specified, return 1 if the result set has exactly `nb` row of
+    """If `nb` is specified, return 1 if the result set has exactly `nb` row of
     result. Else (`nb` is None), return 1 if the result set contains *at least*
     two rows.
     """
@@ -609,11 +539,11 @@
 
     @lltrace
     def __call__(self, cls, req, rset=None, **kwargs):
-        return rset is not None and self.match_expected(rset.rowcount)
+        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
+    """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.
     """
@@ -659,12 +589,17 @@
 @lltrace
 def sorted_rset(cls, req, rset=None, **kwargs):
     """Return 1 for sorted result set (e.g. from an RQL query containing an
-    :ref:ORDERBY clause.
+    :ref: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
-    rqlst = rset.syntax_tree()
-    if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
+    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
 
@@ -712,7 +647,7 @@
 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
-    `implements('Any')` but more optimized.
+    `is_instance('Any')` but more optimized.
 
     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
     class lookup / score rules according to the input context.
@@ -726,7 +661,7 @@
         return 1 # necessarily true if we're there
 
 
-class implements(ImplementsMixIn, EClassSelector):
+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.
@@ -739,10 +674,104 @@
 
     .. 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):
         return self.score_interfaces(req, eclass, eclass)
 
+    def score_interfaces(self, req, cls_or_inst, cls):
+        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, cls_or_inst, cls, 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):
+        return self.score_etypes(req, eclass, eclass)
+
+    def score_etypes(self, req, cls_or_inst, cls):
+        # cache on vreg to avoid reloading issues
+        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),
+                         etypesreg.parent_classes(etype))
+                        )
+                except KeyError:
+                    continue # entity type not in the schema
+        score = 0
+        for iface, parents in expected_eclasses:
+            # adjust score according to class proximity
+            if iface is cls:
+                score += len(parents) + 4
+            elif iface is parents[-1]: # Any
+                score += 1
+            else:
+                for index, basecls in enumerate(reversed(parents[:-1])):
+                    if iface is basecls:
+                        score += index + 3
+                        break
+        return score
+
 
 class score_entity(EntitySelector):
     """Return score according to an arbitrary function given as argument which
@@ -766,6 +795,26 @@
         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=False):
+        super(has_mimetype, self).__init__(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).
@@ -1009,7 +1058,7 @@
         return self.score(req, rset, row, col)
 
     def score_entity(self, entity):
-        if entity.has_perm(self.action):
+        if entity.cw_has_perm(self.action):
             return 1
         return 0
 
@@ -1233,18 +1282,15 @@
         return len(self.expected)
 
 
-class specified_etype_implements(implements):
+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 or implements at
-    least one of the given interfaces. If multiple arguments are given, matching
-    one of them is enough.
+    class is of one of the type(s) given to the initializer. 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.
-
-    .. note:: when interface is an entity class, the score will reflect class
+    .. 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
@@ -1300,25 +1346,30 @@
 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 score_entity x: x.state == 'bla'
-    selector to avoid some gotchas:
+    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, not entity.state for repository side
+    * you must use the latest tr info, not entity.in_state for repository side
       checking of the current state
     """
     def __init__(self, *states):
         def score(entity, states=set(states)):
+            trinfo = entity.cw_adapt_to('IWorkflowable').latest_trinfo()
             try:
-                return entity.latest_trinfo().new_state.name in states
+                return trinfo.new_state.name in states
             except AttributeError:
                 return None
         super(is_in_state, self).__init__(score)
 
+@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 ############################################################
 
-entity_implements = class_renamed('entity_implements', implements)
+entity_implements = class_renamed('entity_implements', is_instance)
 
 class _but_etype(EntitySelector):
     """accept if the given entity types are not found in the result set.
@@ -1336,7 +1387,7 @@
             return 0
         return 1
 
-but_etype = class_renamed('but_etype', _but_etype, 'use ~implements(*etypes) instead')
+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)?
--- a/server/hook.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/hook.py	Mon Jul 19 15:37:02 2010 +0200
@@ -63,7 +63,7 @@
 from cubicweb import RegistryNotFound
 from cubicweb.cwvreg import CWRegistry, VRegistry
 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
-                                implements)
+                                is_instance)
 from cubicweb.appobject import AppObject
 from cubicweb.server.session import security_enabled
 
@@ -246,7 +246,7 @@
                 if ertype.islower():
                     rtypes.append(ertype)
                 else:
-                    cls.__select__ = cls.__select__ & implements(ertype)
+                    cls.__select__ = cls.__select__ & is_instance(ertype)
             if rtypes:
                 cls.__select__ = cls.__select__ & match_rtype(*rtypes)
         return cls
@@ -262,7 +262,7 @@
     def __call__(self):
         if hasattr(self, 'call'):
             cls = self.__class__
-            warn('[3.6] %s.%s: call is deprecated, implements __call__'
+            warn('[3.6] %s.%s: call is deprecated, implement __call__'
                  % (cls.__module__, cls.__name__), DeprecationWarning)
             if self.event.endswith('_relation'):
                 self.call(self._cw, self.eidfrom, self.rtype, self.eidto)
--- a/server/migractions.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/migractions.py	Mon Jul 19 15:37:02 2010 +0200
@@ -50,7 +50,8 @@
 from yams.schema2sql import eschema2sql, rschema2sql
 
 from cubicweb import AuthenticationError
-from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES,
+from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES,
+                             PURE_VIRTUAL_RTYPES,
                              CubicWebRelationSchema, order_eschemas)
 from cubicweb.dbapi import get_repository, repo_connect
 from cubicweb.migration import MigrationHelper, yes
@@ -855,9 +856,39 @@
         `oldname` is a string giving the name of the existing entity type
         `newname` is a string giving the name of the renamed entity type
         """
-        self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(oldname)s',
-                     {'newname' : unicode(newname), 'oldname' : oldname},
-                     ask_confirm=False)
+        schema = self.repo.schema
+        if newname in schema:
+            assert oldname in ETYPE_NAME_MAP, \
+                   '%s should be mappend to %s in ETYPE_NAME_MAP' % (oldname, newname)
+            attrs = ','.join([SQL_PREFIX + rschema.type
+                              for rschema in schema[newname].subject_relations()
+                              if (rschema.final or rschema.inlined)
+                              and not rschema in PURE_VIRTUAL_RTYPES])
+            self.sqlexec('INSERT INTO %s%s(%s) SELECT %s FROM %s%s' % (
+                SQL_PREFIX, newname, attrs, attrs, SQL_PREFIX, oldname))
+            # old entity type has not been added to the schema, can't gather it
+            new = schema.eschema(newname)
+            oldeid = self.rqlexec('CWEType ET WHERE ET name %(on)s', {'on': oldname},
+                                  ask_confirm=False)[0][0]
+            # backport old type relations to new type
+            # XXX workflows, other relations?
+            self.rqlexec('SET X from_entity NET WHERE X from_entity OET, '
+                         'NOT EXISTS(X2 from_entity NET, X relation_type XRT, X2 relation_type XRT, '
+                         'X to_entity XTE, X2 to_entity XTE), '
+                         'OET eid %(o)s, NET eid %(n)s',
+                         {'o': oldeid, 'n': new.eid}, ask_confirm=False)
+            self.rqlexec('SET X to_entity NET WHERE X to_entity OET, '
+                         'NOT EXISTS(X2 to_entity NET, X relation_type XRT, X2 relation_type XRT, '
+                         'X from_entity XTE, X2 from_entity XTE), '
+                         'OET eid %(o)s, NET eid %(n)s',
+                         {'o': oldeid, 'n': new.eid}, ask_confirm=False)
+            # remove the old type: use rql to propagate deletion
+            self.rqlexec('DELETE CWEType ET WHERE ET name %(on)s', {'on': oldname},
+                         ask_confirm=False)
+        else:
+            self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(on)s',
+                         {'newname' : unicode(newname), 'on' : oldname},
+                         ask_confirm=False)
         if commit:
             self.commit()
 
@@ -1152,10 +1183,10 @@
         if commit:
             self.commit()
 
-    @deprecated('[3.5] use entity.fire_transition("transition") or entity.change_state("state")',
-                stacklevel=3)
+    @deprecated('[3.5] use iworkflowable.fire_transition("transition") or '
+                'iworkflowable.change_state("state")', stacklevel=3)
     def cmd_set_state(self, eid, statename, commit=False):
-        self._cw.entity_from_eid(eid).change_state(statename)
+        self._cw.entity_from_eid(eid).cw_adapt_to('IWorkflowable').change_state(statename)
         if commit:
             self.commit()
 
@@ -1215,6 +1246,13 @@
             self.commit()
         return entity
 
+    def cmd_update_etype_fti_weight(self, etype, weight):
+        if self.repo.system_source.dbdriver == 'postgres':
+            self.sqlexec('UPDATE appears SET weight=%(weight)s '
+                         'FROM entities as X '
+                         'WHERE X.eid=appears.uid AND X.type=%(type)s',
+                         {'type': etype, 'weight': weight}, ask_confirm=False)
+
     def cmd_reindex_entities(self, etypes=None):
         """force reindexaction of entities of the given types or of all
         indexable entity types
--- a/server/msplanner.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/msplanner.py	Mon Jul 19 15:37:02 2010 +0200
@@ -96,7 +96,7 @@
 
 from rql.stmts import Union, Select
 from rql.nodes import (VariableRef, Comparison, Relation, Constant, Variable,
-                       Not, Exists)
+                       Not, Exists, SortTerm, Function)
 
 from cubicweb import server
 from cubicweb.utils import make_uid
@@ -1330,6 +1330,12 @@
                                                orderby.append)
                 if orderby:
                     newroot.set_orderby(orderby)
+            elif rqlst.orderby:
+                for sortterm in rqlst.orderby:
+                    if any(f for f in sortterm.iget_nodes(Function) if f.name == 'FTIRANK'):
+                        newnode, oldnode = sortterm.accept(self, newroot, terms)
+                        if newnode is not None:
+                            newroot.add_sort_term(newnode)
             self.process_selection(newroot, terms, rqlst)
         elif not newroot.where:
             # no restrictions have been copied, just select terms and add
@@ -1530,12 +1536,38 @@
             copy.operator = '='
         return copy, node
 
+    def visit_function(self, node, newroot, terms):
+        if node.name == 'FTIRANK':
+            # FTIRANK is somewhat special... Rank function should be included in
+            # the same query has the has_text relation, potentially added to
+            # selection for latter usage
+            if not self.hasaggrstep and self.final and node not in self.skip:
+                return self.visit_default(node, newroot, terms)
+            elif any(s for s in self.sources if s.uri != 'system'):
+                return None, node
+            # p = node.parent
+            # while p is not None and not isinstance(p, SortTerm):
+            #     p = p.parent
+            # if isinstance(p, SortTerm):
+            if not self.hasaggrstep and self.final and node in self.skip:
+                return Constant(self.skip[node], 'Int'), node
+            # XXX only if not yet selected
+            newroot.append_selected(node.copy(newroot))
+            self.skip[node] = len(newroot.selection)
+            return None, node
+        return self.visit_default(node, newroot, terms)
+
     def visit_default(self, node, newroot, terms):
         subparts, node = self._visit_children(node, newroot, terms)
         return copy_node(newroot, node, subparts), node
 
-    visit_mathexpression = visit_constant = visit_function = visit_default
-    visit_sort = visit_sortterm = visit_default
+    visit_mathexpression = visit_constant = visit_default
+
+    def visit_sortterm(self, node, newroot, terms):
+        subparts, node = self._visit_children(node, newroot, terms)
+        if not subparts:
+            return None, node
+        return copy_node(newroot, node, subparts), node
 
     def _visit_children(self, node, newroot, terms):
         subparts = []
--- a/server/mssteps.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/mssteps.py	Mon Jul 19 15:37:02 2010 +0200
@@ -140,13 +140,6 @@
 
     def mytest_repr(self):
         """return a representation of this step suitable for test"""
-        sel = self.select.selection
-        restr = self.select.where
-        self.select.selection = self.selection
-        self.select.where = None
-        rql = self.select.as_string(kwargs=self.plan.args)
-        self.select.selection = sel
-        self.select.where = restr
         try:
             # rely on a monkey patch (cf unittest_querier)
             table = self.plan.tablesinorder[self.table]
@@ -155,12 +148,19 @@
             # not monkey patched
             table = self.table
             outputtable = self.outputtable
-        return (self.__class__.__name__, rql, self.limit, self.offset, table,
-                outputtable)
+        sql = self.get_sql().replace(self.table, table)
+        return (self.__class__.__name__, sql, outputtable)
 
     def execute(self):
         """execute this step"""
         self.execute_children()
+        sql = self.get_sql()
+        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)
+
+    def get_sql(self):
         self.inputmap = inputmap = self.children[-1].outputmap
         # get the select clause
         clause = []
@@ -223,17 +223,15 @@
             sql.append('LIMIT %s' % self.limit)
         if self.offset:
             sql.append('OFFSET %s' % self.offset)
-        #print 'DATA', plan.sqlexec('SELECT * FROM %s' % self.table, None)
-        sql = ' '.join(sql)
-        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)
+        return ' '.join(sql)
 
     def visit_function(self, function):
         """generate SQL name for a function"""
-        return '%s(%s)' % (function.name,
-                           ','.join(c.accept(self) for c in function.children))
+        try:
+            return self.children[0].outputmap[str(function)]
+        except KeyError:
+            return '%s(%s)' % (function.name,
+                               ','.join(c.accept(self) for c in function.children))
 
     def visit_variableref(self, variableref):
         """get the sql name for a variable reference"""
--- a/server/querier.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/querier.py	Mon Jul 19 15:37:02 2010 +0200
@@ -29,7 +29,8 @@
 from logilab.common.compat import any
 from rql import RQLSyntaxError
 from rql.stmts import Union, Select
-from rql.nodes import Relation, VariableRef, Constant, SubQuery, Exists, Not
+from rql.nodes import (Relation, VariableRef, Constant, SubQuery, Function,
+                       Exists, Not)
 
 from cubicweb import Unauthorized, QueryError, UnknownEid, typed_eid
 from cubicweb import server
@@ -50,7 +51,8 @@
         key = term.as_string()
         value = '%s.C%s' % (table, i)
         if varmap.get(key, value) != value:
-            raise Exception('variable name conflict on %s' % key)
+            raise Exception('variable name conflict on %s: got %s / %s'
+                            % (key, value, varmap))
         varmap[key] = value
 
 # permission utilities ########################################################
@@ -294,7 +296,26 @@
                     for term in origselection:
                         newselect.append_selected(term.copy(newselect))
                     if select.orderby:
-                        newselect.set_orderby([s.copy(newselect) for s in select.orderby])
+                        sortterms = []
+                        for sortterm in select.orderby:
+                            sortterms.append(sortterm.copy(newselect))
+                            for fnode in sortterm.get_nodes(Function):
+                                if fnode.name == 'FTIRANK':
+                                    # we've to fetch the has_text relation as well
+                                    var = fnode.children[0].variable
+                                    rel = iter(var.stinfo['ftirels']).next()
+                                    assert not rel.ored(), 'unsupported'
+                                    newselect.add_restriction(rel.copy(newselect))
+                                    # remove relation from the orig select and
+                                    # cleanup variable stinfo
+                                    rel.parent.remove(rel)
+                                    var.stinfo['ftirels'].remove(rel)
+                                    var.stinfo['relations'].remove(rel)
+                                    # XXX not properly re-annotated after security insertion?
+                                    newvar = newselect.get_variable(var.name)
+                                    newvar.stinfo.setdefault('ftirels', set()).add(rel)
+                                    newvar.stinfo.setdefault('relations', set()).add(rel)
+                        newselect.set_orderby(sortterms)
                         _expand_selection(select.orderby, selected, aliases, select, newselect)
                         select.orderby = () # XXX dereference?
                     if select.groupby:
@@ -339,6 +360,7 @@
                     select.set_possible_types(localchecks[()])
                     add_types_restriction(self.schema, select)
                     add_noinvariant(noinvariant, restricted, select, nbtrees)
+                self.rqlhelper.annotate(union)
 
     def _check_permissions(self, rqlst):
         """return a dict defining "local checks", e.g. RQLExpression defined in
@@ -571,6 +593,8 @@
         # rql parsing / analysing helper
         self.solutions = repo.vreg.solutions
         rqlhelper = repo.vreg.rqlhelper
+        # set backend on the rql helper, will be used for function checking
+        rqlhelper.backend = repo.config.sources()['system']['db-driver']
         self._parse = rqlhelper.parse
         self._annotate = rqlhelper.annotate
         # rql planner
--- a/server/repository.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/repository.py	Mon Jul 19 15:37:02 2010 +0200
@@ -104,10 +104,10 @@
     XXX protect pyro access
     """
 
-    def __init__(self, config, vreg=None, debug=False):
+    def __init__(self, config, vreg=None):
         self.config = config
         if vreg is None:
-            vreg = cwvreg.CubicWebVRegistry(config, debug)
+            vreg = cwvreg.CubicWebVRegistry(config)
         self.vreg = vreg
         self.pyro_registered = False
         self.info('starting repository from %s', self.config.apphome)
@@ -154,13 +154,6 @@
                 if not isinstance(session.user, InternalManager):
                     session.user.__class__ = usercls
 
-    def _bootstrap_hook_registry(self):
-        """called during bootstrap since we need the metadata hooks"""
-        hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
-        self.vreg.init_registration([hooksdirectory])
-        self.vreg.load_file(join(hooksdirectory, 'metadata.py'),
-                            'cubicweb.hooks.metadata')
-
     def open_connections_pools(self):
         config = self.config
         self._available_pools = Queue.Queue()
@@ -186,7 +179,9 @@
             for modname in ('__init__', 'authobjs', 'wfobjs'):
                 self.vreg.load_file(join(etdirectory, '%s.py' % modname),
                                     'cubicweb.entities.%s' % modname)
-            self._bootstrap_hook_registry()
+            hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
+            self.vreg.load_file(join(hooksdirectory, 'metadata.py'),
+                                'cubicweb.hooks.metadata')
         elif config.read_instance_schema:
             # normal start: load the instance schema from the database
             self.fill_schema()
@@ -234,8 +229,7 @@
         if resetvreg:
             if self.config._cubes is None:
                 self.config.init_cubes(self.get_cubes())
-            # full reload of all appobjects
-            self.vreg.reset()
+            # trigger full reload of all appobjects
             self.vreg.set_schema(schema)
         else:
             self.vreg._set_schema(schema)
@@ -392,7 +386,7 @@
             raise AuthenticationError('authentication failed with all sources')
         cwuser = self._build_user(session, eid)
         if self.config.consider_user_state and \
-               not cwuser.state in cwuser.AUTHENTICABLE_STATES:
+               not cwuser.cw_adapt_to('IWorkflowable').state in cwuser.AUTHENTICABLE_STATES:
             raise AuthenticationError('user is not in authenticable state')
         return cwuser
 
@@ -573,7 +567,7 @@
             session.close()
         session = Session(user, self, cnxprops)
         user._cw = user.cw_rset.req = session
-        user.clear_related_cache()
+        user.cw_clear_relation_cache()
         self._sessions[session.id] = session
         self.info('opened session %s for user %s', session.id, login)
         self.hm.call_hooks('session_open', session)
@@ -932,7 +926,7 @@
             self._extid_cache[cachekey] = eid
             self._type_source_cache[eid] = (etype, source.uri, extid)
             entity = source.before_entity_insertion(session, extid, etype, eid)
-            entity.edited_attributes = set(entity)
+            entity.edited_attributes = set(entity.cw_attr_cache)
             if source.should_call_hooks:
                 self.hm.call_hooks('before_add_entity', session, entity=entity)
             # XXX call add_info with complete=False ?
@@ -1042,37 +1036,32 @@
         the entity instance
         """
         # init edited_attributes before calling before_add_entity hooks
-        entity._is_saved = False # entity has an eid but is not yet saved
-        entity.edited_attributes = set(entity)
-        entity_ = entity.pre_add_hook()
-        # XXX kill that transmutation feature !
-        if not entity_ is entity:
-            entity.__class__ = entity_.__class__
-            entity.__dict__.update(entity_.__dict__)
+        entity._cw_is_saved = False # entity has an eid but is not yet saved
+        entity.edited_attributes = set(entity.cw_attr_cache) # XXX cw_edited_attributes
         eschema = entity.e_schema
         source = self.locate_etype_source(entity.__regid__)
         # allocate an eid to the entity before calling hooks
-        entity.set_eid(self.system_source.create_eid(session))
+        entity.eid = self.system_source.create_eid(session)
         # set caches asap
         extid = self.init_entity_caches(session, entity, source)
         if server.DEBUG & server.DBG_REPO:
-            print 'ADD entity', entity.__regid__, entity.eid, dict(entity)
+            print 'ADD entity', self, entity.__regid__, entity.eid, entity.cw_attr_cache
         relations = []
         if source.should_call_hooks:
             self.hm.call_hooks('before_add_entity', session, entity=entity)
         # XXX use entity.keys here since edited_attributes is not updated for
         # inline relations XXX not true, right? (see edited_attributes
         # affectation above)
-        for attr in entity.iterkeys():
+        for attr in entity.cw_attr_cache.iterkeys():
             rschema = eschema.subjrels[attr]
             if not rschema.final: # inlined relation
                 relations.append((attr, entity[attr]))
-        entity.set_defaults()
+        entity._cw_set_defaults()
         if session.is_hook_category_activated('integrity'):
-            entity.check(creation=True)
+            entity._cw_check(creation=True)
         source.add_entity(session, entity)
         self.add_info(session, entity, source, extid, complete=False)
-        entity._is_saved = True # entity has an eid and is saved
+        entity._cw_is_saved = True # entity has an eid and is saved
         # prefill entity relation caches
         for rschema in eschema.subject_relations():
             rtype = str(rschema)
@@ -1081,12 +1070,13 @@
             if rschema.final:
                 entity.setdefault(rtype, None)
             else:
-                entity.set_related_cache(rtype, 'subject', session.empty_rset())
+                entity.cw_set_relation_cache(rtype, 'subject',
+                                             session.empty_rset())
         for rschema in eschema.object_relations():
             rtype = str(rschema)
             if rtype in schema.VIRTUAL_RTYPES:
                 continue
-            entity.set_related_cache(rtype, 'object', session.empty_rset())
+            entity.cw_set_relation_cache(rtype, 'object', session.empty_rset())
         # set inline relation cache before call to after_add_entity
         for attr, value in relations:
             session.update_rel_cache_add(entity.eid, attr, value)
@@ -1107,7 +1097,7 @@
         """
         if server.DEBUG & server.DBG_REPO:
             print 'UPDATE entity', entity.__regid__, entity.eid, \
-                  dict(entity), edited_attributes
+                  entity.cw_attr_cache, edited_attributes
         hm = self.hm
         eschema = entity.e_schema
         session.set_entity_cache(entity)
@@ -1145,7 +1135,7 @@
                 if not only_inline_rels:
                     hm.call_hooks('before_update_entity', session, entity=entity)
             if session.is_hook_category_activated('integrity'):
-                entity.check()
+                entity._cw_check()
             source.update_entity(session, entity)
             self.system_source.update_info(session, entity, need_fti_update)
             if source.should_call_hooks:
@@ -1153,7 +1143,7 @@
                     hm.call_hooks('after_update_entity', session, entity=entity)
                 for attr, value, prevvalue in relations:
                     # if the relation is already cached, update existant cache
-                    relcache = entity.relation_cached(attr, 'subject')
+                    relcache = entity.cw_relation_cached(attr, 'subject')
                     if prevvalue is not None:
                         hm.call_hooks('after_delete_relation', session,
                                       eidfrom=entity.eid, rtype=attr, eidto=prevvalue)
@@ -1163,8 +1153,8 @@
                     if relcache is not None:
                         session.update_rel_cache_add(entity.eid, attr, value)
                     else:
-                        entity.set_related_cache(attr, 'subject',
-                                                 session.eid_rset(value))
+                        entity.cw_set_relation_cache(attr, 'subject',
+                                                     session.eid_rset(value))
                     hm.call_hooks('after_add_relation', session,
                                   eidfrom=entity.eid, rtype=attr, eidto=value)
         finally:
--- a/server/schemaserial.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/schemaserial.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""functions for schema / permissions (de)serialization using RQL
+"""functions for schema / permissions (de)serialization using RQL"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import os
@@ -27,7 +26,9 @@
 
 from yams import schema as schemamod, buildobjs as ybo
 
-from cubicweb.schema import CONSTRAINTS, ETYPE_NAME_MAP, VIRTUAL_RTYPES
+from cubicweb import CW_SOFTWARE_ROOT, typed_eid
+from cubicweb.schema import (CONSTRAINTS, ETYPE_NAME_MAP,
+                             VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES)
 from cubicweb.server import sqlutils
 
 def group_mapping(cursor, interactive=True):
@@ -57,10 +58,18 @@
                 if not value:
                     continue
                 try:
-                    res[group] = int(value)
+                    eid = typed_eid(value)
                 except ValueError:
                     print 'eid should be an integer'
                     continue
+                for eid_ in res.values():
+                    if eid == eid_:
+                        break
+                else:
+                    print 'eid is not a group eid'
+                    continue
+                res[name] = eid
+                break
     return res
 
 def cstrtype_mapping(cursor):
@@ -100,17 +109,28 @@
             sidx[eid] = eschema
             continue
         if etype in ETYPE_NAME_MAP:
+            needcopy = False
             netype = ETYPE_NAME_MAP[etype]
             # can't use write rql queries at this point, use raw sql
-            session.system_sql('UPDATE %(p)sCWEType SET %(p)sname=%%(n)s WHERE %(p)seid=%%(x)s'
-                               % {'p': sqlutils.SQL_PREFIX},
-                               {'x': eid, 'n': netype})
-            session.system_sql('UPDATE entities SET type=%(n)s WHERE type=%(x)s',
-                               {'x': etype, 'n': netype})
+            sqlexec = session.system_sql
+            if sqlexec('SELECT 1 FROM %(p)sCWEType WHERE %(p)sname=%%(n)s'
+                       % {'p': sqlutils.SQL_PREFIX}, {'n': netype}).fetchone():
+                # the new type already exists, we should merge
+                assert etype.lower() != netype.lower()
+                needcopy = True
+            else:
+                # the new type doesn't exist, we should rename
+                sqlexec('UPDATE %(p)sCWEType SET %(p)sname=%%(n)s WHERE %(p)seid=%%(x)s'
+                        % {'p': sqlutils.SQL_PREFIX}, {'x': eid, 'n': netype})
+                if etype.lower() != netype.lower():
+                    sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (
+                        sqlutils.SQL_PREFIX, etype, sqlutils.SQL_PREFIX, netype))
+            sqlexec('UPDATE entities SET type=%(n)s WHERE type=%(x)s',
+                    {'x': etype, 'n': netype})
             session.commit(False)
             try:
-                session.system_sql('UPDATE deleted_entities SET type=%(n)s WHERE type=%(x)s',
-                                   {'x': etype, 'n': netype})
+                sqlexec('UPDATE deleted_entities SET type=%(n)s WHERE type=%(x)s',
+                        {'x': etype, 'n': netype})
             except:
                 pass
             tocleanup = [eid]
@@ -118,6 +138,12 @@
                           if etype == eidetype)
             repo.clear_caches(tocleanup)
             session.commit(False)
+            if needcopy:
+                from logilab.common.testlib import mock_object
+                sidx[eid] = mock_object(type=netype)
+                # copy / CWEType entity removal expected to be done through
+                # rename_entity_type in a migration script
+                continue
             etype = netype
         etype = ybo.EntityType(name=etype, description=desc, eid=eid)
         eschema = schema.add_entity_type(etype)
--- a/server/server.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/server.py	Mon Jul 19 15:37:02 2010 +0200
@@ -74,10 +74,10 @@
 
 class RepositoryServer(object):
 
-    def __init__(self, config, debug=False):
+    def __init__(self, config):
         """make the repository available as a PyRO object"""
         self.config = config
-        self.repo = Repository(config, debug=debug)
+        self.repo = Repository(config)
         self.ns = None
         self.quiting = None
         # event queue
--- a/server/serverconfig.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/serverconfig.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""server.serverconfig definition
+"""server.serverconfig definition"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from os.path import join, exists
--- a/server/serverctl.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/serverctl.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-ctl commands and command handlers specific to the server.serverconfig
+"""cubicweb-ctl commands and command handlers specific to the repository"""
 
-"""
 __docformat__ = 'restructuredtext en'
 
 # *ctl module should limit the number of import to be imported as quickly as
@@ -48,14 +47,16 @@
     if dbname is None:
         dbname = source['db-name']
     driver = source['db-driver']
-    print '-> connecting to %s database' % driver,
-    if dbhost:
-        print '%s@%s' % (dbname, dbhost),
-    else:
-        print dbname,
+    if verbose:
+        print '-> connecting to %s database' % driver,
+        if dbhost:
+            print '%s@%s' % (dbname, dbhost),
+        else:
+            print dbname,
     if not verbose or (not special_privs and source.get('db-user')):
         user = source['db-user']
-        print 'as', user
+        if verbose:
+            print 'as', user
         if source.get('db-password'):
             password = source['db-password']
         else:
@@ -152,8 +153,8 @@
     cfgname = 'repository'
 
     def bootstrap(self, cubes, inputlevel=0):
-        """create an instance by copying files from the given cube and by
-        asking information necessary to build required configuration files
+        """create an instance by copying files from the given cube and by asking
+        information necessary to build required configuration files
         """
         from cubicweb.server.utils import ask_source_config
         config = self.config
@@ -249,11 +250,12 @@
     cmdname = 'start'
     cfgname = 'repository'
 
-    def start_server(self, ctlconf, debug):
+    def start_server(self, config):
         command = ['cubicweb-ctl start-repository ']
-        if debug:
+        if config.debugmode:
             command.append('--debug')
-        command.append(self.config.appid)
+        command.append('--loglevel %s' % config['log-threshold'].lower())
+        command.append(config.appid)
         os.system(' '.join(command))
 
 
@@ -262,8 +264,7 @@
     cfgname = 'repository'
 
     def poststop(self):
-        """if pyro is enabled, ensure the repository is correctly
-        unregistered
+        """if pyro is enabled, ensure the repository is correctly unregistered
         """
         if self.config.pyro_enabled():
             from cubicweb.server.repository import pyro_unregister
@@ -272,6 +273,14 @@
 
 # repository specific commands ################################################
 
+def createdb(helper, source, dbcnx, cursor, **kwargs):
+    if dbcnx.logged_user != source['db-user']:
+        helper.create_database(cursor, source['db-name'], source['db-user'],
+                               source['db-encoding'], **kwargs)
+    else:
+        helper.create_database(cursor, source['db-name'],
+                               dbencoding=source['db-encoding'], **kwargs)
+
 class CreateInstanceDBCommand(Command):
     """Create the system database of an instance (run after 'create').
 
@@ -314,14 +323,13 @@
         source = config.sources()['system']
         dbname = source['db-name']
         driver = source['db-driver']
-        create_db = self.config.create_db
         helper = get_db_helper(driver)
         if driver == 'sqlite':
             if os.path.exists(dbname) and (
                 automatic or
                 ASK.confirm('Database %s already exists. Drop it?' % dbname)):
                 os.unlink(dbname)
-        elif create_db:
+        elif self.config.create_db:
             print '\n'+underline_title('Creating the system database')
             # connect on the dbms system base to create our base
             dbcnx = _db_sys_cnx(source, 'CREATE DATABASE and / or USER', verbose=verbose)
@@ -338,12 +346,7 @@
                         cursor.execute('DROP DATABASE %s' % dbname)
                     else:
                         return
-                if dbcnx.logged_user != source['db-user']:
-                    helper.create_database(cursor, dbname, source['db-user'],
-                                           source['db-encoding'])
-                else:
-                    helper.create_database(cursor, dbname,
-                                           dbencoding=source['db-encoding'])
+                createdb(helper, source, dbcnx, cursor)
                 dbcnx.commit()
                 print '-> database %s created.' % dbname
             except:
@@ -523,22 +526,28 @@
         ('debug',
          {'short': 'D', 'action' : 'store_true',
           'help': 'start server in debug mode.'}),
+        ('loglevel',
+         {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
+          'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
+          'help': 'debug if -D is set, error otherwise',
+          }),
         )
 
     def run(self, args):
         from logilab.common.daemon import daemonize
+        from cubicweb.cwctl import init_cmdline_log_threshold
         from cubicweb.server.server import RepositoryServer
         appid = pop_arg(args, msg='No instance specified !')
-        config = ServerConfiguration.config_for(appid)
-        if sys.platform == 'win32':
-            if not self.config.debug:
-                from logging import getLogger
-                logger = getLogger('cubicweb.ctl')
-                logger.info('Forcing debug mode on win32 platform')
-                self.config.debug = True
-        debug = self.config.debug
+        debug = self['debug']
+        if sys.platform == 'win32' and not debug:
+            from logging import getLogger
+            logger = getLogger('cubicweb.ctl')
+            logger.info('Forcing debug mode on win32 platform')
+            debug = True
+        config = ServerConfiguration.config_for(appid, debugmode=debug)
+        init_cmdline_log_threshold(config, self['loglevel'])
         # create the server
-        server = RepositoryServer(config, debug)
+        server = RepositoryServer(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']
--- a/server/session.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/session.py	Mon Jul 19 15:37:02 2010 +0200
@@ -250,7 +250,7 @@
             entity = self.entity_cache(eid)
         except KeyError:
             return
-        rcache = entity.relation_cached(rtype, role)
+        rcache = entity.cw_relation_cached(rtype, role)
         if rcache is not None:
             rset, entities = rcache
             rset = rset.copy()
@@ -266,14 +266,15 @@
                 targetentity.cw_col = 0
             rset.rowcount += 1
             entities.append(targetentity)
-            entity._related_cache['%s_%s' % (rtype, role)] = (rset, tuple(entities))
+            entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+                rset, tuple(entities))
 
     def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid):
         try:
             entity = self.entity_cache(eid)
         except KeyError:
             return
-        rcache = entity.relation_cached(rtype, role)
+        rcache = entity.cw_relation_cached(rtype, role)
         if rcache is not None:
             rset, entities = rcache
             for idx, row in enumerate(rset.rows):
@@ -292,7 +293,8 @@
                 del rset.description[idx]
             del entities[idx]
             rset.rowcount -= 1
-            entity._related_cache['%s_%s' % (rtype, role)] = (rset, tuple(entities))
+            entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+                rset, tuple(entities))
 
     # resource accessors ######################################################
 
@@ -312,16 +314,15 @@
 
     def set_language(self, language):
         """i18n configuration for translation"""
-        vreg = self.vreg
         language = language or self.user.property_value('ui.language')
         try:
-            gettext, pgettext = vreg.config.translations[language]
+            gettext, pgettext = self.vreg.config.translations[language]
             self._ = self.__ = gettext
             self.pgettext = pgettext
         except KeyError:
-            language = vreg.property_value('ui.language')
+            language = self.vreg.property_value('ui.language')
             try:
-                gettext, pgettext = vreg.config.translations[language]
+                gettext, pgettext = self.vreg.config.translations[language]
                 self._ = self.__ = gettext
                 self.pgettext = pgettext
             except KeyError:
@@ -661,16 +662,6 @@
         else:
             del self.transaction_data['ecache'][eid]
 
-    def base_url(self):
-        url = self.repo.config['base-url']
-        if not url:
-            try:
-                url = self.repo.config.default_base_url()
-            except AttributeError: # default_base_url() might not be available
-                self.warning('missing base-url definition in server config')
-                url = u''
-        return url
-
     def from_controller(self):
         """return the id (string) of the controller issuing the request (no
         sense here, always return 'view')
@@ -756,7 +747,6 @@
                         self.pending_operations[:] = processed
                         self.debug('%s session %s done', trstate, self.id)
                     except:
-                        self.exception('error while %sing', trstate)
                         # if error on [pre]commit:
                         #
                         # * set .failed = True on the operation causing the failure
@@ -768,8 +758,12 @@
                         # instead of having to implements rollback, revertprecommit
                         # and revertcommit, that will be enough in mont case.
                         operation.failed = True
-                        for operation in processed:
-                            operation.handle_event('revert%s_event' % trstate)
+                        for operation in reversed(processed):
+                            try:
+                                operation.handle_event('revert%s_event' % trstate)
+                            except:
+                                self.critical('error while reverting %sing', trstate,
+                                              exc_info=True)
                         # XXX use slice notation since self.pending_operations is a
                         # read-only property.
                         self.pending_operations[:] = processed + self.pending_operations
--- a/server/sources/__init__.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/sources/__init__.py	Mon Jul 19 15:37:02 2010 +0200
@@ -342,7 +342,7 @@
         entity.
         """
         entity = self.repo.vreg['etypes'].etype_class(etype)(session)
-        entity.set_eid(eid)
+        entity.eid = eid
         return entity
 
     def after_entity_insertion(self, session, lid, entity):
--- a/server/sources/ldapuser.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/sources/ldapuser.py	Mon Jul 19 15:37:02 2010 +0200
@@ -232,6 +232,8 @@
                 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})
@@ -522,7 +524,7 @@
                              eid, base)
                 entity = session.entity_from_eid(eid, 'CWUser')
                 self.repo.delete_info(session, entity, self.uri, base)
-                self.reset_cache()
+                self.reset_caches()
             return []
         # except ldap.REFERRAL, e:
         #     cnx = self.handle_referral(e)
@@ -589,6 +591,8 @@
             emailaddr = self._cache[dn][self.user_rev_attrs['email']]
         except KeyError:
             return
+        if isinstance(emailaddr, list):
+            emailaddr = emailaddr[0] # XXX consider only the first email in the list
         rset = session.execute('EmailAddress X WHERE X address %(addr)s',
                                {'addr': emailaddr})
         if rset:
--- a/server/sources/native.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/sources/native.py	Mon Jul 19 15:37:02 2010 +0200
@@ -42,6 +42,8 @@
 from logilab.common.shellutils import getlogin
 from logilab.database import get_db_helper
 
+from yams import schema2sql as y2sql
+
 from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary
 from cubicweb import transaction as tx, server, neg_role
 from cubicweb.schema import VIRTUAL_RTYPES
@@ -127,6 +129,21 @@
         restr = '(%s)' % ' OR '.join(clauses)
     return '%s WHERE %s' % (select, restr)
 
+def rdef_table_column(rdef):
+    """return table and column used to store the given relation definition in
+    the database
+    """
+    return (SQL_PREFIX + str(rdef.subject),
+            SQL_PREFIX + str(rdef.rtype))
+
+def rdef_physical_info(dbhelper, rdef):
+    """return backend type and a boolean flag if NULL values should be allowed
+    for a given relation definition
+    """
+    coltype = y2sql.type_from_constraints(dbhelper, rdef.object,
+                                          rdef.constraints, creating=False)
+    allownull = rdef.cardinality[0] != '1'
+    return coltype, allownull
 
 class UndoException(Exception):
     """something went wrong during undoing"""
@@ -678,6 +695,47 @@
 
     # short cut to method requiring advanced db helper usage ##################
 
+    def update_rdef_column(self, session, rdef):
+        """update physical column for a relation definition (final or inlined)
+        """
+        table, column = rdef_table_column(rdef)
+        coltype, allownull = rdef_physical_info(self.dbhelper, rdef)
+        if not self.dbhelper.alter_column_support:
+            self.error("backend can't alter %s.%s to %s%s", table, column, coltype,
+                       not allownull and 'NOT NULL' or '')
+            return
+        self.dbhelper.change_col_type(LogCursor(session.pool[self.uri]),
+                                      table, column, coltype, allownull)
+        self.info('altered %s.%s: now %s%s', table, column, coltype,
+                  not allownull and 'NOT NULL' or '')
+
+    def update_rdef_null_allowed(self, session, rdef):
+        """update NULL / NOT NULL of physical column for a relation definition
+        (final or inlined)
+        """
+        if not self.dbhelper.alter_column_support:
+            # not supported (and NOT NULL not set by yams in that case, so no
+            # worry)
+            return
+        table, column = rdef_table_column(rdef)
+        coltype, allownull = rdef_physical_info(self.dbhelper, rdef)
+        self.dbhelper.set_null_allowed(LogCursor(session.pool[self.uri]),
+                                       table, column, coltype, allownull)
+
+    def update_rdef_indexed(self, session, rdef):
+        table, column = rdef_table_column(rdef)
+        if rdef.indexed:
+            self.create_index(session, table, column)
+        else:
+            self.drop_index(session, table, column)
+
+    def update_rdef_unique(self, session, rdef):
+        table, column = rdef_table_column(rdef)
+        if rdef.constraint_by_type('UniqueConstraint'):
+            self.create_index(session, table, column, unique=True)
+        else:
+            self.drop_index(session, table, column, unique=True)
+
     def create_index(self, session, table, column, unique=False):
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.create_index(cursor, table, column, unique)
@@ -686,14 +744,6 @@
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.drop_index(cursor, table, column, unique)
 
-    def change_col_type(self, session, table, column, coltype, null_allowed):
-        cursor = LogCursor(session.pool[self.uri])
-        self.dbhelper.change_col_type(cursor, table, column, coltype, null_allowed)
-
-    def set_null_allowed(self, session, table, column, coltype, null_allowed):
-        cursor = LogCursor(session.pool[self.uri])
-        self.dbhelper.set_null_allowed(cursor, table, column, coltype, null_allowed)
-
     # system source interface #################################################
 
     def eid_type_source(self, session, eid):
@@ -1079,10 +1129,10 @@
                 entity[rtype] = unicode(value, session.encoding, 'replace')
             else:
                 entity[rtype] = value
-        entity.set_eid(eid)
+        entity.eid = eid
         session.repo.init_entity_caches(session, entity, self)
         entity.edited_attributes = set(entity)
-        entity.check()
+        entity._cw_check()
         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
         # restore the entity
         action.changes['cw_eid'] = eid
@@ -1149,7 +1199,7 @@
             return [session._(
                 "Can't undo creation of entity %(eid)s of type %(etype)s, type "
                 "no more supported" % {'eid': eid, 'etype': etype})]
-        entity.set_eid(eid)
+        entity.eid = eid
         # for proper eid/type cache update
         hook.set_operation(session, 'pendingeids', eid,
                            CleanupDeletedEidsCacheOp)
@@ -1237,7 +1287,8 @@
         try:
             # use cursor_index_object, not cursor_reindex_object since
             # unindexing done in the FTIndexEntityOp
-            self.dbhelper.cursor_index_object(entity.eid, entity,
+            self.dbhelper.cursor_index_object(entity.eid,
+                                              entity.cw_adapt_to('IFTIndexable'),
                                               session.pool['system'])
         except Exception: # let KeyboardInterrupt / SystemExit propagate
             self.exception('error while reindexing %s', entity)
@@ -1262,7 +1313,8 @@
                 # processed
                 return
             done.add(eid)
-            for container in session.entity_from_eid(eid).fti_containers():
+            iftindexable = session.entity_from_eid(eid).cw_adapt_to('IFTIndexable')
+            for container in iftindexable.fti_containers():
                 source.fti_unindex_entity(session, container.eid)
                 source.fti_index_entity(session, container)
 
--- a/server/sources/rql2sql.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/sources/rql2sql.py	Mon Jul 19 15:37:02 2010 +0200
@@ -611,12 +611,14 @@
                 sql += '\nHAVING %s' % having
             # sort
             if sorts:
-                sql += '\nORDER BY %s' % ','.join(self._sortterm_sql(sortterm,
-                                                                     fselectidx)
-                                                  for sortterm in sorts)
-                if fneedwrap:
-                    selection = ['T1.C%s' % i for i in xrange(len(origselection))]
-                    sql = 'SELECT %s FROM (%s) AS T1' % (','.join(selection), sql)
+                sqlsortterms = [self._sortterm_sql(sortterm, fselectidx)
+                                for sortterm in sorts]
+                sqlsortterms = [x for x in sqlsortterms if x is not None]
+                if sqlsortterms:
+                    sql += '\nORDER BY %s' % ','.join(sqlsortterms)
+                    if sorts and fneedwrap:
+                        selection = ['T1.C%s' % i for i in xrange(len(origselection))]
+                        sql = 'SELECT %s FROM (%s) AS T1' % (','.join(selection), sql)
             state.finalize_source_cbs()
         finally:
             select.selection = origselection
@@ -696,12 +698,14 @@
     def _sortterm_sql(self, sortterm, selectidx):
         term = sortterm.term
         try:
-            sqlterm = str(selectidx.index(str(term)) + 1)
+            sqlterm = selectidx.index(str(term)) + 1
         except ValueError:
             # Constant node or non selected term
-            sqlterm = str(term.accept(self))
+            sqlterm = term.accept(self)
+            if sqlterm is None:
+                return None
         if sortterm.asc:
-            return sqlterm
+            return str(sqlterm)
         else:
             return '%s DESC' % sqlterm
 
@@ -1060,7 +1064,8 @@
             not_ = True
         else:
             not_ = False
-        return self.dbhelper.fti_restriction_sql(alias, const.eval(self._args),
+        query = const.eval(self._args)
+        return self.dbhelper.fti_restriction_sql(alias, query,
                                                  jointo, not_) + restriction
 
     def visit_comparison(self, cmp):
@@ -1104,6 +1109,15 @@
 
     def visit_function(self, func):
         """generate SQL name for a function"""
+        if func.name == 'FTIRANK':
+            try:
+                rel = iter(func.children[0].variable.stinfo['ftirels']).next()
+            except KeyError:
+                raise BadRQLQuery("can't use FTIRANK on variable not used in an"
+                                  " 'has_text' relation (eg full-text search)")
+            const = rel.get_parts()[1].children[0]
+            return self.dbhelper.fti_rank_order(self._fti_table(rel),
+                                                const.eval(self._args))
         args = [c.accept(self) for c in func.children]
         if func in self._state.source_cb_funcs:
             # function executed as a callback on the source
@@ -1132,8 +1146,6 @@
                 _id = _id.encode()
         else:
             _id = str(id(constant)).replace('-', '', 1)
-            if isinstance(value, unicode):
-                value = value.encode(self.dbencoding)
             self._query_attrs[_id] = value
         return '%%(%s)s' % _id
 
--- a/server/sources/storages.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/sources/storages.py	Mon Jul 19 15:37:02 2010 +0200
@@ -174,7 +174,7 @@
         # PIL processing that use filename extension to detect content-type, as
         # well as providing more understandable file names on the fs.
         basename = [str(entity.eid), attr]
-        name = entity.attr_metadata(attr, 'name')
+        name = entity.cw_attr_metadata(attr, 'name')
         if name is not None:
             basename.append(name.encode(self.fsencoding))
         fspath = uniquify_path(self.default_directory, '_'.join(basename))
--- a/server/sqlutils.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/sqlutils.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""SQL utilities functions and classes.
+"""SQL utilities functions and classes."""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import os
--- a/server/ssplanner.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/ssplanner.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,15 +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/>.
-"""plan execution of rql queries on a single source
+"""plan execution of rql queries on a single source"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
 
-from copy import copy
-
 from rql.stmts import Union, Select
 from rql.nodes import Constant, Relation
 
@@ -479,7 +476,7 @@
             result = [[]]
         for row in result:
             # get a new entity definition for this row
-            edef = copy(base_edef)
+            edef = base_edef.cw_copy()
             # complete this entity def using row values
             index = 0
             for rtype, rorder, value in self.rdefs:
@@ -487,7 +484,7 @@
                     value = row[index]
                     index += 1
                 if rorder == InsertRelationsStep.FINAL:
-                    edef.rql_set_value(rtype, value)
+                    edef._cw_rql_set_value(rtype, value)
                 elif rorder == InsertRelationsStep.RELATION:
                     self.plan.add_relation_def( (edef, rtype, value) )
                     edef.querier_pending_relations[(rtype, 'subject')] = value
@@ -584,7 +581,7 @@
                         edef = edefs[eid]
                     except KeyError:
                         edefs[eid] = edef = session.entity_from_eid(eid)
-                    edef.rql_set_value(str(rschema), rhsval)
+                    edef._cw_rql_set_value(str(rschema), rhsval)
                 else:
                     repo.glob_add_relation(session, lhsval, str(rschema), rhsval)
             result[i] = newrow
--- a/server/test/data/migratedapp/schema.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/data/migratedapp/schema.py	Mon Jul 19 15:37:02 2010 +0200
@@ -69,7 +69,7 @@
     mydate = Date(default='TODAY')
     shortpara = String(maxsize=64)
     ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
-    attachment = SubjectRelation(('File', 'Image'))
+    attachment = SubjectRelation('File')
 
 class Text(Para):
     __specializes_schema__ = True
--- a/server/test/data/schema.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/data/schema.py	Mon Jul 19 15:37:02 2010 +0200
@@ -92,7 +92,7 @@
                       })
 
     migrated_from = SubjectRelation('Note')
-    attachment = SubjectRelation(('File', 'Image'))
+    attachment = SubjectRelation('File')
     inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
     todo_by = SubjectRelation('CWUser')
 
--- a/server/test/data/site_cubicweb.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/data/site_cubicweb.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
 
 from logilab.database import FunctionDescr
 from logilab.database.sqlite import register_sqlite_pyfunc
@@ -25,7 +22,7 @@
 
 try:
     class DUMB_SORT(FunctionDescr):
-        supported_backends = ('sqlite',)
+        pass
 
     register_function(DUMB_SORT)
     def dumb_sort(something):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data/sources_fti	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,14 @@
+[system]
+
+db-driver   = postgres
+db-host     = localhost
+db-port     = 
+adapter     = native
+db-name     = cw_fti_test
+db-encoding = UTF-8
+db-user     = syt
+db-password = syt
+
+[admin]
+login = admin
+password = gingkow
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_fti.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,52 @@
+from __future__ import with_statement
+
+from cubicweb.devtools import ApptestConfiguration
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.selectors import is_instance
+from cubicweb.entities.adapters import IFTIndexableAdapter
+
+class PostgresFTITC(CubicWebTC):
+    config = ApptestConfiguration('data', sourcefile='sources_fti')
+
+    def test_occurence_count(self):
+        req = self.request()
+        c1 = req.create_entity('Card', title=u'c1',
+                               content=u'cubicweb cubicweb cubicweb')
+        c2 = req.create_entity('Card', title=u'c3',
+                               content=u'cubicweb')
+        c3 = req.create_entity('Card', title=u'c2',
+                               content=u'cubicweb cubicweb')
+        self.commit()
+        self.assertEquals(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
+                          [[c1.eid], [c3.eid], [c2.eid]])
+
+
+    def test_attr_weight(self):
+        class CardIFTIndexableAdapter(IFTIndexableAdapter):
+            __select__ = is_instance('Card')
+            attr_weight = {'title': 'A'}
+        with self.temporary_appobjects(CardIFTIndexableAdapter):
+            req = self.request()
+            c1 = req.create_entity('Card', title=u'c1',
+                                   content=u'cubicweb cubicweb cubicweb')
+            c2 = req.create_entity('Card', title=u'c2',
+                                   content=u'cubicweb cubicweb')
+            c3 = req.create_entity('Card', title=u'cubicweb',
+                                   content=u'autre chose')
+            self.commit()
+            self.assertEquals(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
+                              [[c3.eid], [c1.eid], [c2.eid]])
+
+
+    def test_entity_weight(self):
+        class PersonneIFTIndexableAdapter(IFTIndexableAdapter):
+            __select__ = is_instance('Personne')
+            entity_weight = 2.0
+        with self.temporary_appobjects(PersonneIFTIndexableAdapter):
+            req = self.request()
+            c1 = req.create_entity('Personne', nom=u'c1', prenom=u'cubicweb')
+            c2 = req.create_entity('Comment', content=u'cubicweb cubicweb', comments=c1)
+            c3 = req.create_entity('Comment', content=u'cubicweb cubicweb cubicweb', comments=c1)
+            self.commit()
+            self.assertEquals(req.execute('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
+                              [[c1.eid], [c3.eid], [c2.eid]])
--- a/server/test/unittest_hook.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_hook.py	Mon Jul 19 15:37:02 2010 +0200
@@ -23,7 +23,6 @@
 
 from cubicweb.devtools import TestServerConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.selectors import implements
 from cubicweb.server import hook
 from cubicweb.hooks import integrity, syncschema
 
@@ -65,7 +64,7 @@
     def test_global_operation_order(self):
         session = self.session
         op1 = integrity._DelayedDeleteOp(session)
-        op2 = syncschema.MemSchemaRDefDel(session)
+        op2 = syncschema.RDefDelOp(session)
         # equivalent operation generated by op2 but replace it here by op3 so we
         # can check the result...
         op3 = syncschema.MemSchemaNotifyChanges(session)
--- a/server/test/unittest_ldapuser.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_ldapuser.py	Mon Jul 19 15:37:02 2010 +0200
@@ -178,12 +178,13 @@
         cnx = self.login(SYT, password='dummypassword')
         cu = cnx.cursor()
         adim = cu.execute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
-        adim.fire_transition('deactivate')
+        iworkflowable = adim.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         try:
             cnx.commit()
             adim.clear_all_caches()
             self.assertEquals(adim.in_state[0].name, 'deactivated')
-            trinfo = adim.latest_trinfo()
+            trinfo = iworkflowable.latest_trinfo()
             self.assertEquals(trinfo.owned_by[0].login, SYT)
             # select from_state to skip the user's creation TrInfo
             rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
@@ -195,7 +196,7 @@
             # restore db state
             self.restore_connection()
             adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
-            adim.fire_transition('activate')
+            adim.cw_adapt_to('IWorkflowable').fire_transition('activate')
             self.sexecute('DELETE X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': SYT})
 
     def test_same_column_names(self):
--- a/server/test/unittest_migractions.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_migractions.py	Mon Jul 19 15:37:02 2010 +0200
@@ -425,7 +425,7 @@
                 self.failIf(self.config.cube_dir('email') in self.config.cubes_path())
                 self.failIf('file' in self.config.cubes())
                 self.failIf(self.config.cube_dir('file') in self.config.cubes_path())
-                for ertype in ('Email', 'EmailThread', 'EmailPart', 'File', 'Image',
+                for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
                                'sender', 'in_thread', 'reply_to', 'data_format'):
                     self.failIf(ertype in schema, ertype)
                 self.assertEquals(sorted(schema['see_also'].rdefs.keys()),
@@ -448,7 +448,7 @@
             self.failUnless(self.config.cube_dir('email') in self.config.cubes_path())
             self.failUnless('file' in self.config.cubes())
             self.failUnless(self.config.cube_dir('file') in self.config.cubes_path())
-            for ertype in ('Email', 'EmailThread', 'EmailPart', 'File', 'Image',
+            for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
                            'sender', 'in_thread', 'reply_to', 'data_format'):
                 self.failUnless(ertype in schema, ertype)
             self.assertEquals(sorted(schema['see_also'].rdefs.keys()),
--- a/server/test/unittest_msplanner.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_msplanner.py	Mon Jul 19 15:37:02 2010 +0200
@@ -60,7 +60,7 @@
                      {'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'},
                      {'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'},
                      {'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'},
-                     {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'},
+                     {'X': 'Folder'}, {'X': 'Note'},
                      {'X': 'Personne'}, {'X': 'RQLExpression'}, {'X': 'Societe'},
                      {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'SubWorkflowExitPoint'},
                      {'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'},
@@ -413,7 +413,7 @@
         """retrieve CWUser X from both sources and return concatenation of results
         """
         self._test('CWUser X ORDERBY X LIMIT 10 OFFSET 10',
-                   [('AggrStep', 'Any X ORDERBY X', 10, 10, 'table0', None, [
+                   [('AggrStep', 'SELECT table0.C0 FROM table0 ORDER BY table0.C0 LIMIT 10 OFFSET 10', None, [
                        ('FetchStep', [('Any X WHERE X is CWUser', [{'X': 'CWUser'}])],
                         [self.ldap, self.system], {}, {'X': 'table0.C0'}, []),
                        ]),
@@ -423,7 +423,7 @@
         """
         # COUNT(X) is kept in sub-step and transformed into SUM(X) in the AggrStep
         self._test('Any COUNT(X) WHERE X is CWUser',
-                   [('AggrStep', 'Any COUNT(X)', None, None, 'table0', None, [
+                   [('AggrStep', 'SELECT SUM(table0.C0) FROM table0', None, [
                        ('FetchStep', [('Any COUNT(X) WHERE X is CWUser', [{'X': 'CWUser'}])],
                         [self.ldap, self.system], {}, {'COUNT(X)': 'table0.C0'}, []),
                        ]),
@@ -498,7 +498,7 @@
 
     def test_complex_ordered(self):
         self._test('Any L ORDERBY L WHERE X login L',
-                   [('AggrStep', 'Any L ORDERBY L', None, None, 'table0', None,
+                   [('AggrStep', 'SELECT table0.C0 FROM table0 ORDER BY table0.C0', None,
                      [('FetchStep', [('Any L WHERE X login L, X is CWUser',
                                       [{'X': 'CWUser', 'L': 'String'}])],
                        [self.ldap, self.system], {}, {'X.login': 'table0.C0', 'L': 'table0.C0'}, []),
@@ -507,7 +507,7 @@
 
     def test_complex_ordered_limit_offset(self):
         self._test('Any L ORDERBY L LIMIT 10 OFFSET 10 WHERE X login L',
-                   [('AggrStep', 'Any L ORDERBY L', 10, 10, 'table0', None,
+                   [('AggrStep', 'SELECT table0.C0 FROM table0 ORDER BY table0.C0 LIMIT 10 OFFSET 10', None,
                      [('FetchStep', [('Any L WHERE X login L, X is CWUser',
                                       [{'X': 'CWUser', 'L': 'String'}])],
                        [self.ldap, self.system], {}, {'X.login': 'table0.C0', 'L': 'table0.C0'}, []),
@@ -593,7 +593,7 @@
         2. return content of the table sorted
         """
         self._test('Any X,F ORDERBY F WHERE X firstname F',
-                   [('AggrStep', 'Any X,F ORDERBY F', None, None, 'table0', None,
+                   [('AggrStep', 'SELECT table0.C0, table0.C1 FROM table0 ORDER BY table0.C1', None,
                      [('FetchStep', [('Any X,F WHERE X firstname F, X is CWUser',
                                       [{'X': 'CWUser', 'F': 'String'}])],
                        [self.ldap, self.system], {},
@@ -657,7 +657,7 @@
 
     def test_complex_typed_aggregat(self):
         self._test('Any MAX(X) WHERE X is Card',
-                   [('AggrStep', 'Any MAX(X)', None, None, 'table0',  None,
+                   [('AggrStep', 'SELECT MAX(table0.C0) FROM table0',  None,
                      [('FetchStep',
                        [('Any MAX(X) WHERE X is Card', [{'X': 'Card'}])],
                        [self.cards, self.system], {}, {'MAX(X)': 'table0.C0'}, [])
@@ -784,10 +784,10 @@
                          [{'X': 'Basket'}]),
                         ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser',
                          [{'X': 'CWUser'}]),
-                        ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, SubDivision, Tag)',
+                        ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Note, Personne, Societe, SubDivision, Tag)',
                          [{'X': 'Card'}, {'X': 'Comment'},
                           {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'},
-                          {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'},
+                          {'X': 'File'}, {'X': 'Folder'},
                           {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'},
                           {'X': 'SubDivision'}, {'X': 'Tag'}]),],
                        None, None, [self.system], {}, []),
@@ -810,10 +810,10 @@
                             [{'X': 'Basket'}]),
                            ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser',
                             [{'X': 'CWUser'}]),
-                           ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, SubDivision, Tag)',
+                           ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Note, Personne, Societe, SubDivision, Tag)',
                             [{'X': 'Card'}, {'X': 'Comment'},
                              {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'},
-                             {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'},
+                             {'X': 'File'}, {'X': 'Folder'},
                              {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'},
                              {'X': 'SubDivision'}, {'X': 'Tag'}])],
                           [self.system], {}, {'X': 'table0.C0'}, []),
@@ -823,7 +823,7 @@
                        [{'X': 'Affaire'}, {'X': 'Basket'},
                         {'X': 'CWUser'}, {'X': 'Card'}, {'X': 'Comment'},
                         {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'},
-                        {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'},
+                        {'X': 'File'}, {'X': 'Folder'},
                         {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'},
                         {'X': 'SubDivision'}, {'X': 'Tag'}])],
                      10, 10, [self.system], {'X': 'table0.C0'}, [])
@@ -888,7 +888,7 @@
                                           [{'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, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, 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, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
                              [{'X': 'BaseTransition'}, {'X': 'Bookmark'},
                               {'X': 'CWAttribute'}, {'X': 'CWCache'},
                               {'X': 'CWConstraint'}, {'X': 'CWConstraintType'},
@@ -899,7 +899,7 @@
                               {'X': 'Email'}, {'X': 'EmailAddress'},
                               {'X': 'EmailPart'}, {'X': 'EmailThread'},
                               {'X': 'ExternalUri'}, {'X': 'File'},
-                              {'X': 'Folder'}, {'X': 'Image'},
+                              {'X': 'Folder'},
                               {'X': 'Personne'}, {'X': 'RQLExpression'},
                               {'X': 'Societe'}, {'X': 'SubDivision'},
                               {'X': 'SubWorkflowExitPoint'}, {'X': 'Tag'},
@@ -949,7 +949,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, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, 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, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, 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'},
@@ -961,7 +961,7 @@
                                          {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'},
                                          {'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'},
                                          {'X': 'File', 'ET': 'CWEType'}, {'X': 'Folder', 'ET': 'CWEType'},
-                                         {'X': 'Image', 'ET': 'CWEType'}, {'X': 'Personne', 'ET': 'CWEType'},
+                                         {'X': 'Personne', 'ET': 'CWEType'},
                                          {'X': 'RQLExpression', 'ET': 'CWEType'}, {'X': 'Societe', 'ET': 'CWEType'},
                                          {'X': 'SubDivision', 'ET': 'CWEType'}, {'X': 'SubWorkflowExitPoint', 'ET': 'CWEType'},
                                          {'X': 'Tag', 'ET': 'CWEType'}, {'X': 'TrInfo', 'ET': 'CWEType'},
@@ -1299,9 +1299,66 @@
                         ]),
                     ])
 
+    def test_has_text_orderby_rank(self):
+        self._test('Any X ORDERBY FTIRANK(X) WHERE X has_text "bla", X firstname "bla"',
+                   [('FetchStep', [('Any X WHERE X firstname "bla", X is CWUser', [{'X': 'CWUser'}])],
+                     [self.ldap, self.system], None, {'X': 'table0.C0'}, []),
+                    ('AggrStep', 'SELECT table1.C1 FROM table1 ORDER BY table1.C0', None, [
+                        ('FetchStep', [('Any FTIRANK(X),X WHERE X has_text "bla", X is CWUser',
+                                        [{'X': 'CWUser'}])],
+                         [self.system], {'X': 'table0.C0'}, {'FTIRANK(X)': 'table1.C0', 'X': 'table1.C1'}, []),
+                        ('FetchStep', [('Any FTIRANK(X),X WHERE X has_text "bla", X firstname "bla", X is Personne',
+                                        [{'X': 'Personne'}])],
+                         [self.system], {}, {'FTIRANK(X)': 'table1.C0', 'X': 'table1.C1'}, []),
+                        ]),
+                    ])
+
+    def test_security_has_text_orderby_rank(self):
+        # use a guest user
+        self.session = self.user_groups_session('guests')
+        self._test('Any X ORDERBY FTIRANK(X) WHERE X has_text "bla", X firstname "bla"',
+                   [('FetchStep', [('Any X WHERE X firstname "bla", X is CWUser', [{'X': 'CWUser'}])],
+                     [self.ldap, self.system], None, {'X': 'table1.C0'}, []),
+                    ('UnionFetchStep',
+                     [('FetchStep', [('Any X WHERE X firstname "bla", X is Personne', [{'X': 'Personne'}])],
+                       [self.system], {}, {'X': 'table0.C0'}, []),
+                      ('FetchStep', [('Any X WHERE EXISTS(X owned_by 5), X is CWUser', [{'X': 'CWUser'}])],
+                       [self.system], {'X': 'table1.C0'}, {'X': 'table0.C0'}, [])]),
+                    ('OneFetchStep', [('Any X ORDERBY FTIRANK(X) WHERE X has_text "bla"',
+                                       [{'X': 'CWUser'}, {'X': 'Personne'}])],
+                     None, None, [self.system], {'X': 'table0.C0'}, []),
+                    ])
+
+    def test_has_text_select_rank(self):
+        self._test('Any X, FTIRANK(X) WHERE X has_text "bla", X firstname "bla"',
+                   # XXX unecessary duplicate selection
+                   [('FetchStep', [('Any X,X WHERE X firstname "bla", X is CWUser', [{'X': 'CWUser'}])],
+                     [self.ldap, self.system], None, {'X': 'table0.C1'}, []),
+                    ('UnionStep', None, None, [
+                        ('OneFetchStep', [('Any X,FTIRANK(X) WHERE X has_text "bla", X is CWUser', [{'X': 'CWUser'}])],
+                         None, None, [self.system], {'X': 'table0.C1'}, []),
+                        ('OneFetchStep', [('Any X,FTIRANK(X) WHERE X has_text "bla", X firstname "bla", X is Personne', [{'X': 'Personne'}])],
+                         None, None, [self.system], {}, []),
+                        ]),
+                    ])
+
+    def test_security_has_text_select_rank(self):
+        # use a guest user
+        self.session = self.user_groups_session('guests')
+        self._test('Any X, FTIRANK(X) WHERE X has_text "bla", X firstname "bla"',
+                   [('FetchStep', [('Any X,X WHERE X firstname "bla", X is CWUser', [{'X': 'CWUser'}])],
+                     [self.ldap, self.system], None, {'X': 'table0.C1'}, []),
+                    ('UnionStep', None, None, [
+                        ('OneFetchStep', [('Any X,FTIRANK(X) WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser', [{'X': 'CWUser'}])],
+                         None, None, [self.system], {'X': 'table0.C1'}, []),
+                        ('OneFetchStep', [('Any X,FTIRANK(X) WHERE X has_text "bla", X firstname "bla", X is Personne', [{'X': 'Personne'}])],
+                         None, None, [self.system], {}, []),
+                        ]),
+                    ])
+
     def test_sort_func(self):
         self._test('Note X ORDERBY DUMB_SORT(RF) WHERE X type RF',
-                   [('AggrStep', 'Any X ORDERBY DUMB_SORT(RF)', None, None, 'table0', None, [
+                   [('AggrStep', 'SELECT table0.C0 FROM table0 ORDER BY DUMB_SORT(table0.C1)', None, [
                        ('FetchStep', [('Any X,RF WHERE X type RF, X is Note',
                                        [{'X': 'Note', 'RF': 'String'}])],
                         [self.cards, self.system], {}, {'X': 'table0.C0', 'X.type': 'table0.C1', 'RF': 'table0.C1'}, []),
@@ -1310,8 +1367,7 @@
 
     def test_ambigous_sort_func(self):
         self._test('Any X ORDERBY DUMB_SORT(RF) WHERE X title RF, X is IN (Bookmark, Card, EmailThread)',
-                   [('AggrStep', 'Any X ORDERBY DUMB_SORT(RF)',
-                     None, None, 'table0', None,
+                   [('AggrStep', 'SELECT table0.C0 FROM table0 ORDER BY DUMB_SORT(table0.C1)', None,
                      [('FetchStep', [('Any X,RF WHERE X title RF, X is Card',
                                       [{'X': 'Card', 'RF': 'String'}])],
                        [self.cards, self.system], {},
@@ -1718,8 +1774,9 @@
                     ])
 
     def test_nonregr2(self):
-        self.session.user.fire_transition('deactivate')
-        treid = self.session.user.latest_trinfo().eid
+        iworkflowable = self.session.user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
+        treid = iworkflowable.latest_trinfo().eid
         self._test('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
                    [('FetchStep', [('Any X,D WHERE X modification_date D, X is Note',
                                     [{'X': 'Note', 'D': 'Datetime'}])],
@@ -1727,7 +1784,7 @@
                     ('FetchStep', [('Any X,D WHERE X modification_date D, X is CWUser',
                                     [{'X': 'CWUser', 'D': 'Datetime'}])],
                      [self.ldap, self.system], None, {'X': 'table1.C0', 'X.modification_date': 'table1.C1', 'D': 'table1.C1'}, []),
-                    ('AggrStep', 'Any X ORDERBY D DESC', None, None, 'table2', None, [
+                    ('AggrStep', 'SELECT table2.C0 FROM table2 ORDER BY table2.C1 DESC', None, [
                         ('FetchStep', [('Any X,D WHERE E eid %s, E wf_info_for X, X modification_date D, E is TrInfo, X is Affaire'%treid,
                                         [{'X': 'Affaire', 'E': 'TrInfo', 'D': 'Datetime'}])],
                          [self.system],
@@ -1870,8 +1927,7 @@
                                     [{'X': 'Note', 'Z': 'Datetime'}])],
                      [self.cards, self.system], None, {'X': 'table0.C0', 'X.modification_date': 'table0.C1', 'Z': 'table0.C1'},
                      []),
-                    ('AggrStep', 'Any X ORDERBY Z DESC',
-                     None, None, 'table1', None,
+                    ('AggrStep', 'SELECT table1.C0 FROM table1 ORDER BY table1.C1 DESC', None,
                      [('FetchStep', [('Any X,Z WHERE X modification_date Z, 999999 see_also X, X is Bookmark',
                                       [{'X': 'Bookmark', 'Z': 'Datetime'}])],
                        [self.system], {},   {'X': 'table1.C0', 'X.modification_date': 'table1.C1',
--- a/server/test/unittest_multisources.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_multisources.py	Mon Jul 19 15:37:02 2010 +0200
@@ -111,11 +111,11 @@
         self.assertEquals(len(rset), 4)
         # since they are orderd by eid, we know the 3 first one is coming from the system source
         # and the others from external source
-        self.assertEquals(rset.get_entity(0, 0).metainformation(),
+        self.assertEquals(rset.get_entity(0, 0).cw_metainformation(),
                           {'source': {'adapter': 'native', 'uri': 'system'},
                            'type': u'Card', 'extid': None})
         externent = rset.get_entity(3, 0)
-        metainf = externent.metainformation()
+        metainf = externent.cw_metainformation()
         self.assertEquals(metainf['source'], {'adapter': 'pyrorql', 'base-url': 'http://extern.org/', 'uri': 'extern'})
         self.assertEquals(metainf['type'], 'Card')
         self.assert_(metainf['extid'])
@@ -134,6 +134,8 @@
         self.repo.sources_by_uri['extern'].synchronize(MTIME) # in case fti_update has been run before
         self.failUnless(self.sexecute('Any X WHERE X has_text "affref"'))
         self.failUnless(self.sexecute('Affaire X WHERE X has_text "affref"'))
+        self.failUnless(self.sexecute('Any X ORDERBY FTIRANK(X) WHERE X has_text "affref"'))
+        self.failUnless(self.sexecute('Affaire X ORDERBY FTIRANK(X) WHERE X has_text "affref"'))
 
     def test_anon_has_text(self):
         self.repo.sources_by_uri['extern'].synchronize(MTIME) # in case fti_update has been run before
@@ -145,6 +147,9 @@
         cnx = self.login('anon')
         cu = cnx.cursor()
         rset = cu.execute('Any X WHERE X has_text "card"')
+        # 5: 4 card + 1 readable affaire
+        self.assertEquals(len(rset), 5, zip(rset.rows, rset.description))
+        rset = cu.execute('Any X ORDERBY FTIRANK(X) WHERE X has_text "card"')
         self.assertEquals(len(rset), 5, zip(rset.rows, rset.description))
         Connection_close(cnx)
 
@@ -305,8 +310,9 @@
                      {'x': affaire.eid, 'u': ueid})
 
     def test_nonregr2(self):
-        self.session.user.fire_transition('deactivate')
-        treid = self.session.user.latest_trinfo().eid
+        iworkflowable = self.session.user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
+        treid = iworkflowable.latest_trinfo().eid
         rset = self.sexecute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
                             {'x': treid})
         self.assertEquals(len(rset), 1)
--- a/server/test/unittest_querier.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_querier.py	Mon Jul 19 15:37:02 2010 +0200
@@ -130,7 +130,7 @@
                                        'X': 'Affaire',
                                        'ET': 'CWEType', 'ETN': 'String'}])
         rql, solutions = partrqls[1]
-        self.assertEquals(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, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
+        self.assertEquals(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, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
         self.assertListEquals(sorted(solutions),
                               sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'},
@@ -155,7 +155,6 @@
                                       {'X': 'ExternalUri', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'File', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Folder', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'Image', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Note', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Personne', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'RQLExpression', 'ETN': 'String', 'ET': 'CWEType'},
@@ -491,17 +490,17 @@
                             'WHERE RT name N, RDEF relation_type RT '
                             'HAVING COUNT(RDEF) > 10')
         self.assertListEquals(rset.rows,
-                              [[u'description_format', 13],
-                               [u'description', 14],
+                              [[u'description_format', 12],
+                               [u'description', 13],
                                [u'name', 14],
-                               [u'created_by', 38],
-                               [u'creation_date', 38],
-                               [u'cwuri', 38],
-                               [u'in_basket', 38],
-                               [u'is', 38],
-                               [u'is_instance_of', 38],
-                               [u'modification_date', 38],
-                               [u'owned_by', 38]])
+                               [u'created_by', 37],
+                               [u'creation_date', 37],
+                               [u'cwuri', 37],
+                               [u'in_basket', 37],
+                               [u'is', 37],
+                               [u'is_instance_of', 37],
+                               [u'modification_date', 37],
+                               [u'owned_by', 37]])
 
     def test_select_aggregat_having_dumb(self):
         # dumb but should not raise an error
--- a/server/test/unittest_repository.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_repository.py	Mon Jul 19 15:37:02 2010 +0200
@@ -33,7 +33,7 @@
 
 from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
                       UnknownEid, AuthenticationError)
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.schema import CubicWebSchema, RQLConstraint
 from cubicweb.dbapi import connect, multiple_connections_unfix
 from cubicweb.devtools.testlib import CubicWebTC
@@ -202,7 +202,7 @@
         session = repo._get_session(cnxid)
         session.set_pool()
         user = session.user
-        user.fire_transition('deactivate')
+        user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
         rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
         self.assertEquals(len(rset), 1)
         repo.rollback(cnxid)
@@ -390,7 +390,7 @@
         # local hook
         class DummyBeforeHook(Hook):
             __regid__ = 'dummy-before-hook'
-            __select__ = Hook.__select__ & implements('EmailAddress')
+            __select__ = Hook.__select__ & is_instance('EmailAddress')
             events = ('before_update_entity',)
             def __call__(self):
                 # safety belt: avoid potential infinite recursion if the test
@@ -411,7 +411,7 @@
         # local hook
         class DummyBeforeHook(Hook):
             __regid__ = 'dummy-before-hook'
-            __select__ = Hook.__select__ & implements('EmailAddress')
+            __select__ = Hook.__select__ & is_instance('EmailAddress')
             events = ('before_add_entity',)
             def __call__(self):
                 # set_attributes is forbidden within before_add_entity()
@@ -430,7 +430,7 @@
         class DummyBeforeHook(Hook):
             _test = self # keep reference to test instance
             __regid__ = 'dummy-before-hook'
-            __select__ = Hook.__select__ & implements('Affaire')
+            __select__ = Hook.__select__ & is_instance('Affaire')
             events = ('before_update_entity',)
             def __call__(self):
                 # invoiced attribute shouldn't be considered "edited" before the hook
--- a/server/test/unittest_rql2sql.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_rql2sql.py	Mon Jul 19 15:37:02 2010 +0200
@@ -22,11 +22,13 @@
 from logilab.common.testlib import TestCase, unittest_main, mock_object
 
 from rql import BadRQLQuery
+from rql.utils import register_function, FunctionDescr
 
-#from cubicweb.server.sources.native import remove_unused_solutions
-from cubicweb.server.sources.rql2sql import SQLGenerator, remove_unused_solutions
+from cubicweb.devtools import TestServerConfiguration
+from cubicweb.devtools.repotest import RQLGeneratorTC
+from cubicweb.server.sources.rql2sql import remove_unused_solutions
 
-from rql.utils import register_function, FunctionDescr
+
 # add a dumb registered procedure
 class stockproc(FunctionDescr):
     supported_backends = ('postgres', 'sqlite', 'mysql')
@@ -35,8 +37,6 @@
 except AssertionError, ex:
     pass # already registered
 
-from cubicweb.devtools import TestServerConfiguration
-from cubicweb.devtools.repotest import RQLGeneratorTC
 
 config = TestServerConfiguration('data')
 config.bootstrap_cubes()
@@ -424,13 +424,10 @@
 GROUP BY T1.C1'''),
 
     ('Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 1, N, DF WHERE X data_name N, X data D, X data_format DF;',
-     '''SELECT (MAX(T1.C1) + MIN(LENGTH(T1.C0))), T1.C2 FROM (SELECT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
+     '''SELECT (MAX(_X.cw_eid) + MIN(LENGTH(_X.cw_data))), _X.cw_data_name
 FROM cw_File AS _X
-UNION ALL
-SELECT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
-FROM cw_Image AS _X) AS T1
-GROUP BY T1.C2,T1.C3
-ORDER BY 1,2,T1.C3'''),
+GROUP BY _X.cw_data_name,_X.cw_data_format
+ORDER BY 1,2,_X.cw_data_format'''),
 
     ('DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R',
      '''SELECT T1.C0 FROM (SELECT DISTINCT _A.cw_sujet AS C0, _A.cw_ref AS C1
@@ -438,12 +435,9 @@
 ORDER BY 2) AS T1'''),
 
     ('DISTINCT Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 2, DF WHERE X data_name N, X data D, X data_format DF;',
-     '''SELECT T1.C0,T1.C1 FROM (SELECT DISTINCT (MAX(T1.C1) + MIN(LENGTH(T1.C0))) AS C0, T1.C2 AS C1, T1.C3 AS C2 FROM (SELECT DISTINCT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
+     '''SELECT T1.C0,T1.C1 FROM (SELECT DISTINCT (MAX(_X.cw_eid) + MIN(LENGTH(_X.cw_data))) AS C0, _X.cw_data_name AS C1, _X.cw_data_format AS C2
 FROM cw_File AS _X
-UNION
-SELECT DISTINCT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
-FROM cw_Image AS _X) AS T1
-GROUP BY T1.C2,T1.C3
+GROUP BY _X.cw_data_name,_X.cw_data_format
 ORDER BY 2,3) AS T1
 '''),
 
@@ -1082,11 +1076,9 @@
 WHERE rel_is0.eid_to=2'''),
 
     ]
-from logilab.database import get_db_helper
-
 class CWRQLTC(RQLGeneratorTC):
     schema = schema
-
+    backend = 'sqlite'
     def test_nonregr_sol(self):
         delete = self.rqlhelper.parse(
             'DELETE X read_permission READ_PERMISSIONSUBJECT,X add_permission ADD_PERMISSIONSUBJECT,'
@@ -1112,12 +1104,7 @@
 
 class PostgresSQLGeneratorTC(RQLGeneratorTC):
     schema = schema
-
-    #capture = True
-    def setUp(self):
-        RQLGeneratorTC.setUp(self)
-        dbhelper = get_db_helper('postgres')
-        self.o = SQLGenerator(schema, dbhelper)
+    backend = 'postgres'
 
     def _norm_sql(self, sql):
         return sql.strip()
@@ -1377,13 +1364,53 @@
 UNION ALL
 SELECT _X.cw_eid
 FROM appears AS appears0, cw_Folder AS _X
-WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
-"""),
+WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu"""),
 
             ('Personne X where X has_text %(text)s, X travaille S, S has_text %(text)s',
              """SELECT _X.eid
 FROM appears AS appears0, appears AS appears2, entities AS _X, travaille_relation AS rel_travaille1
 WHERE appears0.words @@ to_tsquery('default', 'hip&hop&momo') AND appears0.uid=_X.eid AND _X.type='Personne' AND _X.eid=rel_travaille1.eid_from AND appears2.uid=rel_travaille1.eid_to AND appears2.words @@ to_tsquery('default', 'hip&hop&momo')"""),
+
+            ('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "toto tata"',
+             """SELECT appears0.uid
+FROM appears AS appears0
+WHERE appears0.words @@ to_tsquery('default', 'toto&tata')
+ORDER BY ts_rank(appears0.words, to_tsquery('default', 'toto&tata'))*appears0.weight DESC"""),
+
+            ('Personne X ORDERBY FTIRANK(X) WHERE X has_text "toto tata"',
+             """SELECT _X.eid
+FROM appears AS appears0, entities AS _X
+WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=_X.eid AND _X.type='Personne'
+ORDER BY ts_rank(appears0.words, to_tsquery('default', 'toto&tata'))*appears0.weight"""),
+
+            ('Personne X ORDERBY FTIRANK(X) WHERE X has_text %(text)s',
+             """SELECT _X.eid
+FROM appears AS appears0, entities AS _X
+WHERE appears0.words @@ to_tsquery('default', 'hip&hop&momo') AND appears0.uid=_X.eid AND _X.type='Personne'
+ORDER BY ts_rank(appears0.words, to_tsquery('default', 'hip&hop&momo'))*appears0.weight"""),
+
+            ('Any X ORDERBY FTIRANK(X) WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,Folder)',
+             """SELECT T1.C0 FROM (SELECT _X.cw_eid AS C0, ts_rank(appears0.words, to_tsquery('default', 'toto&tata'))*appears0.weight AS C1
+FROM appears AS appears0, cw_Basket AS _X
+WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
+UNION ALL
+SELECT _X.cw_eid AS C0, ts_rank(appears0.words, to_tsquery('default', 'toto&tata'))*appears0.weight AS C1
+FROM appears AS appears0, cw_Folder AS _X
+WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
+ORDER BY 2) AS T1"""),
+
+            ('Personne X ORDERBY FTIRANK(X),FTIRANK(S) WHERE X has_text %(text)s, X travaille S, S has_text %(text)s',
+             """SELECT _X.eid
+FROM appears AS appears0, appears AS appears2, entities AS _X, travaille_relation AS rel_travaille1
+WHERE appears0.words @@ to_tsquery('default', 'hip&hop&momo') AND appears0.uid=_X.eid AND _X.type='Personne' AND _X.eid=rel_travaille1.eid_from AND appears2.uid=rel_travaille1.eid_to AND appears2.words @@ to_tsquery('default', 'hip&hop&momo')
+ORDER BY ts_rank(appears0.words, to_tsquery('default', 'hip&hop&momo'))*appears0.weight,ts_rank(appears2.words, to_tsquery('default', 'hip&hop&momo'))*appears2.weight"""),
+
+
+            ('Any X, FTIRANK(X) WHERE X has_text "toto tata"',
+             """SELECT appears0.uid, ts_rank(appears0.words, to_tsquery('default', 'toto&tata'))*appears0.weight
+FROM appears AS appears0
+WHERE appears0.words @@ to_tsquery('default', 'toto&tata')"""),
+
             )):
             yield t
 
@@ -1445,11 +1472,7 @@
 
 
 class SqliteSQLGeneratorTC(PostgresSQLGeneratorTC):
-
-    def setUp(self):
-        RQLGeneratorTC.setUp(self)
-        dbhelper = get_db_helper('sqlite')
-        self.o = SQLGenerator(schema, dbhelper)
+    backend = 'sqlite'
 
     def _norm_sql(self, sql):
         return sql.strip().replace(' ILIKE ', ' LIKE ')
@@ -1547,6 +1570,26 @@
 FROM appears AS appears0, cw_Folder AS _X
 WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
 """),
+
+            ('Any X ORDERBY FTIRANK(X) WHERE X has_text "toto tata"',
+             """SELECT DISTINCT appears0.uid
+FROM appears AS appears0
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata'))"""),
+
+            ('Any X ORDERBY FTIRANK(X) WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,Folder)',
+             """SELECT DISTINCT _X.cw_eid
+FROM appears AS appears0, cw_Basket AS _X
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
+UNION
+SELECT DISTINCT _X.cw_eid
+FROM appears AS appears0, cw_Folder AS _X
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
+"""),
+
+            ('Any X, FTIRANK(X) WHERE X has_text "toto tata"',
+             """SELECT DISTINCT appears0.uid, 1.0
+FROM appears AS appears0
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata'))"""),
             )):
             yield t
 
@@ -1560,11 +1603,7 @@
 
 
 class MySQLGenerator(PostgresSQLGeneratorTC):
-
-    def setUp(self):
-        RQLGeneratorTC.setUp(self)
-        dbhelper = get_db_helper('mysql')
-        self.o = SQLGenerator(schema, dbhelper)
+    backend = 'mysql'
 
     def _norm_sql(self, sql):
         sql = sql.strip().replace(' ILIKE ', ' LIKE ').replace('TRUE', '1').replace('FALSE', '0')
@@ -1672,5 +1711,6 @@
                           ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'}], {}, set())
                           )
 
+
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_schemaserial.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_schemaserial.py	Mon Jul 19 15:37:02 2010 +0200
@@ -68,8 +68,6 @@
                                 {'et': None, 'x': None}),
                                ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
                                 {'et': None, 'x': None}),
-                               # ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
-                               #  {'et': 'File', 'x': 'Image'}),
                                ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
                                 {'et': None, 'x': None})])
 
--- a/server/test/unittest_security.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_security.py	Mon Jul 19 15:37:02 2010 +0200
@@ -213,8 +213,7 @@
         self.assertEquals(len(rset), 1)
         ent = rset.get_entity(0, 0)
         session.set_pool() # necessary
-        self.assertRaises(Unauthorized,
-                          ent.e_schema.check_perm, session, 'update', eid=ent.eid)
+        self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
         self.assertRaises(Unauthorized,
                           cu.execute, "SET P travaille S WHERE P is Personne, S is Societe")
         # test nothing has actually been inserted:
@@ -405,7 +404,7 @@
         # Note.para attribute editable by managers or if the note is in "todo" state
         note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
         self.commit()
-        note.fire_transition('markasdone')
+        note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
         self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
         self.commit()
         cnx = self.login('iaminusersgrouponly')
@@ -414,13 +413,13 @@
         self.assertRaises(Unauthorized, cnx.commit)
         note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
         cnx.commit()
-        note2.fire_transition('markasdone')
+        note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
         cnx.commit()
         self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})),
                           0)
         cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
         self.assertRaises(Unauthorized, cnx.commit)
-        note2.fire_transition('redoit')
+        note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
         cnx.commit()
         cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
         cnx.commit()
@@ -455,7 +454,7 @@
         cnx.commit()
         self.restore_connection()
         affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-        affaire.fire_transition('abort')
+        affaire.cw_adapt_to('IWorkflowable').fire_transition('abort')
         self.commit()
         self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')),
                           1)
@@ -557,14 +556,15 @@
             cu = cnx.cursor()
             self.schema['Affaire'].set_action_permissions('read', ('users',))
             aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-            aff.fire_transition('abort')
+            aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
             cnx.commit()
             # though changing a user state (even logged user) is reserved to managers
             user = cnx.user(self.session)
             # XXX wether it should raise Unauthorized or ValidationError is not clear
             # the best would probably ValidationError if the transition doesn't exist
             # from the current state but Unauthorized if it exists but user can't pass it
-            self.assertRaises(ValidationError, user.fire_transition, 'deactivate')
+            self.assertRaises(ValidationError,
+                              user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
         finally:
             # restore orig perms
             for action, perms in affaire_perms.iteritems():
@@ -572,18 +572,19 @@
 
     def test_trinfo_security(self):
         aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
+        iworkflowable = aff.cw_adapt_to('IWorkflowable')
         self.commit()
-        aff.fire_transition('abort')
+        iworkflowable.fire_transition('abort')
         self.commit()
         # can change tr info comment
         self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"',
                      {'c': u'bouh!'})
         self.commit()
-        aff.clear_related_cache('wf_info_for', 'object')
-        trinfo = aff.latest_trinfo()
+        aff.cw_clear_relation_cache('wf_info_for', 'object')
+        trinfo = iworkflowable.latest_trinfo()
         self.assertEquals(trinfo.comment, 'bouh!')
         # but not from_state/to_state
-        aff.clear_related_cache('wf_info_for', role='object')
+        aff.cw_clear_relation_cache('wf_info_for', role='object')
         self.assertRaises(Unauthorized,
                           self.execute, 'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"',
                           {'ti': trinfo.eid})
--- a/server/test/unittest_storage.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_storage.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +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/>.
-"""unit tests for module cubicweb.server.sources.storages
-
-"""
+"""unit tests for module cubicweb.server.sources.storages"""
 
 from __future__ import with_statement
 
@@ -29,13 +27,13 @@
 import tempfile
 
 from cubicweb import Binary, QueryError
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server.sources import storages
 from cubicweb.server.hook import Hook, Operation
 
 class DummyBeforeHook(Hook):
     __regid__ = 'dummy-before-hook'
-    __select__ = Hook.__select__ & implements('File')
+    __select__ = Hook.__select__ & is_instance('File')
     events = ('before_add_entity',)
 
     def __call__(self):
@@ -44,7 +42,7 @@
 
 class DummyAfterHook(Hook):
     __regid__ = 'dummy-after-hook'
-    __select__ = Hook.__select__ & implements('File')
+    __select__ = Hook.__select__ & is_instance('File')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -89,11 +87,11 @@
         f1.set_attributes(data=Binary('the new data'))
         self.rollback()
         self.assertEquals(file(expected_filepath).read(), 'the-data')
-        f1.delete()
+        f1.cw_delete()
         self.failUnless(osp.isfile(expected_filepath))
         self.rollback()
         self.failUnless(osp.isfile(expected_filepath))
-        f1.delete()
+        f1.cw_delete()
         self.commit()
         self.failIf(osp.isfile(expected_filepath))
 
@@ -133,11 +131,17 @@
         ex = self.assertRaises(QueryError, self.execute,
                                '(Any D WHERE X data D, X is File)'
                                ' UNION '
-                               '(Any D WHERE X data D, X is Image)')
+                               '(Any D WHERE X title D, X is Bookmark)')
         self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
-        ex = self.assertRaises(QueryError,
-                               self.execute, 'Any D WHERE X data D')
-        self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
+
+        storages.set_attribute_storage(self.repo, 'State', 'name',
+                                       storages.BytesFileSystemStorage(self.tempdir))
+        try:
+            ex = self.assertRaises(QueryError,
+                                   self.execute, 'Any D WHERE X name D, X is IN (State, Transition)')
+            self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
+        finally:
+            storages.unset_attribute_storage(self.repo, 'State', 'name')
 
     def test_source_mapped_attribute_advanced(self):
         f1 = self.create_file()
--- a/server/test/unittest_undo.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/server/test/unittest_undo.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
 from __future__ import with_statement
 
 from cubicweb import ValidationError
@@ -104,7 +101,7 @@
                                        address=u'toto@logilab.org',
                                        reverse_use_email=toto)
         txuuid1 = self.commit()
-        toto.delete()
+        toto.cw_delete()
         txuuid2 = self.commit()
         undoable_transactions = self.cnx.undoable_transactions
         txs = undoable_transactions(action='D')
@@ -147,7 +144,7 @@
         self.commit()
         txs = self.cnx.undoable_transactions()
         self.assertEquals(len(txs), 2)
-        toto.delete()
+        toto.cw_delete()
         txuuid = self.commit()
         actions = self.cnx.transaction_info(txuuid).actions_list()
         self.assertEquals(len(actions), 1)
@@ -160,8 +157,8 @@
         self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid}))
         self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid}))
         self.failUnless(self.execute('Any X WHERE X has_text "toto@logilab"'))
-        self.assertEquals(toto.state, 'activated')
-        self.assertEquals(toto.get_email(), 'toto@logilab.org')
+        self.assertEquals(toto.cw_adapt_to('IWorkflowable').state, 'activated')
+        self.assertEquals(toto.cw_adapt_to('IEmailable').get_email(), 'toto@logilab.org')
         self.assertEquals([(p.pkey, p.value) for p in toto.reverse_for_user],
                           [('ui.default-text-format', 'text/rest')])
         self.assertEquals([g.name for g in toto.in_group],
@@ -186,7 +183,7 @@
         c = session.create_entity('Card', title=u'hop', content=u'hop')
         p = session.create_entity('Personne', nom=u'louis', fiche=c)
         self.commit()
-        c.delete()
+        c.cw_delete()
         txuuid = self.commit()
         c2 = session.create_entity('Card', title=u'hip', content=u'hip')
         p.set_relations(fiche=c2)
@@ -207,9 +204,9 @@
         session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
         self.toto.set_relations(in_group=g)
         self.commit()
-        self.toto.delete()
+        self.toto.cw_delete()
         txuuid = self.commit()
-        g.delete()
+        g.cw_delete()
         self.commit()
         errors = self.cnx.undo_transaction(txuuid)
         self.assertEquals(errors,
--- a/skeleton/data/external_resources.tmpl	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-# -*- shell-script -*-
-###############################################################################
-#
-# put here information about external resources used by your components,
-# or to overides existing external resources configuration
-#
-###############################################################################
-
-# CSS stylesheets to include in HTML headers
-# uncomment the line below to use template specific stylesheet
-# STYLESHEETS = DATADIR/cubes.%(cubename)s.css
--- a/skeleton/test/test_CUBENAME.py	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""template automatic tests
-
-"""
-
-from logilab.common.testlib import TestCase, unittest_main
-
-class DefaultTC(TestCase):
-    def test_something(self):
-        self.skip('this cube has no test')
-
-## uncomment the import if you want to activate automatic test for your
-## template
-
-# from cubicweb.devtools.testlib import AutomaticWebTest
-
-
-if __name__ == '__main__':
-    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/skeleton/test/test_CUBENAME.py.tmpl	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,37 @@
+# copyright %(year)s %(author)s, all rights reserved.
+# contact %(author-web-site)s -- mailto:%(author-email)s
+#
+%(long-license)s
+"""%(distname)s automatic tests
+
+
+uncomment code below if you want to activate automatic test for your cube:
+
+.. sourcecode:: python
+
+    from cubicweb.devtools.testlib import AutomaticWebTest
+
+    class AutomaticWebTest(AutomaticWebTest):
+        '''provides `to_test_etypes` and/or `list_startup_views` implementation
+        to limit test scope
+        '''
+
+        def to_test_etypes(self):
+            '''only test views for entities of the returned types'''
+            return set(('My', 'Cube', 'Entity', 'Types'))
+
+        def list_startup_views(self):
+            '''only test startup views of the returned identifiers'''
+            return ('some', 'startup', 'views')
+"""
+
+from cubicweb.devtools import testlib
+
+class DefaultTC(testlib.CubicWebTC):
+    def test_something(self):
+        self.skip('this cube has no test')
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/skeleton/uiprops.py.tmpl	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,15 @@
+###############################################################################
+#
+# Put here information about external resources / styles used by your cube,
+# or to overides existing UI properties.
+#
+# Existing properties are available through the `sheet` dictionary available
+# in the global namespace. You also have access to a `data` function which
+# will return proper url for resources in the 'data' directory.
+#
+# /!\ this file should not be imported /!\
+###############################################################################
+
+# CSS stylesheets to include in HTML headers
+# uncomment the line below to use template specific stylesheet
+# STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.%(cubename)s.css')]
--- a/sobjects/notification.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/sobjects/notification.py	Mon Jul 19 15:37:02 2010 +0200
@@ -46,7 +46,8 @@
         mode = self._cw.vreg.config['default-recipients-mode']
         if mode == 'users':
             execute = self._cw.execute
-            dests = [(u.get_email(), u.property_value('ui.language'))
+            dests = [(u.cw_adapt_to('IEmailable').get_email(),
+                      u.property_value('ui.language'))
                      for u in execute(self.user_rql, build_descr=True).entities()]
         elif mode == 'default-dest-addrs':
             lang = self._cw.vreg.property_value('ui.language')
--- a/sobjects/test/data/sobjects/__init__.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/sobjects/test/data/sobjects/__init__.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,11 +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/>.
-"""
 
-"""
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.sobjects.notification import StatusChangeMixIn, NotificationView
 
 class UserStatusChangeView(StatusChangeMixIn, NotificationView):
-    __select__ = NotificationView.__select__ & implements('CWUser')
+    __select__ = NotificationView.__select__ & is_instance('CWUser')
--- a/sobjects/test/unittest_notification.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/sobjects/test/unittest_notification.py	Mon Jul 19 15:37:02 2010 +0200
@@ -85,7 +85,7 @@
     def test_status_change_view(self):
         req = self.request()
         u = self.create_user('toto', req=req)
-        u.fire_transition('deactivate', comment=u'yeah')
+        u.cw_adapt_to('IWorkflowable').fire_transition('deactivate', comment=u'yeah')
         self.failIf(MAILBOX)
         self.commit()
         self.assertEquals(len(MAILBOX), 1)
--- a/sobjects/test/unittest_supervising.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/sobjects/test/unittest_supervising.py	Mon Jul 19 15:37:02 2010 +0200
@@ -84,7 +84,7 @@
         self.assertEquals(op.to_send[0][1], ['test@logilab.fr'])
         self.commit()
         # some other changes #######
-        user.fire_transition('deactivate')
+        user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
         sentops = [op for op in session.pending_operations
                    if isinstance(op, SupervisionMailOp)]
         self.assertEquals(len(sentops), 1)
--- a/sobjects/textparsers.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/sobjects/textparsers.py	Mon Jul 19 15:37:02 2010 +0200
@@ -74,10 +74,14 @@
             if not hasattr(entity, 'in_state'):
                 self.error('bad change state instruction for eid %s', eid)
                 continue
-            tr = entity.current_workflow and entity.current_workflow.transition_by_name(trname)
+            iworkflowable = entity.cw_adapt_to('IWorkflowable')
+            if iworkflowable.current_workflow:
+                tr = iworkflowable.current_workflow.transition_by_name(trname)
+            else:
+                tr = None
             if tr and tr.may_be_fired(entity.eid):
                 try:
-                    trinfo = entity.fire_transition(tr)
+                    trinfo = iworkflowable.fire_transition(tr)
                     caller.fire_event('state-changed', {'trinfo': trinfo,
                                                         'entity': entity})
                 except:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/scripts/script1.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,3 @@
+assert 'data/scripts/script1.py' == __file__
+assert '__main__' == __name__
+assert [] == __args__, __args__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/scripts/script2.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,3 @@
+assert 'data/scripts/script2.py' == __file__
+assert '__main__' == __name__
+assert ['-v'] == __args__, __args__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/scripts/script3.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,3 @@
+assert 'data/scripts/script3.py' == __file__
+assert '__main__' == __name__
+assert ['-vd', '-f', 'FILE.TXT'] == __args__, __args__
--- a/test/unittest_cwctl.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_cwctl.py	Mon Jul 19 15:37:02 2010 +0200
@@ -24,8 +24,12 @@
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.server.migractions import ServerMigrationHelper
+
 CubicWebConfiguration.load_cwctl_plugins() # XXX necessary?
 
+
 class CubicWebCtlTC(TestCase):
     def setUp(self):
         self.stream = StringIO()
@@ -37,5 +41,25 @@
         from cubicweb.cwctl import ListCommand
         ListCommand().run([])
 
+
+class CubicWebShellTC(CubicWebTC):
+
+    def test_process_script_args_context(self):
+        repo = self.cnx._repo
+        mih = ServerMigrationHelper(None, repo=repo, cnx=self.cnx,
+                                    interactive=False,
+                                    # hack so it don't try to load fs schema
+                                    schema=1)
+        scripts = {'script1.py': list(),
+                   'script2.py': ['-v'],
+                   'script3.py': ['-vd', '-f', 'FILE.TXT'],
+                  }
+        mih.cmd_process_script('data/scripts/script1.py', funcname=None)
+        for script, args in scripts.items():
+            scriptname = os.path.join('data/scripts/', script)
+            self.assert_(os.path.exists(scriptname))
+            mih.cmd_process_script(scriptname, None, scriptargs=args)
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_entity.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_entity.py	Mon Jul 19 15:37:02 2010 +0200
@@ -97,27 +97,27 @@
         user = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"',
                            {'pwd': 'toto'}).get_entity(0, 0)
         self.commit()
-        user.fire_transition('deactivate')
+        user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
         self.commit()
         eid2 = self.execute('INSERT CWUser X: X login "tutu", X upassword %(pwd)s', {'pwd': 'toto'})[0][0]
         e = self.execute('Any X WHERE X eid %(x)s', {'x': eid2}).get_entity(0, 0)
         e.copy_relations(user.eid)
         self.commit()
-        e.clear_related_cache('in_state', 'subject')
-        self.assertEquals(e.state, 'activated')
+        e.cw_clear_relation_cache('in_state', 'subject')
+        self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated')
 
     def test_related_cache_both(self):
         user = self.execute('Any X WHERE X eid %(x)s', {'x':self.user().eid}).get_entity(0, 0)
         adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
         self.commit()
-        self.assertEquals(user._related_cache, {})
+        self.assertEquals(user._cw_related_cache, {})
         email = user.primary_email[0]
-        self.assertEquals(sorted(user._related_cache), ['primary_email_subject'])
-        self.assertEquals(email._related_cache.keys(), ['primary_email_object'])
+        self.assertEquals(sorted(user._cw_related_cache), ['primary_email_subject'])
+        self.assertEquals(email._cw_related_cache.keys(), ['primary_email_object'])
         groups = user.in_group
-        self.assertEquals(sorted(user._related_cache), ['in_group_subject', 'primary_email_subject'])
+        self.assertEquals(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject'])
         for group in groups:
-            self.failIf('in_group_subject' in group._related_cache, group._related_cache.keys())
+            self.failIf('in_group_subject' in group._cw_related_cache, group._cw_related_cache.keys())
 
     def test_related_limit(self):
         req = self.request()
@@ -197,20 +197,20 @@
         Note.fetch_attrs, Note.fetch_order = fetch_config(('type',))
         SubNote.fetch_attrs, SubNote.fetch_order = fetch_config(('type',))
         p = self.request().create_entity('Personne', nom=u'pouet')
-        self.assertEquals(p.related_rql('evaluee'),
+        self.assertEquals(p.cw_related_rql('evaluee'),
                           'Any X,AA,AB ORDERBY AA ASC WHERE E eid %(x)s, E evaluee X, '
                           'X type AA, X modification_date AB')
         Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', ))
         # XXX
-        self.assertEquals(p.related_rql('evaluee'),
+        self.assertEquals(p.cw_related_rql('evaluee'),
                           'Any X,AA ORDERBY AA DESC '
                           'WHERE E eid %(x)s, E evaluee X, X modification_date AA')
 
         tag = self.vreg['etypes'].etype_class('Tag')(self.request())
-        self.assertEquals(tag.related_rql('tags', 'subject'),
+        self.assertEquals(tag.cw_related_rql('tags', 'subject'),
                           'Any X,AA ORDERBY AA DESC '
                           'WHERE E eid %(x)s, E tags X, X modification_date AA')
-        self.assertEquals(tag.related_rql('tags', 'subject', ('Personne',)),
+        self.assertEquals(tag.cw_related_rql('tags', 'subject', ('Personne',)),
                           'Any X,AA,AB ORDERBY AA ASC '
                           'WHERE E eid %(x)s, E tags X, X is IN (Personne), X nom AA, '
                           'X modification_date AB')
@@ -219,47 +219,47 @@
         tag = self.vreg['etypes'].etype_class('Tag')(self.request())
         for ttype in self.schema['tags'].objects():
             self.vreg['etypes'].etype_class(ttype).fetch_attrs = ('modification_date',)
-        self.assertEquals(tag.related_rql('tags', 'subject'),
+        self.assertEquals(tag.cw_related_rql('tags', 'subject'),
                           'Any X,AA ORDERBY AA DESC '
                           'WHERE E eid %(x)s, E tags X, X modification_date AA')
 
     def test_unrelated_rql_security_1(self):
         user = self.request().user
-        rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+        rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
         self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
                           'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
         self.create_user('toto')
         self.login('toto')
         user = self.request().user
-        rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+        rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
         self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
                           'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
         user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
-        self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject')
+        self.assertRaises(Unauthorized, user.cw_unrelated_rql, 'use_email', 'EmailAddress', 'subject')
         self.login('anon')
         user = self.request().user
-        self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject')
+        self.assertRaises(Unauthorized, user.cw_unrelated_rql, 'use_email', 'EmailAddress', 'subject')
 
     def test_unrelated_rql_security_2(self):
         email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
-        rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+        rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
         self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ASC '
                           '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')
-        #rql = email.unrelated_rql('use_email', 'Person', 'object')[0]
+        #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
         #self.assertEquals(rql, '')
         self.login('anon')
         email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
-        rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+        rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
         self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
                           'WHERE NOT EXISTS(S use_email O), O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, '
                           'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
-        #rql = email.unrelated_rql('use_email', 'Person', 'object')[0]
+        #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
         #self.assertEquals(rql, '')
 
     def test_unrelated_rql_security_nonexistant(self):
         self.login('anon')
         email = self.vreg['etypes'].etype_class('EmailAddress')(self.request())
-        rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+        rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
         self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
                           'WHERE S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, '
                           'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
@@ -442,8 +442,8 @@
         e['data_format'] = 'text/html'
         e['data_encoding'] = 'ascii'
         e._cw.transaction_data = {} # XXX req should be a session
-        self.assertEquals(set(e.get_words()),
-                          set(['an', 'html', 'file', 'du', 'html', 'some', 'data']))
+        self.assertEquals(e.cw_adapt_to('IFTIndexable').get_words(),
+                          {'C': [u'du', u'html', 'an', 'html', 'file', u'some', u'data']})
 
 
     def test_nonregr_relation_cache(self):
@@ -462,9 +462,9 @@
         trinfo = self.execute('Any X WHERE X eid %(x)s', {'x': eid}).get_entity(0, 0)
         trinfo.complete()
         self.failUnless(isinstance(trinfo['creation_date'], datetime))
-        self.failUnless(trinfo.relation_cached('from_state', 'subject'))
-        self.failUnless(trinfo.relation_cached('to_state', 'subject'))
-        self.failUnless(trinfo.relation_cached('wf_info_for', 'subject'))
+        self.failUnless(trinfo.cw_relation_cached('from_state', 'subject'))
+        self.failUnless(trinfo.cw_relation_cached('to_state', 'subject'))
+        self.failUnless(trinfo.cw_relation_cached('wf_info_for', 'subject'))
         self.assertEquals(trinfo.by_transition, ())
 
     def test_request_cache(self):
@@ -508,7 +508,7 @@
     def test_metainformation_and_external_absolute_url(self):
         req = self.request()
         note = req.create_entity('Note', type=u'z')
-        metainf = note.metainformation()
+        metainf = note.cw_metainformation()
         self.assertEquals(metainf, {'source': {'adapter': 'native', 'uri': 'system'}, 'type': u'Note', 'extid': None})
         self.assertEquals(note.absolute_url(), 'http://testing.fr/cubicweb/note/%s' % note.eid)
         metainf['source'] = metainf['source'].copy()
--- a/test/unittest_rset.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_rset.py	Mon Jul 19 15:37:02 2010 +0200
@@ -233,10 +233,10 @@
         self.assertEquals(e['surname'], 'di mascio')
         self.assertRaises(KeyError, e.__getitem__, 'firstname')
         self.assertRaises(KeyError, e.__getitem__, 'creation_date')
-        self.assertEquals(pprelcachedict(e._related_cache), [])
+        self.assertEquals(pprelcachedict(e._cw_related_cache), [])
         e.complete()
         self.assertEquals(e['firstname'], 'adrien')
-        self.assertEquals(pprelcachedict(e._related_cache), [])
+        self.assertEquals(pprelcachedict(e._cw_related_cache), [])
 
     def test_get_entity_advanced(self):
         self.request().create_entity('Bookmark', title=u'zou', path=u'/view')
@@ -249,19 +249,19 @@
         self.assertEquals(e['title'], 'zou')
         self.assertRaises(KeyError, e.__getitem__, 'path')
         self.assertEquals(e.view('text'), 'zou')
-        self.assertEquals(pprelcachedict(e._related_cache), [])
+        self.assertEquals(pprelcachedict(e._cw_related_cache), [])
 
         e = rset.get_entity(0, 1)
         self.assertEquals(e.cw_row, 0)
         self.assertEquals(e.cw_col, 1)
         self.assertEquals(e['login'], 'anon')
         self.assertRaises(KeyError, e.__getitem__, 'firstname')
-        self.assertEquals(pprelcachedict(e._related_cache),
+        self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [])
         e.complete()
         self.assertEquals(e['firstname'], None)
         self.assertEquals(e.view('text'), 'anon')
-        self.assertEquals(pprelcachedict(e._related_cache),
+        self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [])
 
         self.assertRaises(NotAnEntity, rset.get_entity, 0, 2)
@@ -273,7 +273,7 @@
         seid = self.execute('State X WHERE X name "activated"')[0][0]
         # for_user / in_group are prefetched in CWUser __init__, in_state should
         # be filed from our query rset
-        self.assertEquals(pprelcachedict(e._related_cache),
+        self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [('in_state_subject', [seid])])
 
     def test_get_entity_advanced_prefilled_cache(self):
@@ -283,7 +283,7 @@
                             'X title XT, S name SN, U login UL, X eid %s' % e.eid)
         e = rset.get_entity(0, 0)
         self.assertEquals(e['title'], 'zou')
-        self.assertEquals(pprelcachedict(e._related_cache),
+        self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [('created_by_subject', [5])])
         # first level of recursion
         u = e.created_by[0]
@@ -302,9 +302,9 @@
         e = rset.get_entity(0, 0)
         # if any of the assertion below fails with a KeyError, the relation is not cached
         # related entities should be an empty list
-        self.assertEquals(e.related_cache('primary_email', 'subject', True), ())
+        self.assertEquals(e._cw_relation_cache('primary_email', 'subject', True), ())
         # related rset should be an empty rset
-        cached = e.related_cache('primary_email', 'subject', False)
+        cached = e._cw_relation_cache('primary_email', 'subject', False)
         self.assertIsInstance(cached, ResultSet)
         self.assertEquals(cached.rowcount, 0)
 
@@ -405,5 +405,19 @@
         rset = self.execute('Any D, COUNT(U) GROUPBY D WHERE U is CWUser, U creation_date D')
         self.assertEquals(rset.related_entity(0,0), (None, None))
 
+    def test_str(self):
+        rset = self.execute('(Any X,N WHERE X is CWGroup, X name N)')
+        self.assertIsInstance(str(rset), basestring)
+        self.assertEquals(len(str(rset).splitlines()), 1)
+
+    def test_repr(self):
+        rset = self.execute('(Any X,N WHERE X is CWGroup, X name N)')
+        self.assertIsInstance(repr(rset), basestring)
+        self.assertTrue(len(repr(rset).splitlines()) > 1)
+
+        rset = self.execute('(Any X WHERE X is CWGroup, X name "managers")')
+        self.assertIsInstance(str(rset), basestring)
+        self.assertEquals(len(str(rset).splitlines()), 1)
+
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_schema.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_schema.py	Mon Jul 19 15:37:02 2010 +0200
@@ -176,7 +176,7 @@
                              'CWCache', 'CWConstraint', 'CWConstraintType', 'CWEType',
                              'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation',
                              'CWPermission', 'CWProperty', 'CWRType', 'CWUser',
-                             'ExternalUri', 'File', 'Float', 'Image', 'Int', 'Interval', 'Note',
+                             'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
                              'Password', 'Personne',
                              'RQLExpression',
                              'Societe', 'State', 'String', 'SubNote', 'SubWorkflowExitPoint',
--- a/test/unittest_selectors.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_selectors.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,15 +15,14 @@
 #
 # 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
-
-"""
+"""unit tests for selectors mechanism"""
 
 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 implements, match_user_groups
+from cubicweb.selectors import is_instance, adaptable, match_user_groups
 from cubicweb.interfaces import IDownloadable
 from cubicweb.web import action
 
@@ -93,12 +92,12 @@
         self.assertEquals(selector(None), 2)
 
     def test_search_selectors(self):
-        sel = implements('something')
-        self.assertIs(sel.search_selector(implements), sel)
+        sel = is_instance('something')
+        self.assertIs(sel.search_selector(is_instance), sel)
         csel = AndSelector(sel, Selector())
-        self.assertIs(csel.search_selector(implements), sel)
+        self.assertIs(csel.search_selector(is_instance), sel)
         csel = AndSelector(Selector(), sel)
-        self.assertIs(csel.search_selector(implements), sel)
+        self.assertIs(csel.search_selector(is_instance), sel)
 
     def test_inplace_and(self):
         selector = _1_()
@@ -140,16 +139,17 @@
 class ImplementsSelectorTC(CubicWebTC):
     def test_etype_priority(self):
         req = self.request()
-        cls = self.vreg['etypes'].etype_class('File')
-        anyscore = implements('Any').score_class(cls, req)
-        idownscore = implements(IDownloadable).score_class(cls, req)
+        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.failUnless(idownscore > anyscore, (idownscore, anyscore))
-        filescore = implements('File').score_class(cls, req)
+        filescore = is_instance('File')(f.__class__, req, rset=rset)
         self.failUnless(filescore > idownscore, (filescore, idownscore))
 
     def test_etype_inheritance_no_yams_inheritance(self):
         cls = self.vreg['etypes'].etype_class('Personne')
-        self.failIf(implements('Societe').score_class(cls, self.request()))
+        self.failIf(is_instance('Societe').score_class(cls, self.request()))
 
 
 class MatchUserGroupsTC(CubicWebTC):
--- a/test/unittest_uilib.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_uilib.py	Mon Jul 19 15:37:02 2010 +0200
@@ -142,6 +142,14 @@
         self.assertEquals(uilib.soup2xhtml('hop </html> hop', 'ascii'),
                           'hop  hop')
 
+    def test_js(self):
+        self.assertEquals(str(uilib.js.pouet(1, "2")),
+                          'pouet(1,"2")')
+        self.assertEquals(str(uilib.js.cw.pouet(1, "2")),
+                          'cw.pouet(1,"2")')
+        self.assertEquals(str(uilib.js.cw.pouet(1, "2").pouet(None)),
+                          'cw.pouet(1,"2").pouet(null)')
+
 if __name__ == '__main__':
     unittest_main()
 
--- a/test/unittest_utils.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_utils.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,16 +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/>.
-"""unit tests for module cubicweb.utils
-
-"""
+"""unit tests for module cubicweb.utils"""
 
 import re
 import decimal
 import datetime
 
 from logilab.common.testlib import TestCase, unittest_main
+
 from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, RepeatList
+from cubicweb.entity import Entity
 
 try:
     from cubicweb.utils import CubicWebJsonEncoder, json
@@ -99,6 +99,7 @@
         l.pop(2)
         self.assertEquals(l, [(1, 3)]*2)
 
+
 class SizeConstrainedListTC(TestCase):
 
     def test_append(self):
@@ -117,6 +118,7 @@
             l.extend(extension)
             yield self.assertEquals, l, expected
 
+
 class JSONEncoderTC(TestCase):
     def setUp(self):
         if json is None:
@@ -136,6 +138,20 @@
     def test_encoding_decimal(self):
         self.assertEquals(self.encode(decimal.Decimal('1.2')), '1.2')
 
+    def test_encoding_bare_entity(self):
+        e = Entity(None)
+        e['pouet'] = 'hop'
+        e.eid = 2
+        self.assertEquals(json.loads(self.encode(e)),
+                          {'pouet': 'hop', 'eid': 2})
+
+    def test_encoding_entity_in_list(self):
+        e = Entity(None)
+        e['pouet'] = 'hop'
+        e.eid = 2
+        self.assertEquals(json.loads(self.encode([e])),
+                          [{'pouet': 'hop', 'eid': 2}])
+
     def test_encoding_unknown_stuff(self):
         self.assertEquals(self.encode(TestCase), 'null')
 
--- a/test/unittest_vregistry.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/test/unittest_vregistry.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +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/>.
-"""
 
-"""
 from logilab.common.testlib import unittest_main, TestCase
 
 from os.path import join
@@ -27,7 +25,7 @@
 from cubicweb.cwvreg import CubicWebVRegistry, UnknownProperty
 from cubicweb.devtools import TestServerConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.interfaces import IMileStone
+from cubicweb.view import EntityAdapter
 
 from cubes.card.entities import Card
 
@@ -56,21 +54,26 @@
 
 
     def test_load_subinterface_based_appobjects(self):
-        self.vreg.reset()
         self.vreg.register_objects([join(BASE, 'web', 'views', 'iprogress.py')])
         # check progressbar was kicked
         self.failIf(self.vreg['views'].get('progressbar'))
-        class MyCard(Card):
-            __implements__ = (IMileStone,)
-        self.vreg.reset()
+        # we've to emulate register_objects to add custom MyCard objects
+        path = [join(BASE, 'entities', '__init__.py'),
+                join(BASE, 'entities', 'adapters.py'),
+                join(BASE, 'web', 'views', 'iprogress.py')]
+        filemods = self.vreg.init_registration(path, None)
+        for filepath, modname in filemods:
+            self.vreg.load_file(filepath, modname)
+        class CardIProgressAdapter(EntityAdapter):
+            __regid__ = 'IProgress'
         self.vreg._loadedmods[__name__] = {}
-        self.vreg.register(MyCard)
-        self.vreg.register_objects([join(BASE, 'entities', '__init__.py'),
-                                    join(BASE, 'web', 'views', 'iprogress.py')])
+        self.vreg.register(CardIProgressAdapter)
+        self.vreg.initialization_completed()
         # check progressbar isn't kicked
         self.assertEquals(len(self.vreg['views']['progressbar']), 1)
 
     def test_properties(self):
+        self.vreg.reset()
         self.failIf('system.version.cubicweb' in self.vreg['propertydefs'])
         self.failUnless(self.vreg.property_info('system.version.cubicweb'))
         self.assertRaises(UnknownProperty, self.vreg.property_info, 'a.non.existent.key')
--- a/uilib.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/uilib.py	Mon Jul 19 15:37:02 2010 +0200
@@ -31,6 +31,8 @@
 from logilab.mtconverter import xml_escape, html_unescape
 from logilab.common.date import ustrftime
 
+from cubicweb.utils import json_dumps
+
 
 def rql_for_eid(eid):
     """return the rql query necessary to fetch entity with the given eid.  This
@@ -228,6 +230,54 @@
 
 # HTML generation helper functions ############################################
 
+class _JSId(object):
+    def __init__(self, id, parent=None):
+        self.id = id
+        self.parent = parent
+    def __unicode__(self):
+        if self.parent:
+            return u'%s.%s' % (self.parent, self.id)
+        return unicode(self.id)
+    def __str__(self):
+        return unicode(self).encode('utf8')
+    def __getattr__(self, attr):
+        return _JSId(attr, self)
+    def __call__(self, *args):
+        return _JSCallArgs(args, self)
+
+class _JSCallArgs(_JSId):
+    def __init__(self, args, parent=None):
+        assert isinstance(args, tuple)
+        self.args = args
+        self.parent = parent
+    def __unicode__(self):
+        args = u','.join(json_dumps(arg) for arg in self.args)
+        if self.parent:
+            return u'%s(%s)' % (self.parent, args)
+        return args
+
+class _JS(object):
+    def __getattr__(self, attr):
+        return _JSId(attr)
+
+"""magic object to return strings suitable to call some javascript function with
+the given arguments (which should be correctly typed).
+
+>>> str(js.pouet(1, "2"))
+'pouet(1,"2")'
+>>> str(js.cw.pouet(1, "2"))
+'cw.pouet(1,"2")'
+>>> str(js.cw.pouet(1, "2").pouet(None))
+'cw.pouet(1,"2").pouet(null)')
+"""
+js = _JS()
+
+def domid(string):
+    """return a valid DOM id from a string (should also be usable in jQuery
+    search expression...)
+    """
+    return string.replace('.', '_').replace('-', '_')
+
 HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
                               'img', 'area', 'input', 'col'))
 
--- a/utils.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/utils.py	Mon Jul 19 15:37:02 2010 +0200
@@ -121,6 +121,9 @@
     def __init__(self, size, item):
         self._size = size
         self._item = item
+    def __repr__(self):
+        return '<cubicweb.utils.RepeatList at %s item=%s size=%s>' % (
+            id(self), self._item, self._size)
     def __len__(self):
         return self._size
     def __nonzero__(self):
@@ -324,32 +327,23 @@
 
 try:
     # may not be there if cubicweb-web not installed
-    if sys.version_info < (2,6):
+    if sys.version_info < (2, 6):
         import simplejson as json
     else:
         import json
 except ImportError:
-    pass
+    json_dumps = None
+
 else:
 
     class CubicWebJsonEncoder(json.JSONEncoder):
         """define a json encoder to be able to encode yams std types"""
 
-        # _iterencode is the only entry point I've found to use a custom encode
-        # hook early enough: .default() is called if nothing else matched before,
-        # .iterencode() is called once on the main structure to encode and then
-        # never gets called again.
-        # For the record, our main use case is in FormValidateController with:
-        #   json.dumps((status, args, entity), cls=CubicWebJsonEncoder)
-        # where we want all the entity attributes, including eid, to be part
-        # of the json object dumped.
-        # This would have once more been easier if Entity didn't extend dict.
-        def _iterencode(self, obj, markers=None):
-            if hasattr(obj, '__json_encode__'):
-                obj = obj.__json_encode__()
-            return json.JSONEncoder._iterencode(self, obj, markers)
-
         def default(self, obj):
+            if hasattr(obj, 'eid'):
+                d = obj.cw_attr_cache.copy()
+                d['eid'] = obj.eid
+                return d
             if isinstance(obj, datetime.datetime):
                 return obj.strftime('%Y/%m/%d %H:%M:%S')
             elif isinstance(obj, datetime.date):
@@ -367,6 +361,9 @@
                 # just return None in those cases.
                 return None
 
+    def json_dumps(value):
+        return json.dumps(value, cls=CubicWebJsonEncoder)
+
 
 @deprecated('[3.7] merge_dicts is deprecated')
 def merge_dicts(dict1, dict2):
--- a/view.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/view.py	Mon Jul 19 15:37:02 2010 +0200
@@ -34,6 +34,7 @@
 from cubicweb.appobject import AppObject
 from cubicweb.utils import UStringIO, HTMLStream
 from cubicweb.schema import display_name
+from cubicweb.vregistry import classid
 
 # robots control
 NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
@@ -366,6 +367,17 @@
     __select__ = non_final_entity()
     category = 'entityview'
 
+    def call(self, **kwargs):
+        if self.cw_rset is None:
+            self.entity_call(self.cw_extra_kwargs.pop('entity'))
+        else:
+            super(EntityView, self).call(**kwargs)
+
+    def cell_call(self, row, col, **kwargs):
+        self.entity_call(self.cw_rset.get_entity(row, col), **kwargs)
+
+    def entity_call(self, entity, **kwargs):
+        raise NotImplementedError()
 
 class StartupView(View):
     """base class for views which doesn't need a particular result set to be
@@ -519,3 +531,37 @@
     # XXX a generic '%s%s' % (self.__regid__, self.__registry__.capitalize()) would probably be nicer
     def div_id(self):
         return '%sComponent' % self.__regid__
+
+
+class Adapter(AppObject):
+    """base class for adapters"""
+    __registry__ = 'adapters'
+
+
+class EntityAdapter(Adapter):
+    """base class for entity adapters (eg adapt an entity to an interface)"""
+    def __init__(self, _cw, **kwargs):
+        try:
+            self.entity = kwargs.pop('entity')
+        except KeyError:
+            self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
+                                                    kwargs.get('col') or 0)
+        Adapter.__init__(self, _cw, **kwargs)
+
+
+def implements_adapter_compat(iface):
+    def _pre39_compat(func):
+        def decorated(self, *args, **kwargs):
+            entity = self.entity
+            if hasattr(entity, func.__name__):
+                warn('[3.9] %s method is deprecated, define it on a custom '
+                     '%s for %s instead' % (func.__name__, iface,
+                                            classid(entity.__class__)),
+                     DeprecationWarning)
+                member = getattr(entity, func.__name__)
+                if callable(member):
+                    return member(*args, **kwargs)
+                return member
+            return func(self, *args, **kwargs)
+        return decorated
+    return _pre39_compat
--- a/vregistry.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/vregistry.py	Mon Jul 19 15:37:02 2010 +0200
@@ -44,7 +44,7 @@
 
 from cubicweb import CW_SOFTWARE_ROOT
 from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
-from cubicweb.appobject import AppObject
+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
@@ -83,16 +83,6 @@
     """returns a unique identifier for an appobject class"""
     return '%s.%s' % (cls.__module__, cls.__name__)
 
-def class_regid(cls):
-    """returns a unique identifier for an appobject class"""
-    if 'id' in cls.__dict__:
-        warn('[3.6] %s.%s: id is deprecated, use __regid__'
-             % (cls.__module__, cls.__name__), DeprecationWarning)
-        cls.__regid__ = cls.id
-    if hasattr(cls, 'id') and not isinstance(cls.id, property):
-        return cls.id
-    return cls.__regid__
-
 def class_registries(cls, registryname):
     if registryname:
         return (registryname,)
@@ -235,8 +225,8 @@
                                      % (args, kwargs.keys(),
                                         [repr(v) for v in appobjects]))
         if len(winners) > 1:
-            # log in production environement, error while debugging
-            if self.config.debugmode:
+            # log in production environement / test, error while debugging
+            if self.config.debugmode or self.config.mode == 'test':
                 raise Exception('select ambiguity, args: %s\nkwargs: %s %s'
                                 % (args, kwargs.keys(),
                                    [repr(v) for v in winners]))
@@ -405,6 +395,7 @@
     # 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
--- a/web/__init__.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/__init__.py	Mon Jul 19 15:37:02 2010 +0200
@@ -17,26 +17,19 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """CubicWeb web client core. You'll need a apache-modpython or twisted
 publisher to get a full CubicWeb web application
-
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-import sys
-if sys.version_info < (2,6):
-    import simplejson as json
-else:
-    import json
-
-dumps = json.dumps
-
 from urllib import quote as urlquote
 
 from logilab.common.deprecation import deprecated
 
 from cubicweb.web._exceptions import *
-from cubicweb.utils import CubicWebJsonEncoder
+from cubicweb.utils import json_dumps
+
+dumps = deprecated('[3.9] use cubicweb.utils.json_dumps instead of dumps')(json_dumps)
 
 INTERNAL_FIELD_VALUE = '__cubicweb_internal_field__'
 
@@ -65,9 +58,6 @@
 FACETTES = set()
 
 
-def json_dumps(value):
-    return dumps(value, cls=CubicWebJsonEncoder)
-
 def jsonize(function):
     def newfunc(*args, **kwargs):
         value = function(*args, **kwargs)
@@ -77,7 +67,7 @@
             return json_dumps(repr(value))
     return newfunc
 
-@deprecated('[3.4] use req.build_ajax_replace_url() instead')
+@deprecated('[3.4] use req.ajax_replace_url() instead')
 def ajax_replace_url(nodeid, rql, vid=None, swap=False, **extraparams):
     """builds a replacePageChunk-like url
     >>> ajax_replace_url('foo', 'Person P')
--- a/web/_exceptions.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/_exceptions.py	Mon Jul 19 15:37:02 2010 +0200
@@ -16,12 +16,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/>.
-"""exceptions used in the core of the CubicWeb web application
+"""exceptions used in the core of the CubicWeb web application"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb._exceptions import *
+from cubicweb.utils import json_dumps
 
 class PublishException(CubicWebException):
     """base class for publishing related exception"""
@@ -66,8 +66,7 @@
         self.reason = reason
 
     def dumps(self):
-        from cubicweb.web import json
-        return json.dumps({'reason': self.reason})
+        return json_dumps({'reason': self.reason})
 
 class LogOut(PublishException):
     """raised to ask for deauthentication of a logged in user"""
--- a/web/action.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/action.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""abstract action classes for CubicWeb web client
+"""abstract action classes for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
--- a/web/application.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/application.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""CubicWeb web client application object
+"""CubicWeb web client application object"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
@@ -234,7 +233,7 @@
     def _update_last_login_time(self, req):
         # XXX should properly detect missing permission / non writeable source
         # and avoid "except (RepositoryError, Unauthorized)" below
-        if req.user.metainformation()['source']['adapter'] == 'ldapuser':
+        if req.user.cw_metainformation()['source']['adapter'] == 'ldapuser':
             return
         try:
             req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
@@ -282,12 +281,12 @@
     to publish HTTP request.
     """
 
-    def __init__(self, config, debug=None,
+    def __init__(self, config,
                  session_handler_fact=CookieSessionHandler,
                  vreg=None):
         self.info('starting web instance from %s', config.apphome)
         if vreg is None:
-            vreg = cwvreg.CubicWebVRegistry(config, debug=debug)
+            vreg = cwvreg.CubicWebVRegistry(config)
         self.vreg = vreg
         # connect to the repository and get instance's schema
         self.repo = config.repository(vreg)
--- a/web/box.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/box.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""abstract box classes for CubicWeb web client
+"""abstract box classes for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -26,10 +25,11 @@
 from cubicweb import Unauthorized, role as get_role, target as get_target
 from cubicweb.schema import display_name
 from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
-                                match_context_prop, partial_has_related_entities)
+                                match_context_prop, partial_relation_possible,
+                                partial_has_related_entities)
 from cubicweb.view import View, ReloadableMixIn
-
-from cubicweb.web import INTERNAL_FIELD_VALUE
+from cubicweb.uilib import domid, js
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
                                       RawBoxItem, BoxSeparator)
 from cubicweb.web.action import UnregisteredAction
@@ -143,7 +143,7 @@
 
     def to_display_rql(self):
         assert self.rql is not None, self.__regid__
-        return (self.rql, {'x': self._cw.user.eid}, 'x')
+        return (self.rql, {'x': self._cw.user.eid})
 
 
 class EntityBoxTemplate(BoxTemplate):
@@ -224,8 +224,8 @@
         """returns the list of unrelated entities, using the entity's
         appropriate vocabulary function
         """
-        skip = set(e.eid for e in entity.related(self.rtype, get_role(self),
-                                                 entities=True))
+        skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self),
+                                                          entities=True))
         skip.add(None)
         skip.add(INTERNAL_FIELD_VALUE)
         filteretype = getattr(self, 'etype', None)
@@ -241,3 +241,92 @@
                     entities.append(entity)
         return entities
 
+
+class AjaxEditRelationBoxTemplate(EntityBoxTemplate):
+    __select__ = EntityBoxTemplate.__select__ & partial_relation_possible()
+
+    # view used to display related entties
+    item_vid = 'incontext'
+    # values separator when multiple values are allowed
+    separator = ','
+    # msgid of the message to display when some new relation has been added/removed
+    added_msg = None
+    removed_msg = None
+
+    # class attributes below *must* be set in concret classes (additionaly to
+    # rtype / role [/ target_etype]. They should correspond to js_* methods on
+    # the json controller
+
+    # function(eid)
+    # -> expected to return a list of values to display as input selector
+    #    vocabulary
+    fname_vocabulary = None
+
+    # function(eid, value)
+    # -> handle the selector's input (eg create necessary entities and/or
+    # relations). If the relation is multiple, you'll get a list of value, else
+    # a single string value.
+    fname_validate = None
+
+    # function(eid, linked entity eid)
+    # -> remove the relation
+    fname_remove = None
+
+    def cell_call(self, row, col, **kwargs):
+        req = self._cw
+        entity = self.cw_rset.get_entity(row, col)
+        related = entity.related(self.rtype, self.role)
+        rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
+        if self.role == 'subject':
+            mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid)
+            maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid)
+        else:
+            mayadd = rdef.has_perm(req, 'add', toeid=entity.eid)
+            maydel = rdef.has_perm(req, 'delete', toeid=entity.eid)
+        if not (related or mayadd):
+            return
+        if mayadd or maydel:
+            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
+        _ = req._
+        w = self.w
+        divid = domid(self.__regid__) + unicode(entity.eid)
+        w(u'<div class="sideBox" id="%s%s">' % (domid(self.__regid__), entity.eid))
+        w(u'<div class="sideBoxTitle"><span>%s</span></div>' %
+               rdef.rtype.display_name(req, self.role))
+        w(u'<div class="sideBox"><div class="sideBoxBody">')
+        if related:
+            w(u'<table>')
+            for rentity in related.entities():
+                # for each related entity, provide a link to remove the relation
+                subview = rentity.view(self.item_vid)
+                if maydel:
+                    jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
+                        self.__regid__, entity.eid, rentity.eid,
+                        self.fname_remove,
+                        self.removed_msg and _(self.removed_msg)))
+                    w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
+                      '<td class="tagged">%s</td></tr>' % (xml_escape(jscall),
+                                                           subview))
+                else:
+                    w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
+            w(u'</table>')
+        else:
+            w(_('no related entity'))
+        if mayadd:
+            req.add_js('jquery.autocomplete.js')
+            req.add_css('jquery.autocomplete.css')
+            multiple = rdef.role_cardinality(self.role) in '*+'
+            w(u'<table><tr><td>')
+            jscall = unicode(js.ajaxBoxShowSelector(
+                self.__regid__, entity.eid, self.fname_vocabulary,
+                self.fname_validate, self.added_msg and _(self.added_msg),
+                _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
+                multiple and self.separator))
+            w('<a class="button sglink" href="javascript: %s">%s</a>' % (
+                xml_escape(jscall),
+                multiple and _('add_relation') or _('update_relation')))
+            w(u'</td><td>')
+            w(u'<div id="%sHolder"></div>' % divid)
+            w(u'</td></tr></table>')
+        w(u'</div>\n')
+        w(u'</div></div>\n')
--- a/web/component.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/component.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,10 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""abstract component class and base components definition for CubicWeb web client
+"""abstract component class and base components definition for CubicWeb web
+client
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -25,7 +26,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import role
-from cubicweb.web import json
+from cubicweb.utils import json_dumps
 from cubicweb.view import Component
 from cubicweb.selectors import (
     paginated_rset, one_line_rset, primary_view, match_context_prop,
@@ -61,9 +62,15 @@
     context = 'navcontentbottom'
 
     def call(self, view=None):
-        return self.cell_call(0, 0, view=view)
+        if self.cw_rset is None:
+            self.entity_call(self.cw_extra_kwargs.pop('entity'))
+        else:
+            self.cell_call(0, 0, view=view)
 
     def cell_call(self, row, col, view=None):
+        self.entity_call(self.cw_rset.get_entity(row, col), view=view)
+
+    def entity_call(self, entity, view=None):
         raise NotImplementedError()
 
 
@@ -126,10 +133,12 @@
         if self.stop_param in params:
             del params[self.stop_param]
 
-    def page_url(self, path, params, start, stop):
+    def page_url(self, path, params, start=None, stop=None):
         params = dict(params)
-        params.update({self.start_param : start,
-                       self.stop_param : stop,})
+        if start is not None:
+            params[self.start_param] = start
+        if stop is not None:
+            params[self.stop_param] = stop
         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)
@@ -137,8 +146,9 @@
             rql = params.pop('rql', self.cw_rset.printable_rql())
             # latest 'true' used for 'swap' mode
             url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
-                json.dumps(params.get('divid', 'pageContent')),
-                json.dumps(rql), json.dumps(params.pop('vid', None)), json.dumps(params))
+                json_dumps(params.get('divid', 'pageContent')),
+                json_dumps(rql), json_dumps(params.pop('vid', None)),
+                json_dumps(params))
         else:
             url = self._cw.build_url(path, **params)
         return url
--- a/web/controller.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/controller.py	Mon Jul 19 15:37:02 2010 +0200
@@ -23,6 +23,7 @@
 
 from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
+from cubicweb.mail import format_mail
 from cubicweb.web import LOGGER, Redirect, RequestError
 
 
@@ -79,18 +80,20 @@
 
     # generic methods useful for concrete implementations ######################
 
-    def process_rql(self, rql):
+    def process_rql(self):
         """execute rql if specified"""
-        # XXX assigning to self really necessary?
-        self.cw_rset = None
+        req = self._cw
+        rql = req.form.get('rql')
         if rql:
-            self._cw.ensure_ro_rql(rql)
+            req.ensure_ro_rql(rql)
             if not isinstance(rql, unicode):
-                rql = unicode(rql, self._cw.encoding)
-            pp = self._cw.vreg['components'].select_or_none('magicsearch', self._cw)
+                rql = unicode(rql, req.encoding)
+            pp = req.vreg['components'].select_or_none('magicsearch', req)
             if pp is not None:
-                self.cw_rset = pp.process_query(rql)
-        return self.cw_rset
+                return pp.process_query(rql)
+        if 'eid' in req.form:
+            return req.eid_rset(req.form['eid'])
+        return None
 
     def notify_edited(self, entity):
         """called by edit_entity() to notify which entity is edited"""
@@ -104,6 +107,16 @@
         view.set_http_cache_headers()
         self._cw.validate_cache()
 
+    def sendmail(self, recipient, subject, body):
+        senderemail = self._cw.user.cw_adapt_to('IEmailable').get_email()
+        msg = format_mail({'email' : senderemail,
+                           'name' : self._cw.user.dc_title(),},
+                          [recipient], body, subject)
+        if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
+            msg = self._cw._('could not connect to the SMTP server')
+            url = self._cw.build_url(__message=msg)
+            raise Redirect(url)
+
     def reset(self):
         """reset form parameters and redirect to a view determinated by given
         parameters
Binary file web/data/actionBoxHeader.png has changed
Binary file web/data/boxHeader.png has changed
Binary file web/data/button.png has changed
--- a/web/data/cubicweb.acl.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.acl.css	Mon Jul 19 15:37:02 2010 +0200
@@ -6,78 +6,35 @@
  */
 
 /******************************************************************************/
-/* security edition form (views/management.py)                                */
+/* security edition form (views/management.py)   web/views/schema.py          */
 /******************************************************************************/
 
 h2.schema{
- background : #ff7700;
- color: #fff;
- font-weight: bold;
- padding : 0.1em 0.3em;
+ color: %(aColor)s;
 }
 
-
-h3.schema{
+table.schemaInfo td a.users{
+ color : #00CC33;
  font-weight: bold;
 }
 
-h4 a,
-h4 a:link,
-h4 a:visited{
- color:#000;
- }
-
-table.schemaInfo {
-  margin: 1em 0em;
-  text-align: left;
-  border: 1px solid black;
-  border-collapse: collapse;
-  width:100%;
-}
-
-table.schemaInfo th,
-table.schemaInfo td {
-  padding: .3em .5em;
-  border: 1px solid grey;
-  width:33%;
-}
-
-
-table.schemaInfo tr th {
- padding: 0.2em 0px 0.2em 5px;
- background-image:none;
- background-color:#dfdfdf;
-}
-
-table.schemaInfo thead tr {
-  border: 1px solid #dfdfdf;
-}
-
-table.schemaInfo td {
-  padding: 3px 10px 3px 5px;
-
-}
-
-a.users{
- color : #00CC33;
- font-weight: bold }
-
-a.guests{
- color :  #ff7700;
+table.schemaInfo td a.guests{
+ color:  #ff7700;
  font-weight: bold;
 }
 
-a.owners{
- color : #8b0000;
+table.schemaInfo td a.owners{
+ color: #8b0000;
  font-weight: bold;
 }
 
-a.managers{
+table.schemaInfo td a.managers{
  color: #000000;
+ font-weight: bold;
 }
 
 .discret,
-a.grey{
+table.schemaInfo td a.grey{
  color:#666;
 }
 
@@ -86,39 +43,9 @@
 }
 
 .red{
- color :  #ff7700;
+ color:  #ff7700;
  }
 
 div#schema_security{
  width:100%;
- }
-/******************************************************************************/
-/* user groups edition form (views/euser.py)                                  */
-/******************************************************************************/
-
-table#groupedit {
-  margin: 1ex 1em;
-  text-align: left;
-  border: 1px solid black;
-  border-collapse: collapse;
-}
-
-table#groupedit th,
-table#groupedit td {
-  padding: 0.5em 1em;
-}
-
-table#groupedit tr {
-  border-bottom: 1px solid black;
-}
-
-table#groupedit tr.nogroup {
-  border: 1px solid red;
-  margin: 1px;
-}
-
-table#groupedit td {
-  text-align: center;
-  padding: 0.5em;
-}
-
+ }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.ajax.box.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,81 @@
+/**
+ * Functions for ajax boxes.
+ *
+ *  :organization: Logilab
+ *  :copyright: 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
+ */
+
+function ajaxBoxValidateSelectorInput(boxid, eid, separator, fname, msg) {
+    var holderid = cw.utils.domid(boxid) + eid + 'Holder';
+    var value = $('#' + holderid + 'Input').val();
+    if (separator) {
+	value = $.map(value.split(separator), jQuery.trim);
+    }
+    var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value));
+    d.addCallback(function() {
+	    $('#' + holderid).empty();
+	    var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+	    $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+	    if (msg) {
+		document.location.hash = '#header';
+		updateMessage(msg);
+	    }
+	});
+}
+
+function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) {
+    var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid));
+    d.addCallback(function() {
+	    var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+	    $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+	    if (msg) {
+		document.location.hash = '#header';
+		updateMessage(msg);
+	    }
+    });
+}
+
+function ajaxBoxShowSelector(boxid, eid,
+			     unrelfname,
+			     addfname, msg,
+			     oklabel, cancellabel,
+			     separator) {
+    var holderid = cw.utils.domid(boxid) + eid + 'Holder';
+    var holder = $('#' + holderid);
+    if (holder.children().length) {
+	holder.empty();
+    }
+    else {
+	var inputid = holderid + 'Input';
+	var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
+	deferred.addCallback(function (unrelated) {
+	    var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
+	    holder.append(input).show();
+	    $input = $(input);
+	    $input.keypress(function (event) {
+		if (event.keyCode == KEYS.KEY_ENTER) {
+		    // XXX not very user friendly: we should test that the suggestions
+		    //     aren't visible anymore
+		    ajaxBoxValidateSelectorInput(boxid, eid, separator, addfname, msg);
+		}
+	    });
+	    var buttons = DIV({'class' : "sgformbuttons"},
+			      A({'href' : "javascript: noop();",
+				 'onclick' : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput',
+								  boxid, eid, separator, addfname, msg)},
+				  oklabel),
+			      ' / ',
+			      A({'href' : "javascript: noop();",
+				 'onclick' : '$("#' + holderid + '").empty()'},
+				  cancellabel));
+	    holder.append(buttons);
+	    $input.autocomplete(unrelated, {
+		multiple: separator,
+		max: 15
+	    });
+	    $input.focus();
+	});
+    }
+}
--- a/web/data/cubicweb.ajax.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.ajax.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,33 +1,132 @@
-/*
- *  :organization: Logilab
- *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
- *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+/* copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ * contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
+ * This file is part of CubicWeb.
+ *
+ * CubicWeb is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation, either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along
+ * with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-CubicWeb.require('python.js');
-CubicWeb.require('htmlhelpers.js');
+/**
+ * .. function:: Deferred
+ *
+ * dummy ultra minimalist implementation of deferred for jQuery
+ */
+function Deferred() {
+    this.__init__(this);
+}
+
+jQuery.extend(Deferred.prototype, {
+    __init__: function() {
+        this._onSuccess = [];
+        this._onFailure = [];
+        this._req = null;
+        this._result = null;
+        this._error = null;
+    },
+
+    addCallback: function(callback) {
+        if (this._req.readyState == 4) {
+            if (this._result) {
+                var args = [this._result, this._req];
+                jQuery.merge(args, cw.utils.sliceList(arguments, 1));
+                callback.apply(null, args);
+            }
+        }
+        else {
+            this._onSuccess.push([callback, cw.utils.sliceList(arguments, 1)]);
+        }
+        return this;
+    },
+
+    addErrback: function(callback) {
+        if (this._req.readyState == 4) {
+            if (this._error) {
+                callback.apply(null, [this._error, this._req]);
+            }
+        }
+        else {
+            this._onFailure.push([callback, cw.utils.sliceList(arguments, 1)]);
+        }
+        return this;
+    },
+
+    success: function(result) {
+        this._result = result;
+        try {
+            for (var i = 0; i < this._onSuccess.length; i++) {
+                var callback = this._onSuccess[i][0];
+                var args = [result, this._req];
+                jQuery.merge(args, this._onSuccess[i][1]);
+                callback.apply(null, args);
+            }
+        } catch(error) {
+            this.error(this.xhr, null, error);
+        }
+    },
+
+    error: function(xhr, status, error) {
+        this._error = error;
+        for (var i = 0; i < this._onFailure.length; i++) {
+            var callback = this._onFailure[i][0];
+            var args = [error, this._req];
+            jQuery.merge(args, this._onFailure[i][1]);
+            callback.apply(null, args);
+        }
+    }
+
+});
+
 
 var JSON_BASE_URL = baseuri() + 'json?';
 
-function _loadAjaxHtmlHead(node, head, tag, srcattr) {
-    var loaded = [];
+//============= utility function handling remote calls responses. ==============//
+function _loadAjaxHtmlHead($node, $head, tag, srcattr) {
     var jqtagfilter = tag + '[' + srcattr + ']';
-    jQuery('head ' + jqtagfilter).each(function(i) {
-        loaded.push(this.getAttribute(srcattr));
-    });
-    node.find(tag).each(function(i) {
-        if (this.getAttribute(srcattr)) {
-            if (!loaded.contains(this.getAttribute(srcattr))) {
-                jQuery(this).appendTo(head);
+    if (cw['loaded_'+srcattr] === undefined) {
+	cw['loaded_'+srcattr] = [];
+	var loaded = cw['loaded_'+srcattr];
+	jQuery('head ' + jqtagfilter).each(function(i) {
+		loaded.push(this.getAttribute(srcattr));
+	    });
+    } else {
+	var loaded = cw['loaded_'+srcattr];
+    }
+    $node.find(tag).each(function(i) {
+	 var url = this.getAttribute(srcattr);
+        if (url) {
+            if (jQuery.inArray(url, loaded) == -1) {
+		// take care to <script> tags: jQuery append method script nodes
+		// don't appears in the DOM (See comments on
+		// http://api.jquery.com/append/), which cause undesired
+		// duplicated load in our case. After trying to use bare DOM api
+		// to avoid this, we switched to handle a list of already loaded
+		// stuff ourselves, since bare DOM api gives bug with the
+		// server-response event, since we loose control on when the
+		// script is loaded (jQuery load it immediatly).
+		loaded.push(url);
+		jQuery(this).appendTo($head);
             }
         } else {
-            jQuery(this).appendTo(head);
+            jQuery(this).appendTo($head);
         }
     });
-    node.find(jqtagfilter).remove();
+    $node.find(jqtagfilter).remove();
 }
 
-/*
+/**
+ * .. function:: function loadAjaxHtmlHead(response)
+ *
  * inspect dom response (as returned by getDomFromResponse), search for
  * a <div class="ajaxHtmlHead"> node and put its content into the real
  * document's head.
@@ -59,18 +158,13 @@
     //    we can safely return this node. Otherwise, the view itself
     //    returned several 'root' nodes and we need to keep the wrapper
     //    created by getDomFromResponse()
-    if (response.childNodes.length == 1 &&
-        response.getAttribute('cubicweb:type') == 'cwResponseWrapper') {
+    if (response.childNodes.length == 1 && response.getAttribute('cubicweb:type') == 'cwResponseWrapper') {
         return response.firstChild;
     }
     return response;
 }
 
-function preprocessAjaxLoad(node, newdomnode) {
-    return loadAjaxHtmlHead(newdomnode);
-}
-
-function postAjaxLoad(node) {
+function _postAjaxLoad(node) {
     // find sortable tables if there are some
     if (typeof(Sortable) != 'undefined') {
         Sortable.sortTables(node);
@@ -89,47 +183,80 @@
         roundedCorners(node);
     }
     if (typeof setFormsTarget != 'undefined') {
-       setFormsTarget(node);
+        setFormsTarget(node);
     }
-    loadDynamicFragments(node);
+    _loadDynamicFragments(node);
     // XXX [3.7] jQuery.one is now used instead jQuery.bind,
     // jquery.treeview.js can be unpatched accordingly.
     jQuery(CubicWeb).trigger('server-response', [true, node]);
+    jQuery(node).trigger('server-response', [true, node]);
+}
+
+function remoteCallFailed(err, req) {
+    cw.log(err);
+    if (req.status == 500) {
+        updateMessage(err);
+    } else {
+        updateMessage(_("an error occured while processing your request"));
+    }
 }
 
-/* cubicweb loadxhtml plugin to make jquery handle xhtml response
+//============= base AJAX functions to make remote calls =====================//
+/**
+ * .. function:: ajaxFuncArgs(fname, form, *args)
  *
- * fetches `url` and replaces this's content with the result
+ * extend `form` parameters to call the js_`fname` function of the json
+ * controller with `args` arguments.
+ */
+function ajaxFuncArgs(fname, form /* ... */) {
+    form = form || {};
+    $.extend(form, {
+        'fname': fname,
+        'pageid': pageid,
+        'arg': $.map(cw.utils.sliceList(arguments, 2), jQuery.toJSON)
+    });
+    return form;
+}
+
+/**
+ * .. function:: loadxhtml(url, form, reqtype='get', mode='replace', cursor=true)
  *
- * @param mode how the replacement should be done (default is 'replace')
- *  Possible values are :
+ * build url given by absolute or relative `url` and `form` parameters
+ * (dictionary), fetch it using `reqtype` method, then evaluate the
+ * returned XHTML and insert it according to `mode` in the
+ * document. Possible modes are :
+ *
  *    - 'replace' to replace the node's content with the generated HTML
  *    - 'swap' to replace the node itself with the generated HTML
  *    - 'append' to append the generated HTML to the node's content
+ *
+ * If `cursor`, turn mouse cursor into 'progress' cursor until the remote call
+ * is back.
  */
-jQuery.fn.loadxhtml = function(url, data, reqtype, mode) {
-    var ajax = null;
-    if (reqtype == 'post') {
-        ajax = jQuery.post;
-    } else {
-        ajax = jQuery.get;
+jQuery.fn.loadxhtml = function(url, form, reqtype, mode, cursor) {
+    if (this.size() > 1) {
+        cw.log('loadxhtml was called with more than one element');
     }
-    if (this.size() > 1) {
-        log('loadxhtml was called with more than one element');
+    var callback = null;
+    if (form && form.callback) {
+        cw.log('[3.9] callback given through form.callback is deprecated, add ' + 'callback on the defered');
+        callback = form.callback;
+        delete form.callback;
     }
     var node = this.get(0); // only consider the first element
-    mode = mode || 'replace';
-    var callback = null;
-    if (data && data.callback) {
-        callback = data.callback;
-        delete data.callback;
+    if (cursor) {
+        setProgressCursor();
     }
-    ajax(url, data, function(response) {
+    var d = loadRemote(url, form, reqtype);
+    d.addCallback(function(response) {
         var domnode = getDomFromResponse(response);
-        domnode = preprocessAjaxLoad(node, domnode);
+        domnode = loadAjaxHtmlHead(domnode);
+        mode = mode || 'replace';
+        // make sure the component is visible
+        $(node).removeClass("hidden");
         if (mode == 'swap') {
             var origId = node.id;
-            node = swapDOM(node, domnode);
+            node = cw.swapDOM(node, domnode);
             if (!node.id) {
                 node.id = origId;
             }
@@ -138,19 +265,97 @@
         } else if (mode == 'append') {
             jQuery(node).append(domnode);
         }
-        postAjaxLoad(node);
+        _postAjaxLoad(node);
         while (jQuery.isFunction(callback)) {
             callback = callback.apply(this, [domnode]);
         }
     });
-};
+    d.addErrback(remoteCallFailed);
+    if (cursor) {
+        d.addCallback(resetCursor);
+        d.addErrback(resetCursor);
+    }
+    return d;
+}
 
+/**
+ * .. function:: loadRemote(url, form, reqtype='GET', sync=false)
+ *
+ * Asynchronously (unless `async` argument is set to false) load an url or path
+ * and return a deferred whose callbacks args are decoded according to the
+ * Content-Type response header. `form` should be additional form params
+ * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
+ */
+function loadRemote(url, form, reqtype, sync) {
+    if (!url.startswith(baseuri())) {
+        url = baseuri() + url;
+    }
+    if (!sync) {
+        var deferred = new Deferred();
+        jQuery.ajax({
+            url: url,
+            type: (reqtype || 'GET').toUpperCase(),
+            data: form,
+            async: true,
+
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+
+            success: function(data, status) {
+                if (deferred._req.getResponseHeader("content-type") == 'application/json') {
+                    data = cw.evalJSON(data);
+                }
+                deferred.success(data);
+            },
 
+            error: function(xhr, status, error) {
+                try {
+                    if (xhr.status == 500) {
+                        var reason_dict = cw.evalJSON(xhr.responseText);
+                        deferred.error(xhr, status, reason_dict['reason']);
+                        return;
+                    }
+                } catch(exc) {
+                    cw.log('error with server side error report:' + exc);
+                }
+                deferred.error(xhr, status, null);
+            }
+        });
+        return deferred;
+    } else {
+        var result = jQuery.ajax({
+            url: url,
+            type: (reqtype || 'GET').toUpperCase(),
+            data: form,
+            async: false
+        });
+        if (result) {
+            // XXX no good reason to force json here, 
+            // it should depends on request content-type
+            result = cw.evalJSON(result.responseText);
+        }
+        return result
+    }
+}
 
-/* finds each dynamic fragment in the page and executes the
+//============= higher level AJAX functions using remote calls ===============//
+/**
+ * .. function:: _(message)
+ *
+ * emulation of gettext's _ shortcut
+ */
+function _(message) {
+    return loadRemote('json', ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
+}
+
+/**
+ * .. function:: _loadDynamicFragments(node)
+ *
+ * finds each dynamic fragment in the page and executes the
  * the associated RQL to build them (Async call)
  */
-function loadDynamicFragments(node) {
+function _loadDynamicFragments(node) {
     if (node) {
         var fragments = jQuery(node).find('div.dynamicFragment');
     } else {
@@ -162,247 +367,136 @@
     if (typeof LOADING_MSG == 'undefined') {
         LOADING_MSG = 'loading'; // this is only a safety belt, it should not happen
     }
-    for(var i=0; i<fragments.length; i++) {
+    for (var i = 0; i < fragments.length; i++) {
         var fragment = fragments[i];
         fragment.innerHTML = '<h3>' + LOADING_MSG + ' ... <img src="data/loading.gif" /></h3>';
+        var $fragment = jQuery(fragment);
         // if cubicweb:loadurl is set, just pick the url et send it to loadxhtml
-        var url = getNodeAttribute(fragment, 'cubicweb:loadurl');
+        var url = $fragment.attr('cubicweb:loadurl');
         if (url) {
-            jQuery(fragment).loadxhtml(url);
+            $fragment.loadxhtml(url);
             continue;
         }
         // else: rebuild full url by fetching cubicweb:rql, cubicweb:vid, etc.
-        var rql = getNodeAttribute(fragment, 'cubicweb:rql');
-        var items = getNodeAttribute(fragment, 'cubicweb:vid').split('&');
+        var rql = $fragment.attr('cubicweb:rql');
+        var items = $fragment.attr('cubicweb:vid').split('&');
         var vid = items[0];
         var extraparams = {};
         // case where vid='myvid&param1=val1&param2=val2': this is a deprecated abuse-case
         if (items.length > 1) {
-            console.log("[3.5] you're using extraargs in cubicweb:vid attribute, this is deprecated, consider using loadurl instead");
-            for (var j=1; j<items.length; j++) {
+            cw.log("[3.5] you're using extraargs in cubicweb:vid " +
+                   "attribute, this is deprecated, consider using " +
+                   "loadurl instead");
+            for (var j = 1; j < items.length; j++) {
                 var keyvalue = items[j].split('=');
                 extraparams[keyvalue[0]] = keyvalue[1];
             }
         }
-        var actrql = getNodeAttribute(fragment, 'cubicweb:actualrql');
-        if (actrql) { extraparams['actualrql'] = actrql; }
-        var fbvid = getNodeAttribute(fragment, 'cubicweb:fallbackvid');
-        if (fbvid) { extraparams['fallbackvid'] = fbvid; }
-        replacePageChunk(fragment.id, rql, vid, extraparams);
-    }
-}
-
-jQuery(document).ready(function() {loadDynamicFragments();});
-
-//============= base AJAX functions to make remote calls =====================//
-
-function remoteCallFailed(err, req) {
-    if (req.status == 500) {
-        updateMessage(err);
-    } else {
-        log(err);
-        updateMessage(_("an error occured while processing your request"));
+        var actrql = $fragment.attr('cubicweb:actualrql');
+        if (actrql) {
+            extraparams['actualrql'] = actrql;
+        }
+        var fbvid = $fragment.attr('cubicweb:fallbackvid');
+        if (fbvid) {
+            extraparams['fallbackvid'] = fbvid;
+        }
+        extraparams['rql'] = rql;
+        extraparams['vid'] = vid;
+        $fragment.loadxhtml('json', ajaxFuncArgs('view', extraparams));
     }
 }
 
-
-/*
- * This function will call **synchronously** a remote method on the cubicweb server
- * @param fname: the function name to call (as exposed by the JSONController)
- *
- * additional arguments will be directly passed to the specified function
- *
- * It looks at http headers to guess the response type.
- */
-function remoteExec(fname /* ... */) {
-    setProgressCursor();
-    var props = {'fname' : fname, 'pageid' : pageid,
-                      'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-    var result  = jQuery.ajax({url: JSON_BASE_URL, data: props, async: false}).responseText;
-    if (result) {
-        result = evalJSON(result);
-    }
-    resetCursor();
-    return result;
-}
-
-/*
- * This function will call **asynchronously** a remote method on the json
- * controller of the cubicweb http server
- *
- * @param fname: the function name to call (as exposed by the JSONController)
- *
- * additional arguments will be directly passed to the specified function
- *
- * It looks at http headers to guess the response type.
- */
-
-function asyncRemoteExec(fname /* ... */) {
-    setProgressCursor();
-    var props = {'fname' : fname, 'pageid' : pageid,
-                 'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-    // XXX we should inline the content of loadRemote here
-    var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
-    deferred = deferred.addErrback(remoteCallFailed);
-    deferred = deferred.addErrback(resetCursor);
-    deferred = deferred.addCallback(resetCursor);
-    return deferred;
-}
-
-
-/* emulation of gettext's _ shortcut
- */
-function _(message) {
-    return remoteExec('i18n', [message])[0];
-}
-
-function userCallback(cbname) {
-    asyncRemoteExec('user_callback', cbname);
-}
+jQuery(document).ready(function() {
+    _loadDynamicFragments();
+});
 
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    remoteExec('unload_page_data');
+    loadRemote('json', ajaxFuncArgs('unload_page_data'), 'GET', true);
+}
+
+function removeBookmark(beid) {
+    var d = loadRemote('json', ajaxFuncArgs('delete_bookmark', null, beid));
+    d.addCallback(function(boxcontent) {
+        $('#bookmarks_box').loadxhtml('json',
+                                      ajaxFuncArgs('render', null, 'boxes',
+                                                   'bookmarks_box'));
+        document.location.hash = '#header';
+        updateMessage(_("bookmark has been removed"));
+    });
+}
+
+function userCallback(cbname) {
+    setProgressCursor();
+    var d = loadRemote('json', ajaxFuncArgs('user_callback', null, cbname));
+    d.addCallback(resetCursor);
+    d.addErrback(resetCursor);
+    d.addErrback(remoteCallFailed);
+    return d;
 }
 
+function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
+    var d = userCallback(cbname);
+    d.addCallback(function() {
+        $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {'rql': rql},
+						       registry, compid));
+        if (msg) {
+            updateMessage(msg);
+        }
+    });
+}
+
+function userCallbackThenReloadPage(cbname, msg) {
+    var d = userCallback(cbname);
+    d.addCallback(function() {
+        window.location.reload();
+        if (msg) {
+            updateMessage(msg);
+        }
+    });
+}
+
+/**
+ * .. function:: unregisterUserCallback(cbname)
+ *
+ * unregisters the python function registered on the server's side
+ * while the page was generated.
+ */
+function unregisterUserCallback(cbname) {
+    setProgressCursor();
+    var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+                                            null, cbname));
+    d.addCallback(resetCursor);
+    d.addErrback(resetCursor);
+    d.addErrback(remoteCallFailed);
+}
+
+//============= XXX move those functions? ====================================//
 function openHash() {
     if (document.location.hash) {
         var nid = document.location.hash.replace('#', '');
         var node = jQuery('#' + nid);
-        if (node) { removeElementClass(node, "hidden"); }
+        if (node) {
+            $(node).removeClass("hidden");
+        }
     };
 }
 jQuery(document).ready(openHash);
 
-function reloadComponent(compid, rql, registry, nodeid, extraargs) {
-    registry = registry || 'components';
-    rql = rql || '';
-    nodeid = nodeid || (compid + 'Component');
-    extraargs = extraargs || {};
-    var node = getNode(nodeid);
-    var d = asyncRemoteExec('component', compid, rql, registry, extraargs);
-    d.addCallback(function(result, req) {
-        var domnode = getDomFromResponse(result);
-        if (node) {
-            // make sure the component is visible
-            removeElementClass(node, "hidden");
-            domnode = preprocessAjaxLoad(node, domnode);
-            swapDOM(node, domnode);
-            postAjaxLoad(domnode);
-        }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-    });
-  return d;
-}
-
-/* XXX: HTML architecture of cubicweb boxes is a bit strange */
-function reloadBox(boxid, rql) {
-    return reloadComponent(boxid, rql, 'boxes', boxid);
-}
-
-function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
-    var d = asyncRemoteExec('user_callback', cbname);
-    d.addCallback(function() {
-        reloadComponent(compid, rql, registry, nodeid);
-        if (msg) { updateMessage(msg); }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-function userCallbackThenReloadPage(cbname, msg) {
-    var d = asyncRemoteExec('user_callback', cbname);
-    d.addCallback(function() {
-        window.location.reload();
-        if (msg) { updateMessage(msg); }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-/*
- * unregisters the python function registered on the server's side
- * while the page was generated.
- */
-function unregisterUserCallback(cbname) {
-    var d = asyncRemoteExec('unregister_user_callback', cbname);
-    d.addCallback(function() {resetCursor();});
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-
-/* executes an async query to the server and replaces a node's
- * content with the query result
+/**
+ * .. function:: buildWysiwygEditors(parent)
  *
- * @param nodeId the placeholder node's id
- * @param rql the RQL query
- * @param vid the vid to apply to the RQL selection (default if not specified)
- * @param extraparmas table of additional query parameters
- */
-function replacePageChunk(nodeId, rql, vid, extraparams, /* ... */ swap, callback) {
-    var params = null;
-    if (callback) {
-        params = {callback: callback};
-    }
-
-    var node = jQuery('#' + nodeId)[0];
-    var props = {};
-    if (node) {
-        props['rql'] = rql;
-        props['fname'] = 'view';
-        props['pageid'] = pageid;
-        if (vid) { props['vid'] = vid; }
-        if (extraparams) { jQuery.extend(props, extraparams); }
-        // FIXME we need to do asURL(props) manually instead of
-        // passing `props` directly to loadxml because replacePageChunk
-        // is sometimes called (abusively) with some extra parameters in `vid`
-        var mode = swap?'swap':'replace';
-        var url = JSON_BASE_URL + asURL(props);
-        jQuery(node).loadxhtml(url, params, 'get', mode);
-    } else {
-        log('Node', nodeId, 'not found');
-    }
-}
-
-/* XXX deprecates?
- * fetches `url` and replaces `nodeid`'s content with the result
- * @param replacemode how the replacement should be done (default is 'replace')
- *  Possible values are :
- *    - 'replace' to replace the node's content with the generated HTML
- *    - 'swap' to replace the node itself with the generated HTML
- *    - 'append' to append the generated HTML to the node's content
- */
-function loadxhtml(nodeid, url, /* ... */ replacemode) {
-    jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode);
-}
-
-/* XXX: this function should go in edition.js but as for now, htmlReplace
+ *XXX: this function should go in edition.js but as for now, htmlReplace
  * references it.
  *
  * replace all textareas with fckeditors.
  */
 function buildWysiwygEditors(parent) {
-    jQuery('textarea').each(function () {
+    jQuery('textarea').each(function() {
         if (this.getAttribute('cubicweb:type') == 'wysiwyg') {
             // mark editor as instanciated, we may be called a number of times
-            // (see postAjaxLoad)
+            // (see _postAjaxLoad)
             this.setAttribute('cubicweb:type', 'fckeditor');
             if (typeof FCKeditor != "undefined") {
                 var fck = new FCKeditor(this.id);
@@ -411,29 +505,29 @@
                 fck.BasePath = "fckeditor/";
                 fck.ReplaceTextarea();
             } else {
-                log('fckeditor could not be found.');
+                cw.log('fckeditor could not be found.');
             }
         }
     });
 }
-
 jQuery(document).ready(buildWysiwygEditors);
 
-
-/*
+/**
+ * .. function:: stripEmptyTextNodes(nodelist)
+ *
  * takes a list of DOM nodes and removes all empty text nodes
  */
 function stripEmptyTextNodes(nodelist) {
     /* this DROPS empty text nodes */
     var stripped = [];
-    for (var i=0; i < nodelist.length; i++) {
+    for (var i = 0; i < nodelist.length; i++) {
         var node = nodelist[i];
         if (isTextNode(node)) {
-             /* all browsers but FF -> innerText, FF -> textContent  */
-             var text = node.innerText || node.textContent;
-             if (text && !text.strip()) {
-               continue;
-             }
+            /* all browsers but FF -> innerText, FF -> textContent  */
+            var text = node.innerText || node.textContent;
+            if (text && ! text.strip()) {
+                continue;
+            }
         } else {
             stripped.push(node);
         }
@@ -441,7 +535,10 @@
     return stripped;
 }
 
-/* convenience function that returns a DOM node based on req's result.
+/**
+ * .. function:: getDomFromResponse(response)
+ *
+ * convenience function that returns a DOM node based on req's result.
  * XXX clarify the need to clone
  * */
 function getDomFromResponse(response) {
@@ -462,17 +559,116 @@
     }
     // several children => wrap them in a single node and return the wrap
     return DIV({'cubicweb:type': "cwResponseWrapper"},
-               map(function(node) {
-                    return jQuery(node).clone().context;
-            }, children));
+	       $.map(children, function(node) {
+		       return jQuery(node).clone().context;})
+	       );
 }
 
-function postJSON(url, data, callback) {
-    return jQuery.post(url, data, callback, 'json');
-}
+/* DEPRECATED *****************************************************************/
+
+preprocessAjaxLoad = cw.utils.deprecatedFunction(
+    '[3.9] preprocessAjaxLoad() is deprecated, use loadAjaxHtmlHead instead',
+    function(node, newdomnode) {
+        return loadAjaxHtmlHead(newdomnode);
+    }
+);
+
+reloadComponent = cw.utils.deprecatedFunction(
+    '[3.9] reloadComponent() is deprecated, use loadxhtml instead',
+    function(compid, rql, registry, nodeid, extraargs) {
+        registry = registry || 'components';
+        rql = rql || '';
+        nodeid = nodeid || (compid + 'Component');
+        extraargs = extraargs || {};
+        var node = cw.jqNode(nodeid);
+        return node.loadxhtml('json', ajaxFuncArgs('component', null, compid,
+                                                   rql, registry, extraargs));
+    }
+);
+
+reloadBox = cw.utils.deprecatedFunction(
+    '[3.9] reloadBox() is deprecated, use loadxhtml instead',
+    function(boxid, rql) {
+        return reloadComponent(boxid, rql, 'boxes', boxid);
+    }
+);
 
-function getJSON(url, data, callback){
-    return jQuery.get(url, data, callback, 'json');
-}
+replacePageChunk = cw.utils.deprecatedFunction(
+    '[3.9] replacePageChunk() is deprecated, use loadxhtml instead',
+    function(nodeId, rql, vid, extraparams, /* ... */ swap, callback) {
+        var params = null;
+        if (callback) {
+            params = {
+                callback: callback
+            };
+        }
+        var node = jQuery('#' + nodeId)[0];
+        var props = {};
+        if (node) {
+            props['rql'] = rql;
+            props['fname'] = 'view';
+            props['pageid'] = pageid;
+            if (vid) {
+                props['vid'] = vid;
+            }
+            if (extraparams) {
+                jQuery.extend(props, extraparams);
+            }
+            // FIXME we need to do asURL(props) manually instead of
+            // passing `props` directly to loadxml because replacePageChunk
+            // is sometimes called (abusively) with some extra parameters in `vid`
+            var mode = swap ? 'swap': 'replace';
+            var url = JSON_BASE_URL + asURL(props);
+            jQuery(node).loadxhtml(url, params, 'get', mode);
+        } else {
+            cw.log('Node', nodeId, 'not found');
+        }
+    }
+);
+
+loadxhtml = cw.utils.deprecatedFunction(
+    '[3.9] loadxhtml() function is deprecated, use loadxhtml method instead',
+    function(nodeid, url, /* ... */ replacemode) {
+        jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode);
+    }
+);
 
-CubicWeb.provide('ajax.js');
+remoteExec = cw.utils.deprecatedFunction(
+    '[3.9] remoteExec() is deprecated, use loadRemote instead',
+    function(fname /* ... */) {
+        setProgressCursor();
+        var props = {
+            'fname': fname,
+            'pageid': pageid,
+            'arg': $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
+        };
+        var result = jQuery.ajax({
+            url: JSON_BASE_URL,
+            data: props,
+            async: false
+        }).responseText;
+        if (result) {
+            result = cw.evalJSON(result);
+        }
+        resetCursor();
+        return result;
+    }
+);
+
+asyncRemoteExec = cw.utils.deprecatedFunction(
+    '[3.9] asyncRemoteExec() is deprecated, use loadRemote instead',
+    function(fname /* ... */) {
+        setProgressCursor();
+        var props = {
+            'fname': fname,
+            'pageid': pageid,
+            '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');
+        deferred = deferred.addErrback(remoteCallFailed);
+        deferred = deferred.addErrback(resetCursor);
+        deferred = deferred.addCallback(resetCursor);
+        return deferred;
+    }
+);
--- a/web/data/cubicweb.bookmarks.js	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-CubicWeb.require('ajax.js');
-
-function removeBookmark(beid) {
-    d = asyncRemoteExec('delete_bookmark', beid);
-    d.addCallback(function(boxcontent) {
-	    reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
-  	document.location.hash = '#header';
- 	updateMessage(_("bookmark has been removed"));
-    });
-}
--- a/web/data/cubicweb.calendar.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.calendar.css	Mon Jul 19 15:37:02 2010 +0200
@@ -230,7 +230,7 @@
 .calendar th.month {
  font-weight:bold;
  padding-bottom:0.2em;
- background: #cfceb7;
+ background: %(actionBoxTitleBgColor)s;
 }
 
 .calendar th.month a{
--- a/web/data/cubicweb.calendar.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.calendar.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,23 +1,20 @@
-/*
+/**
  *  This file contains Calendar utilities
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
-CubicWeb.require('python.js');
-CubicWeb.require('ajax.js');
-
 // IMPORTANT NOTE: the variables DAYNAMES AND MONTHNAMES will be added
 //                 by cubicweb automatically
-
 // dynamically computed (and cached)
 var _CAL_HEADER = null;
 
 TODAY = new Date();
 
-
-/*
+/**
+ * .. class:: Calendar
+ *
  * Calendar (graphical) widget
  * public methods are :
  *   __init__ :
@@ -31,7 +28,7 @@
  *
  *   toggle():
  *    show (resp. hide) the calendar if it's hidden (resp. displayed)
- * 
+ *
  *   displayNextMonth(): (resp. displayPreviousMonth())
  *    update the calendar to display next (resp. previous) month
  */
@@ -39,177 +36,219 @@
     this.containerId = containerId;
     this.inputId = inputId;
     this.year = year;
-    this.month = month-1; // Javascript's counter starts at 0 for january
+    this.month = month - 1; // Javascript's counter starts at 0 for january
     this.cssclass = cssclass || "popupCalendar";
     this.visible = false;
     this.domtable = null;
 
-    this.cellprops = { 'onclick'     : function() {dateSelected(this, containerId); },
-		       'onmouseover' : function() {this.style.fontWeight = 'bold'; },
-		       'onmouseout'  : function() {this.style.fontWeight = 'normal';}
-		     }
+    this.cellprops = {
+        'onclick': function() {
+            dateSelected(this, containerId);
+        },
+        'onmouseover': function() {
+            this.style.fontWeight = 'bold';
+        },
+        'onmouseout': function() {
+            this.style.fontWeight = 'normal';
+        }
+    };
 
-    this.todayprops = jQuery.extend({}, this.cellprops, {'class' : 'today'});
+    this.todayprops = jQuery.extend({},
+    this.cellprops, {
+        'class': 'today'
+    });
 
     this._rowdisplay = function(row) {
-	return TR(null, map(partial(TD, this.cellprops), row));
-    }
+        var _td = function(elt) {
+            return TD(this.cellprops, elt);
+        };
+        return TR(null, $.map(row, _td));
+    };
 
     this._makecell = function(cellinfo) {
-	return TD(cellinfo[0], cellinfo[1]);
-    }
+        return TD(cellinfo[0], cellinfo[1]);
+    };
 
-    /* utility function (the only use for now is inside the calendar) */
-    this._uppercaseFirst = function(s) { return s.charAt(0).toUpperCase(); }
-    
-    /* accepts the cells data and builds the corresponding TR nodes
-     * @param rows a list of list of couples (daynum, cssprops)
+    /**
+     * .. function:: Calendar._uppercaseFirst(s)
+     *
+     * utility function (the only use for now is inside the calendar)
+     */
+    this._uppercaseFirst = function(s) {
+        return s.charAt(0).toUpperCase();
+    };
+
+    /**
+     * .. function:: Calendar._domForRows(rows)
+     *
+     * accepts the cells data and builds the corresponding TR nodes
+     *
+     * * `rows`, a list of list of couples (daynum, cssprops)
      */
     this._domForRows = function(rows) {
-	var lines = []
-	for (i=0; i<rows.length; i++) {
-	    lines.push(TR(null, map(this._makecell, rows[i])));
-	}
-	return lines;
-    }
+        var lines = [];
+        for (i = 0; i < rows.length; i++) {
+            lines.push(TR(null, $.map(rows[i], this._makecell)));
+        }
+        return lines;
+    };
 
-    /* builds the calendar headers */
+    /**
+     * .. function:: Calendar._headdisplay(row)
+     *
+     * builds the calendar headers
+     */
     this._headdisplay = function(row) {
-	if (_CAL_HEADER) {
-	    return _CAL_HEADER;
-	}
-	daynames = map(this._uppercaseFirst, DAYNAMES);
-	_CAL_HEADER = TR(null, map(partial(TH, null), daynames));
-	return _CAL_HEADER;
-    }
-    
+        if (_CAL_HEADER) {
+            return _CAL_HEADER;
+        }
+        var self = this;
+        var _th = function(day) {
+            return TH(null, self._uppercaseFirst(day));
+        };
+        return TR(null, $.map(DAYNAMES, _th));
+    };
+
     this._getrows = function() {
-	var rows = [];
-	var firstday = new Date(this.year, this.month, 1);
-	var stopdate = firstday.nextMonth();
-	var curdate = firstday.sub(firstday.getRealDay());
-	while (curdate.getTime() < stopdate) {
-	    var row = []
-	    for (var i=0; i<7; i++) {
-		if (curdate.getMonth() == this.month) {
-		    props = curdate.equals(TODAY) ? this.todayprops:this.cellprops;
-		    row.push([props, curdate.getDate()]);
-		} else {
-		    row.push([this.cellprops, ""]);
-		}
-		curdate.iadd(1);
-	    }
-	    rows.push(row);
-	}
-	return rows;
-    }
+        var rows = [];
+        var firstday = new Date(this.year, this.month, 1);
+        var stopdate = firstday.nextMonth();
+        var curdate = firstday.sub(firstday.getRealDay());
+        while (curdate.getTime() < stopdate) {
+            var row = [];
+            for (var i = 0; i < 7; i++) {
+                if (curdate.getMonth() == this.month) {
+                    props = curdate.equals(TODAY) ? this.todayprops: this.cellprops;
+                    row.push([props, curdate.getDate()]);
+                } else {
+                    row.push([this.cellprops, ""]);
+                }
+                curdate.iadd(1);
+            }
+            rows.push(row);
+        }
+        return rows;
+    };
 
     this._makecal = function() {
-	var rows = this._getrows();
-	var monthname = MONTHNAMES[this.month] + " " + this.year;
-	var prevlink = "javascript: togglePreviousMonth('" + this.containerId + "');";
-	var nextlink = "javascript: toggleNextMonth('" + this.containerId + "');";
-	this.domtable = TABLE({'class': this.cssclass},
-			      THEAD(null, TR(null,
-					     TH(null, A({'href' : prevlink}, "<<")),
-					     // IE 6/7 requires colSpan instead of colspan
-					     TH({'colSpan': 5, 'colspan':5, 'style' : "text-align: center;"}, monthname),
-					     TH(null, A({'href' : nextlink}, ">>")))),
-			      TBODY(null,
-				    this._headdisplay(),
-				    this._domForRows(rows))
-			     );
-	return this.domtable;
-    }
+        var rows = this._getrows();
+        var monthname = MONTHNAMES[this.month] + " " + this.year;
+        var prevlink = "javascript: togglePreviousMonth('" + this.containerId + "');";
+        var nextlink = "javascript: toggleNextMonth('" + this.containerId + "');";
+        this.domtable = TABLE({
+            'class': this.cssclass
+        },
+        THEAD(null, TR(null, TH(null, A({
+            'href': prevlink
+        },
+        "<<")),
+        // IE 6/7 requires colSpan instead of colspan
+        TH({
+            'colSpan': 5,
+            'colspan': 5,
+            'style': "text-align: center;"
+        },
+        monthname), TH(null, A({
+            'href': nextlink
+        },
+        ">>")))), TBODY(null, this._headdisplay(), this._domForRows(rows)));
+        return this.domtable;
+    };
 
     this._updateDiv = function() {
-	if (!this.domtable) {
-	    this._makecal();
-	}
-	jqNode(this.containerId).empty().append(this.domtable);
-	// replaceChildNodes($(this.containerId), this.domtable);
-    }
+        if (!this.domtable) {
+            this._makecal();
+        }
+        cw.jqNode(this.containerId).empty().append(this.domtable);
+        // replaceChildNodes($(this.containerId), this.domtable);
+    };
 
     this.displayNextMonth = function() {
-	this.domtable = null;
-	if (this.month == 11) {
-	    this.year++;
-	}
-	this.month = (this.month+1) % 12;
-	this._updateDiv();
-    }
+        this.domtable = null;
+        if (this.month == 11) {
+            this.year++;
+        }
+        this.month = (this.month + 1) % 12;
+        this._updateDiv();
+    };
 
     this.displayPreviousMonth = function() {
-	this.domtable = null;
-	if (this.month == 0) {
-	    this.year--;
-	}
-	this.month = (this.month+11) % 12;
-	this._updateDiv();
-    }
-    
+        this.domtable = null;
+        if (this.month == 0) {
+            this.year--;
+        }
+        this.month = (this.month + 11) % 12;
+        this._updateDiv();
+    };
+
     this.show = function() {
-	if (!this.visible) {
-	    container = jqNode(this.containerId);
-	    if (!this.domtable) {
-		this._makecal();
-	    }
-	    container.empty().append(this.domtable);
-	    toggleVisibility(container);
-	    this.visible = true;
-	}
-    }
+        if (!this.visible) {
+            var container = cw.jqNode(this.containerId);
+            if (!this.domtable) {
+                this._makecal();
+            }
+            container.empty().append(this.domtable);
+            toggleVisibility(container);
+            this.visible = true;
+        }
+    };
 
     this.hide = function(event) {
-	var self;
-	if (event) {
-	    self = event.data.self;
-	} else {
-	    self = this;
-	}
-	if (self.visible) {
-	    toggleVisibility(self.containerId);
-	    self.visible = false;
-	}
-    }
+        var self;
+        if (event) {
+            self = event.data.self;
+        } else {
+            self = this;
+        }
+        if (self.visible) {
+            toggleVisibility(self.containerId);
+            self.visible = false;
+        }
+    };
 
     this.toggle = function() {
-	if (this.visible) {
-	    this.hide();
-	}
-	else {
-	    this.show();
-	}
-    }
+        if (this.visible) {
+            this.hide();
+        }
+        else {
+            this.show();
+        }
+    };
 
     // call hide() when the user explicitly sets the focus on the matching input
-    jqNode(inputId).bind('focus', {'self': this}, this.hide); // connect(inputId, 'onfocus', this, 'hide');
+    cw.jqNode(inputId).bind('focus', {
+        'self': this
+    },
+    this.hide); // connect(inputId, 'onfocus', this, 'hide');
 };
 
 // keep track of each calendar created
 Calendar.REGISTRY = {};
 
-/*
+/**
+ * .. function:: toggleCalendar(containerId, inputId, year, month)
+ *
  * popup / hide calendar associated to `containerId`
- */	    
+ */
 function toggleCalendar(containerId, inputId, year, month) {
     var cal = Calendar.REGISTRY[containerId];
     if (!cal) {
-	cal = new Calendar(containerId, inputId, year, month);
-	Calendar.REGISTRY[containerId] = cal;
+        cal = new Calendar(containerId, inputId, year, month);
+        Calendar.REGISTRY[containerId] = cal;
     }
     /* hide other calendars */
     for (containerId in Calendar.REGISTRY) {
-	var othercal = Calendar.REGISTRY[containerId];
-	if (othercal !== cal) {
-	    othercal.hide();
-	}
+        var othercal = Calendar.REGISTRY[containerId];
+        if (othercal !== cal) {
+            othercal.hide();
+        }
     }
     cal.toggle();
 }
 
-
-/*
+/**
+ * .. function:: toggleNextMonth(containerId)
+ *
  * ask for next month to calendar displayed in `containerId`
  */
 function toggleNextMonth(containerId) {
@@ -217,7 +256,9 @@
     cal.displayNextMonth();
 }
 
-/*
+/**
+ * .. function:: togglePreviousMonth(containerId)
+ *
  * ask for previous month to calendar displayed in `containerId`
  */
 function togglePreviousMonth(containerId) {
@@ -225,97 +266,90 @@
     cal.displayPreviousMonth();
 }
 
-
-/*
+/**
+ * .. function:: dateSelected(cell, containerId)
+ *
  * Callback called when the user clicked on a cell in the popup calendar
  */
 function dateSelected(cell, containerId) {
     var cal = Calendar.REGISTRY[containerId];
-    var input = getNode(cal.inputId);
+    var input = cw.getNode(cal.inputId);
     // XXX: the use of innerHTML might cause problems, but it seems to be
     //      the only way understood by both IE and Mozilla. Otherwise,
     //      IE accepts innerText and mozilla accepts textContent
     var selectedDate = new Date(cal.year, cal.month, cell.innerHTML, 12);
-    var xxx = remoteExec("format_date", toISOTimestamp(selectedDate));
-    input.value = xxx;
+    input.value = remoteExec("format_date", cw.utils.toISOTimestamp(selectedDate));
     cal.hide();
 }
 
-function whichElement(e)
-{
-var targ;
-if (!e)
-  {
-  var e=window.event;
-  }
-if (e.target)
-  {
-  targ=e.target;
-  }
-else if (e.srcElement)
-  {
-  targ=e.srcElement;
-  }
-if (targ.nodeType==3) // defeat Safari bug
-  {
-  targ = targ.parentNode;
-  }
-  return targ;
+function whichElement(e) {
+    var targ;
+    if (!e) {
+        e = window.event;
+    }
+    if (e.target) {
+        targ = e.target;
+    }
+    else if (e.srcElement) {
+        targ = e.srcElement;
+    }
+    if (targ.nodeType == 3) // defeat Safari bug
+    {
+        targ = targ.parentNode;
+    }
+    return targ;
 }
 
 function getPosition(element) {
-  var left;
-  var top;
-  var offset;
-  // TODO: deal scrollbar positions also!
-  left = element.offsetLeft;
-  top = element.offsetTop;
+    var left;
+    var top;
+    var offset;
+    // TODO: deal scrollbar positions also!
+    left = element.offsetLeft;
+    top = element.offsetTop;
 
-  if (element.offsetParent != null)
-    {
-      offset = getPosition(element.offsetParent);
-      left = left + offset[0];
-      top = top + offset[1];
-      
+    if (element.offsetParent != null) {
+        offset = getPosition(element.offsetParent);
+        left = left + offset[0];
+        top = top + offset[1];
+
     }
-  return [left, top];
+    return [left, top];
 }
 
 function getMouseInBlock(event) {
-  var elt = event.target;
-  var x = event.clientX;
-  var y = event.clientY;
-  var w = elt.clientWidth;
-  var h = elt.clientHeight;
-  var offset = getPosition(elt);
+    var elt = event.target;
+    var x = event.clientX;
+    var y = event.clientY;
+    var w = elt.clientWidth;
+    var h = elt.clientHeight;
+    var offset = getPosition(elt);
 
-  x = 1.0*(x-offset[0])/w;
-  y = 1.0*(y-offset[1])/h;
-  return [x, y];
+    x = 1.0 * (x - offset[0]) / w;
+    y = 1.0 * (y - offset[1]) / h;
+    return [x, y];
 }
 function getHourFromMouse(event, hmin, hmax) {
-  var pos = getMouseInBlock(event);
-  var y = pos[1];
-  return Math.floor((hmax-hmin)*y + hmin);
+    var pos = getMouseInBlock(event);
+    var y = pos[1];
+    return Math.floor((hmax - hmin) * y + hmin);
 }
 
 function addCalendarItem(event, hmin, hmax, year, month, day, duration, baseurl) {
-  var hour = getHourFromMouse(event, hmin, hmax);
+    var hour = getHourFromMouse(event, hmin, hmax);
+
+    if (0 <= hour && hour < 24) {
+        baseurl += "&start=" + year + "%2F" + month + "%2F" + day + "%20" + hour + ":00";
+        baseurl += "&stop=" + year + "%2F" + month + "%2F" + day + "%20" + (hour + duration) + ":00";
 
-  if (0<=hour && hour < 24) {
-    baseurl += "&start="+year+"%2F"+month+"%2F"+day+"%20"+hour+":00";
-    baseurl += "&stop="+year+"%2F"+month+"%2F"+day+"%20"+(hour+duration)+":00";
-    
-    stopPropagation(event);
-    window.location.assign(baseurl);
-    return false;
-  }
-  return true;
+        stopPropagation(event);
+        window.location.assign(baseurl);
+        return false;
+    }
+    return true;
 }
 
 function stopPropagation(event) {
-  event.cancelBubble = true;
-  if (event.stopPropagation) event.stopPropagation();  
+    event.cancelBubble = true;
+    if (event.stopPropagation) event.stopPropagation();
 }
-     
-CubicWeb.provide('calendar.js');
--- a/web/data/cubicweb.compat.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.compat.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,546 +1,103 @@
-/* MochiKit -> jQuery compatibility module */
-
-function forEach(array, func) {
-    for (var i=0, length=array.length; i<length; i++) {
-	func(array[i]);
-    }
-}
-
-// XXX looks completely unused (candidate for removal)
-function getElementsByTagAndClassName(tag, klass, root) {
-    root = root || document;
-    // FIXME root is not used in this compat implementation
-    return jQuery(tag + '.' + klass);
-}
-
-/* jQUery flattens arrays returned by the mapping function:
-   >>> y = ['a:b:c', 'd:e']
-   >>> jQuery.map(y, function(y) { return y.split(':');})
-   ["a", "b", "c", "d", "e"]
-   // where one would expect:
-   [ ["a", "b", "c"], ["d", "e"] ]
-   XXX why not the same argument order as $.map and forEach ?
-*/
-function map(func, array) {
-    var result = [];
-    for (var i=0, length=array.length;
-         i<length;
-         i++) {
-	result.push(func(array[i]));
-    }
-    return result;
-}
-
-function findValue(array, element) {
-    return jQuery.inArray(element, array);
-}
-
-function filter(func, array) {
-    return jQuery.grep(array, func);
-}
-
-function noop() {}
-
-function addElementClass(node, klass) {
-    jQuery(node).addClass(klass);
-}
-
-// XXX looks completely unused (candidate for removal)
-function toggleElementClass(node, klass) {
-    jQuery(node).toggleClass(klass);
-}
-
-function removeElementClass(node, klass) {
-    jQuery(node).removeClass(klass);
-}
-
-hasElementClass = jQuery.className.has;
-
-
-function partial(func) {
-    var args = sliceList(arguments, 1);
-    return function() {
-	return func.apply(null, merge(args, arguments));
-    };
-}
-
-
-function log() {
-    // XXX dummy implementation
-    // console.log.apply(arguments); ???
-    var args = [];
-    for (var i=0; i<arguments.length; i++) {
-	args.push(arguments[i]);
-    }
-    if (typeof(window) != "undefined" && window.console
-        && window.console.log) {
-	window.console.log(args.join(' '));
-    }
-}
-
-function getNodeAttribute(node, attribute) {
-    return jQuery(node).attr(attribute);
-}
-
-function isArray(it){ // taken from dojo
-    return it && (it instanceof Array || typeof it == "array");
-}
-
-function isString(it){ // taken from dojo
-    return !!arguments.length && it != null && (typeof it == "string" || it instanceof String);
-}
-
-
-function isArrayLike(it) { // taken from dojo
-    return (it && it !== undefined &&
-	    // keep out built-in constructors (Number, String, ...) which have length
-	    // properties
-	    !isString(it) && !jQuery.isFunction(it) &&
-	    !(it.tagName && it.tagName.toLowerCase() == 'form') &&
-	    (isArray(it) || isFinite(it.length)));
-}
+cw.utils.movedToNamespace(['log', 'jqNode', 'getNode', 'evalJSON', 'urlEncode',
+                           'swapDOM'], cw);
+cw.utils.movedToNamespace(['nodeWalkDepthFirst', 'formContents', 'isArray',
+                           'isString', 'isArrayLike', 'sliceList',
+                           'toISOTimestamp'], cw.utils);
 
 
-function getNode(node) {
-    if (typeof(node) == 'string') {
-        return document.getElementById(node);
-    }
-    return node;
-}
-
-/* safe version of jQuery('#nodeid') because we use ':' in nodeids
- * which messes with jQuery selection mechanism
- */
-function jqNode(node) {
-    node = getNode(node);
-    if (node) {
-	return jQuery(node);
-    }
-    return null;
-}
-
-function evalJSON(json) { // trust source
-    return eval("(" + json + ")");
-}
-
-function urlEncode(str) {
-    if (typeof(encodeURIComponent) != "undefined") {
-        return encodeURIComponent(str).replace(/\'/g, '%27');
-    } else {
-        return escape(str).replace(/\+/g, '%2B').replace(/\"/g,'%22').rval.replace(/\'/g, '%27');
-    }
-}
-
-function swapDOM(dest, src) {
-    dest = getNode(dest);
-    var parent = dest.parentNode;
-    if (src) {
-        src = getNode(src);
-        parent.replaceChild(src, dest);
-    } else {
-        parent.removeChild(dest);
-    }
-    return src;
-}
-
-function replaceChildNodes(node/*, nodes...*/) {
-    var elem = getNode(node);
-    arguments[0] = elem;
-    var child;
-    while ((child = elem.firstChild)) {
-        elem.removeChild(child);
-    }
-    if (arguments.length < 2) {
-        return elem;
-    } else {
-	for (var i=1; i<arguments.length; i++) {
-	    elem.appendChild(arguments[i]);
-	}
-	return elem;
-    }
-}
-
-update = jQuery.extend;
-
-
-function createDomFunction(tag) {
-
-    function builddom(params, children) {
-	var node = document.createElement(tag);
-	for (key in params) {
-	    var value = params[key];
-	    if (key.substring(0, 2) == 'on') {
-		// this is an event handler definition
-		if (typeof value == 'string') {
-		    // litteral definition
-		    value = new Function(value);
-		}
-		node[key] = value;
-	    } else { // normal node attribute
-		jQuery(node).attr(key, params[key]);
-	    }
-	}
-	if (children) {
-	    if (!isArrayLike(children)) {
-		children = [children];
-		for (var i=2; i<arguments.length; i++) {
-		    var arg = arguments[i];
-		    if (isArray(arg)) {
-			children = merge(children, arg);
-		    } else {
-			children.push(arg);
-		    }
-		}
-	    }
-	    for (var i=0; i<children.length; i++) {
-		var child = children[i];
-		if (typeof child == "string" || typeof child == "number") {
-		    child = document.createTextNode(child);
-		}
-		node.appendChild(child);
-	    }
-	}
-	return node;
-    }
-    return builddom;
+if ($.noop === undefined) {
+    function noop() {}
+} else {
+    noop = cw.utils.deprecatedFunction(
+        '[3.9] noop() is deprecated, use $.noop() instead (XXX requires jQuery 1.4)',
+        $.noop);
 }
 
-A = createDomFunction('a');
-BUTTON = createDomFunction('button');
-BR = createDomFunction('br');
-CANVAS = createDomFunction('canvas');
-DD = createDomFunction('dd');
-DIV = createDomFunction('div');
-DL = createDomFunction('dl');
-DT = createDomFunction('dt');
-FIELDSET = createDomFunction('fieldset');
-FORM = createDomFunction('form');
-H1 = createDomFunction('H1');
-H2 = createDomFunction('H2');
-H3 = createDomFunction('H3');
-H4 = createDomFunction('H4');
-H5 = createDomFunction('H5');
-H6 = createDomFunction('H6');
-HR = createDomFunction('hr');
-IMG = createDomFunction('img');
-INPUT = createDomFunction('input');
-LABEL = createDomFunction('label');
-LEGEND = createDomFunction('legend');
-LI = createDomFunction('li');
-OL = createDomFunction('ol');
-OPTGROUP = createDomFunction('optgroup');
-OPTION = createDomFunction('option');
-P = createDomFunction('p');
-PRE = createDomFunction('pre');
-SELECT = createDomFunction('select');
-SPAN = createDomFunction('span');
-STRONG = createDomFunction('strong');
-TABLE = createDomFunction('table');
-TBODY = createDomFunction('tbody');
-TD = createDomFunction('td');
-TEXTAREA = createDomFunction('textarea');
-TFOOT = createDomFunction('tfoot');
-TH = createDomFunction('th');
-THEAD = createDomFunction('thead');
-TR = createDomFunction('tr');
-TT = createDomFunction('tt');
-UL = createDomFunction('ul');
+// ========== ARRAY EXTENSIONS ========== ///
+Array.prototype.contains = cw.utils.deprecatedFunction(
+    '[3.9] array.contains(elt) is deprecated, use $.inArray(elt, array) instead',
+    function(element) {
+        return jQuery.inArray(element, this) != - 1;
+    }
+);
 
-// cubicweb specific
-//IFRAME = createDomFunction('iframe');
-function IFRAME(params){
-  if ('name' in params){
-    try {
-      var node = document.createElement('<iframe name="'+params['name']+'">');
-    } catch (ex) {
-      var node = document.createElement('iframe');
-      node.id = node.name = params.name;
+// ========== END OF ARRAY EXTENSIONS ========== ///
+forEach = cw.utils.deprecatedFunction(
+    '[3.9] forEach() is deprecated, use $.each() instead',
+    function(array, func) {
+        return $.each(array, func);
     }
-  }
-  else{
-    var node = document.createElement('iframe');
-  }
-  for (key in params) {
-    if (key != 'name'){
-      var value = params[key];
-      if (key.substring(0, 2) == 'on') {
-	// this is an event handler definition
-	if (typeof value == 'string') {
-	  // litteral definition
-	  value = new Function(value);
-	}
-	node[key] = value;
-      } else { // normal node attribute
-	node.setAttribute(key, params[key]);
-      }
-    }
-  }
-  return node;
-}
-
+);
 
-// dummy ultra minimalist implementation on deferred for jQuery
-function Deferred() {
-    this.__init__(this);
-}
-
-jQuery.extend(Deferred.prototype, {
-    __init__: function() {
-	this._onSuccess = [];
-	this._onFailure = [];
-	this._req = null;
-        this._result = null;
-        this._error = null;
-    },
-
-    addCallback: function(callback) {
-        if (this._req.readyState == 4) {
-            if (this._result) { callback.apply(null, this._result, this._req); }
-        }
-        else { this._onSuccess.push([callback, sliceList(arguments, 1)]); }
-	return this;
-    },
-
-    addErrback: function(callback) {
-        if (this._req.readyState == 4) {
-            if (this._error) { callback.apply(null, this._error, this._req); }
+/**
+ * .. function:: cw.utils.deprecatedFunction(msg, function)
+ *
+ * jQUery flattens arrays returned by the mapping function:
+ * >>> y = ['a:b:c', 'd:e']
+ * >>> jQuery.map(y, function(y) { return y.split(':');})
+ * ["a", "b", "c", "d", "e"]
+ *  // where one would expect:
+ *  [ ["a", "b", "c"], ["d", "e"] ]
+ *  XXX why not the same argument order as $.map and forEach ?
+ */
+map = cw.utils.deprecatedFunction(
+    '[3.9] map() is deprecated, use $.map instead',
+    function(func, array) {
+        var result = [];
+        for (var i = 0, length = array.length; i < length; i++) {
+            result.push(func(array[i]));
         }
-        else { this._onFailure.push([callback, sliceList(arguments, 1)]); }
-	return this;
-    },
-
-    success: function(result) {
-        this._result = result;
-	try {
-	    for (var i=0; i<this._onSuccess.length; i++) {
-		var callback = this._onSuccess[i][0];
-		var args = merge([result, this._req], this._onSuccess[i][1]);
-		callback.apply(null, args);
-	    }
-	} catch (error) {
-	    this.error(this.xhr, null, error);
-	}
-    },
-
-    error: function(xhr, status, error) {
-        this._error = error;
-	for (var i=0; i<this._onFailure.length; i++) {
-	    var callback = this._onFailure[i][0];
-	    var args = merge([error, this._req], this._onFailure[i][1]);
-	    callback.apply(null, args);
-	}
+        return result;
     }
-
-});
-
-
-/*
- * Asynchronously load an url and return a deferred
- * whose callbacks args are decoded according to
- * the Content-Type response header
- */
-function loadRemote(url, data, reqtype) {
-    var d = new Deferred();
-    jQuery.ajax({
-	url: url,
-	type: reqtype,
-	data: data,
-
-	beforeSend: function(xhr) {
-	    d._req = xhr;
-	},
-
-	success: function(data, status) {
-            if (d._req.getResponseHeader("content-type") == 'application/json') {
-              data = evalJSON(data);
-            }
-	    d.success(data);
-	},
+);
 
-	error: function(xhr, status, error) {
-          try {
-            if (xhr.status == 500) {
-                var reason_dict = evalJSON(xhr.responseText);
-                d.error(xhr, status, reason_dict['reason']);
-                return;
-            }
-          } catch(exc) {
-            log('error with server side error report:' + exc);
-          }
-          d.error(xhr, status, null);
-	}
-    });
-    return d;
-}
-
-
-/** @id MochiKit.DateTime.toISOTime */
-toISOTime = function (date, realISO/* = false */) {
-    if (typeof(date) == "undefined" || date === null) {
-        return null;
-    }
-    var hh = date.getHours();
-    var mm = date.getMinutes();
-    var ss = date.getSeconds();
-    var lst = [
-        ((realISO && (hh < 10)) ? "0" + hh : hh),
-        ((mm < 10) ? "0" + mm : mm),
-        ((ss < 10) ? "0" + ss : ss)
-    ];
-    return lst.join(":");
-};
-
-_padTwo = function (n) {
-    return (n > 9) ? n : "0" + n;
-};
-
-/** @id MochiKit.DateTime.toISODate */
-toISODate = function (date) {
-    if (typeof(date) == "undefined" || date === null) {
-        return null;
+findValue = cw.utils.deprecatedFunction(
+    '[3.9] findValue(array, elt) is deprecated, use $.inArray(elt, array) instead',
+    function(array, element) {
+        return jQuery.inArray(element, array);
     }
-    return [
-        date.getFullYear(),
-        _padTwo(date.getMonth() + 1),
-        _padTwo(date.getDate())
-    ].join("-");
-};
-
-
-/** @id MochiKit.DateTime.toISOTimeStamp */
-toISOTimestamp = function (date, realISO/* = false*/) {
-    if (typeof(date) == "undefined" || date === null) {
-        return null;
-    }
-    var sep = realISO ? "T" : " ";
-    var foot = realISO ? "Z" : "";
-    if (realISO) {
-        date = new Date(date.getTime() + (date.getTimezoneOffset() * 60000));
-    }
-    return toISODate(date) + sep + toISOTime(date, realISO) + foot;
-};
-
-
+);
 
-/* depth-first implementation of the nodeWalk function found
- * in MochiKit.Base
- * cf. http://mochikit.com/doc/html/MochiKit/Base.html#fn-nodewalk
- */
-function nodeWalkDepthFirst(node, visitor) {
-    var children = visitor(node);
-    if (children) {
-	for(var i=0; i<children.length; i++) {
-	    nodeWalkDepthFirst(children[i], visitor);
-	}
+filter = cw.utils.deprecatedFunction(
+    '[3.9] filter(func, array) is deprecated, use $.grep(array, f) instead',
+    function(func, array) {
+        return $.grep(array, func);
     }
-}
-
+);
 
-/* Returns true if all the given Array-like or string arguments are not empty (obj.length > 0) */
-function isNotEmpty(obj) {
-    for (var i = 0; i < arguments.length; i++) {
-        var o = arguments[i];
-        if (!(o && o.length)) {
-            return false;
-        }
+addElementClass = cw.utils.deprecatedFunction(
+    '[3.9] addElementClass(node, cls) is depcreated, use $(node).addClass(cls) instead',
+    function(node, klass) {
+        $(node).addClass(klass);
     }
-    return true;
-}
+);
 
-/** this implementation comes from MochiKit  */
-function formContents(elem/* = document.body */) {
-    var names = [];
-    var values = [];
-    if (typeof(elem) == "undefined" || elem === null) {
-        elem = document.body;
-    } else {
-        elem = getNode(elem);
+removeElementClass = cw.utils.deprecatedFunction(
+    '[3.9] removeElementClass(node, cls) is depcreated, use $(node).removeClass(cls) instead',
+    function(node, klass) {
+        $(node).removeClass(klass);
+    }
+);
+
+hasElementClass = cw.utils.deprecatedFunction(
+    '[3.9] hasElementClass(node, cls) is depcreated, use $.className.has(node, cls)',
+    function(node, klass) {
+        return $.className.has(node, klass);
     }
-    nodeWalkDepthFirst(elem, function (elem) {
-        var name = elem.name;
-        if (isNotEmpty(name)) {
-            var tagName = elem.tagName.toUpperCase();
-            if (tagName === "INPUT"
-                && (elem.type == "radio" || elem.type == "checkbox")
-                && !elem.checked
-               ) {
-                return null;
-            }
-            if (tagName === "SELECT") {
-                if (elem.type == "select-one") {
-                    if (elem.selectedIndex >= 0) {
-                        var opt = elem.options[elem.selectedIndex];
-                        var v = opt.value;
-                        if (!v) {
-                            var h = opt.outerHTML;
-                            // internet explorer sure does suck.
-                            if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                v = opt.text;
-                            }
-                        }
-                        names.push(name);
-                        values.push(v);
-                        return null;
-                    }
-                    // no form elements?
-                    names.push(name);
-                    values.push("");
-                    return null;
-                } else {
-                    var opts = elem.options;
-                    if (!opts.length) {
-                        names.push(name);
-                        values.push("");
-                        return null;
-                    }
-                    for (var i = 0; i < opts.length; i++) {
-                        var opt = opts[i];
-                        if (!opt.selected) {
-                            continue;
-                        }
-                        var v = opt.value;
-                        if (!v) {
-                            var h = opt.outerHTML;
-                            // internet explorer sure does suck.
-                            if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                v = opt.text;
-                            }
-                        }
-                        names.push(name);
-                        values.push(v);
-                    }
-                    return null;
-                }
-            }
-            if (tagName === "FORM" || tagName === "P" || tagName === "SPAN"
-                || tagName === "DIV"
-               ) {
-                return elem.childNodes;
-            }
-            names.push(name);
-            values.push(elem.value || '');
-            return null;
-        }
-        return elem.childNodes;
-    });
-    return [names, values];
-}
+);
 
-function merge(array1, array2) {
-    var result = [];
-    for (var i=0,length=arguments.length; i<length; i++) {
-	var array = arguments[i];
-	for (var j=0,alength=array.length; j<alength; j++) {
-	    result.push(array[j]);
-	}
+getNodeAttribute = cw.utils.deprecatedFunction(
+    '[3.9] getNodeAttribute(node, attr) is deprecated, use $(node).attr(attr)',
+    function(node, attribute) {
+        return $(node).attr(attribute);
     }
-    return result;
-}
+);
 
+/**
+ * The only known usage of KEYS is in the tag cube. Once cubicweb-tag 1.7.0 is out,
+ * this current definition can be removed.
+ */
 var KEYS = {
     KEY_ESC: 27,
     KEY_ENTER: 13
 };
-
-
-
--- a/web/data/cubicweb.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.css	Mon Jul 19 15:37:02 2010 +0200
@@ -3,82 +3,67 @@
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
+
 /***************************************/
-/* xhtml tags styles                   */
+/* xhtml tags                          */
 /***************************************/
 
-*{
-  margin:0px;
-  padding :0px;
-}
-
-html, body {
-  background: #e2e2e2;
-}
-
+/* scale and rhythm cf http://lamb.cc/typograph/ */
 body {
-  font-size: 69%;
-  font-weight: normal;
-  font-family: Verdana, sans-serif;
+  font-family:  %(defaultFontFamily)s;
+  font-size: %(defaultSize)s;
+  line-height: %(defaultLineHeight)s;
+  color: %(defaultColor)s;
 }
+h1, h2, h3 { margin-top:0; margin-bottom:0; }
+
+/* got rhythm ? beat of 12*1.25 = 15 px */
+.rhythm_bg { background: url("%(baseRhythmBg)s") repeat ! important; }
+
+/* scale 3:5 stranded */
+/* h1 { font-size:2em; } */
+/* h2 { font-size:1.61538em; } */
+/* h3 { font-size:1.23077em; } */
 
+/* scale le corbusier */
+/* h1 { font-size:2.11538em; } */
+/* h2 { font-size:1.61538em; } */
+/* h3 { font-size:1.30769em; } */
+
+/* scale traditional */
+h1 { font-size: %(h1FontSize)s; }
+h2 { font-size: %(h2FontSize)s; }
+h3 { font-size: %(h3FontSize)s; }
+
+/* paddings */
 h1 {
-  font-size: 188%;
-  margin: 0.2em 0px 0.3em;
-  border-bottom: 1px solid #000;
+  border-bottom: %(h1BorderBottomStyle)s;
+  padding: %(h1Padding)s;
+  margin: %(h1Margin)s;
+  color: %(h1Color)s;
 }
 
-h2, h3 {
-  margin-top: 0.2em;
-  margin-bottom: 0.3em;
-}
-
-h2 {
-  font-size: 135%;
+h1.plain {
+ border-bottom: none;
 }
 
-h3 {
-  font-size: 130%;
-}
+h2 { padding: %(h2Padding)s; }
+h3 { padding: %(h3Padding)s; }
 
-h4 {
-  font-size: 120%;
-  margin: 0.2em 0px;
-}
-
-h5 {
-  font-size:110%;
-}
-
-h6{
-  font-size:105%;
+html, body {
+  background: %(pageBgColor)s;
 }
 
 a, a:active, a:visited, a:link {
-  color: #ff4500;
+  color: %(aColor)s;
   text-decoration: none;
 }
 
-a:hover{
+a:hover {
   text-decoration: underline;
 }
 
-a img, img {
-  border: none;
-  text-align: center;
-}
-
-p {
-  margin: 0em 0px 0.2em;
-  padding-top: 2px;
-}
-
-table, td, input, select{
-  font-size: 100%;
-}
-
 table {
-  border-collapse: collapse;
   border: none;
 }
 
@@ -86,90 +71,93 @@
   vertical-align: top;
 }
 
-table td img {
-  vertical-align: middle;
-  margin-right: 10px;
+label, .label {
+  font-weight: bold;
+}
+
+pre {
+  clear: both;
+  font-family: 'Courier New', monospace;
+  letter-spacing: 0.015em;
+  padding: 0.6em;
+  margin: 0 2em 1.7em;
+  background-color: %(listingHihligthedBgColor)s;
+  border: 1px solid %(listingBorderColor)s;
+}
+
+p {
+  text-align: justify;
+  margin-bottom: %(defaultLineHeightEm)s;
+}
+
+ul {
+  margin-bottom: %(defaultLineHeightEm)s;
 }
 
 ol {
-  margin: 1px 0px 1px 16px;
+  list-style-type: decimal;
+ /* margin-bottom: %(defaultLineHeightEm)s; */
 }
 
-ul{
-  margin: 1px 0px 1px 4px;
-  list-style-type: none;
+ol ol,
+ul ul{
+  margin-left: 8px;
+  margin-bottom : 0px;
 }
 
-ul li {
-  margin-top: 2px;
-  padding: 0px 0px 2px 8px;
-  background: url("bullet_orange.png") 0% 6px no-repeat;
+/* p + ul { */
+/*   margin-top: -%(defaultLineHeightEm)s; */
+/* } */
+
+li {
+  margin-left: 1.5em;
 }
 
-dt {
-  font-size:1.17em;
-  font-weight:600;
+img{
+  border: none;
 }
 
-dd {
-  margin: 0.6em 0 1.5em 2em;
+img.contentimage {
+  width: 100%;
+  height: 100%;
 }
 
 fieldset {
   border: none;
 }
 
-legend {
-  padding: 0px 2px;
-  font: bold 1em Verdana, sans-serif;
+h1 a, h1 a:active, h1 a:visited, h1 a:link,
+h2 a, h2 a:active, h2 a:visited, h2 a:link,
+h3 a, h3 a:active, h3 a:visited, h3 a:link {
+  color: inherit;
+  text-decoration: none;
 }
 
 input, textarea {
-  padding: 0.2em;
-  vertical-align: middle;
-  border: 1px solid #ccc;
+  padding: 0.1em 0.2em;
+  vertical-align: bottom;
+  border: 1px solid %(pageContentBorderColor)s;
+
 }
 
 input:focus {
-  border: 1px inset #ff7700;
-}
-
-label, .label {
-  font-weight: bold;
-}
-
-iframe {
-  border: 0px;
+  border: 1px inset %(headerBgColor)s;
 }
 
-pre {
-  font-family: Courier, "Courier New", Monaco, monospace;
-  font-size: 100%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
-}
-
-code {
-  font-size: 120%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
-}
-
-blockquote {
-  font-family: Courier, "Courier New", serif;
-  font-size: 120%;
-  margin: 5px 0px;
-  padding: 0.8em;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
+hr{
+  border: none;
+  border-bottom: 1px solid %(defaultColor)s;
+  height: 1px;
 }
 
 /***************************************/
 /* generic classes                     */
 /***************************************/
 
+h1 a:hover {
+ text-decoration: none;
+}
+
 .odd {
   background-color: #f7f6f1;
 }
@@ -179,8 +167,14 @@
 }
 
 .hr {
-  border-bottom: 1px dotted #ccc;
-  margin: 1em 0px;
+  border-bottom: 1px dotted %(pageContentBorderColor)s;
+  height: 17px;
+}
+
+hr.boxSeparator{
+  border: none;
+  border-bottom: 1px solid %(listingBorderColor)s;
+  height: 1px;
 }
 
 .left {
@@ -200,14 +194,15 @@
   visibility: hidden;
 }
 
-li.invisible { list-style: none; background: none; padding: 0px 0px
-1px 1px; }
+li.invisible {
+  background: none;
+  padding: 0px 0px 1px 1px;
+}
 
 li.invisible div{
   display: inline;
 }
 
-
 /***************************************/
 /*   LAYOUT                            */
 /***************************************/
@@ -215,7 +210,7 @@
 /* header */
 
 table#header {
-  background: #ff7700 url("banner.png") left top repeat-x;
+  background: %(headerBgColor)s url("banner.png") repeat-x top left;
   text-align: left;
 }
 
@@ -224,129 +219,136 @@
 }
 
 table#header a {
-color: #000;
+  color: %(defaultColor)s;
+}
+
+table#header img#logo{
+  vertical-align: middle;
 }
 
 span#appliName {
- font-weight: bold;
- color: #000;
- white-space: nowrap;
+  font-weight: bold;
+  color: %(defaultColor)s;
+  white-space: nowrap;
 }
 
 table#header td#headtext {
   width: 100%;
 }
 
-/* FIXME appear with 4px width in IE6 */
-div#stateheader{
-  min-width: 66%;
-}
-
 /* Popup on login box and userActionBox */
-div.popupWrapper{
- position:relative;
- z-index:100;
+div.popupWrapper {
+  position: relative;
+  z-index: 100;
 }
 
 div.popup {
   position: absolute;
   background: #fff;
-  border: 1px solid black;
+  /* background-color: #f0eff0; */
+  /* background-image: url(popup.png); */
+  /* background-repeat: repeat-x; */
+  /* background-positon: top left; */
+  border: 1px solid %(listingBorderColor)s;
+  border-top: none;
   text-align: left;
-  z-index:400;
+  z-index: 400;
 }
 
 div.popup ul li a {
   text-decoration: none;
-  color: black;
+  color: #000;
 }
 
 /* main zone */
 
 div#page {
-  background: #e2e2e2;
-  position: relative;
-  min-height: 800px;
+  margin: %(defaultLayoutMargin)s;
 }
 
-table#mainLayout{
- margin:0px 3px;
+table#mainLayout #navColumnLeft {
+  width: 16em;
+  padding-right: %(defaultLayoutMargin)s;
+}
+
+table#mainLayout #navColumnRight {
+  width: 16em;
+  padding-left: %(defaultLayoutMargin)s;
 }
 
-table#mainLayout td#contentcol {
-  padding: 8px 10px 5px;
+div#pageContent {
+  clear: both;
+  /* margin-top:-1px; *//* enable when testing rhythm */
+  background: %(pageContentBgColor)s;
+  border: 1px solid %(pageContentBorderColor)s;
+  padding: 0 %(pageContentPadding)s %(pageContentPadding)s;
 }
 
-table#mainLayout td.navcol {
-  width: 16em;
+div#pageContent #contentmain .pagination {
+  margin-top: 0;
 }
 
+div#pageContent .pagination{
+  margin-top: 1.5em;
+}
+
+div#contentmain{
+  margin-top: %(pageContentPadding)s
+}
+
+/*FIXME */
 #contentheader {
   margin: 0px;
   padding: 0.2em 0.5em 0.5em 0.5em;
 }
 
 #contentheader a {
-  color: #000;
-}
-
-div#pageContent {
-  clear: both;
-  padding: 10px 1em 2em;
-  background: #ffffff;
-  border: 1px solid #ccc;
+  color: %(defaultColor)s;
 }
 
 /* rql bar */
 
 div#rqlinput {
-  border: 1px solid #cfceb7;
-  margin-bottom: 8px;
-  padding: 3px;
-  background: #cfceb7;
+  margin-bottom: %(defaultLayoutMargin)s;
 }
 
 input#rql{
-  width: 95%;
+  padding: 0.25em 0.3em;
+  width: 99%;
 }
 
 /* boxes */
-div.navboxes {
- margin-top: 8px;
-}
 
 div.boxFrame {
   width: 100%;
 }
 
 div.boxTitle {
-  padding-top: 0px;
-  padding-bottom: 0.2em;
-  font: bold 100% Georgia;
   overflow: hidden;
+  font-weight: bold;
   color: #fff;
-  background: #ff9900 url("search.png") left bottom repeat-x;
+  background: %(boxTitleBg)s;
+}
+
+div.boxTitle span,
+div.sideBoxTitle span {
+  padding: 0px 0.5em;
+  white-space: nowrap;
 }
 
 div.searchBoxFrame div.boxTitle,
 div.greyBoxFrame div.boxTitle {
-  background: #cfceb7;
-}
-
-div.boxTitle span,
-div.sideBoxTitle span {
-  padding: 0px 5px;
-  white-space: nowrap;
+  background: %(actionBoxTitleBg)s;
 }
 
 div.sideBoxTitle span,
 div.searchBoxFrame div.boxTitle span,
 div.greyBoxFrame div.boxTitle span {
-  color: #222211;
+  color: %(defaultColor)s;
 }
 
 .boxFrame a {
-  color: #000;
+  color: %(defaultColor)s;
 }
 
 div.boxContent {
@@ -355,80 +357,22 @@
   border-top: none;
 }
 
-ul.boxListing {
-  margin: 0px;
-  padding: 0px 3px;
-}
-
-ul.boxListing li,
-ul.boxListing ul li {
-  display: inline;
-  margin: 0px;
-  padding: 0px;
-  background-image: none;
-}
-
-ul.boxListing ul {
-  margin: 0px 0px 0px 7px;
-  padding: 1px 3px;
-}
-
-ul.boxListing a {
-  color: #000;
+a.boxMenu {
   display: block;
   padding: 1px 9px 1px 3px;
+  background: transparent %(bulletDownImg)s;
 }
 
-ul.boxListing .selected {
-  color: #FF4500;
-  font-weight: bold;
-}
-
-ul.boxListing a.boxBookmark:hover,
-ul.boxListing a:hover,
-ul.boxListing ul li a:hover {
-  text-decoration: none;
-  background: #eeedd9;
-  color: #111100;
+a.boxMenu:hover {
+  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
+  cursor: pointer;
 }
 
-ul.boxListing a.boxMenu:hover {
-                                background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px;
-                                cursor:pointer;
-                                border-top:medium none;
-                                }
-a.boxMenu {
-  background: transparent url("puce_down.png") 98% 6px no-repeat;
-  display: block;
-  padding: 1px 9px 1px 3px;
-}
-
-
 a.popupMenu {
   background: transparent url("puce_down_black.png") 2% 6px no-repeat;
   padding-left: 2em;
 }
 
-ul.boxListing ul li a:hover {
-  background: #eeedd9  url("bullet_orange.png") 0% 6px no-repeat;
-}
-
-a.boxMenu:hover {
-  background: #eeedd9 url("puce_down.png") 98% 6px no-repeat;
-  cursor: pointer;
-}
-
-ul.boxListing a.boxBookmark {
-  padding-left: 3px;
-  background-image:none;
-  background:#fff;
-}
-
-ul.boxListing ul li a {
-  background: #fff url("bullet_orange.png") 0% 6px no-repeat;
-  padding: 1px 3px 0px 10px;
-}
-
 div.searchBoxFrame div.boxContent {
   padding: 4px 4px 3px;
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
@@ -440,29 +384,32 @@
 }
 
 div.sideBoxTitle {
-  background: #cfceb7;
+  background: %(actionBoxTitleBg)s;
   display: block;
-  font: bold 100% Georgia;
+  font-weight: bold;
 }
 
 div.sideBox {
-  padding: 0 0 0.2em;
-  margin-bottom: 0.5em;
+  margin-bottom: 1em;
+}
+
+ul.sideBox,
+ul.sideBox ul{
+  margin-bottom: 0px;
 }
 
 ul.sideBox li{
- list-style: none;
- background: none;
  padding: 0px 0px 1px 1px;
- }
+ margin: 1px 0 1px 4px;
+}
 
 div.sideBoxBody {
   padding: 0.2em 5px;
-  background: #eeedd9;
+  background: %(sideBoxBodyBg)s;
 }
 
 div.sideBoxBody a {
-  color:#555544;
+  color: %(sideBoxBodyColor)s;
 }
 
 div.sideBoxBody a:hover {
@@ -474,10 +421,11 @@
 }
 
 input.rqlsubmit{
-  background: #fffff8 url("go.png") 50% 50% no-repeat;
+  display: block;
   width: 20px;
   height: 20px;
-  margin: 0px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
+  vertical-align: bottom;
 }
 
 input#norql{
@@ -497,14 +445,14 @@
 }
 
 div#userActionsBox a.popupMenu {
-  color: black;
+  color: #000;
   text-decoration: underline;
   padding-right: 2em;
 }
 
 /* download box XXX move to its own file? */
 div.downloadBoxTitle{
- background : #8FBC8F;
+ background : #8fbc8f;
  font-weight: bold;
 }
 
@@ -513,7 +461,7 @@
 }
 
 div.downloadBox div.sideBoxBody{
- background : #EEFED9;
+ background : #eefed9;
 }
 
 /**************/
@@ -521,17 +469,18 @@
 /**************/
 div#etyperestriction {
   margin-bottom: 1ex;
-  border-bottom: 1px solid #ccc;
+  border-bottom: 1px solid %(pageContentBorderColor)s;
 }
 
+/* pagination */
 span.slice a:visited,
 span.slice a:hover{
-  color: #555544;
+  color: %(helperColor)s;
 }
 
 span.selectedSlice a:visited,
 span.selectedSlice a {
-  color: #000;
+  color: %(defaultColor)s;
 }
 
 /* FIXME should be moved to cubes/folder */
@@ -546,19 +495,13 @@
 }
 
 div.prevnext a {
-  color: #000;
+  color: %(defaultColor)s;
 }
 
 /***************************************/
 /* entity views                        */
 /***************************************/
 
-.mainInfo  {
-  margin-right: 1em;
-  padding: 0.2em;
-}
-
-
 div.mainRelated {
   border: none;
   margin-right: 1em;
@@ -566,18 +509,17 @@
 }
 
 div.primaryRight{
- }
+  margin-left: %(defaultLayoutMargin)s;
+}
 
 div.metadata {
   font-size: 90%;
   margin: 5px 0px 3px;
-  color: #666;
-  font-style: italic;
+  color: %(helperColor)s;
   text-align: right;
 }
 
 div.section {
-  margin-top: 0.5em;
   width:100%;
 }
 
@@ -611,56 +553,50 @@
 
 .warning,
 .message,
-.errorMessage ,
-.searchMessage{
-  padding: 0.3em 0.3em 0.3em 1em;
+.errorMessage{
+  padding: 0.2em;
   font-weight: bold;
 }
 
-.simpleMessage {
-  margin: 4px 0px;
-  font-weight: bold;
-  color: #ff7700;
+.searchMessage{
+ margin-top: %(defaultLayoutMargin)s;
 }
 
-div#appMsg, div.appMsg {
-  border: 1px solid #cfceb7;
-  margin-bottom: 8px;
-  padding: 3px;
-  background: #f8f8ee;
+.loginMessage {
+  margin: 4px 0px;
+  font-weight: bold;
+  color: %(aColor)s;
+}
+
+div#appMsg {
+  margin-bottom: %(defaultLayoutMargin)s;
+  border: 1px solid %(actionBoxTitleBgColor)s;
 }
 
 .message {
-  margin: 0px;
-  background: #f8f8ee url("information.png") 5px center no-repeat;
+  background: %(msgBgColor)s %(infoMsgBgImg)s;
   padding-left: 15px;
 }
 
 .errorMessage {
   margin: 10px 0px;
   padding-left: 25px;
-  background: #f7f6f1 url("critical.png") 2px center no-repeat;
-  color: #ed0d0d;
-  border: 1px solid #cfceb7;
+  background: %(msgBgColor)s url("critical.png") 2px center no-repeat;
+  color: %(errorMsgColor)s;
+  border: 1px solid %(actionBoxTitleBgColor)s;
 }
 
-.searchMessage {
-  margin-top: 0.5em;
-  border-top: 1px solid #cfceb7;
-  background: #eeedd9 url("information.png") 0% 50% no-repeat; /*dcdbc7*/
-}
-
+/* search-associate message */
 .stateMessage {
-  border: 1px solid #ccc;
-  background: #f8f8ee url("information.png") 10px 50% no-repeat;
-  padding:4px 0px 4px 20px;
-  border-width: 1px 0px 1px 0px;
+  border: 1px solid %(pageContentBorderColor)s;
+  background: %(msgBgColor)s %(infoMsgBgImg)s;
+  padding: 0.1em 0 0.1em 20px;
 }
 
 /* warning messages like "There are too many results ..." */
 .warning {
   padding-left: 25px;
-  background: #f2f2f2 url("critical.png") 3px 50% no-repeat;
+  background: %(msgBgColor)s url("critical.png") 3px 50% no-repeat;
 }
 
 /* label shown in the top-right hand corner during form validation */
@@ -668,8 +604,8 @@
   position: fixed;
   right: 5px;
   top: 0px;
-  background: #222211;
-  color: white;
+  background: %(defaultColor)s;
+  color: #fff;
   font-weight: bold;
   display: none;
 }
@@ -679,72 +615,71 @@
 /***************************************/
 
 table.listing {
- padding: 10px 0em;
- color: #000;
- width: 100%;
- border-right: 1px solid #dfdfdf;
+  width: 100%;
+  font-size: 0.9167em;
+  padding: 10px 0em;
+  color: %(defaultColor)s;
+  border: 1px solid %(listingBorderColor)s;
+  margin-bottom: 1em;
 }
 
+table.listing th {
+  font-weight: bold;
+  font-size: 8pt;
+  background: %(listingHeaderBgColor)s; 
+  padding: 2px 4px;
+  border: 1px solid %(listingBorderColor)s;
+  border-right:none;
+ /* white-space: nowrap; */
+}
 
 table.listing thead th.over {
-  background-color: #746B6B;
+  background-color: %(listingHeaderBgColor)s;
   cursor: pointer;
 }
 
-table.listing tr th {
-  border: 1px solid #dfdfdf;
-  border-right:none;
-  font-size: 8pt;
-  padding: 4px;
-}
-
 table.listing tr .header {
-  border-right: 1px solid #dfdfdf;
+  border-right: 1px solid %(listingBorderColor)s;
   cursor: pointer;
 }
 
 table.listing td {
-  color: #3D3D3D;
-  padding: 4px;
-  background-color: #FFF;
+  padding: 3px;
   vertical-align: top;
-}
-
-table.listing th,
-table.listing td {
-  padding: 3px 0px 3px 5px;
-  border: 1px solid #dfdfdf;
+  border: 1px solid %(listingBorderColor)s;
   border-right: none;
-}
-
-table.listing th {
-  font-weight: bold;
-  background: #ebe8d9 url("button.png") repeat-x;
+  background-color: #fff;
 }
 
 table.listing td a,
 table.listing td a:visited {
-  color: #666;
+  color: %(defaultColor)s;
 }
 
 table.listing a:hover,
 table.listing tr.highlighted td a {
-  color:#000;
+  color:%(defaultColor)s;
 }
 
 table.listing td.top {
-  border: 1px solid white;
+  border: 1px solid #fff;
   border-bottom: none;
   text-align: right ! important;
-  /* insane IE row bug workaround */
+  /* insane IE row bug workraound */
   position: relative;
   left: -1px;
   top: -1px;
 }
 
+table.listing input,
+table.listing textarea {
+ background: %(listingHihligthedBgColor)s;
+}
+
 table.htableForm {
   vertical-align: middle;
 }
+
 table.htableForm td{
   padding-left: 1em;
   padding-top: 0.5em;
@@ -774,27 +709,26 @@
   color: #ff0000;
 }
 
-
 /***************************************/
 /* addcombobox                         */
 /***************************************/
 
-input#newopt{
- width:120px ;
- display:block;
- float:left;
- }
+input#newopt {
+  display: block;
+  float: left;
+  width: 120px;
+}
 
 div#newvalue{
- margin-top:2px;
- }
+  margin-top: 2px;
+}
 
-#add_newopt{
- background: #fffff8 url("go.png") 50% 50% no-repeat;
- width: 20px;
- line-height: 20px;
- display:block;
- float:left;
+#add_newopt {
+  display: block;
+  float: left;
+  width: 20px;
+  line-height: 20px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
 }
 
 /***************************************/
@@ -803,9 +737,8 @@
 
 input.button{
   margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
+  border: 1px solid %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
 }
 
 /* FileItemInnerView  jquery.treeview.css */
@@ -815,18 +748,105 @@
 }
 
 /***************************************/
+/* lists                               */
+/***************************************/
+
+ul.section,
+ul.startup {
+  margin-bottom: 0px;
+}
+
+ul.startup li,
+ul.section li {
+  margin-left:0px
+}
+
+ul.boxListing {
+  margin: 0px;
+  padding: 0px 3px;
+}
+
+ul.boxListing li,
+ul.boxListing ul li {
+  margin: 0px;
+  padding: 0px;
+  background-image: none;
+}
+
+ul.boxListing ul {
+  padding: 1px 3px;
+}
+
+ul.boxListing a {
+  color: %(defaultColor)s;
+  display:block;
+  padding: 1px 9px 1px 3px;
+}
+
+ul.boxListing .selected {
+  color: %(aColor)s;
+  font-weight: bold;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
+}
+
+ul.boxListing a.boxBookmark {
+  padding-left: 3px;
+  background-image: none;
+  background:#fff;
+}
+
+ul.simple li,
+ul.boxListing ul li ,
+.popupWrapper ul li {
+  background: transparent url("bullet_orange.png") no-repeat 0% 6px;
+}
+
+ul.boxListing a.boxBookmark:hover,
+ul.boxListing a:hover,
+ul.boxListing ul li a:hover {
+  text-decoration: none;
+  background: %(sideBoxBodyBg)s;
+}
+
+ul.boxListing ul li a:hover{
+  background-color: transparent;
+}
+
+ul.boxListing ul li a {
+  padding: 1px 3px 0px 10px;
+}
+
+ul.simple li {
+  padding-left: 8px;
+}
+
+.popupWrapper ul {
+  padding:0.2em 0.3em;
+  margin-bottom: 0px;
+}
+
+.popupWrapper ul li {
+  padding-left: 8px;
+  margin-left: 0px;
+  white-space: nowrap;
+}
+
+/***************************************/
 /* footer                              */
 /***************************************/
 
-div.footer {
+div#footer {
   text-align: center;
 }
-div.footer a {
-  color: #000;
+div#footer a {
+  color: %(defaultColor)s;
   text-decoration: none;
 }
 
-
 /****************************************/
 /* FIXME must by managed by cubes       */
 /****************************************/
@@ -835,21 +855,11 @@
   color: gray;
 }
 
-
-/***************************************/
-/* FIXME : Deprecated ? entity view ?  */
-/***************************************/
-.title {
-  text-align: left;
-  font-size:  large;
-  font-weight: bold;
-}
-
 .validateButton {
   margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
+  border: 1px solid %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
 }
 
 /********************************/
@@ -859,3 +869,44 @@
 .otherView {
   float: right;
 }
+
+/********************************/
+/* rest releted classes         */
+/********************************/
+
+img.align-right {
+  margin-left: 1.5em;
+}
+
+img.align-left {
+  margin-right: 1.5em;
+}
+
+/********************************/
+/* overwite other css here      */
+/********************************/
+
+/* ui.tabs.css */
+ul.ui-tabs-nav,
+div.ui-tabs-panel {
+  font-family: %(defaultFontFamily)s;
+  font-size: %(defaultSize)s;
+}
+
+div.ui-tabs-panel {
+  border-top:1px solid #b6b6b6;
+}
+
+ul.ui-tabs-nav a {
+  color: #3d3d3d;
+}
+
+ul.ui-tabs-nav a:hover {
+  color: #000;
+}
+
+img.ui-datepicker-trigger {
+  margin-left: 0.5em;
+  vertical-align: bottom;
+}
+
--- a/web/data/cubicweb.edition.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.edition.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,255 +1,317 @@
-/*
+/**
+ * Functions dedicated to edition.
+ *
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
  */
 
-CubicWeb.require('python.js');
-CubicWeb.require('htmlhelpers.js');
-CubicWeb.require('ajax.js');
-
-
 //============= Eproperty form functions =====================================//
-
-/* called on Eproperty key selection:
+/**
+ * .. function:: setPropValueWidget(varname, tabindex)
+ *
+ * called on Eproperty key selection:
  * - get the selected value
  * - get a widget according to the key by a sync query to the server
  * - fill associated div with the returned html
  *
- * @param varname the name of the variable as used in the original creation form
- * @param tabindex the tabindex that should be set on the widget
+ * * `varname`, the name of the variable as used in the original creation form
+ * * `tabindex`, the tabindex that should be set on the widget
  */
+
 function setPropValueWidget(varname, tabindex) {
-    var key = firstSelected(document.getElementById('pkey:'+varname));
+    var key = firstSelected(document.getElementById('pkey:' + varname));
     if (key) {
-	var args = {fname: 'prop_widget', pageid: pageid,
-     		    arg: map(jQuery.toJSON, [key, varname, tabindex])};
-	jqNode('div:value:'+varname).loadxhtml(JSON_BASE_URL, args, 'post');
+        var args = {
+            fname: 'prop_widget',
+            pageid: pageid,
+            arg: $.map([key, varname, tabindex], jQuery.toJSON)
+        };
+        cw.jqNode('div:value:' + varname).loadxhtml(JSON_BASE_URL, args, 'post');
     }
 }
 
-
 // *** EDITION FUNCTIONS ****************************************** //
-
-/*
+/**
+ * .. function:: reorderTabindex(start, formid)
+ *
  * this function is called when an AJAX form was generated to
  * make sure tabindex remains consistent
  */
 function reorderTabindex(start, formid) {
-    var form = getNode(formid || 'entityForm');
+    var form = cw.getNode(formid || 'entityForm');
     var inputTypes = ['INPUT', 'SELECT', 'TEXTAREA'];
-    var tabindex = (start==null)?15:start;
-    nodeWalkDepthFirst(form, function(elem) {
+    var tabindex = (start == null) ? 15: start;
+    cw.utils.nodeWalkDepthFirst(form, function(elem) {
         var tagName = elem.tagName.toUpperCase();
-	if (inputTypes.contains(tagName)) {
-	    if (getNodeAttribute(elem, 'tabindex') != null) {
-		tabindex += 1;
-		elem.setAttribute('tabindex', tabindex);
-	    }
-	    return null;
-	}
-	return filter(isElementNode, elem.childNodes);
+        if ($.inArray(tagName, inputTypes)) {
+            if (jQuery(elem).attr('tabindex') != null) {
+                tabindex += 1;
+                jQuery(elem).attr('tabindex', tabindex);
+            }
+            return null;
+        }
+        return jQuery.grep(elem.childNodes, isElementNode);
     });
 }
 
-
 function showMatchingSelect(selectedValue, eid) {
     if (selectedValue) {
-	divId = 'div' + selectedValue + '_' + eid;
-	var divNode = jQuery('#' + divId);
-	if (!divNode.length) {
-	    var args = {vid: 'unrelateddivs', relation: selectedValue,
-			rql: rql_for_eid(eid), '__notemplate': 1,
-			callback: function() {_showMatchingSelect(eid, jQuery('#' + divId));}};
-	    jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
-	} else {
-	    _showMatchingSelect(eid, divNode);
-	}
+        divId = 'div' + selectedValue + '_' + eid;
+        var divNode = jQuery('#' + divId);
+        if (!divNode.length) {
+            var args = {
+                vid: 'unrelateddivs',
+                relation: selectedValue,
+                rql: rql_for_eid(eid),
+                '__notemplate': 1,
+                callback: function() {
+                    _showMatchingSelect(eid, jQuery('#' + divId));
+                }
+            };
+            jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
+        } else {
+            _showMatchingSelect(eid, divNode);
+        }
     } else {
-	_showMatchingSelect(eid, null);
+        _showMatchingSelect(eid, null);
     }
 }
 
-
-// @param divNode is a jQuery selection
+/**
+ * .. function:: _showMatchingSelect(eid, divNode)
+ *
+ * * `divNode`, a jQuery selection
+ */
 function _showMatchingSelect(eid, divNode) {
     // hide all divs, and then show the matching one
     // (would actually be better to directly hide the displayed one)
     jQuery('#unrelatedDivs_' + eid).children().hide();
     // divNode not found means 'no relation selected' (i.e. first blank item)
     if (divNode && divNode.length) {
-	divNode.show();
+        divNode.show();
     }
 }
 
-// this function builds a Handle to cancel pending insertion
+/**
+ * .. function:: buildPendingInsertHandle(elementId, element_name, selectNodeId, eid)
+ *
+ * this function builds a Handle to cancel pending insertion
+ */
 function buildPendingInsertHandle(elementId, element_name, selectNodeId, eid) {
-   jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')";
-   return A({'class' : 'handle', 'href' : jscall,
-	     'title' : _("cancel this insert")}, '[x]');
+    jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')";
+    return A({
+        'class': 'handle',
+        'href': jscall,
+        'title': _("cancel this insert")
+    },
+    '[x]');
 }
 
 function buildEntityLine(relationName, selectedOptionNode, comboId, eid) {
-   // textContent doesn't seem to work on selectedOptionNode
-   var content = selectedOptionNode.firstChild.nodeValue;
-   var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid);
-   var link = A({'href' : 'view?rql=' + selectedOptionNode.value,
-	  	 'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id},
-		content);
-   var tr = TR({'id' : 'tr' + selectedOptionNode.id}, [ TH(null, relationName),
-							TD(null, [handle, link])
-						      ]);
-   try {
-      var separator = getNode('relationSelectorRow_' + eid);
-      //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least)
-      // XXX Warning: separator.parentNode is not (always ?) the
-      // table itself, but an intermediate node (TableSectionElement)
-      var tableBody = separator.parentNode;
-      tableBody.insertBefore(tr, separator);
-   } catch(ex) {
-      log("got exception(2)!" + ex);
-   }
+    // textContent doesn't seem to work on selectedOptionNode
+    var content = selectedOptionNode.firstChild.nodeValue;
+    var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid);
+    var link = A({
+        'href': 'view?rql=' + selectedOptionNode.value,
+        'class': 'editionPending',
+        'id': 'a' + selectedOptionNode.id
+    },
+    content);
+    var tr = TR({
+        'id': 'tr' + selectedOptionNode.id
+    },
+    [TH(null, relationName), TD(null, [handle, link])]);
+    try {
+        var separator = cw.getNode('relationSelectorRow_' + eid);
+        //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least)
+        // XXX Warning: separator.parentNode is not (always ?) the
+        // table itself, but an intermediate node (TableSectionElement)
+        var tableBody = separator.parentNode;
+        tableBody.insertBefore(tr, separator);
+    } catch(ex) {
+        log("got exception(2)!" + ex);
+    }
 }
 
 function buildEntityCell(relationName, selectedOptionNode, comboId, eid) {
     var handle = buildPendingInsertHandle(selectedOptionNode.id, 'div_insert_', comboId, eid);
-    var link = A({'href' : 'view?rql=' + selectedOptionNode.value,
-		  'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id},
-		 content);
-    var div = DIV({'id' : 'div_insert_' + selectedOptionNode.id}, [handle, link]);
+    var link = A({
+        'href': 'view?rql=' + selectedOptionNode.value,
+        'class': 'editionPending',
+        'id': 'a' + selectedOptionNode.id
+    },
+    content);
+    var div = DIV({
+        'id': 'div_insert_' + selectedOptionNode.id
+    },
+    [handle, link]);
     try {
-	var td = jQuery('#cell'+ relationName +'_'+eid);
-	td.appendChild(div);
+        var td = jQuery('#cell' + relationName + '_' + eid);
+        td.appendChild(div);
     } catch(ex) {
-	alert("got exception(3)!" + ex);
+        alert("got exception(3)!" + ex);
     }
 }
 
 function addPendingInsert(optionNode, eid, cell, relname) {
-    var value = getNodeAttribute(optionNode, 'value');
+    var value = jQuery(optionNode).attr('value');
     if (!value) {
-	// occurs when the first element in the box is selected (which is not
-	// an entity but the combobox title)
+        // occurs when the first element in the box is selected (which is not
+        // an entity but the combobox title)
         return;
     }
     // 2nd special case
     if (value.indexOf('http') == 0) {
-	document.location = value;
-	return;
+        document.location = value;
+        return;
     }
     // add hidden parameter
     var entityForm = jQuery('#entityForm');
     var oid = optionNode.id.substring(2); // option id is prefixed by "id"
-    remoteExec('add_pending_inserts', [oid.split(':')]);
+    loadRemote('json', ajaxFuncArgs('add_pending_inserts', null,
+                                    [oid.split(':')]), 'GET', true);
     var selectNode = optionNode.parentNode;
     // remove option node
     selectNode.removeChild(optionNode);
     // add line in table
     if (cell) {
-      // new relation as a cell in multiple edit
-      // var relation_name = relationSelected.getAttribute('value');
-      // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_'));
-      buildEntityCell(relname, optionNode, selectNode.id, eid);
+        // new relation as a cell in multiple edit
+        // var relation_name = relationSelected.getAttribute('value');
+        // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_'));
+        buildEntityCell(relname, optionNode, selectNode.id, eid);
     }
     else {
-	var relationSelector = getNode('relationSelector_'+eid);
-	var relationSelected = relationSelector.options[relationSelector.selectedIndex];
-	// new relation as a line in simple edit
-	buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid);
+        var relationSelector = cw.getNode('relationSelector_' + eid);
+        var relationSelected = relationSelector.options[relationSelector.selectedIndex];
+        // new relation as a line in simple edit
+        buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid);
     }
 }
 
 function cancelPendingInsert(elementId, element_name, comboId, eid) {
     // remove matching insert element
-    var entityView = jqNode('a' + elementId).text();
-    jqNode(element_name + elementId).remove();
+    var entityView = cw.jqNode('a' + elementId).text();
+    cw.jqNode(element_name + elementId).remove();
     if (comboId) {
-	// re-insert option in combobox if it was taken from there
-	var selectNode = getNode(comboId);
+        // re-insert option in combobox if it was taken from there
+        var selectNode = cw.getNode(comboId);
         // XXX what on object relation
-	if (selectNode){
-	   var options = selectNode.options;
-	   var node_id = elementId.substring(0, elementId.indexOf(':'));
-	   options[options.length] = OPTION({'id' : elementId, 'value' : node_id}, entityView);
-	}
+        if (selectNode) {
+            var options = selectNode.options;
+            var node_id = elementId.substring(0, elementId.indexOf(':'));
+            options[options.length] = OPTION({
+                'id': elementId,
+                'value': node_id
+            },
+            entityView);
+        }
     }
     elementId = elementId.substring(2, elementId.length);
-    remoteExec('remove_pending_insert', elementId.split(':'));
+    loadRemote('json', ajaxFuncArgs('remove_pending_inserts', null,
+                                    elementId.split(':')), 'GET', true);
 }
 
-// this function builds a Handle to cancel pending insertion
+/**
+ * .. function:: buildPendingDeleteHandle(elementId, eid)
+ *
+ * this function builds a Handle to cancel pending insertion
+ */
 function buildPendingDeleteHandle(elementId, eid) {
-  var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');";
-  return A({'href' : jscall, 'class' : 'pendingDeleteHandle',
-    'title' : _("delete this relation")}, '[x]');
+    var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');";
+    return A({
+        'href': jscall,
+        'class': 'pendingDeleteHandle',
+        'title': _("delete this relation")
+    },
+    '[x]');
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: addPendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function addPendingDelete(nodeId, eid) {
-    var d = asyncRemoteExec('add_pending_delete', nodeId.split(':'));
-    d.addCallback(function () {
-	// and strike entity view
-	jqNode('span' + nodeId).addClass('pendingDelete');
-	// replace handle text
-	jqNode('handle' + nodeId).text('+');
+    var d = loadRemote('json', ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
+    d.addCallback(function() {
+        // and strike entity view
+        cw.jqNode('span' + nodeId).addClass('pendingDelete');
+        // replace handle text
+        cw.jqNode('handle' + nodeId).text('+');
     });
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: cancelPendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function cancelPendingDelete(nodeId, eid) {
-    var d = asyncRemoteExec('remove_pending_delete', nodeId.split(':'));
-    d.addCallback(function () {
-	// reset link's CSS class
-	jqNode('span' + nodeId).removeClass('pendingDelete');
-	// replace handle text
-	jqNode('handle' + nodeId).text('x');
+    var d = loadRemote('json', ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
+    d.addCallback(function() {
+        // reset link's CSS class
+        cw.jqNode('span' + nodeId).removeClass('pendingDelete');
+        // replace handle text
+        cw.jqNode('handle' + nodeId).text('x');
     });
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: togglePendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function togglePendingDelete(nodeId, eid) {
     // node found means we should cancel deletion
-    if ( hasElementClass(getNode('span' + nodeId), 'pendingDelete') ) {
-	cancelPendingDelete(nodeId, eid);
+    if (jQuery.className.has(cw.getNode('span' + nodeId), 'pendingDelete')) {
+        cancelPendingDelete(nodeId, eid);
     } else {
-	addPendingDelete(nodeId, eid);
+        addPendingDelete(nodeId, eid);
     }
 }
 
-
 function selectForAssociation(tripletIdsString, originalEid) {
-    var tripletlist = map(function (x) { return x.split(':'); },
-			  tripletIdsString.split('-'));
-    var d = asyncRemoteExec('add_pending_inserts', tripletlist);
-    d.addCallback(function () {
-	var args = {vid: 'edition', __mode: 'normal',
-		    rql: rql_for_eid(originalEid)};
-	document.location = 'view?' + asURL(args);
+    var tripletlist = $.map(tripletIdsString.split('-'),
+			    function(x) { return [x.split(':')] ;});
+    var d = loadRemote('json', ajaxFuncArgs('add_pending_inserts', null, tripletlist));
+    d.addCallback(function() {
+        var args = {
+            vid: 'edition',
+            __mode: 'normal',
+            rql: rql_for_eid(originalEid)
+        };
+        document.location = 'view?' + asURL(args);
     });
 
 }
 
-
 function updateInlinedEntitiesCounters(rtype, role) {
-    jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function (i) {
-	this.innerHTML = i+1;
+    jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function(i) {
+        this.innerHTML = i + 1;
     });
 }
 
-
-/*
+/**
+ * .. function:: addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore)
+ *
  * makes an AJAX request to get an inline-creation view's content
- * @param peid : the parent entity eid
- * @param petype : the parent entity type
- * @param ttype : the target (inlined) entity type
- * @param rtype : the relation type between both entities
+ * * `peid`, the parent entity eid
+ *
+ * * `petype`, the parent entity type
+ *
+ * * `ttype`, the target (inlined) entity type
+ *
+ * * `rtype`, the relation type between both entities
  */
 function addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore) {
-    insertBefore = insertBefore || getNode('add' + rtype + ':' + peid + 'link').parentNode;
-    var d = asyncRemoteExec('inline_creation_form', peid, petype, ttype, rtype, role, i18nctx);
-    d.addCallback(function (response) {
+    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);
+    d.addCallback(function(response) {
         var dom = getDomFromResponse(response);
-        preprocessAjaxLoad(null, dom);
+        loadAjaxHtmlHead(dom);
         var form = jQuery(dom);
         form.css('display', 'none');
         form.insertBefore(insertBefore).slideDown('fast');
@@ -259,76 +321,81 @@
         // if the inlined form contains a file input, we must force
         // the form enctype to multipart/form-data
         if (form.find('input:file').length) {
-	    // NOTE: IE doesn't support dynamic enctype modification, we have
-	    //       to set encoding too.
-            form.closest('form').attr('enctype', 'multipart/form-data')
-		.attr('encoding', 'multipart/form-data');
+            // NOTE: IE doesn't support dynamic enctype modification, we have
+            //       to set encoding too.
+            form.closest('form').attr('enctype', 'multipart/form-data').attr('encoding', 'multipart/form-data');
         }
-        postAjaxLoad(dom);
+        _postAjaxLoad(dom);
     });
-    d.addErrback(function (xxx) {
+    d.addErrback(function(xxx) {
         log('xxx =', xxx);
     });
 }
 
-/*
+/**
+ * .. function:: removeInlineForm(peid, rtype, role, eid, showaddnewlink)
+ *
  * removes the part of the form used to edit an inlined entity
  */
 function removeInlineForm(peid, rtype, role, eid, showaddnewlink) {
-    jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() {
-	$(this).remove();
-	updateInlinedEntitiesCounters(rtype, role);
+    cw.jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() {
+            $(this).remove();
+            updateInlinedEntitiesCounters(rtype, role);
     });
     if (showaddnewlink) {
-	toggleVisibility(showaddnewlink);
+        toggleVisibility(showaddnewlink);
     }
 }
 
-/*
+/**
+ * .. function:: removeInlinedEntity(peid, rtype, eid)
+ *
  * alternatively adds or removes the hidden input that make the
  * edition of the relation `rtype` possible between `peid` and `eid`
- * @param peid : the parent entity eid
- * @param rtype : the relation type between both entities
- * @param eid : the inlined entity eid
+ * * `peid`, the parent entity eid
+ *
+ * * `rtype`, the relation type between both entities
+ *
+ * * `eid`, the inlined entity eid
  */
 function removeInlinedEntity(peid, rtype, eid) {
     // XXX work around the eid_param thing (eid + ':' + eid) for #471746
     var nodeid = ['rel', peid, rtype, eid + ':' + eid].join('-');
-    var node = jqNode(nodeid);
-    if (! node.attr('cubicweb:type')) {
+    var node = cw.jqNode(nodeid);
+    if (!node.attr('cubicweb:type')) {
         node.attr('cubicweb:type', node.val());
         node.val('');
-	var divid = ['div', peid, rtype, eid].join('-');
-	jqNode(divid).fadeTo('fast', 0.5);
-	var noticeid = ['notice', peid, rtype, eid].join('-');
-	jqNode(noticeid).fadeIn('fast');
+        var divid = ['div', peid, rtype, eid].join('-');
+        cw.jqNode(divid).fadeTo('fast', 0.5);
+        var noticeid = ['notice', peid, rtype, eid].join('-');
+        cw.jqNode(noticeid).fadeIn('fast');
     }
 }
 
 function restoreInlinedEntity(peid, rtype, eid) {
     // XXX work around the eid_param thing (eid + ':' + eid) for #471746
     var nodeid = ['rel', peid, rtype, eid + ':' + eid].join('-');
-    var node = jqNode(nodeid);
+    var node = cw.jqNode(nodeid);
     if (node.attr('cubicweb:type')) {
         node.val(node.attr('cubicweb:type'));
         node.attr('cubicweb:type', '');
-	jqNode(['fs', peid, rtype, eid].join('-')).append(node);
+        cw.jqNode(['fs', peid, rtype, eid].join('-')).append(node);
         var divid = ['div', peid, rtype, eid].join('-');
-	jqNode(divid).fadeTo('fast', 1);
+        cw.jqNode(divid).fadeTo('fast', 1);
         var noticeid = ['notice', peid, rtype, eid].join('-');
-	jqNode(noticeid).hide();
+        cw.jqNode(noticeid).hide();
     }
 }
 
 function _clearPreviousErrors(formid) {
     // on some case (eg max request size exceeded, we don't know the formid
     if (formid) {
-	jQuery('#' + formid + 'ErrorMessage').remove();
-	jQuery('#' + formid + ' span.errorMsg').remove();
-	jQuery('#' + formid + ' .error').removeClass('error');
+        jQuery('#' + formid + 'ErrorMessage').remove();
+        jQuery('#' + formid + ' span.errorMsg').remove();
+        jQuery('#' + formid + ' .error').removeClass('error');
     } else {
-	jQuery('span.errorMsg').remove();
-	jQuery('.error').removeClass('error');
+        jQuery('span.errorMsg').remove();
+        jQuery('.error').removeClass('error');
     }
 }
 
@@ -336,60 +403,66 @@
     var globalerrors = [];
     var firsterrfield = null;
     for (fieldname in errors) {
-	var errmsg = errors[fieldname];
-	if (!fieldname) {
-	    globalerrors.push(errmsg);
-	} else {
-	    var fieldid = fieldname + ':' + eid;
-	    var suffixes = ['', '-subject', '-object'];
-	    var found = false;
-	    // XXX remove suffixes at some point
-	    for (var i=0, length=suffixes.length; i<length;i++) {
-		var field = jqNode(fieldname + suffixes[i] + ':' + eid);
-		if (field && getNodeAttribute(field, 'type') != 'hidden') {
-		    if ( !firsterrfield ) {
-			firsterrfield = 'err-' + fieldid;
-		    }
-		    addElementClass(field, 'error');
-		    var span = SPAN({'id': 'err-' + fieldid, 'class': "errorMsg"}, errmsg);
-		    field.before(span);
-		    found = true;
-		    break;
-		}
-	    }
-	    if (!found) {
-		firsterrfield = formid;
-		globalerrors.push(_(fieldname) + ' : ' + errmsg);
-	    }
-	}
+        var errmsg = errors[fieldname];
+        if (!fieldname) {
+            globalerrors.push(errmsg);
+        } else {
+            var fieldid = fieldname + ':' + eid;
+            var suffixes = ['', '-subject', '-object'];
+            var found = false;
+            // XXX remove suffixes at some point
+            for (var i = 0, length = suffixes.length; i < length; i++) {
+                var field = cw.jqNode(fieldname + suffixes[i] + ':' + eid);
+                if (field && jQuery(field).attr('type') != 'hidden') {
+                    if (!firsterrfield) {
+                        firsterrfield = 'err-' + fieldid;
+                    }
+                    jQuery(field).addClass('error');
+                    var span = SPAN({
+                        'id': 'err-' + fieldid,
+                        'class': "errorMsg"
+                    },
+                    errmsg);
+                    field.before(span);
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                firsterrfield = formid;
+                globalerrors.push(_(fieldname) + ' : ' + errmsg);
+            }
+        }
     }
     if (globalerrors.length) {
-	if (globalerrors.length == 1) {
-	    var innernode = SPAN(null, globalerrors[0]);
-	} else {
-	    var innernode = UL(null, map(partial(LI, null), globalerrors));
-	}
-	// insert DIV and innernode before the form
-	var div = DIV({'class' : "errorMessage", 'id': formid + 'ErrorMessage'});
-	div.appendChild(innernode);
-	jQuery('#' + formid).before(div);
+        if (globalerrors.length == 1) {
+            var innernode = SPAN(null, globalerrors[0]);
+        } else {
+            var innernode = UL(null, $.map(globalerrors, partial(LI, null)));
+        }
+        // insert DIV and innernode before the form
+        var div = DIV({
+            'class': "errorMessage",
+            'id': formid + 'ErrorMessage'
+        });
+        div.appendChild(innernode);
+        jQuery('#' + formid).before(div);
     }
     return firsterrfield || formid;
 }
 
-
 function handleFormValidationResponse(formid, onsuccess, onfailure, result, cbargs) {
     // Success
     if (result[0]) {
-	if (onsuccess) {
-             onsuccess(result, formid, cbargs);
-	} else {
-	    document.location.href = result[1];
-	}
-      return true;
+        if (onsuccess) {
+            onsuccess(result, formid, cbargs);
+        } else {
+            document.location.href = result[1];
+        }
+        return true;
     }
-    if (onfailure && !onfailure(result, formid, cbargs)) {
-	return false;
+    if (onfailure && ! onfailure(result, formid, cbargs)) {
+        return false;
     }
     unfreezeFormButtons(formid);
     // Failures
@@ -397,11 +470,11 @@
     var descr = result[1];
     var errmsg;
     // Unknown structure
-    if ( !isArrayLike(descr) || descr.length != 2 ) {
-	errmsg = descr;
+    if ( !cw.utils.isArrayLike(descr) || descr.length != 2 ) {
+        errmsg = descr;
     } else {
-	_displayValidationerrors(formid, descr[0], descr[1]);
-	errmsg = _('please correct errors below');
+        _displayValidationerrors(formid, descr[0], descr[1]);
+        errmsg = _('please correct errors below');
     }
     updateMessage(errmsg);
     // ensure the browser does not scroll down
@@ -409,68 +482,101 @@
     return false;
 }
 
-
-/* unfreeze form buttons when the validation process is over*/
+/**
+ * .. function:: unfreezeFormButtons(formid)
+ *
+ * unfreeze form buttons when the validation process is over
+ */
 function unfreezeFormButtons(formid) {
     jQuery('#progress').hide();
     // on some case (eg max request size exceeded, we don't know the formid
     if (formid) {
-	jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
+        jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
     } else {
-	jQuery('.validateButton').removeAttr('disabled');
+        jQuery('.validateButton').removeAttr('disabled');
     }
     return true;
 }
 
-/* disable form buttons while the validation is being done */
+/**
+ * .. function:: freezeFormButtons(formid)
+ *
+ * disable form buttons while the validation is being done
+ */
 function freezeFormButtons(formid) {
     jQuery('#progress').show();
     jQuery('#' + formid + ' .validateButton').attr('disabled', 'disabled');
     return true;
 }
 
-/* used by additional submit buttons to remember which button was clicked */
+/**
+ * .. function:: postForm(bname, bvalue, formid)
+ *
+ * used by additional submit buttons to remember which button was clicked
+ */
 function postForm(bname, bvalue, formid) {
-    var form = getNode(formid);
+    var form = cw.getNode(formid);
     if (bname) {
-	var child = form.appendChild(INPUT({type: 'hidden', name: bname, value: bvalue}));
+        var child = form.appendChild(INPUT({
+            type: 'hidden',
+            name: bname,
+            value: bvalue
+        }));
     }
     var onsubmit = form.onsubmit;
     if (!onsubmit || (onsubmit && onsubmit())) {
-	form.submit();
+        form.submit();
     }
     if (bname) {
-	jQuery(child).remove(); /* cleanup */
+        jQuery(child).remove();
     }
 }
 
-
-/* called on load to set target and iframeso object.
- * NOTE: this is a hack to make the XHTML compliant.
- * NOTE2: `object` nodes might be a potential replacement for iframes
- * NOTE3: there is a XHTML module allowing iframe elements but there
- *        is still the problem of the form's `target` attribute
+/**
+ * .. function:: setFormsTarget(node)
+ *
+ * called on load to set target and iframeso object.
+ *
+ * .. note::
+ *
+ *    this is a hack to make the XHTML compliant.
+ *
+ * .. note::
+ *
+ *   `object` nodes might be a potential replacement for iframes
+ *
+ * .. note::
+ *
+ *    there is a XHTML module allowing iframe elements but there
+ *    is still the problem of the form's `target` attribute
  */
 function setFormsTarget(node) {
     var $node = jQuery(node || document.body);
-    $node.find('form').each(function () {
-	var form = jQuery(this);
-	var target = form.attr('cubicweb:target');
-	if (target) {
-	    form.attr('target', target);
-	    /* do not use display: none because some browsers ignore iframe
+    $node.find('form').each(function() {
+        var form = jQuery(this);
+        var target = form.attr('cubicweb:target');
+        if (target) {
+            form.attr('target', target);
+            /* do not use display: none because some browsers ignore iframe
              * with no display */
-	    form.append(IFRAME({name: target, id: target,
-				src: 'javascript: void(0)',
-				width: '0px', height: '0px'}));
-	}
+            form.append(IFRAME({
+                name: target,
+                id: target,
+                src: 'javascript: void(0)',
+                width: '0px',
+                height: '0px'
+            }));
+        }
     });
 }
 
-jQuery(document).ready(function() {setFormsTarget();});
+jQuery(document).ready(function() {
+    setFormsTarget();
+});
 
-
-/*
+/**
+ * .. function:: validateForm(formid, action, onsuccess, onfailure)
+ *
  * called on traditionnal form submission : the idea is to try
  * to post the form. If the post is successful, `validateForm` redirects
  * to the appropriate URL. Otherwise, the validation errors are displayed
@@ -478,77 +584,183 @@
  */
 function validateForm(formid, action, onsuccess, onfailure) {
     try {
-	var zipped = formContents(formid);
-	var d = asyncRemoteExec('validate_form', action, zipped[0], zipped[1]);
-    } catch (ex) {
-	log('got exception', ex);
-	return false;
+        var zipped = cw.utils.formContents(formid);
+        var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
+        var d = loadRemote('json', args);
+    } catch(ex) {
+        log('got exception', ex);
+        return false;
     }
     function _callback(result, req) {
-	handleFormValidationResponse(formid, onsuccess, onfailure, result);
+        handleFormValidationResponse(formid, onsuccess, onfailure, result);
     }
     d.addCallback(_callback);
     return false;
 }
 
 
-/*
+
+// ======================= DEPRECATED FUNCTIONS ========================= //
+// (mostly reledit related)
+/**
+ * .. function:: inlineValidateRelationFormOptions(rtype, eid, divid, options)
+ *
  * called by reledit forms to submit changes
- * @param formid : the dom id of the form used
- * @param rtype : the attribute being edited
- * @param eid : the eid of the entity being edited
- * @param reload: boolean to reload page if true (when changing URL dependant data)
- * @param default_value : value if the field is empty
- * @param lzone : html fragment (string) for a clic-zone triggering actual edition
+ * * `rtype`, the attribute being edited
+ *
+ * * `eid`, the eid of the entity being edited
+ *
+ * * `options`, a dictionnary of options used by the form validation handler such
+ *    as ``role``, ``onsuccess``, ``onfailure``, ``reload``, ``vid``, ``lzone``
+ *    and ``default_value``:
+ *
+ *     * `onsucess`, javascript function to execute on success, default is noop
+ *
+ *     * `onfailure`, javascript function to execute on failure, default is noop
+ *
+ *     * `default_value`, value if the field is empty
+ *
+ *     * `lzone`, html fragment (string) for a clic-zone triggering actual edition
  */
-function inlineValidateRelationForm(rtype, role, eid, divid, reload, vid,
-                                    default_value, lzone) {
-    try {
-	var form = getNode(divid+'-form');
-        var relname = rtype + ':' + eid;
-        var newtarget = jQuery('[name=' + relname + ']').val();
-	var zipped = formContents(form);
-	var d = asyncRemoteExec('validate_form', 'apply', zipped[0], zipped[1]);
-    } catch (ex) {
-	return false;
+
+
+showInlineEditionForm = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function showInlineEditionForm(eid, rtype, divid) {
+        jQuery('#' + divid).hide();
+        jQuery('#' + divid + '-value').hide();
+        jQuery('#' + divid + '-form').show();
+    }
+);
+
+hideInlineEdit = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function hideInlineEdit(eid, rtype, divid) {
+        jQuery('#appMsg').hide();
+        jQuery('div.errorMessage').remove();
+        jQuery('#' + divid).show();
+        jQuery('#' + divid + '-value').show();
+        jQuery('#' + divid + '-form').hide();
     }
-    d.addCallback(function (result, req) {
-	if (handleFormValidationResponse(divid+'-form', noop, noop, result)) {
-          if (reload) {
-            document.location.reload();
-          } else {
-              var args = {fname: 'reledit_form', rtype: rtype, role: role, eid: eid, divid: divid,
-                          reload: reload, vid: vid, default_value: default_value, landing_zone: lzone};
-              jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
-          }
-	}
+);
+
+
+inlineValidateRelationFormOptions = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function inlineValidateRelationFormOptions(rtype, eid, divid, options) {
+        try {
+            var form = cw.getNode(divid + '-form');
+            var relname = rtype + ':' + eid;
+            var newtarget = jQuery('[name=' + relname + ']').val();
+            var zipped = cw.utils.formContents(form);
+            var args = ajaxFuncArgs('validate_form', null, 'apply', zipped[0], zipped[1]);
+            var d = loadRemote(JSON_BASE_URL, args, 'POST');
+        } catch(ex) {
+            return false;
+        }
+        d.addCallback(function(result, req) {
+            execFormValidationResponse(rtype, eid, divid, options, result);
+        });
         return false;
     });
-  return false;
-}
+
+execFormValidationResponse = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function execFormValidationResponse(rtype, eid, divid, options, result) {
+        options = $.extend({onsuccess: noop,
+                            onfailure: noop
+                           }, options);
+        if (handleFormValidationResponse(divid + '-form', options.onsucess , options.onfailure, result)) {
+            if (options.reload) {
+                document.location.reload();
+            } else {
+                var args = {
+                    fname: 'reledit_form',
+                    rtype: rtype,
+                    role: options.role,
+                    eid: eid,
+                    divid: divid,
+                    reload: options.reload,
+                    vid: options.vid,
+                    default_value: options.default_value,
+                    landing_zone: options.lzone
+                };
+                jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+            }
+        }
+});
 
 
-/**** inline edition ****/
-function loadInlineEditionForm(eid, rtype, role, divid, reload, vid,
-                               default_value, lzone) {
-  var args = {fname: 'reledit_form', rtype: rtype, role: role, eid: eid, divid: divid,
-              reload: reload, vid: vid, default_value: default_value, landing_zone: lzone,
-              callback: function () {showInlineEditionForm(eid, rtype, divid);}};
-  jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
-}
+/**
+ * .. function:: loadInlineEditionFormOptions(eid, rtype, divid, options)
+ *
+ * inline edition
+ */
+loadInlineEditionFormOptions = cw.utils.deprecatedFunction(
+  '[3.9] this is now unused by reledit (see cw.reledit.js) ',
+  function loadInlineEditionFormOptions(eid, rtype, divid, options) {
+    var args = {
+        fname: 'reledit_form',
+        rtype: rtype,
+        role: options.role,
+        eid: eid,
+        divid: divid,
+        reload: options.reload,
+        vid: options.vid,
+        default_value: options.default_value,
+        landing_zone: options.lzone,
+        callback: function() {
+            showInlineEditionForm(eid, rtype, divid);
+        }
+    };
+    jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+});
+
 
-function showInlineEditionForm(eid, rtype, divid) {
-    jQuery('#' + divid).hide();
-    jQuery('#' + divid + '-value' ).hide();
-    jQuery('#' + divid+ '-form').show();
-}
+inlineValidateRelationForm = cw.utils.deprecatedFunction(
+    '[3.9] inlineValidateRelationForm() function is deprecated, use inlineValidateRelationFormOptions instead',
+    function(rtype, role, eid, divid, reload, vid, default_value, lzone, onsucess, onfailure) {
+        try {
+            var form = cw.getNode(divid + '-form');
+            var relname = rtype + ':' + eid;
+            var newtarget = jQuery('[name=' + relname + ']').val();
+            var zipped = cw.utils.formContents(form);
+            var d = asyncRemoteExec('validate_form', 'apply', zipped[0], zipped[1]);
+        } catch(ex) {
+            return false;
+        }
+        d.addCallback(function(result, req) {
+        var options = {role : role,
+                       reload: reload,
+                       vid: vid,
+                       default_value: default_value,
+                       lzone: lzone,
+                       onsucess: onsucess || $.noop,
+                       onfailure: onfailure || $.noop
+                      };
+            execFormValidationResponse(rtype, eid, divid, options);
+        });
+        return false;
+    }
+);
 
-function hideInlineEdit(eid, rtype, divid) {
-    jQuery('#appMsg').hide();
-    jQuery('div.errorMessage').remove();
-    jQuery('#' + divid).show();
-    jQuery('#' + divid + '-value').show();
-    jQuery('#' + divid +'-form').hide();
-}
-
-CubicWeb.provide('edition.js');
+loadInlineEditionForm = cw.utils.deprecatedFunction(
+    '[3.9] loadInlineEditionForm() function is deprecated, use loadInlineEditionFormOptions instead',
+    function(eid, rtype, role, divid, reload, vid, default_value, lzone) {
+        var args = {
+            fname: 'reledit_form',
+            rtype: rtype,
+            role: role,
+            eid: eid,
+            divid: divid,
+            reload: reload,
+            vid: vid,
+            default_value: default_value,
+            landing_zone: lzone,
+            callback: function() {
+                showInlineEditionForm(eid, rtype, divid);
+            }
+        };
+        jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+    }
+);
--- a/web/data/cubicweb.facets.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.facets.css	Mon Jul 19 15:37:02 2010 +0200
@@ -91,6 +91,7 @@
 
 .facetValueDisabled {
   font-style: italic;
+  text-decoration: line-through;
 }
 
 
--- a/web/data/cubicweb.facets.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.facets.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,228 +1,248 @@
-/*
+/** filter form, aka facets, javascript functions
+ *
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
-CubicWeb.require('htmlhelpers.js');
-CubicWeb.require('ajax.js');
+var SELECTED_IMG = baseuri() + "data/black-check.png";
+var UNSELECTED_IMG = baseuri() + "data/no-check-no-border.png";
+var UNSELECTED_BORDER_IMG = baseuri() + "data/black-uncheck.png";
+
 
-//============= filter form functions ========================================//
 function copyParam(origparams, newparams, param) {
-    var index = findValue(origparams[0], param);
-    if (index > -1) {
-	newparams[param] = origparams[1][index];
+    var index = jQuery.inArray(param, origparams[0]);
+    if (index > - 1) {
+        newparams[param] = origparams[1][index];
     }
 }
 
-function facetFormContent(form) {
+
+function facetFormContent($form) {
     var names = [];
     var values = [];
-    jQuery(form).find('.facet').each(function () {
+    $form.find('.facet').each(function() {
         var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
         var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) {
-  	    names.push(facetName);
-  	    values.push(this.getAttribute('cubicweb:value'));
+            names.push(facetName);
+            values.push(this.getAttribute('cubicweb:value'));
         });
     });
-    jQuery(form).find('input').each(function () {
+    $form.find('input').each(function() {
         names.push(this.name);
         values.push(this.value);
     });
-    jQuery(form).find('select option[selected]').each(function () {
-	names.push(this.parentNode.name);
-	values.push(this.value);
+    $form.find('select option[selected]').each(function() {
+        names.push(this.parentNode.name);
+        values.push(this.value);
     });
     return [names, values];
 }
 
+
 function buildRQL(divid, vid, paginate, vidargs) {
     jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
-    var form = getNode(divid+'Form');
-    var zipped = facetFormContent(form);
+    var $form = $('#' + divid + 'Form');
+    var zipped = facetFormContent($form);
     zipped[0].push('facetargs');
     zipped[1].push(vidargs);
-    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
+    var d = loadRemote('json', ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
     d.addCallback(function(result) {
-	var rql = result[0];
-	var $bkLink = jQuery('#facetBkLink');
-	if ($bkLink.length) {
-	    var bkPath = 'view?rql=' + escape(rql);
-	    if (vid) {
-		bkPath += '&vid=' + escape(vid);
-	    }
-	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + escape(bkPath);
-	    $bkLink.attr('href', bkUrl);
-	}
-	var toupdate = result[1];
-	var extraparams = vidargs;
-	if (paginate) { extraparams['paginate'] = '1'; } // XXX in vidargs
-	// copy some parameters
-	// XXX cleanup vid/divid mess
-	// if vid argument is specified , the one specified in form params will
-	// be overriden by replacePageChunk
-	copyParam(zipped, extraparams, 'vid');
-	extraparams['divid'] = divid;
-	copyParam(zipped, extraparams, 'divid');
-	copyParam(zipped, extraparams, 'subvid');
-	copyParam(zipped, extraparams, 'fromformfilter');
-	// paginate used to know if the filter box is acting, in which case we
-	// want to reload action box to match current selection (we don't want
-	// this from a table filter)
-	replacePageChunk(divid, rql, vid, extraparams, true, function() {
-	  jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
-	});
-	if (paginate) {
-	    // FIXME the edit box might not be displayed in which case we don't
-	    // know where to put the potential new one, just skip this case
-	    // for now
-	    if (jQuery('#edit_box').length) {
-		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
-	    }
-	    if (jQuery('#breadcrumbs').length) {
-		reloadComponent('breadcrumbs', rql, 'components', 'breadcrumbs');
-	    }
-	}
-	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
-	d.addCallback(function(updateMap) {
-	    for (facetId in updateMap) {
-		var values = updateMap[facetId];
-		jqNode(facetId).find('.facetCheckBox').each(function () {
-		    var value = this.getAttribute('cubicweb:value');
-		    if (!values.contains(value)) {
-			if (!jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).addClass('facetValueDisabled');
-			}
-		    } else {
-			if (jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).removeClass('facetValueDisabled');
-			}
-		    }
-		});
-	    }
-	});
+        var rql = result[0];
+        var $bkLink = jQuery('#facetBkLink');
+        if ($bkLink.length) {
+            var bkPath = 'view?rql=' + escape(rql);
+            if (vid) {
+                bkPath += '&vid=' + escape(vid);
+            }
+            var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + escape(bkPath);
+            $bkLink.attr('href', bkUrl);
+        }
+        var toupdate = result[1];
+        var extraparams = vidargs;
+        if (paginate) { extraparams['paginate'] = '1'; } // XXX in vidargs
+        // copy some parameters
+        // XXX cleanup vid/divid mess
+        // if vid argument is specified , the one specified in form params will
+        // be overriden by replacePageChunk
+        copyParam(zipped, extraparams, 'vid');
+        extraparams['divid'] = divid;
+        copyParam(zipped, extraparams, 'divid');
+        copyParam(zipped, extraparams, 'subvid');
+        copyParam(zipped, extraparams, 'fromformfilter');
+        // paginate used to know if the filter box is acting, in which case we
+        // want to reload action box to match current selection (we don't want
+        // this from a table filter)
+        extraparams['rql'] = rql;
+        if (vid) { // XXX see copyParam above. Need cleanup
+            extraparams['vid'] = vid;
+        }
+        d = $('#' + divid).loadxhtml('json', ajaxFuncArgs('view', extraparams),
+                                     null, 'swap');
+        d.addCallback(function() {
+            // XXX rql/vid in extraparams
+            jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
+        });
+        if (paginate) {
+            // FIXME the edit box might not be displayed in which case we don't
+            // know where to put the potential new one, just skip this case for
+            // now
+            var $node = jQuery('#edit_box');
+            if ($node.length) {
+                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                    'rql': rql
+                },
+                'boxes', 'edit_box'));
+            }
+            $node = jQuery('#breadcrumbs')
+            if ($node.length) {
+                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                    'rql': rql
+                },
+                'components', 'breadcrumbs'));
+            }
+        }
+        var d = loadRemote('json', ajaxFuncArgs('filter_select_content', null, toupdate, rql));
+        d.addCallback(function(updateMap) {
+            for (facetId in updateMap) {
+                var values = updateMap[facetId];
+                cw.jqNode(facetId).find('.facetCheckBox').each(function() {
+                    var value = this.getAttribute('cubicweb:value');
+                    if (jQuery.inArray(value, values) == -1) {
+                        if (!jQuery(this).hasClass('facetValueDisabled')) {
+                            jQuery(this).addClass('facetValueDisabled');
+                        }
+                    } else {
+                        if (jQuery(this).hasClass('facetValueDisabled')) {
+                            jQuery(this).removeClass('facetValueDisabled');
+                        }
+                    }
+                });
+            }
+        });
     });
 }
 
 
-var SELECTED_IMG = baseuri()+"data/black-check.png";
-var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png";
-var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png";
-
 function initFacetBoxEvents(root) {
     // facetargs : (divid, vid, paginate, extraargs)
     root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	// NOTE: don't evaluate facetargs here but in callbacks since its value
-	//       may changes and we must send its value when the callback is
-	//       called, not when the page is initialized
-	var facetargs = form.attr('cubicweb:facetargs');
-	if (facetargs !== undefined) {
-	    form.submit(function() {
-	        buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-	        return false;
-	    });
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		facet.find('div.facetCheckBox').each(function (i) {
-		    this.setAttribute('cubicweb:idx', i);
-		});
-		facet.find('div.facetCheckBox').click(function () {
-		    var $this = jQuery(this);
-		    // NOTE : add test on the facet operator (i.e. OR, AND)
-		    // if ($this.hasClass('facetValueDisabled')){
-		    //  	    return
-		    // }
-		    if ($this.hasClass('facetValueSelected')) {
-			$this.removeClass('facetValueSelected');
-			$this.find('img').each(function (i){
-			if (this.getAttribute('cubicweb:unselimg')){
-			       this.setAttribute('src', UNSELECTED_BORDER_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			    else{
-			       this.setAttribute('src', UNSELECTED_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			});
-			var index = parseInt($this.attr('cubicweb:idx'));
-			// we dont need to move the element when cubicweb:idx == 0
-			if (index > 0){
-			    var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) {
-				    var nindex = parseInt(n.getAttribute('cubicweb:idx'));
-				    return nindex > index;
-				}).length;
-			    index += shift;
-			    var parent = this.parentNode;
-			    var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')');
-			    if ( ! ($insertAfter.length == 1 && shift == 0) ) {
-				// only rearrange element if necessary
-				$insertAfter.after(this);
-			    }
-			}
-		    } else {
-			var lastSelected = facet.find('.facetValueSelected:last');
-			if (lastSelected.length) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			jQuery(this).addClass('facetValueSelected');
-			var $img = jQuery(this).find('img');
-			$img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
-		    }
-		    buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    facet.find('.facetBody').animate({scrollTop: 0}, '');
-		});
-		facet.find('select.facetOperator').change(function() {
-		    var nbselected = facet.find('div.facetValueSelected').length;
-		    if (nbselected >= 2) {
-			buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    }
-		});
-		facet.find('div.facetTitle').click(function() {
-		  facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
-		  jQuery(this).toggleClass('opened');
-		   });
+    jQuery(root).find('form').each(function() {
+        var form = jQuery(this);
+        // NOTE: don't evaluate facetargs here but in callbacks since its value
+        //       may changes and we must send its value when the callback is
+        //       called, not when the page is initialized
+        var facetargs = form.attr('cubicweb:facetargs');
+        if (facetargs !== undefined) {
+            form.submit(function() {
+                buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                return false;
+            });
+            form.find('div.facet').each(function() {
+                var facet = jQuery(this);
+                facet.find('div.facetCheckBox').each(function(i) {
+                    this.setAttribute('cubicweb:idx', i);
+                });
+                facet.find('div.facetCheckBox').click(function() {
+                    var $this = jQuery(this);
+                    // NOTE : add test on the facet operator (i.e. OR, AND)
+                    // if ($this.hasClass('facetValueDisabled')){
+                    //          return
+                    // }
+                    if ($this.hasClass('facetValueSelected')) {
+                        $this.removeClass('facetValueSelected');
+                        $this.find('img').each(function(i) {
+                            if (this.getAttribute('cubicweb:unselimg')) {
+                                this.setAttribute('src', UNSELECTED_BORDER_IMG);
+                                this.setAttribute('alt', (_('not selected')));
+                            }
+                            else {
+                                this.setAttribute('src', UNSELECTED_IMG);
+                                this.setAttribute('alt', (_('not selected')));
+                            }
+                        });
+                        var index = parseInt($this.attr('cubicweb:idx'));
+                        // we dont need to move the element when cubicweb:idx == 0
+                        if (index > 0) {
+                            var shift = jQuery.grep(facet.find('.facetValueSelected'), function(n) {
+                                var nindex = parseInt(n.getAttribute('cubicweb:idx'));
+                                return nindex > index;
+                            }).length;
+                            index += shift;
+                            var parent = this.parentNode;
+                            var $insertAfter = jQuery(parent).find('.facetCheckBox:nth(' + index + ')');
+                            if (! ($insertAfter.length == 1 && shift == 0)) {
+                                // only rearrange element if necessary
+                                $insertAfter.after(this);
+                            }
+                        }
+                    } else {
+                        var lastSelected = facet.find('.facetValueSelected:last');
+                        if (lastSelected.length) {
+                            lastSelected.after(this);
+                        } else {
+                            var parent = this.parentNode;
+                            jQuery(parent).prepend(this);
+                        }
+                        jQuery(this).addClass('facetValueSelected');
+                        var $img = jQuery(this).find('img');
+                        $img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
+                    }
+                    buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                    facet.find('.facetBody').animate({
+                        scrollTop: 0
+                    },
+                    '');
+                });
+                facet.find('select.facetOperator').change(function() {
+                    var nbselected = facet.find('div.facetValueSelected').length;
+                    if (nbselected >= 2) {
+                        buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                    }
+                });
+                facet.find('div.facetTitle').click(function() {
+                    facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
+                    jQuery(this).toggleClass('opened');
+                });
 
-	    });
-	}
+            });
+        }
     });
 }
 
+
 // trigger this function on document ready event if you provide some kind of
 // persistent search (eg crih)
-function reorderFacetsItems(root){
+function reorderFacetsItems(root) {
     root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	if (form.attr('cubicweb:facetargs')) {
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		var lastSelected = null;
-		facet.find('div.facetCheckBox').each(function (i) {
-		    var $this = jQuery(this);
-		    if ($this.hasClass('facetValueSelected')) {
-			if (lastSelected) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			lastSelected = $this;
-		    }
-		});
-	    });
-	}
+    jQuery(root).find('form').each(function() {
+        var form = jQuery(this);
+        if (form.attr('cubicweb:facetargs')) {
+            form.find('div.facet').each(function() {
+                var facet = jQuery(this);
+                var lastSelected = null;
+                facet.find('div.facetCheckBox').each(function(i) {
+                    var $this = jQuery(this);
+                    if ($this.hasClass('facetValueSelected')) {
+                        if (lastSelected) {
+                            lastSelected.after(this);
+                        } else {
+                            var parent = this.parentNode;
+                            jQuery(parent).prepend(this);
+                        }
+                        lastSelected = $this;
+                    }
+                });
+            });
+        }
     });
 }
 
-// we need to differenciate cases where initFacetBoxEvents is called
-// with one argument or without any argument. If we use `initFacetBoxEvents`
-// as the direct callback on the jQuery.ready event, jQuery will pass some argument
-// of his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {initFacetBoxEvents();});
 
-CubicWeb.provide('facets.js');
+// we need to differenciate cases where initFacetBoxEvents is called with one
+// argument or without any argument. If we use `initFacetBoxEvents` as the
+// direct callback on the jQuery.ready event, jQuery will pass some argument of
+// his, so we use this small anonymous function instead.
+jQuery(document).ready(function() {
+    initFacetBoxEvents();
+});
--- a/web/data/cubicweb.flot.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.flot.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,14 +1,14 @@
 function showTooltip(x, y, contents) {
-    $('<div id="tooltip">' + contents + '</div>').css( {
-            position: 'absolute',
+    $('<div id="tooltip">' + contents + '</div>').css({
+        position: 'absolute',
         display: 'none',
         top: y + 5,
-            left: x + 5,
-            border: '1px solid #fdd',
-            padding: '2px',
-            'background-color': '#fee',
-            opacity: 0.80
-        }).appendTo("body").fadeIn(200);
+        left: x + 5,
+        border: '1px solid #fdd',
+        padding: '2px',
+        'background-color': '#fee',
+        opacity: 0.80
+    }).appendTo("body").fadeIn(200);
 }
 
 var previousPoint = null;
@@ -18,19 +18,19 @@
             previousPoint = item.datapoint;
             $("#tooltip").remove();
             var x = item.datapoint[0].toFixed(2),
-                y = item.datapoint[1].toFixed(2);
+            y = item.datapoint[1].toFixed(2);
             if (item.datapoint.length == 3) {
                 x = new Date(item.datapoint[2]);
                 x = x.toLocaleDateString() + ' ' + x.toLocaleTimeString();
             } else if (item.datapoint.length == 4) {
-               x = new Date(item.datapoint[2]);
-               x = x.strftime(item.datapoint[3]);
+                x = new Date(item.datapoint[2]);
+                x = x.strftime(item.datapoint[3]);
             }
-            showTooltip(item.pageX, item.pageY,
-            item.series.label + ': (' + x + ' ; ' + y + ')');
+            showTooltip(item.pageX, item.pageY, item.series.label + ': (' + x + ' ; ' + y + ')');
         }
     } else {
         $("#tooltip").remove();
         previousPoint = null;
     }
 }
+
--- a/web/data/cubicweb.form.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.form.css	Mon Jul 19 15:37:02 2010 +0200
@@ -9,7 +9,6 @@
   width: 100%;
   font-size : 160%;
   font-weight: bold;
-  color: #ff4500;
   padding-bottom : 0.4em;
   text-transform: capitalize;
   margin-bottom: 0.6em
@@ -23,9 +22,8 @@
 div.iformTitle {
   font-weight: bold;
   font-size: 110%;
-  color: #222211;
-  background: #e4ead8;
-  border: 1px solid #E4EAD8;  /*#b7b6a3 */
+  background: %(formHeaderBgColor)s;
+  border: 1px solid %(formHeaderBgColor)s;  /*#b7b6a3 */
   border-bottom: none;
 }
 
@@ -46,14 +44,14 @@
 }
 
 fieldset.subentity {
-  border: 1px solid #E4EAD8;
+  border: 1px solid %(formHeaderBgColor)s;
   display: block;
   margin-bottom: 1em;
   padding: 0.4em;
 }
 
 table.attributeForm {
-  border: 1px solid #E4EAD8;
+  border: 1px solid %(formHeaderBgColor)s;
   margin-bottom: 1em;
   padding: 0.8em 1.2em;
   width: 100%;
@@ -91,7 +89,7 @@
 table.attributeForm input,
 table.attributeForm textarea,
 table.attributeForm select {
-  border: 1px solid #E4EAD8;  /*#b7b6a3*/
+  border: 1px solid %(formHeaderBgColor)s;  /*#b7b6a3*/
 }
 
 table.attributeForm textarea {
@@ -163,10 +161,10 @@
 }
 
 a.editionPending {
-  color: #557755;
+  color: #9c9b24; /*557755*/
   font-weight: bold;
 }
-
+ 
 div.pendingDelete {
   text-decoration: line-through;
 }
@@ -187,22 +185,20 @@
   display: inline;
 }
 
-div.editableField:hover,
-div.editableField p:hover {
-  background-color: #eeedd9;
-}
+/* div.editableField:hover, */
+/* div.editableField p:hover { */
+/*   background-color: #eeedd9; */
+/* } */
 
-.error input { /* error added by the form renderer */
-  background: transparent url("error.png") 100% 50% no-repeat;
-}
+.error input, /* error added by the form renderer */
 input.error { /* error added by javascript */
-  background: transparent url("error.png") 100% 50% no-repeat;
+  background: transparent %(errorMsgBgImg)s;
 }
 
 span.errorMsg {
   display: block;
   font-weight: bold;
-  color: #ed0d0d;
+  color: %(errorMsgColor)s;
 }
 
 option.separator {
@@ -216,12 +212,12 @@
   font-style: italic;
   font-size: 110%;
   padding-left: 2em;
-  background : #f8f8ee url("information.png") 5px center no-repeat ;
+  background : %(msgBgColor)s %(infoMsgBgImg)s;
 }
 
 .helper{
   font-size: 96%;
-  color: #555544;
+  color: %(helperColor)s;
 }
 
 .helper:hover {
@@ -231,8 +227,8 @@
 
 .validateButton {
   margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
+  border-width: 1px;
+  border-style: solid;
+  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  background: %(buttonBgColor)s %(buttonBgImg)s;
 }
-
--- a/web/data/cubicweb.gmap.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.gmap.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,72 +1,72 @@
-/*
+/**
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
- *
- *
  */
 
 Widgets.GMapWidget = defclass('GMapWidget', null, {
-  __init__: function(wdgnode) {
-    // Assume we have imported google maps JS
-    if (GBrowserIsCompatible()) {
-      var uselabelstr = wdgnode.getAttribute('cubicweb:uselabel');
-      var uselabel = true;
-      if (uselabelstr){
-	if (uselabelstr == 'True'){
-	  uselabel = true;
-	}
-	else{
-	  uselabel = false;
-	}
-      }
-      var map = new GMap2(wdgnode);
-      map.addControl(new GSmallMapControl());
-      var jsonurl = wdgnode.getAttribute('cubicweb:loadurl');
-      var self = this; // bind this to a local variable
-      jQuery.getJSON(jsonurl, function(geodata) {
-	if (geodata.center) {
-	  var zoomLevel = geodata.zoomlevel;
-	  map.setCenter(new GLatLng(geodata.center.latitude, geodata.center.longitude),
-		        zoomLevel);
-	}
-	for (var i=0; i<geodata.markers.length; i++) {
-	  var marker = geodata.markers[i];
-	  self.createMarker(map, marker, i+1, uselabel);
-	}
-      });
-      jQuery(wdgnode).after(this.legendBox);
-    } else { // incompatible browser
-      jQuery.unload(GUnload);
-    }
-  },
+    __init__: function(wdgnode) {
+        // Assume we have imported google maps JS
+        if (GBrowserIsCompatible()) {
+            var uselabelstr = wdgnode.getAttribute('cubicweb:uselabel');
+            var uselabel = true;
+            if (uselabelstr) {
+                if (uselabelstr == 'True') {
+                    uselabel = true;
+                }
+                else {
+                    uselabel = false;
+                }
+            }
+            var map = new GMap2(wdgnode);
+            map.addControl(new GSmallMapControl());
+            var jsonurl = wdgnode.getAttribute('cubicweb:loadurl');
+            var self = this; // bind this to a local variable
+            jQuery.getJSON(jsonurl, function(geodata) {
+                if (geodata.center) {
+                    var zoomLevel = geodata.zoomlevel;
+                    map.setCenter(new GLatLng(geodata.center.latitude, geodata.center.longitude), zoomLevel);
+                }
+                for (var i = 0; i < geodata.markers.length; i++) {
+                    var marker = geodata.markers[i];
+                    self.createMarker(map, marker, i + 1, uselabel);
+                }
+            });
+            jQuery(wdgnode).after(this.legendBox);
+        } else { // incompatible browser
+            jQuery.unload(GUnload);
+        }
+    },
 
-  createMarker: function(map, marker, i, uselabel) {
-    var point = new GLatLng(marker.latitude, marker.longitude);
-    var icon = new GIcon();
-    icon.image = marker.icon[0];
-    icon.iconSize = new GSize(marker.icon[1][0], marker.icon[1][1]) ;
-    icon.iconAnchor = new GPoint(marker.icon[2][0], marker.icon[2][1]);
-    if(marker.icon[3]){
-      icon.shadow4 =  marker.icon[3];
-    }
-    if (typeof LabeledMarker == "undefined") {
-	var gmarker = new GMarker(point, {icon: icon,
-	title: marker.title});
-    } else {
-        var gmarker = new LabeledMarker(point, {
-          icon: icon,
-          title: marker.title,
-          labelText: uselabel?'<strong>' + i + '</strong>':'',
-          labelOffset: new GSize(2, -32)
+    createMarker: function(map, marker, i, uselabel) {
+        var point = new GLatLng(marker.latitude, marker.longitude);
+        var icon = new GIcon();
+        icon.image = marker.icon[0];
+        icon.iconSize = new GSize(marker.icon[1][0], marker.icon[1][1]);
+        icon.iconAnchor = new GPoint(marker.icon[2][0], marker.icon[2][1]);
+        if (marker.icon[3]) {
+            icon.shadow4 = marker.icon[3];
+        }
+        if (typeof LabeledMarker == "undefined") {
+            var gmarker = new GMarker(point, {
+                icon: icon,
+                title: marker.title
+            });
+        } else {
+            var gmarker = new LabeledMarker(point, {
+                icon: icon,
+                title: marker.title,
+                labelText: uselabel ? '<strong>' + i + '</strong>': '',
+                labelOffset: new GSize(2, - 32)
+            });
+        }
+        map.addOverlay(gmarker);
+        GEvent.addListener(gmarker, 'click', function() {
+            jQuery.post(marker.bubbleUrl, function(data) {
+                map.openInfoWindowHtml(point, data);
+            });
         });
     }
-    map.addOverlay(gmarker);
-    GEvent.addListener(gmarker, 'click', function() {
-      jQuery.post(marker.bubbleUrl, function(data) {
-	map.openInfoWindowHtml(point, data);
-      });
-    });
-  }
 
 });
+
--- a/web/data/cubicweb.goa.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.goa.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,10 +1,17 @@
-/*
+/**
  *  functions specific to cubicweb on google appengine
  *
  *  :organization: Logilab
- *  :copyright: 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
-/* overrides rql_for_eid function from htmlhelpers.hs */
-function rql_for_eid(eid) { return 'Any X WHERE X eid "' + eid + '"'; }
+/**
+ * .. function:: rql_for_eid(eid)
+ *
+ * overrides rql_for_eid function from htmlhelpers.hs
+ */
+function rql_for_eid(eid) {
+	return 'Any X WHERE X eid "' + eid + '"';
+}
+
--- a/web/data/cubicweb.htmlhelpers.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,31 +1,34 @@
-CubicWeb.require('python.js');
-CubicWeb.require('jquery.corner.js');
-
-/* returns the document's baseURI. (baseuri() uses document.baseURI if
+/**
+ * .. function:: baseuri()
+ *
+ * returns the document's baseURI. (baseuri() uses document.baseURI if
  * available and inspects the <base> tag manually otherwise.)
-*/
+ */
 function baseuri() {
     var uri = document.baseURI;
     if (uri) { // some browsers don't define baseURI
-	return uri;
+        return uri;
     }
-    var basetags = document.getElementsByTagName('base');
-    if (basetags.length) {
-	return getNodeAttribute(basetags[0], 'href');
-    }
-    return '';
+    return jQuery('base').attr('href');
 }
 
-
-/* set body's cursor to 'progress' */
+/**
+ * .. function:: setProgressCursor()
+ *
+ * set body's cursor to 'progress'
+ */
 function setProgressCursor() {
     var body = document.getElementsByTagName('body')[0];
     body.style.cursor = 'progress';
 }
 
-/* reset body's cursor to default (mouse cursor). The main
+/**
+ * .. function:: resetCursor(result)
+ *
+ * reset body's cursor to default (mouse cursor). The main
  * purpose of this function is to be used as a callback in the
- * deferreds' callbacks chain. */
+ * deferreds' callbacks chain.
+ */
 function resetCursor(result) {
     var body = document.getElementsByTagName('body')[0];
     body.style.cursor = 'default';
@@ -34,14 +37,19 @@
 }
 
 function updateMessage(msg) {
-    var msgdiv = DIV({'class':'message'});
+    var msgdiv = DIV({
+        'class': 'message'
+    });
     // don't pass msg to DIV() directly because DIV will html escape it
     // and msg should alreay be html escaped at this point.
     msgdiv.innerHTML = msg;
     jQuery('#appMsg').removeClass('hidden').empty().append(msgdiv);
 }
 
-/* builds an url from an object (used as a dictionnary)
+/**
+ * .. function:: asURL(props)
+ *
+ * builds an url from an object (used as a dictionnary)
  *
  * >>> asURL({'rql' : "RQL", 'x': [1, 2], 'itemvid' : "oneline"})
  * rql=RQL&vid=list&itemvid=oneline&x=1&x=2
@@ -50,122 +58,140 @@
  */
 function asURL(props) {
     var chunks = [];
-    for(key in props) {
-	var value = props[key];
-	// generate a list of couple key=value if key is multivalued
-	if (isArrayLike(value)) {
-	    for (var i=0; i<value.length;i++) {
-		chunks.push(key + '=' + urlEncode(value[i]));
-	    }
-	} else {
-	    chunks.push(key + '=' + urlEncode(value));
-	}
+    for (key in props) {
+        var value = props[key];
+        // generate a list of couple key=value if key is multivalued
+        if (cw.utils.isArrayLike(value)) {
+            for (var i = 0; i < value.length; i++) {
+                chunks.push(key + '=' + urlEncode(value[i]));
+            }
+        } else {
+            chunks.push(key + '=' + urlEncode(value));
+        }
     }
     return chunks.join('&');
 }
 
-/* return selected value of a combo box if any
+/**
+ * .. function:: firstSelected(selectNode)
+ *
+ * return selected value of a combo box if any
  */
 function firstSelected(selectNode) {
-    var selection = filter(attrgetter('selected'), selectNode.options);
-    return (selection.length > 0) ? getNodeAttribute(selection[0], 'value'):null;
+    var $selection = $(selectNode).find('option:selected:first');
+    return ($selection.length > 0) ? $selection[0] : null;
 }
 
-/* toggle visibility of an element by its id
+/**
+ * .. function:: toggleVisibility(elemId)
+ *
+ * toggle visibility of an element by its id
  */
 function toggleVisibility(elemId) {
-    jqNode(elemId).toggleClass('hidden');
+    $('#' + elemId).toggleClass('hidden');
 }
 
-
-/* toggles visibility of login popup div */
+/**
+ * .. function:: popupLoginBox()
+ *
+ * toggles visibility of login popup div
+ */
 // XXX used exactly ONCE in basecomponents
 function popupLoginBox() {
-    toggleVisibility('popupLoginBox');
+    $('#popupLoginBox').toggleClass('hidden');
     jQuery('#__login:visible').focus();
 }
 
-
-/* returns the list of elements in the document matching the tag name
+/**
+ * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
+ *
+ * returns the list of elements in the document matching the tag name
  * and the properties provided
  *
- * @param tagName the tag's name
- * @param properties a js Object used as a dict
- * @return an iterator (if a *real* array is needed, you can use the
+ * * `tagName`, the tag's name
+ *
+ * * `properties`, a js Object used as a dict
+ *
+ * Return an iterator (if a *real* array is needed, you can use the
  *                      list() function)
  */
 function getElementsMatching(tagName, properties, /* optional */ parent) {
     parent = parent || document;
-    return filter(function elementMatches(element) {
-                     for (prop in properties) {
-                       if (getNodeAttribute(element, prop) != properties[prop]) {
-	                 return false;}}
-                    return true;},
-                  parent.getElementsByTagName(tagName));
+    return jQuery.grep(parent.getElementsByTagName(tagName), function elementMatches(element) {
+        for (prop in properties) {
+            if (jQuery(element).attr(prop) != properties[prop]) {
+                return false;
+            }
+        }
+        return true;
+    });
 }
 
-/*
+/**
+ * .. function:: setCheckboxesState(nameprefix, value, checked)
+ *
  * sets checked/unchecked status of checkboxes
  */
-function setCheckboxesState(nameprefix, checked){
+
+function setCheckboxesState(nameprefix, value, checked) {
     // XXX: this looks in *all* the document for inputs
-    var elements = getElementsMatching('input', {'type': "checkbox"});
-    filterfunc = function(cb) { return nameprefix && cb.name.startsWith(nameprefix); };
-    forEach(filter(filterfunc, elements), function(cb) {cb.checked=checked;});
+    jQuery('input:checkbox[name^=' + nameprefix + ']').each(function() {
+        if (value == null || this.value == value) {
+            this.checked = checked;
+        }
+    });
 }
 
-function setCheckboxesState2(nameprefix, value, checked){
-    // XXX: this looks in *all* the document for inputs
-    var elements = getElementsMatching('input', {'type': "checkbox"});
-    filterfunc = function(cb) { return nameprefix && cb.name.startsWith(nameprefix) && cb.value == value; };
-    forEach(filter(filterfunc, elements), function(cb) {cb.checked=checked;});
-}
-
-
-/* this function is a hack to build a dom node from html source */
+/**
+ * .. function:: html2dom(source)
+ *
+ * this function is a hack to build a dom node from html source
+ */
 function html2dom(source) {
     var tmpNode = SPAN();
     tmpNode.innerHTML = source;
     if (tmpNode.childNodes.length == 1) {
-	return tmpNode.firstChild;
+        return tmpNode.firstChild;
     }
     else {
-	// we leave the span node when `source` has no root node
-	// XXX This is cleary not the best solution, but css/html-wise,
-	///    a span not should not be too  much disturbing
-	return tmpNode;
+        // we leave the span node when `source` has no root node
+        // XXX This is cleary not the best solution, but css/html-wise,
+        ///    a span not should not be too  much disturbing
+        return tmpNode;
     }
 }
 
-
 // *** HELPERS **************************************************** //
-function rql_for_eid(eid) { return 'Any X WHERE X eid ' + eid; }
-function isTextNode(domNode) { return domNode.nodeType == 3; }
-function isElementNode(domNode) { return domNode.nodeType == 1; }
+function rql_for_eid(eid) {
+    return 'Any X WHERE X eid ' + eid;
+}
+function isTextNode(domNode) {
+    return domNode.nodeType == 3;
+}
+function isElementNode(domNode) {
+    return domNode.nodeType == 1;
+}
 
 function autogrow(area) {
-    if (area.scrollHeight > area.clientHeight && !window.opera) {
-	if (area.rows < 20) {
-	    area.rows += 2;
-	}
+    if (area.scrollHeight > area.clientHeight && ! window.opera) {
+        if (area.rows < 20) {
+            area.rows += 2;
+        }
     }
 }
 //============= page loading events ==========================================//
-
-CubicWeb.rounded = [
-		    ['div.sideBoxBody', 'bottom 6px'],
-		    ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']
-		    ];
+cw.rounded = [['div.sideBoxBody', 'bottom 6px'],
+              ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']];
 
 function roundedCorners(node) {
-    node = jQuery(node);
-    for(var r=0; r < CubicWeb.rounded.length; r++) {
-       node.find(CubicWeb.rounded[r][0]).corner(CubicWeb.rounded[r][1]);
+    if (jQuery.fn.corner !== undefined) {
+        node = jQuery(node);
+        for (var r = 0; r < cw.rounded.length; r++) {
+            node.find(cw.rounded[r][0]).corner(cw.rounded[r][1]);
+        }
     }
 }
 
-jQuery(document).ready(function () {roundedCorners(this.body);});
-
-CubicWeb.provide('corners.js');
-
-CubicWeb.provide('htmlhelpers.js');
+jQuery(document).ready(function() {
+    roundedCorners(this.body);
+});
--- a/web/data/cubicweb.ie.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.ie.css	Mon Jul 19 15:37:02 2010 +0200
@@ -4,8 +4,20 @@
   margin-top: 0px;
 }
 
-/* quick and dirty solution for pop to be 
+/* quick and dirty solution for pop to be
    correctly displayed on right edge of window */
-div.popupWrapper{ 
+div.popupWrapper{
   direction:rtl;
 }
+
+div#rqlinput input.rqlsubmit{
+  height: 24px;
+  width: 24px;
+}
+
+
+table#mainLayout #navColumnLeft,
+table#mainLayout #navColumnRight {
+  width: auto;
+}
+
--- a/web/data/cubicweb.iprogress.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.iprogress.css	Mon Jul 19 15:37:02 2010 +0200
@@ -8,11 +8,11 @@
 /* progressbar                                                                */
 /******************************************************************************/
 
-.done{ background:red }
+.done { background:red }
 
-.inprogress{ background:green }
+.inprogress { background:green }
 
-.overpassed{ background: yellow}
+.overpassed { background: yellow}
 
 
 canvas.progressbar {
@@ -20,64 +20,59 @@
 }
 
 .progressbarback {
- border: 1px solid #000000;
- background: transparent;
- height: 10px;
- width: 100px;
+  border: 1px solid #000000;
+  background: transparent;
+  height: 10px;
+  width: 100px;
 }
 
 /******************************************************************************/
 /* progress table                                                             */
 /******************************************************************************/
 
-table.progress{
+table.progress {
  /* The default table view */
- margin: 10px 0px;
- color : #000;
- width:100%;
- font-size:98%;
- border:2px solid #ebe8d9;
+  margin: 10px 0px 1em;
+  width: 100%;
+  font-size: 0.9167em;
 }
 
-table.progress th{
- text-align:left;
- white-space:nowrap;
- font-weight : bold;
- background:#ebe8d9 url("button.png") repeat-x;
- padding:2px 3px;
+table.progress th {
+  white-space: nowrap;
+  font-weight: bold;
+  background: %(listingHeaderBgColor)s;
+  padding: 2px 4px;
+  font-size:8pt;
 }
 
 table.progress th,
-table.progress td{
- border: 1px solid #dedede;
- margin:0px;
+table.progress td {
+  border: 1px solid %(listingBorderColor)s;
 }
 
-table.progress td{
- text-align:right;
- padding:2px 5px 2px 2px;
+table.progress td {
+  text-align: right;
+  padding: 2px 3px;
 }
 
 table.progress th.tdleft,
-table.progress td.tdleft{
- text-align:left;
- padding:2px 3px 2px 5px;
+table.progress td.tdleft {
+  text-align: left;
+  padding: 2px 3px 2px 5px;
 }
 
-
-table.progress tr.highlighted{
- background-color: #f4f5ed;
+table.progress tr.highlighted {
+  background-color: %(listingHihligthedBgColor)s;
 }
 
 table.progress tr.highlighted .progressbarback {
- border: 1px solid #555;
+  border: 1px solid %(listingHihligthedBgColor)s;
 }
 
 table.progress .progressbarback {
- border: 1px solid #777;
+  border: 1px solid #777;
 }
 
-.progress_data{
- padding-right:3px;
-}
-
+.progress_data {
+  padding-right: 3px;
+}
\ No newline at end of file
--- a/web/data/cubicweb.iprogress.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.iprogress.js	Mon Jul 19 15:37:02 2010 +0200
@@ -6,7 +6,7 @@
     this.color_budget = "blue";
     this.color_todo = "#cccccc"; //  grey
     this.height = 16;
-    this.middle = this.height/2;
+    this.middle = this.height / 2;
     this.radius = 4;
 }
 
@@ -15,14 +15,14 @@
     ctx.lineWidth = 1;
     ctx.strokeStyle = color;
     if (fill) {
-	ctx.fillStyle = color;
-	ctx.fillRect(0,0,pos,this.middle*2);
+        ctx.fillStyle = color;
+        ctx.fillRect(0, 0, pos, this.middle * 2);
     } else {
-	ctx.lineWidth = 2;
-	ctx.strokeStyle = "black";
-	ctx.moveTo(pos,0);
-	ctx.lineTo(pos,this.middle*2);
-	ctx.stroke();
+        ctx.lineWidth = 2;
+        ctx.strokeStyle = "black";
+        ctx.moveTo(pos, 0);
+        ctx.lineTo(pos, this.middle * 2);
+        ctx.stroke();
     }
 };
 
@@ -30,36 +30,34 @@
     ctx.beginPath();
     ctx.lineWidth = 2;
     ctx.strokeStyle = color;
-    ctx.moveTo(0,this.middle);
-    ctx.lineTo(pos,this.middle);
-    ctx.arc(pos,this.middle,this.radius,0,Math.PI*2,true);
+    ctx.moveTo(0, this.middle);
+    ctx.lineTo(pos, this.middle);
+    ctx.arc(pos, this.middle, this.radius, 0, Math.PI * 2, true);
     ctx.stroke();
 };
 
-
 ProgressBar.prototype.draw_circ = function(ctx) {
-    this.draw_one_circ(ctx,this.budget,this.color_budget);
-    this.draw_one_circ(ctx,this.todo,this.color_todo);
-    this.draw_one_circ(ctx,this.done,this.color_done);
+    this.draw_one_circ(ctx, this.budget, this.color_budget);
+    this.draw_one_circ(ctx, this.todo, this.color_todo);
+    this.draw_one_circ(ctx, this.done, this.color_done);
 };
 
-
 ProgressBar.prototype.draw_rect = function(ctx) {
-    this.draw_one_rect(ctx,this.todo,this.color_todo,true);
-    this.draw_one_rect(ctx,this.done,this.color_done,true);
-    this.draw_one_rect(ctx,this.budget,this.color_budget,false);
+    this.draw_one_rect(ctx, this.todo, this.color_todo, true);
+    this.draw_one_rect(ctx, this.done, this.color_done, true);
+    this.draw_one_rect(ctx, this.budget, this.color_budget, false);
 };
 
-
 function draw_progressbar(cid, done, todo, budget, color) {
     var canvas = document.getElementById(cid);
     if (canvas.getContext) {
         var ctx = canvas.getContext("2d");
-	var bar = new ProgressBar();
-	bar.budget = budget;
-	bar.todo = todo;
-	bar.done = done;
+        var bar = new ProgressBar();
+        bar.budget = budget;
+        bar.todo = todo;
+        bar.done = done;
         bar.color_done = color;
-	bar.draw_rect(ctx);
+        bar.draw_rect(ctx);
     }
 }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,436 @@
+
+function Namespace(name) {
+   this.__name__ = name;
+}
+
+cw = new Namespace('cw');
+
+jQuery.extend(cw, {
+    log: function () {
+        var args = [];
+        for (var i = 0; i < arguments.length; i++) {
+            args.push(arguments[i]);
+        }
+        if (typeof(window) != "undefined" && window.console && window.console.log) {
+            window.console.log(args.join(' '));
+        }
+    },
+
+    //removed: getElementsByTagAndClassName, replaceChildNodes, toggleElementClass
+    //         partial, merge, isNotEmpty, update,
+    //         String.in_, String.join, list, getattr, attrgetter, methodcaller,
+    //         min, max, dict, concat
+    jqNode: function (node) {
+    /**
+     * .. function:: jqNode(node)
+     *
+     * safe version of jQuery('#nodeid') because we use ':' in nodeids
+     * which messes with jQuery selection mechanism
+     */
+        if (typeof(node) == 'string') {
+            node = document.getElementById(node);
+        }
+        if (node) {
+            return $(node);
+        }
+        return null;
+    },
+
+    getNode: function (node) {
+        if (typeof(node) == 'string') {
+            return document.getElementById(node);
+        }
+        return node;
+    },
+
+    evalJSON: function (json) { // trust source
+        return eval("(" + json + ")");
+    },
+
+    urlEncode: function (str) {
+        if (typeof(encodeURIComponent) != "undefined") {
+            return encodeURIComponent(str).replace(/\'/g, '%27');
+        } else {
+            return escape(str).replace(/\+/g, '%2B').replace(/\"/g, '%22').
+                    rval.replace(/\'/g, '%27');
+        }
+    },
+
+    swapDOM: function (dest, src) {
+        dest = cw.getNode(dest);
+        var parent = dest.parentNode;
+        if (src) {
+            src = cw.getNode(src);
+            parent.replaceChild(src, dest);
+        } else {
+            parent.removeChild(dest);
+        }
+        return src;
+    }
+});
+
+
+cw.utils = new Namespace('cw.utils');
+jQuery.extend(cw.utils, {
+
+    deprecatedFunction: function (msg, newfunc) {
+        return function () {
+            cw.log(msg);
+            return newfunc.apply(this, arguments);
+        };
+    },
+
+    movedToNamespace: function (funcnames, namespace) {
+        for (var i = 0; i < funcnames.length; i++) {
+            var funcname = funcnames[i];
+            var msg = ('[3.9] ' + funcname + ' is deprecated, use ' +
+		       namespace.__name__ + '.' + funcname + ' instead');
+            window[funcname] = cw.utils.deprecatedFunction(msg, namespace[funcname]);
+        }
+    },
+
+    createDomFunction: function (tag) {
+        function builddom(params, children) {
+            var node = document.createElement(tag);
+            for (key in params) {
+                var value = params[key];
+                if (key.substring(0, 2) == 'on') {
+                    // this is an event handler definition
+                    if (typeof value == 'string') {
+                        // litteral definition
+                        value = new Function(value);
+                    }
+                    node[key] = value;
+                } else { // normal node attribute
+                    jQuery(node).attr(key, params[key]);
+                }
+            }
+            if (children) {
+                if (!cw.utils.isArrayLike(children)) {
+                    children = [children];
+                    for (var i = 2; i < arguments.length; i++) {
+                        var arg = arguments[i];
+                        if (cw.utils.isArray(arg)) {
+                            jQuery.merge(children, arg);
+                        } else {
+                            children.push(arg);
+                        }
+                    }
+                }
+                for (var i = 0; i < children.length; i++) {
+                    var child = children[i];
+                    if (typeof child == "string" || typeof child == "number") {
+                        child = document.createTextNode(child);
+                    }
+                    node.appendChild(child);
+                }
+            }
+            return node;
+        }
+        return builddom;
+    },
+
+    /**
+     * .. function:: toISOTimestamp(date)
+     *
+     */
+    toISOTimestamp: function (date) {
+        if (typeof(date) == "undefined" || date === null) {
+            return null;
+        }
+
+        function _padTwo(n) {
+            return (n > 9) ? n : "0" + n;
+        }
+        var isoTime = [_padTwo(date.getHours()), _padTwo(date.getMinutes()),
+                       _padTwo(date.getSeconds())].join(':');
+        var isoDate = [date.getFullYear(), _padTwo(date.getMonth() + 1),
+                       _padTwo(date.getDate())].join("-");
+        return isoDate + " " + isoTime;
+    },
+
+    /**
+     * .. function:: nodeWalkDepthFirst(node, visitor)
+     *
+     * depth-first implementation of the nodeWalk function found
+     * in `MochiKit.Base <http://mochikit.com/doc/html/MochiKit/Base.html#fn-nodewalk>`_
+     */
+    nodeWalkDepthFirst: function (node, visitor) {
+        var children = visitor(node);
+        if (children) {
+            for (var i = 0; i < children.length; i++) {
+                cw.utils.nodeWalkDepthFirst(children[i], visitor);
+            }
+        }
+    },
+
+    isArray: function (it) { // taken from dojo
+        return it && (it instanceof Array || typeof it == "array");
+    },
+
+    isString: function (it) { // taken from dojo
+        return !!arguments.length && it != null && (typeof it == "string" || it instanceof String);
+    },
+
+    isArrayLike: function (it) { // taken from dojo
+        return (it && it !== undefined &&
+                // keep out built-in constructors (Number, String, ...)
+                // which have length properties
+                !cw.utils.isString(it) && !jQuery.isFunction(it) &&
+                !(it.tagName && it.tagName.toLowerCase() == 'form') &&
+                (cw.utils.isArray(it) || isFinite(it.length)));
+    },
+
+    /**
+     * .. function:: formContents(elem \/* = document.body *\/)
+     *
+     * this implementation comes from MochiKit
+     */
+    formContents: function (elem /* = document.body */ ) {
+        var names = [];
+        var values = [];
+        if (typeof(elem) == "undefined" || elem === null) {
+            elem = document.body;
+        } else {
+            elem = cw.getNode(elem);
+        }
+        cw.utils.nodeWalkDepthFirst(elem, function (elem) {
+            var name = elem.name;
+            if (name && name.length) {
+                var tagName = elem.tagName.toUpperCase();
+                if (tagName === "INPUT" && (elem.type == "radio" || elem.type == "checkbox") && !elem.checked) {
+                    return null;
+                }
+                if (tagName === "SELECT") {
+                    if (elem.type == "select-one") {
+                        if (elem.selectedIndex >= 0) {
+                            var opt = elem.options[elem.selectedIndex];
+                            var v = opt.value;
+                            if (!v) {
+                                var h = opt.outerHTML;
+                                // internet explorer sure does suck.
+                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
+                                    v = opt.text;
+                                }
+                            }
+                            names.push(name);
+                            values.push(v);
+                            return null;
+                        }
+                        // no form elements?
+                        names.push(name);
+                        values.push("");
+                        return null;
+                    } else {
+                        var opts = elem.options;
+                        if (!opts.length) {
+                            names.push(name);
+                            values.push("");
+                            return null;
+                        }
+                        for (var i = 0; i < opts.length; i++) {
+                            var opt = opts[i];
+                            if (!opt.selected) {
+                                continue;
+                            }
+                            var v = opt.value;
+                            if (!v) {
+                                var h = opt.outerHTML;
+                                // internet explorer sure does suck.
+                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
+                                    v = opt.text;
+                                }
+                            }
+                            names.push(name);
+                            values.push(v);
+                        }
+                        return null;
+                    }
+                }
+                if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" || tagName === "DIV") {
+                    return elem.childNodes;
+                }
+		var value = elem.value;
+		if (tagName === "TEXTAREA") {
+		    if (typeof(FCKeditor) != 'undefined') {
+			var fck = FCKeditorAPI.GetInstance(elem.id);
+			if (fck) {
+			    value = fck.GetHTML();
+			}
+		    }
+		}
+                names.push(name);
+                values.push(value || '');
+                return null;
+            }
+            return elem.childNodes;
+        });
+        return [names, values];
+    },
+
+    /**
+     * .. function:: sliceList(lst, start, stop, step)
+     *
+     * returns a subslice of `lst` using `start`/`stop`/`step`
+     * start, stop might be negative
+     *
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2)
+     * ['c', 'd', 'e', 'f']
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2, -2)
+     * ['c', 'd']
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], -3)
+     * ['d', 'e', 'f']
+     */
+    sliceList: function (lst, start, stop, step) {
+        start = start || 0;
+        stop = stop || lst.length;
+        step = step || 1;
+        if (stop < 0) {
+            stop = Math.max(lst.length + stop, 0);
+        }
+        if (start < 0) {
+            start = Math.min(lst.length + start, lst.length);
+        }
+        var result = [];
+        for (var i = start; i < stop; i += step) {
+            result.push(lst[i]);
+        }
+        return result;
+    },
+
+    /**
+     * .. function:: domid(string)
+     *
+     * return a valid DOM id from a string (should also be usable in jQuery
+     * search expression...). This is the javascript implementation of
+     * :func:`cubicweb.uilib.domid`.
+     */
+    domid: function (string) {
+	var newstring = string.replace(".", "_").replace("-", "_");
+	while (newstring != string) {
+	    string = newstring;
+	    newstring = newstring.replace(".", "_").replace("-", "_");
+	}
+	return newstring; // XXX
+    },
+
+    /**
+     * .. function:: strFuncCall(fname, *args)
+     *
+     * return a string suitable to call the `fname` javascript function with the
+     * given arguments (which should be correctly typed).. This is providing
+     * javascript implementation equivalent to :func:`cubicweb.uilib.js`.
+     */
+    strFuncCall: function(fname /* ...*/) {
+	    return (fname + '(' +
+		    $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON).join(',')
+		    + ')'
+		    );
+    }
+
+});
+
+String.prototype.startsWith = cw.utils.deprecatedFunction('[3.9] str.startsWith() is deprecated, use str.startswith() instead', function (prefix) {
+    return this.startswith(prefix);
+});
+
+String.prototype.endsWith = cw.utils.deprecatedFunction('[3.9] str.endsWith() is deprecated, use str.endswith() instead', function (suffix) {
+    return this.endswith(prefix);
+});
+
+/** DOM factories ************************************************************/
+A = cw.utils.createDomFunction('a');
+BUTTON = cw.utils.createDomFunction('button');
+BR = cw.utils.createDomFunction('br');
+CANVAS = cw.utils.createDomFunction('canvas');
+DD = cw.utils.createDomFunction('dd');
+DIV = cw.utils.createDomFunction('div');
+DL = cw.utils.createDomFunction('dl');
+DT = cw.utils.createDomFunction('dt');
+FIELDSET = cw.utils.createDomFunction('fieldset');
+FORM = cw.utils.createDomFunction('form');
+H1 = cw.utils.createDomFunction('H1');
+H2 = cw.utils.createDomFunction('H2');
+H3 = cw.utils.createDomFunction('H3');
+H4 = cw.utils.createDomFunction('H4');
+H5 = cw.utils.createDomFunction('H5');
+H6 = cw.utils.createDomFunction('H6');
+HR = cw.utils.createDomFunction('hr');
+IMG = cw.utils.createDomFunction('img');
+INPUT = cw.utils.createDomFunction('input');
+LABEL = cw.utils.createDomFunction('label');
+LEGEND = cw.utils.createDomFunction('legend');
+LI = cw.utils.createDomFunction('li');
+OL = cw.utils.createDomFunction('ol');
+OPTGROUP = cw.utils.createDomFunction('optgroup');
+OPTION = cw.utils.createDomFunction('option');
+P = cw.utils.createDomFunction('p');
+PRE = cw.utils.createDomFunction('pre');
+SELECT = cw.utils.createDomFunction('select');
+SPAN = cw.utils.createDomFunction('span');
+STRONG = cw.utils.createDomFunction('strong');
+TABLE = cw.utils.createDomFunction('table');
+TBODY = cw.utils.createDomFunction('tbody');
+TD = cw.utils.createDomFunction('td');
+TEXTAREA = cw.utils.createDomFunction('textarea');
+TFOOT = cw.utils.createDomFunction('tfoot');
+TH = cw.utils.createDomFunction('th');
+THEAD = cw.utils.createDomFunction('thead');
+TR = cw.utils.createDomFunction('tr');
+TT = cw.utils.createDomFunction('tt');
+UL = cw.utils.createDomFunction('ul');
+
+// cubicweb specific
+//IFRAME = cw.utils.createDomFunction('iframe');
+
+
+function IFRAME(params) {
+    if ('name' in params) {
+        try {
+            var node = document.createElement('<iframe name="' + params['name'] + '">');
+        } catch (ex) {
+            var node = document.createElement('iframe');
+            node.id = node.name = params.name;
+        }
+    }
+    else {
+        var node = document.createElement('iframe');
+    }
+    for (key in params) {
+        if (key != 'name') {
+            var value = params[key];
+            if (key.substring(0, 2) == 'on') {
+                // this is an event handler definition
+                if (typeof value == 'string') {
+                    // litteral definition
+                    value = new Function(value);
+                }
+                node[key] = value;
+            } else { // normal node attribute
+                node.setAttribute(key, params[key]);
+            }
+        }
+    }
+    return node;
+}
+
+// XXX avoid crashes / backward compat
+CubicWeb = {
+    require: cw.utils.deprecatedFunction(
+        '[3.9] CubicWeb.require() is not used anymore',
+        function(module) {}),
+    provide: cw.utils.deprecatedFunction(
+        '[3.9] CubicWeb.provide() is not used anymore',
+        function(module) {})
+};
+
+jQuery(document).ready(function() {
+    jQuery(CubicWeb).trigger('server-response', [false, document]);
+    jQuery(cw).trigger('server-response', [false, document]);
+});
+
+// XXX as of 2010-04-07, no known cube uses this
+jQuery(CubicWeb).bind('ajax-loaded', function() {
+    log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
+    jQuery(cw).trigger('server-response', [false, document]);
+});
--- a/web/data/cubicweb.lazy.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.lazy.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,10 +1,9 @@
-
 function load_now(eltsel, holesel, reloadable) {
     var lazydiv = jQuery(eltsel);
     var hole = lazydiv.children(holesel);
-    if ((hole.length == 0) && !reloadable) {
-	/* the hole is already filled */
-	return;
+    if ((hole.length == 0) && ! reloadable) {
+        /* the hole is already filled */
+        return;
     }
     lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
 }
@@ -12,3 +11,4 @@
 function trigger_load(divid) {
     jQuery('#lazy-' + divid).trigger('load_' + divid);
 }
+
--- a/web/data/cubicweb.login.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.login.css	Mon Jul 19 15:37:02 2010 +0200
@@ -11,7 +11,7 @@
   right: 0px;
   width: 26em;
   padding: 0px 1px 1px;
-  background: #E4EAD8;
+  background: %(listingBorderColor)s; 
 }
 
 div#popupLoginBox label{
@@ -30,13 +30,13 @@
   margin-left: -14em;
   width: 28em;
   background: #fff;
-  border: 2px solid #cfceb7;
+  border: 2px solid %(actionBoxTitleBgColor)s;
   padding-bottom: 0.5em;
   text-align: center;
 }
 
 div#loginBox h1 {
-  color: #FF7700;
+  color: %(aColor)s;
   font-size: 140%;
 }
 
@@ -46,7 +46,7 @@
   font-size: 140%;
   text-align: center;
   padding: 3px 0px;
-  background: #ff7700 url("banner.png") left top repeat-x;
+  background: %(headerBgColor)s url("banner.png") repeat-x top left;
 }
 
 div#loginBox div#loginContent form {
@@ -80,7 +80,7 @@
 
 .loginButton {
   border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
+  border-color: #edecd2 %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s  #edecd2;
   margin: 2px 0px 0px;
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
 }
--- a/web/data/cubicweb.manageview.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.manageview.css	Mon Jul 19 15:37:02 2010 +0200
@@ -6,9 +6,9 @@
   width: 100%;
 }
 
-table.startup td {
-  padding: 0.1em 0.2em;
-}
+/* table.startup td { */
+/*   padding: 0.1em 0.2em; */
+/* } */
 
 table.startup td.addcol {
   text-align: right;
@@ -16,7 +16,5 @@
 }
 
 table.startup th{
-  padding-top: 3px;
-  padding-bottom: 3px;
   text-align: left;
 }
--- a/web/data/cubicweb.massmailing.js	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-
-function insertText(text, areaId) {
-    var textarea = jQuery('#' + areaId);
-    if (document.selection) { // IE
-        var selLength;
-        textarea.focus();
-        sel = document.selection.createRange();
-        selLength = sel.text.length;
-        sel.text = text;
-        sel.moveStart('character', selLength-text.length);
-        sel.select();
-    } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla
-        var startPos = textarea.selectionStart;
-        var endPos = textarea.selectionEnd;
-	// insert text so that it replaces the [startPos, endPos] part
-        textarea.value = textarea.value.substring(0,startPos) + text + textarea.value.substring(endPos,textarea.value.length);
-	// set cursor pos at the end of the inserted text
-        textarea.selectionStart = textarea.selectionEnd = startPos+text.length;
-        textarea.focus();
-    } else { // safety belt for other browsers
-        textarea.value += text;
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.old.css	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,862 @@
+/*
+ *  :organization: Logilab
+ *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ */
+
+/***************************************/
+/* xhtml tags                          */
+/***************************************/
+* {
+  margin: 0px;
+  padding: 0px;
+}
+
+html, body {
+  background: #e2e2e2;
+}
+
+body {
+  font-size: 69%;
+  font-weight: normal;
+  font-family: Verdana, sans-serif;
+}
+
+h1 {
+  font-size: 188%;
+  margin: 0.2em 0px 0.3em;
+  border-bottom: 1px solid #000;
+}
+
+h2, h3 {
+  margin-top: 0.2em;
+  margin-bottom: 0.3em;
+}
+
+h2 {
+  font-size: 135%;
+}
+
+h3 {
+  font-size: 130%;
+}
+
+h4 {
+  font-size: 120%;
+  margin: 0.2em 0px;
+}
+
+h5 {
+  font-size:110%;
+}
+
+h6{
+  font-size:105%;
+}
+
+a, a:active, a:visited, a:link {
+  color: #ff4500;
+  text-decoration: none;
+}
+
+a:hover{
+  text-decoration: underline;
+}
+
+a img, img {
+  border: none;
+  text-align: center;
+}
+
+p {
+  margin: 0em 0px 0.2em;
+  padding-top: 2px;
+}
+
+table, td, input, select{
+  font-size: 100%;
+}
+
+table {
+  border-collapse: collapse;
+  border: none;
+}
+
+table th, table td {
+  vertical-align: top;
+}
+
+table td img {
+  vertical-align: middle;
+  margin-right: 10px;
+}
+
+ol {
+  margin: 1px 0px 1px 16px;
+}
+
+ul{
+  margin: 1px 0px 1px 4px;
+  list-style-type: none;
+}
+
+ul li {
+  margin-top: 2px;
+  padding: 0px 0px 2px 8px;
+  background: url("bullet_orange.png") 0% 6px no-repeat;
+}
+
+dt {
+  font-size:1.17em;
+  font-weight:600;
+}
+
+dd {
+  margin: 0.6em 0 1.5em 2em;
+}
+
+fieldset {
+  border: none;
+}
+
+legend {
+  padding: 0px 2px;
+  font: bold 1em Verdana, sans-serif;
+}
+
+input, textarea {
+  padding: 0.2em;
+  vertical-align: middle;
+  border: 1px solid #ccc;
+}
+
+input:focus {
+  border: 1px inset #ff7700;
+}
+
+label, .label {
+  font-weight: bold;
+}
+
+iframe {
+  border: 0px;
+}
+
+pre {
+  font-family: Courier, "Courier New", Monaco, monospace;
+  font-size: 100%;
+  color: #000;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+}
+
+code {
+  font-size: 120%;
+  color: #000;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+}
+
+blockquote {
+  font-family: Courier, "Courier New", serif;
+  font-size: 120%;
+  margin: 5px 0px;
+  padding: 0.8em;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+}
+
+/***************************************/
+/* generic classes                     */
+/***************************************/
+
+.odd {
+  background-color: #f7f6f1;
+}
+
+.even {
+  background-color: transparent;
+}
+
+.hr {
+  border-bottom: 1px dotted #ccc;
+  margin: 1em 0px;
+}
+
+.left {
+  float: left;
+}
+
+.right {
+  float: right;
+}
+
+.clear {
+  clear: both;
+}
+
+.hidden {
+  display: none;
+  visibility: hidden;
+}
+
+li.invisible { list-style: none; background: none; padding: 0px 0px
+1px 1px; }
+
+li.invisible div{
+  display: inline;
+}
+
+
+/***************************************/
+/*   LAYOUT                            */
+/***************************************/
+
+/* header */
+
+table#header {
+  background: #ff7700 url("banner.png") left top repeat-x;
+  text-align: left;
+}
+
+table#header td {
+  vertical-align: middle;
+}
+
+table#header a {
+color: #000;
+}
+
+span#appliName {
+ font-weight: bold;
+ color: #000;
+ white-space: nowrap;
+}
+
+table#header td#headtext {
+  width: 100%;
+}
+
+/* FIXME appear with 4px width in IE6 */
+div#stateheader{
+  min-width: 66%;
+}
+
+/* Popup on login box and userActionBox */
+div.popupWrapper{
+ position:relative;
+ z-index:100;
+}
+
+div.popup {
+  position: absolute;
+  background: #fff;
+  border: 1px solid black;
+  text-align: left;
+  z-index:400;
+}
+
+div.popup ul li a {
+  text-decoration: none;
+  color: black;
+}
+
+/* main zone */
+
+div#page {
+  background: #e2e2e2;
+  position: relative;
+  min-height: 800px;
+}
+
+table#mainLayout{
+ margin:0px 3px;
+}
+
+table#mainLayout td#contentColumn {
+  padding: 8px 10px 5px;
+}
+
+table#mainLayout td#navColumnLeft,
+table#mainLayout td#navColumnRight {
+  width: 16em;
+}
+
+#contentheader {
+  margin: 0px;
+  padding: 0.2em 0.5em 0.5em 0.5em;
+}
+
+#contentheader a {
+  color: #000;
+}
+
+div#pageContent {
+  clear: both;
+  padding: 10px 1em 2em;
+  background: #ffffff;
+  border: 1px solid #ccc;
+}
+
+/* rql bar */
+
+div#rqlinput {
+  border: 1px solid #cfceb7;
+  margin-bottom: 8px;
+  padding: 3px;
+  background: #cfceb7;
+}
+
+input#rql{
+  width: 95%;
+}
+
+/* boxes */
+div.navboxes {
+ margin-top: 8px;
+}
+
+div.boxFrame {
+  width: 100%;
+}
+
+div.boxTitle {
+  padding-top: 0px;
+  padding-bottom: 0.2em;
+  font: bold 100% Georgia;
+  overflow: hidden;
+  color: #fff;
+  background: #ff9900 url("search.png") left bottom repeat-x;
+}
+
+div.searchBoxFrame div.boxTitle,
+div.greyBoxFrame div.boxTitle {
+  background: #cfceb7;
+}
+
+div.boxTitle span,
+div.sideBoxTitle span {
+  padding: 0px 5px;
+  white-space: nowrap;
+}
+
+div.sideBoxTitle span,
+div.searchBoxFrame div.boxTitle span,
+div.greyBoxFrame div.boxTitle span {
+  color: #222211;
+}
+
+.boxFrame a {
+  color: #000;
+}
+
+div.boxContent {
+  padding: 3px 0px;
+  background: #fff;
+  border-top: none;
+}
+
+ul.boxListing {
+  margin: 0px;
+  padding: 0px 3px;
+}
+
+ul.boxListing li,
+ul.boxListing ul li {
+  display: inline;
+  margin: 0px;
+  padding: 0px;
+  background-image: none;
+}
+
+ul.boxListing ul {
+  margin: 0px 0px 0px 7px;
+  padding: 1px 3px;
+}
+
+ul.boxListing a {
+  color: #000;
+  display: block;
+  padding: 1px 9px 1px 3px;
+}
+
+ul.boxListing .selected {
+  color: #FF4500;
+  font-weight: bold;
+}
+
+ul.boxListing a.boxBookmark:hover,
+ul.boxListing a:hover,
+ul.boxListing ul li a:hover {
+  text-decoration: none;
+  background: #eeedd9;
+  color: #111100;
+}
+
+ul.boxListing a.boxMenu:hover {
+                                background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px;
+                                cursor:pointer;
+                                border-top:medium none;
+                                }
+a.boxMenu {
+  background: transparent url("puce_down.png") 98% 6px no-repeat;
+  display: block;
+  padding: 1px 9px 1px 3px;
+}
+
+
+a.popupMenu {
+  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+  padding-left: 2em;
+}
+
+ul.boxListing ul li a:hover {
+  background: #eeedd9  url("bullet_orange.png") 0% 6px no-repeat;
+}
+
+a.boxMenu:hover {
+  background: #eeedd9 url("puce_down.png") 98% 6px no-repeat;
+  cursor: pointer;
+}
+
+ul.boxListing a.boxBookmark {
+  padding-left: 3px;
+  background-image:none;
+  background:#fff;
+}
+
+ul.boxListing ul li a {
+  background: #fff url("bullet_orange.png") 0% 6px no-repeat;
+  padding: 1px 3px 0px 10px;
+}
+
+div.searchBoxFrame div.boxContent {
+  padding: 4px 4px 3px;
+  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
+}
+
+div.shadow{
+  height: 14px;
+  background: url("shadow.gif") no-repeat top right;
+}
+
+div.sideBoxTitle {
+  background: #cfceb7;
+  display: block;
+  font: bold 100% Georgia;
+}
+
+div.sideBox {
+  padding: 0 0 0.2em;
+  margin-bottom: 0.5em;
+}
+
+ul.sideBox li{
+ list-style: none;
+ background: none;
+ padding: 0px 0px 1px 1px;
+ }
+
+div.sideBoxBody {
+  padding: 0.2em 5px;
+  background: #eeedd9;
+}
+
+div.sideBoxBody a {
+  color:#555544;
+}
+
+div.sideBoxBody a:hover {
+  text-decoration: underline;
+}
+
+div.sideBox table td {
+  padding-right: 1em;
+}
+
+input.rqlsubmit{
+  background: #fffff8 url("go.png") 50% 50% no-repeat;
+  width: 20px;
+  height: 20px;
+  margin: 0px;
+}
+
+input#norql{
+  width:13em;
+  margin-right: 2px;
+}
+
+/* user actions menu */
+a.logout, a.logout:visited, a.logout:hover{
+  color: #fff;
+  text-decoration: none;
+}
+
+div#userActionsBox {
+  width: 14em;
+  text-align: right;
+}
+
+div#userActionsBox a.popupMenu {
+  color: black;
+  text-decoration: underline;
+  padding-right: 2em;
+}
+
+/* download box XXX move to its own file? */
+div.downloadBoxTitle{
+ background : #8FBC8F;
+ font-weight: bold;
+}
+
+div.downloadBox{
+ font-weight: bold;
+}
+
+div.downloadBox div.sideBoxBody{
+ background : #EEFED9;
+}
+
+/**************/
+/* navigation */
+/**************/
+div#etyperestriction {
+  margin-bottom: 1ex;
+  border-bottom: 1px solid #ccc;
+}
+
+span.slice a:visited,
+span.slice a:hover{
+  color: #555544;
+}
+
+span.selectedSlice a:visited,
+span.selectedSlice a {
+  color: #000;
+}
+
+/* FIXME should be moved to cubes/folder */
+div.navigation a {
+  text-align: center;
+  text-decoration: none;
+}
+
+div.prevnext {
+  width: 100%;
+  margin-bottom: 1em;
+}
+
+div.prevnext a {
+  color: #000;
+}
+
+/***************************************/
+/* entity views                        */
+/***************************************/
+
+.mainInfo  {
+  margin-right: 1em;
+  padding: 0.2em;
+}
+
+
+div.mainRelated {
+  border: none;
+  margin-right: 1em;
+  padding: 0.5em 0.2em 0.2em;
+}
+
+div.primaryRight{
+ }
+
+div.metadata {
+  font-size: 90%;
+  margin: 5px 0px 3px;
+  color: #666;
+  font-style: italic;
+  text-align: right;
+}
+
+div.section {
+  margin-top: 0.5em;
+  width:100%;
+}
+
+div.section a:hover {
+  text-decoration: none;
+}
+
+/* basic entity view */
+
+tr.entityfield th {
+  text-align: left;
+  padding-right: 0.5em;
+}
+
+div.field {
+  display: inline;
+}
+
+div.ctxtoolbar {
+  float: right;
+  padding-left: 24px;
+  position: relative;
+}
+div.toolbarButton {
+  display: inline;
+}
+
+/***************************************/
+/* messages                            */
+/***************************************/
+
+.warning,
+.message,
+.errorMessage ,
+.searchMessage{
+  padding: 0.3em 0.3em 0.3em 1em;
+  font-weight: bold;
+}
+
+.loginMessage {
+  margin: 4px 0px;
+  font-weight: bold;
+  color: #ff7700;
+}
+
+div#appMsg, div.appMsg{
+  border: 1px solid #cfceb7;
+  margin-bottom: 8px;
+  padding: 3px;
+  background: #f8f8ee;
+}
+
+.message {
+  margin: 0px;
+  background: #f8f8ee url("information.png") 5px center no-repeat;
+  padding-left: 15px;
+}
+
+.errorMessage {
+  margin: 10px 0px;
+  padding-left: 25px;
+  background: #f7f6f1 url("critical.png") 2px center no-repeat;
+  color: #ed0d0d;
+  border: 1px solid #cfceb7;
+}
+
+.searchMessage {
+  margin-top: 0.5em;
+  border-top: 1px solid #cfceb7;
+  background: #eeedd9 url("information.png") 0% 50% no-repeat; /*dcdbc7*/
+}
+
+.stateMessage {
+  border: 1px solid #ccc;
+  background: #f8f8ee url("information.png") 10px 50% no-repeat;
+  padding:4px 0px 4px 20px;
+  border-width: 1px 0px 1px 0px;
+}
+
+/* warning messages like "There are too many results ..." */
+.warning {
+  padding-left: 25px;
+  background: #f2f2f2 url("critical.png") 3px 50% no-repeat;
+}
+
+/* label shown in the top-right hand corner during form validation */
+div#progress {
+  position: fixed;
+  right: 5px;
+  top: 0px;
+  background: #222211;
+  color: white;
+  font-weight: bold;
+  display: none;
+}
+
+/***************************************/
+/* listing table                       */
+/***************************************/
+
+table.listing {
+ padding: 10px 0em;
+ color: #000;
+ width: 100%;
+ border-right: 1px solid #dfdfdf;
+}
+
+
+table.listing thead th.over {
+  background-color: #746B6B;
+  cursor: pointer;
+}
+
+table.listing tr th {
+  border: 1px solid #dfdfdf;
+  border-right:none;
+  font-size: 8pt;
+  padding: 4px;
+}
+
+table.listing tr .header {
+  border-right: 1px solid #dfdfdf;
+  cursor: pointer;
+}
+
+table.listing td {
+  color: #3D3D3D;
+  padding: 4px;
+  background-color: #FFF;
+  vertical-align: top;
+}
+
+table.listing th,
+table.listing td {
+  padding: 3px 0px 3px 5px;
+  border: 1px solid #dfdfdf;
+  border-right: none;
+}
+
+table.listing th {
+  font-weight: bold;
+  background: #ebe8d9 url("button.png") repeat-x;
+}
+
+table.listing td a,
+table.listing td a:visited {
+  color: #666;
+}
+
+table.listing a:hover,
+table.listing tr.highlighted td a {
+  color:#000;
+}
+
+table.listing td.top {
+  border: 1px solid white;
+  border-bottom: none;
+  text-align: right ! important;
+  /* insane IE row bug workaround */
+  position: relative;
+  left: -1px;
+  top: -1px;
+}
+
+table.htableForm {
+  vertical-align: middle;
+}
+table.htableForm td{
+  padding-left: 1em;
+  padding-top: 0.5em;
+}
+table.htableForm th{
+  padding-left: 1em;
+}
+table.htableForm .validateButton {
+  margin-right: 0.2em;
+  vertical-align: top;
+  margin-bottom: 0.2em; /* because vertical-align doesn't seems to have any effect */
+}
+
+/***************************************/
+/* error view (views/management.py)    */
+/***************************************/
+
+div.pycontext { /* html traceback */
+  font-family: Verdana, sans-serif;
+  font-size: 80%;
+  padding: 1em;
+  margin: 10px 0px 5px 20px;
+  background-color: #dee7ec;
+}
+
+div.pycontext span.name {
+  color: #ff0000;
+}
+
+
+/***************************************/
+/* addcombobox                         */
+/***************************************/
+
+input#newopt{
+ width:120px ;
+ display:block;
+ float:left;
+ }
+
+div#newvalue{
+ margin-top:2px;
+ }
+
+#add_newopt{
+ background: #fffff8 url("go.png") 50% 50% no-repeat;
+ width: 20px;
+ line-height: 20px;
+ display:block;
+ float:left;
+}
+
+/***************************************/
+/* buttons                             */
+/***************************************/
+
+input.button{
+  margin: 1em 1em 0px 0px;
+  border: 1px solid #edecd2;
+  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
+  background: #fffff8 url("button.png") bottom left repeat-x;
+}
+
+/* FileItemInnerView  jquery.treeview.css */
+.folder {
+  /* disable odd/even under folder class */
+  background-color: transparent;
+}
+
+/***************************************/
+/* footer                              */
+/***************************************/
+
+div#footer {
+  text-align: center;
+}
+div#footer a {
+  color: #000;
+  text-decoration: none;
+}
+
+
+/****************************************/
+/* FIXME must by managed by cubes       */
+/****************************************/
+.needsvalidation {
+  font-style: italic;
+  color: gray;
+}
+
+
+/***************************************/
+/* FIXME : Deprecated ? entity view ?  */
+/***************************************/
+.title {
+  text-align: left;
+  font-size:  large;
+  font-weight: bold;
+}
+
+.validateButton {
+  margin: 1em 1em 0px 0px;
+  border: 1px solid #edecd2;
+  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
+  background: #fffff8 url("button.png") bottom left repeat-x;
+}
+
+/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+  float: right;
+}
--- a/web/data/cubicweb.preferences.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.preferences.css	Mon Jul 19 15:37:02 2010 +0200
@@ -5,106 +5,90 @@
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
+div.propertiesform {
+  margin-bottom: 1.2857em;
+  line-height: 1.2857em;
+  font-size: %(h3FontSize)s;
+}
 
-.preferences .validateButton{
- margin-top:0px;
+div.propertiesform a {
+  display: block;
+  margin: 10px 0px 6px 0px;
+  padding-left: 16px;
+  font-weight: bold;
+  color: #000;
+  background: transparent url("puce_down.png") 3px center no-repeat;
+  text-decoration:none;
+}
+
+div.propertiesform a:hover {
+  background-color: %(listingHeaderBgColor)s;
+}
+
+.preferences .validateButton {
+  margin-top: 0px;
  }
 
-fieldset.preferences{
- border : 1px solid #CFCEB7;
- margin:7px 1em 0;
- padding:2px 6px 6px;
+fieldset.preferences {
+  margin: 7px 1em 0;
+  padding: 2px 6px 6px;
+  border : 1px solid %(pageContentBorderColor)s;
 }
 
 div.component {
- margin-left: 1em;
-}
-
-div.componentLink{
- margin-top:0.3em;
- }
-
-a.componentTitle{
- font-weight:bold;
- color: #000/*#0083AB;*/
- }
-
-a.componentTitle:visited{
- color: #000;
+  margin: 0 0 1em 16px;
 }
 
-h2.propertiesform a{
- display:block;
- margin: 10px 0px 6px 0px;
+a.componentTitle {
  font-weight: bold;
- color: #000;
- padding: 0.2em 0.2em 0.2em 16px;
- background:#eeedd9 url("puce_down.png") 3px center no-repeat;
- font-size:89%;
+ color: #000
 }
 
-h2.propertiesform a:hover{
- background-color:#cfceb7;
-}
-
-h2.propertiesform a:hover,
-h2.propertiesform a:visited{
- text-decoration:none;
- color: #000;
+a.componentTitle:visited {
+  color: #000;
 }
 
 div.preffield {
- margin-bottom: 5px;
- padding:2px 5px;
- background:#eeedd9;
+  margin-bottom: 5px;
+  padding: 2px 5px;
+  background: %(listingHeaderBgColor)s;
 }
 
-div.prefinput{
- margin:.3em;
+div.prefinput {
+  margin: .3em;
 }
 
-
 div.prefinput select.changed,
-div.prefinput input.changed{
- border: 1px solid #000;
- font-weight:bold;
-
-}
-
-div.prefinput select,
-div.prefinput input{
- background:#fff;
- border: 1px solid #CFCEB7;
+div.prefinput input.changed {
+  border: 1px solid #000;
+  font-weight: bold;
 }
 
 .prefinput input.error {
- /* background:#fff url(error.png) no-repeat scroll 100% 50% !important; */
- border:1px solid red !important;
- color:red;
- padding-right:1em;
+  border:1px solid red !important;
+  color:red;
+  padding-right:1em;
 }
 
-
-div.formsg{
- font-weight:bold;
- margin:0.5em 0px;
+div.formsg {
+  font-weight: bold;
+  margin: 0.5em 0px;
 }
 
-
-div.critical{
- color:red;
- padding-left:20px;
- background:#fff url(critical.png) no-repeat;
+div.critical {
+  color: red;
+  padding-left: 20px;
+  background: #fff url(critical.png) no-repeat;
  }
 
-div.formsg .msg{
- color : green;
+div.formsg .msg {
+  color: green;
 }
 
-.helper{
+.helper {
   font-size: 96%;
-  color: #555544;
-  padding:0;
+  color: %(helperColor)s;
+  padding: 0;
 }
 
 div.prefinput .helper:hover {
@@ -112,6 +96,6 @@
   cursor: default;
 }
 
-div.openlink{
- display:inline;
+div.openlink {
+  display: inline;
  }
--- a/web/data/cubicweb.preferences.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.preferences.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,4 +1,5 @@
-/* toggle visibility of an element by its id
+/**
+ * toggle visibility of an element by its id
  * & set current visibility status in a cookie
  * XXX whenever used outside of preferences, don't forget to
  *     move me in a more appropriate place
@@ -11,56 +12,62 @@
     jQuery('#' + elemId).toggleClass('hidden');
 }
 
-function closeFieldset(fieldsetid){
+function closeFieldset(fieldsetid) {
     var linklabel = _('open all');
-    var linkhref = 'javascript:openFieldset("' +fieldsetid + '")';
+    var linkhref = 'javascript:openFieldset("' + fieldsetid + '")';
     _toggleFieldset(fieldsetid, 1, linklabel, linkhref);
 }
 
-function openFieldset(fieldsetid){
+function openFieldset(fieldsetid) {
     var linklabel = _('close all');
-    var linkhref = 'javascript:closeFieldset("'+ fieldsetid + '")';
+    var linkhref = 'javascript:closeFieldset("' + fieldsetid + '")';
     _toggleFieldset(fieldsetid, 0, linklabel, linkhref);
 }
 
-function _toggleFieldset(fieldsetid, closeaction, linklabel, linkhref){
-    jQuery('#'+fieldsetid).find('div.openlink').each(function(){
-	    var link = A({'href' : "javascript:noop();",
-			  'onclick' : linkhref},
-			  linklabel);
-	    jQuery(this).empty().append(link);
-	});
-    jQuery('#'+fieldsetid).find('fieldset[id]').each(function(){
-	    var fieldset = jQuery(this);
-	    if(closeaction){
-		fieldset.addClass('hidden');
-	    }else{
-		fieldset.removeClass('hidden');
-		linkLabel = (_('open all'));
-	    }
-	});
+function _toggleFieldset(fieldsetid, closeaction, linklabel, linkhref) {
+    jQuery('#' + fieldsetid).find('div.openlink').each(function() {
+        var link = A({
+            'href': "javascript:noop();",
+            'onclick': linkhref
+        },
+        linklabel);
+        jQuery(this).empty().append(link);
+    });
+    jQuery('#' + fieldsetid).find('fieldset[id]').each(function() {
+        var fieldset = jQuery(this);
+        if (closeaction) {
+            fieldset.addClass('hidden');
+        } else {
+            fieldset.removeClass('hidden');
+            linkLabel = (_('open all'));
+        }
+    });
 }
 
-function validatePrefsForm(formid){
+function validatePrefsForm(formid) {
     clearPreviousMessages();
     clearPreviousErrors(formid);
-    return validateForm(formid, null,  submitSucces, submitFailure);
+    return validateForm(formid, null, submitSucces, submitFailure);
 }
 
-function submitFailure(formid){
-    var form = jQuery('#'+formid);
-    var dom = DIV({'class':'critical'},
-		  _("please correct errors below"));
+function submitFailure(formid) {
+    var form = jQuery('#' + formid);
+    var dom = DIV({
+        'class': 'critical'
+    },
+    _("please correct errors below"));
     jQuery(form).find('div.formsg').empty().append(dom);
     // clearPreviousMessages()
     jQuery(form).find('span.error').next().focus();
 }
 
-function submitSucces(url, formid){
-    var form = jQuery('#'+formid);
+function submitSucces(url, formid) {
+    var form = jQuery('#' + formid);
     setCurrentValues(form);
-    var dom = DIV({'class':'msg'},
-		  _("changes applied"));
+    var dom = DIV({
+        'class': 'msg'
+    },
+    _("changes applied"));
     jQuery(form).find('div.formsg').empty().append(dom);
     jQuery(form).find('input').removeClass('changed');
     checkValues(form, true);
@@ -76,78 +83,79 @@
     jQuery('#err-value:' + formid).remove();
 }
 
-function checkValues(form, success){
+function checkValues(form, success) {
     var unfreezeButtons = false;
-    jQuery(form).find('select').each(function () {
-	    unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
-	});
-    jQuery(form).find('[type=text]').each(function () {
-	    unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
-	});
-    jQuery(form).find('input[type=radio]:checked').each(function () {
-            unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
-     });
+    jQuery(form).find('select').each(function() {
+        unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
+    });
+    jQuery(form).find('[type=text]').each(function() {
+        unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
+    });
+    jQuery(form).find('input[type=radio]:checked').each(function() {
+        unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
+    });
 
-    if (unfreezeButtons){
-	unfreezeFormButtons(form.attr('id'));
-    }else{
-	if (!success){
-	    clearPreviousMessages();
-	}
-	clearPreviousErrors(form.attr('id'));
-	freezeFormButtons(form.attr('id'));
+    if (unfreezeButtons) {
+        unfreezeFormButtons(form.attr('id'));
+    } else {
+        if (!success) {
+            clearPreviousMessages();
+        }
+        clearPreviousErrors(form.attr('id'));
+        freezeFormButtons(form.attr('id'));
     }
 }
 
-function _checkValue(input, unfreezeButtons){
+function _checkValue(input, unfreezeButtons) {
     var currentValue = prefsValues[input.attr('name')];
-     if (currentValue != input.val()){
-	 input.addClass('changed');
-	 unfreezeButtons = true;
-     }else{
-	 input.removeClass('changed');
-	 jQuery("span[id=err-" + input.attr('id') + "]").remove();
-     }
-     input.removeClass('error');
-     return unfreezeButtons;
+    if (currentValue != input.val()) {
+        input.addClass('changed');
+        unfreezeButtons = true;
+    } else {
+        input.removeClass('changed');
+        jQuery("span[id=err-" + input.attr('id') + "]").remove();
+    }
+    input.removeClass('error');
+    return unfreezeButtons;
 }
 
-function setCurrentValues(form){
-    jQuery(form).find('[name^=value]').each(function () {
-	var input = jQuery(this);
-	var name = input.attr('name');
-	if(input.attr('type') == 'radio'){
-	    // NOTE: there seems to be a bug with jQuery(input).attr('checked')
-	    //       in our case, we can't rely on its value, we use
-	    //       the DOM API instead.
-	    if(input[0].checked){
-		prefsValues[name] = input.val();
-	    }
-	}else{
-	    prefsValues[name] = input.val();
-	}
-	jQuery(form).find('input[name=edits-'+ name + ']').val(prefsValues[name]);
+function setCurrentValues(form) {
+    jQuery(form).find('[name^=value]').each(function() {
+        var input = jQuery(this);
+        var name = input.attr('name');
+        if (input.attr('type') == 'radio') {
+            // NOTE: there seems to be a bug with jQuery(input).attr('checked')
+            //       in our case, we can't rely on its value, we use
+            //       the DOM API instead.
+            if (input[0].checked) {
+                prefsValues[name] = input.val();
+            }
+        } else {
+            prefsValues[name] = input.val();
+        }
+        jQuery(form).find('input[name=edits-' + name + ']').val(prefsValues[name]);
     });
 }
 
-function initEvents(){
+function initEvents() {
     jQuery('form').each(function() {
-	var form = jQuery(this);
-	//freezeFormButtons(form.attr('id'));
-	form.find('.validateButton').attr('disabled', 'disabled');
-	form.find('input[type=text]').keyup(function(){
-	    checkValues(form);
-	});
-	form.find('input[type=radio]').change(function(){
-	    checkValues(form);
-	});
-	form.find('select').change(function(){
-	    checkValues(form);
-	});
-	setCurrentValues(form);
+        var form = jQuery(this);
+        //freezeFormButtons(form.attr('id'));
+        form.find('.validateButton').attr('disabled', 'disabled');
+        form.find('input[type=text]').keyup(function() {
+            checkValues(form);
+        });
+        form.find('input[type=radio]').change(function() {
+            checkValues(form);
+        });
+        form.find('select').change(function() {
+            checkValues(form);
+        });
+        setCurrentValues(form);
     });
 }
 
 $(document).ready(function() {
-	initEvents();
+    initEvents();
 });
+
--- a/web/data/cubicweb.print.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.print.css	Mon Jul 19 15:37:02 2010 +0200
@@ -1,4 +1,15 @@
-td#speedbar, img.logo, div.header{ 
- display:none }
+* {
+  color: #000 !important;
+}
 
-a{color:black }
\ No newline at end of file
+div#popupLoginBox,
+div#popupLoginBox,
+img#logo, div.header,
+#navColumnLeft, #navColumnRight,
+#footer {
+  display: none
+}
+
+div#pageContent{
+  border: none;
+}
\ No newline at end of file
--- a/web/data/cubicweb.python.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.python.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,18 +1,14 @@
-/*
+/**
  * This file contains extensions for standard javascript types
  *
  */
 
 ONE_DAY = 86400000; // (in milliseconds)
-
 // ========== DATE EXTENSIONS ========== ///
-
 Date.prototype.equals = function(other) {
     /* compare with other date ignoring time differences */
-    if (this.getYear() == other.getYear() &&
-	this.getMonth() == other.getMonth() &&
-	this.getDate() == other.getDate()) {
-	return true;
+    if (this.getYear() == other.getYear() && this.getMonth() == other.getMonth() && this.getDate() == other.getDate()) {
+        return true;
     }
     return false;
 };
@@ -24,7 +20,7 @@
 };
 
 Date.prototype.sub = function(days) {
-    return this.add(-days);
+    return this.add( - days);
 };
 
 Date.prototype.iadd = function(days) {
@@ -39,33 +35,37 @@
     this.setTime(this.getTime() - (days * ONE_DAY));
 };
 
-/*
+/**
+ * .. function:: Date.prototype.nextMonth()
+ *
  * returns the first day of the next month
  */
 Date.prototype.nextMonth = function() {
     if (this.getMonth() == 11) {
-	var d =new Date(this.getFullYear()+1, 0, 1);
-	return d;
+        var d = new Date(this.getFullYear() + 1, 0, 1);
+        return d;
     } else {
-	var d2 = new Date(this.getFullYear(), this.getMonth()+1, 1);
-	return d2;
+        var d2 = new Date(this.getFullYear(), this.getMonth() + 1, 1);
+        return d2;
     }
 };
 
-/*
+/**
+ * .. function:: Date.prototype.getRealDay()
+ *
  * returns the day of week, 0 being monday, 6 being sunday
  */
 Date.prototype.getRealDay = function() {
     // getDay() returns 0 for Sunday ==> 6 for Saturday
-    return (this.getDay()+6) % 7;
+    return (this.getDay() + 6) % 7;
 };
 
 Date.prototype.strftime = function(fmt) {
     if (this.toLocaleFormat !== undefined) { // browser dependent
-	return this.toLocaleFormat(fmt);
+        return this.toLocaleFormat(fmt);
     }
     // XXX implement at least a decent fallback implementation
-    return this.getFullYear() + '/' + (this.getMonth()+1) + '/' + this.getDate();
+    return this.getFullYear() + '/' + (this.getMonth() + 1) + '/' + this.getDate();
 };
 
 var _DATE_FORMAT_REGXES = {
@@ -74,231 +74,131 @@
     'm': new RegExp('^[0-9]{1,2}'),
     'H': new RegExp('^[0-9]{1,2}'),
     'M': new RegExp('^[0-9]{1,2}')
-}
+};
 
-/*
+/**
+ * .. function:: _parseDate(datestring, format)
+ *
  * _parseData does the actual parsing job needed by `strptime`
  */
 function _parseDate(datestring, format) {
     var skip0 = new RegExp('^0*[0-9]+');
     var parsed = {};
-    for (var i1=0,i2=0;i1<format.length;i1++,i2++) {
-	var c1 = format.charAt(i1);
-	var c2 = datestring.charAt(i2);
-	if (c1 == '%') {
-	    c1 = format.charAt(++i1);
-	    var data = _DATE_FORMAT_REGXES[c1].exec(datestring.substring(i2));
-	    if (!data.length) {
-		return null;
-	    }
-	    data = data[0];
-	    i2 += data.length-1;
-	    var value = parseInt(data, 10);
-	    if (isNaN(value)) {
-		return null;
-	    }
-	    parsed[c1] = value;
-	    continue;
-	}
-	if (c1 != c2) {
-	    return null;
-	}
+    for (var i1 = 0, i2 = 0; i1 < format.length; i1++, i2++) {
+        var c1 = format.charAt(i1);
+        var c2 = datestring.charAt(i2);
+        if (c1 == '%') {
+            c1 = format.charAt(++i1);
+            var data = _DATE_FORMAT_REGXES[c1].exec(datestring.substring(i2));
+            if (!data.length) {
+                return null;
+            }
+            data = data[0];
+            i2 += data.length - 1;
+            var value = parseInt(data, 10);
+            if (isNaN(value)) {
+                return null;
+            }
+            parsed[c1] = value;
+            continue;
+        }
+        if (c1 != c2) {
+            return null;
+        }
     }
     return parsed;
 }
 
-/*
+/**
+ * .. function:: strptime(datestring, format)
+ *
  * basic implementation of strptime. The only recognized formats
  * defined in _DATE_FORMAT_REGEXES (i.e. %Y, %d, %m, %H, %M)
  */
 function strptime(datestring, format) {
     var parsed = _parseDate(datestring, format);
     if (!parsed) {
-	return null;
+        return null;
     }
     // create initial date (!!! year=0 means 1900 !!!)
     var date = new Date(0, 0, 1, 0, 0);
     date.setFullYear(0); // reset to year 0
     if (parsed.Y) {
-	date.setFullYear(parsed.Y);
+        date.setFullYear(parsed.Y);
     }
     if (parsed.m) {
-	if (parsed.m < 1 || parsed.m > 12) {
-	    return null;
-	}
-	// !!! month indexes start at 0 in javascript !!!
-	date.setMonth(parsed.m - 1);
+        if (parsed.m < 1 || parsed.m > 12) {
+            return null;
+        }
+        // !!! month indexes start at 0 in javascript !!!
+        date.setMonth(parsed.m - 1);
     }
     if (parsed.d) {
-	if (parsed.m < 1 || parsed.m > 31) {
-	    return null;
-	}
-	date.setDate(parsed.d);
+        if (parsed.m < 1 || parsed.m > 31) {
+            return null;
+        }
+        date.setDate(parsed.d);
     }
     if (parsed.H) {
-	if (parsed.H < 0 || parsed.H > 23) {
-	    return null;
-	}
-	date.setHours(parsed.H);
+        if (parsed.H < 0 || parsed.H > 23) {
+            return null;
+        }
+        date.setHours(parsed.H);
     }
     if (parsed.M) {
-	if (parsed.M < 0 || parsed.M > 59) {
-	    return null;
-	}
-	date.setMinutes(parsed.M);
+        if (parsed.M < 0 || parsed.M > 59) {
+            return null;
+        }
+        date.setMinutes(parsed.M);
     }
     return date;
 }
 
 // ========== END OF DATE EXTENSIONS ========== ///
-
-
-
-// ========== ARRAY EXTENSIONS ========== ///
-Array.prototype.contains = function(element) {
-    return findValue(this, element) != -1;
-};
-
-// ========== END OF ARRAY EXTENSIONS ========== ///
-
-
-
 // ========== STRING EXTENSIONS ========== //
-
-/* python-like startsWith method for js strings
+/**
+ * .. function:: String.prototype.startswith(prefix)
+ *
+ * python-like startsWith method for js strings
  * >>>
  */
-String.prototype.startsWith = function(prefix) {
+String.prototype.startswith = function(prefix) {
     return this.indexOf(prefix) == 0;
 };
 
-/* python-like endsWith method for js strings */
-String.prototype.endsWith = function(suffix) {
+/**
+ * .. function:: String.prototype.endswith(suffix)
+ *
+ * python-like endsWith method for js strings
+ */
+String.prototype.endswith = function(suffix) {
     var startPos = this.length - suffix.length;
-    if (startPos < 0) { return false; }
+    if (startPos < 0) {
+        return false;
+    }
     return this.lastIndexOf(suffix, startPos) == startPos;
 };
 
-/* python-like strip method for js strings */
+/**
+ * .. function:: String.prototype.strip()
+ *
+ * python-like strip method for js strings
+ */
 String.prototype.strip = function() {
     return this.replace(/^\s*(.*?)\s*$/, "$1");
 };
 
-/* py-equiv: string in list */
-String.prototype.in_ = function(values) {
-    return findValue(values, this) != -1;
-};
-
-/* py-equiv: str.join(list) */
-String.prototype.join = function(args) {
-    return args.join(this);
-};
+// ========= class factories ========= //
 
-/* python-like list builtin
- * transforms an iterable in a js sequence
- * >>> gen = ifilter(function(x) {return x%2==0}, range(10))
- * >>> s = list(gen)
- * [0,2,4,6,8]
- */
-function list(iterable) {
-    var iterator = iter(iterable);
-    var result = [];
-    while (true) {
-	/* iterates until StopIteration occurs */
-	try {
-	    result.push(iterator.next());
-	} catch (exc) {
-	    if (exc != StopIteration) { throw exc; }
-	    return result;
-	}
-    }
-}
-
-/* py-equiv: getattr(obj, attrname, default=None) */
-function getattr(obj, attrname, defaultValue) {
-    // when not passed, defaultValue === undefined
-    return obj[attrname] || defaultValue;
-}
-
-/* py-equiv: operator.attrgetter */
-function attrgetter(attrname) {
-    return function(obj) { return getattr(obj, attrname); };
-}
-
-
-/* returns a subslice of `lst` using `start`/`stop`/`step`
- * start, stop might be negative
+/**
+ * .. function:: makeUnboundMethod(meth)
  *
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2)
- * ['c', 'd', 'e', 'f']
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2, -2)
- * ['c', 'd']
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], -3)
- * ['d', 'e', 'f']
+ * transforms a function into an unbound method
  */
-function sliceList(lst, start, stop, step) {
-    start = start || 0;
-    stop = stop || lst.length;
-    step = step || 1;
-    if (stop < 0) {
-	stop = max(lst.length+stop, 0);
-    }
-    if (start < 0) {
-	start = min(lst.length+start, lst.length);
-    }
-    var result = [];
-    for (var i=start; i < stop; i+=step) {
-	result.push(lst[i]);
-    }
-    return result;
-}
-
-/* returns a partial func that calls a mehod on its argument
- * py-equiv: return lambda obj: getattr(obj, methname)(*args)
- */
-// XXX looks completely unused (candidate for removal)
-function methodcaller(methname) {
-    var args = sliceList(arguments, 1);
-    return function(obj) {
-	return obj[methname].apply(obj, args);
-    };
-}
-
-/* use MochiKit's listMin / listMax */
-function min() { return listMin(arguments); }
-function max() { return listMax(arguments); }
-
-/*
- * >>> d = dict(["x", "y", "z"], [0, 1, 2])
- * >>> d['y']
- * 1
- * >>> d.y
- * 1
- */
-function dict(keys, values) {
-    if (keys.length != values.length) {
-	throw "got different number of keys and values !";
-    }
-    var newobj = {};
-    for(var i=0; i<keys.length; i++) {
-	newobj[keys[i]] = values[i];
-    }
-    return newobj;
-}
-
-
-function concat() {
-    return ''.join(list(arguments));
-}
-
-
-/**** class factories ****/
-
-// transforms a function into an unbound method
 function makeUnboundMethod(meth) {
     function unboundMeth(self) {
-	var newargs = sliceList(arguments, 1);
-	return meth.apply(self, newargs);
+        var newargs = cw.utils.sliceList(arguments, 1);
+        return meth.apply(self, newargs);
     }
     unboundMeth.__name__ = meth.__name__;
     return unboundMeth;
@@ -312,29 +212,40 @@
     cls.prototype[methname] = meth; // for the instance
 }
 
-// simple internal function that tells if the attribute should
-// be copied from baseclasses or not
+/**
+ * .. function:: _isAttrSkipped(attrname)
+ *
+ * simple internal function that tells if the attribute should
+ * be copied from baseclasses or not
+ */
 function _isAttrSkipped(attrname) {
     var skipped = ['__class__', '__dict__', '__bases__', 'prototype'];
-    for (var i=0; i < skipped.length; i++) {
-	if (skipped[i] == attrname) {
-	    return true;
-	}
+    for (var i = 0; i < skipped.length; i++) {
+        if (skipped[i] == attrname) {
+            return true;
+        }
     }
     return false;
 }
 
-// internal function used to build the class constructor
+/**
+ * .. function:: makeConstructor(userctor)
+ *
+ * internal function used to build the class constructor
+ */
 function makeConstructor(userctor) {
     return function() {
-	// this is a proxy to user's __init__
-	if (userctor) {
-	    userctor.apply(this, arguments);
-	}
+        // this is a proxy to user's __init__
+        if (userctor) {
+            userctor.apply(this, arguments);
+        }
     };
 }
 
-/* this is a js class factory. objects returned by this function behave
+/**
+ * .. function:: defclass(name, bases, classdict)
+ *
+ * this is a js class factory. objects returned by this function behave
  * more or less like a python class. The `class` function prototype is
  * inspired by the python `type` builtin
  * Important notes :
@@ -347,19 +258,21 @@
     // this is the static inheritance approach (<=> differs from python)
     var basemeths = {};
     var reverseLookup = [];
-    for(var i=baseclasses.length-1; i >= 0; i--) {
-	reverseLookup.push(baseclasses[i]);
+    for (var i = baseclasses.length - 1; i >= 0; i--) {
+        reverseLookup.push(baseclasses[i]);
     }
-    reverseLookup.push({'__dict__' : classdict});
+    reverseLookup.push({
+        '__dict__': classdict
+    });
 
-    for(var i=0; i < reverseLookup.length; i++) {
-	var cls = reverseLookup[i];
-	for (prop in cls.__dict__) {
-	    // XXX hack to avoid __init__, __bases__...
-	    if ( !_isAttrSkipped(prop) ) {
-		basemeths[prop] = cls.__dict__[prop];
-	    }
-	}
+    for (var i = 0; i < reverseLookup.length; i++) {
+        var cls = reverseLookup[i];
+        for (prop in cls.__dict__) {
+            // XXX hack to avoid __init__, __bases__...
+            if (!_isAttrSkipped(prop)) {
+                basemeths[prop] = cls.__dict__[prop];
+            }
+        }
     }
     var userctor = basemeths['__init__'];
     var constructor = makeConstructor(userctor);
@@ -371,38 +284,8 @@
     constructor.prototype.__class__ = constructor;
     // make bound / unbound methods
     for (methname in basemeths) {
-	attachMethodToClass(constructor, methname, basemeths[methname]);
+        attachMethodToClass(constructor, methname, basemeths[methname]);
     }
 
     return constructor;
 }
-
-// Not really python-like
-CubicWeb = {};
-// XXX backward compatibility
-Erudi = CubicWeb;
-CubicWeb.loaded = [];
-CubicWeb.require = function(module) {
-    if (!CubicWeb.loaded.contains(module)) {
-	// a CubicWeb.load_javascript(module) function would require a dependency on ajax.js
-	log(module, ' is required but not loaded');
-    }
-};
-
-CubicWeb.provide = function(module) {
-    if (!CubicWeb.loaded.contains(module)) {
-	CubicWeb.loaded.push(module);
-    }
-};
-
-jQuery(document).ready(function() {
-    jQuery(CubicWeb).trigger('server-response', [false, document]);
-});
-
-// XXX as of 2010-04-07, no known cube uses this
-jQuery(CubicWeb).bind('ajax-loaded', function() {
-    log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
-    jQuery(CubicWeb).trigger('server-response', [false, document]);
-});
-
-CubicWeb.provide('python.js');
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.reledit.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,73 @@
+cw.reledit = new Namespace('cw.reledit');
+
+
+jQuery.extend(cw.reledit, {
+
+    /* Unhides the part of reledit div containing the form
+     * hides other parts
+     */
+    showInlineEditionForm: function (divid) {
+        jQuery('#' + divid).hide();
+        jQuery('#' + divid + '-value').hide();
+        jQuery('#' + divid + '-form').show();
+      },
+
+    /* Hides and removes edition parts, incl. messages
+     * show initial widget state
+     */
+    cleanupAfterCancel: function (divid) {
+        jQuery('#appMsg').hide();
+        jQuery('div.errorMessage').remove();
+        jQuery('#' + divid).show();
+        jQuery('#' + divid + '-value').show();
+        jQuery('#' + divid + '-form').hide();
+    },
+
+    /* callback used on form validation success
+     * refreshes the whole page or just the edited reledit zone
+     * @param results: [status, ...]
+     * @param formid: the dom id of the reledit form
+     * @param cbargs: ...
+     */
+     onSuccess: function (results, formid, cbargs) {
+        var params = {fname: 'reledit_form'};
+        jQuery('#' + formid + ' input:hidden').each(function (elt) {
+            var name = jQuery(this).attr('name');
+            if (name && name.startswith('__reledit|')) {
+                params[name.split('|')[1]] = this.value;
+            }
+        });
+        var reload = cw.evalJSON(params.reload);
+        if (reload || (params.formid == 'deleteconf')) {
+            if (typeof reload == 'string') {
+                /* Sometimes we want to reload but the reledit thing
+                 * updated a key attribute which was a component of the
+                 * url
+                 */
+                document.location.href = reload;
+                return;
+            }
+            else {
+                document.location.reload();
+                return;
+            }
+        }
+        jQuery('#'+params.divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, params, 'post');
+    },
+
+    /* called by reledit forms to submit changes
+     * @param formid : the dom id of the form used
+     * @param rtype : the attribute being edited
+     * @param eid : the eid of the entity being edited
+     * @param reload: boolean to reload page if true (when changing URL dependant data)
+     * @param default_value : value if the field is empty
+     */
+    loadInlineEditionForm: function(formid, eid, rtype, role, divid, reload, vid, default_value) {
+        var args = {fname: 'reledit_form', rtype: rtype, role: role,
+                    pageid: pageid,
+    	            eid: eid, divid: divid, formid: formid,
+    		    reload: reload, vid: vid, default_value: default_value,
+    		    callback: function () {cw.reledit.showInlineEditionForm(divid);}};
+       jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+    }
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.reset.css	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,53 @@
+/* http://meyerweb.com/eric/tools/css/reset/ */
+/* v1.0 | 20080212 */
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  outline: 0;
+  font-size: 100%;
+  vertical-align: baseline;
+  background: transparent;
+}
+body {
+  line-height: 1;
+}
+ol, ul {
+  list-style: none;
+}
+blockquote, q {
+  quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+  content: '';
+  content: none;
+}
+
+/* remember to define focus styles! */
+:focus {
+  outline: 0;
+}
+
+/* remember to highlight inserts somehow! */
+ins {
+  text-decoration: none;
+}
+del {
+  text-decoration: line-through;
+}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.rhythm.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,7 @@
+$(document).ready(function() {
+    $('a.rhythm').click(function (event){
+        $('div#pageContent').toggleClass('rhythm_bg');
+        $('div#page').toggleClass('rhythm_bg');
+        event.preventDefault();
+    });
+});
--- a/web/data/cubicweb.tablesorter.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.tablesorter.css	Mon Jul 19 15:37:02 2010 +0200
@@ -1,12 +1,4 @@
-/* tables */
-/*table.tablesorter {
-	font-family:arial;
-	background-color: #CDCDCD;
-	margin:10px 0pt 15px;
-	font-size: 8pt;
-	width: 100%;
-	text-align: left;
-} */
+/* sortable tables */
 
 table.listing tr .headerSortUp {
   background-image: url(asc.gif);
@@ -15,7 +7,7 @@
   background-image: url(desc.gif);
 }
 table.listing tr .headerSortDown, table.listing tr .headerSortUp {
-   background-color: #DDD;
+   background-color: %(listingBorderColor)s;
    background-repeat: no-repeat;
    background-position: center right;
 }
--- a/web/data/cubicweb.tableview.css	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.tableview.css	Mon Jul 19 15:37:02 2010 +0200
@@ -6,7 +6,7 @@
   font-weight: bold;
   background: #ebe8d9 url("button.png") repeat-x;
   padding: 0.3em;
-  border-bottom: 1px solid #cfceb7;
+  border-bottom: 1px solid %(actionBoxTitleBgColor)s;
   text-align: left;
 }
 
--- a/web/data/cubicweb.tabs.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.tabs.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,6 +1,7 @@
 function set_tab(tabname, cookiename) {
     // set appropriate cookie
-    asyncRemoteExec('set_cookie', cookiename, tabname);
+    loadRemote('json', ajaxFuncArgs('set_cookie', null, cookiename, tabname));
     // trigger show + tabname event
     trigger_load(tabname);
 }
+
--- a/web/data/cubicweb.timeline-bundle.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.timeline-bundle.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,14 +1,14 @@
 var SimileAjax_urlPrefix = baseuri() + 'data/';
 var Timeline_urlPrefix = baseuri() + 'data/';
 
-/*==================================================
+/*
  *  Simile Ajax API
  *
  *  Include this file in your HTML file as follows:
  *
  *    <script src="http://simile.mit.edu/ajax/api/simile-ajax-api.js" type="text/javascript"></script>
  *
- *==================================================
+ *
  */
 
 if (typeof SimileAjax == "undefined") {
@@ -213,9 +213,9 @@
         SimileAjax.loaded = true;
     })();
 }
-/*==================================================
+/*
  *  Platform Utility Functions and Constants
- *==================================================
+ *
  */
 
 /*  This must be called after our jQuery has been loaded
@@ -319,9 +319,10 @@
 
 SimileAjax.Platform.getDefaultLocale = function() {
     return SimileAjax.Platform.clientLocale;
-};/*==================================================
+};
+/*
  *  Debug Utility Functions
- *==================================================
+ *
  */
 
 SimileAjax.Debug = {
@@ -678,9 +679,9 @@
         }
     };
 })();
-/*==================================================
+/*
  *  DOM Utility Functions
- *==================================================
+ *
  */
 
 SimileAjax.DOM = new Object();
@@ -1040,9 +1041,9 @@
     SimileAjax.includeCssFile(document, SimileAjax.urlPrefix + "styles/graphics-ie6.css");
 }
 
-/*==================================================
+/*
  *  Opacity, translucency
- *==================================================
+ *
  */
 SimileAjax.Graphics._createTranslucentImage1 = function(url, verticalAlign) {
     var elmt = document.createElement("img");
@@ -1119,9 +1120,9 @@
     }
 };
 
-/*==================================================
+/*
  *  Bubble
- *==================================================
+ *
  */
 
 SimileAjax.Graphics.bubbleConfig = {
@@ -1479,9 +1480,9 @@
     };
 };
 
-/*==================================================
+/*
  *  Animation
- *==================================================
+ *
  */
 
 /**
@@ -1549,11 +1550,11 @@
     }
 };
 
-/*==================================================
+/*
  *  CopyPasteButton
  *
  *  Adapted from http://spaces.live.com/editorial/rayozzie/demo/liveclip/liveclipsample/techPreview.html.
- *==================================================
+ *
  */
 
 /**
@@ -1606,9 +1607,9 @@
     return div;
 };
 
-/*==================================================
+/*
  *  getWidthHeight
- *==================================================
+ *
  */
 SimileAjax.Graphics.getWidthHeight = function(el) {
     // RETURNS hash {width:  w, height: h} in pixels
@@ -1633,9 +1634,9 @@
 };
 
 
-/*==================================================
+/*
  *  FontRenderingContext
- *==================================================
+ *
  */
 SimileAjax.Graphics.getFontRenderingContext = function(elmt, width) {
     return new SimileAjax.Graphics._FontRenderingContext(elmt, width);
@@ -2127,9 +2128,9 @@
     var d = new Date().getTimezoneOffset();
     return d / -60;
 };
-/*==================================================
+/*
  *  String Utility Functions and Constants
- *==================================================
+ *
  */
 
 String.prototype.trim = function() {
@@ -2170,9 +2171,9 @@
     }
     return result;
 };
-/*==================================================
+/*
  *  HTML Utility Functions
- *==================================================
+ *
  */
 
 SimileAjax.HTML = new Object();
@@ -2655,9 +2656,9 @@
     return (this._a.length > 0) ? this._a[this._a.length - 1] : null;
 };
 
-/*==================================================
+/*
  *  Event Index
- *==================================================
+ *
  */
 
 SimileAjax.EventIndex = function(unit) {
@@ -2889,9 +2890,9 @@
         return this._index < this._events.length() ?
             this._events.elementAt(this._index++) : null;
     }
-};/*==================================================
+};/*
  *  Default Unit
- *==================================================
+ *
  */
 
 SimileAjax.NativeDateUnit = new Object();
@@ -2953,9 +2954,9 @@
     return new Date(v.getTime() + n);
 };
 
-/*==================================================
+/*
  *  General, miscellaneous SimileAjax stuff
- *==================================================
+ *
  */
 
 SimileAjax.ListenerQueue = function(wildcardHandlerName) {
@@ -2998,7 +2999,7 @@
     }
 };
 
-/*======================================================================
+/*
  *  History
  *
  *  This is a singleton that keeps track of undoable user actions and
@@ -3020,7 +3021,7 @@
  *
  *  An iframe is inserted into the document's body element to track
  *  onload events.
- *======================================================================
+ *
  */
 
 SimileAjax.History = {
@@ -3632,7 +3633,7 @@
     }
     return elmt;
 };
-/*==================================================
+/*
  *  Timeline API
  *
  *  This file will load all the Javascript files
@@ -3696,7 +3697,7 @@
  * Note that the Ajax version is usually NOT the same as the Timeline version.
  * See variable simile_ajax_ver below for the current version
  *
- *==================================================
+ *
  */
 
 (function() {
@@ -3928,7 +3929,7 @@
         loadMe();
     }
 })();
-/*=================================================
+/*
  *
  * Coding standards:
  *
@@ -3950,14 +3951,14 @@
  * We also want to use jslint:  http://www.jslint.com/
  *
  *
- *==================================================
+ *
  */
 
 
 
-/*==================================================
+/*
  *  Timeline VERSION
- *==================================================
+ *
  */
 // Note: version is also stored in the build.xml file
 Timeline.version = 'pre 2.4.0';  // use format 'pre 1.2.3' for trunk versions
@@ -3965,9 +3966,9 @@
 Timeline.display_version = Timeline.version + ' (with Ajax lib ' + Timeline.ajax_lib_version + ')';
  // cf method Timeline.writeVersion
 
-/*==================================================
+/*
  *  Timeline
- *==================================================
+ *
  */
 Timeline.strings = {}; // localization string tables
 Timeline.HORIZONTAL = 0;
@@ -4183,9 +4184,9 @@
 
 
 
-/*==================================================
+/*
  *  Timeline Implementation object
- *==================================================
+ *
  */
 Timeline._Impl = function(elmt, bandInfos, orientation, unit, timelineID) {
     SimileAjax.WindowManager.initialize();
@@ -4585,7 +4586,7 @@
   this.paint();
 };
 
-/*=================================================
+/*
  *
  * Coding standards:
  *
@@ -4607,14 +4608,14 @@
  * We also want to use jslint:  http://www.jslint.com/
  *
  *
- *==================================================
+ *
  */
 
 
 
-/*==================================================
+/*
  *  Band
- *==================================================
+ *
  */
 Timeline._Band = function(timeline, bandInfo, index) {
     // hack for easier subclassing
@@ -5344,9 +5345,9 @@
 Timeline._Band.prototype.closeBubble = function() {
     SimileAjax.WindowManager.cancelPopups();
 };
-/*==================================================
+/*
  *  Classic Theme
- *==================================================
+ *
  */
 
 
@@ -5523,14 +5524,14 @@
     };
 
     this.mouseWheel = 'scroll'; // 'default', 'zoom', 'scroll'
-};/*==================================================
+};/*
  *  An "ether" is a object that maps date/time to pixel coordinates.
- *==================================================
+ *
  */
 
-/*==================================================
+/*
  *  Linear Ether
- *==================================================
+ *
  */
 
 Timeline.LinearEther = function(params) {
@@ -5601,9 +5602,9 @@
 };
 
 
-/*==================================================
+/*
  *  Hot Zone Ether
- *==================================================
+ *
  */
 
 Timeline.HotZoneEther = function(params) {
@@ -5828,9 +5829,9 @@
 Timeline.HotZoneEther.prototype._getScale = function() {
     return this._interval / this._pixelsPerInterval;
 };
-/*==================================================
+/*
  *  Gregorian Ether Painter
- *==================================================
+ *
  */
 
 Timeline.GregorianEtherPainter = function(params) {
@@ -5919,9 +5920,9 @@
 };
 
 
-/*==================================================
+/*
  *  Hot Zone Gregorian Ether Painter
- *==================================================
+ *
  */
 
 Timeline.HotZoneGregorianEtherPainter = function(params) {
@@ -6080,9 +6081,9 @@
   }
 };
 
-/*==================================================
+/*
  *  Year Count Ether Painter
- *==================================================
+ *
  */
 
 Timeline.YearCountEtherPainter = function(params) {
@@ -6169,9 +6170,9 @@
 Timeline.YearCountEtherPainter.prototype.softPaint = function() {
 };
 
-/*==================================================
+/*
  *  Quarterly Ether Painter
- *==================================================
+ *
  */
 
 Timeline.QuarterlyEtherPainter = function(params) {
@@ -6257,9 +6258,9 @@
 Timeline.QuarterlyEtherPainter.prototype.softPaint = function() {
 };
 
-/*==================================================
+/*
  *  Ether Interval Marker Layout
- *==================================================
+ *
  */
 
 Timeline.EtherIntervalMarkerLayout = function(timeline, band, theme, align, showLine) {
@@ -6363,9 +6364,9 @@
     };
 };
 
-/*==================================================
+/*
  *  Ether Highlight Layout
- *==================================================
+ *
  */
 
 Timeline.EtherHighlight = function(timeline, band, theme, backgroundLayer) {
@@ -6404,9 +6405,9 @@
         }
     }
 };
-/*==================================================
+/*
  *  Event Utils
- *==================================================
+ *
  */
 Timeline.EventUtils = {};
 
@@ -6421,7 +6422,7 @@
 };
 
 Timeline.EventUtils.decodeEventElID = function(elementID) {
-    /*==================================================
+    /*
      *
      * Use this function to decode an event element's id on a band (label div,
      * tape div or icon img).
@@ -6447,7 +6448,7 @@
      * by using Timeline.getTimeline, Timeline.getBand, or
      * Timeline.getEvent and passing in the element's id
      *
-     *==================================================
+     *
      */
 
     var parts = elementID.split('-');
@@ -6467,9 +6468,9 @@
     // elType should be one of {label | icon | tapeN | highlightN}
     return elType + "-tl-" + timeline.timelineID +
        "-" + band.getIndex() + "-" + evt.getID();
-};/*==================================================
+};/*
  *  Gregorian Date Labeller
- *==================================================
+ *
  */
 
 Timeline.GregorianDateLabeller = function(locale, timeZone) {
@@ -6558,9 +6559,9 @@
     return { text: text, emphasized: emphasized };
 }
 
-/*==================================================
+/*
  *  Default Event Source
- *==================================================
+ *
  */
 
 
@@ -7125,12 +7126,12 @@
 };
 
 
-/*==================================================
+/*
  *  Original Event Painter
- *==================================================
+ *
  */
 
-/*==================================================
+/*
  *
  * To enable a single event listener to monitor everything
  * on a Timeline, we need a way to map from an event's icon,
@@ -7152,7 +7153,7 @@
  * You can then retrieve the band/timeline objects and event object
  * by using Timeline.EventUtils.decodeEventElID
  *
- *==================================================
+ *
  */
 
 /*
@@ -7818,9 +7819,9 @@
         this._eventPaintListeners[i](this._band, op, evt, els);
     }
 };
-/*==================================================
+/*
  *  Detailed Event Painter
- *==================================================
+ *
  */
 
 // Note: a number of features from original-painter
@@ -8509,9 +8510,9 @@
         this._onSelectListeners[i](eventID);
     }
 };
-/*==================================================
+/*
  *  Overview Event Painter
- *==================================================
+ *
  */
 
 Timeline.OverviewEventPainter = function(params) {
@@ -8767,9 +8768,9 @@
 Timeline.OverviewEventPainter.prototype.showBubble = function(evt) {
     // not implemented
 };
-/*==================================================
+/*
  *  Compact Event Painter
- *==================================================
+ *
  */
 
 Timeline.CompactEventPainter = function(params) {
@@ -9831,9 +9832,9 @@
         this._onSelectListeners[i](eventIDs);
     }
 };
-/*==================================================
+/*
  *  Span Highlight Decorator
- *==================================================
+ *
  */
 
 Timeline.SpanHighlightDecorator = function(params) {
@@ -9948,9 +9949,9 @@
 Timeline.SpanHighlightDecorator.prototype.softPaint = function() {
 };
 
-/*==================================================
+/*
  *  Point Highlight Decorator
- *==================================================
+ *
  */
 
 Timeline.PointHighlightDecorator = function(params) {
@@ -10015,9 +10016,9 @@
 
 Timeline.PointHighlightDecorator.prototype.softPaint = function() {
 };
-/*==================================================
+/*
  *  Default Unit
- *==================================================
+ *
  */
 
 Timeline.NativeDateUnit = new Object();
@@ -10083,35 +10084,35 @@
     return new Date(v.getTime() + n);
 };
 
-/*==================================================
+/*
  *  Common localization strings
- *==================================================
+ *
  */
 
 Timeline.strings["fr"] = {
     wikiLinkLabel:  "Discute"
 };
 
-/*==================================================
+/*
  *  Localization of labellers.js
- *==================================================
+ *
  */
 
 Timeline.GregorianDateLabeller.monthNames["fr"] = [
     "jan", "fev", "mar", "avr", "mai", "jui", "jui", "aou", "sep", "oct", "nov", "dec"
 ];
-/*==================================================
+/*
  *  Common localization strings
- *==================================================
+ *
  */
 
 Timeline.strings["en"] = {
     wikiLinkLabel:  "Discuss"
 };
 
-/*==================================================
+/*
  *  Localization of labellers.js
- *==================================================
+ *
  */
 
 Timeline.GregorianDateLabeller.monthNames["en"] = [
--- a/web/data/cubicweb.timeline-ext.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.timeline-ext.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,49 +1,49 @@
-/*
+/**
  *  :organization: Logilab
- *  :copyright: 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  *
  */
 
-
-/* provide our own custom date parser since the default
+/**
+ * provide our own custom date parser since the default
  * one only understands iso8601 and gregorian dates
  */
 SimileAjax.NativeDateUnit.getParser = Timeline.NativeDateUnit.getParser = function(format) {
     if (typeof format == "string") {
-	if (format.indexOf('%') != -1) {
-	    return function(datestring) {
-		if (datestring) {
-		    return strptime(datestring, format);
-		}
-		return null;
-	    };
-	}
+        if (format.indexOf('%') != - 1) {
+            return function(datestring) {
+                if (datestring) {
+                    return strptime(datestring, format);
+                }
+                return null;
+            };
+        }
         format = format.toLowerCase();
     }
     if (format == "iso8601" || format == "iso 8601") {
-	return Timeline.DateTime.parseIso8601DateTime;
+        return Timeline.DateTime.parseIso8601DateTime;
     }
     return Timeline.DateTime.parseGregorianDateTime;
 };
 
 /*** CUBICWEB EVENT PAINTER *****************************************************/
 Timeline.CubicWebEventPainter = function(params) {
-//  Timeline.OriginalEventPainter.apply(this, arguments);
-   this._params = params;
-   this._onSelectListeners = [];
+    //  Timeline.OriginalEventPainter.apply(this, arguments);
+    this._params = params;
+    this._onSelectListeners = [];
 
-   this._filterMatcher = null;
-   this._highlightMatcher = null;
-   this._frc = null;
+    this._filterMatcher = null;
+    this._highlightMatcher = null;
+    this._frc = null;
 
-   this._eventIdToElmt = {};
+    this._eventIdToElmt = {};
 };
 
 Timeline.CubicWebEventPainter.prototype = new Timeline.OriginalEventPainter();
 
 Timeline.CubicWebEventPainter.prototype._paintEventLabel = function(
-  evt, text, left, top, width, height, theme) {
+evt, text, left, top, width, height, theme) {
     var doc = this._timeline.getDocument();
 
     var labelDiv = doc.createElement("div");
@@ -54,15 +54,21 @@
     labelDiv.style.top = top + "px";
 
     if (evt._obj.onclick) {
-	labelDiv.appendChild(A({'href': evt._obj.onclick}, text));
+        labelDiv.appendChild(A({
+            'href': evt._obj.onclick
+        },
+        text));
     } else if (evt._obj.image) {
-      labelDiv.appendChild(IMG({src: evt._obj.image, width: '30px', height: '30px'}));
+        labelDiv.appendChild(IMG({
+            src: evt._obj.image,
+            width: '30px',
+            height: '30px'
+        }));
     } else {
-      labelDiv.innerHTML = text;
+        labelDiv.innerHTML = text;
     }
 
-    if(evt._title != null)
-        labelDiv.title = evt._title;
+    if (evt._title != null) labelDiv.title = evt._title;
 
     var color = evt.getTextColor();
     if (color == null) {
@@ -72,29 +78,31 @@
         labelDiv.style.color = color;
     }
     var classname = evt.getClassName();
-    if(classname) labelDiv.className +=' ' + classname;
+    if (classname) labelDiv.className += ' ' + classname;
 
     this._eventLayer.appendChild(labelDiv);
 
     return {
-        left:   left,
-        top:    top,
-        width:  width,
+        left: left,
+        top: top,
+        width: width,
         height: height,
-        elmt:   labelDiv
+        elmt: labelDiv
     };
 };
 
+Timeline.CubicWebEventPainter.prototype._showBubble = function(x, y, evt) {
+    var div = DIV({
+        id: 'xxx'
+    });
+    var width = this._params.theme.event.bubble.width;
+    if (!evt._obj.bubbleUrl) {
+        evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
+    }
+    SimileAjax.WindowManager.cancelPopups();
+    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, width);
+    if (evt._obj.bubbleUrl) {
+        jQuery('#xxx').loadxhtml(evt._obj.bubbleUrl, null, 'post', 'replace');
+    }
+};
 
-Timeline.CubicWebEventPainter.prototype._showBubble = function(x, y, evt) {
-  var div = DIV({id: 'xxx'});
-  var width = this._params.theme.event.bubble.width;
-  if (!evt._obj.bubbleUrl) {
-    evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
-  }
-  SimileAjax.WindowManager.cancelPopups();
-  SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, width);
-  if (evt._obj.bubbleUrl) {
-    jQuery('#xxx').loadxhtml(evt._obj.bubbleUrl, null, 'post', 'replace');
-  }
-};
--- a/web/data/cubicweb.widgets.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/cubicweb.widgets.js	Mon Jul 19 15:37:02 2010 +0200
@@ -1,4 +1,6 @@
-/*
+/**
+ * Functions dedicated to widgets.
+ *
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
@@ -9,144 +11,175 @@
 // widget namespace
 Widgets = {};
 
-
-/* this function takes a DOM node defining a widget and
+/**
+ * .. function:: buildWidget(wdgnode)
+ *
+ * this function takes a DOM node defining a widget and
  * instantiates / builds the appropriate widget class
  */
 function buildWidget(wdgnode) {
     var wdgclass = Widgets[wdgnode.getAttribute('cubicweb:wdgtype')];
     if (wdgclass) {
-	var wdg = new wdgclass(wdgnode);
+        var wdg = new wdgclass(wdgnode);
     }
 }
 
-/* This function is called on load and is in charge to build
+/**
+ * .. function:: buildWidgets(root)
+ *
+ * This function is called on load and is in charge to build
  * JS widgets according to DOM nodes found in the page
  */
 function buildWidgets(root) {
     root = root || document;
     jQuery(root).find('.widget').each(function() {
-	if (this.getAttribute('cubicweb:loadtype') == 'auto') {
-	    buildWidget(this);
-	}
+        if (this.getAttribute('cubicweb:loadtype') == 'auto') {
+            buildWidget(this);
+        }
     });
 }
 
-
 // we need to differenciate cases where initFacetBoxEvents is called
 // with one argument or without any argument. If we use `initFacetBoxEvents`
 // as the direct callback on the jQuery.ready event, jQuery will pass some argument
 // of his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {buildWidgets();});
+jQuery(document).ready(function() {
+    buildWidgets();
+});
 
+function postJSON(url, data, callback) {
+    return jQuery.post(url, data, callback, 'json');
+}
+
+function getJSON(url, data, callback) {
+    return jQuery.get(url, data, callback, 'json');
+}
 
 Widgets.SuggestField = defclass('SuggestField', null, {
     __init__: function(node, options) {
-	var multi = node.getAttribute('cubicweb:multi') || "no";
-	options = options || {};
-	options.multiple = (multi == "yes") ? true : false;
-	var dataurl = node.getAttribute('cubicweb:dataurl');
+        var multi = node.getAttribute('cubicweb:multi') || "no";
+        options = options || {};
+        options.multiple = (multi == "yes") ? true: false;
+        var dataurl = node.getAttribute('cubicweb:dataurl');
         var method = postJSON;
-	if (options.method == 'get'){
-	  method = function(url, data, callback) {
-	    // We can't rely on jQuery.getJSON because the server
-	    // might set the Content-Type's response header to 'text/plain'
-	    jQuery.get(url, data, function(response) {
-	      callback(evalJSON(response));
-	    });
-	  };
-	}
-	var self = this; // closure
-	method(dataurl, null, function(data) {
-	    // in case we received a list of couple, we assume that the first
-	    // element is the real value to be sent, and the second one is the
-	    // value to be displayed
-	    if (data.length && data[0].length == 2) {
-		options.formatItem = function(row) { return row[1]; };
-		self.hideRealValue(node);
-		self.setCurrentValue(node, data);
-	    }
-	    jQuery(node).autocomplete(data, options);
-	});
+        if (options.method == 'get') {
+            method = function(url, data, callback) {
+                // We can't rely on jQuery.getJSON because the server
+                // might set the Content-Type's response header to 'text/plain'
+                jQuery.get(url, data, function(response) {
+                    callback(cw.evalJSON(response));
+                });
+            };
+        }
+        var self = this; // closure
+        method(dataurl, null, function(data) {
+            // in case we received a list of couple, we assume that the first
+            // element is the real value to be sent, and the second one is the
+            // value to be displayed
+            if (data.length && data[0].length == 2) {
+                options.formatItem = function(row) {
+                    return row[1];
+                };
+                self.hideRealValue(node);
+                self.setCurrentValue(node, data);
+            }
+            jQuery(node).autocomplete(data, options);
+        });
     },
 
     hideRealValue: function(node) {
-	var hidden = INPUT({'type': "hidden", 'name': node.name, 'value': node.value});
-	node.parentNode.appendChild(hidden);
-	// remove 'name' attribute from visible input so that it is not submitted
-	// and set correct value in the corresponding hidden field
-	jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
-	    hidden.value = row[0];
-	});
+        var hidden = INPUT({
+            'type': "hidden",
+            'name': node.name,
+            'value': node.value
+        });
+        node.parentNode.appendChild(hidden);
+        // remove 'name' attribute from visible input so that it is not submitted
+        // and set correct value in the corresponding hidden field
+        jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
+            hidden.value = row[0];
+        });
     },
 
     setCurrentValue: function(node, data) {
-	// called when the data is loaded to reset the correct displayed
-	// value in the visible input field (typically replacing an eid
-	// by a displayable value)
-	var curvalue = node.value;
-	if (!node.value) {
-	    return;
-	}
-	for (var i=0,length=data.length; i<length; i++) {
-	    var row = data[i];
-	    if (row[0] == curvalue) {
-		node.value = row[1];
-		return;
-	    }
-	}
+        // called when the data is loaded to reset the correct displayed
+        // value in the visible input field (typically replacing an eid
+        // by a displayable value)
+        var curvalue = node.value;
+        if (!node.value) {
+            return;
+        }
+        for (var i = 0, length = data.length; i < length; i++) {
+            var row = data[i];
+            if (row[0] == curvalue) {
+                node.value = row[1];
+                return;
+            }
+        }
     }
 });
 
 Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], {
 
-    __init__ : function(node) {
-	Widgets.SuggestField.__init__(this, node, {method: 'get'});
+    __init__: function(node) {
+        Widgets.SuggestField.__init__(this, node, {
+            method: 'get'
+        });
     }
 
 });
 
 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], {
 
-    __init__ : function(node) {
-	Widgets.SuggestField.__init__(this, node, {mustMatch: true});
+    __init__: function(node) {
+        Widgets.SuggestField.__init__(this, node, {
+            mustMatch: true
+        });
     }
 
 });
 //remote version of RestrictedSuggestField
 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
     __init__: function(node, options) {
-	var self = this;
-	var multi = "no";
-	options = options || {};
-	options.max = 50;
-	options.delay = 50;
-	options.cacheLength=0;
-	options.mustMatch = true;
+        var self = this;
+        var multi = "no";
+        options = options || {};
+        options.max = 50;
+        options.delay = 50;
+        options.cacheLength = 0;
+        options.mustMatch = true;
         // multiple selection not supported yet (still need to formalize correctly
         // initial values / display values)
-        var initialvalue = evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
+        var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
         if (!initialvalue) {
             initialvalue = node.value;
         }
-        options = jQuery.extend({dataType: 'json',
-                                 multiple: (multi == "yes") ? true : false,
-                                 parse: this.parseResult
-                                }, options);
+        options = jQuery.extend({
+            dataType: 'json',
+            multiple: (multi == "yes") ? true: false,
+            parse: this.parseResult
+        },
+        options);
         var dataurl = node.getAttribute('cubicweb:dataurl');
         // remove 'name' from original input and add the hidden one that will
         // store the actual value
-        var hidden = INPUT({'type': "hidden", 'name': node.name, 'value': initialvalue});
+        var hidden = INPUT({
+            'type': "hidden",
+            'name': node.name,
+            'value': initialvalue
+        });
         node.parentNode.appendChild(hidden);
-        jQuery(node).bind('result', {hinput: hidden, input:node}, self.hideRealValue)
-            .removeAttr('name').autocomplete(dataurl, options);
+        jQuery(node).bind('result', {
+            hinput: hidden,
+            input: node
+        },
+        self.hideRealValue).removeAttr('name').autocomplete(dataurl, options);
     },
 
-
     hideRealValue: function(evt, data, value) {
-	if (!value){
-	    value="";
-	}
+        if (!value) {
+            value = "";
+        }
         evt.data.hinput.value = value;
     },
 
@@ -156,68 +189,80 @@
      */
     parseResult: function(data) {
         var parsed = [];
-        for (var i=0; i < data.length; i++) {
-                var value = ''+data[i][0]; // a string is required later by jquery.autocomplete.js
-                var label = data[i][1];
-                parsed[parsed.length] = {
-                    data: [label],
-                    value: value,
-                    result: label
-                };
+        for (var i = 0; i < data.length; i++) {
+            var value = '' + data[i][0]; // a string is required later by jquery.autocomplete.js
+            var label = data[i][1];
+            parsed[parsed.length] = {
+                data: [label],
+                value: value,
+                result: label
+            };
         };
         return parsed;
     }
 
 });
 
-/*
+/**
+ * .. class:: Widgets.SuggestForm
+ *
  * suggestform displays a suggest field and associated validate / cancel buttons
  * constructor's argumemts are the same that BaseSuggestField widget
  */
 Widgets.SuggestForm = defclass("SuggestForm", null, {
 
-    __init__ : function(inputid, initfunc, varargs, validatefunc, options) {
-	this.validatefunc = validatefunc || noop;
-	this.sgfield = new Widgets.BaseSuggestField(inputid, initfunc,
-						    varargs, options);
-	this.oklabel = options.oklabel || 'ok';
-	this.cancellabel = options.cancellabel || 'cancel';
-	bindMethods(this);
-	connect(this.sgfield, 'validate', this, this.entryValidated);
+    __init__: function(inputid, initfunc, varargs, validatefunc, options) {
+        this.validatefunc = validatefunc || noop;
+        this.sgfield = new Widgets.BaseSuggestField(inputid, initfunc, varargs, options);
+        this.oklabel = options.oklabel || 'ok';
+        this.cancellabel = options.cancellabel || 'cancel';
+        bindMethods(this);
+        connect(this.sgfield, 'validate', this, this.entryValidated);
     },
 
-    show : function(parentnode) {
-	var sgnode = this.sgfield.builddom();
-	var buttons = DIV({'class' : "sgformbuttons"},
-			  [A({'href' : "javascript: noop();",
-			      'onclick' : this.onValidateClicked}, this.oklabel),
-			   ' / ',
-			   A({'href' : "javascript: noop();",
-			      'onclick' : this.destroy}, escapeHTML(this.cancellabel))]);
-	var formnode = DIV({'class' : "sgform"}, [sgnode, buttons]);
- 	appendChildNodes(parentnode, formnode);
-	this.sgfield.textinput.focus();
-	this.formnode = formnode;
-	return formnode;
+    show: function(parentnode) {
+        var sgnode = this.sgfield.builddom();
+        var buttons = DIV({
+            'class': "sgformbuttons"
+        },
+        [A({
+            'href': "javascript: noop();",
+            'onclick': this.onValidateClicked
+        },
+        this.oklabel), ' / ', A({
+            'href': "javascript: noop();",
+            'onclick': this.destroy
+        },
+        escapeHTML(this.cancellabel))]);
+        var formnode = DIV({
+            'class': "sgform"
+        },
+        [sgnode, buttons]);
+        appendChildNodes(parentnode, formnode);
+        this.sgfield.textinput.focus();
+        this.formnode = formnode;
+        return formnode;
     },
 
-    destroy : function() {
-	signal(this, 'destroy');
-	this.sgfield.destroy();
-	removeElement(this.formnode);
+    destroy: function() {
+        signal(this, 'destroy');
+        this.sgfield.destroy();
+        removeElement(this.formnode);
     },
 
-    onValidateClicked : function() {
-	this.validatefunc(this, this.sgfield.taglist());
+    onValidateClicked: function() {
+        this.validatefunc(this, this.sgfield.taglist());
     },
     /* just an indirection to pass the form instead of the sgfield as first parameter */
-    entryValidated : function(sgfield, taglist) {
-	this.validatefunc(this, taglist);
+    entryValidated: function(sgfield, taglist) {
+        this.validatefunc(this, taglist);
     }
 });
 
-
-/* called when the use clicks on a tree node
+/**
+ * .. function:: toggleTree(event)
+ *
+ * called when the use clicks on a tree node
  *  - if the node has a `cubicweb:loadurl` attribute, replace the content of the node
  *    by the url's content.
  *  - else, there's nothing to do, let the jquery plugin handle it.
@@ -227,120 +272,136 @@
     var url = linode.attr('cubicweb:loadurl');
     if (url) {
         linode.find('ul.placeholder').remove();
-	linode.loadxhtml(url, {callback: function(domnode) {
-	    linode.removeAttr('cubicweb:loadurl');
-	    jQuery(domnode).treeview({toggle: toggleTree,
-				      prerendered: true});
-	    return null;
-	}}, 'post', 'append');
+        linode.loadxhtml(url, {
+            callback: function(domnode) {
+                linode.removeAttr('cubicweb:loadurl');
+                jQuery(domnode).treeview({
+                    toggle: toggleTree,
+                    prerendered: true
+                });
+                return null;
+            }
+        },
+        'post', 'append');
     }
 }
 
-
-/* widget based on SIMILE's timeline widget
+/**
+ * .. class:: Widgets.TimelineWidget
+ *
+ * widget based on SIMILE's timeline widget
  * http://code.google.com/p/simile-widgets/
  *
  * Beware not to mess with SIMILE's Timeline JS namepsace !
  */
 
 Widgets.TimelineWidget = defclass("TimelineWidget", null, {
-    __init__: function (wdgnode) {
- 	var tldiv = DIV({id: "tl", style: 'height: 200px; border: 1px solid #ccc;'});
-	wdgnode.appendChild(tldiv);
-	var tlunit = wdgnode.getAttribute('cubicweb:tlunit') || 'YEAR';
-	var eventSource = new Timeline.DefaultEventSource();
-	var bandData = {
-	  eventPainter:     Timeline.CubicWebEventPainter,
-	  eventSource:    eventSource,
-	  width:          "100%",
-	  intervalUnit:   Timeline.DateTime[tlunit.toUpperCase()],
-	  intervalPixels: 100
-	};
-	var bandInfos = [ Timeline.createBandInfo(bandData) ];
-	var tl = Timeline.create(tldiv, bandInfos);
-	var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
-	Timeline.loadJSON(loadurl, function(json, url) {
-			    eventSource.loadJSON(json, url); });
+    __init__: function(wdgnode) {
+        var tldiv = DIV({
+            id: "tl",
+            style: 'height: 200px; border: 1px solid #ccc;'
+        });
+        wdgnode.appendChild(tldiv);
+        var tlunit = wdgnode.getAttribute('cubicweb:tlunit') || 'YEAR';
+        var eventSource = new Timeline.DefaultEventSource();
+        var bandData = {
+            eventPainter: Timeline.CubicWebEventPainter,
+            eventSource: eventSource,
+            width: "100%",
+            intervalUnit: Timeline.DateTime[tlunit.toUpperCase()],
+            intervalPixels: 100
+        };
+        var bandInfos = [Timeline.createBandInfo(bandData)];
+        var tl = Timeline.create(tldiv, bandInfos);
+        var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
+        Timeline.loadJSON(loadurl, function(json, url) {
+            eventSource.loadJSON(json, url);
+        });
 
     }
 });
 
 Widgets.TemplateTextField = defclass("TemplateTextField", null, {
 
-    __init__ : function(wdgnode) {
-	this.variables = getNodeAttribute(wdgnode, 'cubicweb:variables').split(',');
-	this.options = {'name'   : wdgnode.getAttribute('cubicweb:inputid'),
-			'rows' : wdgnode.getAttribute('cubicweb:rows') || 40,
-			'cols' : wdgnode.getAttribute('cubicweb:cols') || 80
-		       };
-	// this.variableRegexp = /%\((\w+)\)s/;
-	this.errorField = DIV({'class' : "errorMessage"});
-	this.textField = TEXTAREA(this.options);
-	jQuery(this.textField).bind('keyup', {'self': this}, this.highlightInvalidVariables);
-	jQuery('#substitutions').prepend(this.errorField);
-	jQuery('#substitutions .errorMessage').hide();
-	wdgnode.appendChild(this.textField);
+    __init__: function(wdgnode) {
+        this.variables = jQuery(wdgnode).attr('cubicweb:variables').split(',');
+        this.options = {
+            name: wdgnode.getAttribute('cubicweb:inputid'),
+            rows: wdgnode.getAttribute('cubicweb:rows') || 40,
+            cols: wdgnode.getAttribute('cubicweb:cols') || 80
+        };
+        // this.variableRegexp = /%\((\w+)\)s/;
+        this.errorField = DIV({
+            'class': "errorMessage"
+        });
+        this.textField = TEXTAREA(this.options);
+        jQuery(this.textField).bind('keyup', {
+            'self': this
+        },
+        this.highlightInvalidVariables);
+        jQuery('#substitutions').prepend(this.errorField);
+        jQuery('#substitutions .errorMessage').hide();
+        wdgnode.appendChild(this.textField);
     },
 
     /* signal callbacks */
 
-    highlightInvalidVariables : function(event) {
-	var self = event.data.self;
-	var text = self.textField.value;
-	var unknownVariables = [];
-	var it = 0;
-	var group = null;
-	var variableRegexp = /%\((\w+)\)s/g;
-	// emulates rgx.findAll()
-	while ( group=variableRegexp.exec(text) ) {
-	    if ( !self.variables.contains(group[1]) ) {
-		unknownVariables.push(group[1]);
-	    }
-	    it++;
-	    if (it > 5) {
-		break;
-	    }
-	}
-	var errText = '';
-	if (unknownVariables.length) {
-	    errText = "Detected invalid variables : " + ", ".join(unknownVariables);
-	    jQuery('#substitutions .errorMessage').show();
-	} else {
-	    jQuery('#substitutions .errorMessage').hide();
-	}
-	self.errorField.innerHTML = errText;
+    highlightInvalidVariables: function(event) {
+        var self = event.data.self;
+        var text = self.textField.value;
+        var unknownVariables = [];
+        var it = 0;
+        var group = null;
+        var variableRegexp = /%\((\w+)\)s/g;
+        // emulates rgx.findAll()
+        while (group = variableRegexp.exec(text)) {
+            if (!$.inArray(group[1], self.variables)) {
+                unknownVariables.push(group[1]);
+            }
+            it++;
+            if (it > 5) {
+                break;
+            }
+        }
+        var errText = '';
+        if (unknownVariables.length) {
+            errText = "Detected invalid variables : " + unknownVariables.join(', ');
+            jQuery('#substitutions .errorMessage').show();
+        } else {
+            jQuery('#substitutions .errorMessage').hide();
+        }
+        self.errorField.innerHTML = errText;
     }
 
 });
 
-/*
- * ComboBox with a textinput : allows to add a new value
- */
-
-Widgets.AddComboBox = defclass('AddComboBox', null, {
-   __init__ : function(wdgnode) {
-       jQuery("#add_newopt").click(function() {
-	  var new_val = jQuery("#newopt").val();
-	      if (!new_val){
-		  return false;
-	      }
-          name = wdgnode.getAttribute('name').split(':');
-	  this.rel = name[0];
-	  this.eid_to = name[1];
-          this.etype_to = wdgnode.getAttribute('cubicweb:etype_to');
-          this.etype_from = wdgnode.getAttribute('cubicweb:etype_from');
-     	  var d = asyncRemoteExec('add_and_link_new_entity', this.etype_to, this.rel, this.eid_to, this.etype_from, 'new_val');
-          d.addCallback(function (eid) {
-	      jQuery(wdgnode).find("option[selected]").removeAttr("selected");
-              var new_option = OPTION({'value':eid, 'selected':'selected'}, value=new_val);
-              wdgnode.appendChild(new_option);
-          });
-          d.addErrback(function (xxx) {
-              log('xxx =', xxx);
-          });
-     });
-   }
-});
-
-
-CubicWeb.provide('widgets.js');
+cw.widgets = {
+    /**
+     * .. function:: insertText(text, areaId)
+     *
+     * inspects textarea with id `areaId` and replaces the current selected text
+     * with `text`. Cursor is then set at the end of the inserted text.
+     */
+    insertText: function (text, areaId) {
+        var textarea = jQuery('#' + areaId);
+        if (document.selection) { // IE
+            var selLength;
+            textarea.focus();
+            var sel = document.selection.createRange();
+            selLength = sel.text.length;
+            sel.text = text;
+            sel.moveStart('character', selLength - text.length);
+            sel.select();
+        } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla
+            var startPos = textarea.selectionStart;
+            var endPos = textarea.selectionEnd;
+            // insert text so that it replaces the [startPos, endPos] part
+            textarea.value = textarea.value.substring(0, startPos) + text + textarea.value.substring(endPos, textarea.value.length);
+            // set cursor pos at the end of the inserted text
+            textarea.selectionStart = textarea.selectionEnd = startPos + text.length;
+            textarea.focus();
+        } else { // safety belt for other browsers
+            textarea.value += text;
+        }
+    }
+};
--- a/web/data/external_resources	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-# -*- shell-script -*-
-###############################################################################
-#
-# external resources file for core library resources
-#
-# Commented values are default values used by the application.
-#
-###############################################################################
-
-
-# CSS stylesheets to include in HTML headers
-#STYLESHEETS = DATADIR/cubicweb.css
-
-# CSS stylesheets for print
-#STYLESHEETS_PRINT = DATADIR/cubicweb.print.css
-
-#CSS stylesheets for IE
-#IE_STYLESHEETS = DATADIR/cubicweb.ie.css
-
-# Javascripts files to include in HTML headers
-#JAVASCRIPTS = DATADIR/jquery.js, DATADIR/cubicweb.python.js, DATADIR/jquery.json.js, DATADIR/cubicweb.compat.js, DATADIR/cubicweb.htmlhelpers.js
-
-# path to favicon (relative to the application main script, seen as a
-# directory, hence .. when you are not using an absolute path)
-#FAVICON = DATADIR/favicon.ico
-
-# path to the logo (relative to the application main script, seen as a
-# directory, hence .. when you are not using an absolute path)
-LOGO = DATADIR/logo.png
-
-# rss logo (link to get the rss view of a selection)
-RSS_LOGO = DATADIR/rss.png
-RSS_LOGO_16 = DATADIR/feed-icon16x16.png
-RSS_LOGO_32 = DATADIR/feed-icon32x32.png
-
-# path to search image
-SEARCH_GO =  DATADIR/go.png
-
-#FCKEDITOR_PATH = /usr/share/fckeditor/
-
-PUCE_UP = DATADIR/puce_up.png
-PUCE_DOWN = DATADIR/puce_down.png
-
-# icons for entity types
-BOOKMARK_ICON = DATADIR/icon_bookmark.gif
-EMAILADDRESS_ICON = DATADIR/icon_emailaddress.gif
-EUSER_ICON = DATADIR/icon_euser.gif
-STATE_ICON = DATADIR/icon_state.gif
-
-# other icons
-CALENDAR_ICON = DATADIR/calendar.gif
-CANCEL_EMAIL_ICON = DATADIR/sendcancel.png
-SEND_EMAIL_ICON = DATADIR/sendok.png
-DOWNLOAD_ICON = DATADIR/download.gif
-UPLOAD_ICON = DATADIR/upload.gif
-GMARKER_ICON = DATADIR/gmap_blue_marker.png
-UP_ICON = DATADIR/up.gif
-
-OK_ICON = DATADIR/ok.png
-CANCEL_ICON = DATADIR/cancel.png
-APPLY_ICON = DATADIR/plus.png
-TRASH_ICON = DATADIR/trash_can_small.png
--- a/web/data/jquery.tablesorter.js	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/data/jquery.tablesorter.js	Mon Jul 19 15:37:02 2010 +0200
@@ -705,10 +705,10 @@
 	ts.addParser({
 	    id: "json",
 	    is: function(s) {
-	        return s.startsWith('json:');
+	        return s.startswith('json:');
 	    },
 	    format: function(s,table,cell) {
-		return evalJSON(s.slice(5));
+		return cw.evalJSON(s.slice(5));
 	    },
 	  type: "text"
 	});
Binary file web/data/logo.png has changed
Binary file web/data/mail.gif has changed
Binary file web/data/nomail.gif has changed
Binary file web/data/rhythm15.png has changed
Binary file web/data/rhythm18.png has changed
Binary file web/data/rhythm20.png has changed
Binary file web/data/rhythm22.png has changed
Binary file web/data/rhythm24.png has changed
Binary file web/data/rhythm26.png has changed
Binary file web/data/tab.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/uiprops.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,138 @@
+"""define default ui properties"""
+
+# CSS stylesheets to include systematically in HTML headers
+# use the following line if you *need* to keep the old stylesheet
+STYLESHEETS =       [data('cubicweb.reset.css'),
+                     data('cubicweb.css'), ]
+STYLESHEETS_IE =    [data('cubicweb.ie.css')]
+STYLESHEETS_PRINT = [data('cubicweb.print.css')]
+
+# Javascripts files to include systematically in HTML headers
+JAVASCRIPTS = [data('jquery.js'),
+               data('jquery.corner.js'),
+               data('jquery.json.js'),
+               data('cubicweb.js'),
+               data('cubicweb.compat.js'),
+               data('cubicweb.python.js'),
+               data('cubicweb.htmlhelpers.js')]
+
+# where is installed fckeditor
+FCKEDITOR_PATH = '/usr/share/fckeditor/'
+
+# favicon and logo for the instance
+FAVICON = data('favicon.ico')
+LOGO = data('logo.png')
+
+# rss logo (link to get the rss view of a selection)
+RSS_LOGO = data('rss.png')
+RSS_LOGO_16 = data('feed-icon16x16.png')
+RSS_LOGO_32 = data('feed-icon32x32.png')
+
+# XXX cleanup resources below, some of them are probably not used
+# (at least entity types icons...)
+
+# images
+HELP = data('help.png')
+SEARCH_GO = data('go.png')
+PUCE_UP = data('puce_up.png')
+PUCE_DOWN = data('puce_down.png')
+
+# button icons
+OK_ICON = data('ok.png')
+CANCEL_ICON = data('cancel.png')
+APPLY_ICON = data('plus.png')
+TRASH_ICON = data('trash_can_small.png')
+
+# icons for entity types
+BOOKMARK_ICON = data('icon_bookmark.gif')
+EMAILADDRESS_ICON = data('icon_emailaddress.gif')
+EUSER_ICON = data('icon_euser.gif')
+STATE_ICON = data('icon_state.gif')
+
+# other icons
+CALENDAR_ICON = data('calendar.gif')
+CANCEL_EMAIL_ICON = data('sendcancel.png')
+SEND_EMAIL_ICON = data('sendok.png')
+DOWNLOAD_ICON = data('download.gif')
+UPLOAD_ICON = data('upload.gif')
+GMARKER_ICON = data('gmap_blue_marker.png')
+UP_ICON = data('up.gif')
+
+# colors, fonts, etc
+
+# default (body, html)
+defaultColor = '#000'
+defaultFontFamily = "'Bitstream Vera Sans','Lucida Grande','Lucida Sans Unicode','Geneva','Verdana',sans-serif"
+defaultSize = '12px'
+defaultLineHeight = '1.5'
+defaultLineHeightEm = lazystr('%(defaultLineHeight)sem')
+baseRhythmBg = 'rhythm18.png'
+
+inputHeight = '1.3em'
+inputPadding = 'O.2em'
+# XXX
+defaultLayoutMargin = '8px'
+
+# header
+headerBgColor = '#ff7700'
+
+# h
+h1FontSize = '1.5em' # 18px
+h1Padding = '0 0 0.14em 0 '
+h1Margin = '0.8em 0 0.5em'
+h1Color = '#000'
+h1BorderBottomStyle = lazystr('0.06em solid %(h1Color)s')
+
+h2FontSize = '1.33333em'
+h2Padding = '0.4em 0 0.35em 0'
+h2Margin = '0'
+
+h3FontSize = '1.16667em'
+h3Padding = '0.5em 0 0.57em 0'
+h3Margin = '0'
+
+# links
+aColor = '#e6820e'
+aActiveColor = aVisitedColor = aLinkColor = lazystr('%(aColor)s')
+
+
+# page frame
+pageBgColor = '#e2e2e2'
+pageContentBorderColor = '#ccc'
+pageContentBgColor = '#fff'
+pageContentPadding = '1em'
+pageMinHeight = '800px'
+
+# boxes
+boxTitleBg = lazystr('%(headerBgColor)s url("boxHeader.png") repeat-x 50%% 50%%')
+boxBodyBgColor = '#efefde'
+
+# action, search, sideBoxes
+actionBoxTitleBgColor = '#cfceb7'
+actionBoxTitleBg = lazystr('%(actionBoxTitleBgColor)s url("actionBoxHeader.png") repeat-x 50%% 50%%')
+sideBoxBodyBgColor = '#f8f8ee'
+sideBoxBodyBg = lazystr('%(sideBoxBodyBgColor)s')
+sideBoxBodyColor = '#555544'
+
+# table listing & co
+listingBorderColor = '#ccc'
+listingHeaderBgColor = '#efefef'
+listingHihligthedBgColor = '#fbfbfb'
+
+# puce
+bulletDownImg = 'url("puce_down.png") 98% 6px no-repeat'
+
+#forms
+formHeaderBgColor = lazystr('%(listingHeaderBgColor)s')
+helperColor = '#555'
+
+# button
+buttonBorderColor = '#edecd2'
+buttonBgColor = '#fffff8'
+buttonBgImg = 'url("button.png") repeat-x 50% 50%'
+
+# messages
+msgBgColor = '#f8f8ee'
+infoMsgBgImg = 'url("information.png") 5px center no-repeat'
+errorMsgBgImg = 'url("error.png") 100% 50% no-repeat'
+errorMsgColor = '#ed0d0d'
--- a/web/facet.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/facet.py	Mon Jul 19 15:37:02 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """contains utility functions and some visual component to restrict results of
 a search
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from copy import deepcopy
--- a/web/formfields.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/formfields.py	Mon Jul 19 15:37:02 2010 +0200
@@ -215,13 +215,24 @@
         self.creation_rank = Field.__creation_rank
         Field.__creation_rank += 1
 
+    def as_string(self, repr=True):
+        l = [u'<%s' % self.__class__.__name__]
+        for attr in ('name', 'eidparam', 'role', 'id', 'value'):
+            value = getattr(self, attr)
+            if value is not None and value is not _MARKER:
+                l.append('%s=%r' % (attr, value))
+        if repr:
+            l.append('@%#x' % id(self))
+        return u'%s>' % ' '.join(l)
+
     def __unicode__(self):
-        return u'<%s name=%r eidparam=%s role=%r id=%r value=%r visible=%r @%x>' % (
-            self.__class__.__name__, self.name, self.eidparam, self.role,
-            self.id, self.value, self.is_visible(), id(self))
+        return self.as_string(False)
+
+    def __str__(self):
+        return self.as_string(False).encode('UTF8')
 
     def __repr__(self):
-        return self.__unicode__().encode('utf-8')
+        return self.as_string(True).encode('UTF8')
 
     def init_widget(self, widget):
         if widget is not None:
@@ -325,7 +336,7 @@
                     value = getattr(entity, self.name)
                     if value is not None or not self.fallback_on_none_attribute:
                         return value
-            elif entity.has_eid() or entity.relation_cached(self.name, self.role):
+            elif entity.has_eid() or entity.cw_relation_cached(self.name, self.role):
                 value = [r[0] for r in entity.related(self.name, self.role)]
                 if value or not self.fallback_on_none_attribute:
                     return value
@@ -361,8 +372,11 @@
         return widget.render(form, self, renderer)
 
     def vocabulary(self, form, **kwargs):
-        """return vocabulary for this field. This method will be called by
-        widgets which requires a vocabulary.
+        """return vocabulary for this field. This method will be
+        called by widgets which requires a vocabulary.
+
+        It should return a list of tuple (label, value), where value
+        *must be an unicode string*, not a typed value.
         """
         assert self.choices is not None
         if callable(self.choices):
@@ -387,12 +401,20 @@
         if vocab and not isinstance(vocab[0], (list, tuple)):
             vocab = [(x, x) for x in vocab]
         if self.internationalizable:
-            # the short-cirtcuit 'and' boolean operator is used here to permit
-            # a valid empty string in vocabulary without attempting to translate
-            # it by gettext (which can lead to weird strings display)
-            vocab = [(label and form._cw._(label), value) for label, value in vocab]
+            # the short-cirtcuit 'and' boolean operator is used here
+            # to permit a valid empty string in vocabulary without
+            # attempting to translate it by gettext (which can lead to
+            # weird strings display)
+            vocab = [(label and form._cw._(label), value)
+                     for label, value in vocab]
         if self.sort:
             vocab = vocab_sort(vocab)
+        # XXX pre 3.9 bw compat
+        for i, (label, value) in enumerate(vocab):
+            if value is not None and not isinstance(value, basestring):
+                warn('[3.9] %s: vocabulary value should be an unicode string'
+                     % self, DeprecationWarning)
+                vocab[i] = (label, unicode(value))
         return vocab
 
     def format(self, form):
@@ -401,7 +423,7 @@
             entity = form.edited_entity
             if entity.e_schema.has_metadata(self.name, 'format') and (
                 entity.has_eid() or '%s_format' % self.name in entity):
-                return form.edited_entity.attr_metadata(self.name, 'format')
+                return form.edited_entity.cw_attr_metadata(self.name, 'format')
         return form._cw.property_value('ui.default-text-format')
 
     def encoding(self, form):
@@ -410,7 +432,7 @@
             entity = form.edited_entity
             if entity.e_schema.has_metadata(self.name, 'encoding') and (
                 entity.has_eid() or '%s_encoding' % self.name in entity):
-                return form.edited_entity.attr_metadata(self.name, 'encoding')
+                return form.edited_entity.cw_attr_metadata(self.name, 'encoding')
         return form._cw.encoding
 
     def form_init(self, form):
@@ -420,6 +442,12 @@
         pass
 
     def has_been_modified(self, form):
+        for field in self.actual_fields(form):
+            if field._has_been_modified(form):
+                return True # XXX
+        return False # not modified
+
+    def _has_been_modified(self, form):
         # fields not corresponding to an entity attribute / relations
         # are considered modified
         if not self.eidparam or not self.role or not form.edited_entity.has_eid():
@@ -445,7 +473,7 @@
         except ProcessFormError:
             return True
         except UnmodifiedField:
-            return False
+            return False # not modified
         if previous_value == new_value:
             return False # not modified
         return True
@@ -943,7 +971,7 @@
     linkedto = entity.linked_to(rtype, role)
     if linkedto:
         buildent = entity._cw.entity_from_eid
-        return [(buildent(eid).view('combobox'), eid) for eid in linkedto]
+        return [(buildent(eid).view('combobox'), unicode(eid)) for eid in linkedto]
     return []
 
 def relvoc_init(entity, rtype, role, required=False):
@@ -954,7 +982,7 @@
     # vocabulary doesn't include current values, add them
     if entity.has_eid():
         rset = entity.related(rtype, role)
-        vocab += [(e.view('combobox'), e.eid) for e in rset.entities()]
+        vocab += [(e.view('combobox'), unicode(e.eid)) for e in rset.entities()]
     return vocab
 
 def relvoc_unrelated(entity, rtype, role, limit=None):
@@ -985,7 +1013,7 @@
         if entity.eid in done:
             continue
         done.add(entity.eid)
-        res.append((entity.view('combobox'), entity.eid))
+        res.append((entity.view('combobox'), unicode(entity.eid)))
     return res
 
 
@@ -1044,7 +1072,7 @@
             form.formvalues[(self, form)] = value
 
     def format_single_value(self, req, value):
-        return value
+        return unicode(value)
 
     def process_form_value(self, form):
         """process posted form and return correctly typed value"""
@@ -1080,18 +1108,13 @@
 
 _AFF_KWARGS = uicfg.autoform_field_kwargs
 
-def guess_field(eschema, rschema, role='subject', skip_meta_attr=True, **kwargs):
+def guess_field(eschema, rschema, role='subject', **kwargs):
     """This function return the most adapted field to edit the given relation
     (`rschema`) where the given entity type (`eschema`) is the subject or object
     (`role`).
 
     The field is initialized according to information found in the schema,
     though any value can be explicitly specified using `kwargs`.
-
-    The `skip_meta_attr` flag is used to specify wether this function should
-    return a field for attributes considered as a meta-attributes
-    (e.g. describing an other attribute, such as the format or file name of a
-    file (`Bytes`) attribute).
     """
     fieldclass = None
     rdef = eschema.rdef(rschema, role)
@@ -1113,8 +1136,6 @@
         kwargs.setdefault('label', (eschema.type, rschema.type))
     kwargs.setdefault('help', rdef.description)
     if rschema.final:
-        if skip_meta_attr and rschema in eschema.meta_attributes():
-            return None
         fieldclass = FIELDS[targetschema]
         if fieldclass is StringField:
             if eschema.has_metadata(rschema, 'format'):
@@ -1140,7 +1161,6 @@
                 if metaschema is not None:
                     metakwargs = _AFF_KWARGS.etype_get(eschema, metaschema, 'subject')
                     kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
-                                                                skip_meta_attr=False,
                                                                 **metakwargs)
         return fieldclass(**kwargs)
     return RelationField.fromcardinality(card, **kwargs)
--- a/web/formwidgets.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/formwidgets.py	Mon Jul 19 15:37:02 2010 +0200
@@ -60,7 +60,6 @@
 .. autoclass:: cubicweb.web.formwidgets.AjaxWidget
 .. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget
 
-.. kill or document AddComboBoxWidget
 .. kill or document StaticFileAutoCompletionWidget
 .. kill or document LazyRestrictedAutoCompletionWidget
 .. kill or document RestrictedAutoCompletionWidget
@@ -550,7 +549,7 @@
         return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
 <img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
                 % (helperid, inputid, year, month,
-                   form._cw.external_resource('CALENDAR_ICON'),
+                   form._cw.uiprops['CALENDAR_ICON'],
                    form._cw._('calendar'), helperid) )
 
 
@@ -571,10 +570,10 @@
         # XXX find a way to understand every format
         fmt = req.property_value('ui.date-format')
         fmt = fmt.replace('%Y', 'yy').replace('%m', 'mm').replace('%d', 'dd')
-        req.add_onload(u'jqNode("%s").datepicker('
+        req.add_onload(u'cw.jqNode("%s").datepicker('
                        '{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
                        ' showOn: "button", buttonImageOnly: true})' % (
-                           domid, req.external_resource('CALENDAR_ICON'), fmt))
+                           domid, req.uiprops['CALENDAR_ICON'], fmt))
         if self.datestr is None:
             value = self.values(form, field)[0]
         else:
@@ -599,7 +598,7 @@
     def _render(self, form, field, renderer):
         req = form._cw
         domid = field.dom_id(form, self.suffix)
-        req.add_onload(u'jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
+        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]
@@ -776,24 +775,6 @@
         return entity.view('combobox')
 
 
-class AddComboBoxWidget(Select):
-    def attributes(self, form, field):
-        attrs = super(AddComboBoxWidget, self).attributes(form, field)
-        init_ajax_attributes(attrs, 'AddComboBox')
-        # XXX entity form specific
-        entity = form.edited_entity
-        attrs['cubicweb:etype_to'] = entity.e_schema
-        etype_from = entity.e_schema.subjrels[field.name].objects(entity.e_schema)[0]
-        attrs['cubicweb:etype_from'] = etype_from
-        return attrs
-
-    def _render(self, form, field, renderer):
-        return super(AddComboBoxWidget, self)._render(form, field, renderer) + u'''
-<div id="newvalue">
-  <input type="text" id="newopt" />
-  <a href="javascript:noop()" id="add_newopt">&#160;</a></div>
-'''
-
 # more widgets #################################################################
 
 class IntervalWidget(FieldWidget):
@@ -954,7 +935,7 @@
         if self.settabindex and not 'tabindex' in attrs:
             attrs['tabindex'] = form._cw.next_tabindex()
         if self.icon:
-            img = tags.img(src=form._cw.external_resource(self.icon), alt=self.icon)
+            img = tags.img(src=form._cw.uiprops[self.icon], alt=self.icon)
         else:
             img = u''
         return tags.button(img + xml_escape(label), escapecontent=False,
@@ -985,7 +966,7 @@
 
     def render(self, form, field=None, renderer=None):
         label = form._cw._(self.label)
-        imgsrc = form._cw.external_resource(self.imgressource)
+        imgsrc = form._cw.uiprops[self.imgressource]
         return '<a id="%(domid)s" href="%(href)s">'\
                '<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
             'label': label, 'imgsrc': imgsrc,
--- a/web/htmlwidgets.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/htmlwidgets.py	Mon Jul 19 15:37:02 2010 +0200
@@ -307,8 +307,8 @@
     self._cw.add_js('jquery.tablesorter.js')
     self._cw.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css'))
     """
-    highlight = "onmouseover=\"addElementClass(this, 'highlighted');\" " \
-                "onmouseout=\"removeElementClass(this, 'highlighted');\""
+    highlight = "onmouseover=\"$(this).addClass('highlighted');\" " \
+                "onmouseout=\"$(this).removeClass('highlighted');\""
 
     def __init__(self, model):
         self.model = model
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/propertysheet.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,111 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""property sheets allowing configuration of the web ui"""
+
+__docformat__ = "restructuredtext en"
+
+import re
+import os
+import os.path as osp
+
+class lazystr(object):
+    def __init__(self, string, context):
+        self.string = string
+        self.context = context
+    def __str__(self):
+        return self.string % self.context
+
+
+class PropertySheet(dict):
+    def __init__(self, cache_directory, **context):
+        self._cache_directory = cache_directory
+        self.context = context
+        self.reset()
+        context['sheet'] = self
+        context['lazystr'] = self.lazystr
+        self._percent_rgx = re.compile('%(?!\()')
+
+    def lazystr(self, str):
+        return lazystr(str, self)
+
+    def reset(self):
+        self.clear()
+        self._ordered_propfiles = []
+        self._propfile_mtime = {}
+        self._sourcefile_mtime = {}
+        self._cache = {}
+
+    def load(self, fpath):
+        scriptglobals = self.context.copy()
+        scriptglobals['__file__'] = fpath
+        execfile(fpath, scriptglobals, self)
+        self._propfile_mtime[fpath] = os.stat(fpath)[-2]
+        self._ordered_propfiles.append(fpath)
+
+    def need_reload(self):
+        for rid, (adirectory, rdirectory, mtime) in self._cache.items():
+            if os.stat(osp.join(rdirectory, rid))[-2] > mtime:
+                del self._cache[rid]
+        for fpath, mtime in self._propfile_mtime.iteritems():
+            if os.stat(fpath)[-2] > mtime:
+                return True
+        return False
+
+    def reload(self):
+        ordered_files = self._ordered_propfiles
+        self.reset()
+        for fpath in ordered_files:
+            self.load(fpath)
+
+    def reload_if_needed(self):
+        if self.need_reload():
+            self.reload()
+
+    def process_resource(self, rdirectory, rid):
+        try:
+            return self._cache[rid][0]
+        except KeyError:
+            cachefile = osp.join(self._cache_directory, rid)
+            self.debug('caching processed %s/%s into %s',
+                       rdirectory, rid, cachefile)
+            rcachedir = osp.dirname(cachefile)
+            if not osp.exists(rcachedir):
+                os.makedirs(rcachedir)
+            sourcefile = osp.join(rdirectory, rid)
+            content = file(sourcefile).read()
+            # XXX replace % not followed by a paren by %% to avoid having to do
+            # this in the source css file ?
+            try:
+                content = self.compile(content)
+            except ValueError, ex:
+                self.error("can't process %s/%s: %s", rdirectory, rid, ex)
+                adirectory = rdirectory
+            else:
+                stream = file(cachefile, 'w')
+                stream.write(content)
+                stream.close()
+                adirectory = self._cache_directory
+            self._cache[rid] = (adirectory, rdirectory, os.stat(sourcefile)[-2])
+            return adirectory
+
+    def compile(self, content):
+        return self._percent_rgx.sub('%%', content) % self
+
+from cubicweb.web import LOGGER
+from logilab.common.logging_ext import set_log_methods
+set_log_methods(PropertySheet, LOGGER)
--- a/web/request.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/request.py	Mon Jul 19 15:37:02 2010 +0200
@@ -37,14 +37,12 @@
 from cubicweb.dbapi import DBAPIRequest
 from cubicweb.mail import header
 from cubicweb.uilib import remove_html_tags
-from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
+from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid, json_dumps
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
-                          RequestError, StatusResponse, json)
+                          RequestError, StatusResponse)
 from cubicweb.web.http_headers import Headers
 
-dumps = json.dumps
-
 _MARKER = object()
 
 
@@ -83,6 +81,12 @@
         super(CubicWebRequestBase, self).__init__(vreg)
         self.authmode = vreg.config['auth-mode']
         self.https = https
+        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
         self.html_headers = HTMLHead()
         # form parameters
@@ -99,7 +103,6 @@
         self.next_tabindex = self.tabindexgen.next
         # page id, set by htmlheader template
         self.pageid = None
-        self.datadir_url = self._datadir_url()
         self._set_pageid()
         # prepare output header
         self.headers_out = Headers()
@@ -353,7 +356,7 @@
         """
         self.add_js('cubicweb.ajax.js')
         cbname = self.register_onetime_callback(cb, *args)
-        msg = dumps(msg or '')
+        msg = json_dumps(msg or '')
         return "javascript:userCallbackThenReloadPage('%s', %s)" % (
             cbname, msg)
 
@@ -564,24 +567,30 @@
                 cssfile = self.datadir_url + cssfile
             add_css(cssfile, media, *extraargs)
 
+    @deprecated('[3.9] use ajax_replace_url() instead, naming rql and vid arguments')
     def build_ajax_replace_url(self, nodeid, rql, vid, replacemode='replace',
                                **extraparams):
+        return self.ajax_replace_url(nodeid, replacemode, rql=rql, vid=vid,
+                                     **extraparams)
+
+    def ajax_replace_url(self, nodeid, replacemode='replace', **extraparams):
         """builds an ajax url that will replace nodeid's content
 
         :param nodeid: the dom id of the node to replace
-        :param rql: rql to execute
-        :param vid: the view to apply on the resultset
         :param replacemode: defines how the replacement should be done.
 
-        Possible values are :
-        - 'replace' to replace the node's content with the generated HTML
-        - 'swap' to replace the node itself with the generated HTML
-        - 'append' to append the generated HTML to the node's content
+          Possible values are :
+          - 'replace' to replace the node's content with the generated HTML
+          - 'swap' to replace the node itself with the generated HTML
+          - 'append' to append the generated HTML to the node's content
+
+        Arbitrary extra named arguments may be given, they will be included as
+        parameters of the generated url.
         """
-        url = self.build_url('view', rql=rql, vid=vid, __notemplate=1,
-                             **extraparams)
-        return "javascript: loadxhtml('%s', '%s', '%s')" % (
-            nodeid, xml_escape(url), replacemode)
+        extraparams.setdefault('fname', 'view')
+        url = self.build_url('json', **extraparams)
+        return "javascript: $('#%s').loadxhtml(%s, null, 'get', '%s'); noop()" % (
+                nodeid, json_dumps(url), replacemode)
 
     # urls/path management ####################################################
 
@@ -589,10 +598,6 @@
         """return currently accessed url"""
         return self.base_url() + self.relative_path(includeparams)
 
-    def _datadir_url(self):
-        """return url of the instance's data directory"""
-        return self.base_url() + 'data%s/' % self.vreg.config.instance_md5_version()
-
     def selected(self, url):
         """return True if the url is equivalent to currently accessed url"""
         reqpath = self.relative_path().lower()
@@ -618,25 +623,6 @@
             return controller
         return 'view'
 
-    def external_resource(self, rid, default=_MARKER):
-        """return a path to an external resource, using its identifier
-
-        raise KeyError  if the resource is not defined
-        """
-        try:
-            value = self.vreg.config.ext_resources[rid]
-        except KeyError:
-            if default is _MARKER:
-                raise
-            return default
-        if value is None:
-            return None
-        baseurl = self.datadir_url[:-1] # remove trailing /
-        if isinstance(value, list):
-            return [v.replace('DATADIR', baseurl) for v in value]
-        return value.replace('DATADIR', baseurl)
-    external_resource = cached(external_resource, keyarg=1)
-
     def validate_cache(self):
         """raise a `DirectResponse` exception if a cached page along the way
         exists and is still usable.
@@ -712,12 +698,6 @@
                            auth, ex.__class__.__name__, ex)
         return None, None
 
-    @deprecated("[3.4] use parse_accept_header('Accept-Language')")
-    def header_accept_language(self):
-        """returns an ordered list of preferred languages"""
-        return [value.split('-')[0] for value in
-                self.parse_accept_header('Accept-Language')]
-
     def parse_accept_header(self, header):
         """returns an ordered list of preferred languages"""
         accepteds = self.get_header(header, '')
@@ -823,5 +803,25 @@
                     u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">')
         return u'<div>'
 
+    @deprecated('[3.9] use req.uiprops[rid]')
+    def external_resource(self, rid, default=_MARKER):
+        """return a path to an external resource, using its identifier
+
+        raise `KeyError` if the resource is not defined
+        """
+        try:
+            return self.uiprops[rid]
+        except KeyError:
+            if default is _MARKER:
+                raise
+            return default
+
+    @deprecated("[3.4] use parse_accept_header('Accept-Language')")
+    def header_accept_language(self):
+        """returns an ordered list of preferred languages"""
+        return [value.split('-')[0] for value in
+                self.parse_accept_header('Accept-Language')]
+
+
 from cubicweb import set_log_methods
 set_log_methods(CubicWebRequestBase, LOGGER)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/pouet.css	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,3 @@
+body { background-color: %(bgcolor)s
+       font-size: 100%;
+     }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet1.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,4 @@
+bgcolor = '#000000'
+stylesheets = ['%s/cubicweb.css' % datadir_url]
+logo = '%s/logo.png' % datadir_url
+lazy = lazystr('%(bgcolor)s')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet2.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,3 @@
+fontcolor = 'black'
+bgcolor = '#FFFFFF'
+stylesheets = sheet['stylesheets'] + ['%s/mycube.css' % datadir_url]
--- a/web/test/jstest_python.jst	Thu Jul 15 12:03:13 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-// run tests with the following command line :
-// $ crosscheck jstest_python.jst
-
-crosscheck.addTest({
-
-    setup: function() {
-        crosscheck.load("testutils.js");
-        crosscheck.load("../data/jquery.js");
-        crosscheck.load("../data/cubicweb.compat.js");
-        crosscheck.load("../data/cubicweb.python.js");
-    },
-
-    test_basic_number_parsing: function () {
-	var d = strptime('2008/08/08', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
-	d = strptime('2008/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
-	d = strptime('8/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [8, 8, 8, 0, 0])
-	d = strptime('0/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [0, 8, 8, 0, 0])
-	d = strptime('-10/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [-10, 8, 8, 0, 0])
-	d = strptime('-35000', '%Y');
-	assertArrayEquals(datetuple(d), [-35000, 1, 1, 0, 0])
-    },
-
-    test_custom_format_parsing: function () {
-	var d = strptime('2008-08-08', '%Y-%m-%d');
-	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
- 	d = strptime('2008 - !  08: 08', '%Y - !  %m: %d');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
- 	d = strptime('2008-08-08 12:14', '%Y-%m-%d %H:%M');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 12, 14])
- 	d = strptime('2008-08-08 1:14', '%Y-%m-%d %H:%M');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 1, 14])
- 	d = strptime('2008-08-08 01:14', '%Y-%m-%d %H:%M');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 1, 14])
-   }
-//,
-//
-//  test_gregorian_parsing: function() {
-//     var d = parseGregorianDateTime("May 28 0100 09:00:00 GMT");
-//     assertArrayEquals(datetuple(d), [100, 5, 28, 10, 0]);
-//     d = parseGregorianDateTime("May 28 0099 09:00:00 GMT");
-//     assertArrayEquals(datetuple(d), [99, 5, 28, 10, 0]);
-//   }
-
-})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url0.html	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,3 @@
+<div id="ajaxroot">
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url1.html	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,6 @@
+<div id="ajaxroot">
+  <div class="ajaxHtmlHead">
+    <script src="http://foo.js" type="text/javascript"> </script>
+  </div>
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url2.html	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,7 @@
+<div id="ajaxroot">
+  <div class="ajaxHtmlHead">
+    <script src="http://foo.js" type="text/javascript"> </script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+  </div>
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajaxresult.json	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,1 @@
+['foo', 'bar']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_ajax.html	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,24 @@
+<html>
+  <head>
+    <!-- dependencies -->
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.dom.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.ajax.js" type="text/javascript"></script>
+    <!-- qunit files -->
+    <script type="text/javascript" src="../../../devtools/data/qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="../../../devtools/data/qunit.css" />
+    <!-- test suite -->
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_ajax.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main"> </div>
+    <h1 id="qunit-header">cubicweb.ajax.js functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_ajax.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,238 @@
+$(document).ready(function() {
+
+    module("ajax", {
+        setup: function() {
+          this.scriptsLength = $('head script[src]').length-1;
+	  this.cssLength = $('head link[rel=stylesheet]').length-1;
+        },
+        teardown: function() {
+          $('head script[src]:gt(' + this.scriptsLength + ')').remove();
+          $('head link[rel=stylesheet]:gt(' + this.cssLength + ')').remove();
+        }
+      });
+
+    function jsSources() {
+        return $.map($('head script[src]'), function(script) {
+            return script.getAttribute('src');
+        });
+    }
+
+    test('test simple h1 inclusion (ajax_url0.html)', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(jQuery('#main').children().length, 1);
+                equals(jQuery('#main h1').html(), 'Hello');
+                start();
+            }
+        });
+    });
+
+    test('test simple html head inclusion (ajax_url1.html)', function() {
+        expect(6);
+        var scriptsIncluded = jsSources();
+        equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url1.html', {
+            callback: function() {
+                var origLength = scriptsIncluded.length;
+                scriptsIncluded = jsSources();
+                // check that foo.js has been inserted in <head>
+                equals(scriptsIncluded.length, origLength + 1);
+                equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+                // check that <div class="ajaxHtmlHead"> has been removed
+                equals(jQuery('#main').children().length, 1);
+                equals(jQuery('div.ajaxHtmlHead').length, 0);
+                equals(jQuery('#main h1').html(), 'Hello');
+                start();
+            }
+        });
+    });
+
+    test('test addCallback', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function() {
+            equals(jQuery('#main').children().length, 1);
+            equals(jQuery('#main h1').html(), 'Hello');
+            start();
+        });
+    });
+
+    test('test callback after synchronous request', function() {
+        expect(1);
+        var deferred = new Deferred();
+        var result = jQuery.ajax({
+            url: './ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+        stop();
+        deferred.addCallback(function() {
+            // add an assertion to ensure the callback is executed
+            ok(true, "callback is executed");
+            start();
+        });
+    });
+
+    test('test addCallback with parameters', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function(data, req, arg1, arg2) {
+            equals(arg1, 'Hello');
+            equals(arg2, 'world');
+            start();
+        },
+        'Hello', 'world');
+    });
+
+    test('test callback after synchronous request with parameters', function() {
+        var deferred = new Deferred();
+        var result = jQuery.ajax({
+            url: './ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+        deferred.addCallback(function(data, req, arg1, arg2) {
+            // add an assertion to ensure the callback is executed
+            ok(true, "callback is executed");
+            equals(arg1, 'Hello');
+            equals(arg2, 'world');
+        },
+        'Hello', 'world');
+    });
+
+    test('test addErrback', function() {
+        expect(1);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function() {
+            // throw an exception to start errback chain
+            throw new Error();
+        });
+        d.addErrback(function() {
+            ok(true, "errback is executed");
+            start();
+        });
+    });
+
+    test('test callback / errback execution order', function() {
+        expect(4);
+        var counter = 0;
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(++counter, 1); // should be executed first
+                start();
+            }
+        });
+        d.addCallback(function() {
+            equals(++counter, 2); // should be executed and break callback chain
+            throw new Error();
+        });
+        d.addCallback(function() {
+            // should not be executed since second callback raised an error
+            ok(false, "callback is executed");
+        });
+        d.addErrback(function() {
+            // should be executed after the second callback
+            equals(++counter, 3);
+        });
+        d.addErrback(function() {
+            // should be executed after the first errback
+            equals(++counter, 4);
+        });
+    });
+
+    test('test already included resources are ignored (ajax_url2.html)', function() {
+        expect(10);
+        var scriptsIncluded = jsSources();
+        equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        equals(jQuery('head link').length, 1);
+        /* use endswith because in pytest context we have an absolute path */
+        ok(jQuery('head link').attr('href').endswith('/qunit.css'));
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url1.html', {
+            callback: function() {
+                var origLength = scriptsIncluded.length;
+                scriptsIncluded = jsSources();
+		try {
+                    // check that foo.js has been inserted in <head>
+                    equals(scriptsIncluded.length, origLength + 1);
+                    equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+                    // check that <div class="ajaxHtmlHead"> has been removed
+                    equals(jQuery('#main').children().length, 1);
+                    equals(jQuery('div.ajaxHtmlHead').length, 0);
+                    equals(jQuery('#main h1').html(), 'Hello');
+                    // qunit.css is not added twice
+                    equals(jQuery('head link').length, 1);
+                    /* use endswith because in pytest context we have an absolute path */
+                    ok(jQuery('head link').attr('href').endswith('/qunit.css'));
+                } finally {
+                    start();
+		}
+            }
+        });
+    });
+
+    test('test synchronous request loadRemote', function() {
+        var res = loadRemote('/../ajaxresult.json', {},
+        'GET', true);
+        same(res, ['foo', 'bar']);
+    });
+
+    test('test event on CubicWeb', function() {
+        expect(1);
+        stop();
+        var events = null;
+        jQuery(CubicWeb).bind('server-response', function() {
+            // check that server-response event on CubicWeb is triggered
+            events = 'CubicWeb';
+        });
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(events, 'CubicWeb');
+                start();
+            }
+        });
+    });
+
+    test('test event on node', function() {
+        expect(3);
+        stop();
+        var nodes = [];
+        jQuery('#main').bind('server-response', function() {
+            nodes.push('node');
+        });
+        jQuery(CubicWeb).bind('server-response', function() {
+            nodes.push('CubicWeb');
+        });
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(nodes.length, 2);
+                // check that server-response event on CubicWeb is triggered
+                // only once and event server-response on node is triggered
+                equals(nodes[0], 'CubicWeb');
+                equals(nodes[1], 'node');
+                start();
+            }
+        });
+    });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_htmlhelpers.html	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,21 @@
+<html>
+  <head>
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <script type="text/javascript" src="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_htmlhelpers.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">cubicweb.htmlhelpers.js functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_htmlhelpers.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,36 @@
+$(document).ready(function() {
+
+    module("module2", {
+      setup: function() {
+        $('#main').append('<select id="theselect" multiple="multiple" size="2">' +
+    			'</select>');
+      }
+    });
+
+    test("test first selected", function() {
+        $('#theselect').append('<option value="foo">foo</option>' +
+    			     '<option selected="selected" value="bar">bar</option>' +
+    			     '<option value="baz">baz</option>' +
+    			     '<option selected="selecetd"value="spam">spam</option>');
+        var selected = firstSelected(document.getElementById("theselect"));
+        equals(selected.value, 'bar');
+    });
+
+    test("test first selected 2", function() {
+        $('#theselect').append('<option value="foo">foo</option>' +
+    			     '<option value="bar">bar</option>' +
+    			     '<option value="baz">baz</option>' +
+    			     '<option value="spam">spam</option>');
+        var selected = firstSelected(document.getElementById("theselect"));
+        equals(selected, null);
+    });
+
+    module("visibilty");
+    test('toggleVisibility', function() {
+        $('#main').append('<div id="foo"></div>');
+        toggleVisibility('foo');
+        ok($('#foo').hasClass('hidden'), 'check hidden class is set');
+    });
+
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_utils.html	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,22 @@
+<html>
+  <head>
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/jquery.corner.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="utils.js" type="text/javascript"></script>
+    <script type="text/javascript" src="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_utils.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">cw.utils functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_utils.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,92 @@
+$(document).ready(function() {
+
+  module("datetime");
+
+  test("test full datetime", function() {
+      equals(cw.utils.toISOTimestamp(new Date(1986, 3, 18, 10, 30, 0, 0)),
+	     '1986-04-18 10:30:00');
+  });
+
+  test("test only date", function() {
+      equals(cw.utils.toISOTimestamp(new Date(1986, 3, 18)), '1986-04-18 00:00:00');
+  });
+
+  test("test null", function() {
+      equals(cw.utils.toISOTimestamp(null), null);
+  });
+
+  module("parsing");
+  test("test basic number parsing", function() {
+      var d = strptime('2008/08/08', '%Y/%m/%d');
+      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('2008/8/8', '%Y/%m/%d');
+      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('8/8/8', '%Y/%m/%d');
+      same(datetuple(d), [8, 8, 8, 0, 0]);
+      d = strptime('0/8/8', '%Y/%m/%d');
+      same(datetuple(d), [0, 8, 8, 0, 0]);
+      d = strptime('-10/8/8', '%Y/%m/%d');
+      same(datetuple(d), [-10, 8, 8, 0, 0]);
+      d = strptime('-35000', '%Y');
+      same(datetuple(d), [-35000, 1, 1, 0, 0]);
+  });
+
+  test("test custom format parsing", function() {
+      var d = strptime('2008-08-08', '%Y-%m-%d');
+      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('2008 - !  08: 08', '%Y - !  %m: %d');
+      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('2008-08-08 12:14', '%Y-%m-%d %H:%M');
+      same(datetuple(d), [2008, 8, 8, 12, 14]);
+      d = strptime('2008-08-08 1:14', '%Y-%m-%d %H:%M');
+      same(datetuple(d), [2008, 8, 8, 1, 14]);
+      d = strptime('2008-08-08 01:14', '%Y-%m-%d %H:%M');
+      same(datetuple(d), [2008, 8, 8, 1, 14]);
+  });
+
+  module("sliceList");
+  test("test slicelist", function() {
+      var list = ['a', 'b', 'c', 'd', 'e', 'f'];
+      same(sliceList(list, 2),  ['c', 'd', 'e', 'f']);
+      same(sliceList(list, 2, -2), ['c', 'd']);
+      same(sliceList(list, -3), ['d', 'e', 'f']);
+      same(sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
+      same(sliceList(list),  list);
+  });
+
+  module("formContents", {
+    setup: function() {
+      $('#main').append('<form id="test-form"></form>');
+    }
+  });
+  // XXX test fckeditor
+  test("test formContents", function() {
+      $('#test-form').append('<input name="input-text" ' +
+			     'type="text" value="toto" />');
+      $('#test-form').append('<textarea rows="10" cols="30" '+
+			     'name="mytextarea">Hello World!</textarea> ');
+      $('#test-form').append('<input name="choice" type="radio" ' +
+			     'value="yes" />');
+      $('#test-form').append('<input name="choice" type="radio" ' +
+			     'value="no" checked="checked"/>');
+      $('#test-form').append('<input name="check" type="checkbox" ' +
+			     'value="yes" />');
+      $('#test-form').append('<input name="check" type="checkbox" ' +
+			     'value="no" checked="checked"/>');
+      $('#test-form').append('<select id="theselect" name="theselect" ' +
+			     'multiple="multiple" size="2"></select>');
+      $('#theselect').append('<option selected="selected" ' +
+			     'value="foo">foo</option>' +
+  			     '<option value="bar">bar</option>');
+      //Append an unchecked radio input : should not be in formContents list
+      $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
+			     'value="one" />');
+      $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
+			     'value="two"/>');
+      same(formContents($('#test-form')[0]), [
+	['input-text', 'mytextarea', 'choice', 'check', 'theselect'],
+	['toto', 'Hello World!', 'no', 'no', 'foo']
+      ]);
+  });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/utils.js	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,29 @@
+function datetuple(d) {
+    return [d.getFullYear(), d.getMonth()+1, d.getDate(),
+	    d.getHours(), d.getMinutes()];
+}
+
+function pprint(obj) {
+    print('{');
+    for(k in obj) {
+	print('  ' + k + ' = ' + obj[k]);
+    }
+    print('}');
+}
+
+function arrayrepr(array) {
+    return '[' + array.join(', ') + ']';
+}
+
+function assertArrayEquals(array1, array2) {
+    if (array1.length != array2.length) {
+	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
+    }
+    for (var i=0; i<array1.length; i++) {
+	if (array1[i] != array2[i]) {
+
+	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
+						 + ' differs at index ' + i);
+	}
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/test_jscript.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,42 @@
+from cubicweb.devtools.qunit import QUnitTestCase, unittest_main
+
+from os import path as osp
+
+
+class JScript(QUnitTestCase):
+
+    all_js_tests = (
+        ("jstests/test_utils.js", (
+            "../data/cubicweb.js",
+            "../data/cubicweb.compat.js",
+            "../data/cubicweb.python.js",
+            "jstests/utils.js",
+            ),
+         ),
+
+        ("jstests/test_htmlhelpers.js", (
+            "../data/cubicweb.js",
+            "../data/cubicweb.compat.js",
+            "../data/cubicweb.python.js",
+            "../data/cubicweb.htmlhelpers.js",
+            ),
+         ),
+
+        ("jstests/test_ajax.js", (
+            "../data/cubicweb.python.js",
+            "../data/cubicweb.js",
+            "../data/cubicweb.compat.js",
+            "../data/cubicweb.htmlhelpers.js",
+            "../data/cubicweb.ajax.js",
+            ), (
+            "jstests/ajax_url0.html",
+            "jstests/ajax_url1.html",
+            "jstests/ajax_url2.html",
+            "jstests/ajaxresult.json",
+            ),
+         ),
+    )
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/test/test_views.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/test_views.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +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/>.
-"""automatic tests
-
-"""
+"""automatic tests"""
 
 from cubicweb.devtools.testlib import CubicWebTC, AutoPopulateTest, AutomaticWebTest
 from cubicweb.view import AnyRsetView
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/test_windmill.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,42 @@
+from cubicweb.devtools import cwwindmill
+
+
+class CubicWebWindmillUseCase(cwwindmill.CubicWebWindmillUseCase):
+    """class for windmill use case tests
+
+    From test server parameters:
+
+    :params ports_range: range of http ports to test (range(7000, 8000) by default)
+    :type ports_range: iterable
+    :param anonymous_logged: is anonymous user logged by default ?
+    :type anonymous_logged: bool
+
+    The first port found as available in `ports_range` will be used to launch
+    the test server
+
+    From Windmill configuration:
+
+    :param browser: browser identification string (firefox|ie|safari|chrome) (firefox by default)
+    :param test_dir: testing file path or directory (./windmill by default)
+    """
+    #ports_range = range(7000, 8000)
+    anonymous_logged = False
+    #browser = 'firefox'
+    #test_dir = osp.join(os.getcwd(), 'windmill')
+
+    # If you prefer, you can put here the use cases recorded by windmill GUI
+    # (services transformer) instead of the windmill sub-directory
+    # You can change `test_dir` as following:
+    #test_dir = __file__
+
+
+from windmill.authoring import WindmillTestClient
+def test_usecase():
+    client = WindmillTestClient(__name__)
+    import pdb; pdb.set_trace()
+    client.open(url=u'/')
+#    ...
+
+
+if __name__ == '__main__':
+    cwwindmill.unittest_main()
--- a/web/test/unittest_breadcrumbs.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_breadcrumbs.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,8 +15,10 @@
 #
 # 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
 
+
 class BreadCrumbsTC(CubicWebTC):
 
     def test_base(self):
--- a/web/test/unittest_form.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_form.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
 
 from xml.etree.ElementTree import fromstring
 
@@ -57,15 +54,15 @@
         t = self.req.create_entity('Tag', name=u'x')
         form1 = self.vreg['forms'].select('edition', self.req, entity=t)
         unrelated = [reid for rview, reid in form1.field_by_name('tags', 'subject', t.e_schema).choices(form1)]
-        self.failUnless(b.eid in unrelated, unrelated)
+        self.failUnless(unicode(b.eid) in unrelated, unrelated)
         form2 = self.vreg['forms'].select('edition', self.req, entity=b)
         unrelated = [reid for rview, reid in form2.field_by_name('tags', 'object', t.e_schema).choices(form2)]
-        self.failUnless(t.eid in unrelated, unrelated)
+        self.failUnless(unicode(t.eid) in unrelated, unrelated)
         self.execute('SET X tags Y WHERE X is Tag, Y is BlogEntry')
         unrelated = [reid for rview, reid in form1.field_by_name('tags', 'subject', t.e_schema).choices(form1)]
-        self.failIf(b.eid in unrelated, unrelated)
+        self.failIf(unicode(b.eid) in unrelated, unrelated)
         unrelated = [reid for rview, reid in form2.field_by_name('tags', 'object', t.e_schema).choices(form2)]
-        self.failIf(t.eid in unrelated, unrelated)
+        self.failIf(unicode(t.eid) in unrelated, unrelated)
 
 
     def test_form_field_vocabulary_new_entity(self):
@@ -103,7 +100,7 @@
         rset = self.execute('INSERT BlogEntry X: X title "cubicweb.org", X content "hop"')
         form = self.vreg['views'].select('doreledit', self.request(),
                                          rset=rset, row=0, rtype='content')
-        data = form.render(row=0, rtype='content')
+        data = form.render(row=0, rtype='content', formid='base')
         self.failUnless('content_format' in data)
 
     # form view tests #########################################################
--- a/web/test/unittest_formfields.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_formfields.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +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/>.
-"""unittests for cw.web.formfields
-
-"""
+"""unittests for cw.web.formfields"""
 
 from logilab.common.testlib import TestCase, unittest_main, mock_object as mock
 
@@ -53,10 +51,10 @@
         self.assertEquals(description_field.required, False)
         self.assertEquals(description_field.format_field, None)
 
+        # description_format_field = guess_field(schema['State'], schema['description_format'])
+        # self.assertEquals(description_format_field, None)
+
         description_format_field = guess_field(schema['State'], schema['description_format'])
-        self.assertEquals(description_format_field, None)
-
-        description_format_field = guess_field(schema['State'], schema['description_format'], skip_meta_attr=False)
         self.assertEquals(description_format_field.internationalizable, True)
         self.assertEquals(description_format_field.sort, True)
 
@@ -88,12 +86,12 @@
 
 
     def test_file_fields(self):
-        data_format_field = guess_field(schema['File'], schema['data_format'])
-        self.assertEquals(data_format_field, None)
-        data_encoding_field = guess_field(schema['File'], schema['data_encoding'])
-        self.assertEquals(data_encoding_field, None)
-        data_name_field = guess_field(schema['File'], schema['data_name'])
-        self.assertEquals(data_name_field, None)
+        # data_format_field = guess_field(schema['File'], schema['data_format'])
+        # self.assertEquals(data_format_field, None)
+        # data_encoding_field = guess_field(schema['File'], schema['data_encoding'])
+        # self.assertEquals(data_encoding_field, None)
+        # data_name_field = guess_field(schema['File'], schema['data_name'])
+        # self.assertEquals(data_name_field, None)
 
         data_field = guess_field(schema['File'], schema['data'])
         self.assertIsInstance(data_field, FileField)
--- a/web/test/unittest_magicsearch.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_magicsearch.py	Mon Jul 19 15:37:02 2010 +0200
@@ -16,9 +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/>.
-"""Unit tests for magic_search service
-
-"""
+"""Unit tests for cw.web.views.magicsearch"""
 
 import sys
 
@@ -128,11 +126,11 @@
         self.assertEquals(transform('CWUser', 'E'),
                           ("CWUser E",))
         self.assertEquals(transform('CWUser', 'Smith'),
-                          ('CWUser C WHERE C has_text %(text)s', {'text': 'Smith'}))
+                          ('CWUser C ORDERBY FTIRANK(C) DESC WHERE C has_text %(text)s', {'text': 'Smith'}))
         self.assertEquals(transform('utilisateur', 'Smith'),
-                          ('CWUser C WHERE C has_text %(text)s', {'text': 'Smith'}))
+                          ('CWUser C ORDERBY FTIRANK(C) DESC WHERE C has_text %(text)s', {'text': 'Smith'}))
         self.assertEquals(transform(u'adresse', 'Logilab'),
-                          ('EmailAddress E WHERE E has_text %(text)s', {'text': 'Logilab'}))
+                          ('EmailAddress E ORDERBY FTIRANK(E) DESC WHERE E has_text %(text)s', {'text': 'Logilab'}))
         self.assertEquals(transform(u'adresse', 'Logi%'),
                           ('EmailAddress E WHERE E alias LIKE %(text)s', {'text': 'Logi%'}))
         self.assertRaises(BadRQLQuery, transform, "pers", "taratata")
@@ -152,7 +150,7 @@
                           ('CWUser C WHERE C firstname LIKE %(text)s', {'text': 'cubicweb%'}))
         # expanded shortcuts
         self.assertEquals(transform('CWUser', 'use_email', 'Logilab'),
-                          ('CWUser C WHERE C use_email C1, C1 has_text %(text)s', {'text': 'Logilab'}))
+                          ('CWUser C ORDERBY FTIRANK(C1) DESC WHERE C use_email C1, C1 has_text %(text)s', {'text': 'Logilab'}))
         self.assertEquals(transform('CWUser', 'use_email', '%Logilab'),
                           ('CWUser C WHERE C use_email C1, C1 alias LIKE %(text)s', {'text': '%Logilab'}))
         self.assertRaises(BadRQLQuery, transform, 'word1', 'word2', 'word3')
@@ -160,7 +158,7 @@
     def test_quoted_queries(self):
         """tests how quoted queries are handled"""
         queries = [
-            (u'Adresse "My own EmailAddress"', ('EmailAddress E WHERE E has_text %(text)s', {'text': u'My own EmailAddress'})),
+            (u'Adresse "My own EmailAddress"', ('EmailAddress E ORDERBY FTIRANK(E) DESC WHERE E has_text %(text)s', {'text': u'My own EmailAddress'})),
             (u'Utilisateur prénom "Jean Paul"', ('CWUser C WHERE C firstname %(text)s', {'text': 'Jean Paul'})),
             (u'Utilisateur firstname "Jean Paul"', ('CWUser C WHERE C firstname %(text)s', {'text': 'Jean Paul'})),
             (u'CWUser firstname "Jean Paul"', ('CWUser C WHERE C firstname %(text)s', {'text': 'Jean Paul'})),
@@ -177,7 +175,7 @@
         queries = [
             (u'Utilisateur', (u"CWUser C",)),
             (u'Utilisateur P', (u"CWUser P",)),
-            (u'Utilisateur cubicweb', (u'CWUser C WHERE C has_text %(text)s', {'text': u'cubicweb'})),
+            (u'Utilisateur cubicweb', (u'CWUser C ORDERBY FTIRANK(C) DESC WHERE C has_text %(text)s', {'text': u'cubicweb'})),
             (u'CWUser prénom cubicweb', (u'CWUser C WHERE C firstname %(text)s', {'text': 'cubicweb'},)),
             ]
         for query, expected in queries:
@@ -203,11 +201,11 @@
         """tests QUERY_PROCESSOR"""
         queries = [
             (u'foo',
-             ("Any X WHERE X has_text %(text)s", {'text': u'foo'})),
+             ("Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s", {'text': u'foo'})),
             # XXX this sounds like a language translator test...
             # and it fails
             (u'Utilisateur Smith',
-             ('CWUser C WHERE C has_text %(text)s', {'text': u'Smith'})),
+             ('CWUser C ORDERBY FTIRANK(C) DESC WHERE C has_text %(text)s', {'text': u'Smith'})),
             (u'utilisateur nom Smith',
              ('CWUser C WHERE C surname %(text)s', {'text': u'Smith'})),
             (u'Any P WHERE P is Utilisateur, P nom "Smith"',
@@ -217,11 +215,11 @@
             rset = self.proc.process_query(query)
             self.assertEquals((rset.rql, rset.args), expected)
 
-    def test_iso88591_fulltext(self):
+    def test_accentuated_fulltext(self):
         """we must be able to type accentuated characters in the search field"""
-        rset = self.proc.process_query(u'écrire')
-        self.assertEquals(rset.rql, "Any X WHERE X has_text %(text)s")
-        self.assertEquals(rset.args, {'text': u'écrire'})
+        rset = self.proc.process_query(u'écrire')
+        self.assertEquals(rset.rql, "Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s")
+        self.assertEquals(rset.args, {'text': u'écrire'})
 
     def test_explicit_component(self):
         self.assertRaises(RQLSyntaxError,
@@ -229,7 +227,7 @@
         self.assertRaises(BadRQLQuery,
                           self.proc.process_query, u'rql: CWUser E WHERE E noattr "Smith"')
         rset = self.proc.process_query(u'text: utilisateur Smith')
-        self.assertEquals(rset.rql, 'Any X WHERE X has_text %(text)s')
+        self.assertEquals(rset.rql, 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s')
         self.assertEquals(rset.args, {'text': u'utilisateur Smith'})
 
 if __name__ == '__main__':
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_propertysheet.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,53 @@
+import os
+from os.path import join, dirname
+from shutil import rmtree
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.web.propertysheet import *
+
+DATADIR = join(dirname(__file__), 'data')
+CACHEDIR = join(DATADIR, 'uicache')
+
+class PropertySheetTC(TestCase):
+
+    def tearDown(self):
+        rmtree(CACHEDIR)
+
+    def test(self):
+        ps = PropertySheet(CACHEDIR, datadir_url='http://cwtest.com')
+        ps.load(join(DATADIR, 'sheet1.py'))
+        ps.load(join(DATADIR, 'sheet2.py'))
+        # defined by sheet1
+        self.assertEquals(ps['logo'], 'http://cwtest.com/logo.png')
+        # defined by sheet1, overriden by sheet2
+        self.assertEquals(ps['bgcolor'], '#FFFFFF')
+        # defined by sheet2
+        self.assertEquals(ps['fontcolor'], 'black')
+        # defined by sheet1, extended by sheet2
+        self.assertEquals(ps['stylesheets'], ['http://cwtest.com/cubicweb.css',
+                                              'http://cwtest.com/mycube.css'])
+        # lazy string defined by sheet1
+        self.assertIsInstance(ps['lazy'], lazystr)
+        self.assertEquals(str(ps['lazy']), '#FFFFFF')
+        # test compilation
+        self.assertEquals(ps.compile('a {bgcolor: %(bgcolor)s; size: 1%;}'),
+                          'a {bgcolor: #FFFFFF; size: 1%;}')
+        self.assertEquals(ps.process_resource(DATADIR, 'pouet.css'),
+                          CACHEDIR)
+        self.failUnless('pouet.css' in ps._cache)
+        self.failIf(ps.need_reload())
+        os.utime(join(DATADIR, 'sheet1.py'), None)
+        self.failUnless('pouet.css' in ps._cache)
+        self.failUnless(ps.need_reload())
+        self.failUnless('pouet.css' in ps._cache)
+        ps.reload()
+        self.failIf('pouet.css' in ps._cache)
+        self.failIf(ps.need_reload())
+        ps.process_resource(DATADIR, 'pouet.css') # put in cache
+        os.utime(join(DATADIR, 'pouet.css'), None)
+        self.failIf(ps.need_reload())
+        self.failIf('pouet.css' in ps._cache)
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/test/unittest_views_basecontrollers.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,17 +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/>.
-"""cubicweb.web.views.basecontrollers unit tests
-
-"""
+"""cubicweb.web.views.basecontrollers unit tests"""
 
 from logilab.common.testlib import unittest_main, mock_object
 
 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, json
+from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError
 from cubicweb.entities.authobjs import CWUser
 from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes
 u = unicode
@@ -128,7 +127,7 @@
         self.assertEquals(e.firstname, u'Th\xe9nault')
         self.assertEquals(e.surname, u'Sylvain')
         self.assertEquals([g.eid for g in e.in_group], groupeids)
-        self.assertEquals(e.state, 'activated')
+        self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated')
 
 
     def test_create_multiple_linked(self):
@@ -562,7 +561,7 @@
 #         rql = 'Any T,N WHERE T is Tag, T name N'
 #         ctrl = self.ctrl(self.request(mode='json', rql=rql, pageid='123'))
 #         self.assertEquals(ctrl.publish(),
-#                           json.dumps(self.execute(rql).rows))
+#                           json_dumps(self.execute(rql).rows))
 
     def test_remote_add_existing_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['python'])
@@ -643,14 +642,14 @@
     # silly tests
     def test_external_resource(self):
         self.assertEquals(self.remote_call('external_resource', 'RSS_LOGO')[0],
-                          json.dumps(self.request().external_resource('RSS_LOGO')))
+                          json_dumps(self.config.uiprops['RSS_LOGO']))
     def test_i18n(self):
         self.assertEquals(self.remote_call('i18n', ['bimboom'])[0],
-                          json.dumps(['bimboom']))
+                          json_dumps(['bimboom']))
 
     def test_format_date(self):
         self.assertEquals(self.remote_call('format_date', '2007-01-01 12:00:00')[0],
-                          json.dumps('2007/01/01'))
+                          json_dumps('2007/01/01'))
 
 
 
--- a/web/test/unittest_views_baseviews.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_views_baseviews.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,21 +15,17 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
 
-"""
 from logilab.common.testlib import unittest_main
 from logilab.mtconverter import html_unescape
 
 from cubicweb.devtools.testlib import CubicWebTC
-
+from cubicweb.utils import json
 from cubicweb.web.htmlwidgets import TableWidget
 from cubicweb.web.views import vid_from_rset
-from cubicweb.web import json
-loads = json.loads
 
 def loadjson(value):
-    return loads(html_unescape(value))
+    return json.loads(html_unescape(value))
 
 class VidFromRsetTC(CubicWebTC):
 
--- a/web/test/unittest_viewselector.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_viewselector.py	Mon Jul 19 15:37:02 2010 +0200
@@ -22,7 +22,7 @@
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import CW_SOFTWARE_ROOT as BASE, Binary, UnknownProperty
-from cubicweb.selectors import (match_user_groups, implements,
+from cubicweb.selectors import (match_user_groups, is_instance,
                                 specified_etype_implements, rql_condition,
                                 traced_selection)
 from cubicweb.web import NoSelectableObject
@@ -408,19 +408,19 @@
                               tableview.TableView)
 
     def test_interface_selector(self):
-        image = self.request().create_entity('Image', data_name=u'bim.png', data=Binary('bim'))
+        image = self.request().create_entity('File', data_name=u'bim.png', data=Binary('bim'))
         # image primary view priority
         req = self.request()
-        rset = req.execute('Image X WHERE X data_name "bim.png"')
+        rset = req.execute('File X WHERE X data_name "bim.png"')
         self.assertIsInstance(self.vreg['views'].select('primary', req, rset=rset),
                               idownloadable.IDownloadablePrimaryView)
 
 
     def test_score_entity_selector(self):
-        image = self.request().create_entity('Image', data_name=u'bim.png', data=Binary('bim'))
+        image = self.request().create_entity('File', data_name=u'bim.png', data=Binary('bim'))
         # image primary view priority
         req = self.request()
-        rset = req.execute('Image X WHERE X data_name "bim.png"')
+        rset = req.execute('File X WHERE X data_name "bim.png"')
         self.assertIsInstance(self.vreg['views'].select('image', req, rset=rset),
                               idownloadable.ImageView)
         fileobj = self.request().create_entity('File', data_name=u'bim.txt', data=Binary('bim'))
@@ -476,7 +476,7 @@
 
 class CWETypeRQLAction(Action):
     __regid__ = 'testaction'
-    __select__ = implements('CWEType') & rql_condition('X name "CWEType"')
+    __select__ = is_instance('CWEType') & rql_condition('X name "CWEType"')
     title = 'bla'
 
 class RQLActionTC(ViewSelectorTC):
--- a/web/test/unittest_web.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_web.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,21 +15,20 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
 
-"""
 from logilab.common.testlib import TestCase, unittest_main
 from cubicweb.devtools.fake import FakeRequest
+
 class AjaxReplaceUrlTC(TestCase):
 
     def test_ajax_replace_url(self):
         req = FakeRequest()
-        arurl = req.build_ajax_replace_url
+        arurl = req.ajax_replace_url
         # NOTE: for the simplest use cases, we could use doctest
-        self.assertEquals(arurl('foo', 'Person P', 'list'),
-                          "javascript: loadxhtml('foo', 'http://testing.fr/cubicweb/view?rql=Person%20P&amp;__notemplate=1&amp;vid=list', 'replace')")
-        self.assertEquals(arurl('foo', 'Person P', 'oneline', name='bar', age=12),
-                          '''javascript: loadxhtml('foo', 'http://testing.fr/cubicweb/view?age=12&amp;rql=Person%20P&amp;__notemplate=1&amp;vid=oneline&amp;name=bar', 'replace')''')
+        self.assertEquals(arurl('foo', rql='Person P', vid='list'),
+                          """javascript: $('#foo').loadxhtml("http://testing.fr/cubicweb/json?rql=Person%20P&fname=view&vid=list", null, 'get', 'replace'); noop()""")
+        self.assertEquals(arurl('foo', rql='Person P', vid='oneline', name='bar', age=12),
+                          """javascript: $('#foo').loadxhtml("http://testing.fr/cubicweb/json?name=bar&age=12&rql=Person%20P&fname=view&vid=oneline", null, 'get', 'replace'); noop()""")
 
 
 if __name__ == '__main__':
--- a/web/test/unittest_webconfig.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/test/unittest_webconfig.py	Mon Jul 19 15:37:02 2010 +0200
@@ -33,17 +33,16 @@
     def test_nonregr_print_css_as_list(self):
         """make sure PRINT_CSS *must* is a list"""
         config = self.config
-        req = fake.FakeRequest()
-        print_css = req.external_resource('STYLESHEETS_PRINT')
+        print_css = config.uiprops['STYLESHEETS_PRINT']
         self.failUnless(isinstance(print_css, list))
-        ie_css = req.external_resource('IE_STYLESHEETS')
+        ie_css = config.uiprops['STYLESHEETS_IE']
         self.failUnless(isinstance(ie_css, list))
 
     def test_locate_resource(self):
-        self.failUnless('FILE_ICON' in self.config.ext_resources)
-        rname = self.config.ext_resources['FILE_ICON'].replace('DATADIR/', '')
-        self.failUnless('file' in self.config.locate_resource(rname).split(os.sep))
-        cubicwebcsspath = self.config.locate_resource('cubicweb.css').split(os.sep)
+        self.failUnless('FILE_ICON' in self.config.uiprops)
+        rname = self.config.uiprops['FILE_ICON'].replace(self.config.datadir_url, '')
+        self.failUnless('file' in self.config.locate_resource(rname)[0].split(os.sep))
+        cubicwebcsspath = self.config.locate_resource('cubicweb.css')[0].split(os.sep)
         self.failUnless('web' in cubicwebcsspath or 'shared' in cubicwebcsspath) # 'shared' if tests under apycot
 
 if __name__ == '__main__':
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/windmill/test_connexion.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,35 @@
+from cubicweb.devtools import DEFAULT_SOURCES
+LOGIN, PASSWORD = DEFAULT_SOURCES['admin'].values()
+
+# Generated by the windmill services transformer
+from windmill.authoring import WindmillTestClient
+
+
+def test_connect():
+    client = WindmillTestClient(__name__)
+
+    client.open(url=u'/')
+    client.asserts.assertJS(js=u"$('#loginForm').is(':visible')")
+    client.type(text=LOGIN, id=u'__login')
+    client.type(text=PASSWORD, id=u'__password')
+    client.execJS(js=u"$('#loginForm').submit()")
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertJS(js=u'$(\'.message\').text() == "welcome %s !"' % LOGIN)
+    client.open(url=u'/logout')
+    client.open(url=u'/')
+    client.asserts.assertJS(js=u"$('#loginForm').is(':visible')")
+
+def test_wrong_connect():
+    client = WindmillTestClient(__name__)
+
+    client.open(url=u'/')
+    # XXX windmill wants to use its proxy internally on 403 :-(
+    #client.asserts.assertJS(js=u"$('#loginForm').is(':visible')")
+    #client.type(text=LOGIN, id=u'__login')
+    #client.type(text=u'novalidpassword', id=u'__password')
+    #client.click(value=u'log in')
+    client.open(url=u'/?__login=user&__password=nopassword')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertTextIn(validator=u'authentication failure', id=u'loginBox')
+    client.open(url=u'/')
+    client.asserts.assertJS(js=u"$('#loginForm').is(':visible')")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/windmill/test_creation.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,56 @@
+from cubicweb.devtools import DEFAULT_SOURCES
+LOGIN, PASSWORD = DEFAULT_SOURCES['admin'].values()
+
+# Generated by the windmill services transformer
+from windmill.authoring import WindmillTestClient
+
+def test_creation():
+    client = WindmillTestClient(__name__)
+
+    client.open(url=u'/')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.type(text=LOGIN, id=u'__login')
+    client.type(text=PASSWORD, id=u'__password')
+    client.click(value=u'log in')
+    client.waits.forPageLoad(timeout=u'20000')
+
+    # pre-condition
+    client.open(url=u'/cwuser/myuser')
+    client.asserts.assertJS(js=u'$(\'#contentmain h1\').text() == "this resource does not exist"')
+    client.open(url=u'/?rql=Any U WHERE U is CWUser, U login "myuser"')
+    client.asserts.assertJS(js=u'$(\'.searchMessage strong\').text() == "No result matching query"')
+
+    client.open(url=u'/manage')
+    client.open(url=u'/add/CWUser')
+    client.type(text=u'myuser', id=u'login-subject:A')
+    client.type(text=u'myuser', id=u'upassword-subject:A')
+    client.type(text=u'myuser', name=u'upassword-subject-confirm:A')
+    client.type(text=u'myuser', id=u'firstname-subject:A')
+    client.select(val=u'4', id=u'in_group-subject:A')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.click(id=u'adduse_email:Alink')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.type(text=u'myuser@logilab.fr', id=u'address-subject:B')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertJS(js=u'$(\'.message\').text() == "entity created"')
+    client.open(url=u'/?rql=Any U WHERE U is CWUser, U login "myuser"')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertJS(js=u'$(\'#contentmain h1\').text() == "myuser"')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.open(url=u'/cwuser/myuser?vid=sameetypelist')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.asserts.assertJS(js=u'$(\'#contentmain a\').text() == "myuser"')
+    client.open(url=u'/cwuser/myuser?vid=text')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.asserts.assertJS(js=u'$(\'#contentmain\').text() == "\\nmyuser"')
+    client.open(url=u'/cwuser/myuser?vid=deleteconf')
+    client.waits.forElement(timeout=u'8000', value=u'button_delete')
+    client.click(value=u'button_delete')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.open(url=u'/cwuser/myuser')
+    client.asserts.assertJS(js=u'$(\'#contentmain h1\').text() == "this resource does not exist"')
+    client.open(url=u'/?rql=Any U WHERE U is CWUser, U login "myuser"')
+    client.asserts.assertJS(js=u'$(\'.searchMessage strong\').text() == "No result matching query"')
+
--- a/web/uicfg.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/uicfg.py	Mon Jul 19 15:37:02 2010 +0200
@@ -218,6 +218,12 @@
             sectdict.setdefault('main', 'hidden')
             sectdict.setdefault('muledit', 'hidden')
             sectdict.setdefault('inlined', 'hidden')
+        elif role == 'subject' and rschema in sschema.meta_attributes():
+            # meta attribute, usually embeded by the described attribute's field
+            # (eg RichTextField, FileField...)
+            sectdict.setdefault('main', 'hidden')
+            sectdict.setdefault('muledit', 'hidden')
+            sectdict.setdefault('inlined', 'hidden')
         # ensure we have a tag for each form type
         if not 'main' in sectdict:
             if not rschema.final and (
@@ -383,6 +389,32 @@
 # permissions checking is by-passed and supposed to be ok
 autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides')
 
+class _ReleditTags(RelationTagsDict):
+    _keys = frozenset('reload default_value noedit'.split())
+
+    def tag_subject_of(self, key, *args, **kwargs):
+        subj, rtype, obj = key
+        if obj != '*':
+            self.warning('using explict target type in display_ctrl.tag_subject_of() '
+                         'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
+                         subj, rtype, subj, rtype, obj)
+        super(_ReleditTags, self).tag_subject_of(key, *args, **kwargs)
+
+    def tag_object_of(self, key, *args, **kwargs):
+        subj, rtype, obj = key
+        if subj != '*':
+            self.warning('using explict subject type in display_ctrl.tag_object_of() '
+                         'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
+                         rtype, obj, subj, rtype, obj)
+        super(_ReleditTags, self).tag_object_of(key, *args, **kwargs)
+
+    def tag_relation(self, key, tag):
+        for tagkey in tag.iterkeys():
+            assert tagkey in self._keys
+        return super(_ReleditTags, self).tag_relation(key, tag)
+
+reledit_ctrl = _ReleditTags('reledit')
+
 # boxes.EditBox configuration #################################################
 
 # 'link' / 'create' relation tags, used to control the "add entity" submenu
--- a/web/views/actions.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/actions.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,21 +15,22 @@
 #
 # 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 HTML base actions
+"""Set of HTML base actions"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from warnings import warn
 
+from logilab.mtconverter import xml_escape
+
 from cubicweb.schema import display_name
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import (EntitySelector, yes,
     one_line_rset, multi_lines_rset, one_etype_rset, relation_possible,
     nonempty_rset, non_final_entity,
     authenticated_user, match_user_groups, match_search_state,
-    has_permission, has_add_permission, implements,
+    has_permission, has_add_permission, is_instance, debug_mode,
     )
 from cubicweb.web import uicfg, controller, action
 from cubicweb.web.views import linksearch_select_url, vid_from_rset
@@ -322,7 +323,7 @@
     """when displaying the schema of a CWEType, offer to list entities of that type
     """
     __regid__ = 'entitiesoftype'
-    __select__ = one_line_rset() & implements('CWEType')
+    __select__ = one_line_rset() & is_instance('CWEType')
     category = 'mainactions'
     order = 40
 
@@ -398,12 +399,6 @@
     title = _('manage')
     order = 20
 
-class SiteInfoAction(ManagersAction):
-    __regid__ = 'siteinfo'
-    __select__ = match_user_groups('users','managers')
-    title = _('info')
-    order = 30
-
 
 # footer actions ###############################################################
 
@@ -418,6 +413,20 @@
     def url(self):
         return 'http://www.cubicweb.org'
 
+class GotRhythmAction(action.Action):
+    __regid__ = 'rhythm'
+    __select__ = debug_mode()
+
+    category = 'footer'
+    order = 3
+    title = _('Got rhythm?')
+
+    def url(self):
+        return xml_escape(self._cw.url()+'#')
+
+    def html_class(self):
+        self._cw.add_js('cubicweb.rhythm.js')
+        return 'rhythm'
 
 ## default actions ui configuration ###########################################
 
--- a/web/views/authentication.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/authentication.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""user authentication component
+"""user authentication component"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
--- a/web/views/autoform.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/autoform.py	Mon Jul 19 15:37:02 2010 +0200
@@ -134,8 +134,9 @@
 from cubicweb.selectors import (
     match_kwargs, match_form_params, non_final_entity,
     specified_etype_implements)
-from cubicweb.web import stdmsgs, uicfg, eid_param, dumps, \
-     form as f, formwidgets as fw, formfields as ff
+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
 
 _AFS = uicfg.autoform_section
@@ -374,7 +375,7 @@
     entities
     """
     js = u"javascript: togglePendingDelete('%s', %s);" % (
-        nodeid, xml_escape(dumps(eid)))
+        nodeid, xml_escape(json_dumps(eid)))
     return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (
         js, nodeid, label)
 
@@ -475,7 +476,7 @@
         w(u'<th class="labelCol">')
         w(u'<select id="relationSelector_%s" tabindex="%s" '
           'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
-          % (eid, req.next_tabindex(), xml_escape(dumps(eid))))
+          % (eid, req.next_tabindex(), xml_escape(json_dumps(eid))))
         w(u'<option value="">%s</option>' % _('select a relation'))
         for i18nrtype, rschema, role in field.relations:
             # more entities to link to
@@ -599,7 +600,7 @@
   </select>
 </div>
 """ % (hidden and 'hidden' or '', divid, selectid,
-       xml_escape(dumps(entity.eid)), is_cell and 'true' or 'null', relname,
+       xml_escape(json_dumps(entity.eid)), is_cell and 'true' or 'null', relname,
        '\n'.join(options))
 
     def _get_select_options(self, entity, rschema, role):
@@ -783,7 +784,7 @@
         """return a list of (relation schema, role) to edit for the entity"""
         if self.display_fields is not None:
             return self.display_fields
-        if self.edited_entity.has_eid() and not self.edited_entity.has_perm('update'):
+        if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
             return []
         # XXX we should simply put eid in the generated section, no?
         return [(rtype, role) for rtype, _, role in self._relations_by_section(
@@ -886,7 +887,7 @@
             vvreg = self._cw.vreg['views']
             # display inline-edition view for all existing related entities
             for i, relentity in enumerate(related.entities()):
-                if relentity.has_perm('update'):
+                if relentity.cw_has_perm('update'):
                     yield vvreg.select('inline-edition', self._cw,
                                        rset=related, row=i, col=0,
                                        etype=ttype, rtype=rschema, role=role,
--- a/web/views/basecomponents.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/basecomponents.py	Mon Jul 19 15:37:02 2010 +0200
@@ -19,8 +19,8 @@
 
 * the rql input form
 * the logged user link
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -59,11 +59,9 @@
           <form action="%s">
 <fieldset>
 <input type="text" id="rql" name="rql" value="%s"  title="%s" tabindex="%s" accesskey="q" class="searchField" />
-<input type="submit" value="" class="rqlsubmit" tabindex="%s" />
 </fieldset>
 ''' % (not self.cw_propval('visible') and 'hidden' or '',
-       self._cw.build_url('view'), xml_escape(rql), req._('full text or RQL query'), req.next_tabindex(),
-        req.next_tabindex()))
+       self._cw.build_url('view'), xml_escape(rql), req._('full text or RQL query'), req.next_tabindex()))
         if self._cw.search_state[0] != 'normal':
             self.w(u'<input type="hidden" name="__mode" value="%s"/>'
                    % ':'.join(req.search_state[1]))
@@ -78,8 +76,8 @@
     site_wide = True
 
     def call(self):
-        self.w(u'<a href="%s"><img class="logo" src="%s" alt="logo"/></a>'
-               % (self._cw.base_url(), self._cw.external_resource('LOGO')))
+        self.w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
+               % (self._cw.base_url(), self._cw.uiprops['LOGO']))
 
 
 class ApplHelp(component.Component):
--- a/web/views/basecontrollers.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/basecontrollers.py	Mon Jul 19 15:37:02 2010 +0200
@@ -22,17 +22,14 @@
 
 __docformat__ = "restructuredtext en"
 
-from smtplib import SMTP
-
-from logilab.common.decorators import cached
 from logilab.common.date import strptime
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, typed_eid)
-from cubicweb.utils import CubicWebJsonEncoder
+from cubicweb.utils import json, json_dumps
 from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
 from cubicweb.mail import format_mail
-from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, json_dumps, json
+from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse
 from cubicweb.web.controller import Controller
 from cubicweb.web.views import vid_from_rset, formrenderers
 
@@ -44,7 +41,7 @@
     HAS_SEARCH_RESTRICTION = False
 
 def jsonize(func):
-    """decorator to sets correct content_type and calls `json.dumps` on
+    """decorator to sets correct content_type and calls `json_dumps` on
     results
     """
     def wrapper(self, *args, **kwargs):
@@ -130,7 +127,7 @@
         if rset is None and not hasattr(req, '_rql_processed'):
             req._rql_processed = True
             if req.cnx:
-                rset = self.process_rql(req.form.get('rql'))
+                rset = self.process_rql()
             else:
                 rset = None
         if rset and rset.rowcount == 1 and '__method' in req.form:
@@ -246,7 +243,7 @@
         errback = str(self._cw.form.get('__onfailure', 'null'))
         cbargs = str(self._cw.form.get('__cbargs', 'null'))
         self._cw.set_content_type('text/html')
-        jsargs = json.dumps((status, args, entity), cls=CubicWebJsonEncoder)
+        jsargs = json_dumps((status, args, entity))
         return """<script type="text/javascript">
  window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);
 </script>""" %  (domid, callback, errback, jsargs, cbargs)
@@ -260,6 +257,12 @@
             self._cw.encoding)
         return self.response(domid, status, args, entity)
 
+def optional_kwargs(extraargs):
+    if extraargs is None:
+        return {}
+    else: # we receive unicode keys which is not supported by the **syntax
+        return dict((str(key), value)
+                    for key, value in extraargs.items())
 
 class JSonController(Controller):
     __regid__ = 'json'
@@ -339,12 +342,11 @@
             return None
         return None
 
-    def _call_view(self, view, **kwargs):
-        req = self._cw
-        divid = req.form.get('divid', 'pageContent')
+    def _call_view(self, view, paginate=False, **kwargs):
+        divid = self._cw.form.get('divid', 'pageContent')
         # we need to call pagination before with the stream set
         stream = view.set_stream()
-        if req.form.get('paginate'):
+        if paginate:
             if divid == 'pageContent':
                 # mimick main template behaviour
                 stream.write(u'<div id="pageContent">')
@@ -355,12 +357,12 @@
             if divid == 'pageContent':
                 stream.write(u'<div id="contentmain">')
         view.render(**kwargs)
-        extresources = req.html_headers.getvalue(skiphead=True)
+        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 req.form.get('paginate') and divid == 'pageContent':
+        if paginate and divid == 'pageContent':
             stream.write(u'</div></div>')
         return stream.getvalue()
 
@@ -371,6 +373,8 @@
         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)
@@ -380,7 +384,7 @@
             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)
+        return self._call_view(view, paginate=req.form.get('paginate'))
 
     @xhtmlize
     def js_prop_widget(self, propkey, varname, tabindex=None):
@@ -418,16 +422,19 @@
                                               **extraargs)
         #except NoSelectableObject:
         #    raise RemoteCallFailed('unselectable')
-        extraargs = extraargs or {}
-        stream = comp.set_stream()
-        comp.render(**extraargs)
-        # XXX why not _call_view ?
-        extresources = self._cw.html_headers.getvalue(skiphead=True)
-        if extresources:
-            stream.write(u'<div class="ajaxHtmlHead">\n')
-            stream.write(extresources)
-            stream.write(u'</div>\n')
-        return stream.getvalue()
+        return self._call_view(comp, **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)
+        elif 'rql' in self._cw.form:
+            rset = self._cw.execute(self._cw.form['rql'])
+        else:
+            rset = None
+        selectargs = optional_kwargs(selectargs)
+        view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, **selectargs)
+        return self._call_view(view, **optional_kwargs(renderargs))
 
     @check_pageid
     @xhtmlize
@@ -448,23 +455,15 @@
     @xhtmlize
     def js_reledit_form(self):
         req = self._cw
-        args = dict((x, self._cw.form[x])
-                    for x in frozenset(('rtype', 'role', 'reload', 'landing_zone')))
-        entity = self._cw.entity_from_eid(int(self._cw.form['eid']))
-        # note: default is reserved in js land
-        args['default'] = self._cw.form['default_value']
-        args['reload'] = json.loads(args['reload'])
-        rset = req.eid_rset(int(self._cw.form['eid']))
+        args = dict((x, req.form[x])
+                    for x in ('formid', 'rtype', 'role', 'reload', 'default_value'))
+        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('doreledit', req, rset=rset, rtype=args['rtype'])
-        stream = view.set_stream()
-        view.render(**args)
-        # XXX why not _call_view ?
-        extresources = req.html_headers.getvalue(skiphead=True)
-        if extresources:
-            stream.write(u'<div class="ajaxHtmlHead">\n')
-            stream.write(extresources)
-            stream.write(u'</div>\n')
-        return stream.getvalue()
+        return self._call_view(view, **args)
 
     @jsonize
     def js_i18n(self, msgids):
@@ -480,7 +479,7 @@
     @jsonize
     def js_external_resource(self, resource):
         """returns the URL of the external resource named `resource`"""
-        return self._cw.external_resource(resource)
+        return self._cw.uiprops[resource]
 
     @check_pageid
     @jsonize
@@ -580,52 +579,10 @@
     def js_add_pending_delete(self, (eidfrom, rel, eidto)):
         self._add_pending(eidfrom, rel, eidto, 'delete')
 
-    # XXX specific code. Kill me and my AddComboBox friend
-    @jsonize
-    def js_add_and_link_new_entity(self, etype_to, rel, eid_to, etype_from, value_from):
-        # create a new entity
-        eid_from = self._cw.execute('INSERT %s T : T name "%s"' % ( etype_from, value_from ))[0][0]
-        # link the new entity to the main entity
-        rql = 'SET F %(rel)s T WHERE F eid %(eid_to)s, T eid %(eid_from)s' % {'rel' : rel, 'eid_to' : eid_to, 'eid_from' : eid_from}
-        return eid_from
 
 # XXX move to massmailing
-class SendMailController(Controller):
-    __regid__ = 'sendmail'
-    __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
 
-    def recipients(self):
-        """returns an iterator on email's recipients as entities"""
-        eids = self._cw.form['recipient']
-        # eids may be a string if only one recipient was specified
-        if isinstance(eids, basestring):
-            rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
-        else:
-            rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
-        return rset.entities()
-
-    def sendmail(self, recipient, subject, body):
-        msg = format_mail({'email' : self._cw.user.get_email(),
-                           'name' : self._cw.user.dc_title(),},
-                          [recipient], body, subject)
-        if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
-            msg = self._cw._('could not connect to the SMTP server')
-            url = self._cw.build_url(__message=msg)
-            raise Redirect(url)
-
-    def publish(self, rset=None):
-        # XXX this allows users with access to an cubicweb instance to use it as
-        # a mail relay
-        body = self._cw.form['mailbody']
-        subject = self._cw.form['subject']
-        for recipient in self.recipients():
-            text = body % recipient.as_email_context()
-            self.sendmail(recipient.get_email(), subject, text)
-        url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
-        raise Redirect(url)
-
-
-class MailBugReportController(SendMailController):
+class MailBugReportController(Controller):
     __regid__ = 'reportbug'
     __select__ = match_form_params('description')
 
@@ -636,7 +593,7 @@
         raise Redirect(url)
 
 
-class UndoController(SendMailController):
+class UndoController(Controller):
     __regid__ = 'undo'
     __select__ = authenticated_user() & match_form_params('txuuid')
 
--- a/web/views/basetemplates.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/basetemplates.py	Mon Jul 19 15:37:02 2010 +0200
@@ -132,14 +132,14 @@
             'etypenavigation', self._cw, rset=self.cw_rset)
         if etypefilter and etypefilter.cw_propval('visible'):
             etypefilter.render(w=w)
-        self.nav_html = UStringIO()
+        nav_html = UStringIO()
         if view:
-            view.paginate(w=self.nav_html.write)
-        w(_(self.nav_html.getvalue()))
+            view.paginate(w=nav_html.write)
+        w(_(nav_html.getvalue()))
         w(u'<div id="contentmain">\n')
         view.render(w=w)
         w(u'</div>\n') # close id=contentmain
-        w(_(self.nav_html.getvalue()))
+        w(_(nav_html.getvalue()))
         w(u'</div>\n') # closes id=pageContent
         self.template_footer(view)
 
@@ -168,7 +168,7 @@
         self.wview('header', rset=self.cw_rset, view=view)
         w(u'<div id="page"><table width="100%" border="0" id="mainLayout"><tr>\n')
         self.nav_column(view, 'left')
-        w(u'<td id="contentcol">\n')
+        w(u'<td id="contentColumn">\n')
         components = self._cw.vreg['components']
         rqlcomp = components.select_or_none('rqlinput', self._cw, rset=self.cw_rset)
         if rqlcomp:
@@ -190,7 +190,7 @@
         boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context=context))
         if boxes:
-            self.w(u'<td class="navcol"><div class="navboxes">\n')
+            self.w(u'<td id="navColumn%s"><div class="navboxes">\n' % context.capitalize())
             for box in boxes:
                 box.render(w=self.w, view=view)
             self.w(u'</div></td>\n')
@@ -254,7 +254,7 @@
         w(u'<body>\n')
         w(u'<div id="page">')
         w(u'<table width="100%" height="100%" border="0"><tr>\n')
-        w(u'<td class="navcol">\n')
+        w(u'<td id="navColumnLeft">\n')
         self.topleft_header()
         boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context='left'))
@@ -272,7 +272,7 @@
 
     def topleft_header(self):
         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
-                                                      rset=self.cw_rset)
+                                                          rset=self.cw_rset)
         if logo and logo.cw_propval('visible'):
             self.w(u'<table id="header"><tr>\n')
             self.w(u'<td>')
@@ -294,22 +294,22 @@
         self.alternates()
 
     def favicon(self):
-        favicon = self._cw.external_resource('FAVICON', None)
+        favicon = self._cw.uiprops.get('FAVICON', None)
         if favicon:
             self.whead(u'<link rel="shortcut icon" href="%s"/>\n' % favicon)
 
     def stylesheets(self):
         req = self._cw
         add_css = req.add_css
-        for css in req.external_resource('STYLESHEETS'):
+        for css in req.uiprops['STYLESHEETS']:
             add_css(css, localfile=False)
-        for css in req.external_resource('STYLESHEETS_PRINT'):
+        for css in req.uiprops['STYLESHEETS_PRINT']:
             add_css(css, u'print', localfile=False)
-        for css in req.external_resource('IE_STYLESHEETS'):
+        for css in req.uiprops['STYLESHEETS_IE']:
             add_css(css, localfile=False, ieonly=True)
 
     def javascripts(self):
-        for jscript in self._cw.external_resource('JAVASCRIPTS'):
+        for jscript in self._cw.uiprops['JAVASCRIPTS']:
             self._cw.add_js(jscript, localfile=False)
 
     def alternates(self):
@@ -389,13 +389,15 @@
 
     def call(self, **kwargs):
         req = self._cw
-        self.w(u'<div class="footer">')
+        self.w(u'<div id="footer">')
         actions = self._cw.vreg['actions'].possible_actions(self._cw,
                                                             rset=self.cw_rset)
         footeractions = actions.get('footer', ())
         for i, action in enumerate(footeractions):
-            self.w(u'<a href="%s">%s</a>' % (action.url(),
-                                             self._cw._(action.title)))
+            self.w(u'<a href="%s"' % action.url())
+            if getattr(action, 'html_class'):
+                self.w(u' class="%s"' % action.html_class())
+            self.w(u'>%s</a>' % self._cw._(action.title))
             if i < (len(footeractions) - 1):
                 self.w(u' | ')
         self.w(u'</div>')
@@ -469,7 +471,7 @@
             self.w(u'<div id="loginTitle">%s</div>' % stitle)
         self.w(u'<div id="loginContent">\n')
         if showmessage and self._cw.message:
-            self.w(u'<div class="simpleMessage">%s</div>\n' % self._cw.message)
+            self.w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
         if self._cw.vreg.config['auth-mode'] != 'http':
             # Cookie authentication
             self.login_form(id)
--- a/web/views/bookmark.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/bookmark.py	Mon Jul 19 15:37:02 2010 +0200
@@ -23,7 +23,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import Unauthorized
-from cubicweb.selectors import implements, one_line_rset
+from cubicweb.selectors import is_instance, one_line_rset
 from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, RawBoxItem
 from cubicweb.web import action, box, uicfg, formwidgets as fw
 from cubicweb.web.views import primary
@@ -43,7 +43,7 @@
 
 class FollowAction(action.Action):
     __regid__ = 'follow'
-    __select__ = one_line_rset() & implements('Bookmark')
+    __select__ = one_line_rset() & is_instance('Bookmark')
 
     title = _('follow')
     category = 'mainactions'
@@ -53,7 +53,7 @@
 
 
 class BookmarkPrimaryView(primary.PrimaryView):
-    __select__ = implements('Bookmark')
+    __select__ = is_instance('Bookmark')
 
     def cell_call(self, row, col):
         """the primary view for bookmark entity"""
@@ -96,7 +96,7 @@
         eschema = self._cw.vreg.schema.eschema(self.etype)
         candelete = rschema.has_perm(req, 'delete', toeid=ueid)
         if candelete:
-            req.add_js( ('cubicweb.ajax.js', 'cubicweb.bookmarks.js') )
+            req.add_js('cubicweb.ajax.js')
         else:
             dlink = None
         for bookmark in rset.entities():
--- a/web/views/calendar.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/calendar.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,20 +15,36 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
+"""html calendar views"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from datetime import datetime, date, timedelta
 
 from logilab.mtconverter import xml_escape
-from logilab.common.date import strptime, date_range, todate, todatetime
+from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
 
 from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+
+class ICalendarableAdapter(EntityAdapter):
+    __regid__ = 'ICalendarable'
+    __select__ = implements(ICalendarable, warn=False) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('ICalendarable')
+    def start(self):
+        """return start date"""
+        raise NotImplementedError
+
+    @property
+    @implements_adapter_compat('ICalendarable')
+    def stop(self):
+        """return stop state"""
+        raise NotImplementedError
 
 
 # useful constants & functions ################################################
@@ -52,7 +68,7 @@
 
         Does apply to ICalendarable compatible entities
         """
-        __select__ = implements(ICalendarable)
+        __select__ = adaptable('ICalendarable')
         paginable = False
         content_type = 'text/calendar'
         title = _('iCalendar')
@@ -66,10 +82,11 @@
                 event = ical.add('vevent')
                 event.add('summary').value = task.dc_title()
                 event.add('description').value = task.dc_description()
-                if task.start:
-                    event.add('dtstart').value = task.start
-                if task.stop:
-                    event.add('dtend').value = task.stop
+                icalendarable = task.cw_adapt_to('ICalendarable')
+                if icalendarable.start:
+                    event.add('dtstart').value = icalendarable.start
+                if icalendarable.stop:
+                    event.add('dtend').value = icalendarable.stop
 
             buff = ical.serialize()
             if not isinstance(buff, unicode):
@@ -85,7 +102,7 @@
     Does apply to ICalendarable compatible entities
     """
     __regid__ = 'hcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
     paginable = False
     title = _('hCalendar')
     #templatable = False
@@ -98,10 +115,15 @@
             self.w(u'<h3 class="summary">%s</h3>' % xml_escape(task.dc_title()))
             self.w(u'<div class="description">%s</div>'
                    % task.dc_description(format='text/html'))
-            if task.start:
-                self.w(u'<abbr class="dtstart" title="%s">%s</abbr>' % (task.start.isoformat(), self._cw.format_date(task.start)))
-            if task.stop:
-                self.w(u'<abbr class="dtstop" title="%s">%s</abbr>' % (task.stop.isoformat(), self._cw.format_date(task.stop)))
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            if icalendarable.start:
+                self.w(u'<abbr class="dtstart" title="%s">%s</abbr>'
+                       % (icalendarable.start.isoformat(),
+                          self._cw.format_date(icalendarable.start)))
+            if icalendarable.stop:
+                self.w(u'<abbr class="dtstop" title="%s">%s</abbr>'
+                       % (icalendarable.stop.isoformat(),
+                          self._cw.format_date(icalendarable.stop)))
             self.w(u'</div>')
         self.w(u'</div>')
 
@@ -113,10 +135,15 @@
         task = self.cw_rset.complete_entity(row, 0)
         task.view('oneline', w=self.w)
         if dates:
-            if task.start and task.stop:
-                self.w('<br/>' % self._cw._('from %(date)s' % {'date': self._cw.format_date(task.start)}))
-                self.w('<br/>' % self._cw._('to %(date)s' % {'date': self._cw.format_date(task.stop)}))
-                self.w('<br/>to %s'%self._cw.format_date(task.stop))
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            if icalendarable.start and icalendarable.stop:
+                self.w('<br/> %s' % self._cw._('from %(date)s')
+                       % {'date': self._cw.format_date(icalendarable.start)})
+                self.w('<br/> %s' % self._cw._('to %(date)s')
+                       % {'date': self._cw.format_date(icalendarable.stop)})
+            else:
+                self.w('<br/>%s'%self._cw.format_date(icalendarable.start
+                                                      or icalendarable.stop))
 
 class CalendarLargeItemView(CalendarItemView):
     __regid__ = 'calendarlargeitem'
@@ -128,22 +155,25 @@
         self.color = color
         self.index = index
         self.length = 1
+        icalendarable = task.cw_adapt_to('ICalendarable')
+        self.start = icalendarable.start
+        self.stop = icalendarable.stop
 
     def in_working_hours(self):
         """predicate returning True is the task is in working hours"""
-        if todatetime(self.task.start).hour > 7 and todatetime(self.task.stop).hour < 20:
+        if todatetime(self.start).hour > 7 and todatetime(self.stop).hour < 20:
             return True
         return False
 
     def is_one_day_task(self):
-        task = self.task
-        return task.start and task.stop and task.start.isocalendar() ==  task.stop.isocalendar()
+        return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar()
 
 
 class OneMonthCal(EntityView):
     """At some point, this view will probably replace ampm calendars"""
     __regid__ = 'onemonthcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
+
     paginable = False
     title = _('one month')
 
@@ -181,13 +211,14 @@
             else:
                 user = None
             the_dates = []
-            tstart = task.start
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            tstart = icalendarable.start
             if tstart:
-                tstart = todate(task.start)
+                tstart = todate(icalendarable.start)
                 if tstart > lastday:
                     continue
                 the_dates = [tstart]
-            tstop = task.stop
+            tstop = icalendarable.stop
             if tstop:
                 tstop = todate(tstop)
                 if tstop < firstday:
@@ -199,7 +230,7 @@
                         the_dates = [tstart]
                 else:
                     the_dates = date_range(max(tstart, firstday),
-                                           min(tstop, lastday))
+                                           min(tstop + ONEDAY, lastday))
             if not the_dates:
                 continue
 
@@ -278,12 +309,14 @@
         prevdate = curdate - timedelta(31)
         nextdate = curdate + timedelta(31)
         rql = self.cw_rset.printable_rql()
-        prevlink = self._cw.build_ajax_replace_url('onemonthcalid', rql, 'onemonthcal',
-                                                   year=prevdate.year,
-                                                   month=prevdate.month)
-        nextlink = self._cw.build_ajax_replace_url('onemonthcalid', rql, 'onemonthcal',
-                                                   year=nextdate.year,
-                                                   month=nextdate.month)
+        prevlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
+                                             vid='onemonthcal',
+                                             year=prevdate.year,
+                                             month=prevdate.month)
+        nextlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
+                                             vid='onemonthcal',
+                                             year=nextdate.year,
+                                             month=nextdate.month)
         return prevlink, nextlink
 
     def _build_calendar_cell(self, celldate, rows, curdate):
@@ -335,7 +368,8 @@
 class OneWeekCal(EntityView):
     """At some point, this view will probably replace ampm calendars"""
     __regid__ = 'oneweekcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
+
     paginable = False
     title = _('one week')
 
@@ -361,15 +395,16 @@
         # colors here are class names defined in cubicweb.css
         colors = [ "col%x" % i for i in range(12) ]
         next_color_index = 0
-        done_tasks = []
+        done_tasks = set()
         for row in xrange(self.cw_rset.rowcount):
             task = self.cw_rset.get_entity(row, 0)
-            if task in done_tasks:
+            if task.eid in done_tasks:
                 continue
-            done_tasks.append(task)
+            done_tasks.add(task.eid)
             the_dates = []
-            tstart = task.start
-            tstop = task.stop
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            tstart = icalendarable.start
+            tstop = icalendarable.stop
             if tstart:
                 tstart = todate(tstart)
                 if tstart > lastday:
@@ -382,7 +417,7 @@
                 the_dates = [tstop]
             if tstart and tstop:
                 the_dates = date_range(max(tstart, firstday),
-                                       min(tstop, lastday))
+                                       min(tstop + ONEDAY, lastday))
             if not the_dates:
                 continue
 
@@ -462,7 +497,7 @@
     def _build_calendar_cell(self, date, task_descrs):
         inday_tasks = [t for t in task_descrs if t.is_one_day_task() and  t.in_working_hours()]
         wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
-        inday_tasks.sort(key=lambda t:t.task.start)
+        inday_tasks.sort(key=lambda t:t.start)
         sorted_tasks = []
         for i, t in enumerate(wholeday_tasks):
             t.index = i
@@ -470,7 +505,7 @@
         while inday_tasks:
             t = inday_tasks.pop(0)
             for i, c in enumerate(sorted_tasks):
-                if not c or c[-1].task.stop <= t.task.start:
+                if not c or c[-1].stop <= t.start:
                     c.append(t)
                     t.index = i+ncols
                     break
@@ -491,15 +526,15 @@
             start_min = 0
             stop_hour = 20
             stop_min = 0
-            if task.start:
-                if date < todate(task.start) < date + ONEDAY:
-                    start_hour = max(8, task.start.hour)
-                    start_min = task.start.minute
-            if task.stop:
-                if date < todate(task.stop) < date + ONEDAY:
-                    stop_hour = min(20, task.stop.hour)
+            if task_desc.start:
+                if date < todate(task_desc.start) < date + ONEDAY:
+                    start_hour = max(8, task_desc.start.hour)
+                    start_min = task_desc.start.minute
+            if task_desc.stop:
+                if date < todate(task_desc.stop) < date + ONEDAY:
+                    stop_hour = min(20, task_desc.stop.hour)
                     if stop_hour < 20:
-                        stop_min = task.stop.minute
+                        stop_min = task_desc.stop.minute
 
             height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
             top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
@@ -518,7 +553,7 @@
             self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
             task.view('tooltip', w=self.w)
             self.w(u'</div>')
-            if task.start is None:
+            if task_desc.start is None:
                 self.w(u'<div class="bottommarker">')
                 self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
                 self.w(u'</div>')
@@ -535,10 +570,12 @@
         prevdate = curdate - timedelta(7)
         nextdate = curdate + timedelta(7)
         rql = self.cw_rset.printable_rql()
-        prevlink = self._cw.build_ajax_replace_url('oneweekcalid', rql, 'oneweekcal',
-                                                   year=prevdate.year,
-                                                   week=prevdate.isocalendar()[1])
-        nextlink = self._cw.build_ajax_replace_url('oneweekcalid', rql, 'oneweekcal',
-                                                   year=nextdate.year,
-                                                   week=nextdate.isocalendar()[1])
+        prevlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
+                                             vid='oneweekcal',
+                                             year=prevdate.year,
+                                             week=prevdate.isocalendar()[1])
+        nextlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
+                                             vid='oneweekcal',
+                                             year=nextdate.year,
+                                             week=nextdate.isocalendar()[1])
         return prevlink, nextlink
--- a/web/views/cwproperties.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/cwproperties.py	Mon Jul 19 15:37:02 2010 +0200
@@ -26,7 +26,7 @@
 from logilab.common.decorators import cached
 
 from cubicweb import UnknownProperty
-from cubicweb.selectors import (one_line_rset, none_rset, implements,
+from cubicweb.selectors import (one_line_rset, none_rset, is_instance,
                                 match_user_groups, objectify_selector,
                                 logged_user_in_rset)
 from cubicweb.view import StartupView
@@ -35,7 +35,7 @@
 from cubicweb.web.formfields import FIELDS, StringField
 from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
                                       FieldWidget)
-from cubicweb.web.views import primary, formrenderers
+from cubicweb.web.views import primary, formrenderers, editcontroller
 
 uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
 
@@ -74,7 +74,7 @@
 
 
 class CWPropertyPrimaryView(primary.PrimaryView):
-    __select__ = implements('CWProperty')
+    __select__ = is_instance('CWProperty')
     skip_none = False
 
 
@@ -144,7 +144,7 @@
         for label, group, form in sorted((_(g), g, f)
                                          for g, f in mainopts.iteritems()):
             status = css_class(self._group_status(group))
-            w(u'<h2 class="propertiesform">%s</h2>\n' %
+            w(u'<div class="propertiesform">%s</div>\n' %
             (make_togglable_link('fieldset_' + group, label.capitalize())))
             w(u'<div id="fieldset_%s" %s>' % (group, status))
             w(u'<fieldset class="preferences">')
@@ -154,7 +154,7 @@
         for label, group, objects in sorted((_(g), g, o)
                                             for g, o in groupedopts.iteritems()):
             status = css_class(self._group_status(group))
-            w(u'<h2 class="propertiesform">%s</h2>\n' %
+            w(u'<div class="propertiesform">%s</div>\n' %
               (make_togglable_link('fieldset_' + group, label.capitalize())))
             w(u'<div id="fieldset_%s" %s>' % (group, status))
             # create selection
@@ -243,7 +243,7 @@
     __select__ = (
         (none_rset() & match_user_groups('users','managers'))
         | (one_line_rset() & match_user_groups('users') & logged_user_in_rset())
-        | (one_line_rset() & match_user_groups('managers') & implements('CWUser'))
+        | (one_line_rset() & match_user_groups('managers') & is_instance('CWUser'))
         )
 
     title = _('preferences')
@@ -396,6 +396,15 @@
         w(u'</div>')
 
 
+class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter):
+    __select__ = is_instance('CWProperty')
+
+    def after_deletion_path(self):
+        """return (path, parameters) which should be used as redirect
+        information when this entity is being deleted
+        """
+        return 'view', {}
+
 _afs = uicfg.autoform_section
 _afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
 _afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
--- a/web/views/cwuser.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/cwuser.py	Mon Jul 19 15:37:02 2010 +0200
@@ -21,7 +21,7 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import one_line_rset, implements, match_user_groups
+from cubicweb.selectors import one_line_rset, is_instance, match_user_groups
 from cubicweb.view import EntityView
 from cubicweb.web import action, uicfg
 from cubicweb.web.views import tabs
@@ -38,7 +38,7 @@
 
 class UserPreferencesEntityAction(action.Action):
     __regid__ = 'prefs'
-    __select__ = (one_line_rset() & implements('CWUser') &
+    __select__ = (one_line_rset() & is_instance('CWUser') &
                   match_user_groups('owners', 'managers'))
 
     title = _('preferences')
@@ -51,7 +51,7 @@
 
 class FoafView(EntityView):
     __regid__ = 'foaf'
-    __select__ = implements('CWUser')
+    __select__ = is_instance('CWUser')
 
     title = _('foaf')
     templatable = False
@@ -80,7 +80,7 @@
         if entity.firstname:
             self.w(u'<foaf:givenname>%s</foaf:givenname>\n'
                    % xml_escape(entity.firstname))
-        emailaddr = entity.get_email()
+        emailaddr = entity.cw_adapt_to('IEmailable').get_email()
         if emailaddr:
             self.w(u'<foaf:mbox>%s</foaf:mbox>\n' % xml_escape(emailaddr))
         self.w(u'</foaf:Person>\n')
@@ -93,14 +93,14 @@
 
 
 class CWGroupPrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWGroup')
+    __select__ = is_instance('CWGroup')
     tabs = [_('cwgroup-main'), _('cwgroup-permissions')]
     default_tab = 'cwgroup-main'
 
 
 class CWGroupMainTab(tabs.PrimaryTab):
     __regid__ = 'cwgroup-main'
-    __select__ = tabs.PrimaryTab.__select__ & implements('CWGroup')
+    __select__ = tabs.PrimaryTab.__select__ & is_instance('CWGroup')
 
     def render_entity_attributes(self, entity):
         rql = 'Any U, FN, LN, CD, LL ORDERBY L WHERE U in_group G, ' \
@@ -114,7 +114,7 @@
 
 class CWGroupPermTab(EntityView):
     __regid__ = 'cwgroup-permissions'
-    __select__ = implements('CWGroup')
+    __select__ = is_instance('CWGroup')
 
     def cell_call(self, row, col):
         self._cw.add_css(('cubicweb.schema.css','cubicweb.acl.css'))
@@ -140,7 +140,7 @@
 
 class CWGroupInContextView(EntityView):
     __regid__ = 'incontext'
-    __select__ = implements('CWGroup')
+    __select__ = is_instance('CWGroup')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
--- a/web/views/debug.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/debug.py	Mon Jul 19 15:37:02 2010 +0200
@@ -25,6 +25,7 @@
 
 from cubicweb.selectors import none_rset, match_user_groups
 from cubicweb.view import StartupView
+from cubicweb.web.views import actions
 
 def dict_to_html(w, dict):
     # XHTML doesn't allow emtpy <ul> nodes
@@ -37,10 +38,17 @@
 
 
 
+class SiteInfoAction(actions.ManagersAction):
+    __regid__ = 'siteinfo'
+    __select__ = match_user_groups('users','managers')
+    title = _('info')
+    order = 30
+
+
 class ProcessInformationView(StartupView):
     """display various web server /repository information"""
     __regid__ = 'info'
-    __select__ = none_rset() & match_user_groups('managers')
+    __select__ = none_rset() & match_user_groups('managers', 'users')
 
     title = _('server information')
     cache_max_age = 0
--- a/web/views/editcontroller.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/editcontroller.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""The edit controller, handling form submitting.
+"""The edit controller, automatically handling entity form submitting"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -27,9 +26,37 @@
 from logilab.common.textutils import splitstrip
 
 from cubicweb import Binary, ValidationError, typed_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import is_instance
+from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
+                          ProcessFormError)
 from cubicweb.web.views import basecontrollers, autoform
 
+
+class IEditControlAdapter(EntityAdapter):
+    __regid__ = 'IEditControl'
+    __select__ = is_instance('Any')
+
+    @implements_adapter_compat('IEditControl')
+    def after_deletion_path(self):
+        """return (path, parameters) which should be used as redirect
+        information when this entity is being deleted
+        """
+        parent = self.entity.cw_adapt_to('IBreadCrumbs').parent_entity()
+        if parent is not None:
+            return parent.rest_path(), {}
+        return str(self.entity.e_schema).lower(), {}
+
+    @implements_adapter_compat('IEditControl')
+    def pre_web_edit(self):
+        """callback called by the web editcontroller when an entity will be
+        created/modified, to let a chance to do some entity specific stuff.
+
+        Do nothing by default.
+        """
+        pass
+
+
 def valerror_eid(eid):
     try:
         return typed_eid(eid)
@@ -133,8 +160,6 @@
     def _insert_entity(self, etype, eid, rqlquery):
         rql = rqlquery.insert_query(etype)
         try:
-            # get the new entity (in some cases, the type might have
-            # changed as for the File --> Image mutation)
             entity = self._cw.execute(rql, rqlquery.kwargs).get_entity(0, 0)
             neweid = entity.eid
         except ValidationError, ex:
@@ -155,7 +180,7 @@
         entity.eid = formparams['eid']
         is_main_entity = self._cw.form.get('__maineid') == formparams['eid']
         # let a chance to do some entity specific stuff
-        entity.pre_web_edit()
+        entity.cw_adapt_to('IEditControl').pre_web_edit()
         # create a rql query from parameters
         rqlquery = RqlQuery()
         # process inlined relations at the same time as attributes
@@ -179,9 +204,8 @@
                 field = form.field_by_name(name, role, eschema=entity.e_schema)
             else:
                 field = form.field_by_name(name, role)
-            for field in field.actual_fields(form):
-                if field.has_been_modified(form):
-                    self.handle_formfield(form, field, rqlquery)
+            if field.has_been_modified(form):
+                self.handle_formfield(form, field, rqlquery)
         if self.errors:
             errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors)
             raise ValidationError(valerror_eid(entity.eid), errors)
@@ -276,9 +300,9 @@
         eidtypes = tuple(eidtypes)
         for eid, etype in eidtypes:
             entity = self._cw.entity_from_eid(eid, etype)
-            path, params = entity.after_deletion_path()
+            path, params = entity.cw_adapt_to('IEditControl').after_deletion_path()
             redirect_info.add( (path, tuple(params.iteritems())) )
-            entity.delete()
+            entity.cw_delete()
         if len(redirect_info) > 1:
             # In the face of ambiguity, refuse the temptation to guess.
             self._after_deletion_path = 'view', ()
--- a/web/views/editforms.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/editforms.py	Mon Jul 19 15:37:02 2010 +0200
@@ -26,16 +26,17 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cached
+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, implements, yes)
+                                specified_etype_implements, is_instance, yes)
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
-from cubicweb.web import uicfg, stdmsgs, eid_param, dumps, \
+from cubicweb.web import uicfg, stdmsgs, eid_param, \
      formfields as ff, formwidgets as fw
 from cubicweb.web.form import FormViewMixIn, FieldNotFound
-from cubicweb.web.views import forms
+from cubicweb.web.views import forms, reledit
 
 _pvdc = uicfg.primaryview_display_ctrl
 
@@ -43,7 +44,7 @@
 class DeleteConfForm(forms.CompositeForm):
     __regid__ = 'deleteconf'
     # XXX non_final_entity does not implement eclass_selector
-    __select__ = implements('Any')
+    __select__ = is_instance('Any')
 
     domid = 'deleteconf'
     copy_nav_params = True
@@ -207,7 +208,7 @@
             if not rschema.final:
                 # ensure relation cache is filed
                 rset = self.copying.related(rschema, role)
-                self.newentity.set_related_cache(rschema, role, rset)
+                self.newentity.cw_set_relation_cache(rschema, role, rset)
 
     def submited_message(self):
         """return the message that will be displayed on successful edition"""
@@ -260,213 +261,5 @@
 
 # click and edit handling ('reledit') ##########################################
 
-class DummyForm(object):
-    __slots__ = ('event_args',)
-    def form_render(self, **_args):
-        return u''
-    def render(self, **_args):
-        return u''
-    def append_field(self, *args):
-        pass
-    def field_by_name(self, rtype, role, eschema=None):
-        return None
-
-
-class ClickAndEditFormView(FormViewMixIn, EntityView):
-    """form used to permit ajax edition of a relation or attribute of an entity
-    in a view, if logged user have the permission to edit it.
-
-    (double-click on the field to see an appropriate edition widget).
-    """
-    __regid__ = 'doreledit'
-    __select__ = non_final_entity() & match_kwargs('rtype')
-    # FIXME editableField class could be toggleable from userprefs
-
-    _onclick = u"showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')"
-    _onsubmit = ("return inlineValidateRelationForm('%(rtype)s', '%(role)s', '%(eid)s', "
-                 "'%(divid)s', %(reload)s, '%(vid)s', '%(default)s', '%(lzone)s');")
-    _cancelclick = "hideInlineEdit(%s,\'%s\',\'%s\')"
-    _defaultlandingzone = (u'<img title="%(msg)s" src="data/pen_icon.png" '
-                           'alt="%(msg)s"/>')
-    _landingzonemsg = _('click to edit this field')
-    # default relation vids according to cardinality
-    _one_rvid = 'incontext'
-    _many_rvid = 'csv'
-
-
-    def cell_call(self, row, col, rtype=None, role='subject',
-                  reload=False,      # controls reloading the whole page after change
-                  rvid=None,         # vid to be applied to other side of rtype (non final relations only)
-                  default=None,      # default value
-                  landing_zone=None  # prepend value with a separate html element to click onto
-                                     # (esp. needed when values are links)
-                  ):
-        """display field to edit entity's `rtype` relation on click"""
-        assert rtype
-        assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
-        self._cw.add_js('cubicweb.edition.js')
-        self._cw.add_css('cubicweb.form.css')
-        if default is None:
-            default = xml_escape(self._cw._('<%s not specified>')
-                                 % display_name(self._cw, rtype, role))
-        schema = self._cw.vreg.schema
-        entity = self.cw_rset.get_entity(row, col)
-        rschema = schema.rschema(rtype)
-        lzone = self._build_landing_zone(landing_zone)
-        # compute value, checking perms, build form
-        if rschema.final:
-            form = self._build_form(entity, rtype, role, 'base', default, reload, lzone)
-            if not self.should_edit_attribute(entity, rschema, form):
-                self.w(entity.printable_value(rtype))
-                return
-            value = entity.printable_value(rtype) or default
-        else:
-            rvid = self._compute_best_vid(entity.e_schema, rschema, role)
-            rset = entity.related(rtype, role)
-            if rset:
-                value = self._cw.view(rvid, rset)
-            else:
-                value = default
-            if not self.should_edit_relation(entity, rschema, role, rvid):
-                if rset:
-                    self.w(value)
-                return
-            # XXX do we really have to give lzone twice?
-            form = self._build_form(entity, rtype, role, 'base', default, reload, lzone,
-                                    dict(vid=rvid, lzone=lzone))
-        field = form.field_by_name(rtype, role, entity.e_schema)
-        form.append_field(field)
-        self.relation_form(lzone, value, form,
-                           self._build_renderer(entity, rtype, role))
-
-    def should_edit_attribute(self, entity, rschema, form):
-        if not entity.has_perm('update'):
-            return False
-        rdef = entity.e_schema.rdef(rschema)
-        if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
-            return False
-        try:
-            form.field_by_name(str(rschema), 'subject', entity.e_schema)
-        except FieldNotFound:
-            return False
-        return True
-
-    def should_edit_relation(self, entity, rschema, role, rvid):
-        if ((role == 'subject' and not rschema.has_perm(self._cw, 'add',
-                                                        fromeid=entity.eid))
-            or
-            (role == 'object' and not rschema.has_perm(self._cw, 'add',
-                                                       toeid=entity.eid))):
-            return False
-        return True
-
-    def relation_form(self, lzone, value, form, renderer):
-        """xxx-reledit div (class=field)
-              +-xxx div (class="editableField")
-              |   +-landing zone
-              +-xxx-value div
-              +-xxx-form div
-        """
-        w = self.w
-        divid = form.event_args['divid']
-        w(u'<div id="%s-reledit" class="field" '
-          u'onmouseout="addElementClass(jQuery(\'#%s\'), \'hidden\')" '
-          u'onmouseover="removeElementClass(jQuery(\'#%s\'), \'hidden\')">'
-          % (divid, divid, divid))
-        w(u'<div id="%s-value" class="editableFieldValue">%s</div>' % (divid, value))
-        w(form.render(renderer=renderer))
-        w(u'<div id="%s" class="editableField hidden" onclick="%s" title="%s">' % (
-                divid, xml_escape(self._onclick % form.event_args),
-                self._cw._(self._landingzonemsg)))
-        w(lzone)
-        w(u'</div>')
-        w(u'</div>')
-
-    def _compute_best_vid(self, eschema, rschema, role):
-        dispctrl = _pvdc.etype_get(eschema, rschema, role)
-        if dispctrl.get('rvid'):
-            return dispctrl['rvid']
-        if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
-            return self._many_rvid
-        return self._one_rvid
-
-    def _build_landing_zone(self, lzone):
-        return lzone or self._defaultlandingzone % {
-            'msg': xml_escape(self._cw._(self._landingzonemsg))}
-
-    def _build_renderer(self, entity, rtype, role):
-        return self._cw.vreg['formrenderers'].select(
-            'base', self._cw, entity=entity, display_label=False,
-            display_help=False, table_class='',
-            button_bar_class='buttonbar', display_progress_div=False)
-
-    def _build_args(self, entity, rtype, role, formid, default, reload, lzone,
-                    extradata=None):
-        divid = '%s-%s-%s' % (rtype, role, entity.eid)
-        event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype,
-                      'reload' : dumps(reload), 'default' : default, 'role' : role, 'vid' : u'',
-                      'lzone' : lzone}
-        if extradata:
-            event_args.update(extradata)
-        return divid, event_args
-
-    def _build_form(self, entity, rtype, role, formid, default, reload, lzone,
-                  extradata=None, **formargs):
-        divid, event_args = self._build_args(entity, rtype, role, formid, default,
-                                      reload, lzone, extradata)
-        onsubmit = self._onsubmit % event_args
-        cancelclick = self._cancelclick % (entity.eid, rtype, divid)
-        form = self._cw.vreg['forms'].select(
-            formid, self._cw, entity=entity, domid='%s-form' % divid,
-            cssstyle='display: none', onsubmit=onsubmit, action='#',
-            form_buttons=[fw.SubmitButton(),
-                          fw.Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)],
-            **formargs)
-        form.event_args = event_args
-        return form
-
-
-class AutoClickAndEditFormView(ClickAndEditFormView):
-    """same as ClickAndEditFormView but checking if the view *should* be applied
-    by checking uicfg configuration and composite relation property.
-    """
-    __regid__ = 'reledit'
-    _onclick = (u"loadInlineEditionForm(%(eid)s, '%(rtype)s', '%(role)s', "
-                "'%(divid)s', %(reload)s, '%(vid)s', '%(default)s', '%(lzone)s');")
-
-    def should_edit_attribute(self, entity, rschema, form):
-        rdef = entity.e_schema.rdef(rschema)
-        afs = uicfg.autoform_section.etype_get(
-            entity.__regid__, rschema, 'subject', rdef.object)
-        if 'main_hidden' in afs:
-            return False
-        return super(AutoClickAndEditFormView, self).should_edit_attribute(
-            entity, rschema, form)
-
-    def should_edit_relation(self, entity, rschema, role, rvid):
-        eschema = entity.e_schema
-        dispctrl = _pvdc.etype_get(eschema, rschema, role)
-        vid = dispctrl.get('vid', 'reledit')
-        if vid != 'reledit': # reledit explicitly disabled
-            return False
-        rdef = eschema.rdef(rschema, role)
-        if rdef.composite == role:
-            return False
-        afs = uicfg.autoform_section.etype_get(
-            entity.__regid__, rschema, role, rdef.object)
-        if 'main_hidden' in afs:
-            return False
-        return super(AutoClickAndEditFormView, self).should_edit_relation(
-            entity, rschema, role, rvid)
-
-    def _build_form(self, entity, rtype, role, formid, default, reload, lzone,
-                  extradata=None, **formargs):
-        _divid, event_args = self._build_args(entity, rtype, role, formid, default,
-                                              reload, lzone, extradata)
-        form = DummyForm()
-        form.event_args = event_args
-        return form
-
-    def _build_renderer(self, entity, rtype, role):
-        pass
-
+ClickAndEditFormView = class_moved(reledit.ClickAndEditFormView)
+AutoClickAndEditFormView = class_moved(reledit.AutoClickAndEditFormView)
--- a/web/views/editviews.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/editviews.py	Mon Jul 19 15:37:02 2010 +0200
@@ -59,9 +59,9 @@
         # them. Use fetch_order and not fetch_unrelated_order as sort method
         # since the latter is mainly there to select relevant items in the combo
         # box, it doesn't give interesting result in this context
-        rql, args = entity.unrelated_rql(rtype, etype, role,
-                                         ordermethod='fetch_order',
-                                         vocabconstraints=False)
+        rql, args = entity.cw_unrelated_rql(rtype, etype, role,
+                                            ordermethod='fetch_order',
+                                            vocabconstraints=False)
         rset = self._cw.execute(rql, args, tuple(args))
         return rset, 'list', "search-associate-content", True
 
--- a/web/views/emailaddress.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/emailaddress.py	Mon Jul 19 15:37:02 2010 +0200
@@ -23,17 +23,17 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.schema import display_name
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb import Unauthorized
 from cubicweb.web import uicfg
-from cubicweb.web.views import baseviews, primary
+from cubicweb.web.views import baseviews, primary, ibreadcrumbs
 
 _pvs = uicfg.primaryview_section
 _pvs.tag_subject_of(('*', 'use_email', '*'), 'attributes')
 _pvs.tag_subject_of(('*', 'primary_email', '*'), 'hidden')
 
 class EmailAddressPrimaryView(primary.PrimaryView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, skipeids=None):
         self.skipeids = skipeids
@@ -72,7 +72,7 @@
 
 
 class EmailAddressShortPrimaryView(EmailAddressPrimaryView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
     __regid__ = 'shortprimary'
     title = None # hidden view
 
@@ -83,7 +83,7 @@
 
 
 class EmailAddressOneLineView(baseviews.OneLineView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
@@ -104,7 +104,7 @@
     'mailto:'"""
 
     __regid__ = 'mailto'
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
@@ -127,14 +127,21 @@
 
 
 class EmailAddressInContextView(baseviews.InContextView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         self.wview('mailto', self.cw_rset, row=row, col=col, **kwargs)
 
 
 class EmailAddressTextView(baseviews.TextView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         self.w(self.cw_rset.get_entity(row, col).display_address())
+
+
+class EmailAddressIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('EmailAddress')
+
+    def parent_entity(self):
+        return self.entity.email_of
--- a/web/views/embedding.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/embedding.py	Mon Jul 19 15:37:02 2010 +0200
@@ -28,16 +28,27 @@
 
 from logilab.mtconverter import guess_encoding
 
-from cubicweb.selectors import (one_line_rset, score_entity,
-                                match_search_state, implements)
+from cubicweb.selectors import (one_line_rset, score_entity, implements,
+                                adaptable, match_search_state)
 from cubicweb.interfaces import IEmbedable
-from cubicweb.view import NOINDEX, NOFOLLOW
+from cubicweb.view import NOINDEX, NOFOLLOW, EntityAdapter, implements_adapter_compat
 from cubicweb.uilib import soup2xhtml
 from cubicweb.web.controller import Controller
 from cubicweb.web.action import Action
 from cubicweb.web.views import basetemplates
 
 
+class IEmbedableAdapter(EntityAdapter):
+    """interface for embedable entities"""
+    __regid__ = 'IEmbedable'
+    __select__ = implements(IEmbedable, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('IEmbedable')
+    def embeded_url(self):
+        """embed action interface"""
+        raise NotImplementedError
+
+
 class ExternalTemplate(basetemplates.TheMainTemplate):
     """template embeding an external web pages into CubicWeb web interface
     """
@@ -84,14 +95,14 @@
             except HTTPError, err:
                 body = '<h2>%s</h2><h3>%s</h3>' % (
                     _('error while embedding page'), err)
-        self.process_rql(req.form.get('rql'))
+        rset = self.process_rql()
         return self._cw.vreg['views'].main_template(req, self.template,
-                                                rset=self.cw_rset, body=body)
+                                                    rset=rset, body=body)
 
 
 def entity_has_embedable_url(entity):
     """return 1 if the entity provides an allowed embedable url"""
-    url = entity.embeded_url()
+    url = entity.cw_adapt_to('IEmbedable').embeded_url()
     if not url or not url.strip():
         return 0
     allowed = entity._cw.vreg.config['embed-allowed']
@@ -106,14 +117,14 @@
     """
     __regid__ = 'embed'
     __select__ = (one_line_rset() & match_search_state('normal')
-                  & implements(IEmbedable)
+                  & adaptable('IEmbedable')
                   & score_entity(entity_has_embedable_url))
 
     title = _('embed')
 
     def url(self, row=0):
         entity = self.cw_rset.get_entity(row, 0)
-        url = urljoin(self._cw.base_url(), entity.embeded_url())
+        url = urljoin(self._cw.base_url(), entity.cw_adapt_to('IEmbedable').embeded_url())
         if self._cw.form.has_key('rql'):
             return self._cw.build_url('embed', url=url, rql=self._cw.form['rql'])
         return self._cw.build_url('embed', url=url)
--- a/web/views/error.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/error.py	Mon Jul 19 15:37:02 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Set of HTML errors views. Error view are generally implemented
 as startup views and are used for standard error pages (404, 500, etc.)
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.view import StartupView
--- a/web/views/facets.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/facets.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""the facets box and some basic facets
+"""the facets box and some basic facets"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
@@ -25,7 +24,7 @@
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import (non_final_entity, multi_lines_rset,
                                 match_context_prop, yes, relation_possible)
-from cubicweb.web import dumps
+from cubicweb.utils import json_dumps
 from cubicweb.web.box import BoxTemplate
 from cubicweb.web.facet import (AbstractFacet, FacetStringWidget, RelationFacet,
                                 prepare_facets_rqlst, filter_hiddens, _cleanup_rqlst,
@@ -102,7 +101,7 @@
             self.display_bookmark_link(rset)
         w = self.w
         w(u'<form method="post" id="%sForm" cubicweb:facetargs="%s" action="">'  % (
-            divid, xml_escape(dumps([divid, vid, paginate, self.facetargs()]))))
+            divid, xml_escape(json_dumps([divid, vid, paginate, self.facetargs()]))))
         w(u'<fieldset>')
         hiddens = {'facets': ','.join(wdg.facet.__regid__ for wdg in widgets),
                    'baserql': baserql}
--- a/web/views/formrenderers.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/formrenderers.py	Mon Jul 19 15:37:02 2010 +0200
@@ -40,8 +40,9 @@
 
 from cubicweb import tags
 from cubicweb.appobject import AppObject
-from cubicweb.selectors import implements, yes
-from cubicweb.web import dumps, eid_param, formwidgets as fwdgs
+from cubicweb.selectors import is_instance, yes
+from cubicweb.utils import json_dumps
+from cubicweb.web import eid_param, formwidgets as fwdgs
 
 
 def checkbox(name, value, attrs='', checked=None):
@@ -342,7 +343,7 @@
                 w(u'<th align="left">%s</th>' %
                   tags.input(type='checkbox',
                              title=self._cw._('toggle check boxes'),
-                             onclick="setCheckboxesState('eid', this.checked)"))
+                             onclick="setCheckboxesState('eid', null, this.checked)"))
                 for field in subfields:
                     w(u'<th>%s</th>' % field_label(form, field))
                 w(u'</tr>')
@@ -358,8 +359,8 @@
             entity = form.edited_entity
             values = form.form_previous_values
             qeid = eid_param('eid', entity.eid)
-            cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % \
-                         xml_escape(dumps(entity.eid))
+            cbsetstate = "setCheckboxesState('eid', %s, 'checked')" % \
+                         xml_escape(json_dumps(entity.eid))
             w(u'<tr class="%s">' % (entity.cw_row % 2 and u'even' or u'odd'))
             # XXX turn this into a widget used on the eid field
             w(u'<td>%s</td>' % checkbox('eid', entity.eid,
@@ -392,7 +393,7 @@
     """
     __regid__ = 'default'
     # needs some additional points in some case (XXX explain cases)
-    __select__ = implements('Any') & yes()
+    __select__ = is_instance('Any') & yes()
 
     _options = FormRenderer._options + ('main_form_title',)
     main_form_title = _('main informations')
--- a/web/views/ibreadcrumbs.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/ibreadcrumbs.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,26 +15,84 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""navigation components definition for CubicWeb web client
+"""breadcrumbs components definition for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
 
-from cubicweb.interfaces import IBreadCrumbs
-from cubicweb.selectors import (one_line_rset, implements, one_etype_rset,
-                                multi_lines_rset, any_rset)
-from cubicweb.view import EntityView, Component
+#from cubicweb.interfaces import IBreadCrumbs
+from cubicweb.selectors import (is_instance, one_line_rset, adaptable,
+                                one_etype_rset, multi_lines_rset, any_rset)
+from cubicweb.view import EntityView, Component, EntityAdapter
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
 from cubicweb.entity import Entity
 from cubicweb import tags, uilib
 
 
+# ease bw compat
+def ibreadcrumb_adapter(entity):
+    if hasattr(entity, 'breadcrumbs'):
+        warn('[3.9] breadcrumbs() method is deprecated, define a custom '
+             'IBreadCrumbsAdapter for %s instead' % entity.__class__,
+             DeprecationWarning)
+        return entity
+    return entity.cw_adapt_to('IBreadCrumbs')
+
+
+class IBreadCrumbsAdapter(EntityAdapter):
+    """adapters for entities which can be"located" on some path to display in
+    the web ui
+    """
+    __regid__ = 'IBreadCrumbs'
+    __select__ = is_instance('Any', accept_none=False)
+
+    def parent_entity(self):
+        if hasattr(self.entity, 'parent'):
+            warn('[3.9] parent() method is deprecated, define a '
+                 'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead'
+                 % self.entity.__class__, DeprecationWarning)
+            return self.entity.parent()
+        itree = self.entity.cw_adapt_to('ITree')
+        if itree is not None:
+            return itree.parent()
+        return None
+
+    def breadcrumbs(self, view=None, recurs=False):
+        """return a list containing some:
+
+        * tuple (url, label)
+        * entity
+        * simple label string
+
+        defining path from a root to the current view
+
+        the main view is given as argument so breadcrumbs may vary according
+        to displayed view (may be None). When recursing on a parent entity,
+        the `recurs` argument should be set to True.
+        """
+        parent = self.parent_entity()
+        if parent is not None:
+            adapter = ibreadcrumb_adapter(parent)
+            path = adapter.breadcrumbs(view, True) + [self.entity]
+        else:
+            path = [self.entity]
+        if not recurs:
+            if view is None:
+                if 'vtitle' in self._cw.form:
+                    # embeding for instance
+                    path.append( self._cw.form['vtitle'] )
+            elif view.__regid__ != 'primary' and hasattr(view, 'title'):
+                path.append( self._cw._(view.title) )
+        return path
+
+
 class BreadCrumbEntityVComponent(Component):
     __regid__ = 'breadcrumbs'
-    __select__ = one_line_rset() & implements(IBreadCrumbs, accept_none=False)
+    __select__ = one_line_rset() & adaptable('IBreadCrumbs')
 
     cw_property_defs = {
         _('visible'):  dict(type='Boolean', default=True,
@@ -47,7 +105,8 @@
 
     def call(self, view=None, first_separator=True):
         entity = self.cw_rset.get_entity(0, 0)
-        path = entity.breadcrumbs(view)
+        adapter = ibreadcrumb_adapter(entity)
+        path = adapter.breadcrumbs(view)
         if path:
             self.open_breadcrumbs()
             if first_separator:
@@ -73,7 +132,7 @@
             self.w(u"\n")
             self.wpath_part(parent, contextentity, i == len(path) - 1)
 
-    def wpath_part(self, part, contextentity, last=False):
+    def wpath_part(self, part, contextentity, last=False): # XXX deprecates last argument?
         if isinstance(part, Entity):
             self.w(part.view('breadcrumbs'))
         elif isinstance(part, tuple):
@@ -88,7 +147,7 @@
 
 class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
     __select__ = multi_lines_rset() & one_etype_rset() & \
-                 implements(IBreadCrumbs, accept_none=False)
+                 adaptable('IBreadCrumbs')
 
     def render_breadcrumbs(self, contextentity, path):
         # XXX hack: only display etype name or first non entity path part
--- a/web/views/idownloadable.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/idownloadable.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,29 +15,22 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IDownloadable
+"""Specific views for entities adapting to IDownloadable"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape
 
+from cubicweb import tags
 from cubicweb.view import EntityView
-from cubicweb.selectors import (one_line_rset, score_entity,
-                                implements, match_context_prop)
-from cubicweb.interfaces import IDownloadable
+from cubicweb.selectors import (one_line_rset, is_instance, match_context_prop,
+                                adaptable, has_mimetype)
 from cubicweb.mttransforms import ENGINE
 from cubicweb.web import box, httpcache
 from cubicweb.web.views import primary, baseviews
 
 
-def is_image(entity):
-    mt = entity.download_content_type()
-    if not (mt and mt.startswith('image/')):
-        return 0
-    return 1
-
 def download_box(w, entity, title=None, label=None, footer=u''):
     req = entity._cw
     w(u'<div class="sideBox">')
@@ -47,8 +40,8 @@
       % xml_escape(title))
     w(u'<div class="sideBox downloadBox"><div class="sideBoxBody">')
     w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
-      % (xml_escape(entity.download_url()),
-         req.external_resource('DOWNLOAD_ICON'),
+      % (xml_escape(entity.cw_adapt_to('IDownloadable').download_url()),
+         req.uiprops['DOWNLOAD_ICON'],
          _('download icon'), xml_escape(label or entity.dc_title())))
     w(u'%s</div>' % footer)
     w(u'</div></div>\n')
@@ -58,8 +51,8 @@
     __regid__ = 'download_box'
     # no download box for images
     # XXX primary_view selector ?
-    __select__ = (one_line_rset() & implements(IDownloadable) &
-                  match_context_prop() & ~score_entity(is_image))
+    __select__ = (one_line_rset() & match_context_prop()
+                  & adaptable('IDownloadable') & ~has_mimetype('image/'))
     order = 10
 
     def cell_call(self, row, col, title=None, label=None, **kwargs):
@@ -72,7 +65,7 @@
     downloading of entities providing the necessary interface
     """
     __regid__ = 'download'
-    __select__ = one_line_rset() & implements(IDownloadable)
+    __select__ = one_line_rset() & adaptable('IDownloadable')
 
     templatable = False
     content_type = 'application/octet-stream'
@@ -82,75 +75,88 @@
 
     def set_request_content_type(self):
         """overriden to set the correct filetype and filename"""
-        entity = self.cw_rset.complete_entity(0, 0)
-        encoding = entity.download_encoding()
+        entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+        adapter = entity.cw_adapt_to('IDownloadable')
+        encoding = adapter.download_encoding()
         if encoding in BINARY_ENCODINGS:
             contenttype = 'application/%s' % encoding
             encoding = None
         else:
-            contenttype = entity.download_content_type()
+            contenttype = adapter.download_content_type()
         self._cw.set_content_type(contenttype or self.content_type,
-                                  filename=entity.download_file_name(),
+                                  filename=adapter.download_file_name(),
                                   encoding=encoding)
 
     def call(self):
-        self.w(self.cw_rset.complete_entity(0, 0).download_data())
+        entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+        adapter = entity.cw_adapt_to('IDownloadable')
+        self.w(adapter.download_data())
 
     def last_modified(self):
         return self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0).modification_date
 
+
 class DownloadLinkView(EntityView):
     """view displaying a link to download the file"""
     __regid__ = 'downloadlink'
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
     title = None # should not be listed in possible views
 
 
     def cell_call(self, row, col, title=None, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
-        url = xml_escape(entity.download_url())
+        url = xml_escape(entity.cw_adapt_to('IDownloadable').download_url())
         self.w(u'<a href="%s">%s</a>' % (url, xml_escape(title or entity.dc_title())))
 
 
 class IDownloadablePrimaryView(primary.PrimaryView):
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
 
     def render_entity_attributes(self, entity):
         super(IDownloadablePrimaryView, self).render_entity_attributes(entity)
         self.w(u'<div class="content">')
-        contenttype = entity.download_content_type()
+        adapter = entity.cw_adapt_to('IDownloadable')
+        contenttype = adapter.download_content_type()
         if contenttype.startswith('image/'):
-            self.wview('image', entity.cw_rset, row=entity.cw_row)
+            self.wview('image', entity.cw_rset, row=entity.cw_row, col=entity.cw_col,
+                       link=True, klass='contentimage')
         else:
             self.wview('downloadlink', entity.cw_rset, title=self._cw._('download'), row=entity.cw_row)
+            self.render_data(entity, contenttype, 'text/html')
+        self.w(u'</div>')
+
+    def render_data(self, entity, sourcemt, targetmt):
+        adapter = entity.cw_adapt_to('IDownloadable')
+        if ENGINE.find_path(sourcemt, targetmt):
             try:
-                if ENGINE.has_input(contenttype):
-                    self.w(entity.printable_value('data'))
-            except TransformError:
-                pass
+                self.w(entity._cw_mtc_transform(adapter.download_data(), sourcemt,
+                                                targetmt, adapter.download_encoding()))
             except Exception, ex:
+                self.exception('while rendering data for %s', entity)
                 msg = self._cw._("can't display data, unexpected error: %s") \
-                      % xml_escape(str(ex))
+                      % xml_escape(unicode(ex))
                 self.w('<div class="error">%s</div>' % msg)
-        self.w(u'</div>')
+            return True
+        return False
 
 
 class IDownloadableLineView(baseviews.OneLineView):
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
 
     def cell_call(self, row, col, title=None, **kwargs):
         """the oneline view is a link to download the file"""
         entity = self.cw_rset.get_entity(row, col)
         url = xml_escape(entity.absolute_url())
-        name = xml_escape(title or entity.download_file_name())
-        durl = xml_escape(entity.download_url())
+        adapter = entity.cw_adapt_to('IDownloadable')
+        name = xml_escape(title or adapter.download_file_name())
+        durl = xml_escape(adapter.download_url())
         self.w(u'<a href="%s">%s</a> [<a href="%s">%s</a>]' %
                (url, name, durl, self._cw._('download')))
 
 
 class ImageView(EntityView):
     __regid__ = 'image'
-    __select__ = implements(IDownloadable) & score_entity(is_image)
+    __select__ = has_mimetype('image/')
 
     title = _('image')
 
@@ -161,17 +167,12 @@
             self.wview(self.__regid__, rset, row=i, col=0)
             self.w(u'</div>')
 
-    def cell_call(self, row, col, width=None, height=None, link=False):
+    def cell_call(self, row, col, link=False, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
-        #if entity.data_format.startswith('image/'):
-        imgtag = u'<img src="%s" alt="%s" ' % (
-            xml_escape(entity.download_url()),
-            (self._cw._('download %s')  % xml_escape(entity.download_file_name())))
-        if width:
-            imgtag += u'width="%i" ' % width
-        if height:
-            imgtag += u'height="%i" ' % height
-        imgtag += u'/>'
+        adapter = entity.cw_adapt_to('IDownloadable')
+        imgtag = tags.img(src=adapter.download_url(),
+                          alt=(self._cw._('download %s') % adapter.download_file_name()),
+                          **kwargs)
         if link:
             self.w(u'<a href="%s">%s</a>' % (entity.absolute_url(vid='download'),
                                              imgtag))
--- a/web/views/igeocodable.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/igeocodable.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,33 +15,64 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IGeocodable
+"""Specific views for entities implementing IGeocodable"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.interfaces import IGeocodable
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
-from cubicweb.web import json
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
+from cubicweb.utils import json_dumps
+
+class IGeocodableAdapter(EntityAdapter):
+    """interface required by geocoding views such as gmap-view"""
+    __regid__ = 'IGeocodable'
+    __select__ = implements(IGeocodable, warn=False) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('IGeocodable')
+    def latitude(self):
+        """returns the latitude of the entity"""
+        raise NotImplementedError
+
+    @property
+    @implements_adapter_compat('IGeocodable')
+    def longitude(self):
+        """returns the longitude of the entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IGeocodable')
+    def marker_icon(self):
+        """returns the icon that should be used as the marker.
+
+        an icon is defined by a 4-uple:
+
+          (icon._url, icon.size,  icon.iconAnchor, icon.shadow)
+        """
+        return (self._cw.uiprops['GMARKER_ICON'], (20, 34), (4, 34), None)
+
 
 class GeocodingJsonView(EntityView):
     __regid__ = 'geocoding-json'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     binary = True
     templatable = False
     content_type = 'application/json'
 
     def call(self):
-        # remove entities that don't define latitude and longitude
-        self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
         zoomlevel = self._cw.form.pop('zoomlevel', 8)
         extraparams = self._cw.form.copy()
         extraparams.pop('vid', None)
         extraparams.pop('rql', None)
-        markers = [self.build_marker_data(rowidx, extraparams)
-                   for rowidx in xrange(len(self.cw_rset))]
+        markers = []
+        for entity in self.cw_rset.entities():
+            igeocodable = entity.cw_adapt_to('IGeocodable')
+            # remove entities that don't define latitude and longitude
+            if not (igeocodable.latitude and igeocodable.longitude):
+                continue
+            markers.append(self.build_marker_data(entity, igeocodable,
+                                                  extraparams))
         center = {
             'latitude': sum(marker['latitude'] for marker in markers) / len(markers),
             'longitude': sum(marker['longitude'] for marker in markers) / len(markers),
@@ -51,26 +82,21 @@
             'center': center,
             'markers': markers,
             }
-        self.w(json.dumps(geodata))
+        self.w(json_dumps(geodata))
 
-    def build_marker_data(self, row, extraparams):
-        entity = self.cw_rset.get_entity(row, 0)
-        icon = None
-        if hasattr(entity, 'marker_icon'):
-            icon = entity.marker_icon()
-        else:
-            icon = (self._cw.external_resource('GMARKER_ICON'), (20, 34), (4, 34), None)
-        return {'latitude': entity.latitude, 'longitude': entity.longitude,
+    def build_marker_data(self, entity, igeocodable, extraparams):
+        return {'latitude': igeocodable.latitude,
+                'longitude': igeocodable.longitude,
+                'icon': igeocodable.marker_icon(),
                 'title': entity.dc_long_title(),
-                #icon defines : (icon._url, icon.size,  icon.iconAncho', icon.shadow)
-                'icon': icon,
-                'bubbleUrl': entity.absolute_url(vid='gmap-bubble', __notemplate=1, **extraparams),
+                'bubbleUrl': entity.absolute_url(
+                    vid='gmap-bubble', __notemplate=1, **extraparams),
                 }
 
 
 class GoogleMapBubbleView(EntityView):
     __regid__ = 'gmap-bubble'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -80,16 +106,14 @@
 
 class GoogleMapsView(EntityView):
     __regid__ = 'gmap-view'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     paginable = False
 
     def call(self, gmap_key, width=400, height=400, uselabel=True, urlparams=None):
         self._cw.demote_to_html()
-        # remove entities that don't define latitude and longitude
-        self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
-        self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s' % gmap_key,
-                        localfile=False)
+        self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s'
+                        % gmap_key, localfile=False)
         self._cw.add_js( ('cubicweb.widgets.js', 'cubicweb.gmap.js', 'gmap.utility.labeledmarker.js') )
         rql = self.cw_rset.printable_rql()
         if urlparams is None:
@@ -98,7 +122,8 @@
             loadurl = self._cw.build_url(rql=rql, vid='geocoding-json', **urlparams)
         self.w(u'<div style="width: %spx; height: %spx;" class="widget gmap" '
                u'cubicweb:wdgtype="GMapWidget" cubicweb:loadtype="auto" '
-               u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>' % (width, height, loadurl, uselabel))
+               u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>'
+               % (width, height, loadurl, uselabel))
 
 
 class GoogeMapsLegend(EntityView):
--- a/web/views/iprogress.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/iprogress.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IProgress
+"""Specific views for entities implementing IProgress/IMileStone"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -26,12 +25,12 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import make_uid
-from cubicweb.selectors import implements
-from cubicweb.interfaces import IProgress, IMileStone
+from cubicweb.selectors import adaptable
 from cubicweb.schema import display_name
 from cubicweb.view import EntityView
 from cubicweb.web.views.tableview import EntityAttributesTableView
 
+
 class ProgressTableView(EntityAttributesTableView):
     """The progress table view is able to display progress information
     of any object implement IMileStone.
@@ -50,8 +49,8 @@
     """
 
     __regid__ = 'progress_table_view'
+    __select__ = adaptable('IMileStone')
     title = _('task progression')
-    __select__ = implements(IMileStone)
     table_css = "progress"
     css_files = ('cubicweb.iprogress.css',)
 
@@ -71,30 +70,27 @@
             else:
                 content = entity.printable_value(col)
             infos[col] = content
-        if hasattr(entity, 'progress_class'):
-            cssclass = entity.progress_class()
-        else:
-            cssclass = u''
-        self.w(u"""<tr class="%s" onmouseover="addElementClass(this, 'highlighted');"
-            onmouseout="removeElementClass(this, 'highlighted')">""" % cssclass)
+        cssclass = entity.cw_adapt_to('IMileStone').progress_class()
+        self.w(u"""<tr class="%s" onmouseover="$(this).addClass('highlighted');"
+            onmouseout="$(this).removeClass('highlighted')">""" % cssclass)
         line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
         self.w(line % infos)
         self.w(u'</tr>\n')
 
     ## header management ######################################################
 
-    def header_for_project(self, ecls):
+    def header_for_project(self, sample):
         """use entity's parent type as label"""
-        return display_name(self._cw, ecls.parent_type)
+        return display_name(self._cw, sample.cw_adapt_to('IMileStone').parent_type)
 
-    def header_for_milestone(self, ecls):
+    def header_for_milestone(self, sample):
         """use entity's type as label"""
-        return display_name(self._cw, ecls.__regid__)
+        return display_name(self._cw, sample.__regid__)
 
     ## cell management ########################################################
     def build_project_cell(self, entity):
         """``project`` column cell renderer"""
-        project = entity.get_main_task()
+        project = entity.cw_adapt_to('IMileStone').get_main_task()
         if project:
             return project.view('incontext')
         return self._cw._('no related project')
@@ -105,15 +101,16 @@
 
     def build_state_cell(self, entity):
         """``state`` column cell renderer"""
-        return xml_escape(self._cw._(entity.state))
+        return xml_escape(entity.cw_adapt_to('IWorkflowable').printable_state)
 
     def build_eta_date_cell(self, entity):
         """``eta_date`` column cell renderer"""
-        if entity.finished():
-            return self._cw.format_date(entity.completion_date())
-        formated_date = self._cw.format_date(entity.initial_prevision_date())
-        if entity.in_progress():
-            eta_date = self._cw.format_date(entity.eta_date())
+        imilestone = entity.cw_adapt_to('IMileStone')
+        if imilestone.finished():
+            return self._cw.format_date(imilestone.completion_date())
+        formated_date = self._cw.format_date(imilestone.initial_prevision_date())
+        if imilestone.in_progress():
+            eta_date = self._cw.format_date(imilestone.eta_date())
             _ = self._cw._
             if formated_date:
                 formated_date += u' (%s %s)' % (_('expected:'), eta_date)
@@ -123,12 +120,14 @@
 
     def build_todo_by_cell(self, entity):
         """``todo_by`` column cell renderer"""
-        return u', '.join(p.view('outofcontext') for p in entity.contractors())
+        imilestone = entity.cw_adapt_to('IMileStone')
+        return u', '.join(p.view('outofcontext') for p in imilestone.contractors())
 
     def build_cost_cell(self, entity):
         """``cost`` column cell renderer"""
         _ = self._cw._
-        pinfo = entity.progress_info()
+        imilestone = entity.cw_adapt_to('IMileStone')
+        pinfo = imilestone.progress_info()
         totalcost = pinfo.get('estimatedcorrected', pinfo['estimated'])
         missing = pinfo.get('notestimatedcorrected', pinfo.get('notestimated', 0))
         costdescr = []
@@ -167,8 +166,9 @@
 class ProgressBarView(EntityView):
     """displays a progress bar"""
     __regid__ = 'progressbar'
+    __select__ = adaptable('IProgress')
+
     title = _('progress bar')
-    __select__ = implements(IProgress)
 
     precision = 0.1
     red_threshold = 1.1
@@ -176,10 +176,10 @@
     yellow_threshold = 1
 
     @classmethod
-    def overrun(cls, entity):
+    def overrun(cls, iprogress):
         """overrun = done + todo - """
-        if entity.done + entity.todo > entity.revised_cost:
-            overrun = entity.done + entity.todo - entity.revised_cost
+        if iprogress.done + iprogress.todo > iprogress.revised_cost:
+            overrun = iprogress.done + iprogress.todo - iprogress.revised_cost
         else:
             overrun = 0
         if overrun < cls.precision:
@@ -187,20 +187,21 @@
         return overrun
 
     @classmethod
-    def overrun_percentage(cls, entity):
+    def overrun_percentage(cls, iprogress):
         """pourcentage overrun = overrun / budget"""
-        if entity.revised_cost == 0:
+        if iprogress.revised_cost == 0:
             return 0
         else:
-            return cls.overrun(entity) * 100. / entity.revised_cost
+            return cls.overrun(iprogress) * 100. / iprogress.revised_cost
 
     def cell_call(self, row, col):
         self._cw.add_css('cubicweb.iprogress.css')
         self._cw.add_js('cubicweb.iprogress.js')
         entity = self.cw_rset.get_entity(row, col)
-        done = entity.done
-        todo = entity.todo
-        budget = entity.revised_cost
+        iprogress = entity.cw_adapt_to('IProgress')
+        done = iprogress.done
+        todo = iprogress.todo
+        budget = iprogress.revised_cost
         if budget == 0:
             pourcent = 100
         else:
@@ -229,25 +230,23 @@
 
         title = u'%s/%s = %i%%' % (done_str, budget_str, pourcent)
         short_title = title
-        if self.overrun_percentage(entity):
-            title += u' overrun +%sj (+%i%%)' % (self.overrun(entity),
-                                                 self.overrun_percentage(entity))
-            overrun = self.overrun(entity)
-            if floor(overrun) == overrun or overrun>100:
-                overrun_str = '%i' % overrun
+        overrunpercent = self.overrun_percentage(iprogress)
+        if overrunpercent:
+            overrun = self.overrun(iprogress)
+            title += u' overrun +%sj (+%i%%)' % (overrun, overrunpercent)
+            if floor(overrun) == overrun or overrun > 100:
+                short_title += u' +%i' % overrun
             else:
-                overrun_str = '%.1f' % overrun
-            short_title += u' +%s' % overrun_str
+                short_title += u' +%.1f' % overrun
         # write bars
         maxi = max(done+todo, budget)
         if maxi == 0:
             maxi = 1
-
         cid = make_uid('progress_bar')
-        self._cw.html_headers.add_onload('draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
-                                         (cid,
-                                          int(100.*done/maxi), int(100.*(done+todo)/maxi),
-                                          int(100.*budget/maxi), color))
+        self._cw.html_headers.add_onload(
+            'draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
+            (cid, int(100.*done/maxi), int(100.*(done+todo)/maxi),
+             int(100.*budget/maxi), color))
         self.w(u'%s<br/>'
                u'<canvas class="progressbar" id="canvas%s" width="100" height="10"></canvas>'
                % (xml_escape(short_title), cid))
--- a/web/views/isioc.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/isioc.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,20 +15,70 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for SIOC interfaces
+"""Specific views for SIOC (Semantically-Interlinked Online Communities)
 
+http://sioc-project.org
 """
+
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
 from cubicweb.interfaces import ISiocItem, ISiocContainer
 
+
+class ISIOCItemAdapter(EntityAdapter):
+    """interface for entities which may be represented as an ISIOC items"""
+    __regid__ = 'ISIOCItem'
+    __select__ = implements(ISiocItem, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_content(self):
+        """return item's content"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_container(self):
+        """return container entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_type(self):
+        """return container type (post, BlogPost, MailMessage)"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_replies(self):
+        """return replies items"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_topics(self):
+        """return topics items"""
+        raise NotImplementedError
+
+
+class ISIOCContainerAdapter(EntityAdapter):
+    """interface for entities which may be represented as an ISIOC container"""
+    __regid__ = 'ISIOCContainer'
+    __select__ = implements(ISiocContainer, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ISIOCContainer')
+    def isioc_type(self):
+        """return container type (forum, Weblog, MailingList)"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCContainer')
+    def isioc_items(self):
+        """return contained items"""
+        raise NotImplementedError
+
+
 class SIOCView(EntityView):
     __regid__ = 'sioc'
-    __select__ = EntityView.__select__ & implements(ISiocItem, ISiocContainer)
+    __select__ = adaptable('ISIOCItem', 'ISIOCContainer')
     title = _('sioc')
     templatable = False
     content_type = 'text/xml'
@@ -52,48 +102,51 @@
 
 class SIOCContainerView(EntityView):
     __regid__ = 'sioc_element'
-    __select__ = EntityView.__select__ & implements(ISiocContainer)
+    __select__ = adaptable('ISIOCContainer')
     templatable = False
     content_type = 'text/xml'
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
-        sioct = xml_escape(entity.isioc_type())
+        isioc = entity.cw_adapt_to('ISIOCContainer')
+        isioct = isioc.isioc_type()
         self.w(u'<sioc:%s rdf:about="%s">\n'
-               % (sioct, xml_escape(entity.absolute_url())))
+               % (isioct, xml_escape(entity.absolute_url())))
         self.w(u'<dcterms:title>%s</dcterms:title>'
                % xml_escape(entity.dc_title()))
         self.w(u'<dcterms:created>%s</dcterms:created>'
-               % entity.creation_date)
+               % entity.creation_date) # XXX format
         self.w(u'<dcterms:modified>%s</dcterms:modified>'
-               % entity.modification_date)
+               % entity.modification_date) # XXX format
         self.w(u'<!-- FIXME : here be items -->')#entity.isioc_items()
-        self.w(u'</sioc:%s>\n' % sioct)
+        self.w(u'</sioc:%s>\n' % isioct)
 
 
 class SIOCItemView(EntityView):
     __regid__ = 'sioc_element'
-    __select__ = EntityView.__select__ & implements(ISiocItem)
+    __select__ = adaptable('ISIOCItem')
     templatable = False
     content_type = 'text/xml'
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
-        sioct = xml_escape(entity.isioc_type())
+        isioc = entity.cw_adapt_to('ISIOCItem')
+        isioct = isioc.isioc_type()
         self.w(u'<sioc:%s rdf:about="%s">\n'
-               %  (sioct, xml_escape(entity.absolute_url())))
+               % (isioct, xml_escape(entity.absolute_url())))
         self.w(u'<dcterms:title>%s</dcterms:title>'
                % xml_escape(entity.dc_title()))
         self.w(u'<dcterms:created>%s</dcterms:created>'
-               % entity.creation_date)
+               % entity.creation_date) # XXX format
         self.w(u'<dcterms:modified>%s</dcterms:modified>'
-               % entity.modification_date)
-        if entity.content:
-            self.w(u'<sioc:content>%s</sioc:content>'''
-                   % xml_escape(entity.isioc_content()))
-        if entity.related('entry_of'):
+               % entity.modification_date) # XXX format
+        content = isioc.isioc_content()
+        if content:
+            self.w(u'<sioc:content>%s</sioc:content>' % xml_escape(content))
+        container = isioc.isioc_container()
+        if container:
             self.w(u'<sioc:has_container rdf:resource="%s"/>\n'
-                   % xml_escape(entity.isioc_container().absolute_url()))
+                   % xml_escape(container.absolute_url()))
         if entity.creator:
             self.w(u'<sioc:has_creator>\n')
             self.w(u'<sioc:User rdf:about="%s">\n'
@@ -103,5 +156,5 @@
             self.w(u'</sioc:has_creator>\n')
         self.w(u'<!-- FIXME : here be topics -->')#entity.isioc_topics()
         self.w(u'<!-- FIXME : here be replies -->')#entity.isioc_replies()
-        self.w(u' </sioc:%s>\n' % sioct)
+        self.w(u' </sioc:%s>\n' % isioct)
 
--- a/web/views/magicsearch.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/magicsearch.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,10 +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/>.
-"""a query preprocesser to handle quick search shortcuts for cubicweb
-
-
-"""
+"""a query processor to handle quick search shortcuts for cubicweb"""
 
 __docformat__ = "restructuredtext en"
 
@@ -282,7 +279,13 @@
         if len(word2) == 1 and word2.isupper():
             return '%s %s' % (etype, word2),
         # else, suppose it's a shortcut like : Person Smith
-        rql = '%s %s WHERE %s' % (etype, etype[0], self._complete_rql(word2, etype))
+        restriction = self._complete_rql(word2, etype)
+        if ' has_text ' in restriction:
+            rql = '%s %s ORDERBY FTIRANK(%s) DESC WHERE %s' % (
+                etype, etype[0], etype[0], restriction)
+        else:
+            rql = '%s %s WHERE %s' % (
+                etype, etype[0], restriction)
         return rql, {'text': word2}
 
     def _three_words_query(self, word1, word2, word3):
@@ -314,10 +317,17 @@
         # by 'rtype'
         mainvar = etype[0]
         searchvar = mainvar  + '1'
-        rql =  '%s %s WHERE %s %s %s, %s' % (etype, mainvar,  # Person P
-                                             mainvar, rtype, searchvar, # P worksAt C
-                                             self._complete_rql(searchstr, etype,
-                                                                rtype=rtype, var=searchvar))
+        restriction = self._complete_rql(searchstr, etype, rtype=rtype,
+                                         var=searchvar)
+        if ' has_text ' in restriction:
+            rql =  ('%s %s ORDERBY FTIRANK(%s) DESC '
+                    'WHERE %s %s %s, %s' % (etype, mainvar, searchvar,
+                                            mainvar, rtype, searchvar, # P worksAt C
+                                            restriction))
+        else:
+            rql =  ('%s %s WHERE %s %s %s, %s' % (etype, mainvar,
+                                            mainvar, rtype, searchvar, # P worksAt C
+                                            restriction))
         return rql, {'text': searchstr}
 
 
@@ -352,7 +362,7 @@
 
     def preprocess_query(self, uquery):
         """suppose it's a plain text query"""
-        return 'Any X WHERE X has_text %(text)s', {'text': uquery}
+        return 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s', {'text': uquery}
 
 
 
@@ -385,7 +395,6 @@
                     try:
                         return proc.process_query(uquery)
                     except TypeError, exc: # cw 3.5 compat
-                        print "EXC", exc
                         warn("[3.6] %s.%s.process_query() should now accept uquery "
                              "as unique argument, use self._cw instead of req"
                              % (proc.__module__, proc.__class__.__name__),
--- a/web/views/management.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/management.py	Mon Jul 19 15:37:02 2010 +0200
@@ -122,7 +122,7 @@
                                           cwperm.view('oneline')))
                 else:
                     w(u'<td>%s</td>' % cwperm.view('oneline'))
-                w(u'<td>%s</td>' % self.view('csv', cwperm.related('require_group'), 'null'))
+                w(u'<td>%s</td>' % self._cw.view('csv', cwperm.related('require_group'), 'null'))
                 w(u'</tr>\n')
             w(u'</table>')
         else:
@@ -203,7 +203,7 @@
         cversions = []
         for cube in self._cw.vreg.config.cubes():
             cubeversion = vcconf.get(cube, self._cw._('no version information'))
-            w(u"<b>Package %s version:</b> %s<br/>\n" % (cube, cubeversion))
+            w(u"<b>Cube %s version:</b> %s<br/>\n" % (cube, cubeversion))
             cversions.append((cube, cubeversion))
         w(u"</div>")
         # creates a bug submission link if submit-mail is set
@@ -237,7 +237,7 @@
         binfo += u'\n'.join(u'  * %s = %s' % (k, v) for k, v in req.form.iteritems())
     binfo += u'\n\n:CubicWeb version: %s\n'  % (eversion,)
     for pkg, pkgversion in cubes:
-        binfo += u":Package %s version: %s\n" % (pkg, pkgversion)
+        binfo += u":Cube %s version: %s\n" % (pkg, pkgversion)
     binfo += '\n'
     return binfo
 
--- a/web/views/massmailing.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/massmailing.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,18 +15,18 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Mass mailing form views
+"""Mass mailing handling: send mail to entities adaptable to IEmailable"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 import operator
 
-from cubicweb.interfaces import IEmailable
-from cubicweb.selectors import implements, authenticated_user
+from cubicweb.selectors import (is_instance, authenticated_user,
+                                adaptable, match_form_params)
 from cubicweb.view import EntityView
-from cubicweb.web import stdmsgs, action, form, formfields as ff
+from cubicweb.web import (Redirect, stdmsgs, controller, action,
+                          form, formfields as ff)
 from cubicweb.web.formwidgets import CheckBox, TextInput, AjaxWidget, ImgButton
 from cubicweb.web.views import forms, formrenderers
 
@@ -34,8 +34,9 @@
 class SendEmailAction(action.Action):
     __regid__ = 'sendemail'
     # XXX should check email is set as well
-    __select__ = (action.Action.__select__ & implements(IEmailable)
-                  & authenticated_user())
+    __select__ = (action.Action.__select__
+                  & authenticated_user()
+                  & adaptable('IEmailable'))
 
     title = _('send email')
     category = 'mainactions'
@@ -49,23 +50,28 @@
 
 
 def recipient_vocabulary(form, field):
-    vocab = [(entity.get_email(), entity.eid) for entity in form.cw_rset.entities()]
+    vocab = [(entity.cw_adapt_to('IEmailable').get_email(), unicode(entity.eid))
+             for entity in form.cw_rset.entities()]
     return [(label, value) for label, value in vocab if label]
 
+
 class MassMailingForm(forms.FieldsForm):
     __regid__ = 'massmailing'
 
-    needs_js = ('cubicweb.widgets.js', 'cubicweb.massmailing.js')
+    needs_js = ('cubicweb.widgets.js',)
     needs_css = ('cubicweb.mailform.css')
     domid = 'sendmail'
     action = 'sendmail'
 
     sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
                             label=_('From:'),
-                            value=lambda f: '%s <%s>' % (f._cw.user.dc_title(), f._cw.user.get_email()))
+                            value=lambda f: '%s <%s>' % (
+                                f._cw.user.dc_title(),
+                                f._cw.user.cw_adapt_to('IEmailable').get_email()))
     recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'),
                                choices=recipient_vocabulary,
-                               value= lambda f: [entity.eid for entity in f.cw_rset.entities() if entity.get_email()])
+                               value= lambda f: [entity.eid for entity in f.cw_rset.entities()
+                                                 if entity.cw_adapt_to('IEmailable').get_email()])
     subject = ff.StringField(label=_('Subject:'), max_length=256)
     mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
                                                 inputid='mailbody'))
@@ -73,7 +79,7 @@
     form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
                               _('send email'), 'SEND_EMAIL_ICON'),
                     ImgButton('cancelbutton', "javascript: history.back()",
-                              stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
+                              _(stdmsgs.BUTTON_CANCEL[0]), stdmsgs.BUTTON_CANCEL[1])]
     form_renderer_id = __regid__
 
     def __init__(self, *args, **kwargs):
@@ -84,12 +90,12 @@
     def get_allowed_substitutions(self):
         attrs = []
         for coltype in self.cw_rset.column_types(0):
-            eclass = self._cw.vreg['etypes'].etype_class(coltype)
-            attrs.append(eclass.allowed_massmail_keys())
+            entity = self._cw.vreg['etypes'].etype_class(coltype)(self._cw)
+            attrs.append(entity.cw_adapt_to('IEmailable').allowed_massmail_keys())
         return sorted(reduce(operator.and_, attrs))
 
     def build_substitutions_help(self):
-        insertLink = u'<a href="javascript: insertText(\'%%(%s)s\', \'emailarea\');">%%(%s)s</a>'
+        insertLink = u'<a href="javascript: cw.widgets.insertText(\'%%(%s)s\', \'emailarea\');">%%(%s)s</a>'
         substs = (u'<div class="substitution">%s</div>' % (insertLink % (subst, subst))
                   for subst in self.get_allowed_substitutions())
         helpmsg = self._cw._('You can use any of the following substitutions in your text')
@@ -135,9 +141,36 @@
 
 class MassMailingFormView(form.FormViewMixIn, EntityView):
     __regid__ = 'massmailing'
-    __select__ = implements(IEmailable) & authenticated_user()
+    __select__ = authenticated_user() & adaptable('IEmailable')
 
     def call(self):
         form = self._cw.vreg['forms'].select('massmailing', self._cw,
                                              rset=self.cw_rset)
         self.w(form.render())
+
+
+class SendMailController(controller.Controller):
+    __regid__ = 'sendmail'
+    __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
+
+    def recipients(self):
+        """returns an iterator on email's recipients as entities"""
+        eids = self._cw.form['recipient']
+        # eids may be a string if only one recipient was specified
+        if isinstance(eids, basestring):
+            rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
+        else:
+            rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
+        return rset.entities()
+
+    def publish(self, rset=None):
+        # XXX this allows users with access to an cubicweb instance to use it as
+        # a mail relay
+        body = self._cw.form['mailbody']
+        subject = self._cw.form['subject']
+        for recipient in self.recipients():
+            iemailable = recipient.cw_adapt_to('IEmailable')
+            text = body % iemailable.as_email_context()
+            self.sendmail(iemailable.get_email(), subject, text)
+        url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
+        raise Redirect(url)
--- a/web/views/navigation.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/navigation.py	Mon Jul 19 15:37:02 2010 +0200
@@ -25,11 +25,10 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import deprecated
 
-from cubicweb.interfaces import IPrevNext
 from cubicweb.selectors import (paginated_rset, sorted_rset,
-                                primary_view, match_context_prop,
-                                one_line_rset, implements)
+                                adaptable, implements)
 from cubicweb.uilib import cut
+from cubicweb.view import EntityAdapter, implements_adapter_compat
 from cubicweb.web.component import EntityVComponent, NavigationComponent
 
 
@@ -133,7 +132,7 @@
                 if rel is None:
                     continue
                 attrname = rel.r_type
-                if attrname == 'is':
+                if attrname in ('is', 'has_text'):
                     continue
                 if not rschema(attrname).final:
                     col = var.selected_index()
@@ -182,20 +181,44 @@
         self.w(u'</div>')
 
 
+from cubicweb.interfaces import IPrevNext
+
+class IPrevNextAdapter(EntityAdapter):
+    """interface for entities which can be linked to a previous and/or next
+    entity
+    """
+    __regid__ = 'IPrevNext'
+    __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
+
+    @implements_adapter_compat('IPrevNext')
+    def next_entity(self):
+        """return the 'next' entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IPrevNext')
+    def previous_entity(self):
+        """return the 'previous' entity"""
+        raise NotImplementedError
+
+
 class NextPrevNavigationComponent(EntityVComponent):
     __regid__ = 'prevnext'
     # register msg not generated since no entity implements IPrevNext in cubicweb
     # itself
     title = _('contentnavigation_prevnext')
     help = _('contentnavigation_prevnext_description')
-    __select__ = (one_line_rset() & primary_view()
-                  & match_context_prop() & implements(IPrevNext))
+    __select__ = EntityVComponent.__select__ & adaptable('IPrevNext')
     context = 'navbottom'
     order = 10
+
     def call(self, view=None):
-        entity = self.cw_rset.get_entity(0, 0)
-        previous = entity.previous_entity()
-        next = entity.next_entity()
+        self.cell_call(0, 0, view=view)
+
+    def cell_call(self, row, col, view=None):
+        entity = self.cw_rset.get_entity(row, col)
+        adapter = entity.cw_adapt_to('IPrevNext')
+        previous = adapter.previous_entity()
+        next = adapter.next_entity()
         if previous or next:
             textsize = self._cw.property_value('navigation.short-line-size')
             self.w(u'<div class="prevnext">')
@@ -248,9 +271,11 @@
         nav.clean_params(params)
         # make a link to see them all
         if show_all_option:
-            url = xml_escape(req.build_url(__force_display=1, **params))
-            w(u'<span><a href="%s">%s</a></span>\n'
-              % (url, req._('show %s results') % len(rset)))
+            basepath = req.relative_path(includeparams=False)
+            params['__force_display'] = 1
+            url = nav.page_url(basepath, params)
+            w(u'<div><a href="%s">%s</a></div>\n'
+              % (xml_escape(url), req._('show %s results') % len(rset)))
         rset.limit(offset=start, limit=stop-start, inplace=True)
 
 
--- a/web/views/old_calendar.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/old_calendar.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +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/>.
-"""html calendar views
-
-"""
+"""html calendar views"""
 
 from datetime import date, time, timedelta
 
@@ -26,8 +24,26 @@
                                  next_month, first_day, last_day, date_range)
 
 from cubicweb.interfaces import ICalendarViews
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+class ICalendarViewsAdapter(EntityAdapter):
+    """calendar views interface"""
+    __regid__ = 'ICalendarViews'
+    __select__ = implements(ICalendarViews, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ICalendarViews')
+    def matching_dates(self, begin, end):
+        """
+        :param begin: day considered as begin of the range (`DateTime`)
+        :param end: day considered as end of the range (`DateTime`)
+
+        :return:
+          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+          this entity apply
+        """
+        raise NotImplementedError
+
 
 # used by i18n tools
 WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
@@ -39,7 +55,7 @@
 
 class _CalendarView(EntityView):
     """base calendar view containing helpful methods to build calendar views"""
-    __select__ = implements(ICalendarViews,)
+    __select__ = adaptable('ICalendarViews')
     paginable = False
 
     # Navigation building methods / views ####################################
@@ -126,7 +142,7 @@
             infos = u'<div class="event">'
             infos += self._cw.view(itemvid, self.cw_rset, row=row)
             infos += u'</div>'
-            for date_ in entity.matching_dates(begin, end):
+            for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end):
                 day = date(date_.year, date_.month, date_.day)
                 try:
                     dt = time(date_.hour, date_.minute, date_.second)
@@ -288,7 +304,7 @@
             monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
             self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \
                   % (_('week'), monday.isocalendar()[1], monthlink))
-            for day in date_range(monday, sunday):
+            for day in date_range(monday, sunday+ONEDAY):
                 self.w(u'<tr>')
                 self.w(u'<td>%s</td>' % _(WEEKDAYS[day.weekday()]))
                 self.w(u'<td>%s</td>' % (day.strftime('%Y-%m-%d')))
@@ -478,7 +494,7 @@
             w(u'<tr>%s</tr>' % (
                 WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
             w(u'<tr><th>%s</th><th>&#160;</th></tr>'% _(u'Date'))
-            for day in date_range(monday, sunday):
+            for day in date_range(monday, sunday+ONEDAY):
                 events = schedule.get(day)
                 style = day.weekday() % 2 and "even" or "odd"
                 w(u'<tr class="%s">' % style)
--- a/web/views/plots.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/plots.py	Mon Jul 19 15:37:02 2010 +0200
@@ -23,10 +23,9 @@
 from logilab.common.date import datetime2ticks
 from logilab.mtconverter import xml_escape
 
-from cubicweb.utils import UStringIO
+from cubicweb.utils import UStringIO, json_dumps
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import multi_columns_rset
-from cubicweb.web import dumps
 from cubicweb.web.views import baseviews
 
 @objectify_selector
@@ -107,7 +106,7 @@
         #     cf. function onPlotHover in cubicweb.flot.js
         if self.timemode:
             plot = [(datetime2ticks(x), y, datetime2ticks(x)) for x, y in plot]
-        return dumps(plot)
+        return json_dumps(plot)
 
     def _render(self, req, width=500, height=400):
         if req.ie_browser():
--- a/web/views/primary.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/primary.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""The default primary view
+"""The default primary view"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/reledit.py	Mon Jul 19 15:37:02 2010 +0200
@@ -0,0 +1,355 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""the 'reedit' feature (eg edit attribute/relation from primary view)
+"""
+
+import copy
+
+from logilab.mtconverter import xml_escape
+
+from cubicweb import neg_role
+from cubicweb.schema import display_name
+from cubicweb.utils import json_dumps
+from cubicweb.selectors import non_final_entity, match_kwargs
+from cubicweb.view import EntityView
+from cubicweb.web import uicfg, stdmsgs
+from cubicweb.web.form import FormViewMixIn, FieldNotFound
+from cubicweb.web.formwidgets import Button, SubmitButton
+
+class DummyForm(object):
+    __slots__ = ('event_args',)
+    def form_render(self, **_args):
+        return u''
+    def render(self, *_args, **_kwargs):
+        return u''
+    def append_field(self, *args):
+        pass
+    def field_by_name(self, rtype, role, eschema=None):
+        return None
+
+class ClickAndEditFormView(FormViewMixIn, EntityView):
+    __regid__ = 'doreledit'
+    __select__ = non_final_entity() & match_kwargs('rtype')
+
+    # ui side continuations
+    _onclick = (u"cw.reledit.loadInlineEditionForm('%(formid)s', %(eid)s, '%(rtype)s', '%(role)s', "
+                "'%(divid)s', %(reload)s, '%(vid)s', '%(default_value)s');")
+    _cancelclick = "cw.reledit.cleanupAfterCancel('%s')"
+
+    # ui side actions/buttons
+    _addzone = u'<img title="%(msg)s" src="data/plus.png" alt="%(msg)s"/>'
+    _addmsg = _('click to add a value')
+    _deletezone = u'<img title="%(msg)s" src="data/cancel.png" alt="%(msg)s"/>'
+    _deletemsg = _('click to delete this value')
+    _editzone = u'<img title="%(msg)s" src="data/pen_icon.png" alt="%(msg)s"/>'
+    _editzonemsg = _('click to edit this field')
+
+    # default relation vids according to cardinality
+    _one_rvid = 'incontext'
+    _many_rvid = 'csv'
+
+    def cell_call(self, row, col, rtype=None, role='subject',
+                  reload=False, # controls reloading the whole page after change
+                                # boolean, eid (to redirect), or
+                                # function taking the subject entity & returning a boolean or an eid
+                  rvid=None,    # vid to be applied to other side of rtype (non final relations only)
+                  default_value=None,
+                  formid=None
+                  ):
+        """display field to edit entity's `rtype` relation on click"""
+        assert rtype
+        assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
+        if self.__regid__ == 'doreledit':
+            assert formid
+        self._cw.add_js('cubicweb.reledit.js')
+        if formid:
+            self._cw.add_js('cubicweb.edition.js')
+        self._cw.add_css('cubicweb.form.css')
+        entity = self.cw_rset.get_entity(row, col)
+        rschema = self._cw.vreg.schema[rtype]
+        reload = self._compute_reload(entity, rschema, role, reload)
+        default_value = self._compute_default_value(entity, rschema, role, default_value)
+        # compute value, checking perms, build & display form
+        divid = self._build_divid(rtype, role, entity.eid)
+        if rschema.final:
+            value = entity.printable_value(rtype)
+            form, renderer = self._build_form(entity, rtype, role, divid, 'base',
+                                              default_value, reload)
+            if not self._should_edit_attribute(entity, rschema, form):
+                self.w(value)
+                return
+            value = value or default_value
+            field = form.field_by_name(rtype, role, entity.e_schema)
+            form.append_field(field)
+            self.view_form(divid, value, form, renderer)
+        else:
+            rvid = self._compute_best_vid(entity.e_schema, rschema, role)
+            related_rset = entity.related(rtype, role)
+            if related_rset:
+                value = self._cw.view(rvid, related_rset)
+            else:
+                value = default_value
+            ttypes = self._compute_ttypes(rschema, role)
+
+            if not self._should_edit_relation(entity, rschema, role):
+                self.w(value)
+                return
+            # this is for attribute-like composites (1 target type, 1 related entity at most)
+            add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes)
+            edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes)
+            delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role)
+            # compute formid
+            if len(ttypes) > 1: # redundant safety belt
+                formid = 'base'
+            else:
+                afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0])
+                # is there an afs spec that says we should edit
+                # the rschema as an attribute ?
+                if afs and 'main_attributes' in afs:
+                    formid = 'base'
+
+            form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value,
+                                              reload, dict(vid=rvid),
+                                              edit_related, add_related and ttypes[0])
+            if formid == 'base':
+                field = form.field_by_name(rtype, role, entity.e_schema)
+                form.append_field(field)
+            self.view_form(divid, value, form, renderer, edit_related,
+                           delete_related, add_related)
+
+    def _compute_best_vid(self, eschema, rschema, role):
+        if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
+            return self._many_rvid
+        return self._one_rvid
+
+    def _compute_ttypes(self, rschema, role):
+        dual_role = neg_role(role)
+        return getattr(rschema, '%ss' % dual_role)()
+
+    def _compute_reload(self, entity, rschema, role, reload):
+        rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
+        ctrl_reload = rule.get('reload', reload)
+        if callable(ctrl_reload):
+            ctrl_reload = ctrl_reload(entity)
+        if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False
+            ctrl_reload = self._cw.build_url(ctrl_reload)
+        return ctrl_reload
+
+    def _compute_default_value(self, entity, rschema, role, default_value):
+        etype = entity.e_schema.type
+        rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*')
+        ctrl_default = rule.get('default_value', default_value)
+        if ctrl_default:
+            return ctrl_default
+        if default_value is None:
+            return xml_escape(self._cw._('<%s not specified>') %
+                              display_name(self._cw, rschema.type, role))
+        return default_value
+
+    def _is_composite(self, eschema, rschema, role):
+        return eschema.rdef(rschema, role).composite == role
+
+    def _may_add_related(self, related_rset, entity, rschema, role, ttypes):
+        """ ok for attribute-like composite entities """
+        if self._is_composite(entity.e_schema, rschema, role):
+            if len(ttypes) > 1: # wrong cardinality: do not handle
+                return False
+            ttype = ttypes[0]
+            card = rschema.rdef(entity.e_schema, ttype).role_cardinality(role)
+            if related_rset and card in '?1':
+                return False
+            if rschema.has_perm(self._cw, 'add', toetype=ttype):
+                return True
+        return False
+
+    def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes):
+        """ controls the edition of the related entity """
+        if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1':
+            return False
+        if len(related_rset.rows) != 1:
+            return False
+        if len(ttypes) > 1:
+            return False
+        if not self._is_composite(entity.e_schema, rschema, role):
+            return False
+        return related_rset.get_entity(0, 0).cw_has_perm('update')
+
+    def _may_delete_related(self, related_rset, entity, rschema, role):
+        # we assume may_edit_related
+        kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
+        if not rschema.has_perm(self._cw, 'delete', **kwargs):
+            return False
+        for related_entity in related_rset.entities():
+            if not related_entity.cw_has_perm('delete'):
+                return False
+        return True
+
+    def _build_edit_zone(self):
+        return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))}
+
+    def _build_delete_zone(self):
+        return self._deletezone % {'msg': xml_escape(self._cw._(self._deletemsg))}
+
+    def _build_add_zone(self):
+        return self._addzone % {'msg': xml_escape(self._cw._(self._addmsg))}
+
+    def _build_divid(self, rtype, role, entity_eid):
+        """ builds an id for the root div of a reledit widget """
+        return '%s-%s-%s' % (rtype, role, entity_eid)
+
+    def _build_args(self, entity, rtype, role, formid, default_value, reload,
+                    extradata=None):
+        divid = self._build_divid(rtype, role, entity.eid)
+        event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'formid': formid,
+                      'reload' : json_dumps(reload), 'default_value' : default_value,
+                      'role' : role, 'vid' : u''}
+        if extradata:
+            event_args.update(extradata)
+        return event_args
+
+    def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
+                    extradata=None, edit_related=False, add_related=False, **formargs):
+        event_args = self._build_args(entity, rtype, role, formid, default_value,
+                                      reload, extradata)
+        cancelclick = self._cancelclick % divid
+        if edit_related and not add_related:
+            display_fields = None
+            display_label = True
+            related_entity = entity.related(rtype, role).get_entity(0, 0)
+            self._cw.form['eid'] = related_entity.eid
+        elif add_related:
+            display_fields = None
+            display_label = True
+            _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw)
+            _new_entity.eid = self._cw.varmaker.next()
+            related_entity = _new_entity
+            self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role))
+        else: # base case: edition/attribute relation
+            display_fields = [(rtype, role)]
+            display_label = False
+            related_entity = entity
+        form = self._cw.vreg['forms'].select(
+            formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid,
+            display_fields=display_fields, formtype='inlined',
+            action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'),
+            cwtarget='eformframe', cssstyle='display: none',
+            **formargs)
+        # pass reledit arguments
+        for pname, pvalue in event_args.iteritems():
+            form.add_hidden('__reledit|' + pname, pvalue)
+        # handle buttons
+        if form.form_buttons: # edition, delete
+            form_buttons = []
+            for button in form.form_buttons:
+                if not button.label.endswith('apply'):
+                    if button.label.endswith('cancel'):
+                        button = copy.deepcopy(button)
+                        button.cwaction = None
+                        button.onclick = cancelclick
+                    form_buttons.append(button)
+            form.form_buttons = form_buttons
+        else: # base
+            form.form_buttons = [SubmitButton(),
+                                 Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)]
+        form.event_args = event_args
+        renderer = self._cw.vreg['formrenderers'].select(
+            'base', self._cw, entity=related_entity, display_label=display_label,
+            display_help=False, table_class='',
+            button_bar_class='buttonbar', display_progress_div=False)
+        return form, renderer
+
+    def _should_edit_attribute(self, entity, rschema, form):
+        # examine rtags
+        noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False)
+        if noedit:
+            return False
+        rdef = entity.e_schema.rdef(rschema)
+        afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object)
+        if 'main_hidden' in  afs:
+            return False
+        # check permissions
+        if not entity.cw_has_perm('update'):
+            return False
+        rdef = entity.e_schema.rdef(rschema)
+        if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
+            return False
+        # XXX ?
+        try:
+            form.field_by_name(str(rschema), 'subject', entity.e_schema)
+        except FieldNotFound:
+            return False
+        return True
+
+    def _should_edit_relation(self, entity, rschema, role):
+        # examine rtags
+        rtype = rschema.type
+        noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False)
+        if noedit:
+            return False
+        rdef = entity.e_schema.rdef(rschema, role)
+        afs = uicfg.autoform_section.etype_get(
+            entity.__regid__, rschema, role, rdef.object)
+        if 'main_hidden' in afs:
+            return False
+        perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
+        return rschema.has_perm(self._cw, 'add', **perm_args)
+
+    def view_form(self, divid, value, form=None, renderer=None,
+                  edit_related=False, delete_related=False, add_related=False):
+        w = self.w
+        w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' %
+          {'id': divid,
+           'out': "jQuery('#%s').addClass('hidden')" % divid,
+           'over': "jQuery('#%s').removeClass('hidden')" % divid})
+        w(u'<div id="%s-value" class="editableFieldValue">' % divid)
+        w(value)
+        w(u'</div>')
+        w(form.render(renderer=renderer))
+        w(u'<div id="%s" class="editableField hidden">' % divid)
+        args = form.event_args.copy()
+        if not add_related: # excludes edition
+            args['formid'] = 'edition'
+            w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg)))
+            w(self._build_edit_zone())
+            w(u'</div>')
+        else:
+            args['formid'] = 'edition'
+            w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg)))
+            w(self._build_add_zone())
+            w(u'</div>')
+        if delete_related:
+            args['formid'] = 'deleteconf'
+            w(u'<div id="%s-delete" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg)))
+            w(self._build_delete_zone())
+            w(u'</div>')
+        w(u'</div>')
+        w(u'</div>')
+
+class AutoClickAndEditFormView(ClickAndEditFormView):
+    __regid__ = 'reledit'
+
+    def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
+                  extradata=None, edit_related=False, add_related=False, **formargs):
+        event_args = self._build_args(entity, rtype, role, 'base', default_value,
+                                      reload, extradata)
+        form = DummyForm()
+        form.event_args = event_args
+        return form, None
--- a/web/views/schema.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/schema.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,27 +15,31 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for schema related entities
+"""Specific views for schema related entities"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from itertools import cycle
 
+import tempfile
+import os, os.path as osp
+
+from logilab.common.graph import GraphGenerator, DotBackend
 from logilab.common.ureports import Section, Table
 from logilab.mtconverter import xml_escape
 from yams import BASE_TYPES, schema2dot as s2d
 from yams.buildobjs import DEFAULT_ATTRPERMS
 
-from cubicweb.selectors import (implements, yes, match_user_groups,
-                                has_related_entities, authenticated_user)
+from cubicweb.selectors import (is_instance, match_user_groups, match_kwargs,
+                                has_related_entities, authenticated_user, yes)
 from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                              WORKFLOW_TYPES, INTERNAL_TYPES)
+from cubicweb.utils import make_uid
 from cubicweb.view import EntityView, StartupView
 from cubicweb import tags, uilib
 from cubicweb.web import action, facet, uicfg, schemaviewer
 from cubicweb.web.views import TmpFileViewMixin
-from cubicweb.web.views import primary, baseviews, tabs, tableview, iprogress
+from cubicweb.web.views import primary, baseviews, tabs, tableview, ibreadcrumbs
 
 ALWAYS_SKIP_TYPES = BASE_TYPES | SCHEMA_TYPES
 SKIP_TYPES  = (ALWAYS_SKIP_TYPES | META_RTYPES | SYSTEM_RTYPES | WORKFLOW_TYPES
@@ -83,7 +87,7 @@
         self._cw.add_css('cubicweb.acl.css')
         w = self.w
         _ = self._cw._
-        w(u'<table class="schemaInfo">')
+        w(u'<table class="listing schemaInfo">')
         w(u'<tr><th>%s</th><th>%s</th><th>%s</th></tr>' % (
             _("permission"), _('granted to groups'), _('rql expressions')))
         for action in erschema.ACTIONS:
@@ -158,10 +162,7 @@
         self.w(u'<div><a href="%s">%s</a></div>' %
                (self._cw.build_url('view', vid='owl'),
                 self._cw._(u'Download schema as OWL')))
-        self.w(u'<img src="%s" alt="%s"/>\n' % (
-            xml_escape(self._cw.build_url('view', vid='schemagraph', skipmeta=1)),
-            self._cw._("graphical representation of the instance'schema")))
-
+        self.wview('schemagraph')
 
 class SchemaETypeTab(StartupView):
     __regid__ = 'schema-entity-types'
@@ -202,13 +203,13 @@
         url = xml_escape(self._cw.build_url('schema'))
         self.w(u'<div id="schema_security">')
         self.w(u'<h2 class="schema">%s</h2>' % _('Index'))
-        self.w(u'<h4 id="entities">%s</h4>' % _('Entity types'))
+        self.w(u'<h3 id="entities">%s</h3>' % _('Entity types'))
         ents = []
         for eschema in sorted(entities):
             ents.append(u'<a class="grey" href="%s#%s">%s</a>' % (
                 url,  eschema.type, eschema.type))
         self.w(u', '.join(ents))
-        self.w(u'<h4 id="relations">%s</h4>' % _('Relation types'))
+        self.w(u'<h3 id="relations">%s</h3>' % _('Relation types'))
         rels = []
         for rschema in sorted(relations):
             rels.append(u'<a class="grey" href="%s#%s">%s</a>' %  (
@@ -248,7 +249,7 @@
                 eschema.type, self._cw.build_url('cwetype/%s' % eschema.type),
                 eschema.type, _(eschema.type)))
             self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
-                url,  self._cw.external_resource('UP_ICON'), _('up')))
+                url,  self._cw.uiprops['UP_ICON'], _('up')))
             self.w(u'</h3>')
             self.w(u'<div style="margin: 0px 1.5em">')
             self.permissions_table(eschema)
@@ -277,7 +278,7 @@
                 rschema.type, self._cw.build_url('cwrtype/%s' % rschema.type),
                 rschema.type, _(rschema.type)))
             self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
-                url,  self._cw.external_resource('UP_ICON'), _('up')))
+                url,  self._cw.uiprops['UP_ICON'], _('up')))
             self.w(u'</h3>')
             self.grouped_permissions_table(rschema)
 
@@ -288,7 +289,7 @@
 _('i18ncard_1'), _('i18ncard_?'), _('i18ncard_+'), _('i18ncard_*')
 
 class CWETypePrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWEType')
+    __select__ = is_instance('CWEType')
     tabs = [_('cwetype-description'), _('cwetype-box'), _('cwetype-workflow'),
             _('cwetype-views'), _('cwetype-permissions')]
     default_tab = 'cwetype-description'
@@ -296,7 +297,7 @@
 
 class CWETypeDescriptionTab(tabs.PrimaryTab):
     __regid__ = 'cwetype-description'
-    __select__ = tabs.PrimaryTab.__select__ & implements('CWEType')
+    __select__ = tabs.PrimaryTab.__select__ & is_instance('CWEType')
 
     def render_entity_attributes(self, entity):
         super(CWETypeDescriptionTab, self).render_entity_attributes(entity)
@@ -311,9 +312,7 @@
             self.wview('csv', entity.related('specializes', 'object'))
             self.w(u'</div>')
         # entity schema image
-        self.w(u'<img src="%s" alt="%s"/>' % (
-            xml_escape(entity.absolute_url(vid='schemagraph')),
-            xml_escape(_('graphical schema for %s') % entity.name)))
+        self.wview('schemagraph', etype=entity.name)
         # entity schema attributes
         self.w(u'<h2>%s</h2>' % _('CWAttribute_plural'))
         rset = self._cw.execute(
@@ -380,7 +379,7 @@
 
 class CWETypeBoxTab(EntityView):
     __regid__ = 'cwetype-box'
-    __select__ = implements('CWEType')
+    __select__ = is_instance('CWEType')
 
     def cell_call(self, row, col):
         viewer = schemaviewer.SchemaViewer(self._cw)
@@ -393,7 +392,7 @@
 
 class CWETypePermTab(SecurityViewMixIn, EntityView):
     __regid__ = 'cwetype-permissions'
-    __select__ = implements('CWEType') & authenticated_user()
+    __select__ = is_instance('CWEType') & authenticated_user()
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -413,7 +412,7 @@
 
 class CWETypeWorkflowTab(EntityView):
     __regid__ = 'cwetype-workflow'
-    __select__ = (implements('CWEType')
+    __select__ = (is_instance('CWEType')
                   & has_related_entities('workflow_of', 'object'))
 
     def cell_call(self, row, col):
@@ -444,7 +443,7 @@
 class CWETypeViewsTab(EntityView):
     """possible views for this entity type"""
     __regid__ = 'cwetype-views'
-    __select__ = EntityView.__select__ & implements('CWEType')
+    __select__ = EntityView.__select__ & is_instance('CWEType')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -464,7 +463,7 @@
 
 
 class CWETypeOneLineView(baseviews.OneLineView):
-    __select__ = implements('CWEType')
+    __select__ = is_instance('CWEType')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
@@ -478,22 +477,20 @@
 # CWRType ######################################################################
 
 class CWRTypePrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWRType')
+    __select__ = is_instance('CWRType')
     tabs = [_('cwrtype-description'), _('cwrtype-permissions')]
     default_tab = 'cwrtype-description'
 
 
 class CWRTypeDescriptionTab(tabs.PrimaryTab):
     __regid__ = 'cwrtype-description'
-    __select__ = implements('CWRType')
+    __select__ = is_instance('CWRType')
 
     def render_entity_attributes(self, entity):
         super(CWRTypeDescriptionTab, self).render_entity_attributes(entity)
         _ = self._cw._
         if not entity.final:
-            msg = _('graphical schema for %s') % entity.name
-            self.w(tags.img(src=entity.absolute_url(vid='schemagraph'),
-                            alt=msg))
+            self.wview('schemagraph', rtype=entity.name)
         rset = self._cw.execute('Any R,C,R,R, RT WHERE '
                                 'R relation_type RT, RT eid %(x)s, '
                                 'R cardinality C', {'x': entity.eid})
@@ -506,7 +503,7 @@
 
 class CWRTypePermTab(SecurityViewMixIn, EntityView):
     __regid__ = 'cwrtype-permissions'
-    __select__ = implements('CWRType') & authenticated_user()
+    __select__ = is_instance('CWRType') & authenticated_user()
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -517,14 +514,14 @@
 # CWAttribute / CWRelation #####################################################
 
 class RDEFPrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
     tabs = [_('rdef-description'), _('rdef-permissions')]
     default_tab = 'rdef-description'
 
 
 class RDEFDescriptionTab(tabs.PrimaryTab):
     __regid__ = 'rdef-description'
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
 
     def render_entity_attributes(self, entity):
         super(RDEFDescriptionTab, self).render_entity_attributes(entity)
@@ -536,7 +533,7 @@
 
 class RDEFPermTab(SecurityViewMixIn, EntityView):
     __regid__ = 'rdef-permissions'
-    __select__ = implements('CWRelation', 'CWAttribute') & authenticated_user()
+    __select__ = is_instance('CWRelation', 'CWAttribute') & authenticated_user()
 
     def cell_call(self, row, col):
         self.permissions_table(self.cw_rset.get_entity(row, col).yams_schema())
@@ -548,7 +545,7 @@
     for instance)
     """
     __regid__ = 'rdef-name-cell'
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -561,7 +558,7 @@
     """same as RDEFNameView but when the context is the object entity
     """
     __regid__ = 'rdef-object-name-cell'
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -572,7 +569,7 @@
 
 class RDEFConstraintsCell(EntityView):
     __regid__ = 'rdef-constraints-cell'
-    __select__ = implements('CWAttribute', 'CWRelation')
+    __select__ = is_instance('CWAttribute', 'CWRelation')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -583,7 +580,7 @@
 
 class CWAttributeOptionsCell(EntityView):
     __regid__ = 'rdef-options-cell'
-    __select__ = implements('CWAttribute')
+    __select__ = is_instance('CWAttribute')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -598,7 +595,7 @@
 
 class CWRelationOptionsCell(EntityView):
     __regid__ = 'rdef-options-cell'
-    __select__ = implements('CWRelation',)
+    __select__ = is_instance('CWRelation',)
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -644,48 +641,152 @@
                            s2d.OneHopRSchemaVisitor):
     pass
 
+class CWSchemaDotPropsHandler(s2d.SchemaDotPropsHandler):
+    def __init__(self, visitor):
+        self.visitor = visitor
+        self.nextcolor = cycle( ('#ff7700', '#000000',
+                                 '#ebbc69', '#888888') ).next
+        self.colors = {}
 
-class SchemaImageView(TmpFileViewMixin, StartupView):
-    __regid__ = 'schemagraph'
-    content_type = 'image/png'
+    def node_properties(self, eschema):
+        """return DOT drawing options for an entity schema include href"""
+        label = ['{',eschema.type,'|']
+        label.append(r'\l'.join('%s (%s)' % (rel.type, eschema.rdef(rel.type).object)
+                                for rel in eschema.ordered_relations()
+                                    if rel.final and self.visitor.should_display_attr(eschema, rel)))
+        label.append(r'\l}') # trailing \l ensure alignement of the last one
+        return {'label' : ''.join(label), 'shape' : "record",
+                'fontname' : "Courier", 'style' : "filled",
+                'href': 'cwetype/%s' % eschema.type,
+                'fontsize': '10px'
+                }
 
-    def _generate(self, tmpfile):
-        """display global schema information"""
-        visitor = FullSchemaVisitor(self._cw, self._cw.vreg.schema,
-                                    skiptypes=skip_types(self._cw))
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
+    def edge_properties(self, rschema, subjnode, objnode):
+        """return default DOT drawing options for a relation schema"""
+        # symmetric rels are handled differently, let yams decide what's best
+        if rschema.symmetric:
+            kwargs = {'label': rschema.type,
+                      'color': '#887788', 'style': 'dashed',
+                      'dir': 'both', 'arrowhead': 'normal', 'arrowtail': 'normal',
+                      'fontsize': '10px', 'href': 'cwrtype/%s' % rschema.type}
+        else:
+            kwargs = {'label': rschema.type,
+                      'color' : 'black',  'style' : 'filled', 'fontsize': '10px',
+                      'href': 'cwrtype/%s' % rschema.type}
+            rdef = rschema.rdef(subjnode, objnode)
+            composite = rdef.composite
+            if rdef.composite == 'subject':
+                kwargs['arrowhead'] = 'none'
+                kwargs['arrowtail'] = 'diamond'
+            elif rdef.composite == 'object':
+                kwargs['arrowhead'] = 'diamond'
+                kwargs['arrowtail'] = 'none'
+            else:
+                kwargs['arrowhead'] = 'open'
+                kwargs['arrowtail'] = 'none'
+            # UML like cardinalities notation, omitting 1..1
+            if rdef.cardinality[1] != '1':
+                kwargs['taillabel'] = s2d.CARD_MAP[rdef.cardinality[1]]
+            if rdef.cardinality[0] != '1':
+                kwargs['headlabel'] = s2d.CARD_MAP[rdef.cardinality[0]]
+            try:
+                kwargs['color'] = self.colors[rschema]
+            except KeyError:
+                kwargs['color'] = self.nextcolor()
+                self.colors[rschema] = kwargs['color']
+        kwargs['fontcolor'] = kwargs['color']
+        # dot label decoration is just awful (1 line underlining the label
+        # + 1 line going to the closest edge spline point)
+        kwargs['decorate'] = 'false'
+        #kwargs['labelfloat'] = 'true'
+        return kwargs
 
 
-class CWETypeSchemaImageView(TmpFileViewMixin, EntityView):
+class SchemaGraphView(StartupView):
     __regid__ = 'schemagraph'
-    __select__ = implements('CWEType')
-    content_type = 'image/png'
 
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
-        eschema = self._cw.vreg.schema.eschema(entity.name)
-        visitor = OneHopESchemaVisitor(self._cw, eschema,
-                                       skiptypes=skip_types(self._cw))
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
-
+    def call(self, etype=None, rtype=None, alt=''):
+        schema = self._cw.vreg.schema
+        if etype:
+            assert rtype is None
+            visitor = OneHopESchemaVisitor(self._cw, schema.eschema(etype),
+                                           skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of the %(etype)s '
+                             'entity type from %(appid)s data model')
+        elif rtype:
+            visitor = OneHopRSchemaVisitor(self._cw, schema.rschema(rtype),
+                                           skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of the %(rtype)s '
+                             'relation type from %(appid)s data model')
+        else:
+            visitor = FullSchemaVisitor(self._cw, schema,
+                                        skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of %(appid)s data model')
+        alt %= {'rtype': rtype, 'etype': etype,
+                'appid': self._cw.vreg.config.appid}
+        prophdlr = CWSchemaDotPropsHandler(visitor)
+        generator = GraphGenerator(DotBackend('schema', 'BT',
+                                              ratio='compress',size=None,
+                                              renderer='dot',
+                                              additionnal_param={
+                                                  'overlap':'false',
+                                                  'splines':'true',
+                                                  'sep':'0.2',
+                                              }))
+        # map file
+        pmap, mapfile = tempfile.mkstemp(".map")
+        os.close(pmap)
+        # image file
+        fd, tmpfile = tempfile.mkstemp('.png')
+        os.close(fd)
+        generator.generate(visitor, prophdlr, tmpfile, mapfile)
+        filekeyid = make_uid()
+        self._cw.session.data[filekeyid] = tmpfile
+        self.w(u'<img src="%s" alt="%s" usemap="#schema" />' % (
+            xml_escape(self._cw.build_url(vid='tmppng', tmpfile=filekeyid)),
+            xml_escape(self._cw._(alt))))
+        stream = open(mapfile, 'r').read()
+        stream = stream.decode(self._cw.encoding)
+        self.w(stream)
+        os.unlink(mapfile)
 
-class CWRTypeSchemaImageView(CWETypeSchemaImageView):
-    __select__ = implements('CWRType')
+# breadcrumbs ##################################################################
+
+class CWRelationIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWRelation')
+    def parent_entity(self):
+        return self.entity.rtype
+
+class CWAttributeIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWAttribute')
+    def parent_entity(self):
+        return self.entity.stype
 
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
-        rschema = self._cw.vreg.schema.rschema(entity.name)
-        visitor = OneHopRSchemaVisitor(self._cw, rschema)
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
+class CWConstraintIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWConstraint')
+    def parent_entity(self):
+        if self.entity.reverse_constrained_by:
+            return self.entity.reverse_constrained_by[0]
+
+class RQLExpressionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('RQLExpression')
+    def parent_entity(self):
+        return self.entity.expression_of
+
+class CWPermissionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWPermission')
+    def parent_entity(self):
+        # XXX useless with permission propagation
+        permissionof = getattr(self.entity, 'reverse_require_permission', ())
+        if len(permissionof) == 1:
+            return permissionof[0]
 
 
 # misc: facets, actions ########################################################
 
 class CWFinalFacet(facet.AttributeFacet):
     __regid__ = 'cwfinal-facet'
-    __select__ = facet.AttributeFacet.__select__ & implements('CWEType', 'CWRType')
+    __select__ = facet.AttributeFacet.__select__ & is_instance('CWEType', 'CWRType')
     rtype = 'final'
 
 class ViewSchemaAction(action.Action):
--- a/web/views/startup.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/startup.py	Mon Jul 19 15:37:02 2010 +0200
@@ -26,7 +26,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.view import StartupView
-from cubicweb.selectors import match_user_groups, implements
+from cubicweb.selectors import match_user_groups, is_instance
 from cubicweb.schema import display_name
 from cubicweb.web import ajax_replace_url, uicfg, httpcache
 
@@ -42,7 +42,7 @@
     def call(self, **kwargs):
         """The default view representing the instance's management"""
         self._cw.add_css('cubicweb.manageview.css')
-        self.w(u'<div>\n')
+        self.w(u'<h1>%s</h1>' % self._cw.property_value('ui.site-title'))
         if not self.display_folders():
             self._main_index()
         else:
@@ -53,7 +53,6 @@
             self.folders()
             self.w(u'</td>')
             self.w(u'</tr></table>\n')
-        self.w(u'</div>\n')
 
     def _main_index(self):
         req = self._cw
@@ -79,8 +78,8 @@
             self.w(u'<br/><a href="%s">%s</a>\n' % (xml_escape(href), label))
 
     def folders(self):
-        self.w(u'<h4>%s</h4>\n' % self._cw._('Browse by category'))
-        self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
+        self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by category'))
+        self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1)
 
     def create_links(self):
         self.w(u'<ul class="createLink">')
@@ -93,20 +92,24 @@
         self.w(u'</ul>')
 
     def startup_views(self):
-        self.w(u'<h4>%s</h4>\n' % self._cw._('Startup views'))
+        self.w(u'<h2>%s</h2>\n' % self._cw._('Startup views'))
         self.startupviews_table()
 
     def startupviews_table(self):
         views = self._cw.vreg['views'].possible_views(self._cw, None)
+        if not views:
+            return
+        self.w(u'<ul class="startup">')
         for v in sorted(views, key=lambda x: self._cw._(x.title)):
             if v.category != 'startupview' or v.__regid__ in ('index', 'tree', 'manage'):
                 continue
-            self.w('<p><a href="%s">%s</a></p>' % (
+            self.w('<li><a href="%s">%s</a></li>' % (
                 xml_escape(v.url()), xml_escape(self._cw._(v.title).capitalize())))
+        self.w(u'</ul>')
 
     def entities(self):
         schema = self._cw.vreg.schema
-        self.w(u'<h4>%s</h4>\n' % self._cw._('The repository holds the following entities'))
+        self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by entity type'))
         manager = self._cw.user.matching_groups('managers')
         self.w(u'<table class="startup">')
         if manager:
--- a/web/views/tableview.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/tableview.py	Mon Jul 19 15:37:02 2010 +0200
@@ -16,17 +16,13 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """generic table view, including filtering abilities"""
+
 __docformat__ = "restructuredtext en"
 
-try:
-    from json import dumps
-except ImportError:
-    from simplejson import dumps
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb.selectors import nonempty_rset, match_form_params
-from cubicweb.utils import make_uid
+from cubicweb.utils import make_uid, json_dumps
 from cubicweb.view import EntityView, AnyRsetView
 from cubicweb import tags
 from cubicweb.uilib import toggle_action, limitsize, htmlescape
@@ -77,7 +73,7 @@
         # drop False / None values from vidargs
         vidargs = dict((k, v) for k, v in vidargs.iteritems() if v)
         w(u'<form method="post" cubicweb:facetargs="%s" action="">' %
-          xml_escape(dumps([divid, self.__regid__, False, vidargs])))
+          xml_escape(json_dumps([divid, self.__regid__, False, vidargs])))
         w(u'<fieldset id="%sForm" class="%s">' % (divid, hidden and 'hidden' or ''))
         w(u'<input type="hidden" name="divid" value="%s" />' % divid)
         w(u'<input type="hidden" name="fromformfilter" value="1" />')
@@ -197,7 +193,8 @@
         rql = params.pop('rql', self.cw_rset.printable_rql())
         # latest 'true' used for 'swap' mode
         return 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
-            dumps(divid), dumps(rql), dumps(self.__regid__), dumps(params))
+            json_dumps(divid), json_dumps(rql), json_dumps(self.__regid__),
+            json_dumps(params))
 
     def show_hide_actions(self, divid, currentlydisplayed=False):
         showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
@@ -213,7 +210,7 @@
 
     def render_actions(self, divid, actions):
         box = MenuWidget('', 'tableActionsBox', _class='', islist=False)
-        label = tags.img(src=self._cw.external_resource('PUCE_DOWN'),
+        label = tags.img(src=self._cw.uiprops['PUCE_DOWN'],
                          alt=xml_escape(self._cw._('action(s) on this selection')))
         menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
                             ident='%sActions' % divid)
@@ -386,9 +383,9 @@
             self._cw.add_css(self.css_files)
         _ = self._cw._
         self.columns = columns or self.columns
-        ecls = self._cw.vreg['etypes'].etype_class(self.cw_rset.description[0][0])
+        sample = self.cw_rset.get_entity(0, 0)
         self.w(u'<table class="%s">' % self.table_css)
-        self.table_header(ecls)
+        self.table_header(sample)
         self.w(u'<tbody>')
         for row in xrange(self.cw_rset.rowcount):
             self.cell_call(row=row, col=0)
@@ -407,22 +404,21 @@
             else:
                 content = entity.printable_value(col)
             infos[col] = content
-        self.w(u"""<tr onmouseover="addElementClass(this, 'highlighted');"
-            onmouseout="removeElementClass(this, 'highlighted')">""")
+        self.w(u"""<tr onmouseover="$(this).addClass('highlighted');"
+            onmouseout="$(this).removeClass('highlighted')">""")
         line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
         self.w(line % infos)
         self.w(u'</tr>\n')
 
-    def table_header(self, ecls):
+    def table_header(self, sample):
         """builds the table's header"""
         self.w(u'<thead><tr>')
-        _ = self._cw._
         for column in self.columns:
             meth = getattr(self, 'header_for_%s' % column, None)
             if meth:
-                colname = meth(ecls)
+                colname = meth(sample)
             else:
-                colname = _(column)
+                colname = self._cw._(column)
             self.w(u'<th>%s</th>' % xml_escape(colname))
         self.w(u'</tr></thead>\n')
 
--- a/web/views/tabs.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/tabs.py	Mon Jul 19 15:37:02 2010 +0200
@@ -175,7 +175,7 @@
     class ProjectScreenshotsView(EntityRelationView):
         '''display project's screenshots'''
         __regid__ = title = _('projectscreenshots')
-        __select__ = EntityRelationView.__select__ & implements('Project')
+        __select__ = EntityRelationView.__select__ & is_instance('Project')
         rtype = 'screenshot'
         role = 'subject'
         vid = 'gallery'
--- a/web/views/timeline.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/timeline.py	Mon Jul 19 15:37:02 2010 +0200
@@ -18,16 +18,15 @@
 """basic support for SIMILE's timline widgets
 
 cf. http://code.google.com/p/simile-widgets/
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
+from cubicweb.selectors import adaptable
 from cubicweb.view import EntityView, StartupView
-from cubicweb.web import json
+from cubicweb.utils import json_dumps
 
 _ = unicode
 
@@ -37,11 +36,12 @@
     should be properties of entity classes or subviews)
     """
     __regid__ = 'timeline-json'
+    __select__ = adaptable('ICalendarable')
+
     binary = True
     templatable = False
     content_type = 'application/json'
 
-    __select__ = implements(ICalendarable)
     date_fmt = '%Y/%m/%d'
 
     def call(self):
@@ -52,7 +52,7 @@
                 events.append(event)
         timeline_data = {'dateTimeFormat': self.date_fmt,
                          'events': events}
-        self.w(json.dumps(timeline_data))
+        self.w(json_dumps(timeline_data))
 
     # FIXME: those properties should be defined by the entity class
     def onclick_url(self, entity):
@@ -74,8 +74,9 @@
         'link': 'http://www.allposters.com/-sp/Portrait-of-Horace-Brodsky-Posters_i1584413_.htm'
         }
         """
-        start = entity.start
-        stop = entity.stop
+        icalendarable = entity.cw_adapt_to('ICalendarable')
+        start = icalendarable.start
+        stop = icalendarable.stop
         start = start or stop
         if start is None and stop is None:
             return None
@@ -116,7 +117,7 @@
     """builds a cubicweb timeline widget node"""
     __regid__ = 'timeline'
     title = _('timeline')
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
     paginable = False
     def call(self, tlunit=None):
         self._cw.html_headers.define_var('Timeline_urlPrefix', self._cw.datadir_url)
--- a/web/views/timetable.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/timetable.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,16 +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/>.
-"""html calendar views
+"""html timetable views"""
 
-"""
+__docformat__ = "restructuredtext en"
+_ = unicode
 
 from logilab.mtconverter import xml_escape
-from logilab.common.date import date_range, todatetime
+from logilab.common.date import ONEDAY, date_range, todatetime
 
-from cubicweb.interfaces import ITimetableViews
-from cubicweb.selectors import implements
-from cubicweb.view import AnyRsetView
+from cubicweb.selectors import adaptable
+from cubicweb.view import EntityView
 
 
 class _TaskEntry(object):
@@ -37,10 +37,10 @@
 MIN_COLS = 3  # minimum number of task columns for a single user
 ALL_USERS = object()
 
-class TimeTableView(AnyRsetView):
+class TimeTableView(EntityView):
     __regid__ = 'timetable'
     title = _('timetable')
-    __select__ = implements(ITimetableViews)
+    __select__ = adaptable('ICalendarable')
     paginable = False
 
     def call(self, title=None):
@@ -53,20 +53,22 @@
         # XXX: try refactoring with calendar.py:OneMonthCal
         for row in xrange(self.cw_rset.rowcount):
             task = self.cw_rset.get_entity(row, 0)
+            icalendarable = task.cw_adapt_to('ICalendarable')
             if len(self.cw_rset[row]) > 1:
                 user = self.cw_rset.get_entity(row, 1)
             else:
                 user = ALL_USERS
             the_dates = []
-            if task.start and task.stop:
-                if task.start.toordinal() == task.stop.toordinal():
-                    the_dates.append(task.start)
+            if icalendarable.start and icalendarable.stop:
+                if icalendarable.start.toordinal() == icalendarable.stop.toordinal():
+                    the_dates.append(icalendarable.start)
                 else:
-                    the_dates += date_range( task.start, task.stop )
-            elif task.start:
-                the_dates.append(task.start)
-            elif task.stop:
-                the_dates.append(task.stop)
+                    the_dates += date_range(icalendarable.start,
+                                            icalendarable.stop + ONEDAY)
+            elif icalendarable.start:
+                the_dates.append(icalendarable.start)
+            elif icalendarable.stop:
+                the_dates.append(icalendarable.stop)
             for d in the_dates:
                 d = todatetime(d)
                 d_users = dates.setdefault(d, {})
@@ -91,7 +93,7 @@
 
         visited_tasks = {} # holds a description of a task for a user
         task_colors = {}   # remember a color assigned to a task
-        for date in date_range(date_min, date_max):
+        for date in date_range(date_min, date_max + ONEDAY):
             columns = [date]
             d_users = dates.get(date, {})
             for user in users:
--- a/web/views/treeview.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/treeview.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,21 +15,97 @@
 #
 # 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 tree-building widgets, based on jQuery treeview plugin"""
+"""Set of tree views / tree-building widgets, some based on jQuery treeview
+plugin.
+"""
 
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
-from cubicweb.utils import make_uid
-from cubicweb.interfaces import ITree
-from cubicweb.selectors import implements
+
+from cubicweb.utils import make_uid, json
+from cubicweb.selectors import adaptable
 from cubicweb.view import EntityView
-from cubicweb.web import json
+from cubicweb.mixins import _done_init
+from cubicweb.web.views import baseviews
 
 def treecookiename(treeid):
     return str('%s-treestate' % treeid)
 
+
+class BaseTreeView(baseviews.ListView):
+    """base tree view"""
+    __regid__ = 'tree'
+    __select__ = adaptable('ITree')
+    item_vid = 'treeitem'
+
+    def call(self, done=None, **kwargs):
+        if done is None:
+            done = set()
+        super(BaseTreeView, self).call(done=done, **kwargs)
+
+    def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs):
+        assert maxlevel is None or maxlevel > 0
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<li class="badcontent">%s</li>' % entity)
+            return
+        self.open_item(entity)
+        entity.view(vid or self.item_vid, w=self.w, **kwargs)
+        if maxlevel is not None:
+            maxlevel -= 1
+            if maxlevel == 0:
+                self.close_item(entity)
+                return
+        relatedrset = entity.cw_adapt_to('ITree').children(entities=False)
+        self.wview(self.__regid__, relatedrset, 'null', done=done,
+                   maxlevel=maxlevel, **kwargs)
+        self.close_item(entity)
+
+    def open_item(self, entity):
+        self.w(u'<li class="%s">\n' % entity.__regid__.lower())
+    def close_item(self, entity):
+        self.w(u'</li>\n')
+
+
+class TreePathView(EntityView):
+    """a recursive path view"""
+    __regid__ = 'path'
+    __select__ = adaptable('ITree')
+    item_vid = 'oneline'
+    separator = u'&#160;&gt;&#160;'
+
+    def call(self, **kwargs):
+        self.w(u'<div class="pathbar">')
+        super(TreePathView, self).call(**kwargs)
+        self.w(u'</div>')
+
+    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<span class="badcontent">%s</span>' % entity)
+            return
+        parent = entity.cw_adapt_to('ITree').parent()
+        if parent:
+            parent.view(self.__regid__, w=self.w, done=done)
+            self.w(self.separator)
+        entity.view(vid or self.item_vid, w=self.w)
+
+class TreeComboBoxView(TreePathView):
+    """display folder in edition's combobox"""
+    __regid__ = 'combobox'
+    item_vid = 'text'
+    separator = u' > '
+
+# XXX rename regid to ajaxtree/foldabletree or something like that (same for
+# treeitemview)
 class TreeView(EntityView):
+    """ajax tree view, click to expand folder"""
+
     __regid__ = 'treeview'
     itemvid = 'treeitemview'
     subvid = 'oneline'
@@ -111,7 +187,8 @@
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
-        if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf():
+        itree = entity.cw_adapt_to('ITree')
+        if itree and not itree.is_leaf():
             self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
         else:
             # XXX define specific CSS classes according to mime types
@@ -119,7 +196,7 @@
 
 
 class DefaultTreeViewItemView(EntityView):
-    """default treeitem view for entities which don't implement ITree"""
+    """default treeitem view for entities which don't adapt to ITree"""
     __regid__ = 'treeitemview'
 
     def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs):
@@ -130,12 +207,12 @@
 
 
 class TreeViewItemView(EntityView):
-    """specific treeitem view for entities which implement ITree
+    """specific treeitem view for entities which adapt to ITree
 
     (each item should be expandable if it's not a tree leaf)
     """
     __regid__ = 'treeitemview'
-    __select__ = implements(ITree)
+    __select__ = adaptable('ITree')
     default_branch_state_is_open = False
 
     def open_state(self, eeid, treeid):
@@ -149,15 +226,16 @@
                   is_last=False, **morekwargs):
         w = self.w
         entity = self.cw_rset.get_entity(row, col)
+        itree = entity.cw_adapt_to('ITree')
         liclasses = []
         is_open = self.open_state(entity.eid, treeid)
-        is_leaf = not hasattr(entity, 'is_leaf') or entity.is_leaf()
+        is_leaf = itree is None or itree.is_leaf()
         if is_leaf:
             if is_last:
                 liclasses.append('last')
             w(u'<li class="%s">' % u' '.join(liclasses))
         else:
-            rql = entity.children_rql() % {'x': entity.eid}
+            rql = itree.children_rql() % {'x': entity.eid}
             url = xml_escape(self._cw.build_url('json', rql=rql, vid=parentvid,
                                                 pageid=self._cw.pageid,
                                                 treeid=treeid,
@@ -196,7 +274,7 @@
         # the local node info
         self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs)
         if is_open and not is_leaf: #  => rql is defined
-            self.wview(parentvid, entity.children(entities=False), subvid=vid,
+            self.wview(parentvid, itree.children(entities=False), subvid=vid,
                        treeid=treeid, initial_load=False, **morekwargs)
         w(u'</li>')
 
--- a/web/views/urlrewrite.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/urlrewrite.py	Mon Jul 19 15:37:02 2010 +0200
@@ -206,7 +206,7 @@
     __regid__ = 'schemabased'
     rules = [
         # rgxp : callback
-        (rgx('/search/(.+)'), build_rset(rql=r'Any X WHERE X has_text %(text)s',
+        (rgx('/search/(.+)'), build_rset(rql=r'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s',
                                          rgxgroups=[('text', 1)])),
         ]
 
--- a/web/views/vcard.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/vcard.py	Mon Jul 19 15:37:02 2010 +0200
@@ -20,7 +20,7 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.view import EntityView
 
 _ = unicode
@@ -33,7 +33,7 @@
     title = _('vcard')
     templatable = False
     content_type = 'text/x-vcard'
-    __select__ = implements('CWUser')
+    __select__ = is_instance('CWUser')
 
     def set_request_content_type(self):
         """overriden to set a .vcf filename"""
--- a/web/views/workflow.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/workflow.py	Mon Jul 19 15:37:02 2010 +0200
@@ -31,16 +31,16 @@
 from logilab.common.graph import escape, GraphGenerator, DotBackend
 
 from cubicweb import Unauthorized, view
-from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
+from cubicweb.selectors import (has_related_entities, one_line_rset,
                                 relation_possible, match_form_params,
-                                implements, score_entity)
+                                score_entity, is_instance, adaptable)
 from cubicweb.utils import make_uid
-from cubicweb.interfaces import IWorkflowable
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
 from cubicweb.web import uicfg, stdmsgs, action, component, form, action
 from cubicweb.web import formfields as ff, formwidgets as fwdgs
-from cubicweb.web.views import TmpFileViewMixin, forms, primary, autoform
+from cubicweb.web.views import TmpFileViewMixin
+from cubicweb.web.views import forms, primary, autoform, ibreadcrumbs
 from cubicweb.web.views.tabs import TabbedPrimaryView, PrimaryTab
 
 _pvs = uicfg.primaryview_section
@@ -90,8 +90,9 @@
 class ChangeStateFormView(form.FormViewMixIn, view.EntityView):
     __regid__ = 'statuschange'
     title = _('status change')
-    __select__ = (one_line_rset() & implements(IWorkflowable)
-                  & match_form_params('treid'))
+    __select__ = (one_line_rset()
+                  & match_form_params('treid')
+                  & adaptable('IWorkflowable'))
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -100,7 +101,7 @@
         self.w(u'<h4>%s %s</h4>\n' % (self._cw._(transition.name),
                                       entity.view('oneline')))
         msg = self._cw._('status will change from %(st1)s to %(st2)s') % {
-            'st1': entity.printable_state,
+            'st1': entity.cw_adapt_to('IWorkflowable').printable_state,
             'st2': self._cw._(transition.destination(entity).name)}
         self.w(u'<p>%s</p>\n' % msg)
         self.w(form.render())
@@ -129,7 +130,7 @@
 class WFHistoryView(EntityView):
     __regid__ = 'wfhistory'
     __select__ = relation_possible('wf_info_for', role='object') & \
-                 score_entity(lambda x: x.workflow_history)
+                 score_entity(lambda x: x.cw_adapt_to('IWorkflowable').workflow_history)
 
     title = _('Workflow history')
 
@@ -184,22 +185,24 @@
 
     def fill_menu(self, box, menu):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        menu.label = u'%s: %s' % (self._cw._('state'), entity.printable_state)
+        menu.label = u'%s: %s' % (self._cw._('state'),
+                                  entity.cw_adapt_to('IWorkflowable').printable_state)
         menu.append_anyway = True
         super(WorkflowActions, self).fill_menu(box, menu)
 
     def actual_actions(self):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         hastr = False
-        for tr in entity.possible_transitions():
+        for tr in iworkflowable.possible_transitions():
             url = entity.absolute_url(vid='statuschange', treid=tr.eid)
             yield self.build_action(self._cw._(tr.name), url)
             hastr = True
         # don't propose to see wf if user can't pass any transition
         if hastr:
-            wfurl = entity.current_workflow.absolute_url()
+            wfurl = iworkflowable.current_workflow.absolute_url()
             yield self.build_action(self._cw._('view workflow'), wfurl)
-        if entity.workflow_history:
+        if iworkflowable.workflow_history:
             wfurl = entity.absolute_url(vid='wfhistory')
             yield self.build_action(self._cw._('view history'), wfurl)
 
@@ -223,14 +226,14 @@
 _abaa.tag_object_of(('WorkflowTransition', 'transition_of', 'Workflow'), True)
 
 class WorkflowPrimaryView(TabbedPrimaryView):
-    __select__ = implements('Workflow')
+    __select__ = is_instance('Workflow')
     tabs = [  _('wf_tab_info'), _('wfgraph'),]
     default_tab = 'wf_tab_info'
 
 
 class CellView(view.EntityView):
     __regid__ = 'cell'
-    __select__ = implements('TrInfo')
+    __select__ = is_instance('TrInfo')
 
     def cell_call(self, row, col, cellvid=None):
         self.w(self.cw_rset.get_entity(row, col).view('reledit', rtype='comment'))
@@ -239,7 +242,7 @@
 class StateInContextView(view.EntityView):
     """convenience trick, State's incontext view should not be clickable"""
     __regid__ = 'incontext'
-    __select__ = implements('State')
+    __select__ = is_instance('State')
 
     def cell_call(self, row, col):
         self.w(xml_escape(self._cw.view('textincontext', self.cw_rset,
@@ -247,7 +250,7 @@
 
 class WorkflowTabTextView(PrimaryTab):
     __regid__ = 'wf_tab_info'
-    __select__ = PrimaryTab.__select__ & one_line_rset() & implements('Workflow')
+    __select__ = PrimaryTab.__select__ & one_line_rset() & is_instance('Workflow')
 
     def render_entity_attributes(self, entity):
         _ = self._cw._
@@ -273,7 +276,7 @@
 
 class TransitionSecurityTextView(view.EntityView):
     __regid__ = 'trsecurity'
-    __select__ = implements('Transition')
+    __select__ = is_instance('Transition')
 
     def cell_call(self, row, col):
         _ = self._cw._
@@ -291,7 +294,7 @@
 
 class TransitionAllowedTextView(view.EntityView):
     __regid__ = 'trfromstates'
-    __select__ = implements('Transition')
+    __select__ = is_instance('Transition')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
@@ -316,7 +319,7 @@
 
 
 class TransitionEditionForm(autoform.AutomaticEntityForm):
-    __select__ = implements('Transition')
+    __select__ = is_instance('Transition')
 
     def workflow_states_for_relation(self, targetrelation):
         eids = self.edited_entity.linked_to('transition_of', 'subject')
@@ -337,7 +340,7 @@
 
 
 class StateEditionForm(autoform.AutomaticEntityForm):
-    __select__ = implements('State')
+    __select__ = is_instance('State')
 
     def subject_allowed_transition_vocabulary(self, rtype, limit=None):
         if not self.edited_entity.has_eid():
@@ -347,6 +350,27 @@
                                                    'allowed_transition')
         return []
 
+class WorkflowIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('Workflow')
+    # XXX what if workflow of multiple types?
+    def parent_entity(self):
+        return self.entity.workflow_of and self.entity.workflow_of[0] or None
+
+class WorkflowItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('BaseTransition', 'State')
+    def parent_entity(self):
+        return self.entity.workflow
+
+class TransitionItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('SubWorkflowExitPoint')
+    def parent_entity(self):
+        return self.entity.reverse_subworkflow_exit[0]
+
+class TrInfoIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('TrInfo')
+    def parent_entity(self):
+        return self.entity.for_entity
+
 
 # workflow images ##############################################################
 
@@ -400,7 +424,7 @@
 
 class WorkflowGraphView(view.EntityView):
     __regid__ = 'wfgraph'
-    __select__ = EntityView.__select__ & one_line_rset() & implements('Workflow')
+    __select__ = EntityView.__select__ & one_line_rset() & is_instance('Workflow')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
--- a/web/views/xbel.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/xbel.py	Mon Jul 19 15:37:02 2010 +0200
@@ -23,7 +23,7 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.view import EntityView
 from cubicweb.web.views.xmlrss import XMLView
 
@@ -62,7 +62,7 @@
 
 
 class XbelItemBookmarkView(XbelItemView):
-    __select__ = implements('Bookmark')
+    __select__ = is_instance('Bookmark')
 
     def url(self, entity):
         return entity.actual_url()
--- a/web/views/xmlrss.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/views/xmlrss.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""base xml and rss views
+"""base xml and rss views"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -25,8 +24,10 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import non_final_entity, one_line_rset, appobject_selectable
-from cubicweb.view import EntityView, AnyRsetView, Component
+from cubicweb.selectors import (is_instance, non_final_entity, one_line_rset,
+                                appobject_selectable, adaptable)
+from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
+from cubicweb.view import implements_adapter_compat
 from cubicweb.uilib import simple_sgml_tag
 from cubicweb.web import httpcache, box
 
@@ -120,6 +121,16 @@
 
 # RSS stuff ###################################################################
 
+class IFeedAdapter(EntityAdapter):
+    __regid__ = 'IFeed'
+    __select__ = is_instance('Any')
+
+    @implements_adapter_compat('IFeed')
+    def rss_feed_url(self):
+        """return an url to the rss feed for this entity"""
+        return self.entity.absolute_url(vid='rss')
+
+
 class RSSFeedURL(Component):
     __regid__ = 'rss_feed_url'
     __select__ = non_final_entity()
@@ -130,10 +141,11 @@
 
 class RSSEntityFeedURL(Component):
     __regid__ = 'rss_feed_url'
-    __select__ = non_final_entity() & one_line_rset()
+    __select__ = one_line_rset() & adaptable('IFeed')
 
     def feed_url(self):
-        return self.cw_rset.get_entity(0, 0).rss_feed_url()
+        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        return entity.cw_adapt_to('IFeed').rss_feed_url()
 
 
 class RSSIconBox(box.BoxTemplate):
@@ -147,7 +159,7 @@
 
     def call(self, **kwargs):
         try:
-            rss = self._cw.external_resource('RSS_LOGO')
+            rss = self._cw.uiprops['RSS_LOGO']
         except KeyError:
             self.error('missing RSS_LOGO external resource')
             return
--- a/web/webconfig.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/web/webconfig.py	Mon Jul 19 15:37:02 2010 +0200
@@ -15,16 +15,17 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""common web configuration for twisted/modpython instances
+"""web ui configuration for cubicweb instances"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 import os
 from os.path import join, exists, split
+from warnings import warn
 
 from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
 
 from cubicweb.toolsutils import read_config
 from cubicweb.cwconfig import CubicWebConfiguration, register_persistent_options, merge_options
@@ -77,6 +78,7 @@
     """
     cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')])
     cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views'])
+    uiprops = {'FCKEDITOR_PATH': ''}
 
     options = merge_options(CubicWebConfiguration.options + (
         ('anonymous-user',
@@ -205,10 +207,18 @@
           'group': 'web', 'level': 3,
           }),
 
+        ('use-old-css',
+         {'type' : 'yn',
+          'default': True,
+          'help': 'use cubicweb.old.css instead of 3.9 cubicweb.css',
+          'group': 'web', 'level': 2,
+          }),
+
+
         ))
 
     def fckeditor_installed(self):
-        return exists(self.ext_resources['FCKEDITOR_PATH'])
+        return exists(self.uiprops['FCKEDITOR_PATH'])
 
     def eproperty_definitions(self):
         for key, pdef in super(WebConfiguration, self).eproperty_definitions():
@@ -239,30 +249,6 @@
     def vc_config(self):
         return self.repository().get_versions()
 
-    # mapping to external resources (id -> path) (`external_resources` file) ##
-    ext_resources = {
-        'FAVICON':  'DATADIR/favicon.ico',
-        'LOGO':     'DATADIR/logo.png',
-        'RSS_LOGO': 'DATADIR/rss.png',
-        'HELP':     'DATADIR/help.png',
-        'CALENDAR_ICON': 'DATADIR/calendar.gif',
-        'SEARCH_GO':'DATADIR/go.png',
-
-        'FCKEDITOR_PATH':  '/usr/share/fckeditor/',
-
-        'IE_STYLESHEETS':    ['DATADIR/cubicweb.ie.css'],
-        'STYLESHEETS':       ['DATADIR/cubicweb.css'],
-        'STYLESHEETS_PRINT': ['DATADIR/cubicweb.print.css'],
-
-        'JAVASCRIPTS':       ['DATADIR/jquery.js',
-                              'DATADIR/jquery.corner.js',
-                              'DATADIR/jquery.json.js',
-                              'DATADIR/cubicweb.compat.js',
-                              'DATADIR/cubicweb.python.js',
-                              'DATADIR/cubicweb.htmlhelpers.js'],
-        }
-
-
     def anonymous_user(self):
         """return a login and password to use for anonymous users. None
         may be returned for both if anonymous connections are not allowed
@@ -276,26 +262,37 @@
             user = unicode(user)
         return user, passwd
 
-    def has_resource(self, rid):
-        """return true if an external resource is defined"""
-        return bool(self.ext_resources.get(rid))
+    def locate_resource(self, rid):
+        """return the (directory, filename) where the given resource
+        may be found
+        """
+        return self._fs_locate(rid, 'data')
+
+    def locate_doc_file(self, fname):
+        """return the directory where the given resource may be found"""
+        return self._fs_locate(fname, 'wdoc')[0]
 
     @cached
-    def locate_resource(self, rid):
-        """return the directory where the given resource may be found"""
-        return self._fs_locate(rid, 'data')
-
-    @cached
-    def locate_doc_file(self, fname):
-        """return the directory where the given resource may be found"""
-        return self._fs_locate(fname, 'wdoc')
-
-    def _fs_locate(self, rid, rdirectory):
+    def _fs_path_locate(self, rid, rdirectory):
         """return the directory where the given resource may be found"""
         path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
         for directory in path:
             if exists(join(directory, rdirectory, rid)):
-                return join(directory, rdirectory)
+                return directory
+
+    def _fs_locate(self, rid, rdirectory):
+        """return the (directory, filename) where the given resource
+        may be found
+        """
+        directory = self._fs_path_locate(rid, rdirectory)
+        if directory is None:
+            return None, None
+        if rdirectory == 'data' and rid.endswith('.css'):
+            if self['use-old-css'] and rid == 'cubicweb.css':
+                # @import('cubicweb.css') in css
+                rid = 'cubicweb.old.css'
+            return self.uiprops.process_resource(join(directory, rdirectory), rid), rid
+        return join(directory, rdirectory), rid
 
     def locate_all_files(self, rid, rdirectory='wdoc'):
         """return all files corresponding to the given resource"""
@@ -309,8 +306,8 @@
         """load instance's configuration files"""
         super(WebConfiguration, self).load_configuration()
         # load external resources definition
-        self._build_ext_resources()
         self._init_base_url()
+        self._build_ui_properties()
 
     def _init_base_url(self):
         # normalize base url(s)
@@ -320,29 +317,77 @@
         if not self.repairing:
             self.global_set_option('base-url', baseurl)
         httpsurl = self['https-url']
-        if httpsurl and httpsurl[-1] != '/':
-            httpsurl += '/'
-            if not self.repairing:
-                self.global_set_option('https-url', httpsurl)
+        if httpsurl:
+            if httpsurl[-1] != '/':
+                httpsurl += '/'
+                if not self.repairing:
+                    self.global_set_option('https-url', httpsurl)
+            if self.debugmode:
+                self.https_datadir_url = httpsurl + 'data/'
+            else:
+                self.https_datadir_url = httpsurl + 'data%s/' % self.instance_md5_version()
+        if self.debugmode:
+            self.datadir_url = baseurl + 'data/'
+        else:
+            self.datadir_url = baseurl + 'data%s/' % self.instance_md5_version()
 
-    def _build_ext_resources(self):
-        libresourcesfile = join(self.shared_dir(), 'data', 'external_resources')
-        self.ext_resources.update(read_config(libresourcesfile))
+    def _build_ui_properties(self):
+        # self.datadir_url[:-1] to remove trailing /
+        from cubicweb.web.propertysheet import PropertySheet
+        cachedir = join(self.appdatahome, 'uicache')
+        self.check_writeable_uid_directory(cachedir)
+        self.uiprops = PropertySheet(
+            cachedir,
+            data=lambda x: self.datadir_url + x,
+            datadir_url=self.datadir_url[:-1])
+        self._init_uiprops(self.uiprops)
+        if self['https-url']:
+            cachedir = join(self.appdatahome, 'uicachehttps')
+            self.check_writeable_uid_directory(cachedir)
+            self.https_uiprops = PropertySheet(
+                cachedir,
+                data=lambda x: self.https_datadir_url + x,
+                datadir_url=self.https_datadir_url[:-1])
+            self._init_uiprops(self.https_uiprops)
+
+    def _init_uiprops(self, uiprops):
+        libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
+        uiprops.load(libuiprops)
         for path in reversed([self.apphome] + self.cubes_path()):
-            resourcesfile = join(path, 'data', 'external_resources')
-            if exists(resourcesfile):
-                self.debug('loading %s', resourcesfile)
-                self.ext_resources.update(read_config(resourcesfile))
-        resourcesfile = join(self.apphome, 'external_resources')
+            self._load_ui_properties_file(uiprops, path)
+        self._load_ui_properties_file(uiprops, self.apphome)
+        datadir_url = uiprops.context['datadir_url']
+        # XXX pre 3.9 css compat
+        if self['use-old-css']:
+            if (datadir_url+'/cubicweb.css') in uiprops['STYLESHEETS']:
+                idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.css')
+                uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.old.css'
+            if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
+                uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
+        cubicweb_js_url = datadir_url + '/cubicweb.js'
+        if cubicweb_js_url not in uiprops['JAVASCRIPTS']:
+            uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
+
+    def _load_ui_properties_file(self, uiprops, path):
+        resourcesfile = join(path, 'data', 'external_resources')
         if exists(resourcesfile):
-            self.debug('loading %s', resourcesfile)
-            self.ext_resources.update(read_config(resourcesfile))
-        for resource in ('STYLESHEETS', 'STYLESHEETS_PRINT',
-                         'IE_STYLESHEETS', 'JAVASCRIPTS'):
-            val = self.ext_resources[resource]
-            if isinstance(val, str):
-                files = [w.strip() for w in val.split(',') if w.strip()]
-                self.ext_resources[resource] = files
+            warn('[3.9] %s file is deprecated, use an uiprops.py file'
+                 % resourcesfile, DeprecationWarning)
+            datadir_url = uiprops.context['datadir_url']
+            for rid, val in read_config(resourcesfile).iteritems():
+                if rid in ('STYLESHEETS', 'STYLESHEETS_PRINT',
+                           'IE_STYLESHEETS', 'JAVASCRIPTS'):
+                    val = [w.strip().replace('DATADIR', datadir_url)
+                           for w in val.split(',') if w.strip()]
+                    if rid == 'IE_STYLESHEETS':
+                        rid = 'STYLESHEETS_IE'
+                else:
+                    val = val.strip().replace('DATADIR', datadir_url)
+                uiprops[rid] = val
+        uipropsfile = join(path, 'uiprops.py')
+        if exists(uipropsfile):
+            self.debug('loading %s', uipropsfile)
+            uiprops.load(uipropsfile)
 
     # static files handling ###################################################
 
@@ -369,3 +414,8 @@
     def static_file_del(self, rpath):
         if self.static_file_exists(rpath):
             os.remove(join(self.static_directory, rpath))
+
+    @deprecated('[3.9] use _cw.uiprops.get(rid)')
+    def has_resource(self, rid):
+        """return true if an external resource is defined"""
+        return bool(self.uiprops.get(rid))
--- a/wsgi/handler.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/wsgi/handler.py	Mon Jul 19 15:37:02 2010 +0200
@@ -100,9 +100,8 @@
     NOTE: no pyro
     """
 
-    def __init__(self, config, debug=None, vreg=None):
-        self.appli = CubicWebPublisher(config, debug=debug, vreg=vreg)
-        self.debugmode = debug
+    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()
--- a/xy.py	Thu Jul 15 12:03:13 2010 +0200
+++ b/xy.py	Mon Jul 19 15:37:02 2010 +0200
@@ -19,13 +19,13 @@
 
 from yams import xy
 
-xy.register_prefix('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdf')
-xy.register_prefix('http://purl.org/dc/elements/1.1/', 'dc')
-xy.register_prefix('http://xmlns.com/foaf/0.1/',       'foaf')
-xy.register_prefix('http://usefulinc.com/ns/doap#',    'doap')
-xy.register_prefix('http://rdfs.org/sioc/ns#',         'sioc')
-xy.register_prefix('http://www.w3.org/2002/07/owl#',   'owl')
-xy.register_prefix('http://purl.org/dc/terms/',        'dcterms')
+xy.register_prefix('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
+xy.register_prefix('dc', 'http://purl.org/dc/elements/1.1/')
+xy.register_prefix('foaf', 'http://xmlns.com/foaf/0.1/')
+xy.register_prefix('doap', 'http://usefulinc.com/ns/doap#')
+xy.register_prefix('sioc', 'http://rdfs.org/sioc/ns#')
+xy.register_prefix('owl', 'http://www.w3.org/2002/07/owl#')
+xy.register_prefix('dcterms', 'http://purl.org/dc/terms/')
 
 xy.add_equivalence('creation_date', 'dc:date')
 xy.add_equivalence('created_by', 'dc:creator')