backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 17 Jun 2010 15:15:52 +0200
changeset 5774 0d792bceb25d
parent 5768 1e73a466aa69 (diff)
parent 5773 aae03cd19151 (current diff)
child 5775 34195230dc2a
backport stable
cleanappl.sh
hooks/syncschema.py
i18n/fr.po
stdlib.txt
test/data/migration/depends.map
web/data/cubicweb.ajax.js
web/data/cubicweb.calendar.js
web/data/cubicweb.css
web/data/cubicweb.gmap.js
web/data/cubicweb.goa.js
web/data/cubicweb.timeline-bundle.js
web/data/cubicweb.timeline-ext.js
web/data/cubicweb.widgets.js
web/facet.py
web/request.py
web/views/autoform.py
web/views/formrenderers.py
--- a/__pkginfo__.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/__pkginfo__.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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.30.0',
     'docutils': '>= 0.6',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependancies
@@ -52,7 +52,7 @@
     'Twisted': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '',
+    'logilab-database': '1.1.0',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
     }
 
--- a/appobject.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/appobject.py	Thu Jun 17 15:15:52 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/qunit	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+import sys
+
+from cubicweb.devtools.qunit import make_qunit_html
+
+print make_qunit_html(sys.argv[1], sys.argv[2:])
--- a/cwconfig.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/cwconfig.py	Thu Jun 17 15:15:52 2010 +0200
@@ -295,8 +295,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'):
@@ -662,12 +660,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) )}
         # don't register ReStructured Text directives by simple import, avoid pb
         # with eg sphinx.
         # XXX should be done properly with a function from cw.uicfg
@@ -682,26 +682,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,                                          
-                                         })
+                                          '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)
 
@@ -815,12 +812,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):
@@ -888,9 +885,9 @@
 
     # 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._site_loaded = set()
         self.load_file_configuration(self.main_config_file())
@@ -1003,14 +1000,14 @@
         super(CubicWebConfiguration, self).load_configuration()
         if self.apphome and self.set_language:
             # init gettext
-            self._set_language()
+            self._gettext_init()
 
-    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')
@@ -1031,7 +1028,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')
@@ -1110,6 +1107,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:
@@ -1155,6 +1153,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 Jun 17 15:09:12 2010 +0200
+++ b/cwctl.py	Thu Jun 17 15:15:52 2010 +0200
@@ -477,14 +477,13 @@
 
     def start_instance(self, appid):
         """start the instance's server"""
-        debug = self['debug']
         force = self['force']
         loglevel = self['loglevel']
-        config = cwcfg.config_for(appid)
+        config = cwcfg.config_for(appid, debugmode=self['debug'])
         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.init_log(loglevel, force=True)
         if self['profile']:
             config.global_set_option('profile', self.config.profile)
         helper = self.config_helper(config, cmdname='start')
@@ -493,7 +492,7 @@
             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)
 
 
 class StopInstanceCommand(InstanceCommand):
@@ -781,11 +780,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',
@@ -861,8 +864,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 Jun 17 15:09:12 2010 +0200
+++ b/cwvreg.py	Thu Jun 17 15:15:52 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:
@@ -193,6 +192,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
@@ -212,23 +213,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):
@@ -443,14 +444,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
@@ -479,6 +479,7 @@
     def 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
@@ -520,7 +521,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')
 
@@ -539,6 +539,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.
@@ -564,7 +565,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__)
@@ -582,13 +591,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__)
@@ -602,9 +616,22 @@
                     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
--- a/dataimport.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/dataimport.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/dbapi.py	Thu Jun 17 15:15:52 2010 +0200
@@ -20,8 +20,8 @@
 Take a look at http://www.python.org/peps/pep-0249.html
 
 (most parts of this document are reported here in docstrings)
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logging import getLogger
@@ -254,9 +254,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'
 
@@ -575,6 +572,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/control	Thu Jun 17 15:09:12 2010 +0200
+++ b/debian/control	Thu Jun 17 15:15:52 2010 +0200
@@ -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.2), 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.2), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.50.2), python-yams (>= 0.30.0), python-rql (>= 0.26.2), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/devtools/__init__.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/devtools/__init__.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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)
+
+
--- a/devtools/devctl.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/devtools/devctl.py	Thu Jun 17 15:15:52 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):
--- a/devtools/fake.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/devtools/fake.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/devtools/fill.py	Thu Jun 17 15:15:52 2010 +0200
@@ -217,7 +217,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'
@@ -228,12 +228,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	Thu Jun 17 15:15:52 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)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/qunit.py	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,277 @@
+import os, os.path as osp
+import signal
+from tempfile import mkdtemp, NamedTemporaryFile
+import tempfile
+from Queue import Queue, Empty
+from subprocess import Popen, check_call
+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 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')
+        check_call(['firefox', '-no-remote', '-CreateProfile',
+                    '%s %s' % (self._profile_name, self._tmp_dir)],
+                              stdout=fnull, stderr=fnull)
+        if url is not None:
+            self.start(url)
+
+
+    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:
+            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
+        html_test_file = NamedTemporaryFile(suffix='.html')
+        html_test_file.write(make_qunit_html(test_file, depends,
+                             server_data=(self.test_host, self.test_port)))
+        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):
+    """"""
+    data = {
+            'web_data': cw_path('web', 'data'),
+            'web_test': cw_path('web', 'test', 'jstests'),
+        }
+
+    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/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 Jun 17 15:09:12 2010 +0200
+++ b/devtools/repotest.py	Thu Jun 17 15:15:52 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,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/dep_1.js	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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/devtools/test/unittest_httptest.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/devtools/testlib.py	Thu Jun 17 15:15:52 2010 +0200
@@ -309,7 +309,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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/annexes/docstrings-conventions.rst	Thu Jun 17 15:15:52 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/index.rst	Thu Jun 17 15:09:12 2010 +0200
+++ b/doc/book/en/annexes/index.rst	Thu Jun 17 15:15:52 2010 +0200
@@ -17,3 +17,5 @@
    rql/index
    mercurial
    depends
+   javascript-api
+   docstrings-conventions
--- a/doc/book/en/devrepo/vreg.rst	Thu Jun 17 15:09:12 2010 +0200
+++ b/doc/book/en/devrepo/vreg.rst	Thu Jun 17 15:15:52 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
@@ -75,6 +76,7 @@
 .. autoclass:: cubicweb.selectors.partial_has_related_entities
 .. autoclass:: cubicweb.selectors.has_permission
 .. autoclass:: cubicweb.selectors.has_add_permission
+.. autoclass:: cubicweb.selectors.has_mimetype
 
 
 Logged user selectors
--- a/doc/book/en/devweb/js.rst	Thu Jun 17 15:09:12 2010 +0200
+++ b/doc/book/en/devweb/js.rst	Thu Jun 17 15:15:52 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/makefile	Thu Jun 17 15:09:12 2010 +0200
+++ b/doc/book/en/makefile	Thu Jun 17 15:15:52 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/index.rst	Thu Jun 17 15:09:12 2010 +0200
+++ b/doc/book/en/tutorials/index.rst	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,133 @@
+==========================
+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
+
+Some 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>
+
+
+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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/entities/__init__.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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, 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__ = implements('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__ = implements('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) # XXX for bw compat, else should be abstract
+
+    @implements_adapter_compat('IDownloadable')
+    def download_url(self): # 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) # 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) # 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) # 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 Jun 17 15:09:12 2010 +0200
+++ b/entities/authobjs.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/entities/lib.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/entities/schemaobjs.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/entities/test/unittest_base.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/entities/test/unittest_wfobjs.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/entities/wfobjs.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/entity.py	Thu Jun 17 15:15:52 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,34 @@
                 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)
+        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)
 
     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 +408,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 +435,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 +475,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 +487,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 +510,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 +523,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 +542,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 +559,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 +569,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 +604,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,19 +622,19 @@
             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:
@@ -621,7 +651,7 @@
                    all(matching_groups(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 +668,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 +685,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,9 +697,9 @@
         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
                 var = varmaker.next()
                 targettype = rschema.targets(self.e_schema, role)[0]
@@ -706,9 +736,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 +746,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 +770,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 +778,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 +839,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 +901,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 +914,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 +934,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 +947,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 +974,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 +985,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 +1003,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 +1027,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,61 +1085,29 @@
         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)
 
 # attribute and relation descriptors ##########################################
 
@@ -1108,18 +1121,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 +1144,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 Jun 17 15:09:12 2010 +0200
+++ b/etwist/request.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/etwist/server.py	Thu Jun 17 15:15:52 2010 +0200
@@ -99,12 +99,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 +117,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 +124,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 +156,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 +168,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,7 +176,7 @@
             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)
@@ -187,7 +190,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)
@@ -394,20 +400,21 @@
 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
     # 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 Jun 17 15:09:12 2010 +0200
+++ b/etwist/twctl.py	Thu Jun 17 15:15:52 2010 +0200
@@ -32,9 +32,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 Jun 17 15:09:12 2010 +0200
+++ b/goa/appobjects/components.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/goa/db.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/goa/gaesource.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/goa/skel/loader.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/goa/skel/main.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/goa/test/unittest_rql.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/goa/tools/laxctl.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/hooks/bookmark.py	Thu Jun 17 15:15:52 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/security.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/hooks/security.py	Thu Jun 17 15:15:52 2010 +0200
@@ -17,7 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Security hooks: check permissions to add/delete/update entities according to
 the user connected to a session
-
 """
 __docformat__ = "restructuredtext en"
 
@@ -29,9 +28,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 +58,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)
 
@@ -108,10 +107,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
@@ -125,7 +124,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 Jun 17 15:09:12 2010 +0200
+++ b/hooks/syncschema.py	Thu Jun 17 15:15:52 2010 +0200
@@ -34,7 +34,8 @@
 
 from cubicweb import ValidationError
 from cubicweb.selectors import implements
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS, display_name
+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
 
@@ -815,9 +816,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, name)
         DropTable(self._cw, table=SQL_PREFIX + name)
-        MemSchemaCWETypeDel(self._cw, name)
 
 
 class AfterDelCWETypeHook(DelCWETypeHook):
@@ -982,7 +984,11 @@
 
     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())
@@ -1003,7 +1009,6 @@
         # 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'
@@ -1175,7 +1180,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/workflow.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/hooks/workflow.py	Thu Jun 17 15:15:52 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 implements, 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):
@@ -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})
@@ -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/fr.po	Thu Jun 17 15:09:12 2010 +0200
+++ b/i18n/fr.po	Thu Jun 17 15:15:52 2010 +0200
@@ -535,7 +535,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é"
--- a/interfaces.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/interfaces.py	Thu Jun 17 15:15:52 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 ICalendarAdapter
+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 Jun 17 15:09:12 2010 +0200
+++ b/mail.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/migration.py	Thu Jun 17 15:15:52 2010 +0200
@@ -111,7 +111,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 +281,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 +311,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	Thu Jun 17 15:15:52 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 ')
--- a/mixins.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/mixins.py	Thu Jun 17 15:15:52 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)
     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 Jun 17 15:09:12 2010 +0200
+++ b/mttransforms.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/req.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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/rset.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/rset.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/schema.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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/selectors.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/selectors.py	Thu Jun 17 15:15:52 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*
@@ -204,87 +204,10 @@
 
 from cubicweb import Unauthorized, NoSelectableObject, NotAnEntity, 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
@@ -301,6 +224,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
@@ -374,14 +298,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):
@@ -441,7 +368,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
@@ -449,9 +377,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)
@@ -527,19 +457,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.
+    """
+    # implementing an interface takes precedence other special Any interface,
+    # hence return 2 (implements('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 ##############################################################
 
@@ -585,8 +538,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
@@ -594,7 +547,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.
     """
@@ -608,11 +561,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.
     """
@@ -738,7 +691,12 @@
 
     .. note:: when interface is an entity class, the score will reflect class
               proximity so the most specific object will be selected.
+
+    .. note:: with cubicweb >= 3.9, you should use adapters instead of
+              interface, so no interface should be given to this selector. Use
+              :class:`adaptable` instead.
     """
+
     def score_class(self, eclass, req):
         return self.score_interfaces(req, eclass, eclass)
 
@@ -765,6 +723,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).
@@ -1000,7 +978,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
 
@@ -1291,21 +1269,26 @@
 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 ############################################################
 
--- a/server/migractions.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/migractions.py	Thu Jun 17 15:15:52 2010 +0200
@@ -51,7 +51,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
@@ -856,9 +857,23 @@
         `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))
+            # use rql to propagate deletion. XXX we may miss some stuff since
+            # only the bootstrap schema is set.
+            self.rqlexec('DELETE CWEType ET WHERE ET name %(n)s', {'n': oldname})
+        else:
+            self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(oldname)s',
+                         {'newname' : unicode(newname), 'oldname' : oldname},
+                         ask_confirm=False)
         if commit:
             self.commit()
 
@@ -1153,10 +1168,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()
 
--- a/server/msplanner.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/msplanner.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/mssteps.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/querier.py	Thu Jun 17 15:15:52 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Helper classes to execute RQL queries on a set of sources, performing
 security checking and data aggregation.
+"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
@@ -29,7 +29,7 @@
 from logilab.common.compat import any
 from rql import RQLSyntaxError
 from rql.stmts import Union, Select
-from rql.nodes import Relation, VariableRef, Constant, SubQuery
+from rql.nodes import Relation, VariableRef, Constant, SubQuery, Function
 
 from cubicweb import Unauthorized, QueryError, UnknownEid, typed_eid
 from cubicweb import server
@@ -50,7 +50,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 ########################################################
@@ -285,7 +286,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:
@@ -562,6 +582,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 Jun 17 15:09:12 2010 +0200
+++ b/server/repository.py	Thu Jun 17 15:15:52 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)
@@ -152,13 +152,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()
@@ -184,7 +177,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()
@@ -233,8 +228,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)
@@ -391,7 +385,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
 
@@ -572,7 +566,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)
@@ -923,7 +917,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 ?
@@ -1033,37 +1027,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)
@@ -1072,12 +1061,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)
@@ -1098,7 +1088,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)
@@ -1106,7 +1096,7 @@
         entity.edited_attributes = edited_attributes
         try:
             if session.is_hook_category_activated('integrity'):
-                entity.check()
+                entity._cw_check()
             only_inline_rels, need_fti_update = True, False
             relations = []
             source = self.source_from_eid(entity.eid, session)
@@ -1144,7 +1134,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)
@@ -1154,8 +1144,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 Jun 17 15:09:12 2010 +0200
+++ b/server/schemaserial.py	Thu Jun 17 15:15:52 2010 +0200
@@ -27,7 +27,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
+from cubicweb.schema import (CONSTRAINTS, ETYPE_NAME_MAP,
+                             VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES)
 from cubicweb.server import sqlutils
 
 def group_mapping(cursor, interactive=True):
@@ -100,17 +102,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 +131,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/serverctl.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/serverctl.py	Thu Jun 17 15:15:52 2010 +0200
@@ -48,14 +48,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:
@@ -249,9 +251,9 @@
     cmdname = 'start'
     cfgname = 'repository'
 
-    def start_server(self, ctlconf, debug):
+    def start_server(self, ctlconf):
         command = ['cubicweb-ctl start-repository ']
-        if debug:
+        if ctlconf.debugmode:
             command.append('--debug')
         command.append(self.config.appid)
         os.system(' '.join(command))
@@ -272,6 +274,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 +324,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 +347,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:
--- a/server/session.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/session.py	Thu Jun 17 15:15:52 2010 +0200
@@ -230,7 +230,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()
@@ -246,14 +246,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):
@@ -272,7 +273,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 ######################################################
 
@@ -290,16 +292,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:
@@ -614,16 +615,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')
--- a/server/sources/__init__.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/sources/__init__.py	Thu Jun 17 15:15:52 2010 +0200
@@ -343,7 +343,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/native.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/sources/native.py	Thu Jun 17 15:15:52 2010 +0200
@@ -22,8 +22,8 @@
   from which it comes from) are stored in a varchar column encoded as a base64
   string. This is because it should actually be Bytes but we want an index on
   it for fast querying.
+"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
@@ -1068,10 +1068,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
@@ -1138,7 +1138,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)
@@ -1226,7 +1226,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)
@@ -1251,7 +1252,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 Jun 17 15:09:12 2010 +0200
+++ b/server/sources/rql2sql.py	Thu Jun 17 15:15:52 2010 +0200
@@ -568,12 +568,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
@@ -651,12 +653,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
 
@@ -1014,7 +1018,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):
@@ -1057,6 +1062,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
--- a/server/sources/storages.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/sources/storages.py	Thu Jun 17 15:15:52 2010 +0200
@@ -152,7 +152,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 Jun 17 15:09:12 2010 +0200
+++ b/server/sqlutils.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/ssplanner.py	Thu Jun 17 15:15:52 2010 +0200
@@ -22,8 +22,6 @@
 
 __docformat__ = "restructuredtext en"
 
-from copy import copy
-
 from rql.stmts import Union, Select
 from rql.nodes import Constant, Relation
 
@@ -479,7 +477,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 +485,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 +582,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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/data/migratedapp/schema.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/data/schema.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/data/site_cubicweb.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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 implements
+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__ = implements('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__ = implements('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_ldapuser.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_ldapuser.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_migractions.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_msplanner.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_multisources.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_querier.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_repository.py	Thu Jun 17 15:15:52 2010 +0200
@@ -16,10 +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 module cubicweb.server.repository
-
-"""
-from __future__ import with_statement
+"""unit tests for module cubicweb.server.repository"""
 
 from __future__ import with_statement
 
@@ -205,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)
--- a/server/test/unittest_rql2sql.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_rql2sql.py	Thu Jun 17 15:15:52 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()
@@ -425,13 +425,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
@@ -439,12 +436,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
 '''),
 
@@ -1066,11 +1060,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,'
@@ -1096,12 +1088,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()
@@ -1361,13 +1348,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
 
@@ -1417,11 +1444,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 ')
@@ -1519,17 +1542,33 @@
 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
 
 
 
 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')
@@ -1629,5 +1668,6 @@
                           ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'}], {}, set())
                           )
 
+
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_schemaserial.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_schemaserial.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_security.py	Thu Jun 17 15:15:52 2010 +0200
@@ -192,8 +192,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:
@@ -384,7 +383,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')
@@ -393,13 +392,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()
@@ -435,7 +434,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)
@@ -537,14 +536,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():
@@ -552,18 +552,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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_storage.py	Thu Jun 17 15:15:52 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
 
@@ -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 Jun 17 15:09:12 2010 +0200
+++ b/server/test/unittest_undo.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/skeleton/uiprops.py.tmpl	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/sobjects/notification.py	Thu Jun 17 15:15:52 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/unittest_notification.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/sobjects/test/unittest_notification.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/sobjects/test/unittest_supervising.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/sobjects/textparsers.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/test/unittest_cwctl.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/test/unittest_entity.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/test/unittest_rset.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/test/unittest_schema.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/test/unittest_selectors.py	Thu Jun 17 15:15:52 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 implements, adaptable, match_user_groups
 from cubicweb.interfaces import IDownloadable
 from cubicweb.web import action
 
@@ -140,11 +139,12 @@
 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 = implements('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 = implements('File')(f.__class__, req, rset=rset)
         self.failUnless(filescore > idownscore, (filescore, idownscore))
 
     def test_etype_inheritance_no_yams_inheritance(self):
--- a/test/unittest_utils.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/test/unittest_utils.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/test/unittest_vregistry.py	Thu Jun 17 15:15:52 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/utils.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/utils.py	Thu Jun 17 15:15:52 2010 +0200
@@ -335,21 +335,11 @@
     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):
--- a/view.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/view.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/vregistry.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/__init__.py	Thu Jun 17 15:15:52 2010 +0200
@@ -17,9 +17,8 @@
 # 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
 
@@ -77,7 +76,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/action.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/action.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/application.py	Thu Jun 17 15:15:52 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/component.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/component.py	Thu Jun 17 15:15:52 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
 
@@ -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()
 
 
--- a/web/controller.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/controller.py	Thu Jun 17 15:15:52 2010 +0200
@@ -25,6 +25,7 @@
 
 from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
+from cubicweb.mail import format_mail
 from cubicweb.web import LOGGER, Redirect, RequestError
 
 
@@ -81,18 +82,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"""
@@ -106,6 +109,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
--- a/web/data/cubicweb.ajax.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.ajax.js	Thu Jun 17 15:15:52 2010 +0200
@@ -1,14 +1,96 @@
-/*
- *  :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?';
 
+//============= utility function handling remote calls responses. ==============//
 function _loadAjaxHtmlHead(node, head, tag, srcattr) {
     var loaded = [];
     var jqtagfilter = tag + '[' + srcattr + ']';
@@ -17,7 +99,7 @@
     });
     node.find(tag).each(function(i) {
         if (this.getAttribute(srcattr)) {
-            if (!loaded.contains(this.getAttribute(srcattr))) {
+            if (jQuery.inArray(this.getAttribute(srcattr), loaded) == -1) {
                 jQuery(this).appendTo(head);
             }
         } else {
@@ -27,7 +109,9 @@
     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 +143,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,44 +168,77 @@
         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);
@@ -138,19 +250,97 @@
         } else if (mode == 'append') {
             jQuery(node).append(domnode);
         }
-        postAjaxLoad(node);
+        _postAjaxLoad(node);
         while (jQuery.isFunction(callback)) {
             callback = callback.apply(this, [domnode]);
         }
     });
-};
+    if (cursor) {
+        d.addCallback(resetCursor);
+        d.addErrback(resetCursor);
+        d.addErrback(remoteCallFailed);
+    }
+    return d;
+}
 
+/**
+ * .. function:: loadRemote(url, form, reqtype='GET', async=true)
+ *
+ * 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,35 +352,49 @@
     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);
+        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.id).loadxhtml('json', ajaxFuncArgs('view', extraparams));
     }
 }
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/web/data/cubicweb.ajax.js
+jQuery(document).ready(function() {
+    _loadDynamicFragments();
+});
+=======
 
 jQuery(document).ready(function() {loadDynamicFragments();});
 
@@ -259,150 +463,97 @@
 function userCallback(cbname) {
     asyncRemoteExec('user_callback', cbname);
 }
+>>>>>>> /tmp/cubicweb.ajax.js~other.YR3yr8
 
 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 openHash() {
-    if (document.location.hash) {
-        var nid = document.location.hash.replace('#', '');
-        var node = jQuery('#' + nid);
-        if (node) { removeElementClass(node, "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);
-        }
+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"));
     });
-    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 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 = asyncRemoteExec('user_callback', cbname);
+    var d = userCallback(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();
+        $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {
+            'rql': rql
+        },
+        registry, compid));
+        if (msg) {
+            updateMessage(msg);
+        }
     });
 }
 
 function userCallbackThenReloadPage(cbname, msg) {
-    var d = asyncRemoteExec('user_callback', cbname);
+    var d = userCallback(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();
+        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) {
-    var d = asyncRemoteExec('unregister_user_callback', cbname);
-    d.addCallback(function() {resetCursor();});
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
+    setProgressCursor();
+    var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+                                            null, cbname));
+    d.addCallback(resetCursor);
+    d.addErrback(resetCursor);
+    d.addErrback(remoteCallFailed);
 }
 
-
-/* executes an async query to the server and replaces a node's
- * content with the query result
- *
- * @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};
-    }
+//============= XXX move those functions? ====================================//
+function openHash() {
+    if (document.location.hash) {
+        var nid = document.location.hash.replace('#', '');
+        var node = jQuery('#' + nid);
+        if (node) {
+            $(node).removeClass("hidden");
+        }
+    };
+}
+jQuery(document).ready(openHash);
 
-    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
+/**
+ * .. function:: buildWysiwygEditors(parent)
+ *
+ *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 +562,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 +592,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 +616,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 = 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 Jun 17 15:09:12 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.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.calendar.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.compat.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.css	Thu Jun 17 15:15:52 2010 +0200
@@ -3,82 +3,61 @@
  *  :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;
+/* scale and rhythm cf http://lamb.cc/typograph/ */
+body {
+  font-family:  %(defaultFont)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(/data/%(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 {
+    border-bottom: %(h1BorderBottomStyle)s;
+    padding: %(h1Padding)s;
+    margin: %(h1Margin)s;
+}
+h2 { padding: %(h2Padding)s; }
+h3 { padding: %(h3Padding)s; }
 
 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;
+  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,84 +65,58 @@
   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 {
+  clear: both;
+  font-family: 'Courier New', monospace;
+  letter-spacing: 0.015em;
+  padding: 0.5em;
+  margin: 0 1.5em 1.5em;
+  background-color: #f0f0f0;
+  border: 1px solid #ccbca7;
 }
 
-pre {
-  font-family: Courier, "Courier New", Monaco, monospace;
-  font-size: 100%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
+p {
+  text-align: justify;
+  margin-bottom: %(defaultLineHeightEm)s;
+}
+
+ol, ul {
+  list-style-type: disc;
+  margin-bottom: %(defaultLineHeightEm)s;
 }
 
-code {
-  font-size: 120%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
+ol ol,
+ul ul{
+  margin-left: 8px;
+  margin-bottom : 0px;
+}
+
+p + ul {
+  margin-top: -%(defaultLineHeightEm)s;
+}
+
+li {
+  margin-left: 1.5em;
 }
 
-blockquote {
-  font-family: Courier, "Courier New", serif;
-  font-size: 120%;
-  margin: 5px 0px;
-  padding: 0.8em;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
+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 {
+  border: 1px solid %(pageContentBorderColor)s;
+  padding: 0.1em;
+  vertical-align: middle;
+}
+
+input:focus {
+  border: 1px inset %(headerBgColor)s;
 }
 
 /***************************************/
@@ -179,8 +132,8 @@
 }
 
 .hr {
-  border-bottom: 1px dotted #ccc;
-  margin: 1em 0px;
+  border-bottom: 1px dotted %(pageContentBorderColor)s;
+  height: 17px;
 }
 
 .left {
@@ -200,14 +153,16 @@
   visibility: hidden;
 }
 
-li.invisible { list-style: none; background: none; padding: 0px 0px
-1px 1px; }
+li.invisible {
+  list-style: none;
+  background: none;
+  padding: 0px 0px 1px 1px;
+}
 
 li.invisible div{
   display: inline;
 }
 
-
 /***************************************/
 /*   LAYOUT                            */
 /***************************************/
@@ -215,7 +170,7 @@
 /* header */
 
 table#header {
-  background: #ff7700 url("banner.png") left top repeat-x;
+  background: %(headerBgColor)s url("banner.png") left top repeat-x;
   text-align: left;
 }
 
@@ -224,86 +179,94 @@
 }
 
 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%;
 }
 
+/* Popup on login box and userActionBox */
+div.popupWrapper{
+  position:relative;
+  z-index:100;
+}
+
+div.popup {
+  position: absolute;
+  background: #fff;
+  border: 1px solid #fff;
+  text-align: left;
+  z-index: 400;
+}
+
+div.popup ul li a {
+  text-decoration: none;
+  color: #000;
+}
+
 /* 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;
+  min-height: %(pageMinHeight)s;
+  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: 0px %(pageContentPadding)s %(pageContentPadding)s;
 }
 
-table#mainLayout td.navcol {
-  width: 16em;
+
+div#breadcrumbs {
+  padding: %(pageContentPadding)s 0 0 0;
 }
 
+/*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;
+  background: %(actionBoxTitleBgColor)s;
+  border: 1px solid %(actionBoxTitleBgColor)s;
 }
 
 input#rql{
@@ -311,26 +274,17 @@
 }
 
 /* boxes */
-div.navboxes {
- margin-top: 8px;
-}
 
 div.boxFrame {
   width: 100%;
 }
 
 div.boxTitle {
-  padding-top: 0px;
-  padding-bottom: 0.2em;
+  overflow: hidden;
   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;
+  padding: 0px 0px 0.2em;
+  background: %(headerBgColor)s url("search.png") left bottom repeat-x;
 }
 
 div.boxTitle span,
@@ -339,14 +293,19 @@
   white-space: nowrap;
 }
 
+div.searchBoxFrame div.boxTitle,
+div.greyBoxFrame div.boxTitle {
+  background: %(actionBoxTitleBgColor)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,6 +314,21 @@
   border-top: none;
 }
 
+a.boxMenu {
+  display: block;
+  padding: 1px 9px 1px 3px;
+  background: transparent url("puce_down.png") 98% 6px no-repeat;
+}
+a.boxMenu:hover {
+  background: %(sideBoxBodyBgColor)s url("puce_down.png") 98% 6px no-repeat;
+  cursor: pointer;
+}
+
+a.popupMenu {
+  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+  padding-left: 2em;
+}
+
 ul.boxListing {
   margin: 0px;
   padding: 0px 3px;
@@ -362,65 +336,47 @@
 
 ul.boxListing li,
 ul.boxListing ul li {
-  display: inline;
+  list-style-type: none;
   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;
+  color: %(defaultColor)s;
   padding: 1px 9px 1px 3px;
 }
 
 ul.boxListing .selected {
-  color: #FF4500;
+  color: %(aColor)s;
   font-weight: bold;
 }
 
 ul.boxListing a.boxBookmark:hover,
 ul.boxListing a:hover,
 ul.boxListing ul li a:hover {
+  color: #111100;
   text-decoration: none;
-  background: #eeedd9;
-  color: #111100;
+  background: %(sideBoxBodyBgColor)s;
 }
 
 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;
+  cursor: pointer;
+  border-top: medium none;
+  background: %(sideBoxBodyBgColor)s url(puce_down.png) no-repeat scroll 98% 6px;
 }
 
 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;
+  background: %(sideBoxBodyBgColor)s  url("bullet_orange.png") 0% 6px no-repeat;
 }
 
 ul.boxListing a.boxBookmark {
   padding-left: 3px;
-  background-image:none;
+  background-image: none;
   background:#fff;
 }
 
@@ -440,7 +396,7 @@
 }
 
 div.sideBoxTitle {
-  background: #cfceb7;
+  background: %(actionBoxTitleBgColor)s;
   display: block;
   font: bold 100% Georgia;
 }
@@ -450,15 +406,20 @@
   margin-bottom: 0.5em;
 }
 
+ul.sideBox,
+ul.sideBox ul{
+  margin-bottom: 0px;
+}
+
 ul.sideBox li{
- list-style: none;
- background: none;
+ list-style-type : none;
  padding: 0px 0px 1px 1px;
- }
+ margin: 1px 0 1px 4px;
+}
 
 div.sideBoxBody {
   padding: 0.2em 5px;
-  background: #eeedd9;
+  background: %(sideBoxBodyBgColor)s;
 }
 
 div.sideBoxBody a {
@@ -474,10 +435,10 @@
 }
 
 input.rqlsubmit{
-  background: #fffff8 url("go.png") 50% 50% no-repeat;
+  margin: 0px;
   width: 20px;
   height: 20px;
-  margin: 0px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
 }
 
 input#norql{
@@ -497,7 +458,7 @@
 }
 
 div#userActionsBox a.popupMenu {
-  color: black;
+  color: #000;
   text-decoration: underline;
   padding-right: 2em;
 }
@@ -521,7 +482,7 @@
 /**************/
 div#etyperestriction {
   margin-bottom: 1ex;
-  border-bottom: 1px solid #ccc;
+  border-bottom: 1px solid %(pageContentBorderColor)s;
 }
 
 span.slice a:visited,
@@ -531,7 +492,7 @@
 
 span.selectedSlice a:visited,
 span.selectedSlice a {
-  color: #000;
+  color: %(defaultColor)s;
 }
 
 /* FIXME should be moved to cubes/folder */
@@ -546,19 +507,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;
@@ -577,7 +532,7 @@
 }
 
 div.section {
-  margin-top: 0.5em;
+/*  margin-top: 0.5em; */
   width:100%;
 }
 
@@ -611,50 +566,44 @@
 
 .warning,
 .message,
-.errorMessage ,
-.searchMessage{
+.errorMessage{
   padding: 0.3em 0.3em 0.3em 1em;
   font-weight: bold;
 }
 
-.simpleMessage {
-  margin: 4px 0px;
-  font-weight: bold;
-  color: #ff7700;
+.searchMessage{
+ padding-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: %(headerBgColor)s;
+}
+
+div#appMsg {
+  border: 1px solid %(actionBoxTitleBgColor)s;
+  margin-bottom: %(defaultLayoutMargin)s;
 }
 
 .message {
   margin: 0px;
-  background: #f8f8ee url("information.png") 5px center no-repeat;
+  background: #fff 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;
+  background: #fff 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*/
+  border: 1px solid %(actionBoxTitleBgColor)s;
 }
 
 .stateMessage {
-  border: 1px solid #ccc;
-  background: #f8f8ee url("information.png") 10px 50% no-repeat;
+  border: 1px solid %(pageContentBorderColor)s;
+  background: #fff 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 ..." */
@@ -668,8 +617,8 @@
   position: fixed;
   right: 5px;
   top: 0px;
-  background: #222211;
-  color: white;
+  background: %(defaultColor)s;
+  color: #fff;
   font-weight: bold;
   display: none;
 }
@@ -679,64 +628,59 @@
 /***************************************/
 
 table.listing {
+ width: 100%;
  padding: 10px 0em;
- color: #000;
- width: 100%;
- border-right: 1px solid #dfdfdf;
+ color: %(defaultColor)s;
+ border: 1px solid %(listingBorderColor)s;
+}
+
+table.listing tr th {
+  font-weight: bold;
+  background: #dfdfdf;
+  font-size: 8pt;
+  padding: 3px 0px 3px 5px;
+  border: 1px solid %(listingBorderColor)s;
+  border-right:none}
+
+table.listing thead tr {
+/*  border: 1px solid #dfdfdf; */
 }
 
 
 table.listing thead th.over {
-  background-color: #746B6B;
+  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;
+  border-right: 1px solid %(listingBorderColor)s;
   cursor: pointer;
 }
 
 table.listing td {
-  color: #3D3D3D;
   padding: 4px;
-  background-color: #FFF;
+  padding: 3px 0px 3px 5px;
   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;
@@ -790,7 +734,7 @@
  }
 
 #add_newopt{
- background: #fffff8 url("go.png") 50% 50% no-repeat;
+ background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
  width: 20px;
  line-height: 20px;
  display:block;
@@ -803,9 +747,9 @@
 
 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;
+  background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
 }
 
 /* FileItemInnerView  jquery.treeview.css */
@@ -818,11 +762,11 @@
 /* footer                              */
 /***************************************/
 
-div.footer {
+div#footer {
   text-align: center;
 }
-div.footer a {
-  color: #000;
+div#footer a {
+  color: %(defaultColor)s;
   text-decoration: none;
 }
 
@@ -841,15 +785,15 @@
 /***************************************/
 .title {
   text-align: left;
-  font-size:  large;
+  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 +803,14 @@
 .otherView {
   float: right;
 }
+
+/********************************/
+/* overwite other css here */
+/********************************/
+
+/* ui.tabs.css */
+ul .ui-tabs-nav,
+ul .ui-tabs-panel {
+  font-family: %(defaultFont)s;
+  font-size: %(defaultSize)s;
+}
--- a/web/data/cubicweb.edition.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.edition.js	Thu Jun 17 15:15:52 2010 +0200
@@ -1,141 +1,170 @@
-/*
+/**
+ * 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 (inputTypes.contains(tagName)) {
+            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');
@@ -146,108 +175,138 @@
     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(':'));
 }
 
-// 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);
         var form = jQuery(dom);
@@ -259,76 +318,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);
     });
-    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 +400,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,7 +467,7 @@
     var descr = result[1];
     var errmsg;
     // Unknown structure
-    if ( !isArrayLike(descr) || descr.length != 2 ) {
+    if ( !cw.utils.isArrayLike(descr) || descr.length != 2 ) {
 	errmsg = descr;
     } else {
 	_displayValidationerrors(formid, descr[0], descr[1]);
@@ -409,68 +479,102 @@
     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.append(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();
+        /* cleanup */
     }
 }
 
-
-/* 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,69 +582,110 @@
  */
 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;
 }
 
 
-/*
+/**
+ * .. 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) {
+function inlineValidateRelationFormOptions(rtype, eid, divid, options) {
     try {
-	var form = getNode(divid+'-form');
+        var form = cw.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;
+        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) {
-	if (handleFormValidationResponse(divid+'-form', noop, noop, result)) {
-          if (reload) {
+    d.addCallback(function(result, req) {
+        execFormValidationResponse(rtype, eid, divid, options, result);
+    });
+    return false;
+}
+
+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: 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');
-          }
-	}
-        return false;
-    });
-  return false;
+        } 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
+ */
+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();
+    jQuery('#' + divid + '-value').hide();
+    jQuery('#' + divid + '-form').show();
 }
 
 function hideInlineEdit(eid, rtype, divid) {
@@ -548,7 +693,55 @@
     jQuery('div.errorMessage').remove();
     jQuery('#' + divid).show();
     jQuery('#' + divid + '-value').show();
-    jQuery('#' + divid +'-form').hide();
+    jQuery('#' + divid + '-form').hide();
 }
 
-CubicWeb.provide('edition.js');
+
+// ======================= DEPRECATED FUNCTIONS ========================= //
+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;
+    }
+);
+
+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.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.facets.js	Thu Jun 17 15:15:52 2010 +0200
@@ -1,221 +1,232 @@
-/*
+/**
  *  :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');
-
 //============= 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) {
     var names = [];
     var values = [];
-    jQuery(form).find('.facet').each(function () {
+    jQuery(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 () {
+    jQuery(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);
+    jQuery(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 form = getNode(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;
+        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');
-			}
-		    }
-		});
-	    }
-	});
+        // 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;
+        extraparams['vid'] = vid;
+        d = $('#' + divid).loadxhtml('json', ajaxFuncArgs('view', extraparams));
+        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";
+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;
+                    }
+                });
+            });
+        }
     });
 }
 
@@ -223,6 +234,6 @@
 // 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');
+jQuery(document).ready(function() {
+    initFacetBoxEvents();
+});
--- a/web/data/cubicweb.flot.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.flot.js	Thu Jun 17 15:15:52 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.gmap.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.gmap.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.goa.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Thu Jun 17 15:15:52 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.iprogress.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.iprogress.js	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,407 @@
+
+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;
+    }
+
+
+});
+
+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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.lazy.js	Thu Jun 17 15:15:52 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.manageview.css	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.manageview.css	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,862 @@
+/*
+ *  :organization: Logilab
+ *  :copyright: 2003-2009 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.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.preferences.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.print.css	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.python.js	Thu Jun 17 15:15:52 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.reset.css	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,62 @@
+/* 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;
+}
+
+/* Logilab */
+img{
+ border: none;
+}
+
+fieldset {
+  border: none;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.rhythm.js	Thu Jun 17 15:15:52 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.tabs.js	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.tabs.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.timeline-bundle.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.timeline-ext.js	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/cubicweb.widgets.js	Thu Jun 17 15:15:52 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 (!self.variables.contains(group[1])) {
+                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 Jun 17 15:09:12 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 Jun 17 15:09:12 2010 +0200
+++ b/web/data/jquery.tablesorter.js	Thu Jun 17 15:15:52 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/uiprops.py	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,112 @@
+"""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.old.css')]
+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'
+defaultFont = 'Verdana,sans-serif'
+defaultSize = '12px'
+defaultLineHeight = '1.5'
+defaultLineHeightEm = defaultLineHeight + 'em'
+baseRhythmBg = 'rhythm18.png'
+
+# XXX
+defaultLayoutMargin = '8px'
+
+# header
+headerBgColor = '#ff7700'
+
+# h
+h1FontSize = '1.5em'
+h1BorderBottomStyle = '0.06em solid black'
+h1Padding = '0 0 0.14em 0 '
+h1Margin = '0.8em 0 0.5em'
+
+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 = '#ff4500'
+aActiveColor = aVisitedColor = aLinkColor = aColor
+
+# page frame
+pageContentBorderColor = '#ccc'
+pageContentBgColor = '#fff'
+pageContentPadding = '1em'
+pageMinHeight = '800px'
+
+# button
+buttonBorderColor = '#edecd2'
+buttonBgColor = '#fffff8'
+
+# action, search, sideBoxes
+actionBoxTitleBgColor = '#cfceb7'
+sideBoxBodyBgColor = '#eeedd9'
+
+
+# table listing
+listingBorderColor = '#878787'
--- a/web/facet.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/facet.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/formfields.py	Thu Jun 17 15:15:52 2010 +0200
@@ -325,7 +325,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
@@ -401,7 +401,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 +410,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 +420,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 +451,7 @@
         except ProcessFormError:
             return True
         except UnmodifiedField:
-            return False
+            return False # not modified
         if previous_value == new_value:
             return False # not modified
         return True
@@ -1080,18 +1086,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 +1114,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 +1139,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 Jun 17 15:09:12 2010 +0200
+++ b/web/formwidgets.py	Thu Jun 17 15:15:52 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) )
 
 
@@ -574,7 +573,7 @@
         req.add_onload(u'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:
@@ -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 Jun 17 15:09:12 2010 +0200
+++ b/web/htmlwidgets.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,100 @@
+# 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 PropertySheet(dict):
+    def __init__(self, cache_directory, **context):
+        self._cache_directory = cache_directory
+        self.context = context
+        self.reset()
+        context['sheet'] = self
+        self._percent_rgx = re.compile('%(?!\()')
+
+    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 Jun 17 15:09:12 2010 +0200
+++ b/web/request.py	Thu Jun 17 15:15:52 2010 +0200
@@ -83,6 +83,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 +105,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()
@@ -564,24 +569,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, dumps(url), replacemode)
 
     # urls/path management ####################################################
 
@@ -589,10 +600,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 +625,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 +700,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 +805,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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,3 @@
+bgcolor = '#000000'
+stylesheets = ['%s/cubicweb.css' % datadir_url]
+logo = '%s/logo.png' % datadir_url
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet2.py	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,3 @@
+fontcolor = 'black'
+bgcolor = '#FFFFFF'
+stylesheets = sheet['stylesheets'] + ['%s/mycube.css' % datadir_url]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url0.html	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,1 @@
+['foo', 'bar']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/qunit.css	Thu Jun 17 15:15:52 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/web/test/jstests/qunit.js	Thu Jun 17 15:15:52 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);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_ajax.html	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,23 @@
+<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="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <!-- test suite -->
+    <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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,224 @@
+$(document).ready(function() {
+
+    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();
+                // 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'));
+                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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,20 @@
+<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="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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,21 @@
+<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="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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/test/test_views.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_breadcrumbs.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_form.py	Thu Jun 17 15:15:52 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
 
--- a/web/test/unittest_formfields.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_formfields.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_magicsearch.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,49 @@
+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'])
+        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 Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Thu Jun 17 15:15:52 2010 +0200
@@ -128,7 +128,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):
@@ -643,7 +643,7 @@
     # 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']))
--- a/web/test/unittest_viewselector.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_viewselector.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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'))
--- a/web/test/unittest_web.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_web.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/test/unittest_webconfig.py	Thu Jun 17 15:15:52 2010 +0200
@@ -33,15 +33,14 @@
     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_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).split(os.sep))
         cubicwebcsspath = self.config.locate_resource('cubicweb.css').split(os.sep)
         self.failUnless('web' in cubicwebcsspath or 'shared' in cubicwebcsspath) # 'shared' if tests under apycot
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/windmill/test_connexion.py	Thu Jun 17 15:15:52 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	Thu Jun 17 15:15:52 2010 +0200
@@ -0,0 +1,55 @@
+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.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 Jun 17 15:09:12 2010 +0200
+++ b/web/uicfg.py	Thu Jun 17 15:15:52 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 (
--- a/web/views/actions.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/actions.py	Thu Jun 17 15:15:52 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, implements, debug_mode,
     )
 from cubicweb.web import uicfg, controller, action
 from cubicweb.web.views import linksearch_select_url, vid_from_rset
@@ -418,6 +419,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/autoform.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/autoform.py	Thu Jun 17 15:15:52 2010 +0200
@@ -767,7 +767,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(
@@ -870,7 +870,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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/basecomponents.py	Thu Jun 17 15:15:52 2010 +0200
@@ -78,8 +78,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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/basecontrollers.py	Thu Jun 17 15:15:52 2010 +0200
@@ -22,9 +22,6 @@
 
 __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,
@@ -32,7 +29,8 @@
 from cubicweb.utils import CubicWebJsonEncoder
 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,
+                          json, json_dumps)
 from cubicweb.web.controller import Controller
 from cubicweb.web.views import vid_from_rset, formrenderers
 
@@ -130,7 +128,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:
@@ -250,7 +248,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)
@@ -264,6 +262,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'
@@ -343,12 +347,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">')
@@ -359,12 +362,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()
 
@@ -375,6 +378,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)
@@ -384,7 +389,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):
@@ -422,16 +427,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
@@ -452,23 +460,15 @@
     @xhtmlize
     def js_reledit_form(self):
         req = self._cw
-        args = dict((x, self._cw.form[x])
+        args = dict((x, req.form[x])
                     for x in frozenset(('rtype', 'role', 'reload', 'landing_zone')))
-        entity = self._cw.entity_from_eid(int(self._cw.form['eid']))
+        entity = req.entity_from_eid(typed_eid(req.form['eid']))
         # note: default is reserved in js land
-        args['default'] = self._cw.form['default_value']
+        args['default'] = req.form['default_value']
         args['reload'] = json.loads(args['reload'])
-        rset = req.eid_rset(int(self._cw.form['eid']))
+        rset = req.eid_rset(typed_eid(req.form['eid']))
         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):
@@ -484,7 +484,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
@@ -584,52 +584,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')
 
@@ -640,7 +598,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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/basetemplates.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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'))
@@ -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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/bookmark.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/calendar.py	Thu Jun 17 15:15:52 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) # 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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/cwproperties.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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')
 
@@ -396,6 +396,15 @@
         w(u'</div>')
 
 
+class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter):
+    __select__ = implements('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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/cwuser.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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')
--- a/web/views/editcontroller.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/editcontroller.py	Thu Jun 17 15:15:52 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 implements
+from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
+                          ProcessFormError)
 from cubicweb.web.views import basecontrollers, autoform
 
+
+class IEditControlAdapter(EntityAdapter):
+    __regid__ = 'IEditControl'
+    __select__ = implements('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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/editforms.py	Thu Jun 17 15:15:52 2010 +0200
@@ -207,7 +207,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"""
@@ -283,8 +283,8 @@
     # 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');")
+    _onsubmit = ("return inlineValidateRelationFormOptions('%(rtype)s', '%(eid)s', "
+                 "'%(divid)s', %(options)s);")
     _cancelclick = "hideInlineEdit(%s,\'%s\',\'%s\')"
     _defaultlandingzone = (u'<img title="%(msg)s" src="data/pen_icon.png" '
                            'alt="%(msg)s"/>')
@@ -340,7 +340,7 @@
                            self._build_renderer(entity, rtype, role))
 
     def should_edit_attribute(self, entity, rschema, form):
-        if not entity.has_perm('update'):
+        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):
@@ -370,8 +370,8 @@
         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\')">'
+          u'onmouseout="jQuery(\'#%s\').addClass(\'hidden\')" '
+          u'onmouseover="jQuery(\'#%s\').removeClass(\'hidden\')">'
           % (divid, divid, divid))
         w(u'<div id="%s-value" class="editableFieldValue">%s</div>' % (divid, value))
         w(form.render(renderer=renderer))
@@ -403,9 +403,11 @@
     def _build_args(self, entity, rtype, role, formid, default, reload, lzone,
                     extradata=None):
         divid = '%s-%s-%s' % (rtype, role, entity.eid)
+        options = {'reload' : reload, 'default_value' : default,
+                   'role' : role, 'vid' : '',
+                   'lzone' : lzone}
         event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype,
-                      'reload' : dumps(reload), 'default' : default, 'role' : role, 'vid' : u'',
-                      'lzone' : lzone}
+                      'options' : dumps(options)}
         if extradata:
             event_args.update(extradata)
         return divid, event_args
@@ -413,7 +415,7 @@
     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)
+                                             reload, lzone, extradata)
         onsubmit = self._onsubmit % event_args
         cancelclick = self._cancelclick % (entity.eid, rtype, divid)
         form = self._cw.vreg['forms'].select(
@@ -431,8 +433,8 @@
     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');")
+    _onclick = (u"loadInlineEditionFormOptions(%(eid)s, '%(rtype)s', "
+                "'%(divid)s', %(options)s);")
 
     def should_edit_attribute(self, entity, rschema, form):
         rdef = entity.e_schema.rdef(rschema)
--- a/web/views/editviews.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/editviews.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/emailaddress.py	Thu Jun 17 15:15:52 2010 +0200
@@ -26,7 +26,7 @@
 from cubicweb.selectors import implements
 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')
@@ -138,3 +138,10 @@
 
     def cell_call(self, row, col, **kwargs):
         self.w(self.cw_rset.get_entity(row, col).display_address())
+
+
+class EmailAddressIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('EmailAddress')
+
+    def parent_entity(self):
+        return self.entity.email_of
--- a/web/views/embedding.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/embedding.py	Thu Jun 17 15:15:52 2010 +0200
@@ -16,10 +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/>.
 """Objects interacting together to provides the external page embeding
-functionality.
+functionality."""
 
-
-"""
 __docformat__ = "restructuredtext en"
 
 import re
@@ -29,16 +27,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) # 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
     """
@@ -85,14 +94,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']
@@ -107,14 +116,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/formrenderers.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/formrenderers.py	Thu Jun 17 15:15:52 2010 +0200
@@ -342,7 +342,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,7 +358,7 @@
             entity = form.edited_entity
             values = form.form_previous_values
             qeid = eid_param('eid', entity.eid)
-            cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % \
+            cbsetstate = "setCheckboxesState('eid', %s, 'checked')" % \
                          xml_escape(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
--- a/web/views/ibreadcrumbs.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/ibreadcrumbs.py	Thu Jun 17 15:15:52 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 (implements, 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__ = implements('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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/idownloadable.py	Thu Jun 17 15:15:52 2010 +0200
@@ -15,29 +15,21 @@
 #
 # 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.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, implements, match_context_prop,
+                                adaptable, has_mimetype)
 from cubicweb.mttransforms import ENGINE
 from cubicweb.web.box import EntityBoxTemplate
 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 +39,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 +50,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 +64,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'
@@ -81,73 +73,83 @@
 
     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())
 
 
 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)
         else:
             self.wview('downloadlink', entity.cw_rset, title=self._cw._('download'), row=entity.cw_row)
-            try:
-                if ENGINE.has_input(contenttype):
-                    self.w(entity.printable_value('data'))
-            except TransformError:
-                pass
-            except Exception, ex:
-                msg = self._cw._("can't display data, unexpected error: %s") \
-                      % xml_escape(str(ex))
-                self.w('<div class="error">%s</div>' % msg)
+            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:
+                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(unicode(ex))
+                self.w('<div class="error">%s</div>' % msg)
+            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')
 
@@ -160,10 +162,11 @@
 
     def cell_call(self, row, col, width=None, height=None, link=False):
         entity = self.cw_rset.get_entity(row, col)
+        adapter = entity.cw_adapt_to('IDownloadable')
         #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())))
+            xml_escape(adapter.download_url()),
+            (self._cw._('download %s')  % xml_escape(adapter.download_file_name())))
         if width:
             imgtag += u'width="%i" ' % width
         if height:
--- a/web/views/igeocodable.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/igeocodable.py	Thu Jun 17 15:15:52 2010 +0200
@@ -21,27 +21,59 @@
 __docformat__ = "restructuredtext en"
 
 from cubicweb.interfaces import IGeocodable
-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.web import json
 
+class IGeocodableAdapter(EntityAdapter):
+    """interface required by geocoding views such as gmap-view"""
+    __regid__ = 'IGeocodable'
+    __select__ = implements(IGeocodable) # 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),
@@ -53,24 +85,19 @@
             }
         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 +107,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 +123,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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/iprogress.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/isioc.py	Thu Jun 17 15:15:52 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) # 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) # 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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/magicsearch.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/management.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/massmailing.py	Thu Jun 17 15:15:52 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 (implements, 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(), 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'))
@@ -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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/navigation.py	Thu Jun 17 15:15:52 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
 
 
@@ -182,20 +181,41 @@
         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) # 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()
+        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">')
--- a/web/views/old_calendar.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/old_calendar.py	Thu Jun 17 15:15:52 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,25 @@
                                  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) # 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 +54,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 +141,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 +303,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 +493,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/schema.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/schema.py	Thu Jun 17 15:15:52 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 (implements, 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
@@ -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'
@@ -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)
 
@@ -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(
@@ -491,9 +490,7 @@
         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})
@@ -644,41 +641,145 @@
                            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__ = implements('CWRelation')
+    def parent_entity(self):
+        return self.entity.rtype
+
+class CWAttributeIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('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__ = implements('CWConstraint')
+    def parent_entity(self):
+        if self.entity.reverse_constrained_by:
+            return self.entity.reverse_constrained_by[0]
+
+class RQLExpressionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('RQLExpression')
+    def parent_entity(self):
+        return self.entity.expression_of
+
+class CWPermissionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('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 ########################################################
--- a/web/views/startup.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/startup.py	Thu Jun 17 15:15:52 2010 +0200
@@ -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>')
         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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/tableview.py	Thu Jun 17 15:15:52 2010 +0200
@@ -213,7 +213,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 +386,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 +407,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/timeline.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/timeline.py	Thu Jun 17 15:15:52 2010 +0200
@@ -18,14 +18,13 @@
 """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
 
@@ -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):
@@ -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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/timetable.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/treeview.py	Thu Jun 17 15:15:52 2010 +0200
@@ -15,21 +15,94 @@
 #
 # 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.selectors import adaptable
 from cubicweb.view import EntityView
+from cubicweb.mixins import _done_init
 from cubicweb.web import json
+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_entity()
+        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)
+
+
+# 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 +184,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 +193,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 +204,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 +223,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 +271,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 Jun 17 15:09:12 2010 +0200
+++ b/web/views/urlrewrite.py	Thu Jun 17 15:15:52 2010 +0200
@@ -207,7 +207,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/workflow.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/workflow.py	Thu Jun 17 15:15:52 2010 +0200
@@ -33,14 +33,14 @@
 from cubicweb import Unauthorized, view
 from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
                                 relation_possible, match_form_params,
-                                implements, score_entity)
+                                implements, score_entity, 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)
 
@@ -347,6 +350,27 @@
                                                    'allowed_transition')
         return []
 
+class WorkflowIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('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__ = implements('BaseTransition', 'State')
+    def parent_entity(self):
+        return self.entity.workflow
+
+class TransitionItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('SubWorkflowExitPoint')
+    def parent_entity(self):
+        return self.entity.reverse_subworkflow_exit[0]
+
+class TrInfoIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('TrInfo')
+    def parent_entity(self):
+        return self.entity.for_entity
+
 
 # workflow images ##############################################################
 
--- a/web/views/xmlrss.py	Thu Jun 17 15:09:12 2010 +0200
+++ b/web/views/xmlrss.py	Thu Jun 17 15:15:52 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 (implements, 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__ = implements('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 Jun 17 15:09:12 2010 +0200
+++ b/web/webconfig.py	Thu Jun 17 15:15:52 2010 +0200
@@ -23,8 +23,10 @@
 
 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 +79,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 +208,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 +250,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 +263,30 @@
             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))
-
-    @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):
+    @cached
+    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 where the given resource may be found"""
+        directory = self._fs_path_locate(rid, rdirectory)
+        if directory is None:
+            return None
+        if rdirectory == 'data' and rid.endswith('.css'):
+            return self.uiprops.process_resource(join(directory, rdirectory), rid)
+        return join(directory, rdirectory)
 
     def locate_all_files(self, rid, rdirectory='wdoc'):
         """return all files corresponding to the given resource"""
@@ -309,8 +300,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 +311,73 @@
         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
+        self.uiprops = PropertySheet(
+            join(self.appdatahome, 'uicache'),
+            data=lambda x: self.datadir_url + x,
+            datadir_url=self.datadir_url[:-1])
+        self._init_uiprops(self.uiprops)
+        if self['https-url']:
+            self.https_uiprops = PropertySheet(
+                join(self.appdatahome, 'uicache'),
+                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 +404,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 Jun 17 15:09:12 2010 +0200
+++ b/wsgi/handler.py	Thu Jun 17 15:15:52 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 Jun 17 15:09:12 2010 +0200
+++ b/xy.py	Thu Jun 17 15:15:52 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')