oldstable is now 3.9 oldstable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 03 Nov 2010 16:38:28 +0100
brancholdstable
changeset 6665 90f2f20367bc
parent 6018 f4d1d5d9ccbb (current diff)
parent 6661 1719137de7da (diff)
child 6701 fd4267ecbba6
child 6710 a89dc08e5970
oldstable is now 3.9
devtools/test/data/dbfill.conf
doc/book/_maybe_to_integrate/treemixin.rst
doc/book/en/devrepo/entityclasses/interfaces.rst
goa/__init__.py
goa/appobjects/__init__.py
goa/appobjects/components.py
goa/appobjects/dbmgmt.py
goa/appobjects/gauthservice.py
goa/appobjects/sessions.py
goa/bin/laxctl
goa/db.py
goa/dbinit.py
goa/dbmyams.py
goa/doc/FAQ.en.txt
goa/doc/README_LAX.fr.txt
goa/doc/devmanual_fr/advanced_notes.txt
goa/doc/devmanual_fr/archi_globale.dia
goa/doc/devmanual_fr/archi_globale.png
goa/doc/devmanual_fr/chap_autres_composants_ui.txt
goa/doc/devmanual_fr/chap_bases_framework_erudi.txt
goa/doc/devmanual_fr/chap_configuration_instance.txt
goa/doc/devmanual_fr/chap_definition_schema.txt
goa/doc/devmanual_fr/chap_definition_workflows.txt
goa/doc/devmanual_fr/chap_fondements_erudi.txt
goa/doc/devmanual_fr/chap_i18n.txt
goa/doc/devmanual_fr/chap_manipulation_donnees.txt
goa/doc/devmanual_fr/chap_migration.txt
goa/doc/devmanual_fr/chap_mise_en_place_environnement.txt
goa/doc/devmanual_fr/chap_rql.txt
goa/doc/devmanual_fr/chap_serveur_crochets.txt
goa/doc/devmanual_fr/chap_serveur_notification.txt
goa/doc/devmanual_fr/chap_tests.txt
goa/doc/devmanual_fr/chap_ui_gestion_formulaire.txt
goa/doc/devmanual_fr/chap_ui_js_json.txt
goa/doc/devmanual_fr/chap_visualisation_donnees.txt
goa/doc/devmanual_fr/index.txt
goa/doc/devmanual_fr/main_template_layout.dia
goa/doc/devmanual_fr/main_template_layout.png
goa/doc/devmanual_fr/makefile
goa/doc/devmanual_fr/sect_definition_entites.txt
goa/doc/devmanual_fr/sect_definition_schema.txt
goa/doc/devmanual_fr/sect_erudi-ctl.txt
goa/doc/devmanual_fr/sect_installation.txt
goa/doc/devmanual_fr/sect_mercurial.txt
goa/doc/devmanual_fr/sect_stdlib_schemas.txt
goa/doc/devmanual_fr/sect_stdlib_vues.txt
goa/doc/quickstart.txt
goa/doc/tutorial-wine.txt
goa/doc/tutorial.en.txt
goa/gaesource.py
goa/goaconfig.py
goa/goactl.py
goa/goavreg.py
goa/overrides/__init__.py
goa/overrides/mttransforms.py
goa/overrides/rqlannotation.py
goa/overrides/server__init__.py
goa/overrides/server_utils.py
goa/overrides/toolsutils.py
goa/rqlinterpreter.py
goa/skel/app.yaml.tmpl
goa/skel/bootstrap_cubes
goa/skel/custom.py
goa/skel/cw-cubes/README.txt
goa/skel/i18n/en.po
goa/skel/i18n/fr.po
goa/skel/loader.py
goa/skel/main.py
goa/skel/schema.py
goa/skel/views.py
goa/test/data/__init__.py
goa/test/data/bootstrap_cubes
goa/test/data/schema.py
goa/test/data/settings.py
goa/test/data/views.py
goa/test/unittest_db.py
goa/test/unittest_editcontroller.py
goa/test/unittest_metadata.py
goa/test/unittest_rql.py
goa/test/unittest_schema.py
goa/test/unittest_views.py
goa/testlib.py
goa/tools/__init__.py
goa/tools/generate_schema_img.py
goa/tools/laxctl.py
i18n/static-messages.pot
misc/migration/3.6.0_Any.py
server/test/data/sourcesldap
skeleton/data/external_resources.tmpl
skeleton/test/test_CUBENAME.py
web/data/cubicweb.bookmarks.js
web/data/cubicweb.massmailing.js
web/data/external_resources
web/data/mail.gif
web/data/nomail.gif
web/test/jstest_python.jst
--- a/.hgtags	Tue Jul 27 12:36:03 2010 +0200
+++ b/.hgtags	Wed Nov 03 16:38:28 2010 +0100
@@ -135,5 +135,25 @@
 5d05b08adeab1ea301e49ed8537e35ede6db92f6 cubicweb-debian-version-3.8.5-1
 1a24c62aefc5e57f61be3d04affd415288e81904 cubicweb-version-3.8.6
 607a90073911b6bb941a49b5ec0b0d2a9cd479af cubicweb-debian-version-3.8.6-1
+d9936c39d478b6701a4adef17bc28888ffa011c6 cubicweb-version-3.9.0
+eda4940ffef8b7d36127e68de63a52388374a489 cubicweb-debian-version-3.9.0-1
 a1a334d934390043a4293a4ee42bdceb1343246e cubicweb-version-3.8.7
 1cccf88d6dfe42986e1091de4c364b7b5814c54f cubicweb-debian-version-3.8.7-1
+4d75f743ed49dd7baf8bde7b0e475244933fa08e cubicweb-version-3.9.1
+9bd75af3dca36d7be5d25fc5ab1b89b34c811456 cubicweb-debian-version-3.9.1-1
+e51796b9caf389c224c6f66dcb8aa75bf1b82eff cubicweb-version-3.9.2
+8a23821dc1383e14a7e92a931b91bc6eed4d0af7 cubicweb-debian-version-3.9.2-1
+900772fd9caaf068eb2fdd4544b03efec91901e6 cubicweb-version-3.9.3
+ab1f9686ff3e0843b570b98f89fb5ccc8d7dec8c cubicweb-debian-version-3.9.3-1
+6cebb361dcb27ded654426b4c82f6401c862e034 cubicweb-version-3.9.4
+8d32d82134dc1d8eb0ce230191f34fd49084a168 cubicweb-debian-version-3.9.4-1
+0a1fce8ddc672ca9ee7328ed4f88c1aa6e48d286 cubicweb-version-3.9.5
+12038ca95f0fff2205f7ee029f5602d192118aec cubicweb-debian-version-3.9.5-1
+d37428222a6325583be958d7c7fe7c595115663d cubicweb-version-3.9.6
+7d2cab567735a17cab391c1a7f1bbe39118308a2 cubicweb-debian-version-3.9.6-1
+de588e756f4fbe9c53c72159c6b96580a36d3fa6 cubicweb-version-3.9.7
+1c01f9dffd64d507863c9f8f68e3585b7aa24374 cubicweb-debian-version-3.9.7-1
+eed788018b595d46a55805bd8d2054c401812b2b cubicweb-version-3.9.8
+e4dba8ae963701a36be94ae58c790bc97ba029bb cubicweb-debian-version-3.9.8-1
+df0b2de62cec10c84a2fff5233db05852cbffe93 cubicweb-version-3.9.9
+1ba51b00fc44faa0d6d57448000aaa1fd5c6ab57 cubicweb-debian-version-3.9.9-1
--- a/MANIFEST.in	Tue Jul 27 12:36:03 2010 +0200
+++ b/MANIFEST.in	Wed Nov 03 16:38:28 2010 +0100
@@ -5,13 +5,14 @@
 include bin/cubicweb-*
 include man/cubicweb-ctl.1
 
-recursive-include doc README makefile *.conf *.py *.rst *.txt *.html *.png *.svg *.zargo *.dia
+recursive-include doc README makefile *.conf *.css *.py *.rst *.txt *.html *.png *.svg *.zargo *.dia
 
 recursive-include misc *.py *.png *.display
 
 include web/views/*.pt
 recursive-include web/data external_resources *.js *.css *.py *.png *.gif *.ico *.ttf
 recursive-include web/wdoc *.rst *.png *.xml ChangeLog*
+recursive-include devtools/data *.js *.css
 
 recursive-include i18n *.pot *.po
 recursive-include schemas *.py *.sql
@@ -21,10 +22,15 @@
 recursive-include sobjects/test/data bootstrap_cubes *.py
 recursive-include hooks/test/data bootstrap_cubes *.py
 recursive-include server/test/data bootstrap_cubes *.py source*
-recursive-include web/test/data bootstrap_cubes *.py
-recursive-include devtools/test/data bootstrap_cubes *.py *.txt
+recursive-include devtools/test/data bootstrap_cubes *.py *.txt *.js
+recursive-include web/test/data bootstrap_cubes pouet.css *.py
+
+recursive-include web/test/jstests *.js *.html *.css *.json
+recursive-include web/test/windmill *.py
 
 recursive-include skeleton *.py *.css *.js *.po compat *.in *.tmpl
 
+prune doc/book/en/.static
+prune doc/book/fr/.static
 prune misc/cwfs
 prune goa
--- a/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """CubicWeb is a generic framework to quickly build applications which describes
 relations between entitites.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 # ignore the pygments UserWarnings
--- a/__pkginfo__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/__pkginfo__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 8, 7)
+numversion = (3, 9, 9)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -40,10 +40,10 @@
 ]
 
 __depends__ = {
-    'logilab-common': '>= 0.50.2',
-    'logilab-mtconverter': '>= 0.6.0',
+    'logilab-common': '>= 0.51.0',
+    'logilab-mtconverter': '>= 0.8.0',
     'rql': '>= 0.26.2',
-    'yams': '>= 0.28.1',
+    'yams': '>= 0.30.1',
     'docutils': '>= 0.6',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependancies
@@ -52,12 +52,12 @@
     'Twisted': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '>= 1.0.5',
+    'logilab-database': '>= 1.3.0',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
     }
 
 __recommends__ = {
-    'Pyro': '>= 3.9.1',
+    'Pyro': '>= 3.9.1, < 4.0.0',
     'PIL': '',                  # for captcha
     'pycrypto': '',             # for crypto extensions
     'fyzz': '>= 0.1.0',         # for sparql
@@ -77,6 +77,7 @@
                 join('server', 'test', 'data'),
                 join('hooks', 'test', 'data'),
                 join('web', 'test', 'data'),
+                join('devtools', 'data'),
                 join('devtools', 'test', 'data'),
                 'schemas', 'skeleton']
 
@@ -95,7 +96,13 @@
 else:
     pydir = join('python' + _pyversion, 'site-packages')
 
+# data files that shall be copied into the main package directory
+package_data = {
+    'cubicweb.web.views':['*.pt'],
+    }
+
 try:
+    # data files that shall be copied outside the main package directory
     data_files = [
         # server data
         [join('share', 'cubicweb', 'schemas'),
@@ -118,10 +125,6 @@
          [join(_wdocimages_dir, fname) for fname in listdir(_wdocimages_dir)]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'i18n'),
          [join(_i18n_dir, fname) for fname in listdir(_i18n_dir)]],
-        # XXX: drop .pt files
-        [join('lib', pydir, 'cubicweb', 'web', 'views'),
-         [join(_views_dir, fname) for fname in listdir(_views_dir)
-          if fname.endswith('.pt')]],
         # skeleton
         ]
 except OSError:
--- a/_exceptions.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/_exceptions.py	Wed Nov 03 16:38:28 2010 +0100
@@ -80,6 +80,8 @@
 class MultiSourcesError(RepositoryError, InternalError):
     """usually due to bad multisources configuration or rql query"""
 
+class UniqueTogetherError(RepositoryError):
+    """raised when a unique_together constraint caused an IntegrityError"""
 
 # security exceptions #########################################################
 
@@ -128,14 +130,20 @@
     """
 
 class NoSelectableObject(RegistryException):
-    """some views with the given vid have been found but no
-    one is applicable to the result set
-    """
+    """raised when no appobject is selectable for a given context."""
+    def __init__(self, args, kwargs, appobjects):
+        self.args = args
+        self.kwargs = kwargs
+        self.appobjects = appobjects
+
+    def __str__(self):
+        return ('args: %s, kwargs: %s\ncandidates: %s'
+                % (self.args, self.kwargs.keys(), self.appobjects))
+
 
 class UnknownProperty(RegistryException):
     """property found in database but unknown in registry"""
 
-
 # query exception #############################################################
 
 class QueryError(CubicWebRuntimeError):
--- a/appobject.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/appobject.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/clone_deps.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,115 @@
+#!/usr/bin/python
+import os
+import sys
+from subprocess import call, Popen, PIPE
+try:
+    from mercurial.dispatch import dispatch as hg_call
+except ImportError:
+    print '-' * 20
+    print "mercurial module is not reachable from this Python interpreter"
+    print "trying from command line ..."
+    tryhg = os.system('hg --version')
+    if tryhg:
+        print 'mercurial seems to unavailable, please install it'
+        raise
+    print 'found it, ok'
+    print '-' * 20
+    def hg_call(args):
+        call(['hg'] + args)
+from urllib import urlopen
+from os import path as osp, pardir
+from os.path import normpath, join, dirname
+
+BASE_URL = 'http://www.logilab.org/hg/'
+
+to_clone = ['fyzz', 'yams', 'rql',
+            'logilab/common', 'logilab/constraint', 'logilab/database',
+            'logilab/devtools', 'logilab/mtconverter',
+            'cubes/blog', 'cubes/calendar', 'cubes/card', 'cubes/comment',
+            'cubes/datafeed', 'cubes/email', 'cubes/file', 'cubes/folder',
+            'cubes/forgotpwd', 'cubes/keyword', 'cubes/link',
+            'cubes/mailinglist', 'cubes/nosylist', 'cubes/person',
+            'cubes/preview', 'cubes/registration', 'cubes/rememberme',
+            'cubes/tag', 'cubes/vcsfile', 'cubes/zone']
+
+# a couple of functions to be used to explore available
+# repositories and cubes
+def list_repos(repos_root):
+    assert repos_root.startswith('http://')
+    hgwebdir_repos = (repo.strip()
+                      for repo in urlopen(repos_root + '?style=raw').readlines()
+                      if repo.strip())
+    prefix = osp.commonprefix(hgwebdir_repos)
+    return (repo[len(prefix):].strip('/')
+            for repo in hgwebdir_repos)
+
+def list_all_cubes(base_url=BASE_URL):
+    all_repos = list_repos(base_url)
+    #search for cubes
+    for repo in all_repos:
+        if repo.startswith('cubes'):
+            to_clone.append(repo)
+
+def get_latest_debian_tag(path):
+    proc = Popen(['hg', '-R', path, 'tags'], stdout=PIPE)
+    out, _err = proc.communicate()
+    for line in out.splitlines():
+        if 'debian-version' in line:
+            return line.split()[0]
+
+def main():
+    if len(sys.argv) == 1:
+        base_url = BASE_URL
+    elif len(sys.argv) == 2:
+        base_url = sys.argv[1]
+    else:
+        print >> sys.stderr, 'usage %s [base_url]' %  sys.argv[0]
+        sys.exit(1)
+    print len(to_clone), 'repositories will be cloned'
+    base_dir = normpath(join(dirname(__file__), pardir, pardir))
+    os.chdir(base_dir)
+    not_updated = []
+    for repo in to_clone:
+        url = base_url + repo
+        if '/' not in repo:
+            target_path = repo
+        else:
+            assert repo.count('/') == 1, repo
+            directory, repo = repo.split('/')
+            if not osp.isdir(directory):
+                os.mkdir(directory)
+                open(join(directory, '__init__.py'), 'w').close()
+            target_path = osp.join(directory, repo)
+        if osp.exists(target_path):
+            print target_path, 'seems already cloned. Skipping it.'
+        else:
+            hg_call(['clone', '-U', url, target_path])
+            tag = get_latest_debian_tag(target_path)
+            if tag:
+                print 'updating to', tag
+                hg_call(['update', '-R', target_path, tag])
+            else:
+                not_updated.append(target_path)
+    print """
+CubicWeb dependencies and standard set of cubes have been fetched and
+update to the latest stable version.
+
+You should ensure your PYTHONPATH contains `%(basedir)s`.
+You might want to read the environment configuration section of the documentation
+at http://docs.cubicweb.org/admin/setup.html#environment-configuration
+
+You can find more cubes at http://www.cubicweb.org.
+Clone them from `%(baseurl)scubes/` into the `%(basedir)s%(sep)scubes%(sep)s` directory.
+
+To get started you may read http://docs.cubicweb.org/tutorials/base/index.html.
+""" % {'basedir': os.getcwd(), 'baseurl': base_url, 'sep': os.sep}
+    if not_updated:
+        print >> sys.stderr, 'WARNING: The following repositories were not updated (no debian tag found):'
+        for path in not_updated:
+            print >> sys.stderr, '\t-', path
+
+if __name__ == '__main__':
+    main()
+
+
+
--- a/cwconfig.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/cwconfig.py	Wed Nov 03 16:38:28 2010 +0100
@@ -51,7 +51,7 @@
         CW_INSTANCES_DATA_DIR = /var/lib/cubicweb/instances/
         CW_RUNTIME_DIR = /var/run/cubicweb/
 
- * 'user': ::
+* 'user': ::
 
         CW_INSTANCES_DIR = ~/etc/cubicweb.d/
         CW_INSTANCES_DATA_DIR = ~/etc/cubicweb.d/
@@ -151,7 +151,7 @@
 
 from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP,
                       ConfigurationError, Binary)
-from cubicweb.toolsutils import env_path, create_dir
+from cubicweb.toolsutils import create_dir
 
 CONFIGURATIONS = []
 
@@ -201,7 +201,8 @@
     old_prefix = None
     if not isdir(start_path):
         prefix = dirname(start_path)
-    while not isdir(join(prefix, 'share', 'cubicweb')) and prefix != old_prefix:
+    while (not isdir(join(prefix, 'share', 'cubicweb'))
+          or prefix.endswith('.egg')) and prefix != old_prefix:
         old_prefix = prefix
         prefix = dirname(prefix)
     if isdir(join(prefix, 'share', 'cubicweb')):
@@ -283,6 +284,7 @@
     _INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
 except KeyError:
     _INSTALL_PREFIX = _find_prefix()
+_USR_INSTALL = _INSTALL_PREFIX == '/usr'
 
 class CubicWebNoAppConfiguration(ConfigurationMixIn):
     """base class for cubicweb configuration without a specific instance directory
@@ -296,9 +298,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'):
         mode = 'user'
@@ -307,7 +306,7 @@
         mode = _forced_mode or 'system'
         _CUBES_DIR = join(_INSTALL_PREFIX, 'share', 'cubicweb', 'cubes')
 
-    CUBES_DIR = env_path('CW_CUBES_DIR', _CUBES_DIR, 'cubes', checkexists=False)
+    CUBES_DIR = abspath(os.environ.get('CW_CUBES_DIR', _CUBES_DIR))
     CUBES_PATH = os.environ.get('CW_CUBES_PATH', '').split(os.pathsep)
 
     options = (
@@ -499,9 +498,26 @@
             deps = dict((key, None) for key in deps)
             warn('[3.8] cube %s should define %s as a dict' % (cube, key),
                  DeprecationWarning)
+        for depcube in deps:
+            try:
+                newname = CW_MIGRATION_MAP[depcube]
+            except KeyError:
+                pass
+            else:
+                deps[newname] = deps.pop(depcube)
         return deps
 
     @classmethod
+    def cube_depends_cubicweb_version(cls, cube):
+        # XXX no backward compat (see _cube_deps above)
+        try:
+            pkginfo = cls.cube_pkginfo(cube)
+            deps = getattr(pkginfo, '__depends__')
+            return deps.get('cubicweb')
+        except AttributeError:
+            return None
+
+    @classmethod
     def cube_dependencies(cls, cube):
         """return cubicweb cubes used by the given cube"""
         return cls._cube_deps(cube, '__depends_cubes__', '__use__')
@@ -518,17 +534,17 @@
         """
         cubes = list(cubes)
         todo = cubes[:]
+        if with_recommends:
+            available = set(cls.available_cubes())
         while todo:
             cube = todo.pop(0)
             for depcube in cls.cube_dependencies(cube):
                 if depcube not in cubes:
-                    depcube = CW_MIGRATION_MAP.get(depcube, depcube)
                     cubes.append(depcube)
                     todo.append(depcube)
             if with_recommends:
                 for depcube in cls.cube_recommends(cube):
-                    if depcube not in cubes:
-                        depcube = CW_MIGRATION_MAP.get(depcube, depcube)
+                    if depcube not in cubes and depcube in available:
                         cubes.append(depcube)
                         todo.append(depcube)
         return cubes
@@ -663,12 +679,14 @@
                     vregpath.append(path + '.py')
         return vregpath
 
-    def __init__(self):
+    def __init__(self, debugmode=False):
         register_stored_procedures()
         ConfigurationMixIn.__init__(self)
+        self.debugmode = debugmode
         self.adjust_sys_path()
         self.load_defaults()
-        self.translations = {}
+        # will be properly initialized later by _gettext_init
+        self.translations = {'en': (unicode, lambda ctx, msgid: unicode(msgid) )}
         self._site_loaded = set()
         # don't register ReStructured Text directives by simple import, avoid pb
         # with eg sphinx.
@@ -684,25 +702,23 @@
         # overriden in CubicWebConfiguration
         self.cls_adjust_sys_path()
 
-    def init_log(self, logthreshold=None, debug=False,
-                 logfile=None, syslog=False):
+    def init_log(self, logthreshold=None, logfile=None, syslog=False):
         """init the log service"""
         if logthreshold is None:
-            if debug:
+            if self.debugmode:
                 logthreshold = 'DEBUG'
             else:
                 logthreshold = self['log-threshold']
-        self.debugmode = debug
         if sys.platform == 'win32':
             # no logrotate on win32, so use logging rotation facilities
             # for now, hard code weekly rotation every sunday, and 52 weeks kept
             # idea: make this configurable?
-            init_log(debug, syslog, logthreshold, logfile, self.log_format,
+            init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format,
                      rotation_parameters={'when': 'W6', # every sunday
                                           'interval': 1,
                                           'backupCount': 52})
         else:
-            init_log(debug, syslog, logthreshold, logfile, self.log_format)
+            init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format)
         # configure simpleTal logger
         logging.getLogger('simpleTAL').setLevel(logging.ERROR)
 
@@ -737,7 +753,7 @@
         # XXX extrapath argument to load_module_from_file only in lgc > 0.50.2
         from logilab.common.modutils import load_module_from_modpath, modpath_from_file
         module = load_module_from_modpath(modpath_from_file(sitefile, self.extrapath))
-        self.info('%s loaded', sitefile)
+        self.debug('%s loaded', sitefile)
         return module
 
     def eproperty_definitions(self):
@@ -774,11 +790,11 @@
 
     if CubicWebNoAppConfiguration.mode == 'user':
         _INSTANCES_DIR = expanduser('~/etc/cubicweb.d/')
-    else: #mode = 'system'
-        if _INSTALL_PREFIX == '/usr':
-            _INSTANCES_DIR = '/etc/cubicweb.d/'
-        else:
-            _INSTANCES_DIR = join(_INSTALL_PREFIX, 'etc', 'cubicweb.d')
+    #mode == system'
+    elif _USR_INSTALL:
+        _INSTANCES_DIR = '/etc/cubicweb.d/'
+    else:
+        _INSTANCES_DIR = join(_INSTALL_PREFIX, 'etc', 'cubicweb.d')
 
     if os.environ.get('APYCOT_ROOT'):
         _cubes_init = join(CubicWebNoAppConfiguration.CUBES_DIR, '__init__.py')
@@ -831,7 +847,7 @@
     @classmethod
     def instances_dir(cls):
         """return the control directory"""
-        return env_path('CW_INSTANCES_DIR', cls._INSTANCES_DIR, 'registry')
+        return abspath(os.environ.get('CW_INSTANCES_DIR', cls._INSTANCES_DIR))
 
     @classmethod
     def migration_scripts_dir(cls):
@@ -844,12 +860,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):
@@ -903,23 +919,34 @@
                     path = '%s-%s.log' % (basepath, i)
                     i += 1
             return path
-        return '/var/log/cubicweb/%s-%s.log' % (self.appid, self.name)
+        if _USR_INSTALL:
+            return '/var/log/cubicweb/%s-%s.log' % (self.appid, self.name)
+        else:
+            log_path = os.path.join(_INSTALL_PREFIX, 'var', 'log', 'cubicweb', '%s-%s.log')
+            return log_path % (self.appid, self.name)
+
+
 
     def default_pid_file(self):
         """return default path to the pid file of the instance'server"""
         if self.mode == 'system':
-            # XXX not under _INSTALL_PREFIX, right?
-            rtdir = env_path('CW_RUNTIME_DIR', '/var/run/cubicweb/', 'run time')
+            if _USR_INSTALL:
+                default = '/var/run/cubicweb/'
+            else:
+                default = os.path.join(_INSTALL_PREFIX, 'var', 'run', 'cubicweb')
         else:
             import tempfile
-            rtdir = env_path('CW_RUNTIME_DIR', tempfile.gettempdir(), 'run time')
+            default = tempfile.gettempdir()
+        # runtime directory created on startup if necessary, don't check it
+        # exists
+        rtdir = abspath(os.environ.get('CW_RUNTIME_DIR', default))
         return join(rtdir, '%s-%s.pid' % (self.appid, self.name))
 
     # instance methods used to get instance specific resources #############
 
-    def __init__(self, appid):
+    def __init__(self, appid, debugmode=False):
         self.appid = appid
-        CubicWebNoAppConfiguration.__init__(self)
+        CubicWebNoAppConfiguration.__init__(self, debugmode)
         self._cubes = None
         self.load_file_configuration(self.main_config_file())
 
@@ -937,11 +964,13 @@
     @property
     def appdatahome(self):
         if self.mode == 'system':
-            # XXX not under _INSTALL_PREFIX, right?
-            iddir = '/var/lib/cubicweb/instances/'
+            if _USR_INSTALL:
+                iddir = os.path.join('/var','lib', 'cubicweb', 'instances')
+            else:
+                iddir = os.path.join(_INSTALL_PREFIX, 'var', 'lib', 'cubicweb', 'instances')
         else:
             iddir = self.instances_dir()
-        iddir = env_path('CW_INSTANCES_DATA_DIR', iddir, 'additional data')
+        iddir = abspath(os.environ.get('CW_INSTANCES_DATA_DIR', iddir))
         return join(iddir, self.appid)
 
     def init_cubes(self, cubes):
@@ -986,6 +1015,32 @@
         """write down current configuration"""
         self.generate_config(open(self.main_config_file(), 'w'))
 
+    def check_writeable_uid_directory(self, path):
+        """check given directory path exists, belongs to the user running the
+        server process and is writeable.
+
+        If not, try to fix this, letting exception propagate when not possible.
+        """
+        if not exists(path):
+            os.makedirs(path)
+        if self['uid']:
+            try:
+                uid = int(self['uid'])
+            except ValueError:
+                from pwd import getpwnam
+                uid = getpwnam(self['uid']).pw_uid
+        else:
+            try:
+                uid = os.getuid()
+            except AttributeError: # we are on windows
+                return
+        fstat = os.stat(path)
+        if fstat.st_uid != uid:
+            os.chown(path, uid, os.getgid())
+        import stat
+        if not (fstat.st_mode & stat.S_IWUSR):
+            os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+
     @cached
     def instance_md5_version(self):
         import hashlib
@@ -1000,7 +1055,7 @@
         super(CubicWebConfiguration, self).load_configuration()
         if self.apphome and self.set_language:
             # init gettext
-            self._set_language()
+            self._gettext_init()
 
     def _load_site_cubicweb(self, sitefile):
         # overriden to register cube specific options
@@ -1009,12 +1064,12 @@
             self.register_options(mod.options)
             self.load_defaults()
 
-    def init_log(self, logthreshold=None, debug=False, force=False):
+    def init_log(self, logthreshold=None, force=False):
         """init the log service"""
         if not force and hasattr(self, '_logging_initialized'):
             return
         self._logging_initialized = True
-        CubicWebNoAppConfiguration.init_log(self, logthreshold, debug,
+        CubicWebNoAppConfiguration.init_log(self, logthreshold,
                                             logfile=self.get('log-file'))
         # read a config file if it exists
         logconfig = join(self.apphome, 'logging.conf')
@@ -1035,7 +1090,7 @@
             if lang != 'en':
                 yield lang
 
-    def _set_language(self):
+    def _gettext_init(self):
         """set language for gettext"""
         from gettext import translation
         path = join(self.apphome, 'i18n')
@@ -1115,6 +1170,7 @@
 def register_stored_procedures():
     from logilab.database import FunctionDescr
     from rql.utils import register_function, iter_funcnode_variables
+    from rql.nodes import SortTerm, Constant, VariableRef
 
     global _EXT_REGISTERED
     if _EXT_REGISTERED:
@@ -1160,6 +1216,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/cwctl.py	Wed Nov 03 16:38:28 2010 +0100
@@ -17,9 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """the cubicweb-ctl tool, based on logilab.common.clcommands to
 provide a pluggable commands system.
-
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 # *ctl module should limit the number of import to be imported as quickly as
@@ -36,14 +35,18 @@
     def getpgid():
         """win32 getpgid implementation"""
 
+
 from os.path import exists, join, isfile, isdir, dirname, abspath
 
-from logilab.common.clcommands import register_commands, pop_arg
+from logilab.common.clcommands import CommandLine
 from logilab.common.shellutils import ASK
 
 from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CWDEV, CONFIGURATIONS
-from cubicweb.toolsutils import Command, main_run, rm, create_dir, underline_title
+from cubicweb.toolsutils import Command, rm, create_dir, underline_title
+from cubicweb.__pkginfo__ import version
+
+CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', version=version)
 
 def wait_process_end(pid, maxtry=10, waittime=1):
     """wait for a process to actually die"""
@@ -61,7 +64,10 @@
         raise ExecutionError('can\'t kill process %s' % pid)
 
 def list_instances(regdir):
-    return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
+    if isdir(regdir):
+        return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
+    else:
+        return []
 
 def detect_available_modes(templdir):
     modes = []
@@ -277,15 +283,15 @@
             print 'Warnings:\n', '\n'.join('* '+txt for txt in cfgpb.warnings)
         if cfgpb.errors:
             print 'Errors:'
-            for op, cube, version in cfgpb.errors:
+            for op, cube, version, src in cfgpb.errors:
                 if op == 'add':
                     print '* cube', cube,
                     if version:
                         print ' version', version,
-                    print 'is not installed, but required by %s' % ' '.join(cfgpb.reverse_constraints[cube])
+                    print 'is not installed, but required by %s' % src
                 else:
-                    print '* cube %s version %s is installed, but version %s is required by (%s)' % (
-                        cube, cfgpb.cubes[cube], version, ', '.join(cfgpb.reverse_constraints[cube]))
+                    print '* cube %s version %s is installed, but version %s is required by %s' % (
+                        cube, cfgpb.cubes[cube], version, src)
 
 class CreateInstanceCommand(Command):
     """Create an instance from a cube. This is an unified
@@ -302,6 +308,7 @@
     """
     name = 'create'
     arguments = '<cube> <instance>'
+    min_args = max_args = 2
     options = (
         ("config-level",
          {'short': 'l', 'type' : 'int', 'metavar': '<level>',
@@ -326,8 +333,8 @@
         """run the command with its specific arguments"""
         from logilab.common.textutils import splitstrip
         configname = self.config.config
-        cubes = splitstrip(pop_arg(args, 1))
-        appid = pop_arg(args)
+        cubes, appid = args
+        cubes = splitstrip(cubes)
         # get the configuration and helper
         config = cwcfg.config_for(appid, configname)
         config.set_language = False
@@ -416,12 +423,12 @@
     """
     name = 'delete'
     arguments = '<instance>'
-
+    min_args = max_args = 1
     options = ()
 
     def run(self, args):
         """run the command with its specific arguments"""
-        appid = pop_arg(args, msg="No instance specified !")
+        appid = args[0]
         configs = [cwcfg.config_for(appid, configname)
                    for configname in cwcfg.possible_configurations(appid)]
         if not configs:
@@ -477,23 +484,23 @@
 
     def start_instance(self, appid):
         """start the instance's server"""
-        debug = self['debug']
-        force = self['force']
-        loglevel = self['loglevel']
-        config = cwcfg.config_for(appid)
-        if loglevel is not None:
-            loglevel = 'LOG_%s' % loglevel.upper()
-            config.global_set_option('log-threshold', loglevel)
-            config.init_log(loglevel, debug=debug, force=True)
+        config = cwcfg.config_for(appid, debugmode=self['debug'])
+        init_cmdline_log_threshold(config, self['loglevel'])
         if self['profile']:
             config.global_set_option('profile', self.config.profile)
         helper = self.config_helper(config, cmdname='start')
         pidf = config['pid-file']
-        if exists(pidf) and not force:
+        if exists(pidf) and not self['force']:
             msg = "%s seems to be running. Remove %s by hand if necessary or use \
 the --force option."
             raise ExecutionError(msg % (appid, pidf))
-        helper.start_server(config, debug)
+        helper.start_server(config)
+
+
+def init_cmdline_log_threshold(config, loglevel):
+    if loglevel is not None:
+        config.global_set_option('log-threshold', loglevel.upper())
+        config.init_log(config['log-threshold'], force=True)
 
 
 class StopInstanceCommand(InstanceCommand):
@@ -570,7 +577,7 @@
                 print '*'*72
                 if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
                     continue
-            StopInstanceCommand().stop_instance(appid)
+            StopInstanceCommand(self.logger).stop_instance(appid)
         forkcmd = [w for w in sys.argv if not w in args]
         forkcmd[1] = 'start'
         forkcmd = ' '.join(forkcmd)
@@ -580,7 +587,7 @@
                 sys.exit(status)
 
     def restart_instance(self, appid):
-        StopInstanceCommand().stop_instance(appid)
+        StopInstanceCommand(self.logger).stop_instance(appid)
         self.start_instance(appid)
 
 
@@ -739,7 +746,7 @@
             print '-> migration needed from %s to %s for %s' % (fromversion, toversion, cube)
         # only stop once we're sure we have something to do
         if not (CWDEV or self.config.nostartstop):
-            StopInstanceCommand().stop_instance(appid)
+            StopInstanceCommand(self.logger).stop_instance(appid)
         # run cubicweb/componants migration scripts
         mih.migrate(vcconf, reversed(toupgrade), self.config)
         # rewrite main configuration file
@@ -788,11 +795,16 @@
     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>]'
+    min_args = 1
     options = (
         ('system-only',
          {'short': 'S', 'action' : 'store_true',
@@ -831,7 +843,7 @@
         )
 
     def run(self, args):
-        appid = pop_arg(args, None, msg="No instance specified !")
+        appid = args.pop(0)
         if self.config.pyro:
             from cubicweb import AuthenticationError
             from cubicweb.dbapi import connect
@@ -868,8 +880,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:
@@ -924,30 +939,32 @@
         for cube in cwcfg.available_cubes():
             print cube
 
-register_commands((ListCommand,
-                   CreateInstanceCommand,
-                   DeleteInstanceCommand,
-                   StartInstanceCommand,
-                   StopInstanceCommand,
-                   RestartInstanceCommand,
-                   ReloadConfigurationCommand,
-                   StatusCommand,
-                   UpgradeInstanceCommand,
-                   ShellCommand,
-                   RecompileInstanceCatalogsCommand,
-                   ListInstancesCommand, ListCubesCommand,
-                   ))
+for cmdcls in (ListCommand,
+               CreateInstanceCommand, DeleteInstanceCommand,
+               StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand,
+               ReloadConfigurationCommand, StatusCommand,
+               UpgradeInstanceCommand,
+               ShellCommand,
+               RecompileInstanceCatalogsCommand,
+               ListInstancesCommand, ListCubesCommand,
+               ):
+    CWCTL.register(cmdcls)
 
 
 def run(args):
     """command line tool"""
+    import os
+    sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
+    sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0)
     cwcfg.load_cwctl_plugins()
-    main_run(args, """%%prog %s [options] %s
-
-The CubicWeb swiss-knife.
-
-%s"""
-)
+    try:
+        CWCTL.run(args)
+    except ConfigurationError, err:
+        print 'ERROR: ', err
+        sys.exit(1)
+    except ExecutionError, err:
+        print err
+        sys.exit(2)
 
 if __name__ == '__main__':
     run(sys.argv[1:])
--- a/cwvreg.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/cwvreg.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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:
@@ -162,14 +161,14 @@
 'primary'`) view (`__registry__ = 'views'`) for a result set
 containing a `Card` entity, two objects will probably be selectable:
 
-* the default primary view (`__select__ = implements('Any')`), meaning
+* the default primary view (`__select__ = is_instance('Any')`), meaning
   that the object is selectable for any kind of entity type
 
-* the specific `Card` primary view (`__select__ = implements('Card')`,
+* the specific `Card` primary view (`__select__ = is_instance('Card')`,
   meaning that the object is selectable for Card entities
 
 Other primary views specific to other entity types won't be selectable in this
-case. Among selectable objects, the `implements('Card')` selector will return a higher
+case. Among selectable objects, the `is_instance('Card')` selector will return a higher
 score since it's more specific, so the correct view will be selected as expected.
 
 .. _SelectionAPI:
@@ -194,6 +193,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import  deprecated
 from logilab.common.modutils import cleanup_sys_modules
@@ -202,9 +203,9 @@
 
 from cubicweb import (ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid,
                       ObjectNotFound, NoSelectableObject, RegistryNotFound,
-                      CW_EVENT_MANAGER, onevent)
+                      CW_EVENT_MANAGER)
 from cubicweb.utils import dump_class
-from cubicweb.vregistry import VRegistry, Registry, class_regid
+from cubicweb.vregistry import VRegistry, Registry, class_regid, classid
 from cubicweb.rtags import RTAGS
 
 def clear_rtag_objects():
@@ -213,23 +214,23 @@
 
 def use_interfaces(obj):
     """return interfaces used by the given object by searching for implements
-    selectors, with a bw compat fallback to accepts_interfaces attribute
+    selectors
     """
     from cubicweb.selectors import implements
-    try:
-        # XXX deprecated
-        return sorted(obj.accepts_interfaces)
-    except AttributeError:
-        try:
-            impl = obj.__select__.search_selector(implements)
-            if impl:
-                return sorted(impl.expected_ifaces)
-        except AttributeError:
-            pass # old-style appobject classes with no accepts_interfaces
-        except:
-            print 'bad selector %s on %s' % (obj.__select__, obj)
-            raise
-        return ()
+    impl = obj.__select__.search_selector(implements)
+    if impl:
+        return sorted(impl.expected_ifaces)
+    return ()
+
+def require_appobject(obj):
+    """return interfaces used by the given object by searching for implements
+    selectors
+    """
+    from cubicweb.selectors import appobject_selectable
+    impl = obj.__select__.search_selector(appobject_selectable)
+    if impl:
+        return (impl.registry, impl.regids)
+    return None
 
 
 class CWRegistry(Registry):
@@ -309,11 +310,10 @@
     @cached
     def parent_classes(self, etype):
         if etype == 'Any':
-            return [self.etype_class('Any')]
-        eschema = self.schema.eschema(etype)
-        parents = [self.etype_class(e.type) for e in eschema.ancestors()]
-        parents.append(self.etype_class('Any'))
-        return parents
+            return (), self.etype_class('Any')
+        parents = tuple(self.etype_class(e.type)
+                        for e in self.schema.eschema(etype).ancestors())
+        return parents, self.etype_class('Any')
 
     @cached
     def etype_class(self, etype):
@@ -444,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
@@ -478,8 +477,10 @@
         return (value for key, value in self.items())
 
     def reset(self):
+        CW_EVENT_MANAGER.emit('before-registry-reset', self)
         super(CubicWebVRegistry, self).reset()
         self._needs_iface = {}
+        self._needs_appobject = {}
         # two special registries, propertydefs which care all the property
         # definitions, and propertyvals which contains values for those
         # properties
@@ -488,6 +489,7 @@
             self['propertyvalues'] = self.eprop_values = {}
             for key, propdef in self.config.eproperty_definitions():
                 self.register_property(key, **propdef)
+        CW_EVENT_MANAGER.emit('after-registry-reset', self)
 
     def set_schema(self, schema):
         """set instance'schema and load application objects"""
@@ -521,7 +523,6 @@
                 if not cube in cubes:
                     cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
                     cleanup_sys_modules(cpath)
-        self.reset()
         self.register_objects(path)
         CW_EVENT_MANAGER.emit('after-registry-reload')
 
@@ -540,6 +541,7 @@
                 for obj in objects:
                     obj.schema = schema
 
+    @deprecated('[3.9] use .register instead')
     def register_if_interface_found(self, obj, ifaces, **kwargs):
         """register `obj` but remove it if no entity class implements one of
         the given `ifaces` interfaces at the end of the registration process.
@@ -565,7 +567,15 @@
         # XXX bw compat
         ifaces = use_interfaces(obj)
         if ifaces:
+            if not obj.__name__.endswith('Adapter') and \
+                   any(iface for iface in ifaces if not isinstance(iface, basestring)):
+                warn('[3.9] %s: interfaces in implements selector are '
+                     'deprecated in favor of adapters / adaptable '
+                     'selector' % obj.__name__, DeprecationWarning)
             self._needs_iface[obj] = ifaces
+        depends_on = require_appobject(obj)
+        if depends_on is not None:
+            self._needs_appobject[obj] = depends_on
 
     def register_objects(self, path):
         """overriden to give cubicweb's extrapath (eg cubes package's __path__)
@@ -583,13 +593,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__)
@@ -600,18 +615,31 @@
                                    or iface
                                    for iface in ifaces)
                 if not ('Any' in ifaces or ifaces & implemented_interfaces):
-                    self.debug('kicking appobject %s (no implemented '
-                               'interface among %s)', obj, ifaces)
+                    self.debug('unregister %s (no implemented '
+                               'interface among %s)', classid(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('unregister %s (no registry %s)', classid(obj),
+                               regname)
+                    self.unregister(obj)
+                    continue
+                for regid in regids:
+                    if registry.get(regid):
+                        break
+                else:
+                    self.debug('unregister %s (no %s object in registry %s)',
+                               classid(obj), ' or '.join(regids), regname)
+                    self.unregister(obj)
         super(CubicWebVRegistry, self).initialization_completed()
         for rtag in RTAGS:
             # don't check rtags if we don't want to cleanup_interface_sobjects
             rtag.init(self.schema, check=self.config.cleanup_interface_sobjects)
 
-
     # rql parsing utilities ####################################################
 
     @property
--- a/dataimport.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/dataimport.py	Wed Nov 03 16:38:28 2010 +0100
@@ -34,7 +34,7 @@
            ]
 
   def gen_users(ctl):
-      for row in ctl.get_data('utilisateurs'):
+      for row in ctl.iter_and_commit('utilisateurs'):
           entity = mk_entity(row, USERS)
           entity['upassword'] = u'motdepasse'
           ctl.check('login', entity['login'], None)
@@ -51,10 +51,15 @@
   GENERATORS.append( (gen_users, CHK) )
 
   # create controller
-  ctl = CWImportController(RQLObjectStore(cnx))
+  if 'cnx' in globals():
+      ctl = CWImportController(RQLObjectStore(cnx))
+  else:
+      print 'debug mode (not connected)'
+      print 'run through cubicweb-ctl shell to access an instance'
+      ctl = CWImportController(ObjectStore())
   ctl.askerror = 1
   ctl.generators = GENERATORS
-  ctl.data['utilisateurs'] = lazytable(utf8csvreader(open('users.csv')))
+  ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
   # run
   ctl.run()
 
@@ -77,17 +82,32 @@
 
 from cubicweb.server.utils import eschema_eid
 
-def ucsvreader_pb(filepath, encoding='utf-8', separator=',', quote='"',
+def count_lines(stream_or_filename):
+    if isinstance(stream_or_filename, basestring):
+        f = open(filename)
+    else:
+        f = stream_or_filename
+        f.seek(0)
+    for i, line in enumerate(f):
+        pass
+    f.seek(0)
+    return i+1
+
+def ucsvreader_pb(stream_or_path, encoding='utf-8', separator=',', quote='"',
                   skipfirst=False, withpb=True):
     """same as ucsvreader but a progress bar is displayed as we iter on rows"""
-    if not osp.exists(filepath):
-        raise Exception("file doesn't exists: %s" % filepath)
-    rowcount = int(shellutils.Execute('wc -l "%s"' % filepath).out.strip().split()[0])
+    if isinstance(stream_or_path, basestring):
+        if not osp.exists(filepath):
+            raise Exception("file doesn't exists: %s" % filepath)
+        stream = open(stream_or_path)
+    else:
+        stream = stream_or_path
+    rowcount = count_lines(stream)
     if skipfirst:
         rowcount -= 1
     if withpb:
         pb = shellutils.ProgressBar(rowcount, 50)
-    for urow in ucsvreader(file(filepath), encoding, separator, quote, skipfirst):
+    for urow in ucsvreader(stream, encoding, separator, quote, skipfirst):
         yield urow
         if withpb:
             pb.update()
@@ -104,19 +124,21 @@
     for row in it:
         yield [item.decode(encoding) for item in row]
 
-def commit_every(nbit, store, it):
-    for i, x in enumerate(it):
-        yield x
-        if nbit is not None and i % nbit:
-            store.commit()
-    if nbit is not None:
-        store.commit()
+def callfunc_every(func, number, iterable):
+    """yield items of `iterable` one by one and call function `func`
+    every `number` iterations. Always call function `func` at the end.
+    """
+    for idx, item in enumerate(iterable):
+        yield item
+        if idx % number:
+            func()
+    func()
 
 def lazytable(reader):
     """The first row is taken to be the header of the table and
     used to output a dict for each row of data.
 
-    >>> data = lazytable(utf8csvreader(open(filename)))
+    >>> data = lazytable(ucsvreader(open(filename)))
     """
     header = reader.next()
     for row in reader:
@@ -209,7 +231,7 @@
     return None
 
 def required(value):
-    """raise ValueError is value is empty
+    """raise ValueError if value is empty
 
     This check should be often found in last position in the chain.
     """
@@ -396,20 +418,19 @@
 
     def __init__(self, session=None, commit=None):
         ObjectStore.__init__(self)
-        if session is not None:
-            if not hasattr(session, 'set_pool'):
-                # connection
-                cnx = session
-                session = session.request()
-                session.set_pool = lambda : None
-                commit = commit or cnx.commit
-            else:
-                session.set_pool()
-            self.session = session
-            self._commit = commit or session.commit
-        elif commit is not None:
-            self._commit = commit
-            # XXX .session
+        if session is None:
+            sys.exit('please provide a session of run this script with cubicweb-ctl shell and pass cnx as session')
+            session = cnx
+        if not hasattr(session, 'set_pool'):
+            # connection
+            cnx = session
+            session = session.request()
+            session.set_pool = lambda : None
+            commit = commit or cnx.commit
+        else:
+            session.set_pool()
+        self.session = session
+        self._commit = commit or session.commit
 
     @deprecated("[3.7] checkpoint() deprecated. use commit() instead")
     def checkpoint(self):
@@ -551,7 +572,12 @@
 
     def iter_and_commit(self, datakey):
         """iter rows, triggering commit every self.commitevery iterations"""
-        return commit_every(self.commitevery, self.store, self.get_data(datakey))
+        if self.commitevery is None:
+            return self.get_data(datakey)
+        else:
+            return callfunc_every(self.store.commit,
+                                  self.commitevery,
+                                  self.get_data(datakey))
 
 
 
@@ -584,7 +610,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/dbapi.py	Wed Nov 03 16:38:28 2010 +0100
@@ -100,9 +100,9 @@
     else: # method == 'pyro'
         # resolve the Pyro object
         from logilab.common.pyro_ext import ns_get_proxy
+        pyroid = database or config['pyro-instance-id'] or config.appid
         try:
-            return ns_get_proxy(database,
-                                defaultnsgroup=config['pyro-ns-group'],
+            return ns_get_proxy(pyroid, defaultnsgroup=config['pyro-ns-group'],
                                 nshost=config['pyro-ns-host'])
         except Exception, ex:
             raise ConnectionError(str(ex))
@@ -255,9 +255,6 @@
             self.session = None
             self.cnx = self.user = _NeedAuthAccessMock()
 
-    def base_url(self):
-        return self.vreg.config['base-url']
-
     def from_controller(self):
         return 'view'
 
@@ -462,6 +459,12 @@
                                                  time() - tstart, clock() - cstart))
         return rset
 
+def check_not_closed(func):
+    def decorator(self, *args, **kwargs):
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
+        return func(self, *args, **kwargs)
+    return decorator
 
 class Connection(object):
     """DB-API 2.0 compatible Connection object for CubicWeb
@@ -502,51 +505,15 @@
             self.rollback()
             return False #propagate the exception
 
-    def _txid(self, cursor=None): # XXX could now handle various isolation level!
-        # return a dict as bw compat trick
-        return {'txid': currentThread().getName()}
-
-    def request(self):
-        return DBAPIRequest(self.vreg, DBAPISession(self))
-
-    def check(self):
-        """raise `BadConnectionId` if the connection is no more valid"""
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        self._repo.check_session(self.sessionid)
-
-    def set_session_props(self, **props):
-        """raise `BadConnectionId` if the connection is no more valid"""
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        self._repo.set_session_props(self.sessionid, props)
+    def __del__(self):
+        """close the remote connection if necessary"""
+        if self._closed is None and self._close_on_del:
+            try:
+                self.close()
+            except:
+                pass
 
-    def get_shared_data(self, key, default=None, pop=False):
-        """return value associated to `key` in shared data"""
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.get_shared_data(self.sessionid, key, default, pop)
-
-    def set_shared_data(self, key, value, querydata=False):
-        """set value associated to `key` in shared data
-
-        if `querydata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction, and won't be available through the connexion, only on the
-        repository side.
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.set_shared_data(self.sessionid, key, value, querydata)
-
-    def get_schema(self):
-        """Return the schema currently used by the repository.
-
-        This is NOT part of the DB-API.
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.get_schema()
+    # connection initialization methods ########################################
 
     def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
         config = self.vreg.config
@@ -582,6 +549,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
@@ -601,20 +569,18 @@
         if sitetitle is not None:
             self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle}
 
+    @check_not_closed
     def source_defs(self):
         """Return the definition of sources used by the repository.
 
         This is NOT part of the DB-API.
         """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
         return self._repo.source_defs()
 
+    @check_not_closed
     def user(self, req=None, props=None):
         """return the User object associated to this connection"""
         # cnx validity is checked by the call to .user_info
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
         eid, login, groups, properties = self._repo.user_info(self.sessionid,
                                                               props)
         if req is None:
@@ -630,19 +596,98 @@
         user['login'] = login # cache login
         return user
 
-    def __del__(self):
-        """close the remote connection if necessary"""
-        if self._closed is None and self._close_on_del:
-            try:
-                self.close()
-            except:
-                pass
+    @check_not_closed
+    def check(self):
+        """raise `BadConnectionId` if the connection is no more valid"""
+        self._repo.check_session(self.sessionid)
+
+    def _txid(self, cursor=None): # XXX could now handle various isolation level!
+        # return a dict as bw compat trick
+        return {'txid': currentThread().getName()}
+
+    def request(self):
+        return DBAPIRequest(self.vreg, DBAPISession(self))
+
+    # session data methods #####################################################
+
+    @check_not_closed
+    def set_session_props(self, **props):
+        """raise `BadConnectionId` if the connection is no more valid"""
+        self._repo.set_session_props(self.sessionid, props)
+
+    @check_not_closed
+    def get_shared_data(self, key, default=None, pop=False):
+        """return value associated to `key` in shared data"""
+        return self._repo.get_shared_data(self.sessionid, key, default, pop)
 
+    @check_not_closed
+    def set_shared_data(self, key, value, querydata=False):
+        """set value associated to `key` in shared data
+
+        if `querydata` is true, the value will be added to the repository
+        session's query data which are cleared on commit/rollback of the current
+        transaction, and won't be available through the connexion, only on the
+        repository side.
+        """
+        return self._repo.set_shared_data(self.sessionid, key, value, querydata)
+
+    # meta-data accessors ######################################################
+
+    @check_not_closed
+    def get_schema(self):
+        """Return the schema currently used by the repository."""
+        return self._repo.get_schema()
+
+    @check_not_closed
+    def get_option_value(self, option, foreid=None):
+        """Return the value for `option` in the configuration. If `foreid` is
+        specified, the actual repository to which this entity belongs is
+        dereferenced and the option value retrieved from it.
+        """
+        return self._repo.get_option_value(option, foreid)
+
+    @check_not_closed
     def describe(self, eid):
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
         return self._repo.describe(self.sessionid, eid, **self._txid())
 
+    # db-api like interface ####################################################
+
+    @check_not_closed
+    def commit(self):
+        """Commit pending transaction for this connection to the repository.
+
+        may raises `Unauthorized` or `ValidationError` if we attempted to do
+        something we're not allowed to for security or integrity reason.
+
+        If the transaction is undoable, a transaction id will be returned.
+        """
+        return self._repo.commit(self.sessionid, **self._txid())
+
+    @check_not_closed
+    def rollback(self):
+        """This method is optional since not all databases provide transaction
+        support.
+
+        In case a database does provide transactions this method causes the the
+        database to roll back to the start of any pending transaction.  Closing
+        a connection without committing the changes first will cause an implicit
+        rollback to be performed.
+        """
+        self._repo.rollback(self.sessionid, **self._txid())
+
+    @check_not_closed
+    def cursor(self, req=None):
+        """Return a new Cursor Object using the connection.
+
+        On pyro connection, you should get cursor after calling if
+        load_appobjects method if desired (which you should call if you intend
+        to use ORM abilities).
+        """
+        if req is None:
+            req = self.request()
+        return self.cursor_class(self, self._repo, req=req)
+
+    @check_not_closed
     def close(self):
         """Close the connection now (rather than whenever __del__ is called).
 
@@ -652,52 +697,13 @@
         connection.  Note that closing a connection without committing the
         changes first will cause an implicit rollback to be performed.
         """
-        if self._closed:
-            raise ProgrammingError('Connection is already closed')
         self._repo.close(self.sessionid, **self._txid())
         del self._repo # necessary for proper garbage collection
         self._closed = 1
 
-    def commit(self):
-        """Commit pending transaction for this connection to the repository.
-
-        may raises `Unauthorized` or `ValidationError` if we attempted to do
-        something we're not allowed to for security or integrity reason.
-
-        If the transaction is undoable, a transaction id will be returned.
-        """
-        if not self._closed is None:
-            raise ProgrammingError('Connection is already closed')
-        return self._repo.commit(self.sessionid, **self._txid())
-
-    def rollback(self):
-        """This method is optional since not all databases provide transaction
-        support.
-
-        In case a database does provide transactions this method causes the the
-        database to roll back to the start of any pending transaction.  Closing
-        a connection without committing the changes first will cause an implicit
-        rollback to be performed.
-        """
-        if not self._closed is None:
-            raise ProgrammingError('Connection is already closed')
-        self._repo.rollback(self.sessionid, **self._txid())
-
-    def cursor(self, req=None):
-        """Return a new Cursor Object using the connection.
-
-        On pyro connection, you should get cursor after calling if
-        load_appobjects method if desired (which you should call if you intend
-        to use ORM abilities).
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Can\'t get cursor on closed connection')
-        if req is None:
-            req = self.request()
-        return self.cursor_class(self, self._repo, req=req)
-
     # undo support ############################################################
 
+    @check_not_closed
     def undoable_transactions(self, ueid=None, req=None, **actionfilters):
         """Return a list of undoable transaction objects by the connection's
         user, ordered by descendant transaction time.
@@ -730,6 +736,7 @@
             txinfo.req = req
         return txinfos
 
+    @check_not_closed
     def transaction_info(self, txuuid, req=None):
         """Return transaction object for the given uid.
 
@@ -744,6 +751,7 @@
         txinfo.req = req
         return txinfo
 
+    @check_not_closed
     def transaction_actions(self, txuuid, public=True):
         """Return an ordered list of action effectued during that transaction.
 
@@ -757,6 +765,7 @@
         return self._repo.transaction_actions(self.sessionid, txuuid, public,
                                               **self._txid())
 
+    @check_not_closed
     def undo_transaction(self, txuuid):
         """Undo the given transaction. Return potential restoration errors.
 
--- a/debian/changelog	Tue Jul 27 12:36:03 2010 +0200
+++ b/debian/changelog	Wed Nov 03 16:38:28 2010 +0100
@@ -1,3 +1,63 @@
+cubicweb (3.9.9-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 21 Oct 2010 09:33:46 +0200
+
+cubicweb (3.9.8-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 23 Sep 2010 18:45:23 +0200
+
+cubicweb (3.9.7-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 16 Sep 2010 15:41:51 +0200
+
+cubicweb (3.9.6-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 13 Sep 2010 10:50:15 +0200
+
+cubicweb (3.9.5-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 26 Aug 2010 10:53:34 +0200
+
+cubicweb (3.9.4-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 02 Aug 2010 14:25:38 +0200
+
+cubicweb (3.9.3-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 27 Jul 2010 16:39:04 +0200
+
+cubicweb (3.9.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 16 Jul 2010 12:40:59 +0200
+
+cubicweb (3.9.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 12 Jul 2010 13:25:10 +0200
+
+cubicweb (3.9.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 07 Jul 2010 13:01:06 +0200
+
 cubicweb (3.8.7-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Tue Jul 27 12:36:03 2010 +0200
+++ b/debian/control	Wed Nov 03 16:38:28 2010 +0100
@@ -10,7 +10,7 @@
 Build-Depends: debhelper (>= 5), python-dev (>=2.5), python-central (>= 0.5)
 Standards-Version: 3.8.0
 Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.5, << 2.6
+XS-Python-Version: >= 2.5, << 2.7
 
 Package: cubicweb
 Architecture: all
@@ -33,8 +33,8 @@
 Conflicts: cubicweb-multisources
 Replaces: cubicweb-multisources
 Provides: cubicweb-multisources
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.0.5), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
-Recommends: pyro, cubicweb-documentation (= ${source:Version})
+Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.3.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Recommends: pyro (< 4.0.0), cubicweb-documentation (= ${source:Version})
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -69,7 +69,7 @@
 XB-Python-Version: ${python:Versions}
 Provides: cubicweb-web-frontend
 Depends: ${python:Depends}, cubicweb-web (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-twisted-web
-Recommends: pyro, cubicweb-documentation (= ${source:Version})
+Recommends: pyro (< 4.0.0), cubicweb-documentation (= ${source:Version})
 Description: twisted-based web interface for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -97,7 +97,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.50.2), python-yams (>= 0.29.0), python-rql (>= 0.26.3), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.51.0), python-yams (>= 0.30.1), python-rql (>= 0.26.3), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/debian/cubicweb-common.install.in	Tue Jul 27 12:36:03 2010 +0200
+++ b/debian/cubicweb-common.install.in	Wed Nov 03 16:38:28 2010 +0100
@@ -1,5 +1,5 @@
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/common/ usr/lib/PY_VERSION/site-packages/cubicweb
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/entities/ usr/lib/PY_VERSION/site-packages/cubicweb
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/ext/ usr/lib/PY_VERSION/site-packages/cubicweb
-debian/tmp/usr/share/cubicweb/cubes/shared/i18n usr/share/cubicweb/cubes/shared/
+debian/tmp/usr/share/cubicweb/cubes/ usr/share/cubicweb/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/*.py usr/share/pyshared/cubicweb
--- a/debian/cubicweb-dev.install.in	Tue Jul 27 12:36:03 2010 +0200
+++ b/debian/cubicweb-dev.install.in	Wed Nov 03 16:38:28 2010 +0100
@@ -8,4 +8,3 @@
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/web/test usr/lib/PY_VERSION/site-packages/cubicweb/web/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/etwist/test usr/lib/PY_VERSION/site-packages/cubicweb/etwist/
-debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/goa/test usr/lib/PY_VERSION/site-packages/cubicweb/goa/
--- a/debian/rules	Tue Jul 27 12:36:03 2010 +0200
+++ b/debian/rules	Wed Nov 03 16:38:28 2010 +0100
@@ -53,8 +53,6 @@
 	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/ext/test
 	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/entities/test
 
-	# cubes directory must be managed as a valid python module
-	touch debian/cubicweb-common/usr/share/cubicweb/cubes/__init__.py
 
 %: %.in
 	sed "s/PY_VERSION/${PY_VERSION}/g" < $< > $@
--- a/devtools/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,12 +15,12 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Test tools for cubicweb
+"""Test tools for cubicweb"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import os
+import sys
 import logging
 from datetime import timedelta
 from os.path import (abspath, join, exists, basename, dirname, normpath, split,
@@ -95,6 +95,7 @@
     set_language = False
     read_instance_schema = False
     init_repository = True
+    db_require_setup = True
     options = cwconfig.merge_options(ServerConfiguration.options + (
         ('anonymous-user',
          {'type' : 'string',
@@ -110,7 +111,14 @@
           }),
         ))
 
-    def __init__(self, appid, log_threshold=logging.CRITICAL+10):
+    def __init__(self, appid, apphome=None, log_threshold=logging.CRITICAL+10):
+        # must be set before calling parent __init__
+        if apphome is None:
+            if exists(appid):
+                apphome = abspath(appid)
+            else: # cube test
+                apphome = abspath('..')
+        self._apphome = apphome
         ServerConfiguration.__init__(self, appid)
         self.init_log(log_threshold, force=True)
         # need this, usually triggered by cubicweb-ctl
@@ -120,10 +128,7 @@
 
     @property
     def apphome(self):
-        if exists(self.appid):
-            return abspath(self.appid)
-        # cube test
-        return abspath('..')
+        return self._apphome
     appdatahome = apphome
 
     def load_configuration(self):
@@ -137,9 +142,6 @@
         """return instance's control configuration file"""
         return join(self.apphome, '%s.conf' % self.name)
 
-    def instance_md5_version(self):
-        return ''
-
     def bootstrap_cubes(self):
         try:
             super(TestServerConfiguration, self).bootstrap_cubes()
@@ -170,6 +172,15 @@
             sources = DEFAULT_SOURCES
         return sources
 
+    # web config methods needed here for cases when we use this config as a web
+    # config
+
+    def instance_md5_version(self):
+        return ''
+
+    def default_base_url(self):
+        return BASE_URL
+
 
 class BaseApptestConfiguration(TestServerConfiguration, TwistedConfiguration):
     repo_method = 'inmemory'
@@ -181,23 +192,45 @@
     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
 
-
+# XXX merge with BaseApptestConfiguration ?
 class ApptestConfiguration(BaseApptestConfiguration):
 
-    def __init__(self, appid, log_threshold=logging.CRITICAL, sourcefile=None):
-        BaseApptestConfiguration.__init__(self, appid, log_threshold=log_threshold)
+    def __init__(self, appid, apphome=None,
+                 log_threshold=logging.CRITICAL, sourcefile=None):
+        BaseApptestConfiguration.__init__(self, appid, apphome,
+                                          log_threshold=log_threshold)
         self.init_repository = sourcefile is None
         self.sourcefile = sourcefile
 
 
+class RealDatabaseConfiguration(ApptestConfiguration):
+    """configuration class for tests to run on a real database.
+
+    The intialization is done by specifying a source file path.
+
+    Important note: init_test_database / reset_test_database steps are
+    skipped. It's thus up to the test developer to implement setUp/tearDown
+    accordingly.
+
+    Example usage::
+
+      class MyTests(CubicWebTC):
+          _config = RealDatabseConfiguration('myapp',
+                                             sourcefile='/path/to/sources')
+          def test_something(self):
+              rset = self.execute('Any X WHERE X is CWUser')
+              self.view('foaf', rset)
+
+    """
+    db_require_setup = False    # skip init_db / reset_db steps
+    read_instance_schema = True # read schema from database
+
+
 # test database handling #######################################################
 
 def init_test_database(config=None, configdir='data'):
@@ -206,14 +239,13 @@
     config = config or TestServerConfiguration(configdir)
     sources = config.sources()
     driver = sources['system']['db-driver']
-    if driver == 'sqlite':
-        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)
+    if config.db_require_setup:
+        if driver == 'sqlite':
+            init_test_database_sqlite(config)
+        elif driver == 'postgres':
+            init_test_database_postgres(config)
+        else:
+            raise ValueError('no initialization function for driver %r' % driver)
     config._cubes = None # avoid assertion error
     repo, cnx = in_memory_cnx(config, unicode(sources['admin']['login']),
                               password=sources['admin']['password'] or 'xxx')
@@ -221,16 +253,15 @@
         install_sqlite_patch(repo.querier)
     return repo, cnx
 
-
 def reset_test_database(config):
     """init a test database for a specific driver"""
+    if not config.db_require_setup:
+        return
     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 +270,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"""
@@ -286,7 +352,6 @@
         dbfile = config.sources()['system']['db-name']
         shutil.copy(dbfile, '%s-template' % dbfile)
 
-
 def install_sqlite_patch(querier):
     """This patch hotfixes the following sqlite bug :
        - http://www.sqlite.org/cvstrac/tktview?tn=1327,33
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/cwwindmill.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,148 @@
+# 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
+
+:todo:
+
+    * import CubicWeb session object into windmill scope to be able to run RQL
+    * manage command line option from pytest to run specific use tests only
+"""
+
+
+import os, os.path as osp
+from logging import getLogger, ERROR
+import sys
+
+# imported by default to simplify further import statements
+from logilab.common.testlib import TestCase, unittest_main
+
+import windmill
+from windmill.dep import functest
+from windmill.bin.admin_lib import configure_global_settings, setup, teardown
+
+from cubicweb.devtools.httptest import CubicWebServerTC, CubicWebServerConfig
+
+
+# Excerpt from :ref:`windmill.authoring.unit`
+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):
+    """basic class for Windmill use case tests
+
+    If you want to change cubicweb test server parameters, define a new
+    :class:`CubicWebServerConfig` and override the :var:`configcls`
+    attribute:
+
+        configcls = CubicWebServerConfig
+
+    From Windmill configuration:
+
+    .. attribute:: browser
+        identification string (firefox|ie|safari|chrome) (firefox by default)
+    .. attribute :: edit_test
+        load and edit test for debugging (False by default)
+    .. attribute:: test_dir (optional)
+        testing file path or directory (windmill directory under your unit case
+        file by default)
+
+    Examples:
+
+        browser = 'firefox'
+        test_dir = osp.join(__file__, 'windmill')
+        edit_test = False
+
+    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__
+
+    Instead of toggle `edit_test` value, try `pytest -i`
+    """
+    browser = 'firefox'
+    edit_test = "-i" in sys.argv # detection for pytest invocation
+    # Windmill use case are written with no anonymous user
+    anonymous_logged = False
+
+    def _test_dir(self):
+        """access to class attribute if possible or make assumption
+        of expected directory"""
+        try:
+            return getattr(self, 'test_dir')
+        except AttributeError:
+            if os.path.basename(sys.argv[0]) == "pytest":
+                test_dir = os.getcwd()
+            else:
+                import inspect
+                test_dir = os.path.dirname(inspect.stack()[-1][1])
+            return osp.join(test_dir, 'windmill')
+
+    def setUp(self):
+        # Start CubicWeb session before running the server to populate self.vreg
+        CubicWebServerTC.setUp(self)
+        # XXX reduce log output (should be done in a cleaner way)
+        # windmill fu** up our logging configuration
+        for logkey in ('windmill', 'logilab', 'cubicweb'):
+            getLogger(logkey).setLevel(ERROR)
+        self.test_dir = self._test_dir()
+        msg = "provide a valid 'test_dir' as the given test file/dir (current: %s)"
+        assert os.path.exists(self.test_dir), (msg % self.test_dir)
+        # windmill setup
+        windmill.stdout, windmill.stdin = sys.stdout, sys.stdin
+        configure_global_settings()
+        windmill.settings['TEST_URL'] = self.config['base-url']
+        if hasattr(self,"windmill_settings"):
+            for (setting,value) in self.windmill_settings.iteritems():
+                windmill.settings[setting] = value
+        self.windmill_shell_objects = setup()
+
+    def tearDown(self):
+        teardown(self.windmill_shell_objects)
+        CubicWebServerTC.tearDown(self)
+
+    def testWindmill(self):
+        if self.edit_test:
+            # see windmill.bin.admin_options.Firebug
+            windmill.settings['INSTALL_FIREBUG'] = 'firebug'
+            windmill.settings.setdefault('MOZILLA_PLUGINS', []).extend(
+                ['/usr/share/mozilla-extensions/',
+                 '/usr/share/xul-ext/'])
+        controller = self.windmill_shell_objects['start_' + self.browser]()
+        self.windmill_shell_objects['do_test'](self.test_dir,
+                                               load=self.edit_test,
+                                               threaded=False)
+        # set a breakpoint to be able to debug windmill test
+        if self.edit_test:
+            import pdb; pdb.set_trace()
+            return
+
+        # reporter
+        for test in unittestreporter.test_list:
+            msg = ""
+            self._testMethodDoc = getattr(test, "__doc__", None)
+            self._testMethodName = test.__name__
+            # try to display a better message in case of failure
+            if hasattr(test, "tb"):
+                msg = '\n'.join(test.tb)
+            self.assertEqual(test.result, True, msg=msg)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/cwmock.js	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,11 @@
+/*
+ * cubicweb js mock for unit tests
+ * This module defines variables and functions used in quite a few places
+ * in cw js framework that can't be used or guessed without a real CW server
+ */
+
+var pageid = 'my-page-id';
+
+function _(message) {
+    return message;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/qunit.css	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,119 @@
+
+ol#qunit-tests {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	margin:0;
+	padding:0;
+	list-style-position:inside;
+
+	font-size: smaller;
+}
+ol#qunit-tests li{
+	padding:0.4em 0.5em 0.4em 2.5em;
+	border-bottom:1px solid #fff;
+	font-size:small;
+	list-style-position:inside;
+}
+ol#qunit-tests li ol{
+	box-shadow: inset 0px 2px 13px #999;
+	-moz-box-shadow: inset 0px 2px 13px #999;
+	-webkit-box-shadow: inset 0px 2px 13px #999;
+	margin-top:0.5em;
+	margin-left:0;
+	padding:0.5em;
+	background-color:#fff;
+	border-radius:15px;
+	-moz-border-radius: 15px;
+	-webkit-border-radius: 15px;
+}
+ol#qunit-tests li li{
+	border-bottom:none;
+	margin:0.5em;
+	background-color:#fff;
+	list-style-position: inside;
+	padding:0.4em 0.5em 0.4em 0.5em;
+}
+
+ol#qunit-tests li li.pass{
+	border-left:26px solid #C6E746;
+	background-color:#fff;
+	color:#5E740B;
+	}
+ol#qunit-tests li li.fail{
+	border-left:26px solid #EE5757;
+	background-color:#fff;
+	color:#710909;
+}
+ol#qunit-tests li.pass{
+	background-color:#D2E0E6;
+	color:#528CE0;
+}
+ol#qunit-tests li.fail{
+	background-color:#EE5757;
+	color:#000;
+}
+ol#qunit-tests li strong {
+	cursor:pointer;
+}
+h1#qunit-header{
+	background-color:#0d3349;
+	margin:0;
+	padding:0.5em 0 0.5em 1em;
+	color:#fff;
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	border-top-right-radius:15px;
+	border-top-left-radius:15px;
+	-moz-border-radius-topright:15px;
+	-moz-border-radius-topleft:15px;
+	-webkit-border-top-right-radius:15px;
+	-webkit-border-top-left-radius:15px;
+	text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px;
+}
+h2#qunit-banner{
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	height:5px;
+	margin:0;
+	padding:0;
+}
+h2#qunit-banner.qunit-pass{
+	background-color:#C6E746;
+}
+h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar {
+	background-color:#EE5757;
+}
+#qunit-testrunner-toolbar {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	padding:0;
+	/*width:80%;*/
+	padding:0em 0 0.5em 2em;
+	font-size: small;
+}
+h2#qunit-userAgent {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	background-color:#2b81af;
+	margin:0;
+	padding:0;
+	color:#fff;
+	font-size: small;
+	padding:0.5em 0 0.5em 2.5em;
+	text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+p#qunit-testresult{
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	margin:0;
+	font-size: small;
+	color:#2b81af;
+	border-bottom-right-radius:15px;
+	border-bottom-left-radius:15px;
+	-moz-border-radius-bottomright:15px;
+	-moz-border-radius-bottomleft:15px;
+	-webkit-border-bottom-right-radius:15px;
+	-webkit-border-bottom-left-radius:15px;
+	background-color:#D2E0E6;
+	padding:0.5em 0.5em 0.5em 2.5em;
+}
+strong b.fail{
+	color:#710909;
+	}
+strong b.pass{
+	color:#5E740B;
+	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/qunit.js	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1069 @@
+/*
+ * QUnit - A JavaScript Unit Testing Framework
+ * 
+ * http://docs.jquery.com/QUnit
+ *
+ * Copyright (c) 2009 John Resig, Jörn Zaefferer
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ */
+
+(function(window) {
+
+var QUnit = {
+
+	// Initialize the configuration options
+	init: function() {
+		config = {
+			stats: { all: 0, bad: 0 },
+			moduleStats: { all: 0, bad: 0 },
+			started: +new Date,
+			updateRate: 1000,
+			blocking: false,
+			autorun: false,
+			assertions: [],
+			filters: [],
+			queue: []
+		};
+
+		var tests = id("qunit-tests"),
+			banner = id("qunit-banner"),
+			result = id("qunit-testresult");
+
+		if ( tests ) {
+			tests.innerHTML = "";
+		}
+
+		if ( banner ) {
+			banner.className = "";
+		}
+
+		if ( result ) {
+			result.parentNode.removeChild( result );
+		}
+	},
+	
+	// call on start of module test to prepend name to all tests
+	module: function(name, testEnvironment) {
+		config.currentModule = name;
+
+		synchronize(function() {
+			if ( config.currentModule ) {
+				QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+			}
+
+			config.currentModule = name;
+			config.moduleTestEnvironment = testEnvironment;
+			config.moduleStats = { all: 0, bad: 0 };
+
+			QUnit.moduleStart( name, testEnvironment );
+		});
+	},
+
+	asyncTest: function(testName, expected, callback) {
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = 0;
+		}
+
+		QUnit.test(testName, expected, callback, true);
+	},
+	
+	test: function(testName, expected, callback, async) {
+		var name = testName, testEnvironment, testEnvironmentArg;
+
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = null;
+		}
+		// is 2nd argument a testEnvironment?
+		if ( expected && typeof expected === 'object') {
+			testEnvironmentArg =  expected;
+			expected = null;
+		}
+
+		if ( config.currentModule ) {
+			name = config.currentModule + " module: " + name;
+		}
+
+		if ( !validTest(name) ) {
+			return;
+		}
+
+		synchronize(function() {
+			QUnit.testStart( testName );
+
+			testEnvironment = extend({
+				setup: function() {},
+				teardown: function() {}
+			}, config.moduleTestEnvironment);
+			if (testEnvironmentArg) {
+				extend(testEnvironment,testEnvironmentArg);
+			}
+
+			// allow utility functions to access the current test environment
+			QUnit.current_testEnvironment = testEnvironment;
+			
+			config.assertions = [];
+			config.expected = expected;
+
+			try {
+				if ( !config.pollution ) {
+					saveGlobal();
+				}
+
+				testEnvironment.setup.call(testEnvironment);
+			} catch(e) {
+				QUnit.ok( false, "Setup failed on " + name + ": " + e.message );
+			}
+
+			if ( async ) {
+				QUnit.stop();
+			}
+
+			try {
+				callback.call(testEnvironment);
+			} catch(e) {
+				fail("Test " + name + " died, exception and test follows", e, callback);
+				QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message );
+				// else next test will carry the responsibility
+				saveGlobal();
+
+				// Restart the tests if they're blocking
+				if ( config.blocking ) {
+					start();
+				}
+			}
+		});
+
+		synchronize(function() {
+			try {
+				checkPollution();
+				testEnvironment.teardown.call(testEnvironment);
+			} catch(e) {
+				QUnit.ok( false, "Teardown failed on " + name + ": " + e.message );
+			}
+
+			try {
+				QUnit.reset();
+			} catch(e) {
+				fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset);
+			}
+
+			if ( config.expected && config.expected != config.assertions.length ) {
+				QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" );
+			}
+
+			var good = 0, bad = 0,
+				tests = id("qunit-tests");
+
+			config.stats.all += config.assertions.length;
+			config.moduleStats.all += config.assertions.length;
+
+			if ( tests ) {
+				var ol  = document.createElement("ol");
+				ol.style.display = "none";
+
+				for ( var i = 0; i < config.assertions.length; i++ ) {
+					var assertion = config.assertions[i];
+
+					var li = document.createElement("li");
+					li.className = assertion.result ? "pass" : "fail";
+					li.appendChild(document.createTextNode(assertion.message || "(no message)"));
+					ol.appendChild( li );
+
+					if ( assertion.result ) {
+						good++;
+					} else {
+						bad++;
+						config.stats.bad++;
+						config.moduleStats.bad++;
+					}
+				}
+
+				var b = document.createElement("strong");
+				b.innerHTML = name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>";
+				
+				addEvent(b, "click", function() {
+					var next = b.nextSibling, display = next.style.display;
+					next.style.display = display === "none" ? "block" : "none";
+				});
+				
+				addEvent(b, "dblclick", function(e) {
+					var target = e && e.target ? e.target : window.event.srcElement;
+					if ( target.nodeName.toLowerCase() === "strong" ) {
+						var text = "", node = target.firstChild;
+
+						while ( node.nodeType === 3 ) {
+							text += node.nodeValue;
+							node = node.nextSibling;
+						}
+
+						text = text.replace(/(^\s*|\s*$)/g, "");
+
+						if ( window.location ) {
+							window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text);
+						}
+					}
+				});
+
+				var li = document.createElement("li");
+				li.className = bad ? "fail" : "pass";
+				li.appendChild( b );
+				li.appendChild( ol );
+				tests.appendChild( li );
+
+				if ( bad ) {
+					var toolbar = id("qunit-testrunner-toolbar");
+					if ( toolbar ) {
+						toolbar.style.display = "block";
+						id("qunit-filter-pass").disabled = null;
+						id("qunit-filter-missing").disabled = null;
+					}
+				}
+
+			} else {
+				for ( var i = 0; i < config.assertions.length; i++ ) {
+					if ( !config.assertions[i].result ) {
+						bad++;
+						config.stats.bad++;
+						config.moduleStats.bad++;
+					}
+				}
+			}
+
+			QUnit.testDone( testName, bad, config.assertions.length );
+
+			if ( !window.setTimeout && !config.queue.length ) {
+				done();
+			}
+		});
+
+		if ( window.setTimeout && !config.doneTimer ) {
+			config.doneTimer = window.setTimeout(function(){
+				if ( !config.queue.length ) {
+					done();
+				} else {
+					synchronize( done );
+				}
+			}, 13);
+		}
+	},
+	
+	/**
+	 * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
+	 */
+	expect: function(asserts) {
+		config.expected = asserts;
+	},
+
+	/**
+	 * Asserts true.
+	 * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
+	 */
+	ok: function(a, msg) {
+		QUnit.log(a, msg);
+
+		config.assertions.push({
+			result: !!a,
+			message: msg
+		});
+	},
+
+	/**
+	 * Checks that the first two arguments are equal, with an optional message.
+	 * Prints out both actual and expected values.
+	 *
+	 * Prefered to ok( actual == expected, message )
+	 *
+	 * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
+	 *
+	 * @param Object actual
+	 * @param Object expected
+	 * @param String message (optional)
+	 */
+	equal: function(actual, expected, message) {
+		push(expected == actual, actual, expected, message);
+	},
+
+	notEqual: function(actual, expected, message) {
+		push(expected != actual, actual, expected, message);
+	},
+	
+	deepEqual: function(a, b, message) {
+		push(QUnit.equiv(a, b), a, b, message);
+	},
+
+	notDeepEqual: function(a, b, message) {
+		push(!QUnit.equiv(a, b), a, b, message);
+	},
+
+	strictEqual: function(actual, expected, message) {
+		push(expected === actual, actual, expected, message);
+	},
+
+	notStrictEqual: function(actual, expected, message) {
+		push(expected !== actual, actual, expected, message);
+	},
+	
+	start: function() {
+		// A slight delay, to avoid any current callbacks
+		if ( window.setTimeout ) {
+			window.setTimeout(function() {
+				if ( config.timeout ) {
+					clearTimeout(config.timeout);
+				}
+
+				config.blocking = false;
+				process();
+			}, 13);
+		} else {
+			config.blocking = false;
+			process();
+		}
+	},
+	
+	stop: function(timeout) {
+		config.blocking = true;
+
+		if ( timeout && window.setTimeout ) {
+			config.timeout = window.setTimeout(function() {
+				QUnit.ok( false, "Test timed out" );
+				QUnit.start();
+			}, timeout);
+		}
+	},
+	
+	/**
+	 * Resets the test setup. Useful for tests that modify the DOM.
+	 */
+	reset: function() {
+		if ( window.jQuery ) {
+			jQuery("#main").html( config.fixture );
+			jQuery.event.global = {};
+			jQuery.ajaxSettings = extend({}, config.ajaxSettings);
+		}
+	},
+	
+	/**
+	 * Trigger an event on an element.
+	 *
+	 * @example triggerEvent( document.body, "click" );
+	 *
+	 * @param DOMElement elem
+	 * @param String type
+	 */
+	triggerEvent: function( elem, type, event ) {
+		if ( document.createEvent ) {
+			event = document.createEvent("MouseEvents");
+			event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
+				0, 0, 0, 0, 0, false, false, false, false, 0, null);
+			elem.dispatchEvent( event );
+
+		} else if ( elem.fireEvent ) {
+			elem.fireEvent("on"+type);
+		}
+	},
+	
+	// Safe object type checking
+	is: function( type, obj ) {
+		return Object.prototype.toString.call( obj ) === "[object "+ type +"]";
+	},
+	
+	// Logging callbacks
+	done: function(failures, total) {},
+	log: function(result, message) {},
+	testStart: function(name) {},
+	testDone: function(name, failures, total) {},
+	moduleStart: function(name, testEnvironment) {},
+	moduleDone: function(name, failures, total) {}
+};
+
+// Backwards compatibility, deprecated
+QUnit.equals = QUnit.equal;
+QUnit.same = QUnit.deepEqual;
+
+// Maintain internal state
+var config = {
+	// The queue of tests to run
+	queue: [],
+
+	// block until document ready
+	blocking: true
+};
+
+// Load paramaters
+(function() {
+	var location = window.location || { search: "", protocol: "file:" },
+		GETParams = location.search.slice(1).split('&');
+
+	for ( var i = 0; i < GETParams.length; i++ ) {
+		GETParams[i] = decodeURIComponent( GETParams[i] );
+		if ( GETParams[i] === "noglobals" ) {
+			GETParams.splice( i, 1 );
+			i--;
+			config.noglobals = true;
+		} else if ( GETParams[i].search('=') > -1 ) {
+			GETParams.splice( i, 1 );
+			i--;
+		}
+	}
+	
+	// restrict modules/tests by get parameters
+	config.filters = GETParams;
+	
+	// Figure out if we're running the tests from a server or not
+	QUnit.isLocal = !!(location.protocol === 'file:');
+})();
+
+// Expose the API as global variables, unless an 'exports'
+// object exists, in that case we assume we're in CommonJS
+if ( typeof exports === "undefined" || typeof require === "undefined" ) {
+	extend(window, QUnit);
+	window.QUnit = QUnit;
+} else {
+	extend(exports, QUnit);
+	exports.QUnit = QUnit;
+}
+
+if ( typeof document === "undefined" || document.readyState === "complete" ) {
+	config.autorun = true;
+}
+
+addEvent(window, "load", function() {
+	// Initialize the config, saving the execution queue
+	var oldconfig = extend({}, config);
+	QUnit.init();
+	extend(config, oldconfig);
+
+	config.blocking = false;
+
+	var userAgent = id("qunit-userAgent");
+	if ( userAgent ) {
+		userAgent.innerHTML = navigator.userAgent;
+	}
+	
+	var toolbar = id("qunit-testrunner-toolbar");
+	if ( toolbar ) {
+		toolbar.style.display = "none";
+		
+		var filter = document.createElement("input");
+		filter.type = "checkbox";
+		filter.id = "qunit-filter-pass";
+		filter.disabled = true;
+		addEvent( filter, "click", function() {
+			var li = document.getElementsByTagName("li");
+			for ( var i = 0; i < li.length; i++ ) {
+				if ( li[i].className.indexOf("pass") > -1 ) {
+					li[i].style.display = filter.checked ? "none" : "";
+				}
+			}
+		});
+		toolbar.appendChild( filter );
+
+		var label = document.createElement("label");
+		label.setAttribute("for", "qunit-filter-pass");
+		label.innerHTML = "Hide passed tests";
+		toolbar.appendChild( label );
+
+		var missing = document.createElement("input");
+		missing.type = "checkbox";
+		missing.id = "qunit-filter-missing";
+		missing.disabled = true;
+		addEvent( missing, "click", function() {
+			var li = document.getElementsByTagName("li");
+			for ( var i = 0; i < li.length; i++ ) {
+				if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) {
+					li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block";
+				}
+			}
+		});
+		toolbar.appendChild( missing );
+
+		label = document.createElement("label");
+		label.setAttribute("for", "qunit-filter-missing");
+		label.innerHTML = "Hide missing tests (untested code is broken code)";
+		toolbar.appendChild( label );
+	}
+
+	var main = id('main');
+	if ( main ) {
+		config.fixture = main.innerHTML;
+	}
+
+	if ( window.jQuery ) {
+		config.ajaxSettings = window.jQuery.ajaxSettings;
+	}
+
+	QUnit.start();
+});
+
+function done() {
+	if ( config.doneTimer && window.clearTimeout ) {
+		window.clearTimeout( config.doneTimer );
+		config.doneTimer = null;
+	}
+
+	if ( config.queue.length ) {
+		config.doneTimer = window.setTimeout(function(){
+			if ( !config.queue.length ) {
+				done();
+			} else {
+				synchronize( done );
+			}
+		}, 13);
+
+		return;
+	}
+
+	config.autorun = true;
+
+	// Log the last module results
+	if ( config.currentModule ) {
+		QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+	}
+
+	var banner = id("qunit-banner"),
+		tests = id("qunit-tests"),
+		html = ['Tests completed in ',
+		+new Date - config.started, ' milliseconds.<br/>',
+		'<span class="passed">', config.stats.all - config.stats.bad, '</span> tests of <span class="total">', config.stats.all, '</span> passed, <span class="failed">', config.stats.bad,'</span> failed.'].join('');
+
+	if ( banner ) {
+		banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
+	}
+
+	if ( tests ) {	
+		var result = id("qunit-testresult");
+
+		if ( !result ) {
+			result = document.createElement("p");
+			result.id = "qunit-testresult";
+			result.className = "result";
+			tests.parentNode.insertBefore( result, tests.nextSibling );
+		}
+
+		result.innerHTML = html;
+	}
+
+	QUnit.done( config.stats.bad, config.stats.all );
+}
+
+function validTest( name ) {
+	var i = config.filters.length,
+		run = false;
+
+	if ( !i ) {
+		return true;
+	}
+	
+	while ( i-- ) {
+		var filter = config.filters[i],
+			not = filter.charAt(0) == '!';
+
+		if ( not ) {
+			filter = filter.slice(1);
+		}
+
+		if ( name.indexOf(filter) !== -1 ) {
+			return !not;
+		}
+
+		if ( not ) {
+			run = true;
+		}
+	}
+
+	return run;
+}
+
+function push(result, actual, expected, message) {
+	message = message || (result ? "okay" : "failed");
+	QUnit.ok( result, result ? message + ": " + QUnit.jsDump.parse(expected) : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) );
+}
+
+function synchronize( callback ) {
+	config.queue.push( callback );
+
+	if ( config.autorun && !config.blocking ) {
+		process();
+	}
+}
+
+function process() {
+	var start = (new Date()).getTime();
+
+	while ( config.queue.length && !config.blocking ) {
+		if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
+			config.queue.shift()();
+
+		} else {
+			setTimeout( process, 13 );
+			break;
+		}
+	}
+}
+
+function saveGlobal() {
+	config.pollution = [];
+	
+	if ( config.noglobals ) {
+		for ( var key in window ) {
+			config.pollution.push( key );
+		}
+	}
+}
+
+function checkPollution( name ) {
+	var old = config.pollution;
+	saveGlobal();
+	
+	var newGlobals = diff( old, config.pollution );
+	if ( newGlobals.length > 0 ) {
+		ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
+		config.expected++;
+	}
+
+	var deletedGlobals = diff( config.pollution, old );
+	if ( deletedGlobals.length > 0 ) {
+		ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
+		config.expected++;
+	}
+}
+
+// returns a new Array with the elements that are in a but not in b
+function diff( a, b ) {
+	var result = a.slice();
+	for ( var i = 0; i < result.length; i++ ) {
+		for ( var j = 0; j < b.length; j++ ) {
+			if ( result[i] === b[j] ) {
+				result.splice(i, 1);
+				i--;
+				break;
+			}
+		}
+	}
+	return result;
+}
+
+function fail(message, exception, callback) {
+	if ( typeof console !== "undefined" && console.error && console.warn ) {
+		console.error(message);
+		console.error(exception);
+		console.warn(callback.toString());
+
+	} else if ( window.opera && opera.postError ) {
+		opera.postError(message, exception, callback.toString);
+	}
+}
+
+function extend(a, b) {
+	for ( var prop in b ) {
+		a[prop] = b[prop];
+	}
+
+	return a;
+}
+
+function addEvent(elem, type, fn) {
+	if ( elem.addEventListener ) {
+		elem.addEventListener( type, fn, false );
+	} else if ( elem.attachEvent ) {
+		elem.attachEvent( "on" + type, fn );
+	} else {
+		fn();
+	}
+}
+
+function id(name) {
+	return !!(typeof document !== "undefined" && document && document.getElementById) &&
+		document.getElementById( name );
+}
+
+// Test for equality any JavaScript type.
+// Discussions and reference: http://philrathe.com/articles/equiv
+// Test suites: http://philrathe.com/tests/equiv
+// Author: Philippe Rathé <prathe@gmail.com>
+QUnit.equiv = function () {
+
+    var innerEquiv; // the real equiv function
+    var callers = []; // stack to decide between skip/abort functions
+    var parents = []; // stack to avoiding loops from circular referencing
+
+
+    // Determine what is o.
+    function hoozit(o) {
+        if (QUnit.is("String", o)) {
+            return "string";
+            
+        } else if (QUnit.is("Boolean", o)) {
+            return "boolean";
+
+        } else if (QUnit.is("Number", o)) {
+
+            if (isNaN(o)) {
+                return "nan";
+            } else {
+                return "number";
+            }
+
+        } else if (typeof o === "undefined") {
+            return "undefined";
+
+        // consider: typeof null === object
+        } else if (o === null) {
+            return "null";
+
+        // consider: typeof [] === object
+        } else if (QUnit.is( "Array", o)) {
+            return "array";
+        
+        // consider: typeof new Date() === object
+        } else if (QUnit.is( "Date", o)) {
+            return "date";
+
+        // consider: /./ instanceof Object;
+        //           /./ instanceof RegExp;
+        //          typeof /./ === "function"; // => false in IE and Opera,
+        //                                          true in FF and Safari
+        } else if (QUnit.is( "RegExp", o)) {
+            return "regexp";
+
+        } else if (typeof o === "object") {
+            return "object";
+
+        } else if (QUnit.is( "Function", o)) {
+            return "function";
+        } else {
+            return undefined;
+        }
+    }
+
+    // Call the o related callback with the given arguments.
+    function bindCallbacks(o, callbacks, args) {
+        var prop = hoozit(o);
+        if (prop) {
+            if (hoozit(callbacks[prop]) === "function") {
+                return callbacks[prop].apply(callbacks, args);
+            } else {
+                return callbacks[prop]; // or undefined
+            }
+        }
+    }
+    
+    var callbacks = function () {
+
+        // for string, boolean, number and null
+        function useStrictEquality(b, a) {
+            if (b instanceof a.constructor || a instanceof b.constructor) {
+                // to catch short annotaion VS 'new' annotation of a declaration
+                // e.g. var i = 1;
+                //      var j = new Number(1);
+                return a == b;
+            } else {
+                return a === b;
+            }
+        }
+
+        return {
+            "string": useStrictEquality,
+            "boolean": useStrictEquality,
+            "number": useStrictEquality,
+            "null": useStrictEquality,
+            "undefined": useStrictEquality,
+
+            "nan": function (b) {
+                return isNaN(b);
+            },
+
+            "date": function (b, a) {
+                return hoozit(b) === "date" && a.valueOf() === b.valueOf();
+            },
+
+            "regexp": function (b, a) {
+                return hoozit(b) === "regexp" &&
+                    a.source === b.source && // the regex itself
+                    a.global === b.global && // and its modifers (gmi) ...
+                    a.ignoreCase === b.ignoreCase &&
+                    a.multiline === b.multiline;
+            },
+
+            // - skip when the property is a method of an instance (OOP)
+            // - abort otherwise,
+            //   initial === would have catch identical references anyway
+            "function": function () {
+                var caller = callers[callers.length - 1];
+                return caller !== Object &&
+                        typeof caller !== "undefined";
+            },
+
+            "array": function (b, a) {
+                var i, j, loop;
+                var len;
+
+                // b could be an object literal here
+                if ( ! (hoozit(b) === "array")) {
+                    return false;
+                }   
+                
+                len = a.length;
+                if (len !== b.length) { // safe and faster
+                    return false;
+                }
+                
+                //track reference to avoid circular references
+                parents.push(a);
+                for (i = 0; i < len; i++) {
+                    loop = false;
+                    for(j=0;j<parents.length;j++){
+                        if(parents[j] === a[i]){
+                            loop = true;//dont rewalk array
+                        }
+                    }
+                    if (!loop && ! innerEquiv(a[i], b[i])) {
+                        parents.pop();
+                        return false;
+                    }
+                }
+                parents.pop();
+                return true;
+            },
+
+            "object": function (b, a) {
+                var i, j, loop;
+                var eq = true; // unless we can proove it
+                var aProperties = [], bProperties = []; // collection of strings
+
+                // comparing constructors is more strict than using instanceof
+                if ( a.constructor !== b.constructor) {
+                    return false;
+                }
+
+                // stack constructor before traversing properties
+                callers.push(a.constructor);
+                //track reference to avoid circular references
+                parents.push(a);
+                
+                for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
+                    loop = false;
+                    for(j=0;j<parents.length;j++){
+                        if(parents[j] === a[i])
+                            loop = true; //don't go down the same path twice
+                    }
+                    aProperties.push(i); // collect a's properties
+
+                    if (!loop && ! innerEquiv(a[i], b[i])) {
+                        eq = false;
+                        break;
+                    }
+                }
+
+                callers.pop(); // unstack, we are done
+                parents.pop();
+
+                for (i in b) {
+                    bProperties.push(i); // collect b's properties
+                }
+
+                // Ensures identical properties name
+                return eq && innerEquiv(aProperties.sort(), bProperties.sort());
+            }
+        };
+    }();
+
+    innerEquiv = function () { // can take multiple arguments
+        var args = Array.prototype.slice.apply(arguments);
+        if (args.length < 2) {
+            return true; // end transition
+        }
+
+        return (function (a, b) {
+            if (a === b) {
+                return true; // catch the most you can
+            } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) {
+                return false; // don't lose time with error prone cases
+            } else {
+                return bindCallbacks(a, callbacks, [b, a]);
+            }
+
+        // apply transition with (1..n) arguments
+        })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
+    };
+
+    return innerEquiv;
+
+}();
+
+/**
+ * jsDump
+ * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
+ * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
+ * Date: 5/15/2008
+ * @projectDescription Advanced and extensible data dumping for Javascript.
+ * @version 1.0.0
+ * @author Ariel Flesler
+ * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ */
+QUnit.jsDump = (function() {
+	function quote( str ) {
+		return '"' + str.toString().replace(/"/g, '\\"') + '"';
+	};
+	function literal( o ) {
+		return o + '';	
+	};
+	function join( pre, arr, post ) {
+		var s = jsDump.separator(),
+			base = jsDump.indent(),
+			inner = jsDump.indent(1);
+		if ( arr.join )
+			arr = arr.join( ',' + s + inner );
+		if ( !arr )
+			return pre + post;
+		return [ pre, inner + arr, base + post ].join(s);
+	};
+	function array( arr ) {
+		var i = arr.length,	ret = Array(i);					
+		this.up();
+		while ( i-- )
+			ret[i] = this.parse( arr[i] );				
+		this.down();
+		return join( '[', ret, ']' );
+	};
+	
+	var reName = /^function (\w+)/;
+	
+	var jsDump = {
+		parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance
+			var	parser = this.parsers[ type || this.typeOf(obj) ];
+			type = typeof parser;			
+			
+			return type == 'function' ? parser.call( this, obj ) :
+				   type == 'string' ? parser :
+				   this.parsers.error;
+		},
+		typeOf:function( obj ) {
+			var type;
+			if ( obj === null ) {
+				type = "null";
+			} else if (typeof obj === "undefined") {
+				type = "undefined";
+			} else if (QUnit.is("RegExp", obj)) {
+				type = "regexp";
+			} else if (QUnit.is("Date", obj)) {
+				type = "date";
+			} else if (QUnit.is("Function", obj)) {
+				type = "function";
+			} else if (obj.setInterval && obj.document && !obj.nodeType) {
+				type = "window";
+			} else if (obj.nodeType === 9) {
+				type = "document";
+			} else if (obj.nodeType) {
+				type = "node";
+			} else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) {
+				type = "array";
+			} else {
+				type = typeof obj;
+			}
+			return type;
+		},
+		separator:function() {
+			return this.multiline ?	this.HTML ? '<br />' : '\n' : this.HTML ? '&nbsp;' : ' ';
+		},
+		indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
+			if ( !this.multiline )
+				return '';
+			var chr = this.indentChar;
+			if ( this.HTML )
+				chr = chr.replace(/\t/g,'   ').replace(/ /g,'&nbsp;');
+			return Array( this._depth_ + (extra||0) ).join(chr);
+		},
+		up:function( a ) {
+			this._depth_ += a || 1;
+		},
+		down:function( a ) {
+			this._depth_ -= a || 1;
+		},
+		setParser:function( name, parser ) {
+			this.parsers[name] = parser;
+		},
+		// The next 3 are exposed so you can use them
+		quote:quote, 
+		literal:literal,
+		join:join,
+		//
+		_depth_: 1,
+		// This is the list of parsers, to modify them, use jsDump.setParser
+		parsers:{
+			window: '[Window]',
+			document: '[Document]',
+			error:'[ERROR]', //when no parser is found, shouldn't happen
+			unknown: '[Unknown]',
+			'null':'null',
+			undefined:'undefined',
+			'function':function( fn ) {
+				var ret = 'function',
+					name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
+				if ( name )
+					ret += ' ' + name;
+				ret += '(';
+				
+				ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
+				return join( ret, this.parse(fn,'functionCode'), '}' );
+			},
+			array: array,
+			nodelist: array,
+			arguments: array,
+			object:function( map ) {
+				var ret = [ ];
+				this.up();
+				for ( var key in map )
+					ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
+				this.down();
+				return join( '{', ret, '}' );
+			},
+			node:function( node ) {
+				var open = this.HTML ? '&lt;' : '<',
+					close = this.HTML ? '&gt;' : '>';
+					
+				var tag = node.nodeName.toLowerCase(),
+					ret = open + tag;
+					
+				for ( var a in this.DOMAttrs ) {
+					var val = node[this.DOMAttrs[a]];
+					if ( val )
+						ret += ' ' + a + '=' + this.parse( val, 'attribute' );
+				}
+				return ret + close + open + '/' + tag + close;
+			},
+			functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
+				var l = fn.length;
+				if ( !l ) return '';				
+				
+				var args = Array(l);
+				while ( l-- )
+					args[l] = String.fromCharCode(97+l);//97 is 'a'
+				return ' ' + args.join(', ') + ' ';
+			},
+			key:quote, //object calls it internally, the key part of an item in a map
+			functionCode:'[code]', //function calls it internally, it's the content of the function
+			attribute:quote, //node calls it internally, it's an html attribute value
+			string:quote,
+			date:quote,
+			regexp:literal, //regex
+			number:literal,
+			'boolean':literal
+		},
+		DOMAttrs:{//attributes to dump from nodes, name=>realName
+			id:'id',
+			name:'name',
+			'class':'className'
+		},
+		HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
+		indentChar:'   ',//indentation unit
+		multiline:false //if true, items in a collection, are separated by a \n, else just a space.
+	};
+
+	return jsDump;
+})();
+
+})(this);
--- a/devtools/devctl.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/devctl.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -26,17 +26,16 @@
 # completion). So import locally in command helpers.
 import sys
 from datetime import datetime
-from os import mkdir, chdir
-from os.path import join, exists, abspath, basename, normpath, split, isdir
+from os import mkdir, chdir, listdir, path as osp
 from warnings import warn
 
 from logilab.common import STD_BLACKLIST
-from logilab.common.clcommands import register_commands, pop_arg
 
 from cubicweb.__pkginfo__ import version as cubicwebversion
 from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage
-from cubicweb.toolsutils import (SKEL_EXCLUDE, Command,
-                                 copy_skeleton, underline_title)
+from cubicweb.cwctl import CWCTL
+from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton,
+                                 underline_title)
 from cubicweb.web.webconfig import WebConfiguration
 from cubicweb.server.serverconfig import ServerConfiguration
 
@@ -67,7 +66,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
@@ -102,7 +101,7 @@
     """
     from cubicweb.cwvreg import CubicWebVRegistry
     if cubedir:
-        cube = split(cubedir)[-1]
+        cube = osp.split(cubedir)[-1]
         config = DevConfiguration(cube)
         depcubes = list(config._cubes)
         depcubes.remove(cube)
@@ -125,7 +124,8 @@
     from cubicweb.web import uicfg
     from cubicweb.schema import META_RTYPES, SYSTEM_RTYPES, CONSTRAINTS
     no_context_rtypes = META_RTYPES | SYSTEM_RTYPES
-    w('# schema pot file, generated on %s\n' % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
+    w('# schema pot file, generated on %s\n'
+      % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
     w('# \n')
     w('# singular and plural forms for each entity type\n')
     w('\n')
@@ -177,7 +177,8 @@
                     add_msg(w, str(tschema),
                             'inlined:%s.%s.%s' % (etype, rschema, role))
                 if appearsin_addmenu.etype_get(eschema, rschema, role, tschema):
-                    if libconfig is not None and libappearsin_addmenu.etype_get(eschema, rschema, role, tschema):
+                    if libconfig is not None and libappearsin_addmenu.etype_get(
+                        eschema, rschema, role, tschema):
                         if eschema in libschema and tschema in libschema:
                             continue
                     if role == 'subject':
@@ -200,7 +201,8 @@
     for rschema in sorted(schema.relations()):
         rtype = rschema.type
         if rtype not in libschema:
-            # bw compat, necessary until all translation of relation are done properly...
+            # bw compat, necessary until all translation of relation are done
+            # properly...
             add_msg(w, rtype)
             if rschema.description and rschema.description not in done:
                 done.add(rschema.description)
@@ -222,7 +224,8 @@
                     if not objschema in libobjects:
                         add_msg(w, '%s_object' % rtype, objschema.type)
             if rtype not in libschema:
-                # bw compat, necessary until all translation of relation are done properly...
+                # bw compat, necessary until all translation of relation are
+                # done properly...
                 add_msg(w, '%s_object' % rtype)
     for objid in _iter_vreg_objids(vreg, vregdone):
         add_msg(w, '%s_description' % objid)
@@ -246,8 +249,6 @@
                     break
 
 
-LANGS = ('en', 'fr', 'es')
-I18NDIR = join(BASEDIR, 'i18n')
 DEFAULT_POT_HEAD = r'''msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb %s\n"
@@ -262,6 +263,11 @@
 
 ''' % cubicwebversion
 
+def cw_languages():
+    for fname in listdir(osp.join(WebConfiguration.i18n_lib_dir())):
+        if fname.endswith('.po'):
+            yield osp.splitext(fname)[0]
+
 
 class UpdateCubicWebCatalogCommand(Command):
     """Update i18n catalogs for cubicweb library.
@@ -270,11 +276,10 @@
     files to add translations of newly added messages.
     """
     name = 'i18ncubicweb'
+    min_args = max_args = 0
 
     def run(self, args):
         """run the command with its specific arguments"""
-        if args:
-            raise BadCommandUsage('Too many arguments')
         import shutil
         import tempfile
         import yams
@@ -283,9 +288,10 @@
         from logilab.common.modutils import get_module_files
         from cubicweb.i18n import extract_from_tal, execute
         tempdir = tempfile.mkdtemp()
-        potfiles = [join(I18NDIR, 'static-messages.pot')]
+        cwi18ndir = WebConfiguration.i18n_lib_dir()
         print '-> extract schema messages.'
-        schemapot = join(tempdir, 'schema.pot')
+        schemapot = osp.join(tempdir, 'schema.pot')
+        potfiles = [schemapot]
         potfiles.append(schemapot)
         # explicit close necessary else the file may not be yet flushed when
         # we'll using it below
@@ -293,36 +299,43 @@
         generate_schema_pot(schemapotstream.write, cubedir=None)
         schemapotstream.close()
         print '-> extract TAL messages.'
-        tali18nfile = join(tempdir, 'tali18n.py')
-        extract_from_tal(find(join(BASEDIR, 'web'), ('.py', '.pt')), tali18nfile)
+        tali18nfile = osp.join(tempdir, 'tali18n.py')
+        extract_from_tal(find(osp.join(BASEDIR, 'web'), ('.py', '.pt')),
+                         tali18nfile)
         print '-> generate .pot files.'
-        for id, files, lang in [('pycubicweb', get_module_files(BASEDIR) + list(globfind(join(BASEDIR, 'misc', 'migration'), '*.py')), None),
-                                ('schemadescr', globfind(join(BASEDIR, 'schemas'), '*.py'), None),
+        pyfiles = get_module_files(BASEDIR)
+        pyfiles += globfind(osp.join(BASEDIR, 'misc', 'migration'), '*.py')
+        schemafiles = globfind(osp.join(BASEDIR, 'schemas'), '*.py')
+        jsfiles = globfind(osp.join(BASEDIR, 'web'), 'cub*.js')
+        for id, files, lang in [('pycubicweb', pyfiles, None),
+                                ('schemadescr', schemafiles, None),
                                 ('yams', get_module_files(yams.__path__[0]), None),
                                 ('tal', [tali18nfile], None),
-                                ('js', globfind(join(BASEDIR, 'web'), 'cub*.js'), 'java'),
+                                ('js', jsfiles, 'java'),
                                 ]:
             cmd = 'xgettext --no-location --omit-header -k_ -o %s %s'
             if lang is not None:
                 cmd += ' -L %s' % lang
-            potfile = join(tempdir, '%s.pot' % id)
+            potfile = osp.join(tempdir, '%s.pot' % id)
             execute(cmd % (potfile, ' '.join('"%s"' % f for f in files)))
-            if exists(potfile):
+            if osp.exists(potfile):
                 potfiles.append(potfile)
             else:
                 print '-> WARNING: %s file was not generated' % potfile
         print '-> merging %i .pot files' % len(potfiles)
-        cubicwebpot = join(tempdir, 'cubicweb.pot')
-        execute('msgcat -o %s %s' % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles)))
+        cubicwebpot = osp.join(tempdir, 'cubicweb.pot')
+        execute('msgcat -o %s %s'
+                % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles)))
         print '-> merging main pot file with existing translations.'
-        chdir(I18NDIR)
+        chdir(cwi18ndir)
         toedit = []
-        for lang in LANGS:
+        for lang in cw_languages():
             target = '%s.po' % lang
-            execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"' % (target, target, cubicwebpot))
+            execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"'
+                    % (target, target, cubicwebpot))
             ensure_fs_mode(target)
             shutil.move('%snew' % target, target)
-            toedit.append(abspath(target))
+            toedit.append(osp.abspath(target))
         # cleanup
         rm(tempdir)
         # instructions pour la suite
@@ -346,13 +359,14 @@
         else:
             cubes = [DevConfiguration.cube_dir(cube)
                      for cube in DevConfiguration.available_cubes()]
-            cubes = [cubepath for cubepath in cubes if exists(join(cubepath, 'i18n'))]
+            cubes = [cubepath for cubepath in cubes
+                     if osp.exists(osp.join(cubepath, 'i18n'))]
         update_cubes_catalogs(cubes)
 
 
 def update_cubes_catalogs(cubes):
     for cubedir in cubes:
-        if not isdir(cubedir):
+        if not osp.isdir(cubedir):
             print '-> ignoring %s that is not a directory.' % cubedir
             continue
         try:
@@ -376,20 +390,20 @@
     from logilab.common.fileutils import ensure_fs_mode
     from logilab.common.shellutils import find, rm
     from cubicweb.i18n import extract_from_tal, execute
-    cube = basename(normpath(cubedir))
+    cube = osp.basename(osp.normpath(cubedir))
     tempdir = tempfile.mkdtemp()
     print underline_title('Updating i18n catalogs for cube %s' % cube)
     chdir(cubedir)
-    if exists(join('i18n', 'entities.pot')):
+    if osp.exists(osp.join('i18n', 'entities.pot')):
         warn('entities.pot is deprecated, rename file to static-messages.pot (%s)'
-             % join('i18n', 'entities.pot'), DeprecationWarning)
-        potfiles = [join('i18n', 'entities.pot')]
-    elif exists(join('i18n', 'static-messages.pot')):
-        potfiles = [join('i18n', 'static-messages.pot')]
+             % osp.join('i18n', 'entities.pot'), DeprecationWarning)
+        potfiles = [osp.join('i18n', 'entities.pot')]
+    elif osp.exists(osp.join('i18n', 'static-messages.pot')):
+        potfiles = [osp.join('i18n', 'static-messages.pot')]
     else:
         potfiles = []
     print '-> extract schema messages'
-    schemapot = join(tempdir, 'schema.pot')
+    schemapot = osp.join(tempdir, 'schema.pot')
     potfiles.append(schemapot)
     # explicit close necessary else the file may not be yet flushed when
     # we'll using it below
@@ -397,30 +411,32 @@
     generate_schema_pot(schemapotstream.write, cubedir)
     schemapotstream.close()
     print '-> extract TAL messages'
-    tali18nfile = join(tempdir, 'tali18n.py')
-    extract_from_tal(find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',)), tali18nfile)
+    tali18nfile = osp.join(tempdir, 'tali18n.py')
+    ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',))
+    extract_from_tal(ptfiles, tali18nfile)
     print '-> extract Javascript messages'
-    jsfiles =  [jsfile for jsfile in find('.', '.js') if basename(jsfile).startswith('cub')]
+    jsfiles =  [jsfile for jsfile in find('.', '.js')
+                if osp.basename(jsfile).startswith('cub')]
     if jsfiles:
-        tmppotfile = join(tempdir, 'js.pot')
-        execute('xgettext --no-location --omit-header -k_ -L java --from-code=utf-8 -o %s %s'
-                % (tmppotfile, ' '.join(jsfiles)))
+        tmppotfile = osp.join(tempdir, 'js.pot')
+        execute('xgettext --no-location --omit-header -k_ -L java '
+                '--from-code=utf-8 -o %s %s' % (tmppotfile, ' '.join(jsfiles)))
         # no pot file created if there are no string to translate
-        if exists(tmppotfile):
+        if osp.exists(tmppotfile):
             potfiles.append(tmppotfile)
     print '-> create cube-specific catalog'
-    tmppotfile = join(tempdir, 'generated.pot')
+    tmppotfile = osp.join(tempdir, 'generated.pot')
     cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',))
     cubefiles.append(tali18nfile)
     execute('xgettext --no-location --omit-header -k_ -o %s %s'
             % (tmppotfile, ' '.join('"%s"' % f for f in cubefiles)))
-    if exists(tmppotfile): # doesn't exists of no translation string found
+    if osp.exists(tmppotfile): # doesn't exists of no translation string found
         potfiles.append(tmppotfile)
-    potfile = join(tempdir, 'cube.pot')
+    potfile = osp.join(tempdir, 'cube.pot')
     print '-> merging %i .pot files:' % len(potfiles)
     execute('msgcat -o %s %s' % (potfile,
                                  ' '.join('"%s"' % f for f in potfiles)))
-    if not exists(potfile):
+    if not osp.exists(potfile):
         print 'no message catalog for cube', cube, 'nothing to translate'
         # cleanup
         rm(tempdir)
@@ -428,16 +444,16 @@
     print '-> merging main pot file with existing translations:'
     chdir('i18n')
     toedit = []
-    for lang in LANGS:
+    for lang in cw_languages():
         print '-> language', lang
         cubepo = '%s.po' % lang
-        if not exists(cubepo):
+        if not osp.exists(cubepo):
             shutil.copy(potfile, cubepo)
         else:
             execute('msgmerge -N -s -o %snew %s %s' % (cubepo, cubepo, potfile))
             ensure_fs_mode(cubepo)
             shutil.move('%snew' % cubepo, cubepo)
-        toedit.append(abspath(cubepo))
+        toedit.append(osp.abspath(cubepo))
     # cleanup
     rm(tempdir)
     return toedit
@@ -465,7 +481,7 @@
     """
     name = 'newcube'
     arguments = '<cubename>'
-
+    min_args = max_args = 1
     options = (
         ("layout",
          {'short': 'L', 'type' : 'choice', 'metavar': '<cube layout>',
@@ -546,32 +562,34 @@
     def run(self, args):
         import re
         from logilab.common.shellutils import ASK
-        if len(args) != 1:
-            raise BadCommandUsage("exactly one argument (cube name) is expected")
         cubename = args[0]
         if not re.match('[_A-Za-z][_A-Za-z0-9]*$', cubename):
-            raise BadCommandUsage("cube name should be a valid python module name")
+            raise BadCommandUsage(
+                'cube name must be a valid python module name')
         verbose = self.get('verbose')
         cubesdir = self.get('directory')
         if not cubesdir:
             cubespath = ServerConfiguration.cubes_search_path()
             if len(cubespath) > 1:
-                raise BadCommandUsage("can't guess directory where to put the new cube."
-                                      " Please specify it using the --directory option")
+                raise BadCommandUsage(
+                    "can't guess directory where to put the new cube."
+                    " Please specify it using the --directory option")
             cubesdir = cubespath[0]
-        if not isdir(cubesdir):
+        if not osp.isdir(cubesdir):
             print "-> creating cubes directory", cubesdir
             try:
                 mkdir(cubesdir)
             except OSError, err:
-                self.fail("failed to create directory %r\n(%s)" % (cubesdir, err))
-        cubedir = join(cubesdir, cubename)
-        if exists(cubedir):
-            self.fail("%s already exists !" % (cubedir))
-        skeldir = join(BASEDIR, 'skeleton')
+                self.fail("failed to create directory %r\n(%s)"
+                          % (cubesdir, err))
+        cubedir = osp.join(cubesdir, cubename)
+        if osp.exists(cubedir):
+            self.fail("%s already exists !" % cubedir)
+        skeldir = osp.join(BASEDIR, 'skeleton')
         default_name = 'cubicweb-%s' % cubename.lower().replace('_', '-')
         if verbose:
-            distname = raw_input('Debian name for your cube ? [%s]): ' % default_name).strip()
+            distname = raw_input('Debian name for your cube ? [%s]): '
+                                 % default_name).strip()
             if not distname:
                 distname = default_name
             elif not distname.startswith('cubicweb-'):
@@ -580,10 +598,13 @@
         else:
             distname = default_name
         if not re.match('[a-z][-a-z0-9]*$', distname):
-            raise BadCommandUsage("cube distname should be a valid debian package name")
-        longdesc = shortdesc = raw_input('Enter a short description for your cube: ')
+            raise BadCommandUsage(
+                'cube distname should be a valid debian package name')
+        longdesc = shortdesc = raw_input(
+            'Enter a short description for your cube: ')
         if verbose:
-            longdesc = raw_input('Enter a long description (leave empty to reuse the short one): ')
+            longdesc = raw_input(
+                'Enter a long description (leave empty to reuse the short one): ')
         dependencies = {'cubicweb': '>= %s' % cubicwebversion}
         if verbose:
             dependencies.update(self._ask_for_dependencies())
@@ -603,7 +624,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):
@@ -638,8 +659,7 @@
     """
     arguments = 'rql.log'
     name = 'exlog'
-    options = (
-        )
+    options = ()
 
     def run(self, args):
         import re
@@ -684,34 +704,44 @@
     """Generate schema image for the given cube"""
     name = "schema"
     arguments = '<cube>'
-    options = [('output-file', {'type':'file', 'default': None,
-                 'metavar': '<file>', 'short':'o', 'help':'output image file',
-                 'input':False}),
-               ('viewer', {'type': 'string', 'default':None,
-                'short': "d", 'metavar':'<cmd>',
-                 'help':'command use to view the generated file (empty for none)'}
-               ),
-               ('show-meta', {'action': 'store_true', 'default':False,
-                'short': "m", 'metavar': "<yN>",
-                 'help':'include meta and internal entities in schema'}
-               ),
-               ('show-workflow', {'action': 'store_true', 'default':False,
-                'short': "w", 'metavar': "<yN>",
-                'help':'include workflow entities in schema'}
-               ),
-               ('show-cw-user', {'action': 'store_true', 'default':False,
-                'metavar': "<yN>",
-                'help':'include cubicweb user entities in schema'}
-               ),
-               ('exclude-type', {'type':'string', 'default':'',
-                'short': "x", 'metavar': "<types>",
-                 'help':'coma separated list of entity types to remove from view'}
-               ),
-               ('include-type', {'type':'string', 'default':'',
-                'short': "i", 'metavar': "<types>",
-                 'help':'coma separated list of entity types to include in view'}
-               ),
-              ]
+    min_args = max_args = 1
+    options = [
+        ('output-file',
+         {'type':'file', 'default': None,
+          'metavar': '<file>', 'short':'o', 'help':'output image file',
+          'input':False,
+          }),
+        ('viewer',
+         {'type': 'string', 'default':None,
+          'short': "d", 'metavar':'<cmd>',
+          'help':'command use to view the generated file (empty for none)',
+          }),
+        ('show-meta',
+         {'action': 'store_true', 'default':False,
+          'short': "m", 'metavar': "<yN>",
+          'help':'include meta and internal entities in schema',
+          }),
+        ('show-workflow',
+         {'action': 'store_true', 'default':False,
+          'short': "w", 'metavar': "<yN>",
+          'help':'include workflow entities in schema',
+          }),
+        ('show-cw-user',
+         {'action': 'store_true', 'default':False,
+          'metavar': "<yN>",
+          'help':'include cubicweb user entities in schema',
+          }),
+        ('exclude-type',
+         {'type':'string', 'default':'',
+          'short': "x", 'metavar': "<types>",
+          'help':'coma separated list of entity types to remove from view',
+          }),
+        ('include-type',
+         {'type':'string', 'default':'',
+          'short': "i", 'metavar': "<types>",
+          'help':'coma separated list of entity types to include in view',
+          }),
+        ]
 
     def run(self, args):
         from subprocess import Popen
@@ -720,7 +750,7 @@
         from yams import schema2dot, BASE_TYPES
         from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                                      WORKFLOW_TYPES, INTERNAL_TYPES)
-        cubes = splitstrip(pop_arg(args, 1))
+        cubes = splitstrip(args[0])
         dev_conf = DevConfiguration(*cubes)
         schema = dev_conf.load_schema()
         out, viewer = self['output-file'], self['viewer']
@@ -741,10 +771,22 @@
             p = Popen((viewer, out))
             p.wait()
 
-register_commands((UpdateCubicWebCatalogCommand,
-                   UpdateTemplateCatalogCommand,
-                   #LiveServerCommand,
-                   NewCubeCommand,
-                   ExamineLogCommand,
-                   GenerateSchema,
-                   ))
+
+class GenerateQUnitHTML(Command):
+    """Generate a QUnit html file to see test in your browser"""
+    name = "qunit-html"
+    arguments = '<test file> [<dependancy js file>...]'
+
+    def run(self, args):
+        from cubicweb.devtools.qunit import make_qunit_html
+        print make_qunit_html(args[0], args[1:])
+
+for cmdcls in (UpdateCubicWebCatalogCommand,
+               UpdateTemplateCatalogCommand,
+               #LiveServerCommand,
+               NewCubeCommand,
+               ExamineLogCommand,
+               GenerateSchema,
+               GenerateQUnitHTML,
+               ):
+    CWCTL.register(cmdcls)
--- a/devtools/fake.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/fake.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/fill.py	Wed Nov 03 16:38:28 2010 +0100
@@ -216,7 +216,7 @@
 
     # XXX nothing to do here
     def generate_Any_data_format(self, entity, index, **kwargs):
-        # data_format attribute of Image/File has no vocabulary constraint, we
+        # data_format attribute of File has no vocabulary constraint, we
         # need this method else stupid values will be set which make mtconverter
         # raise exception
         return u'application/octet-stream'
@@ -227,12 +227,6 @@
         # raise exception
         return u'text/plain'
 
-    def generate_Image_data_format(self, entity, index, **kwargs):
-        # data_format attribute of Image/File has no vocabulary constraint, we
-        # need this method else stupid values will be set which make mtconverter
-        # raise exception
-        return u'image/png'
-
 
 class autoextend(type):
     def __new__(mcs, name, bases, classdict):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/httptest.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,197 @@
+# 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 urlparse import urlparse
+
+from twisted.internet import reactor, error
+
+from cubicweb.etwist.server import run
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.devtools import ApptestConfiguration
+
+
+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
+
+    .. see:: :func:`test.test_support.bind_port`
+    """
+    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 CubicWebServerConfig(ApptestConfiguration):
+    """basic configuration class for configuring test server
+
+    Class attributes:
+
+    * `ports_range`: list giving range of http ports to test (range(7000, 8000)
+      by default). The first port found as available in `ports_range` will be
+      used to launch the test web server.
+
+    """
+    ports_range = range(7000, 8000)
+
+    def default_base_url(self):
+        port = self['port'] or get_available_port(self.ports_range)
+        self.global_set_option('port', port) # force rewrite here
+        return 'http://127.0.0.1:%d/' % self['port']
+
+    def pyro_enabled(self):
+        return False
+
+    def load_configuration(self):
+        super(CubicWebServerConfig, self).load_configuration()
+        self.global_set_option('force-html-content-type', True)
+
+
+class CubicWebServerTC(CubicWebTC):
+    """Class for running test web server. See :class:`CubicWebServerConfig`.
+
+    Class attributes:
+    * ` anonymous_logged`: flag telling ifs anonymous user should be log logged
+      by default (True by default)
+    """
+    configcls = CubicWebServerConfig
+    # anonymous is logged by default in cubicweb test cases
+    anonymous_logged = True
+
+    def start_server(self):
+        # 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
+        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
+        parseurl = urlparse(self.config['base-url'])
+        assert parseurl.port == self.config['port'], (self.config['base-url'], self.config['port'])
+        self._web_test_cnx = httplib.HTTPConnection(parseurl.hostname,
+                                                    parseurl.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')
+        assert self._ident_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.start_server()
+
+    def tearDown(self):
+        try:
+            self.stop_server()
+        except error.ReactorNotRunning, err:
+            # Server could be launched manually
+            print err
+        CubicWebTC.tearDown(self)
+
+    @classmethod
+    def init_config(cls, config):
+        super(CubicWebServerTC, cls).init_config(config)
+        if not cls.anonymous_logged:
+            config.global_set_option('anonymous-user', None)
+        else:
+            config.global_set_option('anonymous-user', 'anon')
+            config.global_set_option('anonymous-password', 'anon')
--- a/devtools/livetest.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/livetest.py	Wed Nov 03 16:38:28 2010 +0100
@@ -55,7 +55,7 @@
         """Indicate which resource to use to process down the URL's path"""
         if len(segments) and segments[0] == 'data':
             # Anything in data/ is treated as static files
-            datadir = self.config.locate_resource(segments[1])
+            datadir = self.config.locate_resource(segments[1])[0]
             if datadir:
                 return static.File(str(datadir), segments[1:])
         # Otherwise we use this single resource
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/qunit.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,305 @@
+import os, os.path as osp
+import signal
+from tempfile import mkdtemp, NamedTemporaryFile, TemporaryFile
+import tempfile
+from Queue import Queue, Empty
+from subprocess import Popen, check_call, CalledProcessError
+from shutil import rmtree, copy as copyfile
+from uuid import uuid4 
+
+# imported by default to simplify further import statements
+from logilab.common.testlib import unittest_main, with_tempdir, InnerTest
+from logilab.common.shellutils import getlogin
+
+import cubicweb
+from cubicweb.view import StartupView
+from cubicweb.web.controller import Controller
+from cubicweb.devtools.httptest import CubicWebServerTC
+
+
+class VerboseCalledProcessError(CalledProcessError):
+
+    def __init__(self, returncode, command, stdout, stderr):
+        super(VerboseCalledProcessError, self).__init__(returncode, command)
+        self.stdout = stdout
+        self.stderr = stderr
+
+    def __str__(self):
+        str = [ super(VerboseCalledProcessError, self).__str__()]
+        if self.stdout.strip():
+            str.append('******************')
+            str.append('* process stdout *')
+            str.append('******************')
+            str.append(self.stdout)
+        if self.stderr.strip():
+            str.append('******************')
+            str.append('* process stderr *')
+            str.append('******************')
+            str.append(self.stderr)
+        return '\n'.join(str)
+
+
+
+class FirefoxHelper(object):
+
+    profile_name_mask = 'PYTEST_PROFILE_%(uid)s'
+
+    def __init__(self, url=None):
+        self._process = None
+        self._tmp_dir = mkdtemp()
+        self._profile_data = {'uid': uuid4()}
+        self._profile_name = self.profile_name_mask % self._profile_data
+        fnull = open(os.devnull, 'w')
+        stdout = TemporaryFile()
+        stderr = TemporaryFile()
+        try:
+            home = osp.expanduser('~')
+            user = getlogin()
+            assert os.access(home, os.W_OK), \
+                   'No write access to your home directory, Firefox will crash.'\
+                   ' Are you sure "%s" is a valid home  for user "%s"' % (home, user)
+            check_call(['firefox', '-no-remote', '-CreateProfile',
+                        '%s %s' % (self._profile_name, self._tmp_dir)],
+                                  stdout=stdout, stderr=stderr)
+        except CalledProcessError, cpe:
+            stdout.seek(0)
+            stderr.seek(0)
+            raise VerboseCalledProcessError(cpe.returncode, cpe.cmd, stdout.read(), stderr.read())
+
+    def start(self, url):
+        self.stop()
+        fnull = open(os.devnull, 'w')
+        self._process = Popen(['firefox', '-no-remote', '-P', self._profile_name, url],
+                              stdout=fnull, stderr=fnull)
+
+    def stop(self):
+        if self._process is not None:
+            assert self._process.returncode is None,  self._process.returncode
+            os.kill(self._process.pid, signal.SIGTERM)
+            self._process.wait()
+            self._process = None
+
+    def __del__(self):
+        self.stop()
+        rmtree(self._tmp_dir)
+
+
+class QUnitTestCase(CubicWebServerTC):
+
+    # testfile, (dep_a, dep_b)
+    all_js_tests = ()
+
+    def setUp(self):
+        super(QUnitTestCase, self).setUp()
+        self.test_queue = Queue()
+        class MyQUnitResultController(QUnitResultController):
+            tc = self
+            test_queue = self.test_queue
+        self._qunit_controller = MyQUnitResultController
+        self.vreg.register(MyQUnitResultController)
+
+    def tearDown(self):
+        super(QUnitTestCase, self).tearDown()
+        self.vreg.unregister(self._qunit_controller)
+
+
+    def abspath(self, path):
+        """use self.__module__ to build absolute path if necessary"""
+        if not osp.isabs(path):
+           dirname = osp.dirname(__import__(self.__module__).__file__)
+           return osp.abspath(osp.join(dirname,path))
+        return path
+
+    def test_javascripts(self):
+        for args in self.all_js_tests:
+            test_file = self.abspath(args[0])
+            if len(args) > 1:
+                depends   = [self.abspath(dep) for dep in args[1]]
+            else:
+                depends = ()
+            if len(args) > 2:
+                data   = [self.abspath(data) for data in args[2]]
+            else:
+                data = ()
+            for js_test in self._test_qunit(test_file, depends, data):
+                yield js_test
+
+    @with_tempdir
+    def _test_qunit(self, test_file, depends=(), data_files=(), timeout=30):
+        assert osp.exists(test_file), test_file
+        for dep in depends:
+            assert osp.exists(dep), dep
+        for data in data_files:
+            assert osp.exists(data), data
+
+        # generate html test file
+        jquery_dir = 'file://' + self.config.locate_resource('jquery.js')[0]
+        html_test_file = NamedTemporaryFile(suffix='.html')
+        html_test_file.write(make_qunit_html(test_file, depends,
+                             base_url=self.config['base-url'],
+                             web_data_path=jquery_dir))
+        html_test_file.flush()
+        # copying data file
+        for data in data_files:
+            copyfile(data, tempfile.tempdir)
+
+        while not self.test_queue.empty():
+            self.test_queue.get(False)
+
+        browser = FirefoxHelper()
+        browser.start(html_test_file.name)
+        test_count = 0
+        error = False
+        def raise_exception(cls, *data):
+            raise cls(*data)
+        while not error:
+            try:
+                result, test_name, msg = self.test_queue.get(timeout=timeout)
+                test_name = '%s (%s)' % (test_name, test_file)
+                self.set_description(test_name)
+                if result is None:
+                    break
+                test_count += 1
+                if result:
+                    yield InnerTest(test_name, lambda : 1)
+                else:
+                    yield InnerTest(test_name, self.fail, msg)
+            except Empty:
+                error = True
+                yield InnerTest(test_file, raise_exception, RuntimeError, "%s did not report execution end. %i test processed so far." % (test_file, test_count))
+
+        browser.stop()
+        if test_count <= 0 and not error:
+            yield InnerTest(test_name, raise_exception, RuntimeError, 'No test yielded by qunit for %s' % test_file)
+
+class QUnitResultController(Controller):
+
+    __regid__ = 'qunit_result'
+
+
+    # Class variables to circumvent the instantiation of a new Controller for each request.
+    _log_stack = [] # store QUnit log messages
+    _current_module_name = '' # store the current QUnit module name
+
+    def publish(self, rset=None):
+        event = self._cw.form['event']
+        getattr(self, 'handle_%s' % event)()
+
+    def handle_module_start(self):
+        self.__class__._current_module_name = self._cw.form.get('name', '')
+
+    def handle_test_done(self):
+        name = '%s // %s' %  (self._current_module_name, self._cw.form.get('name', ''))
+        failures = int(self._cw.form.get('failures', 0))
+        total = int(self._cw.form.get('total', 0))
+
+        self._log_stack.append('%i/%i assertions failed' % (failures, total))
+        msg = '\n'.join(self._log_stack)
+
+        if failures:
+            self.tc.test_queue.put((False, name, msg))
+        else:
+            self.tc.test_queue.put((True, name, msg))
+        self._log_stack[:] = []
+
+    def handle_done(self):
+        self.tc.test_queue.put((None, None, None))
+
+    def handle_log(self):
+        result = self._cw.form['result']
+        message = self._cw.form['message']
+        self._log_stack.append('%s: %s' % (result, message))
+
+
+def cw_path(*paths):
+  return file_path(osp.join(cubicweb.CW_SOFTWARE_ROOT, *paths))
+
+def file_path(path):
+    return 'file://' + osp.abspath(path)
+
+def build_js_script(host):
+    return """
+    var host = '%s';
+
+    QUnit.moduleStart = function (name) {
+      jQuery.ajax({
+                  url: host+'/qunit_result',
+                 data: {"event": "module_start",
+                        "name": name},
+                 async: false});
+    }
+
+    QUnit.testDone = function (name, failures, total) {
+      jQuery.ajax({
+                  url: host+'/qunit_result',
+                 data: {"event": "test_done",
+                        "name": name,
+                        "failures": failures,
+                        "total":total},
+                 async: false});
+    }
+
+    QUnit.done = function (failures, total) {
+      jQuery.ajax({
+                   url: host+'/qunit_result',
+                   data: {"event": "done",
+                          "failures": failures,
+                          "total":total},
+                   async: false});
+      window.close();
+    }
+
+    QUnit.log = function (result, message) {
+      jQuery.ajax({
+                   url: host+'/qunit_result',
+                   data: {"event": "log",
+                          "result": result,
+                          "message": message},
+                   async: false});
+    }
+    """ % host
+
+def make_qunit_html(test_file, depends=(), base_url=None,
+                    web_data_path=cw_path('web', 'data')):
+    """"""
+    data = {
+            'web_data': web_data_path,
+            'web_test': cw_path('devtools', 'data'),
+        }
+
+    html = ['''<html>
+  <head>
+    <!-- JS lib used as testing framework -->
+    <link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
+    <script src="%(web_data)s/jquery.js" type="text/javascript"></script>
+    <script src="%(web_test)s/cwmock.js" type="text/javascript"></script>
+    <script src="%(web_test)s/qunit.js" type="text/javascript"></script>'''
+    % data]
+    if base_url is not None:
+        html.append('<!-- result report tools -->')
+        html.append('<script type="text/javascript">')
+        html.append(build_js_script(base_url))
+        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	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/repotest.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -41,7 +41,7 @@
     plan = self._prepare_plan(rql, kwargs)
     self.planner.build_plan(plan)
     try:
-        self.assertEquals(len(plan.steps), len(expected),
+        self.assertEqual(len(plan.steps), len(expected),
                           'expected %s steps, got %s' % (len(expected), len(plan.steps)))
         # step order is important
         for i, step in enumerate(plan.steps):
@@ -52,20 +52,20 @@
 
 def compare_steps(self, step, expected):
     try:
-        self.assertEquals(step[0], expected[0], 'expected step type %s, got %s' % (expected[0], step[0]))
+        self.assertEqual(step[0], expected[0], 'expected step type %s, got %s' % (expected[0], step[0]))
         if len(step) > 2 and isinstance(step[1], list) and isinstance(expected[1], list):
             queries, equeries = step[1], expected[1]
-            self.assertEquals(len(queries), len(equeries),
+            self.assertEqual(len(queries), len(equeries),
                               'expected %s queries, got %s' % (len(equeries), len(queries)))
             for i, (rql, sol) in enumerate(queries):
-                self.assertEquals(rql, equeries[i][0])
-                self.assertEquals(sorted(sol), sorted(equeries[i][1]))
+                self.assertEqual(rql, equeries[i][0])
+                self.assertEqual(sorted(sol), sorted(equeries[i][1]))
             idx = 2
         else:
             idx = 1
-        self.assertEquals(step[idx:-1], expected[idx:-1],
+        self.assertEqual(step[idx:-1], expected[idx:-1],
                           'expected step characteristic \n%s\n, got\n%s' % (expected[1:-1], step[1:-1]))
-        self.assertEquals(len(step[-1]), len(expected[-1]),
+        self.assertEqual(len(step[-1]), len(expected[-1]),
                           'got %s child steps, expected %s' % (len(step[-1]), len(expected[-1])))
     except AssertionError:
         print 'error on step ',
@@ -134,24 +134,35 @@
             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:
+            try:
+                dbhelper = get_db_helper(self.backend)
+            except ImportError, ex:
+                self.skipTest(str(ex))
+            self.o = SQLGenerator(self.schema, dbhelper)
 
     def tearDown(self):
         ExecutionPlan._check_permissions = _orig_check_permissions
@@ -270,6 +281,7 @@
         self.system = self.sources[-1]
         do_monkey_patch()
         self._dumb_sessions = [] # by hi-jacked parent setup
+        self.repo.vreg.rqlhelper.backend = 'postgres' # so FTIRANK is considered
 
     def add_source(self, sourcecls, uri):
         self.sources.append(sourcecls(self.repo, self.o.schema,
--- a/devtools/test/data/dbfill.conf	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-[BASE]
-APPLICATION_SCHEMA = /home/adim/cvs_work/soft_prive/ginco/applications/crm/schema
-APPLICATION_HOME = /home/adim/etc/erudi.d/crmadim # ???
-FAKEDB_NAME = crmtest
-ENCODING = UTF-8
-HOST = crater
-USER = adim
-PASSWORD = adim
-
-
-[ENTITIES]
-default = 20 #means default is 20 entities
-Person = 10 # means 10 Persons
-Company = 5# means 5 companies
-
-
-[RELATIONS]
-Person works_for Company = 4
-Division subsidiary_of Company = 3
-
-[DEFAULT_VALUES]
-Person.firstname = data/firstnames.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/dep_1.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,29 @@
+function datetuple(d) {
+    return [d.getFullYear(), d.getMonth()+1, d.getDate(), 
+	    d.getHours(), d.getMinutes()];
+}
+    
+function pprint(obj) {
+    print('{');
+    for(k in obj) {
+	print('  ' + k + ' = ' + obj[k]);
+    }
+    print('}');
+}
+
+function arrayrepr(array) {
+    return '[' + array.join(', ') + ']';
+}
+    
+function assertArrayEquals(array1, array2) {
+    if (array1.length != array2.length) {
+	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
+    }
+    for (var i=0; i<array1.length; i++) {
+	if (array1[i] != array2[i]) {
+	    
+	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
+						 + ' differs at index ' + i);
+	}
+    }
+}
--- a/devtools/test/data/views.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/test/data/views.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,12 +15,10 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""only for unit tests !
-
-"""
+"""only for unit tests !"""
 
 from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 
 HTML_PAGE = u"""<html>
   <body>
@@ -31,7 +29,7 @@
 
 class SimpleView(EntityView):
     __regid__ = 'simple'
-    __select__ = implements('Bug',)
+    __select__ = is_instance('Bug',)
 
     def call(self, **kwargs):
         self.cell_call(0, 0)
@@ -41,7 +39,7 @@
 
 class RaisingView(EntityView):
     __regid__ = 'raising'
-    __select__ = implements('Bug',)
+    __select__ = is_instance('Bug',)
 
     def cell_call(self, row, col):
         raise ValueError()
--- a/devtools/test/unittest_dbfill.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/test/unittest_dbfill.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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 database value generator
-
-"""
+"""unit tests for database value generator"""
 
 import os.path as osp
 import re
@@ -56,7 +54,7 @@
         return [f.strip() for f in file(osp.join(DATADIR, 'firstnames.txt'))]
 
     def setUp(self):
-        config = ApptestConfiguration('data')
+        config = ApptestConfiguration('data', apphome=DATADIR)
         config.bootstrap_cubes()
         schema = config.load_schema()
         e_schema = schema.eschema('Person')
@@ -68,7 +66,7 @@
     def test_string(self):
         """test string generation"""
         surname = self.person_valgen.generate_attribute_value({}, 'surname', 12)
-        self.assertEquals(surname, u'é&surname12')
+        self.assertEqual(surname, u'é&surname12')
 
     def test_domain_value(self):
         """test value generation from a given domain value"""
@@ -100,21 +98,21 @@
 
     def test_phone(self):
         """tests make_tel utility"""
-        self.assertEquals(make_tel(22030405), '22 03 04 05')
+        self.assertEqual(make_tel(22030405), '22 03 04 05')
 
     def test_customized_generation(self):
-        self.assertEquals(self.bug_valgen.generate_attribute_value({}, 'severity', 12),
+        self.assertEqual(self.bug_valgen.generate_attribute_value({}, 'severity', 12),
                           u'dangerous')
-        self.assertEquals(self.bug_valgen.generate_attribute_value({}, 'description', 12),
+        self.assertEqual(self.bug_valgen.generate_attribute_value({}, 'description', 12),
                           u'yo')
-        self.assertEquals(self.person_valgen.generate_attribute_value({}, 'description', 12),
+        self.assertEqual(self.person_valgen.generate_attribute_value({}, 'description', 12),
                           u'yo')
 
 
 class ConstraintInsertionTC(TestCase):
 
     def test_writeme(self):
-        self.skip('Test automatic insertion / Schema Constraints')
+        self.skipTest('Test automatic insertion / Schema Constraints')
 
 
 if __name__ == '__main__':
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/unittest_httptest.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,67 @@
+# 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/>.
+"""unittest for cubicweb.devtools.httptest module"""
+
+import httplib
+
+from cubicweb.devtools.httptest import CubicWebServerTC, CubicWebServerConfig
+
+
+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.assertEqual(response.status, httplib.OK)
+
+    def test_base_url(self):
+        if self.config['base-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.assertEqual(response.status, httplib.FORBIDDEN)
+
+    def test_login(self):
+        response = self.web_get()
+        if response.status != httplib.FORBIDDEN:
+             self.skipTest('Already authenticated')
+        # login
+        self.web_login(self.admlogin, self.admpassword)
+        response = self.web_get()
+        self.assertEqual(response.status, httplib.OK, response.body)
+        # logout
+        self.web_logout()
+        response = self.web_get()
+        self.assertEqual(response.status, httplib.FORBIDDEN, response.body)
+
+
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/unittest_qunit.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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.assertEqual(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/test/unittest_testlib.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/test/unittest_testlib.py	Wed Nov 03 16:38:28 2010 +0100
@@ -20,9 +20,8 @@
 """
 
 from cStringIO import StringIO
-from unittest import TestSuite
 
-from logilab.common.testlib import (TestCase, unittest_main,
+from logilab.common.testlib import (TestCase, unittest_main, TestSuite,
                                     SkipAwareTextTestRunner)
 
 from cubicweb.devtools import htmlparser
@@ -47,9 +46,9 @@
 
         tests = [MyWebTest('test_error_view'), MyWebTest('test_correct_view')]
         result = self.runner.run(TestSuite(tests))
-        self.assertEquals(result.testsRun, 2)
-        self.assertEquals(len(result.errors), 0)
-        self.assertEquals(len(result.failures), 1)
+        self.assertEqual(result.testsRun, 2)
+        self.assertEqual(len(result.errors), 0)
+        self.assertEqual(len(result.failures), 1)
         clean_repo_test_cls(MyWebTest)
 
 
@@ -104,7 +103,7 @@
 
     def test_source1(self):
         """make sure source is stored correctly"""
-        self.assertEquals(self.page_info.source, HTML_PAGE2)
+        self.assertEqual(self.page_info.source, HTML_PAGE2)
 
     def test_source2(self):
         """make sure source is stored correctly - raise exception"""
@@ -114,47 +113,47 @@
 
     def test_has_title_no_level(self):
         """tests h? tags information"""
-        self.assertEquals(self.page_info.has_title('Test'), True)
-        self.assertEquals(self.page_info.has_title('Test '), False)
-        self.assertEquals(self.page_info.has_title('Tes'), False)
-        self.assertEquals(self.page_info.has_title('Hello world !'), True)
+        self.assertEqual(self.page_info.has_title('Test'), True)
+        self.assertEqual(self.page_info.has_title('Test '), False)
+        self.assertEqual(self.page_info.has_title('Tes'), False)
+        self.assertEqual(self.page_info.has_title('Hello world !'), True)
 
     def test_has_title_level(self):
         """tests h? tags information"""
-        self.assertEquals(self.page_info.has_title('Test', level = 1), True)
-        self.assertEquals(self.page_info.has_title('Test', level = 2), False)
-        self.assertEquals(self.page_info.has_title('Test', level = 3), False)
-        self.assertEquals(self.page_info.has_title('Test', level = 4), False)
+        self.assertEqual(self.page_info.has_title('Test', level = 1), True)
+        self.assertEqual(self.page_info.has_title('Test', level = 2), False)
+        self.assertEqual(self.page_info.has_title('Test', level = 3), False)
+        self.assertEqual(self.page_info.has_title('Test', level = 4), False)
         self.assertRaises(IndexError, self.page_info.has_title, 'Test', level = 5)
 
     def test_has_title_regexp_no_level(self):
         """tests has_title_regexp() with no particular level specified"""
-        self.assertEquals(self.page_info.has_title_regexp('h[23] title'), True)
+        self.assertEqual(self.page_info.has_title_regexp('h[23] title'), True)
 
     def test_has_title_regexp_level(self):
         """tests has_title_regexp() with a particular level specified"""
-        self.assertEquals(self.page_info.has_title_regexp('h[23] title', 2), True)
-        self.assertEquals(self.page_info.has_title_regexp('h[23] title', 3), True)
-        self.assertEquals(self.page_info.has_title_regexp('h[23] title', 4), False)
+        self.assertEqual(self.page_info.has_title_regexp('h[23] title', 2), True)
+        self.assertEqual(self.page_info.has_title_regexp('h[23] title', 3), True)
+        self.assertEqual(self.page_info.has_title_regexp('h[23] title', 4), False)
 
     def test_appears(self):
         """tests PageInfo.appears()"""
-        self.assertEquals(self.page_info.appears('CW'), True)
-        self.assertEquals(self.page_info.appears('Logilab'), True)
-        self.assertEquals(self.page_info.appears('Logilab introduces'), True)
-        self.assertEquals(self.page_info.appears('H2 title'), False)
+        self.assertEqual(self.page_info.appears('CW'), True)
+        self.assertEqual(self.page_info.appears('Logilab'), True)
+        self.assertEqual(self.page_info.appears('Logilab introduces'), True)
+        self.assertEqual(self.page_info.appears('H2 title'), False)
 
     def test_has_link(self):
         """tests has_link()"""
-        self.assertEquals(self.page_info.has_link('Logilab'), True)
-        self.assertEquals(self.page_info.has_link('logilab'), False)
-        self.assertEquals(self.page_info.has_link('Logilab', 'http://www.logilab.org'), True)
-        self.assertEquals(self.page_info.has_link('Logilab', 'http://www.google.com'), False)
+        self.assertEqual(self.page_info.has_link('Logilab'), True)
+        self.assertEqual(self.page_info.has_link('logilab'), False)
+        self.assertEqual(self.page_info.has_link('Logilab', 'http://www.logilab.org'), True)
+        self.assertEqual(self.page_info.has_link('Logilab', 'http://www.google.com'), False)
 
     def test_has_link_regexp(self):
         """test has_link_regexp()"""
-        self.assertEquals(self.page_info.has_link_regexp('L[oi]gilab'), True)
-        self.assertEquals(self.page_info.has_link_regexp('L[ai]gilab'), False)
+        self.assertEqual(self.page_info.has_link_regexp('L[oi]gilab'), True)
+        self.assertEqual(self.page_info.has_link_regexp('L[ai]gilab'), False)
 
 
 if __name__ == '__main__':
--- a/devtools/testlib.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/devtools/testlib.py	Wed Nov 03 16:38:28 2010 +0100
@@ -24,6 +24,8 @@
 import os
 import sys
 import re
+import urlparse
+from os.path import dirname, join
 from urllib import unquote
 from math import log
 from contextlib import contextmanager
@@ -31,7 +33,7 @@
 
 import yams.schema
 
-from logilab.common.testlib import TestCase, InnerTest
+from logilab.common.testlib import TestCase, InnerTest, Tags
 from logilab.common.pytest import nocoverage, pause_tracing, resume_tracing
 from logilab.common.debugger import Debugger
 from logilab.common.umessage import message_from_string
@@ -44,8 +46,9 @@
 from cubicweb.sobjects import notification
 from cubicweb.web import Redirect, application
 from cubicweb.server.session import security_enabled
+from cubicweb.server.hook import SendMailOp
 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
-from cubicweb.devtools import fake, htmlparser
+from cubicweb.devtools import BASE_URL, fake, htmlparser
 from cubicweb.utils import json
 
 # low-level utilities ##########################################################
@@ -69,7 +72,6 @@
         after = before
     return center - before <= line_no <= center + after
 
-
 def unprotected_entities(schema, strict=False):
     """returned a set of each non final entity type, excluding "system" entities
     (eg CWGroup, CWUser...)
@@ -80,7 +82,6 @@
         protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES)
     return set(schema.entities()) - protected_entities
 
-
 def refresh_repo(repo, resetschema=False, resetvreg=False):
     for pool in repo.pools:
         pool.close(True)
@@ -143,6 +144,30 @@
 cwconfig.SMTP = MockSMTP
 
 
+class TestCaseConnectionProxy(object):
+    """thin wrapper around `cubicweb.dbapi.Connection` context-manager
+    used in CubicWebTC (cf. `cubicweb.devtools.testlib.CubicWebTC.login` method)
+
+    It just proxies to the default connection context manager but
+    restores the original connection on exit.
+    """
+    def __init__(self, testcase, cnx):
+        self.testcase = testcase
+        self.cnx = cnx
+
+    def __getattr__(self, attrname):
+        return getattr(self.cnx, attrname)
+
+    def __enter__(self):
+        return self.cnx.__enter__()
+
+    def __exit__(self, exctype, exc, tb):
+        try:
+            return self.cnx.__exit__(exctype, exc, tb)
+        finally:
+            self.cnx.close()
+            self.testcase.restore_connection()
+
 # base class for cubicweb tests requiring a full cw environments ###############
 
 class CubicWebTC(TestCase):
@@ -163,22 +188,30 @@
     appid = 'data'
     configcls = devtools.ApptestConfiguration
     reset_schema = reset_vreg = False # reset schema / vreg between tests
+    tags = TestCase.tags | Tags('cubicweb', 'cw_repo')
 
     @classproperty
     def config(cls):
-        """return the configuration object. Configuration is cached on the test
-        class.
+        """return the configuration object
+
+        Configuration is cached on the test class.
         """
         try:
             return cls.__dict__['_config']
         except KeyError:
-            config = cls._config = cls.configcls(cls.appid)
+            home = join(dirname(sys.modules[cls.__module__].__file__), cls.appid)
+            config = cls._config = cls.configcls(cls.appid, apphome=home)
             config.mode = 'test'
             return config
 
     @classmethod
     def init_config(cls, config):
-        """configuration initialization hooks. You may want to override this."""
+        """configuration initialization hooks.
+
+        You may only want to override here the configuraton logic.
+
+        Otherwise, consider to use a different :class:`ApptestConfiguration`
+        defined in the `configcls` class attribute"""
         source = config.sources()['system']
         cls.admlogin = unicode(source['db-user'])
         cls.admpassword = source['db-password']
@@ -200,8 +233,9 @@
         config.global_set_option('default-dest-addrs', send_to)
         config.global_set_option('sender-name', 'cubicweb-test')
         config.global_set_option('sender-addr', 'cubicweb-test@logilab.fr')
+        # default_base_url on config class isn't enough for TestServerConfiguration
+        config.global_set_option('base-url', config.default_base_url())
         # web resources
-        config.global_set_option('base-url', devtools.BASE_URL)
         try:
             config.global_set_option('embed-allowed', re.compile('.*'))
         except: # not in server only configuration
@@ -266,10 +300,13 @@
     # default test setup and teardown #########################################
 
     def setUp(self):
+        # monkey patch send mail operation so emails are sent synchronously
+        self._old_mail_commit_event = SendMailOp.commit_event
+        SendMailOp.commit_event = SendMailOp.sendmails
         pause_tracing()
         previous_failure = self.__class__.__dict__.get('_repo_init_failed')
         if previous_failure is not None:
-            self.skip('repository is not initialised: %r' % previous_failure)
+            self.skipTest('repository is not initialised: %r' % previous_failure)
         try:
             self._init_repo()
         except Exception, ex:
@@ -287,6 +324,7 @@
         for cnx in self._cnxs:
             if not cnx._closed:
                 cnx.close()
+        SendMailOp.commit_event = self._old_mail_commit_event
 
     def setup_database(self):
         """add your database setup code by overriding this method"""
@@ -313,7 +351,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
@@ -322,14 +360,18 @@
         """return a connection for the given login/password"""
         if login == self.admlogin:
             self.restore_connection()
-        else:
-            if not kwargs:
-                kwargs['password'] = str(login)
-            self.cnx = repo_connect(self.repo, unicode(login), **kwargs)
-            self.websession = DBAPISession(self.cnx)
-            self._cnxs.append(self.cnx)
+            # definitly don't want autoclose when used as a context manager
+            return self.cnx
+        autoclose = kwargs.pop('autoclose', True)
+        if not kwargs:
+            kwargs['password'] = str(login)
+        self.cnx = repo_connect(self.repo, unicode(login), **kwargs)
+        self.websession = DBAPISession(self.cnx)
+        self._cnxs.append(self.cnx)
         if login == self.vreg.config.anonymous_user()[0]:
             self.cnx.anonymous_connection = True
+        if autoclose:
+            return TestCaseConnectionProxy(self, self.cnx)
         return self.cnx
 
     def restore_connection(self):
@@ -499,9 +541,11 @@
         return publisher
 
     requestcls = fake.FakeRequest
-    def request(self, *args, **kwargs):
+    def request(self, rollbackfirst=False, **kwargs):
         """return a web ui request"""
         req = self.requestcls(self.vreg, form=kwargs)
+        if rollbackfirst:
+            self.websession.cnx.rollback()
         req.set_session(self.websession)
         return req
 
@@ -527,6 +571,30 @@
             raise
         return result
 
+    def req_from_url(self, url):
+        """parses `url` and builds the corresponding CW-web request
+
+        req.form will be setup using the url's query string
+        """
+        req = self.request()
+        if isinstance(url, unicode):
+            url = url.encode(req.encoding) # req.setup_params() expects encoded strings
+        querystring = urlparse.urlparse(url)[-2]
+        params = urlparse.parse_qs(querystring)
+        req.setup_params(params)
+        return req
+
+    def url_publish(self, url):
+        """takes `url`, uses application's app_resolver to find the
+        appropriate controller, and publishes the result.
+
+        This should pretty much correspond to what occurs in a real CW server
+        except the apache-rewriter component is not called.
+        """
+        req = self.req_from_url(url)
+        ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
+        return self.ctrl_publish(req, ctrlid)
+
     def expect_redirect(self, callback, req):
         """call the given callback with req as argument, expecting to get a
         Redirect exception
@@ -573,18 +641,18 @@
         sh = self.app.session_handler
         path, params = self.expect_redirect(lambda x: self.app.connect(x), req)
         session = req.session
-        self.assertEquals(len(self.open_sessions), nbsessions, self.open_sessions)
-        self.assertEquals(session.login, origsession.login)
-        self.assertEquals(session.anonymous_session, False)
-        self.assertEquals(path, 'view')
-        self.assertEquals(params, {'__message': 'welcome %s !' % req.user.login})
+        self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
+        self.assertEqual(session.login, origsession.login)
+        self.assertEqual(session.anonymous_session, False)
+        self.assertEqual(path, 'view')
+        self.assertEqual(params, {'__message': 'welcome %s !' % req.user.login})
 
     def assertAuthFailure(self, req, nbsessions=0):
         self.app.connect(req)
         self.assertIsInstance(req.session, DBAPISession)
-        self.assertEquals(req.session.cnx, None)
-        self.assertEquals(req.cnx, None)
-        self.assertEquals(len(self.open_sessions), nbsessions)
+        self.assertEqual(req.session.cnx, None)
+        self.assertEqual(req.cnx, None)
+        self.assertEqual(len(self.open_sessions), nbsessions)
         clear_cache(req, 'get_authorization')
 
     # content validation #######################################################
@@ -620,7 +688,7 @@
              **kwargs):
         """This method tests the view `vid` on `rset` using `template`
 
-        If no error occured while rendering the view, the HTML is analyzed
+        If no error occurred while rendering the view, the HTML is analyzed
         and parsed.
 
         :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo`
@@ -633,10 +701,10 @@
         view = viewsreg.select(vid, req, **kwargs)
         # set explicit test description
         if rset is not None:
-            self.set_description("testing %s, mod=%s (%s)" % (
+            self.set_description("testing vid=%s defined in %s with (%s)" % (
                 vid, view.__module__, rset.printable_rql()))
         else:
-            self.set_description("testing %s, mod=%s (no rset)" % (
+            self.set_description("testing vid=%s defined in %s without rset" % (
                 vid, view.__module__))
         if template is None: # raw view testing, no template
             viewfunc = view.render
@@ -652,7 +720,7 @@
     def _test_view(self, viewfunc, view, template='main-template', kwargs={}):
         """this method does the actual call to the view
 
-        If no error occured while rendering the view, the HTML is analyzed
+        If no error occurred while rendering the view, the HTML is analyzed
         and parsed.
 
         :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo`
@@ -704,7 +772,7 @@
             validatorclass = self.content_type_validators.get(view.content_type,
                                                               default_validator)
         if validatorclass is None:
-            return None
+            return output.strip()
         validator = validatorclass()
         if isinstance(validator, htmlparser.DTDValidator):
             # XXX remove <canvas> used in progress widget, unknown in html dtd
@@ -786,6 +854,8 @@
     """base class for test with auto-populating of the database"""
     __abstract__ = True
 
+    tags = CubicWebTC.tags | Tags('autopopulated')
+
     pdbclass = CubicWebDebugger
     # this is a hook to be able to define a list of rql queries
     # that are application dependent and cannot be guessed automatically
@@ -842,6 +912,7 @@
             except ValidationError, ex:
                 # failed to satisfy some constraint
                 print 'error in automatic db population', ex
+                self.session.commit_state = None # reset uncommitable flag
         self.post_populate(cu)
         self.commit()
 
@@ -911,6 +982,9 @@
 
 class AutomaticWebTest(AutoPopulateTest):
     """import this if you wan automatic tests to be ran"""
+
+    tags = AutoPopulateTest.tags | Tags('web', 'generated')
+
     def setUp(self):
         AutoPopulateTest.setUp(self)
         # access to self.app for proper initialization of the authentication
--- a/doc/book/README	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/README	Wed Nov 03 16:38:28 2010 +0100
@@ -46,9 +46,40 @@
 .. [foot note] the foot note content
 
 
+Boxes
+=====
 
-XXX
-* lien vers cw.cwconfig.CW_CUBES_PATH par ex.
+- warning box: 
+    .. warning::
+
+       Warning content
+- note box:
+    .. note::
+
+       Note content
+
 
 
-automodule, autofunction, automethod, autofunction
+Cross references
+================
+
+To arbitrary section
+--------------------
+
+:ref:`identifier` ou :ref:`label <identifier>`
+
+Label required of referencing node which as no title, else the node's title will be used.
+
+
+To API objects
+--------------
+See the autodoc sphinx extension documentation. Quick overview:
+
+* ref to a class: :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+
+* if you can to see only the class name in the generated documentation, add a ~:
+  :class:`~cubicweb.devtools.testlib.AutomaticWebTest`
+
+* you can also use :mod: (module), :exc: (exception), :func: (function), :meth: (method)...
+
+* syntax explained above to specify label explicitly may also be used
--- a/doc/book/_maybe_to_integrate/treemixin.rst	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-
-Class `TreeMixIn`
------------------
-
-This class provides a tree interface. This mixin has to be inherited 
-explicitly and configured using the tree_attribute, parent_target and 
-children_target class attribute to benefit from this default implementation.
-
-This class provides the following methods:
-
-  * `different_type_children(entities=True)`, returns children entities
-    of different type as this entity. According to the `entities` parameter, 
-    returns entity objects (if entity=True) or the equivalent result set.
-
-  * `same_type_children(entities=True)`, returns children entities of 
-    the same type as this entity. According to the `entities` parameter, 
-    return entity objects (if entity=True) or the equivalent result set.
-  
-  * `iterchildren( _done=None)`, iters on the children of the entity.
-  
-  * `prefixiter( _done=None)`
-  
-  * `path()`, returns the list of eids from the root object to this object.
-  
-  * `iterparents()`, iters on the parents of the entity.
-  
-  * `notification_references(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.
-
-`TreeMixIn` implements also the ITree interface (``cubicweb.interfaces``):
-
-  * `parent()`, returns the parent entity if any, else None (e.g. if we are on the
-    root)
-
-  * `children(entities=True, sametype=False)`, returns children entities
-    according to the `entities` parameter, return entity objects or the
-    equivalent result set.
-
-  * `children_rql()`, returns the RQL query corresponding to the children
-    of the entity.
-
-  * `is_leaf()`, returns True if the entity does not have any children.
-
-  * `is_root()`, returns True if the entity does not have any parent.
-
-  * `root()`, returns the root object of the tree representation of
-    the entity and its related entities.
-
-Example of use
-``````````````
-
-Imagine you defined three types of entities in your schema, and they
-relates to each others as follows in ``schema.py``::
-
-  class Entity1(EntityType):
-      title = String()
-      is_related_to = SubjectRelation('Entity2', 'subject')
-
-  class Entity2(EntityType):
-      title = String()
-      belongs_to = SubjectRelation('Entity3', 'subject')
-
-  class Entity3(EntityType):
-      name = String()
-
-You would like to create a view that applies to both entity types
-`Entity1` and `Entity2` and which lists the entities they are related to.
-That means when you view `Entity1` you want to list all `Entity2`, and
-when you view `Entity2` you want to list all `Entity3`.
-
-In ``entities.py``::
-
-  class Entity1(TreeMixIn, AnyEntity):
-      id = 'Entity1'
-      __implements__ = AnyEntity.__implements__ + (ITree,)
-      __rtags__ = {('is_related_to', 'Entity2', 'object'): 'link'}
-      tree_attribute = 'is_related_to'
-
-      def children(self, entities=True):
-          return self.different_type_children(entities)
-
-  class Entity2(TreeMixIn, AnyEntity):
-      id = 'Entity2'
-      __implements__ = AnyEntity.__implements__ + (ITree,)
-      __rtags__ = {('belongs_to', 'Entity3', 'object'): 'link'}
-      tree_attribute = 'belongs_to'
-
-      def children(self, entities=True):
-          return self.different_type_children(entities)
-
-Once this is done, you can define your common view as follows::
-
-  class E1E2CommonView(baseviews.PrimaryView):
-      accepts = ('Entity11, 'Entity2')
-      
-      def render_entity_relations(self, entity, siderelations):
-          self.wview('list', entity.children(entities=False))
-
--- a/doc/book/en/admin/setup.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/admin/setup.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -59,8 +59,27 @@
 .. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
 
 
+.. _PipInstallation:
+
+Installation with pip
+`````````````````````
+
+|cubicweb| and its cubes have been pip_ installable since version 3.8. Search
+for them on pypi_::
+
+  pip install cubicweb
+
+.. _pip: http://pypi.python.org/pypi/pip
+.. _pypi: http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb
+
 .. _SourceInstallation:
 
+.. warning::
+
+  This method may still have hiccups. If it does not work for you,
+  please consider installing from version control system
+  (:ref:`MercurialInstallation`).
+
 Install from source
 ```````````````````
 
@@ -70,31 +89,46 @@
 
 .. _`ftp site`: http://ftp.logilab.org/pub/cubicweb/
 
-Make sure you have installed the dependencies (see appendixes for the list).
+Make sure you also have all the :ref:`InstallDependencies`.
 
-|cubicweb| should soon be pip_ installable, stay tuned (expected in 3.8).
-
-.. _pip: http://pypi.python.org/pypi/pip
-
+.. _MercurialInstallation:
 
 Install from version control system
 ```````````````````````````````````
 
-You can keep up to date with on-going development by using Mercurial and its
-forest extension::
+You can keep up to date with on-going development by using Mercurial::
 
-  hg fclone http://www.logilab.org/hg/forests/cubicweb
+  hg clone http://www.logilab.org/hg/forests/cubicweb
 
 See :ref:`MercurialPresentation` for more details about Mercurial.
 
+A practical way to get many of CubicWeb's dependencies and a nice set
+of base cubes is to run the `clone_deps.py` script located in
+`cubicweb/bin/`::
+
+  python cubicweb/bin/clone_deps.py
+
+(Windows users should replace slashes with antislashes).
+
+This script will clone a set of mercurial repositories into in the
+directory containing the CubicWeb repository, and update them to the
+latest published version tag (if any).
+
 When cloning a repository, you might be set in a development branch
 (the 'default' branch). You should check that the branches of the
 repositories are set to 'stable' (using `hg up stable` for each one)
 if you do not intend to develop the framework itself.
 
-Do not forget to update the forest itself (using `cd path/to/forest ; hg up`).
+Even better, `hg tags` will display a list of tags in reverse
+chronological order. One reasonnable way to get to a working version
+is to pick the latest published version (as done by the `clone_deps`
+script). These look like `cubicweb-debian-version-3.9.7-1`. Typing::
 
-Make sure you have installed the dependencies (see appendixes for the list).
+ hg update cubicweb-debian-version-3.9.7-1
+
+will update the repository files to this version.
+
+Make sure you also have all the :ref:`InstallDependencies`.
 
 
 .. _WindowsInstallation:
@@ -102,6 +136,10 @@
 Windows installation
 ````````````````````
 
+Your best option is probably the :ref:`PipInstallation`. If it does not work or
+if you want more control over the process, continue with the following
+instructions.
+
 Base elements
 ~~~~~~~~~~~~~
 
@@ -110,14 +148,15 @@
 done. We assume everything goes into `C:\\` in this document. Adjusting the
 installation drive should be straightforward.
 
-You should start by downloading and installing the Python(x,y) distribution. It
-contains python 2.5 plus numerous useful third-party modules and applications::
+You should start by downloading and installing Python version >= 2.5 and < 3.
 
-  http://www.pythonxy.com/download_fr.php
+An alternative option would be installing the Python(x,y)
+distribution. Python(x,y) is not a requirement, but it makes things easier for
+Windows user by wrapping in a single installer python 2.5 plus numerous useful
+third-party modules and applications (including Eclipse + pydev, which is an
+arguably good IDE for Python under Windows). Download it from this page::
 
-At the time of this writting, one gets version 2.1.15. Among the many things
-provided, one finds Eclipse + pydev (an arguably good IDE for python under
-windows).
+  http://code.google.com/p/pythonxy/wiki/Downloads
 
 Then you must grab Twisted. There is a windows installer directly available from
 this page::
@@ -166,11 +205,14 @@
 
   http://www.graphviz.org/Download_windows.php
 
-Simplejson will be provided within the forest, but a win32 compiled version will
-run much faster::
+Simplejson is needed when installing with Python 2.5, but included in the
+standard library for Python >= 2.6. Get it from there::
 
   http://www.osuch.org/python-simplejson%3Awin32
 
+Make sure you also have all the :ref:`InstallDependencies` that are not specific
+to Windows.
+
 Tools
 ~~~~~
 
@@ -189,32 +231,13 @@
 
   http://www.vectrace.com/mercurialeclipse/
 
-Setting up the sources
-~~~~~~~~~~~~~~~~~~~~~~
-
-You need to enable the mercurial forest extension. To do this, edit the file::
-
-  C:\Program Files\TortoiseHg\Mercurial.ini
-
-In the [extensions] section, add the following line::
-
-  forest=C:\Program Files\TortoiseHg\ext\forest\forest.py
+Getting the sources
+~~~~~~~~~~~~~~~~~~~
 
-Now, you need to clone the cubicweb repository. We assume that you use
-Eclipse. From the IDE, choose File -> Import. In the box, select `Mercurial/Clone
-repository using MercurialEclipse`.
-
-In the import main panel you just have to:
-
-* fill the URL field with http://www.logilab.org/hg/forests/cubicwin32
-
-* check the 'Repository is a forest' box.
-
-Then, click on 'Finish'. It might take some time to get it all. Note that the
-`cubicwin32` forest contains additional python packages such as yapps, vobject,
-simplejson and twisted-web2 which are not provided with Python(x,y). This is
-provided for convenience, as we do not ensure the up-to-dateness of these
-packages, especially with respect to security fixes.
+You can either download the latest release (see
+:ref:`SourceInstallation`) or get the development version using
+Mercurial (see :ref:`MercurialInstallation` and below), which is more
+convenient.
 
 Environment variables
 ~~~~~~~~~~~~~~~~~~~~~
@@ -250,14 +273,14 @@
 This currently assumes that the instances configurations is located at
 C:\\etc\\cubicweb.d.
 
-For a cube 'my_cube', you will then find
-C:\\etc\\cubicweb.d\\my_cube\\win32svc.py that has to be used thusly::
+For a cube 'my_instance', you will then find
+C:\\etc\\cubicweb.d\\my_instance\\win32svc.py that has to be used as follows::
 
   win32svc install
 
 This should just register your instance as a windows service. A simple::
 
-  net start cubicweb-my_cube
+  net start cubicweb-my_instance
 
 should start the service.
 
@@ -280,9 +303,17 @@
 Databases configuration
 -----------------------
 
-Whatever the backend used, database connection information are stored in the
-instance's :file:`sources` file. Currently cubicweb has been tested using
-Postgresql (recommanded), MySQL, SQLServer and SQLite.
+Each instance can be configured with its own database connection information,
+that will be stored in the instance's :file:`sources` file. The database to use
+will be chosen when creating the instance. Currently cubicweb has been tested
+using Postgresql (recommended), MySQL, SQLServer and SQLite.
+
+Other possible sources of data include CubicWeb, Subversion, LDAP and Mercurial,
+but at least one relational database is required for CubicWeb to work. You do
+not need to install a backend that you do not intend to use for one of your
+instances. SQLite is not fit for production use, but it works well for testing
+and ships with Python, which saves installation time when you want to get
+started quickly.
 
 .. _PostgresqlConfiguration:
 
@@ -394,7 +425,7 @@
     max_allowed_packet = 128M
 
 .. Note::
-    It is unclear whether mysql supports indexed string of arbitrary lenght or
+    It is unclear whether mysql supports indexed string of arbitrary length or
     not.
 
 
@@ -403,9 +434,10 @@
 SQLServer configuration
 ```````````````````````
 
-As of this writing, sqlserver support is in progress. You should be able to
-connect, create a database and go quite far, but some of the generated SQL is
-still currently not accepted by the backend.
+As of this writing, support for SQLServer 2005 is functional but incomplete. You
+should be able to connect, create a database and go quite far, but some of the
+SQL generated from RQL queries is still currently not accepted by the
+backend. Porting to SQLServer 2008 is also an item on the backlog.
 
 The `source` configuration file may look like this (specific parts only are
 shown)::
@@ -440,14 +472,13 @@
 ------------------
 
 If you want to use Pyro to access your instance remotly, or to have multi-source
-or distributed configuration, it is required to have a name server Pyro running
-on your network. By by default it is detected by a broadcast request, but you can
+or distributed configuration, it is required to have a Pyro name server running
+on your network. By default it is detected by a broadcast request, but you can
 specify a location in the instance's configuration file.
 
 To do so, you need to :
 
-* launch the server manually before starting cubicweb as a server with `pyro-nsd
-  start`
+* launch the pyro name server with `pyro-nsd start` before starting cubicweb
 
 * under debian, edit the file :file:`/etc/default/pyro-nsd` so that the name
   server pyro will be launched automatically when the machine fire up
--- a/doc/book/en/annexes/depends.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/annexes/depends.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -1,9 +1,9 @@
 .. -*- coding: utf-8 -*-
 
-.. _dependencies:
+.. _InstallDependencies:
 
-Dependencies
-============
+Installation dependencies
+=========================
 
 When you run CubicWeb from source, either by downloading the tarball or
 cloning the mercurial forest, here is the list of tools and libraries you need
@@ -27,6 +27,9 @@
 * logilab-common - http://www.logilab.org/project/logilab-common -
   http://pypi.python.org/pypi/logilab-common/ - included in the forest
 
+* logilab-database - http://www.logilab.org/project/logilab-database -
+  http://pypi.python.org/pypi/logilab-database/ - included in the forest
+
 * logilab-constraint - http://www.logilab.org/project/logilab-constraint -
   http://pypi.python.org/pypi/constraint/ - included in the forest
 
@@ -44,7 +47,7 @@
 
 To use network communication between cubicweb instances / clients:
 
-* Pyro - http://pyro.sourceforge.net/ - http://pypi.python.org/pypi/Pyro
+* Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro
 
 If you're using a Postgres database (recommended):
 
@@ -52,9 +55,7 @@
 * plpythonu extension
 * tsearch2 extension (for postgres < 8.3, in postgres-contrib)
 
-Other optional packages :
-
-:
+Other optional packages:
 
 * fyzz - http://www.logilab.org/project/fyzz -
   http://pypi.python.org/pypi/fyzz - included in the forest, *to activate Sparql querying*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/annexes/docstrings-conventions.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,106 @@
+Javascript docstrings
+=====================
+
+Whereas in Python source code we only need to include a module docstrings
+using the directive `.. automodule:: mypythonmodule`, we will have to
+explicitely define Javascript modules and functions in the doctrings since
+there is no native directive to include Javascript files.
+
+Rest generation
+---------------
+
+`pyjsrest` is a small utility parsing Javascript doctrings and generating the
+corresponding Restructured file used by Sphinx to generate HTML documentation.
+This script will have the following structure::
+
+  ===========
+  filename.js
+  ===========
+  .. module:: filename.js
+
+We use the `.. module::` directive to register a javascript library
+as a Python module for Sphinx. This provides an entry in the module index.
+
+The contents of the docstring found in the javascript file will be added as is
+following the module declaration. No treatment will be done on the doctring.
+All the documentation structure will be in the docstrings and will comply
+with the following rules.
+
+Docstring structure
+-------------------
+
+Basically we document javascript with RestructuredText docstring
+following the same convention as documenting Python code.
+
+The doctring in Javascript files must be contained in standard
+Javascript comment signs, starting with `/**` and ending with `*/`,
+such as::
+
+ /**
+  * My comment starts here.
+  * This is the second line prefixed with a `*`.
+  * ...
+  * ...
+  * All the follwing line will be prefixed with a `*` followed by a space.
+  * ...
+  * ...
+  */
+
+
+Comments line prefixed by `//` will be ignored. They are reserved for source
+code comments dedicated to developers.
+
+
+Javscript functions docstring
+-----------------------------
+
+By default, the `function` directive describes a module-level function.
+
+`function` directive
+~~~~~~~~~~~~~~~~~~~~
+
+Its purpose is to define the function prototype such as::
+
+    .. function:: loadxhtml(url, data, reqtype, mode)
+
+If any namespace is used, we should add it in the prototype for now,
+until we define an appropriate directive::
+
+    .. function:: jQuery.fn.loadxhtml(url, data, reqtype, mode)
+
+Function parameters
+~~~~~~~~~~~~~~~~~~~
+
+We will define function parameters as a bulleted list, where the
+parameter name will be backquoted and followed by its description.
+
+Example of a javascript function docstring::
+
+    .. function:: loadxhtml(url, data, reqtype, mode)
+
+    cubicweb loadxhtml plugin to make jquery handle xhtml response
+
+    fetches `url` and replaces this's content with the result
+
+    Its arguments are:
+
+    * `url`
+
+    * `mode`, how the replacement should be done (default is 'replace')
+       Possible values are :
+           - 'replace' to replace the node's content with the generated HTML
+           - 'swap' to replace the node itself with the generated HTML
+           - 'append' to append the generated HTML to the node's content
+
+
+Optional parameter specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Javascript functions handle arguments not listed in the function signature.
+In the javascript code, they will be flagged using `/* ... */`. In the docstring,
+we flag those optional arguments the same way we would define it in
+Python::
+
+    .. function:: asyncRemoteExec(fname, arg1=None, arg2=None)
+
+
--- a/doc/book/en/annexes/faq.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/annexes/faq.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -115,7 +115,7 @@
 
     from cubicweb import dbapi
 
-    cnx = dbapi.connection(database='instance-id', user='admin', password='admin')
+    cnx = dbapi.connect(database='instance-id', user='admin', password='admin')
     cur = cnx.cursor()
     for name in ('Personal', 'Professional', 'Computers'):
         cur.execute('INSERT Blog B: B name %s', name)
@@ -302,10 +302,10 @@
     import pwd
     import sys
 
-    from logilab.common.db import get_connection
+    from logilab.database import get_connection
 
     def getlogin():
-        """avoid usinng os.getlogin() because of strange tty / stdin problems
+        """avoid using os.getlogin() because of strange tty/stdin problems
         (man 3 getlogin)
         Another solution would be to use $LOGNAME, $USER or $USERNAME
         """
@@ -402,6 +402,31 @@
     mydb=> update cw_cwuser set cw_upassword='qHO8282QN5Utg' where cw_login='joe';
     UPDATE 1
 
+if you're running over SQL Server, you need to use the CONVERT
+function to convert the string to varbinary(255). The SQL query is
+therefore::
+
+    update cw_cwuser set cw_upassword=CONVERT(varbinary(255), 'qHO8282QN5Utg') where cw_login='joe';
+
+Be careful, the encryption algorithm is different on Windows and on
+Unix. You cannot therefore use a hash generated on Unix to fill in a
+Windows database, nor the other way round. 
+
+
+You can prefer use a migration script similar to this shell invocation instead::
+
+    $ cubicweb-ctl shell <instance>
+    >>> from cubicweb.server.utils import crypt_password
+    >>> crypted = crypt_password('joepass')
+    >>> rset = rql('Any U WHERE U is CWUser, U login "joe"')
+    >>> joe = rset.get_entity(0,0)
+    >>> joe.set_attributes(upassword=crypted)
+
+The more experimented people would use RQL request directly::
+
+    >>> rql('SET X upassword %(a)s WHERE X is CWUser, X login "joe"',
+    ...     {'a': crypted})
+
 I've just created a user in a group and it doesn't work !
 ---------------------------------------------------------
 
--- a/doc/book/en/annexes/index.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/annexes/index.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -17,3 +17,5 @@
    rql/index
    mercurial
    depends
+   javascript-api
+   docstrings-conventions
Binary file doc/book/en/annexes/rql/Graph-ex.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/annexes/rql/debugging.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,56 @@
+.. -*- coding: utf-8 -*-
+
+.. _DEBUGGING:
+
+Debugging RQL
+-------------
+
+Available levels
+~~~~~~~~~~~~~~~~
+
+:DBG_NONE:
+    no debug information (current mode)
+
+:DBG_RQL:
+    rql execution information
+
+:DBG_SQL:
+    executed sql
+
+:DBG_REPO:
+    repository events
+
+:DBG_MS:
+    multi-sources
+
+:DBG_MORE:
+    more verbosity
+
+:DBG_ALL:
+    all level enabled
+
+
+Enable verbose output
+~~~~~~~~~~~~~~~~~~~~~
+
+It may be interested to enable a verboser output to debug your RQL statements:
+
+.. sourcecode:: python
+
+    from cubicweb import server
+    server.set_debug(server.DBG_RQL|server.DBG_SQL|server.DBG_ALL)
+
+
+Detect largest RQL queries
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+See `Profiling and performance` chapter (see :ref:`PROFILING`).
+
+
+API
+~~~
+
+.. autofunction:: cubicweb.server.set_debug
+
+.. autoclass:: cubicweb.server.debugged
+
--- a/doc/book/en/annexes/rql/index.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/annexes/rql/index.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -4,8 +4,9 @@
 This chapter describes the Relation Query Language syntax and its implementation in CubicWeb.
 
 .. toctree::
-   :maxdepth: 1
+   :maxdepth: 2
 
    intro
    language
+   debugging
    implementation
--- a/doc/book/en/annexes/rql/intro.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/annexes/rql/intro.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -7,8 +7,13 @@
 Goals of RQL
 ~~~~~~~~~~~~
 
-The goal is to have a language making relations browsing easy. As
-such, attributes will be regarded as cases of special relations (in
+The goal is to have a semantic language in order to:
+
+- query relations in a clear syntax
+- empowers access to data repository manipulation
+- making attributes/relations browsing easy
+
+As such, attributes will be regarded as cases of special relations (in
 terms of usage, the user should see no syntactic difference between an
 attribute and a relation).
 
@@ -40,6 +45,13 @@
 conversion and basic types manipulation, which we may want to look at one time
 or another.  Finally, the syntax is a little esoteric.
 
+Datalog
+```````
+
+Datalog_ is a prolog derived query langage which applies to relational
+databases. It is more expressive than RQL in that it accepts either
+extensional_ and intensional_ predicates (or relations). As of now,
+RQL only deals with intensional relations.
 
 The different types of queries
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -59,7 +71,91 @@
    Remove entities or relations existing in the database.
 
 
+RQL relation expressions
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+RQL expressions apply to a live database defined by a
+:ref:`datamodel_definition`. Apart from the main type, or head, of the
+expression (search, insert, etc.) the most common constituent of an
+RQL expression is a (set of) relation expression(s).
+
+An RQL relation expression contains three components:
+
+* the subject, which is an entity type
+* the predicate, which is a relation definition (an arc of the schema)
+* the object, which is either an attribute or a relation to another entity
+
+.. image:: Graph-ex.gif
+    :alt: <subject> <predicate> <object>
+    :align: center
+
+.. warning::
+
+ A relation is always expressed in the order: ``subject``,
+ ``predicate``, ``object``.
+
+ It is important to determine if the entity type is subject or object
+ to construct a valid expression. Inverting the subject/object is an
+ error since the relation cannot be found in the schema.
+
+ If one does not have access to the code, one can find the order by
+ looking at the schema image in manager views (the subject is located
+ at the beginning of the arrow).
+
+An example of two related relation expressions::
+
+  P works_for C, P name N
+
+RQL variables represent typed entities. The type of entities is
+either automatically inferred (by looking at the possible relation
+definitions, see :ref:`RelationDefinition`) or explicitely constrained
+using the ``is`` meta relation.
+
+In the example above, we barely need to look at the schema. If
+variable names (in the RQL expression) and relation type names (in the
+schema) are expresssively designed, the human reader can infer as much
+as the |cubicweb| querier.
+
+The ``P`` variable is used twice but it always represent the same set
+of entities. Hence ``P works_for C`` and ``P name N`` must be
+compatible in the sense that all the Ps (which *can* refer to
+different entity types) must accept the ``works_for`` and ``name``
+relation types. This does restrict the set of possible values of P.
+
+Adding another relation expression::
+
+  P works_for C, P name N, C name "logilab"
+
+This further restricts the possible values of P through an indirect
+constraint on the possible values of ``C``. The RQL-level unification_
+happening there is translated to one (or several) joins_ at the
+database level.
+
+.. note::
+
+ In |cubicweb|, the term `relation` is often found without ambiguity
+ instead of `predicate`.  This predicate is also known as the
+ `property` of the triple in `RDF concepts`_
 
 
-.. _Versa: http://uche.ogbuji.net/tech/rdf/versa/
+RQL Operators
+~~~~~~~~~~~~~
+
+An RQL expression's head can be completed using various operators such
+as ``ORDERBY``, ``GROUPBY``, ``HAVING``, ``LIMIT`` etc.
+
+RQL relation expressions can be grouped with ``UNION`` or
+``WITH``. Predicate oriented keywords such as ``EXISTS``, ``OR``,
+``NOT`` are available.
+
+The complete zoo of RQL operators is described extensively in the
+following chapter (:ref:`RQL`).
+
+.. _RDF concepts: http://www.w3.org/TR/rdf-concepts/
+.. _Versa: http://wiki.xml3k.org/Versa
 .. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
+.. _unification: http://en.wikipedia.org/wiki/Unification_(computing)
+.. _joins: http://en.wikipedia.org/wiki/Join_(SQL)
+.. _Datalog: http://en.wikipedia.org/wiki/Datalog
+.. _intensional: http://en.wikipedia.org/wiki/Intensional_definition
+.. _extensional: http://en.wikipedia.org/wiki/Extension_(predicate_logic)
--- a/doc/book/en/annexes/rql/language.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/annexes/rql/language.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -15,6 +15,7 @@
   HAVING, ILIKE, IN, INSERT, LIKE, LIMIT, NOT, NOW, NULL, OFFSET,
   OR, ORDERBY, SET, TODAY, TRUE, UNION, WHERE, WITH
 
+
 Variables and Typing
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -29,10 +30,11 @@
 There is a special type **Any**, referring to a non specific type.
 
 We can restrict the possible types for a variable using the
-special relation **is**.
+special relation **is** in the constraints.
+
 The possible type(s) for each variable is derived from the schema
-according to the constraints expressed above and thanks to the relations between
-each variable.
+according to the constraints expressed above and thanks to the relations
+between each variable.
 
 Built-in types
 ``````````````
@@ -63,7 +65,7 @@
   of logical operators (see :ref:`PriorityOperators`).
 
 Mathematical Operators
-```````````````````````
+``````````````````````
 ::
 
      +, -, *, /
@@ -74,7 +76,13 @@
 
      =, <, <=, >=, >, ~=, IN, LIKE, ILIKE
 
-* The operator `=` is the default operator.
+* Syntax to use comparison operator:
+
+    `VARIABLE relation operator VALUE`
+
+* The operator `=` is the default operator and can be omitted.
+
+* `relation` name is always attended
 
 * The operator `LIKE` equivalent to `~=` can be used with the
   special character `%` in a string to indicate that the chain
@@ -89,7 +97,7 @@
 * The operator `IN` provides a list of possible values:
   ::
 
-    Any X WHERE X name IN ( 'chauvat', 'fayolle', 'di mascio', 'thenault')
+    Any X WHERE X name IN ('chauvat', 'fayolle', 'di mascio', 'thenault')
 
 
 .. XXX nico: "A trick <> 'bar'" wouldn't it be more convenient than "NOT A trick 'bar'" ?
@@ -99,17 +107,13 @@
 Operators priority
 ``````````````````
 
-1. '*', '/'
-
-2. '+', '-'
-
-3. 'not'
-
-4 'and'
-
-5 'or'
-
-6 ','
+#. "(", ")"
+#. '*', '/'
+#. '+', '-'
+#. 'NOT'
+#. 'AND'
+#. 'OR'
+#. ','
 
 
 Search Query
@@ -141,25 +145,53 @@
 ``````````````````
 
 - For grouped queries (e.g. with a GROUPBY clause), all
-  selected variables should be grouped.
-
-- To group and/or sort by attributes, we can do: "X,L user U, U
-  login L GROUPBY L, X ORDERBY L"
+  selected variables should be grouped at the right of the keyword.
 
 - If the sorting method (SORT_METHOD) is not specified, then the sorting is
-  ascendant.
+  ascendant (`ASC`).
+
+- Aggregate Functions: COUNT, MIN, MAX, AVG, SUM, GROUP_CONCAT
+
+Having
+``````
+
+The HAVING clause, as in SQL, has been originally introduced to restrict a query
+according to value returned by an aggregate function, e.g.::
+
+    Any X GROUPBY X WHERE X relation Y HAVING COUNT(Y) > 10
+
+It may however be used for something else...
 
-- Aggregate Functions: COUNT, MIN, MAX, AVG, SUM
+In the WHERE clause, we are limited to 3-expression, such thing can't be
+expressed directly as in the SQL's way. But this can be expressed using HAVING
+comparison expression.
+
+For instance, let's say you want to get people whose uppercased first name equals
+to another person uppercased first name::
+
+    Person X WHERE X firstname XFN, Y firstname YFN HAVING X > Y, UPPER(XFN) = UPPER(YFN)
 
+This open some new possibilities. Another example::
+
+    Person X WHERE X birthday XB HAVING YEAR(XB) = 2000
+
+That lets you use transformation functions not only in selection but for
+restriction as well and to by-pass limitation of the WHERE clause, which was the
+major flaw in the RQL language.
+
+Notice that while we would like this to work without the HAVING clause, this
+can't be currently be done because it introduces an ambiguity in RQL's grammar
+that can't be handled by Yapps_, the parser's generator we're using.
 
 Negation
 ````````
 
-* A query such as `Document X WHERE NOT X owned_by U` means "the
-  documents have no relation `owned_by`".
-* But the query `Document X WHERE NOT X owned_by U, U login "syt"`
-  means "the documents have no relation `owned_by` with the user
-  syt". They may have a relation "owned_by" with another user.
+* A query such as `Document X WHERE NOT X owned_by U` means "the documents have
+  no relation `owned_by`".
+
+* But the query `Document X WHERE NOT X owned_by U, U login "syt"` means "the
+  documents have no relation `owned_by` with the user syt". They may have a
+  relation "owned_by" with another user.
 
 Identity
 ````````
@@ -170,9 +202,8 @@
 
    Any A WHERE A comments B, A identity B
 
-return all objects that comment themselves. The relation
-`identity` is especially useful when defining the rules for securities
-with `RQLExpressions`.
+return all objects that comment themselves. The relation `identity` is
+especially useful when defining the rules for securities with `RQLExpressions`.
 
 
 Limit / offset
@@ -181,13 +212,6 @@
 
     Any P ORDERBY N LIMIT 5 OFFSET 10 WHERE P is Person, P firstname N
 
-Function calls
-``````````````
-::
-
-    Any UPPER(N) WHERE P firstname N
-
-Functions on string: UPPER, LOWER
 
 Exists
 ``````
@@ -199,8 +223,14 @@
           OR EXISTS(T tags X, T name "priority")
 
 
-Optional relations (Left outer join)
-````````````````````````````````````
+Optional relations
+``````````````````
+
+It is a similar concept that the `Left outer join`_:
+
+    the result of a left outer join (or simply left join) for table A and B
+    always contains all records of the "left" table (A), even if the
+    join-condition does not find any matching record in the "right" table (B).
 
 * They allow you to select entities related or not to another.
 
@@ -218,12 +248,6 @@
     Any T,P,V WHERE T is Ticket, T concerns P, T done_in V?
 
 
-Having
-``````
-::
-
-    Any X GROUPBY X WHERE X knows Y HAVING COUNT(Y) > 10
-
 Subqueries
 ``````````
 ::
@@ -234,16 +258,29 @@
      DISTINCT Any W, REF
         WITH W, REF BEING
             (
-	      (Any W, REF WHERE W is Workcase, W ref REF,
+              (Any W, REF WHERE W is Workcase, W ref REF,
                                  W concerned_by D, D name "Logilab")
                UNION
               (Any W, REF WHERE W is Workcase, W ref REF, '
                                 W split_into WP, WP name "WP1")
             )
 
+Function calls
+``````````````
+::
+
+    Any UPPER(N) WHERE P firstname N
+    Any LOWER(N) WHERE P firstname N
+
+Functions available on string: `UPPER`, `LOWER`
+
+.. XXX retrieve available function automatically
+
+For a performance issue, you can enrich the RQL dialect by RDMS (Relational database management system) functions.
+
 
 Examples
-````````
+~~~~~~~~
 
 - *Search for the object of identifier 53*
   ::
@@ -280,11 +317,11 @@
         P is Person, (P interested_by T, T name 'training') OR
         (P city 'Paris')
 
-- *The name and surname of all people*
+- *The surname and firstname of all people*
   ::
 
         Any N, P WHERE
-        X is Person, X name N, X first_name P
+        X is Person, X name N, X firstname P
 
   Note that the selection of several entities generally force
   the use of "Any" because the type specification applies otherwise
@@ -304,7 +341,7 @@
 
 
 Insertion query
-~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~
 
     `INSERT` <entity type> V1 (, <entity type> V2) \ * `:` <assignments>
     [ `WHERE` <restriction>]
@@ -336,6 +373,7 @@
 
 Update and relation creation queries
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
     `SET` <assignements>
     [ `WHERE` <restriction>]
 
@@ -345,7 +383,7 @@
 - *Renaming of the person named 'foo' to 'bar' with the first name changed*
   ::
 
-        SET X name 'bar', X first_name 'original' WHERE X is Person, X name 'foo'
+        SET X name 'bar', X firstname 'original' WHERE X is Person, X name 'foo'
 
 - *Insert a relation of type 'know' between objects linked by
   the relation of type 'friend'*
@@ -356,6 +394,7 @@
 
 Deletion query
 ~~~~~~~~~~~~~~
+
     `DELETE` (<entity type> V) | (V1 relation v2 ),...
     [ `WHERE` <restriction>]
 
@@ -372,6 +411,7 @@
 
         DELETE X friend Y WHERE X is Person, X name 'foo'
 
+
 Virtual RQL relations
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -381,6 +421,13 @@
 * `has_text`: relation to use to query the full text index (only for
   entities having fulltextindexed attributes).
 
-* `identity`: relation to use to tell that a RQL variable should be
+* `identity`: `Identity`_ relation to use to tell that a RQL variable should be
   the same as another (but you've to use two different rql variables
   for querying purpose)
+
+* `is`: relation to enforce possible types for a variable
+
+
+
+.. _Yapps: http://theory.stanford.edu/~amitp/yapps/
+.. _Left outer join: http://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join
--- a/doc/book/en/conf.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/conf.py	Wed Nov 03 16:38:28 2010 +0100
@@ -32,8 +32,16 @@
 # serve to show the default value.
 
 import sys, os
+from os import path as osp
 
-from cubicweb import __pkginfo__ as cw
+path = __file__
+path = osp.dirname(path) #./doc/book/en
+path = osp.dirname(path) #./doc/book/
+path = osp.dirname(path) #./doc/
+path = osp.dirname(path) #./
+path = osp.join(path,'__pkginfo__.py') #./__pkginfo__.py
+cw = {}
+execfile(path,{},cw)
 
 # If your extensions are in another directory, add it here. If the directory
 # is relative to the documentation root, use os.path.abspath to make it
@@ -64,9 +72,9 @@
 # other places throughout the built documents.
 #
 # The short X.Y version.
-version = '.'.join(str(n) for n in cw.numversion[:2])
+version = '.'.join(str(n) for n in cw['numversion'][:2])
 # The full version, including alpha/beta/rc tags.
-release = cw.version
+release = cw['version']
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
--- a/doc/book/en/devrepo/cubes/layout.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/cubes/layout.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -108,8 +108,24 @@
 The :file:`__pkginfo__.py` file
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-It contains metadata describing your cube, mostly useful for
-packaging.
+It contains metadata describing your cube, mostly useful for packaging.
+
+Two important attributes of this module are __depends__ and __recommends__
+dictionaries that indicates what should be installed (and each version if
+necessary) for the cube to work.
+
+Dependency on other cubes are expected to be of the form 'cubicweb-<cubename>'.
+
+When an instance is created, dependencies are automatically installed, while
+recommends are not.
+
+Recommends may be seen as a kind of 'weak dependency'. Eg, the most important
+effect of recommending a cube is that, if cube A recommends cube B, the cube B
+will be loaded before the cube A (same thing happend when A depends on B).
+
+Having this behaviour is sometime desired: on schema creation, you may rely on
+something defined in the other's schema; on database creation, on something
+created by the other's postcreate, and so on.
 
 
 :file:`migration/precreate.py` and :file:`migration/postcreate.py`
--- a/doc/book/en/devrepo/datamodel/definition.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -1,5 +1,7 @@
  .. -*- coding: utf-8 -*-
 
+.. _datamodel_definition:
+
 Yams *schema*
 -------------
 
@@ -11,6 +13,8 @@
 
 .. _`Yams`: http://www.logilab.org/project/yams
 
+.. _datamodel_overview:
+
 Overview
 ~~~~~~~~
 
@@ -78,10 +82,16 @@
 a set of attributes and relations, and some permissions which define who can add, read,
 update or delete entities of this type.
 
-The following built-in types are available: ``String``, ``Int``,
-``Float``, ``Decimal``, ``Boolean``, ``Date``, ``Datetime``, ``Time``,
-``Interval``, ``Byte`` and ``Password``. They can only be used as
-attributes of an other entity type.
+The following built-in types are available: ``String``,
+``Int``, ``Float``, ``Decimal``, ``Boolean``,
+``Date``, ``Datetime``, ``Time``, ``Interval``, ``Byte`` and
+``Password``. They can only be used as attributes of an other entity
+type.
+
+There is also a `RichString` kindof type:
+
+ .. autoclass:: yams.buildobjs.RichString
+
 
 You can find more base entity types in
 :ref:`pre_defined_entity_types`.
@@ -293,36 +303,38 @@
 For *CubicWeb* in particular:
 
 * we associate rights at the entities/relations schema level
-* for each entity, we distinguish four kinds of permissions: `read`,
-  `add`, `update` and `delete`
-* for each relation, we distinguish three kinds of permissions: `read`,
-  `add` and `delete` (it is not possible to `modify` a relation)
+
 * the default groups are: `administrators`, `users` and `guests`
-* by default, users belong to the `users` group
-* there is a virtual group called `owners` to which we
-  can associate only `delete` and `update` permissions
+
+* users belong to the `users` group
+
+* there is a virtual group called `owners` to which we can associate only
+  `delete` and `update` permissions
 
-  * we can not add users to the `Owners` group, they are
-    implicitly added to it according to the context of the objects
-    they own
-  * the permissions of this group are only checked on `update`/`delete`
-    actions if all the other groups the user belongs to do not provide
-    those permissions
+  * we can not add users to the `owners` group, they are implicitly added to it
+    according to the context of the objects they own
+
+  * the permissions of this group are only checked on `update`/`delete` actions
+    if all the other groups the user belongs to do not provide those permissions
 
 Setting permissions is done with the attribute `__permissions__` of entities and
-relation types. The value of this attribute is a dictionary where the keys are the access types
-(action), and the values are the authorized groups or expressions.
+relation definition. The value of this attribute is a dictionary where the keys
+are the access types (action), and the values are the authorized groups or
+expressions.
 
 For an entity type, the possible actions are `read`, `add`, `update` and
 `delete`.
 
-For a relation type, the possible actions are `read`, `add`, and `delete`.
+For a relation, the possible actions are `read`, `add`, and `delete`.
+
+For an attribute, the possible actions are `read`, and `update`.
 
 For each access type, a tuple indicates the name of the authorized groups and/or
 one or multiple RQL expressions to satisfy to grant access. The access is
 provided if the user is in one of the listed groups or if one of the RQL condition
 is satisfied.
 
+
 The standard user groups
 ````````````````````````
 
@@ -336,66 +348,77 @@
   This can only be used for the actions `update` and `delete` of an entity
   type.
 
-It is also possible to use specific groups if they are defined in the
-precreate script of the cube (``migration/precreate.py``). Defining groups in
-postcreate script or later makes them unavailable for security
-purposes (in this case, an `sync_schema_props_perms` command has to
-be issued in a CubicWeb shell).
+It is also possible to use specific groups if they are defined in the precreate
+script of the cube (``migration/precreate.py``). Defining groups in postcreate
+script or later makes them unavailable for security purposes (in this case, an
+`sync_schema_props_perms` command has to be issued in a CubicWeb shell).
 
 
 Use of RQL expression for write permissions
 ```````````````````````````````````````````
-It is possible to define RQL expression to provide update permission
-(`add`, `delete` and `update`) on relation and entity types.
 
-RQL expression for entity type permission:
+It is possible to define RQL expression to provide update permission (`add`,
+`delete` and `update`) on entity type / relation definitions. An rql expression
+is a piece of query (corresponds to the WHERE statement of an RQL query), and the
+expression will be considered as satisfied if it returns some results. They can
+not be used in `read` permission.
 
-* you have to use the class `ERQLExpression`
+To use RQL expression in entity type permission:
 
-* the used expression corresponds to the WHERE statement of an RQL query
+* you have to use the class :class:`~cubicweb.schema.ERQLExpression`
 
 * in this expression, the variables `X` and `U` are pre-defined references
-  respectively on the current entity (on which the action is verified) and
-  on the user who send the request
+  respectively on the current entity (on which the action is verified) and on the
+  user who send the request
+
+For RQL expressions on a relation type, the principles are the same except for
+the following:
 
-* it is possible to use, in this expression, a special relation
-  "has_<ACTION>_permission" where the subject is the user and the
-  object is any variable, meaning that the user needs to have
-  permission to execute the action <ACTION> on the entities related
-  to this variable
+* you have to use the class :class:`~cubicweb.schema.RRQLExpression` instead of
+  :class:`~cubicweb.schema.ERQLExpression`
 
-For RQL expressions on a relation type, the principles are the same except
-for the following:
+* in the expression, the variables `S`, `O` and `U` are pre-defined references to
+  respectively the subject and the object of the current relation (on which the
+  action is being verified) and the user who executed the query
+
+To define security for attributes of an entity (non-final relation), you have to
+use the class :class:`~cubicweb.schema.ERQLExpression` in which `X` represents
+the entity the attribute belongs to.
 
-* you have to use the class `RRQLExpression` in the case of a non-final relation
+It is possible to use in those expression a special relation
+`has_<ACTION>_permission` where the subject is the user (eg 'U') and the object
+is any variable representing an entity (usually 'X' in
+:class:`~cubicweb.schema.ERQLExpression`, 'S' or 'O' in
+:class:`~cubicweb.schema.RRQLExpression`), meaning that the user needs to have
+permission to execute the action <ACTION> on the entities represented by this
+variable. It's recommanded to use this feature whenever possible since it
+simplify greatly complex security definition and upgrade.
 
-* in the expression, the variables `S`, `O` and `U` are pre-defined references
-  to respectively the subject and the object of the current relation (on
-  which the action is being verified) and the user who executed the query
 
-* we can also define rights over attributes of an entity (non-final relation),
-  knowing that:
+.. sourcecode:: python
 
-  - to define RQL expression, we have to use the class `ERQLExpression`
-    in which `X` represents the entity the attribute belongs to
+  class my_relation(RelationDefinition):
+    __permissions__ = {'read': ('managers', 'users'),
+                       'add': ('managers', RRQLExpression('U has_update_permission S')),
+                       'delete': ('managers', RRQLExpression('U has_update_permission S'))
+		       }
 
-  - the permissions `add` and `delete` are equivalent. Only `add`/`read`
-    are actually taken in consideration.
+In the above example, user will be allowed to add/delete `my_relation` if he has
+the `update` permission on the subject of the relation.
 
 .. note::
 
-  Potentially, the `use of an RQL expression to add an entity or a
-  relation` can cause problems for the user interface, because if the
-  expression uses the entity or the relation to create, then we are
-  not able to verify the permissions before we actually add the entity
-  (please note that this is not a problem for the RQL server at all,
-  because the permissions checks are done after the creation). In such
-  case, the permission check methods (CubicWebEntitySchema.check_perm
-  and has_perm) can indicate that the user is not allowed to create
-  this entity but can obtain the permission.  To compensate this
-  problem, it is usually necessary, for such case, to use an action
-  that reflects the schema permissions but which enables to check
-  properly the permissions so that it would show up if necessary.
+  Potentially, the `use of an RQL expression to add an entity or a relation` can
+  cause problems for the user interface, because if the expression uses the
+  entity or the relation to create, we are not able to verify the permissions
+  before we actually added the entity (please note that this is not a problem for
+  the RQL server at all, because the permissions checks are done after the
+  creation). In such case, the permission check methods
+  (CubicWebEntitySchema.check_perm and has_perm) can indicate that the user is
+  not allowed to create this entity while it would obtain the permission.  To
+  compensate this problem, it is usually necessary in such case to use an action
+  that reflects the schema permissions but which check properly the permissions
+  so that it would show up only if possible.
 
 
 Use of RQL expression for reading rights
@@ -403,12 +426,54 @@
 
 The principles are the same but with the following restrictions:
 
-* we can not use `RRQLExpression` on relation types for reading
+* you can not use rql expression for the `read` permission of relations and
+  attributes,
 
-* special relations "has_<ACTION>_permission" can not be used
+* you can not use special `has_<ACTION>_permission` relation in the rql
+  expression.
 
 
+Important notes about write permissions checking
+````````````````````````````````````````````````
 
+Write permissions (e.g. 'add', 'update', 'delete') are checked in core hooks.
+
+When a permission is checked slightly vary according to if it's an entity or
+relation, and if the relation is an attribute relation or not). It's important to
+understand that since according to when a permission is checked, values returned
+by rql expressions may changes, hence the permission being granted or not.
+
+Here are the current rules:
+
+1. permission to add/update entity and its attributes are checked:
+
+   - on commit if the entity has been added
+
+   - in an 'after_update_entity' hook if the entity has been updated. If it fails
+     at this time, it will be retried on commit (hence you get the permission if
+     you have it just after the modification or *at* commit time)
+
+2. permission to delete an entity is checked in 'before_delete_entity' hook
+
+3. permission to add a relation is checked either:
+
+   - in 'before_add_relation' hook if the relation type is in the
+     `BEFORE_ADD_RELATIONS` set
+
+   - else at commit time if the relation type is in the `ON_COMMIT_ADD_RELATIONS`
+     set
+
+   - else in 'after_add_relation' hook (the default)
+
+4. permission to delete a relation is checked in 'before_delete_relation' hook
+
+Last but not least, remember queries issued from hooks and operation are by
+default 'unsafe', eg there are no read or write security checks.
+
+See :mod:`cubicweb.hooks.security` for more details.
+
+
+.. _yams_example:
 
 Defining your schema using yams
 -------------------------------
@@ -459,6 +524,8 @@
 birth and a relation that connects a `Person` to another entity of type
 `Company` through the semantic `works_for`.
 
+
+
 :Naming convention:
 
  Entity class names must start with an uppercase letter. The common
@@ -494,15 +561,15 @@
 means that you need two separate entities that implement the `ITree` interface and
 get the result from `.children()` which ever entity is concerned.
 
-Inheritance
-```````````
-XXX feed me
+.. Inheritance
+.. ```````````
+.. XXX feed me
 
 
 Definition of relations
 ~~~~~~~~~~~~~~~~~~~~~~~
 
-XXX add note about defining relation type / definition
+.. XXX add note about defining relation type / definition
 
 A relation is defined by a Python class heriting `RelationType`. The name
 of the class corresponds to the name of the type. The class then contains
@@ -524,12 +591,7 @@
 
 * a string corresponding to an entity type
 * a tuple of string corresponding to multiple entity types
-* special string such as follows:
-
-  - "**": all types of entities
-  - "*": all types of non-meta entities
-  - "@": all types of meta entities but not system entities (e.g. used for
-    the basic schema description)
+* the '*' special string, meaning all types of entities
 
 When a relation is not inlined and not symmetrical, and it does not require
 specific permissions, it can be defined using a `SubjectRelation`
@@ -546,7 +608,7 @@
 :Historical note:
 
    It has been historically possible to use `ObjectRelation` which
-   defines a relation in the opposite direction. This feature is soon to be
+   defines a relation in the opposite direction. This feature is
    deprecated and therefore should not be used in newly written code.
 
 :Future deprecation note:
--- a/doc/book/en/devrepo/devcore/dbapi.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/devcore/dbapi.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -22,10 +22,14 @@
 
 .. note::
 
-  While executing update queries (SET, INSERT, DELETE), if a query generates
-  an error related to security, a rollback is automatically done on the current
+  If a query generates an error related to security (:exc:`Unauthorized`) or to
+  integrity (:exc:`ValidationError`), the transaction can still continue but you
+  won't be able to commit it, a rollback will be necessary to start a new
   transaction.
 
+  Also, a rollback is automatically done if an error occurs during commit.
+
+
 Executing RQL queries from a view or a hook
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devrepo/entityclasses/adapters.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,173 @@
+.. _adapters:
+
+Interfaces and Adapters
+-----------------------
+
+Interfaces are the same thing as object-oriented programming `interfaces`_.
+Adapter refers to a well-known `adapter`_ design pattern that helps separating
+concerns in object oriented applications.
+
+.. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
+.. _`adapter`: http://en.wikipedia.org/wiki/Adapter_pattern
+
+In |cubicweb| adapters provide logical functionalities to entity types. They
+are introduced in version `3.9`. Before that one had to implement Interfaces in
+entity classes to achieve a similar goal. However, the problem with this
+approach is that is clutters the entity class's namespace, exposing name
+collision risks with schema attributes/relations or even methods names
+(different interfaces may define the same method with not necessarily the same
+behaviour expected).
+
+Definition of an adapter is quite trivial. An excerpt from cubicweb
+itself (found in :mod:`cubicweb.entities.adapters`):
+
+.. sourcecode:: python
+
+
+    class ITreeAdapter(EntityAdapter):
+        """This adapter has to be overriden to be configured using the
+        tree_relation, child_role and parent_role class attributes to
+        benefit from this default implementation
+        """
+        __regid__ = 'ITree'
+
+        child_role = 'subject'
+        parent_role = 'object'
+
+        def children_rql(self):
+            """returns RQL to get children """
+            return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
+
+The adapter object has ``self.entity`` attribute which represents the
+entity being adapted.
+
+.. Note::
+
+   Adapters came with the notion of service identified by the registry identifier
+   of an adapters, hence dropping the need for explicit interface and the
+   :class:`cubicweb.selectors.implements` selector. You should instead use
+   :class:`cubicweb.selectors.is_instance` when you want to select on an entity
+   type, or :class:`cubicweb.selectors.adaptable` when you want to select on a
+   service.
+
+
+Specializing and binding an adapter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
+
+  from cubicweb.entities.adapters import ITreeAdapter
+
+  class MyEntityITreeAdapter(ITreeAdapter):
+      __select__ = is_instance('MyEntity')
+      tree_relation = 'filed_under'
+
+The ITreeAdapter here provides a default implementation. The
+tree_relation class attribute is actually used by this implementation
+to help implement correct behaviour.
+
+Here we provide a specific implementation which will be bound for
+``MyEntity`` entity type (the `adaptee`).
+
+
+.. _interfaces_to_adapters:
+
+Converting code from Interfaces/Mixins to Adapters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here we go with a small example. Before:
+
+.. sourcecode:: python
+
+    from cubicweb.selectors import implements
+    from cubicweb.interfaces import ITree
+    from cubicweb.mixins import ITreeMixIn
+
+    class MyEntity(ITreeMixIn, AnyEntity):
+        __implements__ = AnyEntity.__implements__ + (ITree,)
+
+
+    class ITreeView(EntityView):
+        __select__ = implements('ITree')
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            children = entity.children()
+
+After:
+
+.. sourcecode:: python
+
+    from cubicweb.selectors import adaptable, is_instance
+    from cubicweb.entities.adapters import ITreeAdapter
+
+    class MyEntityITreeAdapter(ITreeAdapter):
+        __select__ = is_instance('MyEntity')
+
+    class ITreeView(EntityView):
+        __select__ = adaptable('ITree')
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            itree = entity.cw_adapt_to('ITree')
+            children = itree.children()
+
+As we can see, the interface/mixin duality disappears and the entity
+class itself is completely freed from these concerns. When you want
+to use the ITree interface of an entity, call its `cw_adapt_to` method
+to get an adapter for this interface, then access to members of the
+interface on the adapter
+
+Let's look at an example where we defined everything ourselves. We
+start from:
+
+.. sourcecode:: python
+
+    class IFoo(Interface):
+        def bar(self, *args):
+            raise NotImplementedError
+
+    class MyEntity(AnyEntity):
+        __regid__ = 'MyEntity'
+        __implements__ = AnyEntity.__implements__ + (IFoo,)
+
+        def bar(self, *args):
+            return sum(captain.age for captain in self.captains)
+
+    class FooView(EntityView):
+       __regid__ = 'mycube.fooview'
+       __select__ = implements('IFoo')
+
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            self.w('bar: %s' % entity.bar())
+
+Converting to:
+
+.. sourcecode:: python
+
+   class IFooAdapter(EntityAdapter):
+       __regid__ = 'IFoo'
+       __select__ = is_instance('MyEntity')
+
+       def bar(self, *args):
+           return sum(captain.age for captain in self.entity.captains)
+
+   class FooView(EntityView):
+      __regid__ = 'mycube.fooview'
+      __select__ = adaptable('IFoo')
+
+        def cell_call(self, row, col):
+            entity = self.cw_rset.get_entity(row, col)
+            self.w('bar: %s' % entity.cw_adapt_to('IFoo').bar())
+
+.. note::
+
+   When migrating an entity method to an adapter, the code can be moved as is
+   except for the `self` of the entity class, which in the adapter must become `self.entity`.
+
+Adapters defined in the library
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: cubicweb.entities.adapters
+   :members:
+
+More are defined in web/views.
--- a/doc/book/en/devrepo/entityclasses/application-logic.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/entityclasses/application-logic.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -1,5 +1,5 @@
-How to use entities objects
----------------------------
+How to use entities objects and adapters
+----------------------------------------
 
 The previous chapters detailed the classes and methods available to
 the developper at the so-called `ORM`_ level. However they say little
@@ -7,9 +7,9 @@
 
 .. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
 
-Entities objects are used in the repository and web sides of
-CubicWeb. On the repository side of things, one should manipulate them
-in Hooks and Operations.
+Entities objects (and their adapters) are used in the repository and
+web sides of CubicWeb. On the repository side of things, one should
+manipulate them in Hooks and Operations.
 
 Hooks and Operations provide support for the implementation of rules
 such as computed attributes, coherency invariants, etc (they play the
@@ -32,21 +32,22 @@
 wire. There is no way state can be shared between these processes
 (there is a specific API for that). Hence, it is not possible to use
 entity objects as messengers between these components of an
-application. It means that an attribute set as in `obj.x = 42`,
+application. It means that an attribute set as in ``obj.x = 42``,
 whether or not x is actually an entity schema attribute, has a short
 life span, limited to the hook, operation or view within which the
 object was built.
 
 Setting an attribute or relation value can be done in the context of a
-Hook/Operation, using the obj.set_attributes(x=42) notation or a plain
+Hook/Operation, using the obj.set_relations(x=42) notation or a plain
 RQL SET expression.
 
 In views, it would be preferable to encapsulate the necessary logic in
-a method of the concerned entity class(es). But of course, this advice
-is also reasonnable for Hooks/Operations, though the separation of
-concerns here is less stringent than in the case of views.
+a method of an adapter for the concerned entity class(es). But of
+course, this advice is also reasonnable for Hooks/Operations, though
+the separation of concerns here is less stringent than in the case of
+views.
 
-This leads to the practical role of entity objects: it's where an
+This leads to the practical role of objects adapters: it's where an
 important part of the application logic lie (the other part being
 located in the Hook/Operations).
 
@@ -58,26 +59,31 @@
 
 .. sourcecode:: python
 
-    class Project(TreeMixIn, AnyEntity):
+    from cubicweb.entities.adapters import ITreeAdapter
+
+    class ProjectAdapter(ITreeAdapter):
+        __select__ = is_instance('Project')
+        tree_relation = 'subproject_of'
+
+    class Project(AnyEntity):
         __regid__ = 'Project'
-        __implements__ = AnyEntity.__implements__ + (ITree,)
         fetch_attrs, fetch_order = fetch_config(('name', 'description',
                                                  'description_format', 'summary'))
 
         TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
 
-        tree_attribute = 'subproject_of'
-        parent_target = 'subject'
-        children_target = 'object'
-
         def dc_title(self):
             return self.name
 
-First we see that it uses an ITree interface and the TreeMixIn default
-implementation. The attributes `tree_attribute`, `parent_target` and
-`children_target` are used by the TreeMixIn code. This is typically
-used in views concerned with the representation of tree-like
-structures (CubicWeb provides several such views).
+The fact that the `Project` entity type implements an ``ITree``
+interface is materialized by the ``ProjectAdapter`` class (inheriting
+the pre-defined ``ITreeAdapter`` whose __regid__ is of course
+``ITree``), which will be selected on `Project` entity types because
+of its selector. On this adapter, we redefine the ``tree_relation``
+attribute of the ITreeAdapter class.
+
+This is typically used in views concerned with the representation of
+tree-like structures (CubicWeb provides several such views).
 
 It is important that the views themselves try not to implement this
 logic, not only because such views would be hardly applyable to other
@@ -89,7 +95,17 @@
 about the transitive closure of the child relation). This is a further
 argument to implement it at entity class level.
 
-The `dc_title` method provides a (unicode string) value likely to be
+The fetch_attrs, fetch_order class attributes are parameters of the
+`ORM`_ layer. They tell which attributes should be loaded at once on
+entity object instantiation (by default, only the eid is known, other
+attributes are loaded on demand), and which attribute is to be used to
+order the .related() and .unrelated() methods output.
+
+We can observe the big TICKET_DEFAULT_STATE_RESTR is a pure
+application domain piece of data. There is, of course, no limitation
+to the amount of class attributes of this kind.
+
+The ``dc_title`` method provides a (unicode string) value likely to be
 consummed by views, but note that here we do not care about output
 encodings. We care about providing data in the most universal format
 possible, because the data could be used by a web view (which would be
@@ -97,17 +113,14 @@
 oriented output (which would have the necessary context about the
 needed byte stream encoding).
 
-The fetch_attrs, fetch_order class attributes are parameters of the
-`ORM`_ layer. They tell which attributes should be loaded at once on
-entity object instantiation (by default, only the eid is known, other
-attributes are loaded on demand), and which attribute is to be used to
-order the .related() and .unrelated() methods output.
+.. note::
 
-Finally, we can observe the big TICKET_DEFAULT_STATE_RESTR is a pure
-application domain piece of data. There is, of course, no limitation
-to the amount of class attributes of this kind.
+  The dublin code `dc_xxx` methods are not moved to an adapter as they
+  are extremely prevalent in cubicweb and assorted cubes and should be
+  available for all entity types.
 
-Let us now dig into more substantial pieces of code.
+Let us now dig into more substantial pieces of code, continuing the
+Project class.
 
 .. sourcecode:: python
 
@@ -151,7 +164,7 @@
 * it is NOT concerned with database coherency (this is the realm of
   Hooks/Operations); in other words, it assumes a coherent world
 
-* it is NOT concerned with end-user interfaces
+* it is NOT (directly) concerned with end-user interfaces
 
 * however it can be used in both contexts
 
--- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/entityclasses/data-as-objects.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -4,23 +4,22 @@
 Python-level access to persistent data is provided by the
 :class:`Entity <cubicweb.entity>` class.
 
-An entity class is bound to a schema entity type.  Descriptors are added when
+.. XXX this part is not clear. refactor it.
+
+An entity class is bound to a schema entity type. Descriptors are added when
 classes are registered in order to initialize the class according to its schema:
 
-* we can access the defined attributes in the schema thanks to the attributes of
-  the same name on instances (typed value)
+* the attributes defined in the schema appear as attributes of these classes
 
-* we can access the defined relations in the schema thanks to the relations of
-  the same name on instances (entities instances list)
-
+* the relations defined in the schema appear as attributes of these classes,
+  but are lists of instances
 
 `Formatting and output generation`:
 
 * `view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
   (and returns an unicode string)
 
-* `absolute_url(*args, **kwargs)`, returns an absolute URL to access the primary view
-  of an entity
+* `absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
 
 * `rest_path()`, returns a relative REST URL to get the entity
 
@@ -31,7 +30,7 @@
 `Data handling`:
 
 * `as_rset()`, converts the entity into an equivalent result set simulating the
-   request `Any X WHERE X eid _eid_`
+  request `Any X WHERE X eid _eid_`
 
 * `complete(skip_bytes=True)`, executes a request that recovers at
   once all the missing attributes of an entity
@@ -52,10 +51,10 @@
   values given named parameters
 
 * `set_relations(**kwargs)`, add relations to the given object. To
-   set a relation where this entity is the object of the relation,
-   use `reverse_<relation>` as argument name.  Values may be an
-   entity, a list of entities, or None (meaning that all relations of
-   the given type from or to this object should be deleted).
+  set a relation where this entity is the object of the relation,
+  use `reverse_<relation>` as argument name.  Values may be an
+  entity, a list of entities, or None (meaning that all relations of
+  the given type from or to this object should be deleted).
 
 * `copy_relations(ceid)`, copies the relations of the entities having the eid
   given in the parameters on the current entity
@@ -66,7 +65,7 @@
 The :class:`AnyEntity` class
 ----------------------------
 
-To provide a specific behavior for each entity, we have to define a class
+To provide a specific behavior for each entity, we can define a class
 inheriting from `cubicweb.entities.AnyEntity`. In general, we define this class
 in `mycube.entities` module (or in a submodule if we want to split code among
 multiple files) so that it will be available on both server and client side.
@@ -111,7 +110,7 @@
 `Misc methods`:
 
 * `after_deletion_path`, return (path, parameters) which should be
-   used as redirect information when this entity is being deleted
+  used as redirect information when this entity is being deleted
 
 * `pre_web_edit`, callback called by the web editcontroller when an
   entity will be created/modified, to let a chance to do some entity
@@ -139,5 +138,18 @@
 one in OTHER_CUBE. These types are stored in the `etype` section of
 the `vregistry`.
 
-Notice this is different than yams schema inheritance.
+Notice this is different than yams schema inheritance, which is an
+experimental undocumented feature.
+
+
+Application logic
+-----------------
 
+While a lot of custom behaviour and application logic can be
+implemented using entity classes, the programmer must be aware that
+adding new attributes and method on an entity class adds may shadow
+schema-level attribute or relation definitions.
+
+To keep entities clean (mostly data structures plus a few universal
+methods such as listed above), one should use `adapters` (see
+:ref:`adapters`).
--- a/doc/book/en/devrepo/entityclasses/index.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/entityclasses/index.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -9,5 +9,5 @@
 
    data-as-objects
    load-sort
-   interfaces
+   adapters
    application-logic
--- a/doc/book/en/devrepo/entityclasses/interfaces.rst	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-Interfaces
-----------
-
-This is the same thing as object-oriented programming `interfaces`_.
-
-.. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
-
-Definition of an interface is quite trivial. An example from cubicweb
-itself (found in cubicweb/interfaces.py):
-
-.. sourcecode:: python
-
-    class ITree(Interface):
-
-        def parent(self):
-            """returns the parent entity"""
-
-        def children(self):
-            """returns the item's children"""
-
-        def children_rql(self):
-            """returns RQL to get children"""
-
-        def iterchildren(self):
-            """iterates over the item's children"""
-
-        def is_leaf(self):
-            """returns true if this node as no child"""
-
-        def is_root(self):
-            """returns true if this node has no parent"""
-
-        def root(self):
-            """returns the root object"""
-
-
-Declaration of interfaces implemented by a class
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. sourcecode:: python
-
-  from cubicweb.interfaces import ITree
-  from cubicweb.mixins import TreeMixIn
-
-  class MyEntity(TreeMixIn, AnyEntity):
-      __regid__ = 'MyEntity'
-      __implements__ = AnyEntity.__implements__ + ('ITree',)
-
-      tree_attribute = 'filed_under'
-
-The TreeMixIn here provides a default implementation for the
-interface. The tree_attribute class attribute is actually used by this
-implementation to help implement correct behaviour.
-
-Interfaces (and some implementations as mixins) defined in the library
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.interfaces
-   :members:
-
-.. automodule:: cubicweb.mixins
-   :members:
-
-
-
--- a/doc/book/en/devrepo/migration.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/migration.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -91,6 +91,24 @@
 * `session`, repository session object
 
 
+New cube dependencies
+---------------------
+
+If your code depends on some new cubes, you have to add them in a migration
+script by using:
+
+* `add_cube(cube, update_database=True)`, add a cube.
+* `add_cubes(cubes, update_database=True)`, add a list of cubes.
+
+The `update_database` parameter is telling if the database schema
+should be updated or if only the relevant persistent property should be
+inserted (for the case where a new cube has been extracted from an
+existing one, so the new cube schema is actually already in there).
+
+If some of the added cubes are already used by an instance, they'll simply be
+silently skipped.
+
+
 Schema migration
 ----------------
 The following functions for schema migration are available in `repository`
@@ -182,6 +200,40 @@
 
 * `option_removed(oldname, newname)`, indicates that an option has been deleted.
 
+The `config` variable is an object which can be used to access the
+configuration values, for reading and updating, with a dictionary-like
+syntax. 
+
+Example 1: migration script changing the variable 'sender-addr' in
+all-in-one.conf. The script also checks that in that the instance is
+configured with a known value for that variable, and only updates the
+value in that case.
+
+.. sourcecode:: python
+
+ wrong_addr = 'cubicweb@loiglab.fr' # known wrong address
+ fixed_addr = 'cubicweb@logilab.fr'
+ configured_addr = config.get('sender-addr')
+ # check that the address has not been hand fixed by a sysadmin
+ if configured_addr == wrong_addr: 
+     config['sender-addr'] = fixed-addr
+     config.save()
+
+Example 2: checking the value of the database backend driver, which
+can be useful in case you need to issue backend-dependent raw SQL
+queries in a migration script.
+
+.. sourcecode:: python
+
+ dbdriver  = config.sources()['system']['db-driver']
+ if dbdriver == "sqlserver2005":
+     # this is now correctly handled by CW :-)
+     sql('ALTER TABLE cw_Xxxx ALTER COLUMN cw_name varchar(64) NOT NULL;')
+     commit()
+ else: # postgresql
+     sync_schema_props_perms(ertype=('Xxxx', 'name', 'String'),
+     syncperms=False)
+
 
 Others migration functions
 --------------------------
--- a/doc/book/en/devrepo/profiling.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/profiling.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -1,3 +1,5 @@
+.. _PROFILING:
+
 Profiling and performance
 =========================
 
--- a/doc/book/en/devrepo/repo/hooks.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/repo/hooks.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -58,6 +58,10 @@
 implementing `precommit_event` and other standard methods (wholly
 described in :ref:`operations_api`).
 
+.. hint::
+
+   It is a good practice, to write unit tests for each hook. See an example in :ref:`hook_test`
+
 Events
 ------
 
@@ -157,13 +161,13 @@
 .. sourcecode:: python
 
    from cubicweb import ValidationError
-   from cubicweb.selectors import implements
+   from cubicweb.selectors import is_instance
    from cubicweb.server.hook import Hook
 
    class PersonAgeRange(Hook):
         __regid__ = 'person_age_range'
         events = ('before_add_entity', 'before_update_entity')
-        __select__ = Hook.__select__ & implements('Person')
+        __select__ = Hook.__select__ & is_instance('Person')
 
         def __call__(self):
             if 0 >= self.entity.age <= 120:
@@ -173,7 +177,7 @@
 
 Hooks being AppObjects like views, they have a __regid__ and a
 __select__ class attribute. The base __select__ is augmented with an
-`implements` selector matching the desired entity type. The `events`
+`is_instance` selector matching the desired entity type. The `events`
 tuple is used by the Hook.__select__ base selector to dispatch the
 hook on the right events. In an entity hook, it is possible to
 dispatch on any entity event (e.g. 'before_add_entity',
@@ -241,6 +245,8 @@
 
 .. sourcecode:: python
 
+    from cubicweb.server.hook import Hook, Operation, match_rtype
+
     def check_cycle(self, session, eid, rtype, role='subject'):
         parents = set([eid])
         parent = session.entity_from_eid(eid)
@@ -300,7 +306,7 @@
    class CheckSubsidiaryCycleOp(Operation):
 
        def precommit_event(self):
-           for eid in self._cw.transaction_data['subsidiary_cycle_detection']:
+           for eid in self.session.transaction_data['subsidiary_cycle_detection']:
                check_cycle(self.session, eid, self.rtype)
 
 Here, we call set_operation with a session object, a specially forged
--- a/doc/book/en/devrepo/repo/sessions.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/repo/sessions.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -3,15 +3,16 @@
 Sessions
 ========
 
-There are three kinds of sessions.
-
-* `user sessions` are the most common: they are related to users and
-  carry security checks coming with user credentials
+Sessions are object carrying the `.execute` method to query the data
+sources.
 
-* `super sessions` are children of ordinary user sessions and allow to
-  bypass security checks (they are created by calling unsafe_execute
-  on a user session); this is often convenient in hooks which may
-  touch data that is not directly updatable by users
+Kinds of sessions
+-----------------
+
+There are two kinds of sessions.
+
+* `normal sessions` are the most common: they are related to users and
+  carry security checks coming with user credentials
 
 * `internal sessions` have all the powers; they are also used in only a
   few situations where you don't already have an adequate session at
@@ -20,8 +21,181 @@
 
 .. note::
   Do not confuse the session type with their connection mode, for
-  instance : 'in memory' or 'pyro'.
+  instance : `in memory` or `pyro`.
+
+Normal sessions are typically named `_cw` in most appobjects or
+sometimes just `session`.
+
+Internal sessions are available from the `Repository` object and are
+to be used like this:
+
+.. sourcecode:: python
+
+   session = self.repo.internal_session()
+   try:
+       do_stuff_with(session)
+   finally:
+       session.close()
+
+.. warning::
+  Do not forget to close such a session after use for a session leak
+  will quickly lead to an application crash.
+
+Authentication and management of sessions
+-----------------------------------------
+
+The authentication process is a ballet involving a few dancers:
+
+* through its `connect` method the top-level application object (the
+  `CubicWebPublisher`) will open a session whenever a web request
+  comes in; it asks the `session manager` to open a session (giving
+  the web request object as context) using `open_session`
+
+  * the session manager asks its authentication manager (which is a
+    `component`) to authenticate the request (using `authenticate`)
+
+    * the authentication manager asks, in order, to its authentication
+      information retrievers, a login and an opaque object containing
+      other credentials elements (calling `authentication_information`),
+      giving the request object each time
+
+      * the default retriever (bizarrely named
+        `LoginPaswordRetreiver`) will in turn defer login and password
+        fetching to the request object (which, depending on the
+        authentication mode (`cookie` or `http`), will do the
+        appropriate things and return a login and a password)
+
+    * the authentication manager, on success, asks the `Repository`
+      object to connect with the found credentials (using `connect`)
+
+      * the repository object asks authentication to all of its
+        sources which support the `CWUser` entity with the given
+        credentials; when successful it can build the cwuser entity,
+        from which a regular `Session` object is made; it returns the
+        session id
+
+        * the source in turn will defer work to an authentifier class
+          that define the ultimate `authenticate` method (for instance
+          the native source will query the database against the
+          provided credentials)
+
+    * the authentication manager, on success, will call back _all_
+      retrievers with `authenticated` and return its authentication
+      data (on failure, it will try the anonymous login or, if the
+      configuration forbids it, raise an `AuthenticationError`)
+
+Writing authentication plugins
+------------------------------
+
+Sometimes CubicWeb's out-of-the-box authentication schemes (cookie and
+http) are not sufficient. Nowadays there is a plethore of such schemes
+and the framework cannot provide them all, but as the sequence above
+shows, it is extensible.
+
+Two levels have to be considered when writing an authentication
+plugin: the web client and the repository.
+
+We invented a scenario where it makes sense to have a new plugin in
+each side: some middleware will do pre-authentication and under the
+right circumstances add a new HTTP `x-foo-user` header to the query
+before it reaches the CubicWeb instance. For a concrete example of
+this, see the `apachekerberos`_ cube.
+
+.. _`apachekerberos`: http://www.cubicweb.org/project/cubicweb-apachekerberos
+
+Repository authentication plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On the repository side, it is possible to register a source
+authentifier using the following kind of code:
 
-[WRITE ME]
+.. sourcecode:: python
+
+ from cubicweb.server.sources import native
+
+ class FooAuthentifier(native.LoginPasswordAuthentifier):
+     """ a source authentifier plugin
+     if 'foo' in authentication information, no need to check
+     password
+     """
+     auth_rql = 'Any X WHERE X is CWUser, X login %(login)s'
+
+     def authenticate(self, session, login, **kwargs):
+         """return CWUser eid for the given login
+         if this account is defined in this source,
+         else raise `AuthenticationError`
+         """
+         session.debug('authentication by %s', self.__class__.__name__)
+         if 'foo' not in kwargs:
+             return super(FooAuthentifier, self).authenticate(session, login, **kwargs)
+         try:
+             rset = session.execute(self.auth_rql, {'login': login})
+             return rset[0][0]
+         except Exception, exc:
+             session.debug('authentication failure (%s)', exc)
+         raise AuthenticationError('foo user is unknown to us')
+
+Since repository authentifiers are not appobjects, we have to register
+them through a `server_startup` hook.
+
+.. sourcecode:: python
+
+ class ServerStartupHook(hook.Hook):
+     """ register the foo authenticator """
+     __regid__ = 'fooauthenticatorregisterer'
+     events = ('server_startup',)
+
+     def __call__(self):
+         self.debug('registering foo authentifier')
+         self.repo.system_source.add_authentifier(FooAuthentifier())
+
+Web authentication plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
 
-* authentication and management of sessions
+ class XFooUserRetriever(authentication.LoginPasswordRetreiver):
+     """ authenticate by the x-foo-user http header
+     or just do normal login/password authentication
+     """
+     __regid__ = 'x-foo-user'
+     order = 0
+
+     def authentication_information(self, req):
+         """retrieve authentication information from the given request, raise
+         NoAuthInfo if expected information is not found
+         """
+         self.debug('web authenticator building auth info')
+         try:
+            login = req.get_header('x-foo-user')
+            if login:
+                return login, {'foo': True}
+            else:
+                return super(XFooUserRetriever, self).authentication_information(self, req)
+         except Exception, exc:
+            self.debug('web authenticator failed (%s)', exc)
+         raise authentication.NoAuthInfo()
+
+     def authenticated(self, retriever, req, cnx, login, authinfo):
+         """callback when return authentication information have opened a
+         repository connection successfully. Take care req has no session
+         attached yet, hence req.execute isn't available.
+
+         Here we set a flag on the request to indicate that the user is
+         foo-authenticated. Can be used by a selector
+         """
+         self.debug('web authenticator running post authentication callback')
+         cnx.foo_user = authinfo.get('foo')
+
+In the `authenticated` method we add (in an admitedly slightly hackish
+way) an attribute to the connection object. This, in turn, can be used
+to build a selector dispatching on the fact that the user was
+preauthenticated or not.
+
+.. sourcecode:: python
+
+ @objectify_selector
+ def foo_authenticated(cls, req, rset=None, **kwargs):
+     if hasattr(req.cnx, 'foo_user') and req.foo_user:
+         return 1
+     return 0
--- a/doc/book/en/devrepo/testing.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/testing.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -6,24 +6,24 @@
 Unit tests
 ----------
 
-The *CubicWeb* framework provides the `CubicWebTC` test base class in
-the module `cubicweb.devtools.testlib`.
+The *CubicWeb* framework provides the
+:class:`cubicweb.devtools.testlib.CubicWebTC` test base class .
 
 Tests shall be put into the mycube/test directory. Additional test
 data shall go into mycube/test/data.
 
-It is much advised to write tests concerning entities methods, hooks
-and operations, security. The CubicWebTC base class has convenience
-methods to help test all of this.
-
-.. note::
+It is much advised to write tests concerning entities methods,
+actions, hooks and operations, security. The
+:class:`~cubicweb.devtools.testlib.CubicWebTC` base class has
+convenience methods to help test all of this.
 
-  In the realm of views, there is not much to do but check that the
-  views are valid XHTML.  See :ref:`automatic_views_tests` for
-  details. Integration of CubicWeb tests with UI testing tools such as
-  `selenium`_ are currently under invesitgation.
+In the realm of views, automatic tests check that views are valid
+XHTML. See :ref:`automatic_views_tests` for details. Since 3.9, bases
+for web functional testing using `windmill
+<http://www.getwindmill.com/>`_ are set. See test cases in
+cubicweb/web/test/windmill and python wrapper in
+cubicweb/web/test_windmill/ if you want to use this in your own cube.
 
-.. _selenium: http://seleniumhq.org/projects/ide/
 
 Most unit tests need a live database to work against. This is achieved
 by CubicWeb using automatically sqlite (bundled with Python, see
@@ -42,6 +42,8 @@
   `sync_schema_props_perms()` fonction of the migration environment
   need not a database regeneration step.
 
+.. _hook_test:
+
 Unit test by example
 ````````````````````
 
@@ -77,13 +79,29 @@
             self.kw1.set_relations(subkeyword_of=kw3)
             self.assertRaises(ValidationError, self.commit)
 
-The test class defines a `setup_database` method which populates the
+The test class defines a :meth:`setup_database` method which populates the
 database with initial data. Each test of the class runs with this
-pre-populated database.
+pre-populated database. A commit is done automatically after the
+:meth:`setup_database` call. You don't have to call it explicitely.
 
 The test case itself checks that an Operation does it job of
 preventing cycles amongst Keyword entities.
 
+`create_entity` is a useful method, which easily allows to create an
+entity. You can link this entity to others entities, by specifying as
+argument, the relation name, and the entity to link, as value. In the
+above example, the `Classification` entity is linked to a `CWEtype`
+via the relation `classifies`. Conversely, if you are creating a
+`CWEtype` entity, you can link it to a `Classification` entity, by
+adding `reverse_classifies` as argument.
+
+.. note::
+
+   :meth:`commit` method is not called automatically in test_XXX
+   methods. You have to call it explicitely if needed (notably to test
+   operations). It is a good practice to call :meth:`clear_all_caches`
+   on entities after a commit to avoid request cache effects.
+
 You can see an example of security tests in the
 :ref:`adv_tuto_security`.
 
@@ -145,11 +163,11 @@
    connection from another !
 
 Email notifications tests
--------------------------
+`````````````````````````
 
 When running tests potentially generated e-mails are not really sent
 but is found in the list `MAILBOX` of module
-`cubicweb.devtools.testlib`.
+:mod:`cubicweb.devtools.testlib`.
 
 You can test your notifications by analyzing the contents of this list, which
 contains objects with two attributes:
@@ -184,15 +202,70 @@
             mail = MAILBOX[1]
             self.assertEquals(mail.subject, '[data] yes')
 
+Visible actions tests
+`````````````````````
+
+It is easy to write unit tests to test actions which are visible to
+user or to a category of users. Let's take an example in the
+`conference cube`_.
+
+.. _`conference cube`: http://www.cubicweb.org/project/cubicweb-conference
+.. sourcecode:: python
+
+    class ConferenceActionsTC(CubicWebTC):
+
+        def setup_database(self):
+            self.conf = self.create_entity('Conference',
+                                           title=u'my conf',
+                                           url_id=u'conf',
+                                           start_on=date(2010, 1, 27),
+                                           end_on = date(2010, 1, 29),
+                                           call_open=True,
+                                           reverse_is_chair_at=chair,
+                                           reverse_is_reviewer_at=reviewer)
+
+        def test_admin(self):
+            req = self.request()
+            rset = req.execute('Any C WHERE C is Conference')
+            self.assertListEquals(self.pactions(req, rset),
+                                  [('workflow', workflow.WorkflowActions),
+                                   ('edit', confactions.ModifyAction),
+                                   ('managepermission', actions.ManagePermissionsAction),
+                                   ('addrelated', actions.AddRelatedActions),
+                                   ('delete', actions.DeleteAction),
+                                   ('generate_badge_action', badges.GenerateBadgeAction),
+                                   ('addtalkinconf', confactions.AddTalkInConferenceAction)
+                                   ])
+            self.assertListEquals(self.action_submenu(req, rset, 'addrelated'),
+                                  [(u'add Track in_conf Conference object',
+                                    u'http://testing.fr/cubicweb/add/Track'
+                                    u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
+                                    u'__redirectpath=conference%%2Fconf&'
+                                    u'__redirectvid=' % {'conf': self.conf.eid}),
+                                   ])
+
+You just have to execute a rql query corresponding to the view you want to test,
+and to compare the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions
+that must be visible in the interface. This is a list of tuples. The first
+element is the action's `__regid__`, the second the action's class.
+
+To test actions in submenu, you just have to test the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.action_submenu` method. The last
+parameter of the method is the action's category. The result is a list of
+tuples. The first element is the action's title, and the second element the
+action's url.
+
+
 .. _automatic_views_tests:
 
 Automatic views testing
 -----------------------
 
-This is done automatically with the AutomaticWebTest class. At cube
-creation time, the mycube/test/test_mycube.py file contains such a
-test. The code here has to be uncommented to be usable, without
-further modification.
+This is done automatically with the :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+class. At cube creation time, the mycube/test/test_mycube.py file
+contains such a test. The code here has to be uncommented to be
+usable, without further modification.
 
 The ``auto_populate`` method uses a smart algorithm to create
 pseudo-random data in the database, thus enabling the views to be
@@ -212,6 +285,61 @@
   auto_populate cannot guess by itself; these must yield resultsets
   against which views may be selected.
 
+.. warning::
+
+  Take care to not let the imported `AutomaticWebTest` in your test module
+  namespace, else both your subclass *and* this parent class will be run.
+
+Testing on a real-life database
+-------------------------------
+
+The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration`
+configuration class to setup its testing environment (database driver,
+user password, application home, and so on). The `cubicweb.devtools`
+module also provides a `RealDatabaseConfiguration`
+class that will read a regular cubicweb sources file to fetch all
+this information but will also prevent the database to be initalized
+and reset between tests.
+
+For a test class to use a specific configuration, you have to set
+the `_config` class attribute on the class as in:
+
+.. sourcecode:: python
+
+    from cubicweb.devtools import RealDatabaseConfiguration
+    from cubicweb.devtools.testlib import CubicWebTC
+
+    class BlogRealDatabaseTC(CubicWebTC):
+        _config = RealDatabaseConfiguration('blog',
+                                            sourcefile='/path/to/realdb_sources')
+
+        def test_blog_rss(self):
+	    req = self.request()
+	    rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
+	                       'B created_by U, U login "logilab", B creation_date D')
+            self.view('rss', rset)
+
+
+
+Testing with other cubes
+------------------------
+
+Sometimes a small component cannot be tested all by itself, so one
+needs to specify other cubes to be used as part of the the unit test
+suite. This is handled by the ``bootstrap_cubes`` file located under
+``mycube/test/data``. One example from the `preview` cube::
+
+ card, file, preview
+
+The format is:
+
+* possibly several empy lines or lines starting with ``#`` (comment lines)
+* one line containing a coma separated list of cube names.
+
+It is also possible to add a ``schema.py`` file in
+``mycube/test/data``, which will be used by the testing framework,
+therefore making new entity types and relations available to the
+tests. 
 
 Test APIS
 ---------
--- a/doc/book/en/devrepo/vreg.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devrepo/vreg.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -37,6 +37,7 @@
 .. autoclass:: cubicweb.appobject.yes
 .. autoclass:: cubicweb.selectors.match_kwargs
 .. autoclass:: cubicweb.selectors.appobject_selectable
+.. autoclass:: cubicweb.selectors.adaptable
 
 
 Result set selectors
@@ -66,7 +67,7 @@
 match or not according to entity's (instance or class) properties.
 
 .. autoclass:: cubicweb.selectors.non_final_entity
-.. autoclass:: cubicweb.selectors.implements
+.. autoclass:: cubicweb.selectors.is_instance
 .. autoclass:: cubicweb.selectors.score_entity
 .. autoclass:: cubicweb.selectors.rql_condition
 .. autoclass:: cubicweb.selectors.relation_possible
@@ -75,6 +76,8 @@
 .. autoclass:: cubicweb.selectors.partial_has_related_entities
 .. autoclass:: cubicweb.selectors.has_permission
 .. autoclass:: cubicweb.selectors.has_add_permission
+.. autoclass:: cubicweb.selectors.has_mimetype
+.. autoclass:: cubicweb.selectors.implements
 
 
 Logged user selectors
--- a/doc/book/en/devweb/controllers.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/controllers.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -15,13 +15,12 @@
 
 `Browsing`:
 
-* the View controlleris associated with most browsing actions within a
-  CubicWeb application: it always instantiates a
-  :ref:`the_main_template` and lets the ResultSet/Views dispatch
-  system build up the whole content; it handles ObjectNotFound and
-  NoSelectableObject errors that may bubble up to its entry point, in
-  an end-user-friendly way (but other programming errors will slip
-  through)
+* the View controller is associated with most browsing actions within a
+  CubicWeb application: it always instantiates a :ref:`the_main_template` and
+  lets the ResultSet/Views dispatch system build up the whole content; it
+  handles :exc:`ObjectNotFound` and :exc:`NoSelectableObject` errors that may
+  bubble up to its entry point, in an end-user-friendly way (but other
+  programming errors will slip through)
 
 * the JSon controller (same module) provides services for Ajax calls,
   typically using JSON as a serialization format for input, and
@@ -49,7 +48,7 @@
   for outgoing email notifications
 
 * the MailBugReport controller (web/views/basecontrollers.py) allows
-  to quickly have a `repotbug` feature in one's application
+  to quickly have a `reportbug` feature in one's application
 
 Registration
 ++++++++++++
--- a/doc/book/en/devweb/edition/examples.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/edition/examples.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -18,7 +18,7 @@
  from cubicweb.web import formfields as ff, formwidgets as fwdgs
  class SendToReviewerStatusChangeView(ChangeStateFormView):
      __select__ = (ChangeStateFormView.__select__ &
-                   implements('Talk') &
+                   is_instance('Talk') &
                    rql_condition('X in_state S, S name "submitted"'))
 
      def get_form(self, entity, transition, **kwargs):
@@ -126,7 +126,7 @@
 
     class MassMailingFormView(form.FormViewMixIn, EntityView):
 	__regid__ = 'massmailing'
-	__select__ = implements(IEmailable) & authenticated_user()
+	__select__ = is_instance(IEmailable) & authenticated_user()
 
 	def call(self):
 	    form = self._cw.vreg['forms'].select('massmailing', self._cw,
--- a/doc/book/en/devweb/edition/form.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/edition/form.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -1,3 +1,5 @@
+.. _webform:
+
 HTML form construction
 ----------------------
 
--- a/doc/book/en/devweb/facets.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/facets.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -1,172 +1,23 @@
 The facets system
 -----------------
 
-Facets allow to restrict searches according to some criteria. CubicWeb
-has a builtin `facet`_ system to define restrictions `filters`_ really
-as easily as possible. A few base classes for facets are provided in
-``cubicweb.web.facet.py``. All classes inherits from the base class
-``AbstractFacet``.
+Facets allow to restrict searches according to some user friendly criterias.
+CubicWeb has a builtin `facet`_ system to define restrictions `filters`_ really
+as easily as possible.
 
-Here is an overview of the facets rendering pick from the `tracker` cube:
+Here is an exemple of the facets rendering picked from our
+http://www.cubicweb.org web site:
 
 .. image:: ../images/facet_overview.png
 
-Facets will appear on each page presenting more than one entity.
-
-
-
-VocabularyFacet
-~~~~~~~~~~~~~~~~
-The ``VocabularyFacet`` inherits from the ``AbstractFacet``.
-A class which inherits from VocabularyFacets must redefine these methods:
-
-.. automethod:: cubicweb.web.facet.VocabularyFacet.vocabulary
-.. automethod:: cubicweb.web.facet.VocabularyFacet.possible_values
-
-RelationFacet
-~~~~~~~~~~~~~~
-
-The ``RelationFacet`` inherits from the ``VocabularyFacet``. It allows to filter entities according to certain relation's values. Generally, you just have to define some class attributes like:
-
-- rtype: the name of the relation
-- role: the default value is set to `subject`
-- target_attr: needed if it is not the default attribute of the entity
-
-
-To illustrate this facet, let's take for example an *excerpt* of the schema of an office location search application:
-
-.. sourcecode:: python
-
-  class Office(WorkflowableEntityType):
-      price = Int(description='euros / m2 / HC / HT')
-      surface = Int(description='m2')
-      description = RichString(fulltextindexed=True)
-      has_address = SubjectRelation('PostalAddress',
-                                    cardinality='1?',
-                                    composite='subject')
-      proposed_by = SubjectRelation('Agency')
-      comments = ObjectRelation('Comment',
-                                cardinality='1*',
-                                composite='object')
-      screenshots = SubjectRelation(('File', 'Image'),
-                                    cardinality='*1',
-                                    composite='subject')
-
-
-We define a facet to filter offices according to the attribute
-`postalcode` of their associated `PostalAdress`.
-
-.. sourcecode:: python
-
-  class PostalCodeFacet(RelationFacet):
-      __regid__ = 'postalcode-facet'      # every registered class must have an id
-      __select__ = implements('Office')   # this facet should only be selected when
-                                          # visualizing offices
-      rtype = 'has_address'               # this facet is a filter on the entity linked to
-                                          # the office thrhough the relation
-                                          # has_address
-      target_attr = 'postalcode'          # the filter's key is the attribute "postal_code"
-                                          # of the target PostalAddress entity
-
-
-AttributeFacet
-~~~~~~~~~~~~~~
-
-The ``AttributeFacet`` inherits from the ``RelationFacet``. It allows to filter entities according to certain attribute's values.
-
-The example below resumes the former schema. We define now a filter based on the `surface` attribute of the
-`Office`.
-
-.. sourcecode:: python
+Facets will appear on each page presenting more than one entity that may be
+filtered according to some known criteria.
 
-  class SurfaceFacet(AttributeFacet):
-      __regid__ = 'surface-facet'       # every registered class must have an id
-      __select__ = implements('Office') # this facet should only be selected when
-                                        # visualizing offices
-      rtype = 'surface'                 # the filter's key is the attribute "surface"
-      comparator = '>='                 # override the default value of operator since
-                                        # we want to filter according to a
-                                        # minimal
-                                        # value, not an exact one
-
-      def rset_vocabulary(self, ___):
-          """override the default vocabulary method since we want to hard-code
-          our threshold values.
-          Not overriding would generate a filter box with all existing surfaces
-          defined in the database.
-          """
-          return [('> 200', '200'), ('> 250', '250'),
-                  ('> 275', '275'), ('> 300', '300')]
-
-RangeFacet
-~~~~~~~~~~
-The ``RangeFacet`` inherits from the ``AttributeFacet``. It allows to filter entities according to certain attributes of numerical type.
-
-The ``RangeFacet`` displays a slider using `jquery`_ to choose a lower bound and an upper bound.
-
-The example below defines a facet to filter a selection of books according to their number of pages.
-
-.. sourcecode:: python
-
-   class BookPagesFacet(RangeFacet):
-       __regid__ = 'priority-facet'
-       __select__ = RangeFacet.__select__ & implements('Book')
-       rtype = 'pages'
-
-The image below display the rendering of the ``RangeFacet``:
-
-.. image:: ../images/facet_range.png
-
-DateRangeFacet
-~~~~~~~~~~~~~~
-The ``DateRangeFacet`` inherits from the ``RangeFacet``. It allows to filter entities according to certain attributes of date type.
+Base classes for facets
+~~~~~~~~~~~~~~~~~~~~~~~
+.. automodule:: cubicweb.web.facet
 
-Here is an example of code that defines a facet to filter
-musical works according to their composition date:
-
-.. sourcecode:: python
-
-    class CompositionDateFacet(DateRangeFacet):
-        # 1. make sure this facet is displayed only on Track selection
-        __select__ = DateRangeFacet.__select__ & implements('Track')
-        # 2. give the facet an id required by CubicWeb)
-        __regid__ = 'compdate-facet'
-        # 3. specify the attribute name that actually stores the date in the DB
-        rtype = 'composition_date'
-
-With this facet, on each page displaying tracks, you'll be able to filter them
-according to their composition date with a jquery slider.
-
-The image below display the rendering of the ``DateRangeFacet``:
-
-.. image:: ../images/facet_date_range.png
-
-
-HasRelationFacet
-~~~~~~~~~~~~~~~~
-
-The ``DateRangeFacet`` inherits from the ``AbstractFacet``. It will
-display a simple checkbox and lets you refine your selection in order
-to get only entities that actually use this relation.
-
-Here is an example of the rendering of the ``HasRelationFacet`` to
-filter entities with image and the corresponding code:
-
-.. image:: ../images/facet_has_image.png
-
-.. sourcecode:: python
-
-  class HasImageFacet(HasRelationFacet):
-      __regid__ = 'hasimage-facet'
-      __select__ = HasRelationFacet.__select__ & implements('Book')
-      rtype = 'has_image'
-
-
-
-To use ``HasRelationFacet`` on a reverse relation add ``role = 'object'`` in
-it's definitions.
 
 .. _facet: http://en.wikipedia.org/wiki/Faceted_browser
 .. _filters: http://www.cubicweb.org/blogentry/154152
-.. _jquery: http://www.jqueryui.com/
 
--- a/doc/book/en/devweb/js.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/js.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -350,3 +350,47 @@
 There is also javascript support for massmailing, gmap (google maps),
 fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over
 AppEngine), flot (charts drawing), tabs and bookmarks.
+
+API
+~~~
+
+.. toctree::
+    :maxdepth: 1
+    
+    js_api/index
+
+
+Testing javascript
+~~~~~~~~~~~~~~~~~~~~~~
+
+You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
+inside the python unittest run . You simply have to define a new class that
+inherit from ``QUnitTestCase`` and register your javascript test file in the
+``all_js_tests`` lclass attribut. This  ``all_js_tests`` is a sequence a
+3-tuple (<test_file, [<dependencies> ,] [<data_files>]):
+
+The <test_file> should contains the qunit test. <dependencies> defines the list
+of javascript file that must be imported before the test script.  Dependencies
+are included their definition order. <data_files> are additional files copied in the
+test directory. both <dependencies> and <data_files> are optionnal.
+``jquery.js`` is preincluded in for all test.
+
+.. sourcecode:: python
+
+    from cubicweb.qunit import QUnitTestCase
+
+    class MyQUnitTest(QUnitTestCase):
+
+        all_js_tests = (
+            ("relative/path/to/my_simple_testcase.js",)
+            ("relative/path/to/my_qunit_testcase.js",(
+                "rel/path/to/dependency_1.js",
+                "rel/path/to/dependency_2.js",)),
+            ("relative/path/to/my_complexe_qunit_testcase.js",(
+                 "rel/path/to/dependency_1.js",
+                 "rel/path/to/dependency_2.js",
+               ),(
+                 "rel/path/file_dependency.html",
+                 "path/file_dependency.json")
+                ),
+            )
--- a/doc/book/en/devweb/views/basetemplates.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/views/basetemplates.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -11,17 +11,33 @@
 in :ref:`views_base_class`, there are two kinds of views: the
 templatable and non-templatable.
 
-Non-templatable views are standalone. They are responsible for all the
-details such as setting a proper content type (or mime type), the
-proper document headers, namespaces, etc. Examples are pure xml views
-such as RSS or Semantic Web views (`SIOC`_, `DOAP`_, `FOAF`_, `Linked
-Data`_, etc.).
+
+Non-templatable views
+---------------------
+
+Non-templatable views are standalone. They are responsible for all the details
+such as setting a proper content type (or mime type), the proper document
+headers, namespaces, etc. Examples are pure xml views such as RSS or Semantic Web
+views (`SIOC`_, `DOAP`_, `FOAF`_, `Linked Data`_, etc.), and views which generate
+binary files (pdf, excel files, etc.)
 
 .. _`SIOC`: http://sioc-project.org/
 .. _`DOAP`: http://trac.usefulinc.com/doap
 .. _`FOAF`: http://www.foaf-project.org/
 .. _`Linked Data`: http://linkeddata.org/
 
+
+To notice that a view is not templatable, you just have to set the
+view's class attribute `templatable` to `False`. In this case, it
+should set the `content_type` class attribute to the correct MIME
+type. By default, it is text/xhtml. Additionally, if your view
+generate a binary file, you have to set the view's class attribute
+`binary` to `True` too.
+
+
+Templatable views
+-----------------
+
 Templatable views are not concerned with such pesky details. They
 leave it to the template. Conversely, the template's main job is to:
 
@@ -30,14 +46,14 @@
 * invoke adequate views in the various sections of the document
 
 
-Look at :mod:`cubicweb.web.views.basetemplates` and you will find the
-base templates used to generate (X)HTML for your application. The most
-important template there is `TheMainTemplate`.
+Look at :mod:`cubicweb.web.views.basetemplates` and you will find the base
+templates used to generate (X)HTML for your application. The most important
+template there is :class:`~cubicweb.web.views.basetemplates.TheMainTemplate`.
 
 .. _the_main_template_layout:
 
 TheMainTemplate
----------------
+~~~~~~~~~~~~~~~
 
 .. _the_main_template_sections:
 
@@ -88,28 +104,60 @@
   How and why a view object is given to the main template is explained
   in the :ref:`publisher` chapter.
 
-Class attributes
-````````````````
+Configure the main template
+```````````````````````````
+
+You can overload some methods of the
+:class:`~cubicweb.web.views.basetemplates.TheMainTemplate`, in order to fulfil
+your needs. There are also some attributes and methods which can be defined on a
+view to modify the base template behaviour:
+
+* `paginable`: if the result set is bigger than a configurable size, your result
+  page will be paginated by default. You can set this attribute to `False` to
+  avoid this.
+
+* `binary`: boolean flag telling if the view generates some text or a binary
+  stream.  Default to False. When view generates text argument given to `self.w`
+  **must be an unicode string**, encoded string otherwise.
 
-We can also control certain aspects of the main template thanks to the following
-forms parameters:
+* `content_type`, view's content type, default to 'text/xhtml'
+
+* `templatable`, boolean flag telling if the view's content should be returned
+  directly (when `False`) or included in the main template layout (including
+  header, boxes and so on).
+
+* `page_title()`, method that should return a title that will be set as page
+  title in the html headers.
+
+* `html_headers()`, method that should return a list of HTML headers to be
+  included the html headers.
+
+
+You can also modify certain aspects of the main template of a page
+when building an url or setting these parameters in the req.form:
 
 * `__notemplate`, if present (whatever the value assigned), only the content view
   is returned
-* `__force_display`, if present and its value is not null, no navigation
-  whatever the number of entities to display
+
+* `__force_display`, if present and its value is not null, no pagination whatever
+  the number of entities to display (e.g. similar effect as view's `paginable`
+  attribute described above.
+
 * `__method`, if the result set to render contains only one entity and this
-  parameter is set, it refers to a method to call on the entity by passing it
-  the dictionary of the forms parameters, before going the classic way (through
-  step 1 and 2 described juste above)
+  parameter is set, it refers to a method to call on the entity by passing it the
+  dictionary of the forms parameters, before going the classic way (through step
+  1 and 2 described juste above)
+
+* `vtitle`, a title to be set as <h1> of the content
 
 Other templates
----------------
+~~~~~~~~~~~~~~~
 
-Other standard templates include:
+There are also the following other standard templates:
 
-* `login` and `logout`
-
-* `error-template` specializes TheMainTemplate to do proper end-user
-  output if an error occurs during the computation of TheMainTemplate
-  (it is a fallback view).
+* :class:`cubicweb.web.views.basetemplates.LogInTemplate`
+* :class:`cubicweb.web.views.basetemplates.LogOutTemplate`
+* :class:`cubicweb.web.views.basetemplates.ErrorTemplate` specializes
+  :class:`~cubicweb.web.views.basetemplates.TheMainTemplate` to do
+  proper end-user output if an error occurs during the computation of
+  TheMainTemplate (it is a fallback view).
--- a/doc/book/en/devweb/views/baseviews.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/views/baseviews.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -4,11 +4,12 @@
 ----------
 
 *CubicWeb* provides a lot of standard views, that can be found in
- :mod:`cubicweb.web.views` and :mod:`cubicweb.web.views.baseviews`.
+:mod:`cubicweb.web.views` sub-modules.
 
-A certain number of views are used to build the web interface, which
-apply to one or more entities. Their identifier is what distinguish
-them from each others and the main ones are:
+A certain number of views are used to build the web interface, which apply to one
+or more entities. As other appobject, Their identifier is what distinguish them
+from each others. The most generic ones, found in
+:mod:`cubicweb.web.views.baseviews`, are described below.
 
 HTML views
 ~~~~~~~~~~
@@ -32,53 +33,105 @@
     This view is the default view used when nothing needs to be rendered.
     It is always applicable.
 
+
 Entity views
 ````````````
 
 *incontext, outofcontext*
-    Those are used to display a link to an entity, depending on the
-    entity having to be displayed in or out of context
-    (of another entity).  By default it respectively produces the
-    result of `textincontext` and `textoutofcontext` wrapped in a link
-    leading to the primary view of the entity.
+
+    Those are used to display a link to an entity, whose label depends on the
+    entity having to be displayed in or out of context (of another entity): some
+    entities make sense in the context of another entity. For instance, the
+    `Version` of a `Project` in forge. So one may expect that 'incontext' will
+    be called when display a version from within the context of a project, while
+    'outofcontext"' will be called in other cases. In our example, the
+    'incontext' view of the version would be something like '0.1.2', while the
+    'outofcontext' view would include the project name, e.g. 'baz 0.1.2' (since
+    only a version number without the associated project doesn't make sense if
+    you don't know yet that you're talking about the famous 'baz' project. |cubicweb|
+    tries to make guess and call 'incontext'/'outofcontext' nicely. When it can't
+    know, the 'oneline' view should be used.
+
+    By default it respectively produces the result of `textincontext` and
+    `textoutofcontext` wrapped in a link leading to the primary view of the
+    entity.
+
 
 *oneline*
+
     This view is used when we can't tell if the entity should be considered as
-    displayed in or out of context.  By default it produces the result of `text`
+    displayed in or out of context. By default it produces the result of `text`
     in a link leading to the primary view of the entity.
 
+
 List
 `````
 
 *list*
-    This view displays a list of entities by creating a HTML list (`<ul>`)
-    and call the view `listitem` for each entity of the result set.
+
+    This view displays a list of entities by creating a HTML list (`<ul>`) and
+    call the view `listitem` for each entity of the result set. The 'list' view
+    will generate html like:
+
+    .. sourcecode:: html
+
+      <ul class="section">
+        <li>"result of 'subvid' view for a row</li>
+        ...
+      </ul>
+
 
-*listitem*
-    This view redirects by default to the `outofcontext` view.
+*simplelist*
+
+  This view is not 'ul' based, and rely on div behaviour to separate items. html
+  will look like
+
+    .. sourcecode:: html
+
+      <div class="section">"result of 'subvid' view for a row</div>
+      ...
+
+
+  It relies on base :class:`~cubicweb.view.View` class implementation of the
+  :meth:`call` method to insert those <div>.
+
 
 *sameetypelist*
-    This view displays a list of entities of the same type, in HTML section (`<div>`)
-    and call the view `sameetypelistitem` for each entity of the result set.
 
-*sameetypelistitem*
-    This view redirects by default to the `listitem` view.
+    This view displays a list of entities of the same type, in HTML section
+    (`<div>`) and call the view `sameetypelistitem` for each entity of the result
+    set. It's designed to get a more adapted global list when displayed entities
+    are all of the same type.
+
 
 *csv*
-    This view applies to entity groups, which are individually
-    displayed using the `incontext` view. It displays each entity as a
-    coma separated list. It is NOT related to the well-known text file
-    format.
+
+    This view displays each entity in a coma separated list. It is NOT related to
+    the well-known text file format.
+
+
+Those list view can be given a 'subvid' arguments, telling the view to use of
+each item in the list. When not specified, the value of the 'redirect_vid'
+attribute of :class:`ListItemView` (for 'listview') or of :class:`SimpleListView`
+will be used. This default to 'outofcontext' for 'list' / 'incontext' for
+'simplelist'
+
 
 Text entity views
 ~~~~~~~~~~~~~~~~~
 
+Basic html view have some variantsto be used when generating raw text, not html
+(for notifications for instance).
+
 *text*
+
     This is the simplest text view for an entity. By default it returns the
     result of the `.dc_title` method, which is cut to fit the
     `navigation.short-line-size` property if necessary.
 
 *textincontext, textoutofcontext*
-    Similar to the `text` view, but called when an entity is considered out or
-    in context. By default it returns respectively the result of the
-    methods `.dc_title` and `.dc_long_title` of the entity.
+
+    Similar to the `text` view, but called when an entity is considered out or in
+    context (see description of incontext/outofcontext html views for more
+    information on this). By default it returns respectively the result of the
+    methods `.dc_title()` and `.dc_long_title()` of the entity.
--- a/doc/book/en/devweb/views/breadcrumbs.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/views/breadcrumbs.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -8,11 +8,11 @@
 ~~~~~~~
 
 Breadcrumbs are displayed by default in the header section (see
-:ref:`the_main_template_sections`).  With the default main
-template, the header section is composed by the logo, the application
-name, breadcrumbs and, at the most right, the login box. Breadcrumbs
-are displayed just next to the application name, thus breadcrumbs
-begin with a separator.
+:ref:`the_main_template_sections`).  With the default main template,
+the header section is composed by the logo, the application name,
+breadcrumbs and, at the most right, the login box. Breadcrumbs are
+displayed just next to the application name, thus they begin with a
+separator.
 
 Here is the header section of the CubicWeb's forge:
 
@@ -22,29 +22,31 @@
 :mod:`cubicweb.web.views.ibreadcrumbs`:
 
 - `BreadCrumbEntityVComponent`: displayed for a result set with one line
-  if the entity implements the ``IBreadCrumbs`` interface.
+  if the entity is adaptable to ``IBreadCrumbsAdapter``.
 - `BreadCrumbETypeVComponent`: displayed for a result set with more than
-  one line, but with all entities of the same type which implement the
-  ``IBreadCrumbs`` interface.
+  one line, but with all entities of the same type which can adapt to
+  ``IBreadCrumbsAdapter``.
 - `BreadCrumbAnyRSetVComponent`: displayed for any other result set.
 
 Building breadcrumbs
 ~~~~~~~~~~~~~~~~~~~~
 
-The ``IBreadCrumbs`` interface is defined in the
-:mod:`cubicweb.interfaces` module. It specifies that an entity which
-implements this interface must have a ``breadcrumbs`` method.
+The ``IBreadCrumbsAdapter`` adapter is defined in the
+:mod:`cubicweb.web.views.ibreadcrumbs` module. It specifies that an
+entity which implements this interface must have a ``breadcrumbs`` and
+a ``parent_entity`` method. A default implementation for each is
+provided. This implementation expoits the ITreeAdapter.
 
 .. note::
 
    Redefining the breadcrumbs is the hammer way to do it. Another way
-   is to define the `parent` method on an entity (as defined in the
-   `ITree` interface). If available, it will be used to compute
-   breadcrumbs.
+   is to define an `ITreeAdapter` adapter on an entity type. If
+   available, it will be used to compute breadcrumbs.
 
-Here is the API of the ``breadcrumbs`` method:
+Here is the API of the ``IBreadCrumbsAdapter`` class:
 
-.. automethod:: cubicweb.interfaces.IBreadCrumbs.breadcrumbs
+.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbsAdapter.parent_entity
+.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbsAdapter.breadcrumbs
 
 If the breadcrumbs method return a list of entities, the
 ``cubicweb.web.views.ibreadcrumbs.BreadCrumbView`` is used to display
--- a/doc/book/en/devweb/views/index.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/views/index.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -12,6 +12,7 @@
    views
    basetemplates
    primary
+   reledit
    baseviews
    startup
    boxes
--- a/doc/book/en/devweb/views/primary.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/views/primary.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -36,15 +36,16 @@
 Attributes/relations display location
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-In the primary view, there are 3 sections where attributes and
+In the primary view, there are three sections where attributes and
 relations can be displayed (represented in pink in the image above):
 
-* attributes
-* relations
-* sideboxes
+* 'attributes'
+* 'relations'
+* 'sideboxes'
 
 **Attributes** can only be displayed in the attributes section (default
-  behavior). They can also be hidden.
+  behavior). They can also be hidden. By default, attributes of type `Password`
+  and `Bytes` are hidden.
 
 For instance, to hide the ``title`` attribute of the ``Blog`` entity:
 
@@ -95,6 +96,10 @@
 * ``order``: int used to control order within a section. When not specified,
   automatically set according to order in which tags are added.
 
+* ``label``: label for the relations section or side box
+
+* ``showlabel``: boolean telling whether the label is displayed
+
 .. sourcecode:: python
 
    # let us remind the schema of a blog entry
@@ -110,15 +115,31 @@
    for index, attr in enumerate('title', 'content', 'publish_date'):
        view_ctrl.tag_attribute(('BlogEntry', attr), {'order': index})
 
-Keys for relations only:
+By default, relations displayed in the 'relations' section are being displayed by
+the 'autolimited' view. This view will use comma separated values, or list view
+and/or limit your rset if there is too much items in it (and generate the "view
+all" link in this case).
 
-* ``label``: label for the relations section or side box
+You can control this view by setting the following values in the
+`primaryview_display_ctrl` relation tag:
+
+* `limit`, maximum number of entities to display. The value of the
+  'navigation.related-limit'  cwproperty is used by default (which is 8 by default).
+  If None, no limit.
 
-* ``showlabel``: boolean telling whether the label is displayed
+* `use_list_limit`, number of entities until which they should be display as a list
+  (eg using the 'list' view). Below that limit, the 'csv' view is used. If None,
+  display using 'csv' anyway.
+
+* `subvid`, the subview identifier (eg view that should be used of each item in the
+  list)
 
-* ``limit``: boolean telling if the results should be limited. If so, a link to all results is displayed
+Notice you can also use the `filter` key to set up a callback taking the related
+result set as argument and returning it filtered, to do some arbitrary filtering
+that can't be done using rql for instance.
 
-* ``filter``: callback taking the related result set as argument and returning it filtered
+
+
 
 .. sourcecode:: python
 
@@ -153,22 +174,19 @@
 are:
 
 *render_entity_title(self, entity)*
-    Renders the entity title using the ``def dc_title(self)`` method.
-
-*render_entity_metadata(self, entity)*
-    Renders the entity metadata by calling the ``metadata`` view on the
-    entity. This generic view is in cubicweb.views.baseviews.
+    Renders the entity title, by default using entity's :meth:`dc_title()` method.
 
 *render_entity_attributes(self, entity)*
-    Renders all the attribute of an entity with the exception of
-    attribute of type `Password` and `Bytes`. The skip_none class
-    attribute controls the display of None valued attributes.
+    Renders all attributes and relations in the 'attributes' section . The
+    :attr:`skip_none` attribute controls the display of `None` valued attributes.
 
 *render_entity_relations(self, entity)*
-    Renders all the relations of the entity in the main section of the page.
+    Renders all relations in the 'relations' section.
 
 *render_side_boxes(self, entity, boxes)*
-    Renders relations of the entity in a side box.
+    Renders side boxes on the right side of the content. This will generate a box
+    for each relation in the 'sidebox' section, as well as explicit box
+    appobjects selectable in this context.
 
 The placement of relations in the relations section or in side boxes
 can be controlled through the :ref:`primary_view_configuration` mechanism.
@@ -184,24 +202,25 @@
 subclass, you can already customize some of the rendering:
 
 *show_attr_label*
-    Renders the attribute label next to the attribute value if set to True.
+    Renders the attribute label next to the attribute value if set to `True`.
     Otherwise, does only display the attribute value.
 
 *show_rel_label*
-    Renders the relation label next to the relation value if set to True.
+    Renders the relation label next to the relation value if set to `True`.
     Otherwise, does only display the relation value.
 
 *skip_none*
-    Does not render an attribute value that is None if set to True.
+    Does not render an attribute value that is None if set to `True`.
 
 *main_related_section*
-    Renders the relations of the entity if set to True.
+    Renders the relations of the entity if set to `True`.
 
 A good practice is for you to identify the content of your entity type for which
 the default rendering does not answer your need so that you can focus on the specific
 method (from the list above) that needs to be modified. We do not advise you to
 overwrite ``render_entity`` unless you want a completely different layout.
 
+
 Example of customization and creation
 `````````````````````````````````````
 
@@ -215,11 +234,11 @@
 
 .. sourcecode:: python
 
-   from cubicweb.selectors import implements
+   from cubicweb.selectors import is_instance
    from cubicweb.web.views.primary import Primaryview
 
    class BlogEntryPrimaryView(PrimaryView):
-     __select__ = PrimaryView.__select__ & implements('BlogEntry')
+     __select__ = PrimaryView.__select__ & is_instance('BlogEntry')
 
        def render_entity_attributes(self, entity):
            self.w(u'<p>published on %s</p>' %
@@ -245,12 +264,12 @@
 .. sourcecode:: python
 
  from logilab.mtconverter import xml_escape
- from cubicweb.selectors import implements, one_line_rset
+ from cubicweb.selectors import is_instance, one_line_rset
  from cubicweb.web.views.primary import Primaryview
 
  class BlogPrimaryView(PrimaryView):
      __regid__ = 'primary'
-     __select__ = PrimaryView.__select__ & implements('Blog')
+     __select__ = PrimaryView.__select__ & is_instance('Blog')
      rql = 'Any BE ORDERBY D DESC WHERE BE entry_of B, BE publish_date D, B eid %(b)s'
 
      def render_entity_relations(self, entity):
@@ -260,7 +279,7 @@
 
  class BlogEntryInBlogView(EntityView):
      __regid__ = 'inblogcontext'
-     __select__ = implements('BlogEntry')
+     __select__ = is_instance('BlogEntry')
 
      def cell_call(self, row, col):
          entity = self.cw_rset.get_entity(row, col)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/views/reledit.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,122 @@
+.. _reledit:
+
+The "Click and Edit" (also `reledit`) View
+------------------------------------------
+
+The principal way to update data through the Web UI is through the
+`modify` action on entities, which brings a full form. This is
+described in the :ref:`webform` chapter.
+
+There is however another way to perform piecewise edition of entities
+and relations, using a specific `reledit` (for *relation edition*)
+view from the :mod:`cubicweb.web.views.reledit` module.
+
+This is typically applied from the default Primary View (see
+:ref:`primary_view`) on the attributes and relation section. It makes
+small editions more convenient.
+
+Of course, this can be used customely in any other view. Here come
+some explanation about its capabilities and instructions on the way to
+use it.
+
+Using `reledit`
+***************
+
+Let's start again with a simple example:
+
+.. sourcecode:: python
+
+   class Company(EntityType):
+        name = String(required=True, unique=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+        status = SubjectRelation('File', cardinality='?*', composite='subject')
+
+In some view code we might want to show these attributes/relations and
+allow the user to edit each of them in turn without having to leave
+the current page. We would write code as below:
+
+.. sourcecode:: python
+
+   company.view('reledit', rtype='name', default_value='<name>') # editable name attribute
+   company.view('reledit', rtype='boss') # editable boss relation
+   company.view('reledit', rtype='status') # editable attribute-like relation
+
+If one wanted to edit the company from a boss's point of view, one
+would have to indicate the proper relation's role. By default the role
+is `subject`.
+
+.. sourcecode:: python
+
+   person.view('reledit', rtype='boss', role='object')
+
+Each of these will provide with a different editing widget. The `name`
+attribute will obviously get a text input field. The `boss` relation
+will be edited through a selection box, allowing to pick another
+`Person` as boss. The `status` relation, given that it defines Company
+as a composite entity with one file inside, will provide additional actions
+
+* to `add` a `File` when there is one
+* to `delete` the `File` (if the cardinality allows it)
+
+Moreover, editing the relation or using the `add` action leads to an
+embedded edition/creation form allowing edition of the target entity
+(which is `File` in our example) instead of merely allowing to choose
+amongst existing files.
+
+The `reledit_ctrl` rtag
+***********************
+
+The behaviour of reledited attributes/relations can be finely
+controlled using the reledit_ctrl rtag, defined in
+:mod:`cubicweb.web.uicfg`.
+
+This rtag provides four control variables:
+
+* ``default_value``: alternative default value
+   The default value is what is shown when there is no value.
+* ``reload``: boolean, eid (to reload to) or function taking subject
+   and returning bool/eid This is useful when editing a relation (or
+   attribute) that impacts the url or another parts of the current
+   displayed page. Defaults to false.
+* ``rvid``: alternative view id (as str) for relation or composite
+   edition Default is 'incontext' or 'csv' depending on the
+   cardinality. They can also be statically changed by subclassing
+   ClickAndEditFormView and redefining _one_rvid (resp. _many_rvid).
+* ``edit_target``: 'rtype' (to edit the relation) or 'related' (to
+   edit the related entity) This controls whether to edit the relation
+   or the target entity of the relation.  Currently only one-to-one
+   relations support target entity edition. By default, the 'related'
+   option is taken whenever the relation is composite and one-to-one.
+
+Let's see how to use these controls.
+
+.. sourcecode:: python
+
+    from logilab.mtconverter import xml_escape
+    from cubicweb.web.uicfg import reledit_ctrl
+    reledit_ctrl.tag_attribute(('Company', 'name'),
+                               {'reload': lambda x:x.eid,
+                                'default_value': xml_escape(u'<logilab tastes better>')})
+    reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'edit_target': 'related'})
+
+The `default_value` needs to be an xml escaped unicode string.
+
+The `edit_target` tag on the `boss` relation being set to `related` will
+ensure edition of the `Person` entity instead (using a standard
+automatic form) of the association of Company and Person.
+
+Finally, the `reload` key accepts either a boolean, an eid or an
+unicode string representing an url. If an eid is provided, it will be
+internally transformed into an url. The eid/url case helps when one
+needs to reload and the current url is inappropriate. A common case is
+edition of a key attribute, which is part of the current url. If one
+user changed the Company's name from `lozilab` to `logilab`, reloading
+on http://myapp/company/lozilab would fail. Providing the entity's
+eid, then, forces to reload on something like http://myapp/company/42,
+which always work.
+
+
+
+
+
+
--- a/doc/book/en/devweb/views/table.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/views/table.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -7,6 +7,10 @@
     Creates a HTML table (`<table>`) and call the view `cell` for each cell of
     the result set. Applicable on any result set.
 
+*editable-table*
+    Creates an **editable** HTML table (`<table>`) and call the view `cell` for each cell of
+    the result set. Applicable on any result set.
+
 *cell*
     By default redirects to the `final` view if this is a final entity or
     `outofcontext` view otherwise
@@ -17,3 +21,58 @@
 
 .. autoclass:: cubicweb.web.views.tableview.TableView
    :members:
+
+Example
+```````
+
+Let us take an example from the timesheet cube:
+
+.. sourcecode:: python
+
+    class ActivityTable(EntityView):
+        __regid__ = 'activitytable'
+        __select__ = is_instance('Activity')
+        title = _('activitytable')
+
+        def call(self, showresource=True):
+            _ = self._cw._
+            headers  = [_("diem"), _("duration"), _("workpackage"), _("description"), _("state"), u""]
+            eids = ','.join(str(row[0]) for row in self.cw_rset)
+            rql = ('Any R, D, DUR, WO, DESCR, S, A, SN, RT, WT ORDERBY D DESC '
+                   'WHERE '
+                   '   A is Activity, A done_by R, R title RT, '
+                   '   A diem D, A duration DUR, '
+                   '   A done_for WO, WO title WT, '
+                   '   A description DESCR, A in_state S, S name SN, A eid IN (%s)' % eids)
+            if showresource:
+                displaycols = range(7)
+                headers.insert(0, display_name(self._cw, 'Resource'))
+            else: # skip resource column if asked to
+                displaycols = range(1, 7)
+            rset = self._cw.execute(rql)
+            self.wview('editable-table', rset, 'null',
+                       displayfilter=True, displayactions=False,
+                       headers=headers, displaycols=displaycols,
+                       cellvids={3: 'editable-final'})
+
+To obtain an editable table, specify 'edtitable-table' as vid. You
+have to select the entity in the rql request too (in order to kwnow
+which entity must be edited). You can specify an optional
+`displaycols` argument which defines column's indexes that will be
+displayed. In the above example, setting `showresource` to `False`
+will only render columns from index 1 to 7.
+
+The previous example results in:
+
+.. image:: ../../images/views-table-shadow.png
+
+
+In order to activate table filter mechanism, set the `displayfilter`
+argument to True. A small arrow will be displayed at the table's top
+right corner. Clicking on `show filter form` action, will display the
+filter form as below:
+
+.. image:: ../../images/views-table-filter-shadow.png
+
+By the same way, you can display all registered actions for the
+selected entity, setting `displayactions` argument to True.
--- a/doc/book/en/devweb/views/views.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/devweb/views/views.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -121,7 +121,7 @@
         """
         __regid__ = 'search-associate'
         title = _('search for association')
-        __select__ = one_line_rset() & match_search_state('linksearch') & implements('Any')
+        __select__ = one_line_rset() & match_search_state('linksearch') & is_instance('Any')
 
 
 XML views, binaries views...
Binary file doc/book/en/images/views-table-filter-shadow.png has changed
Binary file doc/book/en/images/views-table-filter.png has changed
Binary file doc/book/en/images/views-table-shadow.png has changed
Binary file doc/book/en/images/views-table.png has changed
--- a/doc/book/en/index.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/index.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -62,5 +62,3 @@
 
 * the :ref:`genindex`,
 * the :ref:`modindex`,
-
-.. |cubicweb| replace:: *CubicWeb*
--- a/doc/book/en/makefile	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/makefile	Wed Nov 03 16:38:28 2010 +0100
@@ -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/advanced/index.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/tutorials/advanced/index.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -335,7 +335,7 @@
 
 .. sourcecode:: python
 
-    from cubicweb.selectors import implements
+    from cubicweb.selectors import is_instance
     from cubicweb.server import hook
 
     class SetVisibilityOp(hook.Operation):
@@ -347,7 +347,7 @@
 
     class SetVisibilityHook(hook.Hook):
 	__regid__ = 'sytweb.setvisibility'
-	__select__ = hook.Hook.__select__ & implements('Folder', 'File', 'Image', 'Comment')
+	__select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Image', 'Comment')
 	events = ('after_add_entity',)
 	def __call__(self):
 	    hook.set_operation(self._cw, 'pending_visibility', self.entity.eid,
--- a/doc/book/en/tutorials/base/create-cube.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/tutorials/base/create-cube.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -307,11 +307,11 @@
 
 .. sourcecode:: python
 
-  from cubicweb.selectors import implements
+  from cubicweb.selectors import is_instance
   from cubicweb.web.views import primary
 
   class BlogEntryPrimaryView(primary.PrimaryView):
-      __select__ = implements('BlogEntry')
+      __select__ = is_instance('BlogEntry')
 
       def render_entity_attributes(self, entity):
           self.w(u'<p>published on %s</p>' %
@@ -357,7 +357,6 @@
     class BlogEntry(AnyEntity):
         """customized class for BlogEntry entities"""
     	__regid__ = 'BlogEntry'
-    	__implements__ = AnyEntity.__implements__
 
         def display_cw_logo(self):
             if 'CW' in self.title:
@@ -376,7 +375,7 @@
 .. sourcecode:: python
 
  class BlogEntryPrimaryView(primary.PrimaryView):
-     __select__ = implements('BlogEntry')
+     __select__ = is_instance('BlogEntry')
 
      ...
 
--- a/doc/book/en/tutorials/base/maintemplate.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/tutorials/base/maintemplate.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -123,8 +123,8 @@
 
 .. image:: ../../images/lax-book_06-simple-main-template_en.png
 
-XXX
-[WRITE ME]
+.. XXX
+.. [WRITE ME]
 
 * customize MainTemplate and show that everything in the user
   interface can be changed
--- a/doc/book/en/tutorials/index.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/doc/book/en/tutorials/index.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,227 @@
+==========================
+Use Windmill with CubicWeb
+==========================
+
+Windmill_ implements cross browser testing, in-browser recording and playback,
+and functionality for fast accurate debugging and test environment integration.
+
+.. _Windmill: http://www.getwindmill.com/
+
+`Online features list <http://www.getwindmill.com/features>`_ is available.
+
+
+Installation
+============
+
+Windmill
+--------
+
+You have to install Windmill manually for now. If you're using Debian, there is
+no binary package (`yet <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=579109>`_).
+
+The simplest solution is to use a *setuptools/pip* command (for a clean
+environment, take a look to the `virtualenv
+<http://pypi.python.org/pypi/virtualenv>`_ project as well)::
+
+    pip install windmill
+    curl -O http://github.com/windmill/windmill/tarball/master
+
+However, the Windmill project doesn't release frequently. Our recommandation is
+to used the last snapshot of the Git repository:
+
+.. sourcecode:: bash
+
+    git clone git://github.com/windmill/windmill.git HEAD
+    cd windmill
+    python setup.py develop
+
+Install instructions are `available <http://wiki.github.com/windmill/windmill/installing>`_.
+
+Be sure to have the windmill module in your PYTHONPATH afterwards::
+
+    python -c "import windmill"
+
+X dummy
+-------
+
+In order to reduce unecessary system load from your test machines, It's
+recommended to use X dummy server for testing the Unix web clients, you need a
+dummy video X driver (as xserver-xorg-video-dummy package in Debian) coupled
+with a light X server as `Xvfb <http://en.wikipedia.org/wiki/Xvfb>`_.
+
+    The dummy driver is a special driver available with the XFree86 DDX. To use
+    the dummy driver, simply substitue it for your normal card driver in the
+    Device section of your xorg.conf configuration file. For example, if you
+    normally uses an ati driver, then you will have a Device section with
+    Driver "ati" to let the X server know that you want it to load and use the
+    ati driver; however, for these conformance tests, you would change that
+    line to Driver "dummy" and remove any other ati specific options from the
+    Device section.
+
+    *From: http://www.x.org/wiki/XorgTesting*
+
+Then, you can run the X server with the following command :
+
+    /usr/bin/X11/Xvfb :1 -ac -screen 0 1280x1024x8 -fbdir /tmp
+
+
+Windmill usage
+==============
+
+Record your use case
+--------------------
+
+- start your instance manually
+- start Windmill_ with url site as last argument (read Usage_ or use *'-h'*
+  option to find required command line arguments)
+- use the record button
+- click on save to obtain python code of your use case
+- copy the content to a new file in a *windmill* directory
+
+.. _Usage: http://wiki.github.com/windmill/windmill/running-tests
+
+If you are using firefox as client, consider the "firebug" option.
+
+If you have a running instance, you can refine the test by the *loadtest* windmill option:
+
+    windmill -m firebug loadtest=<test_file.py> <instance url>
+
+Or use the internal windmill shell to explore available commands:
+
+    windmill -m firebug shell <instance url>
+
+.. sourcecode:: python
+
+    >>> load_test(<your test file>)
+    >>> run_test(<your test file>)
+
+
+
+Integrate Windmill tests into CubicWeb
+======================================
+
+Set environment
+---------------
+
+You have to create a new unit test file and a `windmill` directory and copy all
+your windmill use case into it.
+
+.. sourcecode:: python
+
+    # test_windmill.py
+
+    # Run all scenarii found in windmill directory
+    from cubicweb.devtools.cwwindmill import (CubicWebWindmillUseCase,
+                                              unittest_main)
+
+    if __name__ == '__main__':
+        unittest_main()
+
+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`.
+
+To run your test series::
+
+    % pytest test/test_windmill.py
+
+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 :class:`cubicweb.devtools.cwwindmill.CubicWebWindmillUseCase` for
+Windmill configuration. You can edit windmill settings with following class attributes:
+
+* browser
+  identification string (firefox|ie|safari|chrome) (firefox by default)
+* test_dir
+  testing file path or directory (windmill directory under your unit case
+  file by default)
+* edit_test
+  load and edit test for debugging (False by default)
+
+Examples:
+
+    browser = 'firefox'
+    test_dir = osp.join(__file__, 'windmill')
+    edit_test = False
+
+If you want to change cubicweb test server parameters, you can check class
+variables from :class:`CubicWebServerConfig` or inherit it with overriding the
+:var:`configcls` attribute in :class:`CubicWebServerTC` ::
+
+.. sourcecode:: python
+
+    class OtherCubicWebServerConfig(CubicWebServerConfig):
+        port = 9999
+
+    class NewCubicWebServerTC(CubicWebServerTC):
+        configcls = OtherCubicWebServerConfig
+
+For instance, CubicWeb framework windmill tests can be manually run by::
+
+    % pytest web/test/test_windmill.py
+
+Edit your tests
+---------------
+
+You can toggle the `edit_test` variable to enable test edition.
+
+But if you are using `pytest` as test runner, use the `-i` option directly.
+The test series will be loaded and you can run assertions step-by-step::
+
+    % pytest -i test/test_windmill.py
+
+In this case, the `firebug` extension will be loaded automatically for you.
+
+Afterwards, don't forget to save your edited test into the right file (no autosave feature).
+
+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.
+
+
+Caveats
+=======
+
+File Upload
+-----------
+
+Windmill can't do file uploads. This is a limitation of browser Javascript
+support / sandboxing, not of Windmill per se.  It would be nice if there were
+some command that would prime the Windmill HTTP proxy to add a particular file
+to the next HTTP request that comes through, so that uploads could at least be
+faked.
+
+.. http://groups.google.com/group/windmill-dev/browse_thread/thread/cf9dc969722bd6bb/01aa18fdd652f7ff?lnk=gst&q=input+type+file#01aa18fdd652f7ff
+
+.. http://davisagli.com/blog/in-browser-integration-testing-with-windmill
+
+.. http://groups.google.com/group/windmill-dev/browse_thread/thread/b7bebcc38ed30dc7
+
+
+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/coding_standards_css.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,33 @@
+CSS Coding Standards
+--------------------
+
+(Draft, to be continued)
+
+:Naming: camelCase
+
+Indentation rules
+~~~~~~~~~~~~~~~~~
+- 2 espaces avant les propriétés
+
+- pas d'espace avant les ":", un espace après
+
+- 1 seul espace entre les différentes valeurs pour une même propriété
+
+
+Documentation
+~~~~~~~~~~~~~
+Please keep rules semantically linked grouped together, with a comment about
+what they are for.
+
+Recommendation
+~~~~~~~~~~~~~~
+- Try to use existing classes rather than introduce new ones
+
+- Keep things as simple as possible while in the framework
+
+- Think about later customization by application
+
+- Avoid introducing a new CSS file for a few lines of CSS, at least while the
+  framework doesn't include packing functionalities
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/coding_standards_js.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,37 @@
+Javascript Coding Standards
+---------------------------
+
+(Draft, to be continued)
+
+:Naming: camelCase, except for CONSTANTS
+
+Indentation rules
+~~~~~~~~~~~~~~~~~
+- espace avant accolade ouvrante
+
+- retour à la ligne après accolade ouvrante (éventuellement pas
+  de retour à la ligne s'il y a tout sur la même ligne, mais ce n'est
+  pas le cas ici.
+
+- no tabs
+
+
+Documentation
+~~~~~~~~~~~~~
+XXX explain comment format for documentation generation
+
+
+Coding
+~~~~~~
+- Don't forget 'var' before variable definition, and semi-colon (';') after **each** statement.
+- Check the firebug console for deprecation warnings
+
+
+API usage
+~~~~~~~~~
+- unless intended, use jQuery('container') rather than jqNode('container')
+
+
+See also
+~~~~~~~~
+http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/refactoring-the-css-with-uiprops.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/entities/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,465 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""some basic entity adapter implementations, for interfaces used in the
+framework itself.
+"""
+
+__docformat__ = "restructuredtext en"
+
+from itertools import chain
+from warnings import warn
+
+from logilab.mtconverter import TransformError
+from logilab.common.decorators import cached
+
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, is_instance, relation_possible
+from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
+
+
+class IEmailableAdapter(EntityAdapter):
+    __regid__ = 'IEmailable'
+    __select__ = relation_possible('primary_email') | relation_possible('use_email')
+
+    def get_email(self):
+        if getattr(self.entity, 'primary_email', None):
+            return self.entity.primary_email[0].address
+        if getattr(self.entity, 'use_email', None):
+            return self.entity.use_email[0].address
+        return None
+
+    def allowed_massmail_keys(self):
+        """returns a set of allowed email substitution keys
+
+        The default is to return the entity's attribute list but you might
+        override this method to allow extra keys.  For instance, a Person
+        class might want to return a `companyname` key.
+        """
+        return set(rschema.type
+                   for rschema, attrtype in self.entity.e_schema.attribute_definitions()
+                   if attrtype.type not in ('Password', 'Bytes'))
+
+    def as_email_context(self):
+        """returns the dictionary as used by the sendmail controller to
+        build email bodies.
+
+        NOTE: the dictionary keys should match the list returned by the
+        `allowed_massmail_keys` method.
+        """
+        return dict( (attr, getattr(self.entity, attr))
+                     for attr in self.allowed_massmail_keys() )
+
+
+class INotifiableAdapter(EntityAdapter):
+    __regid__ = 'INotifiable'
+    __select__ = is_instance('Any')
+
+    @implements_adapter_compat('INotifiableAdapter')
+    def notification_references(self, view):
+        """used to control References field of email send on notification
+        for this entity. `view` is the notification view.
+
+        Should return a list of eids which can be used to generate message
+        identifiers of previously sent email(s)
+        """
+        itree = self.entity.cw_adapt_to('ITree')
+        if itree is not None:
+            return itree.path()[:-1]
+        return ()
+
+
+class IFTIndexableAdapter(EntityAdapter):
+    __regid__ = 'IFTIndexable'
+    __select__ = is_instance('Any')
+
+    def fti_containers(self, _done=None):
+        if _done is None:
+            _done = set()
+        entity = self.entity
+        _done.add(entity.eid)
+        containers = tuple(entity.e_schema.fulltext_containers())
+        if containers:
+            for rschema, target in containers:
+                if target == 'object':
+                    targets = getattr(entity, rschema.type)
+                else:
+                    targets = getattr(entity, 'reverse_%s' % rschema)
+                for entity in targets:
+                    if entity.eid in _done:
+                        continue
+                    for container in entity.cw_adapt_to('IFTIndexable').fti_containers(_done):
+                        yield container
+                        yielded = True
+        else:
+            yield entity
+
+    # weight in ABCD
+    entity_weight = 1.0
+    attr_weight = {}
+
+    def get_words(self):
+        """used by the full text indexer to get words to index
+
+        this method should only be used on the repository side since it depends
+        on the logilab.database package
+
+        :rtype: list
+        :return: the list of indexable word of this entity
+        """
+        from logilab.database.fti import tokenize
+        # take care to cases where we're modyfying the schema
+        entity = self.entity
+        pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
+        words = {}
+        for rschema in entity.e_schema.indexable_attributes():
+            if (entity.e_schema, rschema) in pending:
+                continue
+            weight = self.attr_weight.get(rschema, 'C')
+            try:
+                value = entity.printable_value(rschema, format='text/plain')
+            except TransformError:
+                continue
+            except:
+                self.exception("can't add value of %s to text index for entity %s",
+                               rschema, entity.eid)
+                continue
+            if value:
+                words.setdefault(weight, []).extend(tokenize(value))
+        for rschema, role in entity.e_schema.fulltext_relations():
+            if role == 'subject':
+                for entity_ in getattr(entity, rschema.type):
+                    merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
+            else: # if role == 'object':
+                for entity_ in getattr(entity, 'reverse_%s' % rschema.type):
+                    merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
+        return words
+
+def merge_weight_dict(maindict, newdict):
+    for weight, words in newdict.iteritems():
+        maindict.setdefault(weight, []).extend(words)
+
+class IDownloadableAdapter(EntityAdapter):
+    """interface for downloadable entities"""
+    __regid__ = 'IDownloadable'
+    __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract
+
+    @implements_adapter_compat('IDownloadable')
+    def download_url(self, **kwargs): # XXX not really part of this interface
+        """return an url to download entity's content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_content_type(self):
+        """return MIME type of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_encoding(self):
+        """return encoding of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_file_name(self):
+        """return file name of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_data(self):
+        """return actual data of the downloadable content"""
+        raise NotImplementedError
+
+
+class ITreeAdapter(EntityAdapter):
+    """This adapter has to be overriden to be configured using the
+    tree_relation, child_role and parent_role class attributes to benefit from
+    this default implementation.
+
+    This adapter provides a tree interface. It has to be overriden to be
+    configured using the tree_relation, child_role and parent_role class
+    attributes to benefit from this default implementation.
+
+    This class provides the following methods:
+
+    .. automethod: iterparents
+    .. automethod: iterchildren
+    .. automethod: prefixiter
+
+    .. automethod: is_leaf
+    .. automethod: is_root
+
+    .. automethod: root
+    .. automethod: parent
+    .. automethod: children
+    .. automethod: different_type_children
+    .. automethod: same_type_children
+    .. automethod: children_rql
+    .. automethod: path
+    """
+    __regid__ = 'ITree'
+    __select__ = implements(ITree, warn=False) # XXX for bw compat, else should be abstract
+
+    child_role = 'subject'
+    parent_role = 'object'
+
+    @property
+    def tree_relation(self):
+        warn('[3.9] tree_attribute is deprecated, define tree_relation on a custom '
+             'ITree for %s instead' % (self.entity.__class__),
+             DeprecationWarning)
+        return self.entity.tree_attribute
+
+    # XXX should be removed from the public interface
+    @implements_adapter_compat('ITree')
+    def children_rql(self):
+        """Returns RQL to get the children of the entity."""
+        return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
+
+    @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 the entity does not have any children."""
+        return len(self.children()) == 0
+
+    @implements_adapter_compat('ITree')
+    def is_root(self):
+        """Returns true if the entity is root of the tree (e.g. has no parent).
+        """
+        return self.parent() is None
+
+    @implements_adapter_compat('ITree')
+    def root(self):
+        """Return the root entity of the tree."""
+        return self._cw.entity_from_eid(self.path()[0])
+
+    @implements_adapter_compat('ITree')
+    def parent(self):
+        """Returns 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):
+        """Return an iterator on the parents of the entity."""
+        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):
+        """Return an iterator 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: %s', child.__regid__.lower(), child)
+                continue
+            yield child
+            _done.add(child.eid)
+
+    @implements_adapter_compat('ITree')
+    def prefixiter(self, _done=None):
+        """Return an iterator over the item's descendants in a prefixed order."""
+        if _done is None:
+            _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: %s', entity.__regid__.lower(), entity)
+                break
+            path.append(entity.eid)
+            try:
+                # check we are not jumping to another tree
+                if (adapter.tree_relation != self.tree_relation or
+                    adapter.child_role != self.child_role):
+                    break
+                entity = adapter.parent()
+                adapter = entity.cw_adapt_to('ITree')
+            except AttributeError:
+                break
+        path.reverse()
+        return path
+
+
+class IProgressAdapter(EntityAdapter):
+    """something that has a cost, a state and a progression.
+
+    You should at least override progress_info an in_progress methods on concret
+    implementations.
+    """
+    __regid__ = 'IProgress'
+    __select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def cost(self):
+        """the total cost"""
+        return self.progress_info()['estimated']
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def revised_cost(self):
+        return self.progress_info().get('estimatedcorrected', self.cost)
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def done(self):
+        """what is already done"""
+        return self.progress_info()['done']
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def todo(self):
+        """what remains to be done"""
+        return self.progress_info()['todo']
+
+    @implements_adapter_compat('IProgress')
+    def progress_info(self):
+        """returns a dictionary describing progress/estimated cost of the
+        version.
+
+        - mandatory keys are (''estimated', 'done', 'todo')
+
+        - optional keys are ('notestimated', 'notestimatedcorrected',
+          'estimatedcorrected')
+
+        'noestimated' and 'notestimatedcorrected' should default to 0
+        'estimatedcorrected' should default to 'estimated'
+        """
+        raise NotImplementedError
+
+    @implements_adapter_compat('IProgress')
+    def finished(self):
+        """returns True if status is finished"""
+        return not self.in_progress()
+
+    @implements_adapter_compat('IProgress')
+    def in_progress(self):
+        """returns True if status is not finished"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IProgress')
+    def progress(self):
+        """returns the % progress of the task item"""
+        try:
+            return 100. * self.done / self.revised_cost
+        except ZeroDivisionError:
+            # total cost is 0 : if everything was estimated, task is completed
+            if self.progress_info().get('notestimated'):
+                return 0.
+            return 100
+
+    @implements_adapter_compat('IProgress')
+    def progress_class(self):
+        return ''
+
+
+class IMileStoneAdapter(IProgressAdapter):
+    __regid__ = 'IMileStone'
+    __select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract
+
+    parent_type = None # specify main task's type
+
+    @implements_adapter_compat('IMileStone')
+    def get_main_task(self):
+        """returns the main ITask entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def initial_prevision_date(self):
+        """returns the initial expected end of the milestone"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def eta_date(self):
+        """returns expected date of completion based on what remains
+        to be done
+        """
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def completion_date(self):
+        """returns date on which the subtask has been completed"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def contractors(self):
+        """returns the list of persons supposed to work on this task"""
+        raise NotImplementedError
--- a/entities/authobjs.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/entities/authobjs.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/entities/lib.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/entities/schemaobjs.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/entities/test/unittest_base.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
 
 
@@ -44,16 +44,16 @@
         self.login(u'member')
         entity = self.request().create_entity('Bookmark', title=u"hello", path=u'project/cubicweb')
         self.commit()
-        self.assertEquals(entity.creator.eid, self.member.eid)
-        self.assertEquals(entity.dc_creator(), u'member')
+        self.assertEqual(entity.creator.eid, self.member.eid)
+        self.assertEqual(entity.dc_creator(), u'member')
 
     def test_type(self):
-        self.assertEquals(self.member.dc_type(), 'cwuser')
+        self.assertEqual(self.member.dc_type(), 'cwuser')
 
     def test_entity_meta_attributes(self):
         # XXX move to yams
-        self.assertEquals(self.schema['CWUser'].meta_attributes(), {})
-        self.assertEquals(dict((str(k), v) for k, v in self.schema['State'].meta_attributes().iteritems()),
+        self.assertEqual(self.schema['CWUser'].meta_attributes(), {})
+        self.assertEqual(dict((str(k), v) for k, v in self.schema['State'].meta_attributes().iteritems()),
                           {'description_format': ('format', 'description')})
 
 
@@ -63,20 +63,20 @@
         email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0)
         email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0)
         email1.set_relations(prefered_form=email2)
-        self.assertEquals(email1.prefered.eid, email2.eid)
-        self.assertEquals(email2.prefered.eid, email2.eid)
-        self.assertEquals(email3.prefered.eid, email3.eid)
+        self.assertEqual(email1.prefered.eid, email2.eid)
+        self.assertEqual(email2.prefered.eid, email2.eid)
+        self.assertEqual(email3.prefered.eid, email3.eid)
 
     def test_mangling(self):
         email = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0)
-        self.assertEquals(email.display_address(), 'maarten.ter.huurne@philips.com')
-        self.assertEquals(email.printable_value('address'), 'maarten.ter.huurne@philips.com')
+        self.assertEqual(email.display_address(), 'maarten.ter.huurne@philips.com')
+        self.assertEqual(email.printable_value('address'), 'maarten.ter.huurne@philips.com')
         self.vreg.config.global_set_option('mangle-emails', True)
-        self.assertEquals(email.display_address(), 'maarten.ter.huurne at philips dot com')
-        self.assertEquals(email.printable_value('address'), 'maarten.ter.huurne at philips dot com')
+        self.assertEqual(email.display_address(), 'maarten.ter.huurne at philips dot com')
+        self.assertEqual(email.printable_value('address'), 'maarten.ter.huurne at philips dot com')
         email = self.execute('INSERT EmailAddress X: X address "syt"').get_entity(0, 0)
-        self.assertEquals(email.display_address(), 'syt')
-        self.assertEquals(email.printable_value('address'), 'syt')
+        self.assertEqual(email.display_address(), 'syt')
+        self.assertEqual(email.printable_value('address'), 'syt')
 
 
 class CWUserTC(BaseEntityTC):
@@ -94,19 +94,19 @@
 
     def test_dc_title_and_name(self):
         e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0)
-        self.assertEquals(e.dc_title(), 'member')
-        self.assertEquals(e.name(), 'member')
+        self.assertEqual(e.dc_title(), 'member')
+        self.assertEqual(e.name(), 'member')
         e.set_attributes(firstname=u'bouah')
-        self.assertEquals(e.dc_title(), 'member')
-        self.assertEquals(e.name(), u'bouah')
+        self.assertEqual(e.dc_title(), 'member')
+        self.assertEqual(e.name(), u'bouah')
         e.set_attributes(surname=u'lôt')
-        self.assertEquals(e.dc_title(), 'member')
-        self.assertEquals(e.name(), u'bouah lôt')
+        self.assertEqual(e.dc_title(), 'member')
+        self.assertEqual(e.name(), u'bouah lôt')
 
     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.assertEqual(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):
@@ -143,7 +144,7 @@
         # no specific class for Subdivisions, the default one should be selected
         eclass = self.select_eclass('SubDivision')
         self.failUnless(eclass.__autogenerated__)
-        #self.assertEquals(eclass.__bases__, (AnyEntity,))
+        #self.assertEqual(eclass.__bases__, (AnyEntity,))
         # build class from most generic to most specific and make
         # sure the most specific is always selected
         self.vreg._loadedmods[__name__] = {}
@@ -155,12 +156,12 @@
             self.failUnless(eclass.__autogenerated__)
             self.failIf(eclass is Foo)
             if etype == 'SubDivision':
-                self.assertEquals(eclass.__bases__, (Foo,))
+                self.assertEqual(eclass.__bases__, (Foo,))
             else:
-                self.assertEquals(eclass.__bases__[0].__bases__, (Foo,))
+                self.assertEqual(eclass.__bases__[0].__bases__, (Foo,))
         # check Division eclass is still selected for plain Division entities
         eclass = self.select_eclass('Division')
-        self.assertEquals(eclass.__regid__, 'Division')
+        self.assertEqual(eclass.__regid__, 'Division')
 
 if __name__ == '__main__':
     unittest_main()
--- a/entities/test/unittest_wfobjs.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/entities/test/unittest_wfobjs.py	Wed Nov 03 16:38:28 2010 +0100
@@ -43,12 +43,12 @@
         wf = add_wf(self, 'Company')
         foo = wf.add_state(u'foo', initial=True)
         bar = wf.add_state(u'bar')
-        self.assertEquals(wf.state_by_name('bar').eid, bar.eid)
-        self.assertEquals(wf.state_by_name('barrr'), None)
+        self.assertEqual(wf.state_by_name('bar').eid, bar.eid)
+        self.assertEqual(wf.state_by_name('barrr'), None)
         baz = wf.add_transition(u'baz', (foo,), bar, ('managers',))
-        self.assertEquals(wf.transition_by_name('baz').eid, baz.eid)
-        self.assertEquals(len(baz.require_group), 1)
-        self.assertEquals(baz.require_group[0].name, 'managers')
+        self.assertEqual(wf.transition_by_name('baz').eid, baz.eid)
+        self.assertEqual(len(baz.require_group), 1)
+        self.assertEqual(baz.require_group[0].name, 'managers')
 
     def test_duplicated_state(self):
         wf = add_wf(self, 'Company')
@@ -56,7 +56,7 @@
         self.commit()
         wf.add_state(u'foo')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a state of that name'})
+        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a state of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf2.add_state(u'foo', initial=True)
@@ -66,7 +66,7 @@
         self.commit()
         bar.set_attributes(name=u'foo')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a state of that name'})
+        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a state of that name'})
 
     def test_duplicated_transition(self):
         wf = add_wf(self, 'Company')
@@ -75,7 +75,7 @@
         wf.add_transition(u'baz', (foo,), bar, ('managers',))
         wf.add_transition(u'baz', (bar,), foo)
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
+        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf.add_state(u'foo', initial=True)
@@ -87,7 +87,7 @@
         self.commit()
         biz.set_attributes(name=u'baz')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
+        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
 
 
 class WorkflowTC(CubicWebTC):
@@ -95,51 +95,55 @@
     def setup_database(self):
         rschema = self.schema['in_state']
         for rdef in rschema.rdefs.values():
-            self.assertEquals(rdef.cardinality, '1*')
+            self.assertEqual(rdef.cardinality, '1*')
         self.member = self.create_user('member')
 
     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.assertEqual(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')
-        self.assertEquals([tr.comment for tr in e.reverse_wf_info_for],
+        e.cw_clear_relation_cache('wf_info_for', 'object')
+        self.assertEqual([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.assertEqual(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())
-        self.assertEquals(len(trs), 1)
-        self.assertEquals(trs[0].name, u'deactivate')
-        self.assertEquals(trs[0].destination(None).name, u'deactivated')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        trs = list(iworkflowable.possible_transitions())
+        self.assertEqual(len(trs), 1)
+        self.assertEqual(trs[0].name, u'deactivate')
+        self.assertEqual(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())
-        self.assertEquals(len(trs), 0)
+        trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions())
+        self.assertEqual(len(trs), 0)
 
     def _test_manager_deactivate(self, user):
-        user.clear_related_cache('in_state', 'subject')
-        self.assertEquals(len(user.in_state), 1)
-        self.assertEquals(user.state, 'deactivated')
-        trinfo = user.latest_trinfo()
-        self.assertEquals(trinfo.previous_state.name, 'activated')
-        self.assertEquals(trinfo.new_state.name, 'deactivated')
-        self.assertEquals(trinfo.comment, 'deactivate user')
-        self.assertEquals(trinfo.comment_format, 'text/plain')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        user.cw_clear_relation_cache('in_state', 'subject')
+        self.assertEqual(len(user.in_state), 1)
+        self.assertEqual(iworkflowable.state, 'deactivated')
+        trinfo = iworkflowable.latest_trinfo()
+        self.assertEqual(trinfo.previous_state.name, 'activated')
+        self.assertEqual(trinfo.new_state.name, 'deactivated')
+        self.assertEqual(trinfo.comment, 'deactivate user')
+        self.assertEqual(trinfo.comment_format, 'text/plain')
         return trinfo
 
     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)
+        self.assertEqual(trinfo.transition, None)
 
     def test_set_in_state_bad_wf(self):
         wf = add_wf(self, 'CWUser')
@@ -149,38 +153,41 @@
             ex = self.assertRaises(ValidationError, self.session.execute,
                                'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
                                {'x': self.user().eid, 's': s.eid})
-            self.assertEquals(ex.errors, {'in_state-subject': "state doesn't belong to entity's workflow. "
+            self.assertEqual(ex.errors, {'in_state-subject': "state doesn't belong to entity's workflow. "
                                       "You may want to set a custom workflow for this entity first."})
 
     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.assertEqual(iworkflowable.state, 'deactivated')
         self._test_manager_deactivate(user)
         trinfo = self._test_manager_deactivate(user)
-        self.assertEquals(trinfo.transition.name, 'deactivate')
+        self.assertEqual(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.assertEqual(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.assertEqual(iworkflowable.state, 'deactivated')
 
     # XXX test managers can change state without matching transition
 
@@ -189,19 +196,19 @@
         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')
-        self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
+                               iworkflowable.fire_transition, 'deactivate')
+        self.assertEqual(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')
-        self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
+                               iworkflowable.fire_transition, 'activate')
+        self.assertEqual(ex.errors, {'by_transition-subject': "transition may not be fired"})
 
     def test_fire_transition_owned_by(self):
         self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
@@ -248,45 +255,46 @@
         state3 = mwf.add_state(u'state3')
         swftr1 = mwf.add_wftransition(u'swftr1', swf, state1,
                                       [(swfstate2, state2), (swfstate3, state3)])
-        self.assertEquals(swftr1.destination(None).eid, swfstate1.eid)
+        self.assertEqual(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.assertEqual(iworkflowable.current_state.eid, state1.eid)
+        self.assertEqual(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEqual(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEqual(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.assertEqual(iworkflowable.current_state.eid, swfstate1.eid)
+        self.assertEqual(iworkflowable.current_workflow.eid, swf.eid)
+        self.assertEqual(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEqual(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.assertEqual(iworkflowable.current_state.eid, state2.eid)
+        self.assertEqual(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEqual(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEqual(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')
-        self.assertEquals(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
+                               iworkflowable.change_state, swfstate1, u'gadget')
+        self.assertEqual(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.assertEqual(iworkflowable.current_state.eid, state3.eid)
+        self.assertEqual(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEqual(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertListEqual(parse_hist(iworkflowable.workflow_history),
                               [('state1', 'swfstate1', 'swftr1', 'go'),
                                ('swfstate1', 'swfstate2', 'tr1', 'go'),
                                ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'),
@@ -310,7 +318,7 @@
         mwf.add_wftransition(u'swftr1', swf, state1,
                              [(swfstate2, state2), (swfstate2, state3)])
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
+        self.assertEqual(ex.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
 
     def test_swf_fire_in_a_row(self):
         # sub-workflow
@@ -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.assertEqual(iworkflowable.state, nextstate)
 
 
 class CustomWorkflowTC(CubicWebTC):
@@ -389,41 +399,44 @@
         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.assertEqual(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.assertEqual(iworkflowable.current_workflow.eid, wf.eid)
+        self.assertEqual(iworkflowable.state, 'asleep')
+        self.assertEqual(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.assertEqual(iworkflowable.current_workflow.eid, wf.eid)
+        self.assertEqual(iworkflowable.state, 'asleep')
+        self.assertEqual(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',
                      {'wf': wf.eid, 'x': self.member.eid})
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'custom_workflow-subject': u'workflow has no initial state'})
+        self.assertEqual(ex.errors, {'custom_workflow-subject': u'workflow has no initial state'})
 
     def test_custom_wf_bad_etype(self):
         """try to set a custom workflow which doesn't apply to entity type"""
@@ -432,13 +445,14 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'custom_workflow-subject': u"workflow isn't a workflow for this type"})
+        self.assertEqual(ex.errors, {'custom_workflow-subject': u"workflow isn't a workflow for this type"})
 
     def test_del_custom_wf(self):
         """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.assertEqual(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.assertEqual(iworkflowable.current_workflow.name, "default user workflow")
+        self.assertEqual(iworkflowable.state, 'activated')
+        self.assertEqual(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.assertEqual(iworkflowable.state, 'asleep')
+        self.assertEqual([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.assertEqual(iworkflowable.state, 'asleep')
+        self.assertEqual([t.name for t in iworkflowable.possible_transitions()],
                           ['rest'])
-        self.assertEquals(parse_hist(user.workflow_history),
+        self.assertEqual(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.assertEqual(iworkflowable.state, 'dead')
+        self.assertEqual(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.assertEqual(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.assertEqual(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
@@ -538,7 +555,7 @@
         self.commit()
         initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
                                     {'x' : ueid})[0][0]
-        self.assertEquals(initialstate, u'activated')
+        self.assertEqual(initialstate, u'activated')
         # give access to users group on the user's wf transitions
         # so we can test wf enforcing on euser (managers don't have anymore this
         # enforcement
@@ -572,18 +589,20 @@
     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')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
+                               iworkflowable.fire_transition, 'activate')
+        self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
     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')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
+                               iworkflowable.fire_transition, 'dummy')
+        self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
@@ -591,15 +610,18 @@
         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')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
+                               iworkflowable.fire_transition, 'deactivate')
+        self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
                                             u"transition isn't allowed from")
+        cnx.rollback()
+        session.set_pool()
         # get back now
-        user.fire_transition('activate')
+        iworkflowable.fire_transition('activate')
         cnx.commit()
         cnx.close()
 
--- a/entities/wfobjs.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/entities/wfobjs.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/entity.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -50,8 +51,20 @@
                 return card
     return '1'
 
+def can_use_rest_path(value):
+    """return True if value can be used at the end of a Rest URL path"""
+    if value is None:
+        return False
+    value = unicode(value)
+    # the check for ?, /, & are to prevent problems when running
+    # behind Apache mod_proxy
+    if value == u'' or u'?' in value or u'/' in value or u'&' in value:
+        return False
+    return True
 
-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 +119,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 +137,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'):
@@ -192,9 +223,10 @@
                 destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
                                             selection, orderby, restrictions,
                                             user, ordermethod, visited=visited)
-            orderterm = getattr(cls, ordermethod)(attr, var)
-            if orderterm:
-                orderby.append(orderterm)
+            if ordermethod is not None:
+                orderterm = getattr(cls, ordermethod)(attr, var)
+                if orderterm:
+                    orderby.append(orderterm)
         return selection, orderby, restrictions
 
     @classmethod
@@ -269,17 +301,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 +330,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 +359,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 +383,35 @@
                 del self.entity['load_left']
 
         """
-        super(Entity, self).__delitem__(attr)
+        del self.cw_attr_cache[attr]
         if hasattr(self, 'edited_attributes'):
             self.edited_attributes.remove(attr)
 
+    def clear(self):
+        self.cw_attr_cache.clear()
+
+    def get(self, key, default=None):
+        return self.cw_attr_cache.get(key, default)
+
     def setdefault(self, attr, default):
         """override setdefault to update self.edited_attributes"""
-        super(Entity, self).setdefault(attr, default)
+        value = self.cw_attr_cache.setdefault(attr, default)
         # don't add attribute into skip_security if already in edited
         # attributes, else we may accidentaly skip a desired security check
         if hasattr(self, 'edited_attributes') and \
                attr not in self.edited_attributes:
             self.edited_attributes.add(attr)
-            self.skip_security_attributes.add(attr)
+            self._cw_skip_security_attributes.add(attr)
+        return value
 
     def pop(self, attr, default=_marker):
         """override pop to update self.edited_attributes on cleanup of
         undesired changes introduced in the entity's dict. See `__delitem__`
         """
         if default is _marker:
-            value = super(Entity, self).pop(attr)
+            value = self.cw_attr_cache.pop(attr)
         else:
-            value = super(Entity, self).pop(attr, default)
+            value = self.cw_attr_cache.pop(attr, default)
         if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
             self.edited_attributes.remove(attr)
         return value
@@ -377,27 +422,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 +449,38 @@
         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, initargs=None, **kwargs): # XXX cw_view
         """shortcut to apply a view on this entity"""
+        if initargs is None:
+            initargs = kwargs
+        else:
+            initargs.update(kwargs)
         view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
                                                 row=self.cw_row, col=self.cw_col,
-                                                **kwargs)
+                                                **initargs)
         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
@@ -450,11 +492,16 @@
         # in linksearch mode, we don't want external urls else selecting
         # 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')
+        use_ext_id = False
+        if 'base_url' not in kwargs and \
+               getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
+            baseurl = self.cw_metainformation()['source'].get('base-url')
+            if baseurl:
+                kwargs['base_url'] = baseurl
+                use_ext_id = True
         if method in (None, 'view'):
             try:
-                kwargs['_restpath'] = self.rest_path(kwargs.get('base_url'))
+                kwargs['_restpath'] = self.rest_path(use_ext_id)
             except TypeError:
                 warn('[3.4] %s: rest_path() now take use_ext_eid argument, '
                      'please update' % self.__regid__, DeprecationWarning)
@@ -463,14 +510,14 @@
             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)
         path = etype.lower()
         if mainattr != 'eid':
             value = getattr(self, mainattr)
-            if value is None or unicode(value) == u'':
+            if not can_use_rest_path(value):
                 mainattr = 'eid'
                 path += '/eid'
             elif needcheck:
@@ -486,12 +533,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 +546,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 +565,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 +582,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 +592,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 +627,7 @@
             rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
                 rschema.type, rschema.type)
             execute(rql, {'x': self.eid, 'y': ceid})
-            self.clear_related_cache(rschema.type, 'subject')
+            self.cw_clear_relation_cache(rschema.type, 'subject')
         for rschema in self.e_schema.object_relations():
             if rschema.meta:
                 continue
@@ -592,36 +645,32 @@
             rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
                 rschema.type, rschema.type)
             execute(rql, {'x': self.eid, 'y': ceid})
-            self.clear_related_cache(rschema.type, 'object')
+            self.cw_clear_relation_cache(rschema.type, 'object')
 
     # data fetching methods ###################################################
 
     @cached
-    def as_rset(self):
+    def as_rset(self): # XXX .cw_as_rset
         """returns a resultset containing `self` information"""
         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
                          {'x': self.eid}, [(self.__regid__,)])
         rset.req = self._cw
         return rset
 
-    def to_complete_relations(self):
+    def _cw_to_complete_relations(self):
         """by default complete final relations to when calling .complete()"""
         for rschema in self.e_schema.subject_relations():
             if rschema.final:
                 continue
             targets = rschema.objects(self.e_schema)
-            if len(targets) > 1:
-                # ambigous relations, the querier doesn't handle
-                # outer join correctly in this case
-                continue
             if rschema.inlined:
                 matching_groups = self._cw.user.matching_groups
-                rdef = rschema.rdef(self.e_schema, targets[0])
-                if matching_groups(rdef.get_groups('read')) and \
-                   all(matching_groups(e.get_groups('read')) for e in targets):
+                if all(matching_groups(e.get_groups('read')) and
+                       rschema.rdef(self.e_schema, e).get_groups('read')
+                       for e in targets):
                     yield rschema, 'subject'
 
-    def to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
+    def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
         for rschema, attrschema in self.e_schema.attribute_definitions():
             # skip binary data by default
             if skip_bytes and attrschema.type == 'Bytes':
@@ -638,7 +687,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 +704,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()
@@ -665,28 +714,24 @@
             selected.append((attr, var))
         # +1 since this doen't include the main variable
         lastattr = len(selected) + 1
-        if attributes is None:
+        # don't fetch extra relation if attributes specified or of the entity is
+        # coming from an external source (may lead to error)
+        if attributes is None and self.cw_metainformation()['source']['uri'] == 'system':
             # fetch additional relations (restricted to 0..1 relations)
-            for rschema, role in self.to_complete_relations():
+            for rschema, role in self._cw_to_complete_relations():
                 rtype = rschema.type
-                if self.relation_cached(rtype, role):
+                if self.cw_relation_cached(rtype, role):
                     continue
+                # at this point we suppose that:
+                # * this is a inlined relation
+                # * entity (self) is the subject
+                # * user has read perm on the relation and on the target entity
+                assert rschema.inlined
+                assert role == 'subject'
                 var = varmaker.next()
-                targettype = rschema.targets(self.e_schema, role)[0]
-                rdef = rschema.role_rdef(self.e_schema, targettype, role)
-                card = rdef.role_cardinality(role)
-                assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
-                                                      role, card)
-                if role == 'subject':
-                    if card == '1':
-                        rql.append('%s %s %s' % (V, rtype, var))
-                    else:
-                        rql.append('%s %s %s?' % (V, rtype, var))
-                else:
-                    if card == '1':
-                        rql.append('%s %s %s' % (var, rtype, V))
-                    else:
-                        rql.append('%s? %s %s' % (var, rtype, V))
+                # keep outer join anyway, we don't want .complete to crash on
+                # missing mandatory relation (see #1058267)
+                rql.append('%s %s %s?' % (V, rtype, var))
                 selected.append(((rtype, role), var))
         if selected:
             # select V, we need it as the left most selected variable
@@ -706,9 +751,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 +761,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 +785,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 +793,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 +854,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 +916,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 +929,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 +949,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 +962,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 +989,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 +1000,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 +1018,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 +1042,42 @@
                 restr, ','.join(str(r.eid) for r in values)),
                              {'x': self.eid})
 
-    def delete(self, **kwargs):
+    def cw_delete(self, **kwargs):
         assert self.has_eid(), self.eid
         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
                          {'x': self.eid}, **kwargs)
 
     # server side utilities ###################################################
 
+    def _cw_rql_set_value(self, attr, value):
+        """call by rql execution plan when some attribute is modified
+
+        don't use dict api in such case since we don't want attribute to be
+        added to skip_security_attributes.
+
+        This method is for internal use, you should not use it.
+        """
+        self.cw_attr_cache[attr] = value
+
+    def _cw_clear_local_perm_cache(self, action):
+        for rqlexpr in self.e_schema.get_rqlexprs(action):
+            self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
+
     @property
-    def skip_security_attributes(self):
+    def _cw_skip_security_attributes(self):
         try:
-            return self._skip_security_attributes
+            return self.__cw_skip_security_attributes
         except:
-            self._skip_security_attributes = set()
-            return self._skip_security_attributes
+            self.__cw_skip_security_attributes = set()
+            return self.__cw_skip_security_attributes
 
-    def set_defaults(self):
+    def _cw_set_defaults(self):
         """set default values according to the schema"""
         for attr, value in self.e_schema.defaults():
-            if not self.has_key(attr):
+            if not self.cw_attr_cache.has_key(attr):
                 self[str(attr)] = value
 
-    def check(self, creation=False):
+    def _cw_check(self, creation=False):
         """check this entity against its schema. Only final relation
         are checked here, constraint on actual relations are checked in hooks
         """
@@ -1040,60 +1100,33 @@
         self.e_schema.check(self, creation=creation, _=_,
                             relations=relations)
 
-    def fti_containers(self, _done=None):
-        if _done is None:
-            _done = set()
-        _done.add(self.eid)
-        containers = tuple(self.e_schema.fulltext_containers())
-        if containers:
-            for rschema, target in containers:
-                if target == 'object':
-                    targets = getattr(self, rschema.type)
-                else:
-                    targets = getattr(self, 'reverse_%s' % rschema)
-                for entity in targets:
-                    if entity.eid in _done:
-                        continue
-                    for container in entity.fti_containers(_done):
-                        yield container
-                        yielded = True
-        else:
-            yield self
+    @deprecated('[3.9] use entity.cw_attr_value(attr)')
+    def get_value(self, name):
+        return self.cw_attr_value(name)
 
-    def get_words(self):
-        """used by the full text indexer to get words to index
+    @deprecated('[3.9] use entity.cw_delete()')
+    def delete(self, **kwargs):
+        return self.cw_delete(**kwargs)
 
-        this method should only be used on the repository side since it depends
-        on the logilab.database package
+    @deprecated('[3.9] use entity.cw_attr_metadata(attr, metadata)')
+    def attr_metadata(self, attr, metadata):
+        return self.cw_attr_metadata(attr, metadata)
 
-        :rtype: list
-        :return: the list of indexable word of this entity
-        """
-        from logilab.database.fti import tokenize
-        # take care to cases where we're modyfying the schema
-        pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
-        words = []
-        for rschema in self.e_schema.indexable_attributes():
-            if (self.e_schema, rschema) in pending:
-                continue
-            try:
-                value = self.printable_value(rschema, format='text/plain')
-            except TransformError:
-                continue
-            except:
-                self.exception("can't add value of %s to text index for entity %s",
-                               rschema, self.eid)
-                continue
-            if value:
-                words += tokenize(value)
-        for rschema, role in self.e_schema.fulltext_relations():
-            if role == 'subject':
-                for entity in getattr(self, rschema.type):
-                    words += entity.get_words()
-            else: # if role == 'object':
-                for entity in getattr(self, 'reverse_%s' % rschema.type):
-                    words += entity.get_words()
-        return words
+    @deprecated('[3.9] use entity.cw_has_perm(action)')
+    def has_perm(self, action):
+        return self.cw_has_perm(action)
+
+    @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)')
+    def set_related_cache(self, rtype, role, rset):
+        self.cw_set_relation_cache(rtype, role, rset)
+
+    @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role, rset)')
+    def clear_related_cache(self, rtype=None, role=None):
+        self.cw_clear_relation_cache(rtype, role)
+
+    @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
+    def related_rql(self, rtype, role='subject', targettypes=None):
+        return self.cw_related_rql(rtype, role, targettypes)
 
 
 # attribute and relation descriptors ##########################################
@@ -1108,22 +1141,22 @@
     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:
-            raise AttributeError('%s cannot be only be accessed from instances'
+            raise AttributeError('%s can only be accessed from instances'
                                  % self._rtype)
         return eobj.related(self._rtype, self._role, entities=True)
 
@@ -1131,14 +1164,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/etwist/request.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/etwist/server.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""twisted server for CubicWeb web instances
+"""twisted server for CubicWeb web instances"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -39,11 +38,11 @@
 from twisted.web import static, resource
 from twisted.web.server import NOT_DONE_YET
 
-from cubicweb.web import dumps
 
 from logilab.common.decorators import monkeypatch
 
 from cubicweb import AuthenticationError, ConfigurationError, CW_EVENT_MANAGER
+from cubicweb.utils import json_dumps
 from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.web.http_headers import generateDateTime
@@ -99,12 +98,11 @@
 
 
 class CubicWebRootResource(resource.Resource):
-    def __init__(self, config, debug=None):
-        self.debugmode = debug
+    def __init__(self, config, vreg=None):
         self.config = config
         # instantiate publisher here and not in init_publisher to get some
         # checks done before daemonization (eg versions consistency)
-        self.appli = CubicWebPublisher(config, debug=self.debugmode)
+        self.appli = CubicWebPublisher(config, vreg=vreg)
         self.base_url = config['base-url']
         self.https_url = config['https-url']
         self.children = {}
@@ -118,8 +116,6 @@
         # when we have an in-memory repository, clean unused sessions every XX
         # seconds and properly shutdown the server
         if config.repo_method == 'inmemory':
-            reactor.addSystemEventTrigger('before', 'shutdown',
-                                          self.shutdown_event)
             if config.pyro_enabled():
                 # if pyro is enabled, we have to register to the pyro name
                 # server, create a pyro daemon, and create a task to handle pyro
@@ -127,7 +123,10 @@
                 self.pyro_daemon = self.appli.repo.pyro_register()
                 self.pyro_listen_timeout = 0.02
                 self.appli.repo.looping_task(1, self.pyro_loop_event)
-            self.appli.repo.start_looping_tasks()
+            if config.mode != 'test':
+                reactor.addSystemEventTrigger('before', 'shutdown',
+                                              self.shutdown_event)
+                self.appli.repo.start_looping_tasks()
         self.set_url_rewriter()
         CW_EVENT_MANAGER.bind('after-registry-reload', self.set_url_rewriter)
 
@@ -156,6 +155,9 @@
         pre_path = request.path.split('/')[1:]
         if pre_path[0] == 'https':
             pre_path.pop(0)
+            uiprops = self.config.https_uiprops
+        else:
+            uiprops = self.config.uiprops
         directory = pre_path[0]
         # Anything in data/, static/, fckeditor/ and the generated versioned
         # data directory is treated as static files
@@ -165,7 +167,7 @@
             if directory == 'static':
                 return File(self.config.static_directory)
             if directory == 'fckeditor':
-                return File(self.config.ext_resources['FCKEDITOR_PATH'])
+                return File(uiprops['FCKEDITOR_PATH'])
             if directory != 'data':
                 # versioned directory, use specific file with http cache
                 # headers so their are cached for a very long time
@@ -173,10 +175,10 @@
             else:
                 cls = File
             if path == 'fckeditor':
-                return cls(self.config.ext_resources['FCKEDITOR_PATH'])
+                return cls(uiprops['FCKEDITOR_PATH'])
             if path == directory: # recurse
                 return self
-            datadir = self.config.locate_resource(path)
+            datadir, path = self.config.locate_resource(path)
             if datadir is None:
                 return self # recurse
             self.debug('static file %s from %s', path, datadir)
@@ -187,7 +189,10 @@
     def render(self, request):
         """Render a page from the root resource"""
         # reload modified files in debug mode
-        if self.debugmode:
+        if self.config.debugmode:
+            self.config.uiprops.reload_if_needed()
+            if self.https_url:
+                self.config.https_uiprops.reload_if_needed()
             self.appli.vreg.reload_if_needed()
         if self.config['profile']: # default profiler don't trace threads
             return self.render_request(request)
@@ -312,12 +317,12 @@
         self.setResponseCode(http.BAD_REQUEST)
         if path in JSON_PATHS: # XXX better json path detection
             self.setHeader('content-type',"application/json")
-            body = dumps({'reason': 'request max size exceeded'})
+            body = json_dumps({'reason': 'request max size exceeded'})
         elif path in FRAME_POST_PATHS: # XXX better frame post path detection
             self.setHeader('content-type',"text/html")
             body = ('<script type="text/javascript">'
                     'window.parent.handleFormValidationResponse(null, null, null, %s, null);'
-                    '</script>' % dumps( (False, 'request max size exceeded', None) ))
+                    '</script>' % json_dumps( (False, 'request max size exceeded', None) ))
         else:
             self.setHeader('content-type',"text/html")
             body = ("<html><head><title>Processing Failed</title></head><body>"
@@ -394,20 +399,22 @@
 LOGGER = getLogger('cubicweb.twisted')
 set_log_methods(CubicWebRootResource, LOGGER)
 
-def run(config, debug):
+def run(config, vreg=None, debug=None):
+    if debug is not None:
+        config.debugmode = debug
+    config.check_writeable_uid_directory(config.appdatahome)
     # create the site
-    root_resource = CubicWebRootResource(config, debug)
+    root_resource = CubicWebRootResource(config, vreg=vreg)
     website = server.Site(root_resource)
     # serve it via standard HTTP on port set in the configuration
     port = config['port'] or 8080
     reactor.listenTCP(port, website)
-    logger = getLogger('cubicweb.twisted')
-    if not debug:
+    if not config.debugmode:
         if sys.platform == 'win32':
             raise ConfigurationError("Under windows, you must use the service management "
                                      "commands (e.g : 'net start my_instance)'")
         from logilab.common.daemon import daemonize
-        print 'instance starting in the background'
+        LOGGER.info('instance started in the background on %s', root_resource.base_url)
         if daemonize(config['pid-file']):
             return # child process
     root_resource.init_publisher() # before changing uid
@@ -419,7 +426,7 @@
             uid = getpwnam(config['uid']).pw_uid
         os.setuid(uid)
     root_resource.start_service()
-    logger.info('instance started on %s', root_resource.base_url)
+    LOGGER.info('instance started on %s', root_resource.base_url)
     # avoid annoying warnign if not in Main Thread
     signals = threading.currentThread().getName() == 'MainThread'
     if config['profile']:
--- a/etwist/service.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/etwist/service.py	Wed Nov 03 16:38:28 2010 +0100
@@ -72,8 +72,9 @@
             # create the site
             config = cwcfg.config_for(self.instance)
             config.init_log(force=True)
+            config.debugmode = False
             logger.info('starting cubicweb instance %s ', self.instance)
-            root_resource = CubicWebRootResource(config, False)
+            root_resource = CubicWebRootResource(config)
             website = server.Site(root_resource)
             # serve it via standard HTTP on port set in the configuration
             port = config['port'] or 8080
--- a/etwist/twctl.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/etwist/twctl.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-clt handlers for twisted
-
-"""
+"""cubicweb-clt handlers for twisted"""
 
 from cubicweb.toolsutils import CommandHandler
 from cubicweb.web.webctl import WebCreateHandler
@@ -32,9 +30,9 @@
     cmdname = 'start'
     cfgname = 'twisted'
 
-    def start_server(self, config, debug):
+    def start_server(self, config):
         from cubicweb.etwist import server
-        server.run(config, debug)
+        server.run(config)
 
 class TWStopHandler(CommandHandler):
     cmdname = 'stop'
--- a/ext/rest.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/ext/rest.py	Wed Nov 03 16:38:28 2010 +0100
@@ -229,7 +229,7 @@
 
     :rtype: unicode
     :return:
-      the data formatted as HTML or the original data if an error occured
+      the data formatted as HTML or the original data if an error occurred
     """
     req = context._cw
     if isinstance(data, unicode):
@@ -242,8 +242,14 @@
         data = data.translate(ESC_CAR_TABLE)
     settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
                 'warning_stream': StringIO(),
+                'traceback': True, # don't sys.exit
+                'stylesheet': None, # don't try to embed stylesheet (may cause
+                                    # obscure bug due to docutils computing
+                                    # relative path according to the directory
+                                    # used *at import time*
                 # dunno what's the max, severe is 4, and we never want a crash
-                # (though try/except may be a better option...)
+                # (though try/except may be a better option...). May be the
+                # above traceback option will avoid this?
                 'halt_level': 10,
                 }
     if context:
--- a/ext/test/unittest_rest.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/ext/test/unittest_rest.py	Wed Nov 03 16:38:28 2010 +0100
@@ -29,9 +29,9 @@
 
     def test_eid_role(self):
         context = self.context()
-        self.assertEquals(rest_publish(context, ':eid:`%s`' % context.eid),
+        self.assertEqual(rest_publish(context, ':eid:`%s`' % context.eid),
                           '<p><a class="reference" href="http://testing.fr/cubicweb/cwuser/admin">#%s</a></p>\n' % context.eid)
-        self.assertEquals(rest_publish(context, ':eid:`%s:some text`' %  context.eid),
+        self.assertEqual(rest_publish(context, ':eid:`%s:some text`' %  context.eid),
                           '<p><a class="reference" href="http://testing.fr/cubicweb/cwuser/admin">some text</a></p>\n')
 
     def test_bad_rest_no_crash(self):
--- a/goa/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb on google appengine
-
-"""
-__docformat__ = "restructuredtext en"
-
-
-try:
-    # WARNING: do not import the google's db module here since it will take
-    #          precedence over our own db submodule
-    from google.appengine.api.datastore import Key, Get, Query
-    from google.appengine.api.datastore_errors import BadKeyError
-except ImportError:
-    # not in google app environment
-    pass
-else:
-
-    import os
-    _SS = os.environ.get('SERVER_SOFTWARE')
-    if _SS is None:
-        MODE = 'test'
-    elif _SS.startswith('Dev'):
-        MODE = 'dev'
-    else:
-        MODE = 'prod'
-
-    from cubicweb.server import SOURCE_TYPES
-    from cubicweb.goa.gaesource import GAESource
-    SOURCE_TYPES['gae'] = GAESource
-
-
-    def do_monkey_patch():
-
-        # monkey patch yams Bytes validator since it should take a bytes string with gae
-        # and not a StringIO
-        def check_bytes(eschema, value):
-            """check value is a bytes string"""
-            return isinstance(value, str)
-        from yams import constraints
-        constraints.BASE_CHECKERS['Bytes'] = check_bytes
-
-        def rql_for_eid(eid):
-            return 'Any X WHERE X eid "%s"' % eid
-        from cubicweb import uilib
-        uilib.rql_for_eid = rql_for_eid
-
-        def typed_eid(eid):
-            try:
-                return str(Key(eid))
-            except BadKeyError:
-                raise ValueError(eid)
-        import cubicweb
-        cubicweb.typed_eid = typed_eid
-
-        # XXX monkey patch cubicweb.schema.CubicWebSchema to have string eid with
-        #     optional cardinality (since eid is set after the validation)
-
-        import re
-        from yams import buildobjs as ybo
-
-        def add_entity_type(self, edef):
-            edef.name = edef.name.encode()
-            assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name)
-            eschema = super(CubicWebSchema, self).add_entity_type(edef)
-            if not eschema.final:
-                # automatically add the eid relation to non final entity types
-                rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Bytes',
-                                              cardinality='?1', uid=True)
-                self.add_relation_def(rdef)
-                rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type)
-                self.add_relation_def(rdef)
-            self._eid_index[eschema.eid] = eschema
-            return eschema
-
-        from cubicweb.schema import CubicWebSchema
-        CubicWebSchema.add_entity_type = add_entity_type
-
-
-        # don't reset vreg on repository set_schema
-        from cubicweb.server import repository
-        orig_set_schema = repository.Repository.set_schema
-        def set_schema(self, schema, resetvreg=True):
-            orig_set_schema(self, schema, False)
-        repository.Repository.set_schema = set_schema
-        # deactivate function ensuring relation cardinality consistency
-        repository.del_existing_rel_if_needed = lambda *args: None
-
-        def get_cubes(self):
-            """return the list of top level cubes used by this instance"""
-            config = self.config
-            cubes = config['included-cubes'] + config['included-yams-cubes']
-            return config.expand_cubes(cubes)
-        repository.Repository.get_cubes = get_cubes
-
-        from rql import RQLHelper
-        RQLHelper.simplify = lambda x, r: None
-
-        # activate entity caching on the server side
-
-        def set_entity_cache(self, entity):
-            self.transaction_data.setdefault('_eid_cache', {})[entity.eid] = entity
-
-        def entity_cache(self, eid):
-            return self.transaction_data['_eid_cache'][eid]
-
-        def drop_entity_cache(self, eid=None):
-            if eid is None:
-                self.transaction_data['_eid_cache'] = {}
-            elif '_eid_cache' in self.transaction_data:
-                self.transaction_data['_eid_cache'].pop(eid, None)
-
-        def datastore_get(self, key):
-            if isinstance(key, basestring):
-                key = Key(key)
-            try:
-                gentity = self.transaction_data['_key_cache'][key]
-                #self.critical('cached %s', gentity)
-            except KeyError:
-                gentity = Get(key)
-                #self.critical('Get %s', gentity)
-                self.transaction_data.setdefault('_key_cache', {})[key] = gentity
-            return gentity
-
-        def clear_datastore_cache(self, key=None):
-            if key is None:
-                self.transaction_data['_key_cache'] = {}
-            else:
-                if isinstance(key, basestring):
-                    key = Key(key)
-                self.transaction_data['_key_cache'].pop(key, None)
-
-        from cubicweb.server.session import Session
-        Session.set_entity_cache = set_entity_cache
-        Session.entity_cache = entity_cache
-        Session.drop_entity_cache = drop_entity_cache
-        Session.datastore_get = datastore_get
-        Session.clear_datastore_cache = clear_datastore_cache
-
-        from docutils.frontend import OptionParser
-        # avoid a call to expanduser which is not available under gae
-        def get_standard_config_files(self):
-            return self.standard_config_files
-        OptionParser.get_standard_config_files = get_standard_config_files
--- a/goa/appobjects/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/goa/appobjects/components.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""overrides some base views for cubicweb on google appengine
-
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.mtconverter import xml_escape
-
-from cubicweb import typed_eid
-from cubicweb.selectors import one_line_rset, match_search_state, accept
-from cubicweb.schema import display_name
-from cubicweb.view import StartupView, EntityView
-from cubicweb.web import Redirect
-from cubicweb.web.views import vid_from_rset
-
-from google.appengine.api import mail
-
-
-class SearchForAssociationView(EntityView):
-    """view called by the edition view when the user asks
-    to search for something to link to the edited eid
-    """
-    id = 'search-associate'
-
-    __select__ = one_line_rset() & match_search_state('linksearch') & accept
-
-    def cell_call(self, row, col):
-        entity = self.rset.get_entity(0, 0)
-        role, eid, rtype, etype = self.req.search_state[1]
-        assert entity.eid == typed_eid(eid)
-        rset = entity.unrelated(rtype, etype, role, ordermethod='fetch_order')
-        vid = vid_from_rset(self.req, rset, self.schema)
-        self.w(u'<div id="search-associate-content">')
-        self.pagination(self.req, rset, w=self.w)
-        self.wview(vid, rset)
-        self.w(u'</div>')
-
-
-class SchemaImageView(StartupView):
-    id = 'schemagraph'
-    binary = True
-    content_type = 'image/png'
-    def call(self):
-        """display global schema information"""
-        skipmeta = int(self.req.form.get('skipmeta', 1))
-        if skipmeta:
-            url = self.build_url('data/schema.png')
-        else:
-            url = self.build_url('data/metaschema.png')
-        raise Redirect(url)
-
-
-from cubicweb.web.views.baseviews import MetaDataView
-
-class GAEMetaDataView(MetaDataView):
-    show_eid = False
-
-
-from cubicweb.web.views.startup import ManageView
-
-def entity_types_no_count(self, eschemas):
-    """return a list of formatted links to get a list of entities of
-    a each entity's types
-    """
-    req = self.req
-    for eschema in eschemas:
-        if eschema.final or not (eschema.has_perm(req, 'read') or
-                                      eschema.has_local_role('read')):
-            continue
-        etype = eschema.type
-        label = display_name(req, etype, 'plural')
-        view = self.vreg.select('views', 'list', req, req.etype_rset(etype))
-        url = view.url()
-        etypelink = u'&#160;<a href="%s">%s</a>' % (xml_escape(url), label)
-        yield (label, etypelink, self.add_entity_link(eschema, req))
-
-ManageView.entity_types = entity_types_no_count
-
-
-from cubicweb.web.views.basecontrollers import SendMailController
-
-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'])
-    mail.send_mail(sender=sender, to=recipient,
-                   subject=subject, body=body)
-
-SendMailController.sendmail = sendmail
--- a/goa/appobjects/dbmgmt.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,200 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""special management views to manage repository content (initialization and
-restoration).
-
-"""
-__docformat__ = "restructuredtext en"
-
-from os.path import exists, join, abspath
-from pickle import loads, dumps
-
-from logilab.common.decorators import cached
-from logilab.mtconverter import xml_escape
-
-from cubicweb.selectors import none_rset, match_user_groups
-from cubicweb.view import StartupView
-from cubicweb.web import Redirect
-from cubicweb.goa.dbinit import fix_entities, init_persistent_schema, insert_versions
-
-from google.appengine.api.datastore import Entity, Key, Get, Put, Delete
-from google.appengine.api.datastore_types import Blob
-from google.appengine.api.datastore_errors import EntityNotFoundError
-
-
-def _get_status(name, create=True):
-    key = Key.from_path('EApplicationStatus', name)
-    try:
-        status = Get(key)
-    except EntityNotFoundError:
-        if create:
-            status = Entity('EApplicationStatus', name=name)
-        else:
-            status = None
-    return status
-
-
-class AuthInfo(StartupView):
-    """special management view to get cookie values to give to laxctl commands
-    which are doing datastore administration requests
-    """
-    id = 'authinfo'
-    __select__ = none_rset() & match_user_groups('managers')
-
-    def call(self):
-        cookie = self.req.get_cookie()
-        values = []
-        if self.config['use-google-auth']:
-            for param in ('ACSID', 'dev_appserver_login'):
-                morsel = cookie.get(param)
-                if morsel:
-                    values.append('%s=%s' % (param, morsel.value))
-                    break
-        values.append('__session=%s' % cookie['__session'].value)
-        self.w(u"<p>pass this flag to the client: --cookie='%s'</p>"
-               % xml_escape('; '.join(values)))
-
-
-
-class ContentInit(StartupView):
-    """special management view to initialize content of a repository,
-    step by step to avoid depassing quotas
-    """
-    id = 'contentinit'
-    __select__ = none_rset() & match_user_groups('managers')
-
-    def server_session(self):
-        ssession = self.config.repo_session(self.req.cnx.sessionid)
-        ssession.set_pool()
-        return ssession
-
-    def end_core_step(self, msg, status, stepid):
-        status['cpath'] = ''
-        status['stepid'] = stepid
-        Put(status)
-        self.msg(msg)
-
-    def call(self):
-        status = _get_status('creation')
-        if status.get('finished'):
-            self.redirect('process already completed')
-        config = self.config
-        # execute cubicweb's post<event> script
-        #mhandler.exec_event_script('post%s' % event)
-        # execute cubes'post<event> script if any
-        paths = [p for p in config.cubes_path() + [config.apphome]
-                 if exists(join(p, 'migration'))]
-        paths = [abspath(p) for p in (reversed(paths))]
-        cpath = status.get('cpath')
-        if cpath is None and status.get('stepid') is None:
-            init_persistent_schema(self.server_session(), self.schema)
-            self.end_core_step(u'inserted schema entities', status, 0)
-            return
-        if cpath == '' and status.get('stepid') == 0:
-            fix_entities(self.schema)
-            self.end_core_step(u'fixed bootstrap groups and users', status, 1)
-            return
-        if cpath == '' and status.get('stepid') == 1:
-            insert_versions(self.server_session(), self.config)
-            self.end_core_step(u'inserted software versions', status, None)
-            return
-        for i, path in enumerate(paths):
-            if not cpath or cpath == path:
-                self.info('running %s', path)
-                stepid = status.get('stepid')
-                context = status.get('context')
-                if context is not None:
-                    context = loads(context)
-                else:
-                    context = {}
-                stepid = self._migrhandler.exec_event_script(
-                    'postcreate', path, 'stepable_postcreate', stepid, context)
-                if stepid is None: # finished for this script
-                    # reset script state
-                    context = stepid = None
-                    # next time, go to the next script
-                    self.msg(u'finished postcreate for %s' % path)
-                    try:
-                        path = paths[i+1]
-                        self.continue_link()
-                    except IndexError:
-                        status['finished'] = True
-                        path = None
-                        self.redirect('process completed')
-                else:
-                    if context.get('stepidx'):
-                        self.msg(u'created %s entities for step %s of %s' % (
-                            context['stepidx'], stepid, path))
-                    else:
-                        self.msg(u'finished postcreate step %s for %s' % (
-                            stepid, path))
-                    context = Blob(dumps(context))
-                    self.continue_link()
-                status['context'] = context
-                status['stepid'] = stepid
-                status['cpath'] = path
-                break
-        else:
-            if not cpath:
-                # nothing to be done
-                status['finished'] = True
-                self.redirect('process completed')
-            else:
-                # Note the error: is expected by the laxctl command line tool,
-                # deal with this if internationalization is introduced
-                self.msg(u'error: strange creation state, can\'t find %s'
-                         % cpath)
-                self.w(u'<div>click <a href="%s?vid=contentclear">here</a> to '
-                       '<b>delete all datastore content</b> so process can be '
-                       'reinitialized</div>' % xml_escape(self.req.base_url()))
-        Put(status)
-
-    @property
-    @cached
-    def _migrhandler(self):
-        return self.config.migration_handler(self.schema, interactive=False,
-                                             cnx=self.req.cnx,
-                                             repo=self.config.repository())
-
-    def msg(self, msg):
-        self.w(u'<div class="message">%s</div>' % xml_escape(msg))
-    def redirect(self, msg):
-        raise Redirect(self.req.build_url('', msg))
-    def continue_link(self):
-        self.w(u'<a href="%s">continue</a><br/>' % xml_escape(self.req.url()))
-
-
-class ContentClear(StartupView):
-    id = 'contentclear'
-    __select__ = none_rset() & match_user_groups('managers')
-    skip_etypes = ('CWGroup', 'CWUser')
-
-    def call(self):
-        # XXX should use unsafe execute with all hooks deactivated
-        # XXX step by catching datastore errors?
-        for eschema in self.schema.entities():
-            if eschema.final or eschema in self.skip_etypes:
-                continue
-            self.req.execute('DELETE %s X' % eschema)
-            self.w(u'deleted all %s entities<br/>' % eschema)
-        status = _get_status('creation', create=False)
-        if status:
-            Delete(status)
-        self.w(u'done<br/>')
-        self.w(u'click <a href="%s?vid=contentinit">here</a> to start the data '
-               'initialization process<br/>' % self.req.base_url())
--- a/goa/appobjects/gauthservice.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""authentication using google authentication service
-
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb.web.views.basecomponents import UserLink
-from cubicweb.web.views.actions import LogoutAction
-
-from google.appengine.api import users
-
-
-class GACWUserLink(UserLink):
-
-    def anon_user_link(self):
-        self.w(self.req._('anonymous'))
-        self.w(u'&#160;[<a class="logout" href="%s">%s</a>]'
-               % (users.create_login_url(self.req.url()), self.req._('login')))
-
-class GAELogoutAction(LogoutAction):
-
-    def url(self):
-        return users.create_logout_url(self.req.build_url('logout') )
-
-def registration_callback(vreg):
-    if hasattr(vreg.config, 'has_resource'):
-        vreg.register(GACWUserLink, clear=True)
-        vreg.register(GAELogoutAction, clear=True)
--- a/goa/appobjects/sessions.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,291 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""persistent sessions stored in big table
-
-
-XXX TODO:
-* cleanup persistent session
-* use user as ancestor?
-"""
-__docformat__ = "restructuredtext en"
-
-from pickle import loads, dumps
-from time import localtime, strftime
-
-from logilab.common.decorators import cached, clear_cache
-
-from cubicweb import BadConnectionId
-from cubicweb.dbapi import Connection, ConnectionProperties, repo_connect
-from cubicweb.selectors import none_rset, match_user_groups
-from cubicweb.server.session import Session
-from cubicweb.web import InvalidSession
-from cubicweb.web.application import AbstractSessionManager
-from cubicweb.web.application import AbstractAuthenticationManager
-
-from google.appengine.api.datastore import Key, Entity, Get, Put, Delete, Query
-from google.appengine.api.datastore_errors import EntityNotFoundError
-from google.appengine.api.datastore_types import Blob
-
-try:
-    del Connection.__del__
-except AttributeError:
-    pass # already deleted
-
-
-class GAEAuthenticationManager(AbstractAuthenticationManager):
-    """authenticate user associated to a request and check session validity,
-    using google authentication service
-    """
-
-    def __init__(self, *args, **kwargs):
-        super(GAEAuthenticationManager, self).__init__(*args, **kwargs)
-        self._repo = self.config.repository(vreg=self.vreg)
-
-    def authenticate(self, req, _login=None, _password=None):
-        """authenticate user and return an established connection for this user
-
-        :raise ExplicitLogin: if authentication is required (no authentication
-        info found or wrong user/password)
-        """
-        if _login is not None:
-            login, password = _login, _password
-        else:
-            login, password = req.get_authorization()
-        # remove possibly cached cursor coming from closed connection
-        clear_cache(req, 'cursor')
-        cnxprops = ConnectionProperties(self.vreg.config.repo_method,
-                                        close=False, log=False)
-        cnx = repo_connect(self._repo, login, password=password, cnxprops=cnxprops)
-        self._init_cnx(cnx, login, password)
-        # associate the connection to the current request
-        req.set_connection(cnx)
-        return cnx
-
-    def _init_cnx(self, cnx, login, password):
-        cnx.anonymous_connection = self.config.is_anonymous_user(login)
-        cnx.vreg = self.vreg
-        cnx.login = login
-        cnx.password = password
-
-
-class GAEPersistentSessionManager(AbstractSessionManager):
-    """manage session data associated to a session identifier"""
-
-    def __init__(self, vreg, *args, **kwargs):
-        super(GAEPersistentSessionManager, self).__init__(vreg, *args, **kwargs)
-        self._repo = self.config.repository(vreg=vreg)
-
-    def get_session(self, req, sessionid):
-        """return existing session for the given session identifier"""
-        # search a record for the given session
-        key = Key.from_path('CubicWebSession', 'key_' + sessionid, parent=None)
-        try:
-            record = Get(key)
-        except EntityNotFoundError:
-            raise InvalidSession()
-        repo = self._repo
-        if self.has_expired(record):
-            repo._sessions.pop(sessionid, None)
-            Delete(record)
-            raise InvalidSession()
-        # associate it with a repository session
-        try:
-            reposession = repo._get_session(sessionid)
-            user = reposession.user
-            # touch session to avoid closing our own session when sessions are
-            # cleaned (touch is done on commit/rollback on the server side, too
-            # late in that case)
-            reposession._touch()
-        except BadConnectionId:
-            # can't found session in the repository, this probably mean the
-            # session is not yet initialized on this server, hijack the repo
-            # to create it
-            # use an internal connection
-            ssession = repo.internal_session()
-            # try to get a user object
-            try:
-                user = repo.authenticate_user(ssession, record['login'],
-                                              record['password'])
-            finally:
-                ssession.close()
-            reposession = Session(user, self._repo, _id=sessionid)
-            self._repo._sessions[sessionid] = reposession
-        cnx = Connection(self._repo, sessionid)
-        return self._get_proxy(req, record, cnx, user)
-
-    def open_session(self, req):
-        """open and return a new session for the given request"""
-        cnx = self.authmanager.authenticate(req)
-        # avoid rebuilding a user
-        user = self._repo._get_session(cnx.sessionid).user
-        # build persistent record for session data
-        record = Entity('CubicWebSession', name='key_' + cnx.sessionid)
-        record['login'] = cnx.login
-        record['password'] = cnx.password
-        record['anonymous_connection'] = cnx.anonymous_connection
-        Put(record)
-        return self._get_proxy(req, record, cnx, user)
-
-    def close_session(self, proxy):
-        """close session on logout or on invalid session detected (expired out,
-        corrupted...)
-        """
-        proxy.close()
-
-    def current_sessions(self):
-        for record in Query('CubicWebSession').Run():
-            yield ConnectionProxy(record)
-
-    def _get_proxy(self, req, record, cnx, user):
-        proxy = ConnectionProxy(record, cnx, user)
-        user.req = req
-        req.set_connection(proxy, user)
-        return proxy
-
-
-class ConnectionProxy(object):
-
-    def __init__(self, record, cnx=None, user=None):
-        self.__record = record
-        self.__cnx = cnx
-        self.__user = user
-        self.__data = None
-        self.__is_dirty = False
-        self.sessionid = record.key().name()[4:] # remove 'key_' prefix
-
-    def __repr__(self):
-        sstr = '<ConnectionProxy %s' % self.sessionid
-        if self.anonymous_connection:
-            sstr += ' (anonymous)'
-        elif self.__user:
-            sstr += ' for %s' % self.__user.login
-        sstr += ', last used %s>' % strftime('%T', localtime(self.last_usage_time))
-        return sstr
-
-    def __getattribute__(self, name):
-        try:
-            return super(ConnectionProxy, self).__getattribute__(name)
-        except AttributeError:
-            return getattr(self.__cnx, name)
-
-    def _set_last_usage_time(self, value):
-        self.__is_dirty = True
-        self.__record['last_usage_time'] = value
-    def _get_last_usage_time(self):
-        return self.__record['last_usage_time']
-
-    last_usage_time = property(_get_last_usage_time, _set_last_usage_time)
-
-    @property
-    def anonymous_connection(self):
-        # use get() for bw compat if sessions without anonymous information are
-        # found. Set default to True to limit lifetime of those sessions.
-        return self.__record.get('anonymous_connection', True)
-
-    @property
-    @cached
-    def data(self):
-        if self.__record.get('data') is not None:
-            try:
-                return loads(self.__record['data'])
-            except:
-                self.__is_dirty = True
-                self.exception('corrupted session data for session %s',
-                               self.__cnx)
-        return {}
-
-    def get_session_data(self, key, default=None, pop=False):
-        """return value associated to `key` in session data"""
-        if pop:
-            try:
-                value = self.data.pop(key)
-                self.__is_dirty = True
-                return value
-            except KeyError:
-                return default
-        else:
-            return self.data.get(key, default)
-
-    def set_session_data(self, key, value):
-        """set value associated to `key` in session data"""
-        self.data[key] = value
-        self.__is_dirty = True
-
-    def del_session_data(self, key):
-        """remove value associated to `key` in session data"""
-        try:
-            del self.data[key]
-            self.__is_dirty = True
-        except KeyError:
-            pass
-
-    def commit(self):
-        if self.__is_dirty:
-            self.__save()
-        self.__cnx.commit()
-
-    def rollback(self):
-        self.__save()
-        self.__cnx.rollback()
-
-    def close(self):
-        if self.__cnx is not None:
-            self.__cnx.close()
-        Delete(self.__record)
-
-    def __save(self):
-        if self.__is_dirty:
-            self.__record['data'] = Blob(dumps(self.data))
-            Put(self.__record)
-            self.__is_dirty = False
-
-    def user(self, req=None, props=None):
-        """return the User object associated to this connection"""
-        return self.__user
-
-
-import logging
-from cubicweb import set_log_methods
-set_log_methods(ConnectionProxy, logging.getLogger('cubicweb.web.goa.session'))
-
-
-from cubicweb.view import StartupView
-from cubicweb.web import application
-
-class SessionsCleaner(StartupView):
-    id = 'cleansessions'
-    __select__ = none_rset() & match_user_groups('managers')
-
-    def call(self):
-        # clean web session
-        session_manager = application.SESSION_MANAGER
-        nbclosed, remaining = session_manager.clean_sessions()
-        self.w(u'<div class="message">')
-        self.w(u'%s web sessions closed<br/>\n' % nbclosed)
-        # clean repository sessions
-        repo = self.config.repository(vreg=self.vreg)
-        nbclosed = repo.clean_sessions()
-        self.w(u'%s repository sessions closed<br/>\n' % nbclosed)
-        self.w(u'%s remaining sessions<br/>\n' % remaining)
-        self.w(u'</div>')
-
-
-def registration_callback(vreg):
-    vreg.register(SessionsCleaner)
-    vreg.register(GAEAuthenticationManager, clear=True)
-    vreg.register(GAEPersistentSessionManager, clear=True)
--- a/goa/bin/laxctl	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-import os.path as osp
-
-APPLROOT = osp.abspath(osp.join(osp.dirname(osp.abspath(__file__)), '..'))
-if APPLROOT not in sys.path:
-    sys.path.insert(0, APPLROOT)
-CUBES_DIR = osp.join(APPLROOT, 'cw-cubes')
-if CUBES_DIR not in sys.path:
-    sys.path.insert(1, CUBES_DIR)
-    
-try:
-    import custom
-except ImportError, exc:
-    print exc
-    sys.exit(2)
-
-from tools.laxctl import run
-run()
--- a/goa/db.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,469 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""provide replacement classes for gae db module, so that a gae model can be
-used as base for a cubicweb application by simply replacing ::
-
-  from google.appengine.ext import db
-
-by
-
-  from cubicweb.goa import db
-
-The db.model api should be fully featured by replacement classes, with the
-following differences:
-
-* all methods returning `google.appengine.ext.db.Model` instance(s) will return
-  `cubicweb.goa.db.Model` instance instead (though you should see almost no
-  difference since those instances have the same api)
-
-* class methods returning model instance take a `req` as first argument, unless
-  they are called through an instance, representing the current request
-  (accessible through `self.req` on almost all objects)
-
-* XXX no instance.<modelname>_set attributes, use instance.reverse_<attr name>
-      instead
-* XXX reference property always return a list of objects, not the instance
-* XXX name/collection_name argument of properties constructor are ignored
-* XXX ListProperty
-
-"""
-__docformat__ = "restructuredtext en"
-
-from copy import deepcopy
-
-from logilab.common.decorators import cached, iclassmethod
-
-from cubicweb import Binary, entities
-from cubicweb.req import RequestSessionBase
-from cubicweb.rset import ResultSet
-from cubicweb.entity import metaentity
-from cubicweb.server.utils import crypt_password
-from cubicweb.goa import MODE
-from cubicweb.goa.dbinit import init_relations
-
-from google.appengine.api.datastore import Get, Put, Key, Entity, Query
-from google.appengine.api.datastore import NormalizeAndTypeCheck, RunInTransaction
-from google.appengine.api.datastore_types import Text, Blob
-from google.appengine.api.datastore_errors import BadKeyError
-
-# XXX remove this dependancy
-from google.appengine.ext import db
-
-
-def rset_from_objs(req, objs, attrs=('eid',), rql=None, args=None):
-    """return a ResultSet instance for list of objects"""
-    if objs is None:
-        objs = ()
-    elif isinstance(objs, Entity):
-        objs = (objs,)
-    if rql is None:
-        rql = 'Any X'
-    rows = []
-    description = []
-    rset = ResultSet(rows, rql, args, description=description)
-    vreg = req.vreg
-    for i, obj in enumerate(objs):
-        line = []
-        linedescr = []
-        eschema = vreg.schema.eschema(obj.kind())
-        for j, attr in enumerate(attrs):
-            if attr == 'eid':
-                value = obj.key()
-                obj.row, obj.col = i, j
-                descr = eschema.type
-                value = str(value)
-            else:
-                value = obj[attr]
-                descr = str(eschema.destination(attr))
-            line.append(value)
-            linedescr.append(descr)
-        rows.append(line)
-        description.append(linedescr)
-        for j, attr in enumerate(attrs):
-            if attr == 'eid':
-                entity = vreg.etype_class(eschema.type)(req, rset, i, j)
-                rset._get_entity_cache_ = {(i, j): entity}
-    rset.rowcount = len(rows)
-    rset.req = req
-    return rset
-
-
-def needrequest(wrapped):
-    def wrapper(cls, *args, **kwargs):
-        req = kwargs.pop('req', None)
-        if req is None and args and isinstance(args[0], RequestSessionBase):
-            args = list(args)
-            req = args.pop(0)
-        if req is None:
-            req = getattr(cls, 'req', None)
-            if req is None:
-                raise Exception('either call this method on an instance or '
-                                'specify the req argument')
-        return wrapped(cls, req, *args, **kwargs)
-    return iclassmethod(wrapper)
-
-
-class gaedbmetaentity(metaentity):
-    """metaclass for goa.db.Model classes: filter entity / db model part,
-    put aside the db model part for later creation of db model class.
-    """
-    def __new__(mcs, name, bases, classdict):
-        if not 'id' in classdict:
-            classdict['id'] = name
-        entitycls = super(gaedbmetaentity, mcs).__new__(mcs, name, bases, classdict)
-        return entitycls
-
-
-TEST_MODELS = {}
-
-def extract_dbmodel(entitycls):
-    if MODE == 'test' and entitycls in TEST_MODELS:
-        dbclassdict = TEST_MODELS[entitycls]
-    else:
-        dbclassdict = {}
-        for attr, value in entitycls.__dict__.items():
-            if isinstance(value, db.Property) or isinstance(value, ReferencePropertyStub):
-                dbclassdict[attr] = value
-                # don't remove attr from entitycls, this make tests fail, and it's anyway
-                # overwritten by descriptor at class initialization time
-                #delattr(entitycls, attr)
-    if MODE == 'test':
-        TEST_MODELS[entitycls] = dbclassdict
-        dbclassdict = deepcopy(dbclassdict)
-        for propname, prop in TEST_MODELS[entitycls].iteritems():
-            if getattr(prop, 'reference_class', None) is db._SELF_REFERENCE:
-                dbclassdict[propname].reference_class = db._SELF_REFERENCE
-    return dbclassdict
-
-
-class Model(entities.AnyEntity):
-    id = 'Any'
-    __metaclass__ = gaedbmetaentity
-
-    row = col = 0
-
-    @classmethod
-    def __initialize__(cls):
-        super(Model, cls).__initialize__()
-        cls._attributes = frozenset(rschema for rschema in cls.e_schema.subject_relations()
-                                    if rschema.final)
-
-    def __init__(self, *args, **kwargs):
-        # db.Model prototype:
-        #   __init__(self, parent=None, key_name=None, **kw)
-        #
-        # Entity prototype:
-        #   __init__(self, req, rset, row=None, col=0)
-        if args and isinstance(args[0], RequestSessionBase) or 'req' in kwargs:
-            super(Model, self).__init__(*args, **kwargs)
-            self._gaeinitargs = None
-        else:
-            super(Model, self).__init__(None, None)
-            # if Model instances are given in kwargs, turn them into db model
-            for key, val in kwargs.iteritems():
-                if key in self.e_schema.subject_relations() and not self.e_schema.schema[key].final:
-                    if isinstance(kwargs, (list, tuple)):
-                        val = [isinstance(x, Model) and x._dbmodel or x for x in val]
-                    elif isinstance(val, Model):
-                        val = val._dbmodel
-                    kwargs[key] = val.key()
-            self._gaeinitargs = (args, kwargs)
-
-    def __repr__(self):
-        return '<ModelEntity %s %s %s at %s>' % (
-            self.e_schema, self.eid, self.keys(), id(self))
-
-    def _cubicweb_to_datastore(self, attr, value):
-        attr = attr[2:] # remove 's_' / 'o_' prefix
-        if attr in self._attributes:
-            tschema = self.e_schema.destination(attr)
-            if tschema == 'String':
-                if len(value) > 500:
-                    value = Text(value)
-            elif tschema == 'Password':
-                # if value is a Binary instance, this mean we got it
-                # from a query result and so it is already encrypted
-                if isinstance(value, Binary):
-                    value = value.getvalue()
-                else:
-                    value = crypt_password(value)
-            elif tschema == 'Bytes':
-                if isinstance(value, Binary):
-                    value = value.getvalue()
-                value = Blob(value)
-        else:
-            value = Key(value)
-        return value
-
-    def _to_gae_dict(self, convert=True):
-        gaedict = {}
-        for attr, value in self.iteritems():
-            attr = 's_' + attr
-            if value is not None and convert:
-                value = self._cubicweb_to_datastore(attr, value)
-            gaedict[attr] = value
-        return gaedict
-
-    def to_gae_model(self):
-        dbmodel = self._dbmodel
-        dbmodel.update(self._to_gae_dict())
-        return dbmodel
-
-    @property
-    @cached
-    def _dbmodel(self):
-        if self.has_eid():
-            assert self._gaeinitargs is None
-            try:
-                return self.req.datastore_get(self.eid)
-            except AttributeError: # self.req is not a server session
-                return Get(self.eid)
-        self.set_defaults()
-        values = self._to_gae_dict(convert=False)
-        parent = key_name = _app = None
-        if self._gaeinitargs is not None:
-            args, kwargs = self._gaeinitargs
-            args = list(args)
-            if args:
-                parent = args.pop(0)
-            if args:
-                key_name = args.pop(0)
-            if args:
-                _app = args.pop(0)
-            assert not args
-            if 'parent' in kwargs:
-                assert parent is None
-                parent = kwargs.pop('parent')
-            if 'key_name' in kwargs:
-                assert key_name is None
-                key_name = kwargs.pop('key_name')
-            if '_app' in kwargs:
-                assert _app is None
-                _app = kwargs.pop('_app')
-
-            for key, value in kwargs.iteritems():
-                if key in self._attributes:
-                    values['s_'+key] = value
-        else:
-            kwargs = None
-        if key_name is None:
-            key_name = self.db_key_name()
-            if key_name is not None:
-                key_name = 'key_' + key_name
-        for key, value in values.iteritems():
-            if value is None:
-                continue
-            values[key] = self._cubicweb_to_datastore(key, value)
-        entity = Entity(self.id, parent, _app, key_name)
-        entity.update(values)
-        init_relations(entity, self.e_schema)
-        return entity
-
-    def db_key_name(self):
-        """override this method to control datastore key name that should be
-        used at entity creation.
-
-        Note that if this function return something else than None, the returned
-        value will be prefixed by 'key_' to build the actual key name.
-        """
-        return None
-
-    def metainformation(self):
-        return {'type': self.id, 'source': {'uri': 'system'}, 'extid': None}
-
-    def view(self, vid, __registry='views', **kwargs):
-        """shortcut to apply a view on this entity"""
-        return self.vreg[__registry].render(vid, self.req, rset=self.rset,
-                                           row=self.row, col=self.col, **kwargs)
-
-    @classmethod
-    def _rest_attr_info(cls):
-        mainattr, needcheck = super(Model, cls)._rest_attr_info()
-        if needcheck:
-            return 'eid', False
-        return mainattr, needcheck
-
-    def get_value(self, name):
-        try:
-            value = self[name]
-        except KeyError:
-            if not self.has_eid():
-                return None
-            value = self._dbmodel.get('s_'+name)
-            if value is not None:
-                if isinstance(value, Text):
-                    value = unicode(value)
-                elif isinstance(value, Blob):
-                    value = Binary(str(value))
-            self[name] = value
-        return value
-
-    def has_eid(self):
-        if self.eid is None:
-            return False
-        try:
-            Key(self.eid)
-            return True
-        except BadKeyError:
-            return False
-
-    def complete(self, skip_bytes=True):
-        pass
-
-    def unrelated(self, rtype, targettype, role='subject', limit=None,
-                  ordermethod=None):
-        # XXX dumb implementation
-        if limit is not None:
-            objs = Query(str(targettype)).Get(limit)
-        else:
-            objs = Query(str(targettype)).Run()
-        return rset_from_objs(self.req, objs, ('eid',),
-                              'Any X WHERE X is %s' % targettype)
-
-    def key(self):
-        return Key(self.eid)
-
-    def put(self, req=None):
-        if req is not None and self.req is None:
-            self.req = req
-        dbmodel = self.to_gae_model()
-        key = Put(dbmodel)
-        self.set_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})
-            self.row = self.col = 0
-        return dbmodel
-
-    @needrequest
-    def get(cls, req, keys):
-        # if check if this is a dict.key call
-        if isinstance(cls, Model) and keys in cls._attributes:
-            return super(Model, cls).get(keys)
-        rset = rset_from_objs(req, Get(keys), ('eid',),
-                              'Any X WHERE X eid IN %(x)s', {'x': keys})
-        return list(rset.entities())
-
-    @needrequest
-    def get_by_id(cls, req, ids, parent=None):
-        if isinstance(parent, Model):
-            parent = parent.key()
-        ids, multiple = NormalizeAndTypeCheck(ids, (int, long))
-        keys = [Key.from_path(cls.kind(), id, parent=parent)
-                for id in ids]
-        rset = rset_from_objs(req, Get(keys))
-        return list(rset.entities())
-
-    @classmethod
-    def get_by_key_name(cls, req, key_names, parent=None):
-        if isinstance(parent, Model):
-            parent = parent.key()
-        key_names, multiple = NormalizeAndTypeCheck(key_names, basestring)
-        keys = [Key.from_path(cls.kind(), name, parent=parent)
-                for name in key_names]
-        rset = rset_from_objs(req, Get(keys))
-        return list(rset.entities())
-
-    @classmethod
-    def get_or_insert(cls, req, key_name, **kwds):
-        def txn():
-            entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
-            if entity is None:
-                entity = cls(key_name=key_name, **kwds)
-                entity.put()
-            return entity
-        return RunInTransaction(txn)
-
-    @classmethod
-    def all(cls, req):
-        rset = rset_from_objs(req, Query(cls.id).Run())
-        return list(rset.entities())
-
-    @classmethod
-    def gql(cls, req, query_string, *args, **kwds):
-        raise NotImplementedError('use rql')
-
-    @classmethod
-    def kind(cls):
-        return cls.id
-
-    @classmethod
-    def properties(cls):
-        raise NotImplementedError('use eschema')
-
-    def dynamic_properties(self):
-        raise NotImplementedError('use eschema')
-
-    def is_saved(self):
-        return self.has_eid()
-
-    def parent(self):
-        parent = self._dbmodel.parent()
-        if not parent is None:
-            rset = rset_from_objs(self.req, (parent,), ('eid',),
-                                  'Any X WHERE X eid %(x)s', {'x': parent.key()})
-            parent = rset.get_entity(0, 0)
-        return parent
-
-    def parent_key(self):
-        return self.parent().key()
-
-    def to_xml(self):
-        return self._dbmodel.ToXml()
-
-# hijack AnyEntity class
-entities.AnyEntity = Model
-
-BooleanProperty = db.BooleanProperty
-URLProperty = db.URLProperty
-DateProperty = db.DateProperty
-DateTimeProperty = db.DateTimeProperty
-TimeProperty = db.TimeProperty
-StringProperty = db.StringProperty
-TextProperty = db.TextProperty
-BlobProperty = db.BlobProperty
-IntegerProperty = db.IntegerProperty
-FloatProperty = db.FloatProperty
-ListProperty = db.ListProperty
-SelfReferenceProperty = db.SelfReferenceProperty
-UserProperty = db.UserProperty
-
-
-class ReferencePropertyStub(object):
-    def __init__(self, cls, args, kwargs):
-        self.cls = cls
-        self.args = args
-        self.kwargs = kwargs
-        self.required = False
-        self.__dict__.update(kwargs)
-        self.creation_counter = db.Property.creation_counter
-        db.Property.creation_counter += 1
-
-    @property
-    def data_type(self):
-        class FakeDataType(object):
-            @staticmethod
-            def kind():
-                return self.cls.__name__
-        return FakeDataType
-
-def ReferenceProperty(cls, *args, **kwargs):
-    if issubclass(cls, db.Model):
-        cls = db.class_for_kind(cls.__name__)
-        return db.ReferenceProperty(cls, *args, **kwargs)
-    return ReferencePropertyStub(cls, args, kwargs)
--- a/goa/dbinit.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""some utility functions for datastore initialization.
-
-"""
-__docformat__ = "restructuredtext en"
-
-from google.appengine.api.datastore import Key, Entity, Put, Get, Query
-from google.appengine.api import datastore_errors
-
-_GROUP_CACHE = {} # XXX use memcache
-
-def _get_group(groupname):
-    try:
-        return _GROUP_CACHE[groupname]
-    except KeyError:
-        key = Key.from_path('CWGroup', 'key_' + groupname, parent=None)
-        try:
-            group = Get(key)
-        except datastore_errors.EntityNotFoundError:
-            raise Exception('can\'t find required group %s, is your instance '
-                            'correctly initialized (eg did you run the '
-                            'initialization script) ?' % groupname)
-        _GROUP_CACHE[groupname] = group
-        return group
-
-
-def create_user(login, password, groups):
-    """create a cubicweb user"""
-    from cubicweb.server.utils import crypt_password
-    user = Entity('CWUser', name=login)
-    user['s_login'] = unicode(login)
-    user['s_upassword'] = crypt_password(password)
-    set_user_groups(user, groups)
-    Put(user)
-    return user
-
-def create_groups():
-    """create initial cubicweb groups"""
-    for groupname in ('managers', 'users', 'guests'):
-        group = Entity('CWGroup', name='key_' + groupname)
-        group['s_name'] = unicode(groupname)
-        Put(group)
-        _GROUP_CACHE[groupname] = group
-
-def set_user_groups(user, groups):
-    """set user in the given groups (as string). The given user entity
-    (datastore.Entity) is not putted back to the repository, this is the caller
-    responsability.
-    """
-    groups = [_get_group(g) for g in groups]
-    user['s_in_group'] = [g.key() for g in groups] or None
-    for group in groups:
-        try:
-            group['o_in_group'].append(user.key())
-        except (KeyError, AttributeError):
-            group['o_in_group'] = [user.key()]
-        Put(group)
-
-def init_relations(gaeentity, eschema):
-    """set None for every subject relations which is not yet defined"""
-    for rschema in eschema.subject_relations():
-        if rschema in ('identity', 'has_text'):
-            continue
-        dsrelation = 's_' + rschema.type
-        if not dsrelation in gaeentity:
-            gaeentity[dsrelation] = None
-    for rschema in eschema.object_relations():
-        if rschema == 'identity':
-            continue
-        dsrelation = 'o_' + rschema.type
-        if not dsrelation in gaeentity:
-            gaeentity[dsrelation] = None
-
-def fix_entities(schema):
-    for etype in ('CWUser', 'CWGroup'):
-        eschema = schema.eschema(etype)
-        for gaeentity in Query(etype).Run():
-            init_relations(gaeentity, eschema)
-            # XXX o_is on CWEType entity
-            gaeentity['s_is'] = Key.from_path('CWEType', 'key_' + etype, parent=None)
-            Put(gaeentity)
-
-def init_persistent_schema(ssession, schema):
-    execute = ssession.execute
-    rql = ('INSERT CWEType X: X name %(name)s, X description %(descr)s,'
-           'X final FALSE')
-    eschema = schema.eschema('CWEType')
-    execute(rql, {'name': u'CWEType', 'descr': unicode(eschema.description)})
-    for eschema in schema.entities():
-        if eschema.final or eschema == 'CWEType':
-            continue
-        execute(rql, {'name': unicode(eschema),
-                      'descr': unicode(eschema.description)})
-
-def insert_versions(ssession, config):
-    execute = ssession.execute
-    # insert versions
-    execute('INSERT CWProperty X: X pkey %(pk)s, X value%(v)s',
-            {'pk': u'system.version.cubicweb',
-             'v': unicode(config.cubicweb_version())})
-    for cube in config.cubes():
-        execute('INSERT CWProperty X: X pkey %(pk)s, X value%(v)s',
-                {'pk': u'system.version.%s' % cube,
-                 'v': unicode(config.cube_version(cube))})
--- a/goa/dbmyams.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,223 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""extends yams to be able to load google appengine's schemas
-
-MISSING FEATURES:
- - ListProperty, StringList, EmailProperty, etc. (XXX)
- - ReferenceProperty.verbose_name, collection_name, etc. (XXX)
-
-XXX proprify this knowing we'll use goa.db
-"""
-
-from os.path import join
-from datetime import datetime, date, time
-
-from google.appengine.ext import db
-from google.appengine.api import datastore_types
-
-from yams.buildobjs import (String, Int, Float, Boolean, Date, Time, Datetime,
-                            Bytes, SubjectRelation)
-from yams.buildobjs import metadefinition, EntityType
-
-from cubicweb.schema import CubicWebSchemaLoader
-from cubicweb.goa import db as goadb
-
-# db.Model -> yams ############################################################
-
-DBM2Y_TYPESMAP = {
-    basestring: String,
-    datastore_types.Text: String,
-    int: Int,
-    float: Float,
-    bool: Boolean,
-    time: Time,
-    date: Date,
-    datetime: Datetime,
-    datastore_types.Blob: Bytes,
-    }
-
-
-def dbm2y_default_factory(prop, **kwargs):
-    """just wraps the default types map to set
-    basic constraints like `required`, `default`, etc.
-    """
-    yamstype = DBM2Y_TYPESMAP[prop.data_type]
-    if 'default' not in kwargs:
-        default = prop.default_value()
-        if default is not None:
-            kwargs['default'] = default
-    if prop.required:
-        kwargs['required'] = True
-    return yamstype(**kwargs)
-
-def dbm2y_string_factory(prop):
-    """like dbm2y_default_factory but also deals with `maxsize` and `vocabulary`"""
-    kwargs = {}
-    if prop.data_type is basestring:
-        kwargs['maxsize'] = 500
-    if prop.choices is not None:
-        kwargs['vocabulary'] = prop.choices
-    return dbm2y_default_factory(prop, **kwargs)
-
-def dbm2y_date_factory(prop):
-    """like dbm2y_default_factory but also deals with today / now definition"""
-    kwargs = {}
-    if prop.auto_now_add:
-        if prop.data_type is datetime:
-            kwargs['default'] = 'now'
-        else:
-            kwargs['default'] = 'today'
-    # XXX no equivalent to Django's `auto_now`
-    return dbm2y_default_factory(prop, **kwargs)
-
-
-def dbm2y_relation_factory(etype, prop, multiple=False):
-    """called if `prop` is a `db.ReferenceProperty`"""
-    if multiple:
-        cardinality = '**'
-    elif prop.required:
-        cardinality = '1*'
-    else:
-        cardinality = '?*'
-    # XXX deal with potential kwargs of ReferenceProperty.__init__()
-    try:
-        return SubjectRelation(prop.data_type.kind(), cardinality=cardinality)
-    except AttributeError, ex:
-        # hack, data_type is still _SELF_REFERENCE_MARKER
-        return SubjectRelation(etype, cardinality=cardinality)
-
-
-DBM2Y_FACTORY = {
-    basestring: dbm2y_string_factory,
-    datastore_types.Text: dbm2y_string_factory,
-    int: dbm2y_default_factory,
-    float: dbm2y_default_factory,
-    bool: dbm2y_default_factory,
-    time: dbm2y_date_factory,
-    date: dbm2y_date_factory,
-    datetime: dbm2y_date_factory,
-    datastore_types.Blob: dbm2y_default_factory,
-    }
-
-
-class GaeSchemaLoader(CubicWebSchemaLoader):
-    """Google appengine schema loader class"""
-    def __init__(self, *args, **kwargs):
-        self.use_gauthservice = kwargs.pop('use_gauthservice', False)
-        super(GaeSchemaLoader, self).__init__(*args, **kwargs)
-        self.defined = {}
-        self.created = []
-        self.loaded_files = []
-        self._instantiate_handlers()
-
-    def finalize(self, register_base_types=False):
-        return self._build_schema('google-appengine', register_base_types)
-
-    def load_dbmodel(self, name, props):
-        clsdict = {}
-        ordered_props = sorted(props.items(),
-                               key=lambda x: x[1].creation_counter)
-        for pname, prop in ordered_props:
-            if isinstance(prop, db.ListProperty):
-                if not issubclass(prop.item_type, db.Model):
-                    self.error('ignoring list property with %s item type'
-                               % prop.item_type)
-                    continue
-                rdef = dbm2y_relation_factory(name, prop, multiple=True)
-            else:
-                try:
-                    if isinstance(prop, (db.ReferenceProperty,
-                                         goadb.ReferencePropertyStub)):
-                        rdef = dbm2y_relation_factory(name, prop)
-                    else:
-                        rdef = DBM2Y_FACTORY[prop.data_type](prop)
-                except KeyError, ex:
-                    import traceback
-                    traceback.print_exc()
-                    self.error('ignoring property %s (keyerror on %s)' % (pname, ex))
-                    continue
-            rdef.creation_rank = prop.creation_counter
-            clsdict[pname] = rdef
-        edef = metadefinition(name, (EntityType,), clsdict)
-        self.add_definition(self, edef())
-
-    def error(self, msg):
-        print 'ERROR:', msg
-
-    def import_yams_schema(self, ertype, schemamod):
-        erdef = self.pyreader.import_erschema(ertype, schemamod)
-
-    def import_yams_cube_schema(self, templpath):
-        for filepath in self.get_schema_files(templpath):
-            self.handle_file(filepath)
-
-    @property
-    def pyreader(self):
-        return self._live_handlers['.py']
-
-import os
-from cubicweb import CW_SOFTWARE_ROOT
-
-def load_schema(config, schemaclasses=None, extrahook=None):
-    """high level method to load all the schema for a lax instance"""
-    # IMPORTANT NOTE: dbmodel schemas must be imported **BEFORE**
-    # the loader is instantiated because this is where the dbmodels
-    # are registered in the yams schema
-    for compname in config['included-cubes']:
-        __import__('%s.schema' % compname)
-    loader = GaeSchemaLoader(use_gauthservice=config['use-google-auth'], db=db)
-    if schemaclasses is not None:
-        for cls in schemaclasses:
-            loader.load_dbmodel(cls.__name__, goadb.extract_dbmodel(cls))
-    elif config['schema-type'] == 'dbmodel':
-        import schema as appschema
-        for obj in vars(appschema).values():
-            if isinstance(obj, type) and issubclass(obj, goadb.Model) and obj.__module__ == appschema.__name__:
-                loader.load_dbmodel(obj.__name__, goadb.extract_dbmodel(obj))
-    for erschema in ('CWGroup', 'CWEType', 'CWRType', 'RQLExpression',
-                     'is_', 'is_instance_of',
-                     'read_permission', 'add_permission',
-                     'delete_permission', 'update_permission'):
-        loader.import_yams_schema(erschema, 'bootstrap')
-    loader.handle_file(join(CW_SOFTWARE_ROOT, 'schemas', 'base.py'))
-    cubes = config['included-yams-cubes']
-    for cube in reversed(config.expand_cubes(cubes)):
-        config.info('loading cube %s', cube)
-        loader.import_yams_cube_schema(config.cube_dir(cube))
-    if config['schema-type'] == 'yams':
-        loader.import_yams_cube_schema('.')
-    if extrahook is not None:
-        extrahook(loader)
-    if config['use-google-auth']:
-        loader.defined['CWUser'].remove_relation('upassword')
-        loader.defined['CWUser'].permissions['add'] = ()
-        loader.defined['CWUser'].permissions['delete'] = ()
-    for etype in ('CWGroup', 'RQLExpression'):
-        read_perm_rel = loader.defined[etype].get_relations('read_permission').next()
-        read_perm_rel.cardinality = '**'
-    # XXX not yet ready for CWUser workflow
-    loader.defined['CWUser'].remove_relation('in_state')
-    loader.defined['CWUser'].remove_relation('wf_info_for')
-    # remove RQLConstraint('NOT O name "owners"') on CWUser in_group CWGroup
-    # since "owners" group is not persistent with gae
-    loader.defined['CWUser'].get_relations('in_group').next().constraints = []
-    # return the full schema including the cubes' schema
-    for ertype in loader.defined.values():
-        if getattr(ertype, 'inlined', False):
-            ertype.inlined = False
-    return loader.finalize()
--- a/goa/doc/FAQ.en.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-==============================
-LAX Frequently Asked Questions
-==============================
-
-[WRITE ME]
\ No newline at end of file
--- a/goa/doc/README_LAX.fr.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-Qu'est-ce que ``LAX`` ?
-=======================
-
-``LAX`` (Logilab Application engine eXtension) est un framework 
-d'application web qui facilite les développements faits pour
-``Google AppEngine``.
-
-``LAX`` est un portage de la partie web de la plate-forme
-applicative développée par Logilab depuis 2001. Cette plate-forme 
-publie des données que la partie stockage tire de bases SQL, 
-d'annuaires LDAP et de systèmes de gestion de version. Depuis mai 
-2008, elle fonctionne sur le "datastore" de ``Google AppEngine``.
-
-``LAX`` est pour le moment en version alpha.
-
-Django/GAE vs. LAX/GAE
-=======================
-
-NotImplementedError()
-
-
-Téléchargement des sources
-==========================
-
-- Les sources de ``Google AppEngine`` peuvent être obtenues à l'adresse
-  suivante : http://code.google.com/appengine/downloads.html
-
-- Les sources de ``LAX`` se trouvent à l'adresse suivante :
-  http://lax.logilab.org/
-
-
-Installation
-============
-
-Les sources de ``Google AppEngine`` doivent être décompressées et le
-répertoire `google` qui s'y trouve doit être accessible par la variable
-d'environnement ``PYTHONPATH``. Correctement définir le ``PYTHONPATH`` 
-n'est pas nécessaire pour le lancement de l'application elle-même mais 
-pour l'utilisation des scripts fournis par ``LAX`` ou pour l'exécution 
-des tests unitaires.
-
-Une fois décompactée, l'archive ``lax-0.1.0-alpha.tar.gz``, on obtient
-l'arborescence suivante::
-  
-  .
-  |-- app.yaml
-  |-- custom.py
-  |-- data
-  |-- cubicweb/
-  |-- i18n/
-  |-- logilab/
-  |-- main.py
-  |-- mx/
-  |-- rql/
-  |-- schema.py
-  |-- simplejson/
-  |-- tools/
-  |   |-- generate_schema_img.py
-  |   `-- i18ncompile.py
-  |-- views.py
-  |-- yams/
-  `-- yapps/
-
-  
-On retrouve le squelette d'une application web de ``Google AppEngine``
-(fichiers ``app.yaml``, ``main.py``en particulier) avec les dépendances
-supplémentaires nécessaires à l'utilisation du framework ``LAX``
-
-
-Lancement de l'application de base
-==================================
-
-python /path/to/google_appengine/dev_appserver.py /path/to/lax
-
-
--- a/goa/doc/devmanual_fr/advanced_notes.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-
-La différence entre la classe `AppRsetObject` et la classe `AppObject` est que
-les instances de la premières sont séléctionnées pour une requête et un "result
-set" et alors que les secondes ne sont séléctionnées qu'en fonction de leur
-identifiant.
Binary file goa/doc/devmanual_fr/archi_globale.dia has changed
Binary file goa/doc/devmanual_fr/archi_globale.png has changed
--- a/goa/doc/devmanual_fr/chap_autres_composants_ui.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-Autres composants de l'interface web
-====================================
-
-Actions
--------
-XXXFILLME
-
-Component, VComponent
----------------------
-XXXFILLME
-
-CWProperty
----------
-XXXFILLME
--- a/goa/doc/devmanual_fr/chap_bases_framework_erudi.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,226 +0,0 @@
-Fondements du framework CubicWeb
-=============================
-
-Le moteur web d'cubicweb consiste en quelques classes gérant un ensemble d'objets
-chargés dynamiquement au lancement d'cubicweb. Ce sont ces objets dynamiques, issus
-du modèle ou de la librairie, qui construisent le site web final. Les différents
-composants dynamiques sont par exemple : 
-
-* coté client et serveur
-
- - les définitions d'entités, contenant la logique permettant la manipulation des
-   données de l'application
-
-* coté client
-
-  - les *vues* , ou encore plus spécifiquement 
-
-    - les boites
-    - l'en-tête et le pied de page
-    - les formulaires
-    - les gabarits de pages
-
-  - les *actions*
-  - les *controleurs*
-
-* coté serveur
-
-  - les crochets de notification
-  - les vues de notification
-
-Les différents composants du moteur sont :
-
-* un frontal web (seul twisted disponible pour le moment), transparent du point
-  de vue des objets dynamiques
-* un objet encapsulant la configuration
-* un `vregistry` (`cubicweb.cwvreg`) contenant les objets chargés dynamiquements
-
-
-Détail de la procédure d'enregistrement
----------------------------------------
-Au démarage le `vregistry` ou base de registres inspecte un certain nombre de
-répertoires à la recherche de définition de classes "compatible". Après une
-procédure d'enregistrement les objets sont affectés dans différents registres
-afin d'être ensuite séléctionné dynamiquement pendant le fonctionnement de
-l'application.
-
-La classe de base de tout ces objets est la classe `AppRsetObject` (module
-`cubicweb.common.appobject`). 
-
-
-API Python/RQL
---------------
-
-Inspiré de la db-api standard, avec un object Connection possédant les méthodes
-cursor, rollback et commit principalement. La méthode importante est la méthode
-`execute` du curseur :
-
-`execute(rqlstring, args=None, eid_key=None, build_descr=True)`
-
-:rqlstring: la requête rql à éxécuter (unicode)
-:args: si la requête contient des substitutions, un dictionnaire contenant les
-       valeurs à utiliser
-:eid_key: 
-   un détail d'implémentation du cache de requêtes RQL fait que si une substitution est
-   utilisée pour introduire un eid *levant des ambiguités dans la résolution de
-   type de la requête*, il faut spécifier par cet argument la clé correspondante
-   dans le dictionnaire
-
-C'est l'objet Connection qui possède les méthodes classiques `commit` et
-`rollback`. Vous ne *devriez jamais avoir à les utiliser* lors du développement
-d'interface web sur la base du framework CubicWeb étant donné que la fin de la
-transaction est déterminée par celui-ci en fonction du succès d'éxécution de la
-requête. 
-
-NOTE : lors de l'éxécution de requêtes de modification (SET,INSERT,DELETE), si une
-requête génère une erreur liée à la sécurité, un rollback est systématiquement
-effectuée sur la transaction courante.
-
-
-La classe `Request` (`cubicweb.web`)
----------------------------------
-Une instance de requête est créée lorsque une requête HTTP est transmise au
-serveur web. Elle contient des informations telles que les paramètres de
-formulaires, l'utilisateur connecté, etc. 
-
-**De manière plus générale une requête représente une demande d'un utilisateur,
-que se soit par HTTP ou non (on parle également de requête rql coté serveur par
-exemple)**
-
-Une instance de la classe `Request` possède les attributs :
-
-* `user`, instance de`cubicweb.common.utils.User` correspondant à l'utilisateur
-  connecté 
-* `form`, dictionaire contenant les valeurs de formulaire web
-* `encoding`, l'encodage de caractère à utiliser dans la réponse
-
-Mais encore :
-
-:Gestion des données de session:        
-  * `session_data()`, retourne un dictionaire contenant l'intégralité des
-    données de la session
-  * `get_session_data(key, default=None)`, retourne la valeur associée à
-    la clé ou la valeur `default` si la clé n'est pas définie
-  * `set_session_data(key, value)`, associe une valeur à une clé
-  * `del_session_data(key)`,  supprime la valeur associé à une clé
-    
-
-:Gestion de cookie:
-  * `get_cookie()`, retourne un dictionnaire contenant la valeur de l'entête
-    HTTP 'Cookie'
-  * `set_cookie(cookie, key, maxage=300)`, ajoute un en-tête HTTP `Set-Cookie`,
-    avec une durée de vie 5 minutes par défault (`maxage` = None donne un cooke
-    *de session"* expirant quand l'utilisateur ferme son navigateur
-  * `remove_cookie(cookie, key)`, fait expirer une valeur
-
-:Gestion d'URL:
-  * `url()`, retourne l'url complète de la requête HTTP
-  * `base_url()`, retourne l'url de la racine de l'application
-  * `relative_path()`, retourne chemin relatif de la requête
-
-:Et encore...:
-  * `set_content_type(content_type, filename=None)`, place l'en-tête HTTP
-    'Content-Type'
-  * `get_header(header)`, retourne la valeur associé à un en-tête HTTP
-    arbitraire de la requête
-  * `set_header(header, value)`, ajoute un en-tête HTTP arbitraire dans la
-    réponse 
-  * `cursor()` retourne un curseur RQL sur la session
-  * `execute(*args, **kwargs)`, raccourci vers .cursor().execute()
-  * `property_value(key)`, gestion des propriétés (`CWProperty`)
-  * le dictionaire `data` pour stocker des données pour partager de
-    l'information entre les composants *durant l'éxécution de la requête*.
-
-A noter que cette classe est en réalité abstraite et qu'une implémentation
-concrète sera fournie par le *frontend* web utilisé (en l'occurent *twisted*
-aujourd'hui). Enfin pour les vues ou autres qui sont éxécutés coté serveur,
-la majeure partie de l'interface de `Request` est définie sur la session
-associée au client. 
-
-
-La classe `AppObject`
----------------------
-
-En général :
-
-* on n'hérite pas directement des cette classe mais plutôt d'une classe
-  plus spécifique comme par exemple `AnyEntity`, `EntityView`, `AnyRsetView`,
-  `Action`...
-
-* pour être enregistrable, un classe fille doit définir son registre (attribut
-  `__registry__`) et son identifiant (attribut `id`). Généralement on n'a pas à
-  s'occuper du registre, uniquement de l'identifiant `id` :) 
-
-On trouve un certain nombre d'attributs et de méthodes définis dans cette classe
-et donc commune à tous les objets de l'application :
-
-A l'enregistrement, les attributs suivants sont ajoutés dynamiquement aux
-*classes* filles:
-
-* `vreg`, le `vregistry` de l'application
-* `schema`, le schéma de l'application
-* `config`, la configuration de l'application
-
-On trouve également sur les instances les attributs :
-
-* `req`, instance de `Request`
-* `rset`, le "result set" associé à l'objet le cas échéant
-* `cursor`, curseur rql sur la session
-
-
-:Gestion d'URL:
-  * `build_url(method=None, **kwargs)`, retourne une URL absolue construites à
-    partir des arguments donnés. Le *controleur* devant gérer la réponse
-    peut-être spécifié via l'argument spécial `method` (le branchement est
-    théoriquement bien effectué automatiquement :).
-
-  * `datadir_url()`, retourne l'url du répertoire de données de l'application
-    (contenant les fichiers statiques tels que les images, css, js...)
-
-  * `base_url()`, raccourci sur `req.base_url()`
-
-  * `url_quote(value)`, version *unicode safe* de de la fonction `urllib.quote`
-
-:Manipulation de données:
-
-  * `etype_rset(etype, size=1)`, raccourci vers `vreg.etype_rset()`
-
-  * `eid_rset(eid, rql=None, descr=True)`, retourne un objet result set pour
-    l'eid donné
-  * `entity(row, col=0)`, retourne l'entité correspondant à la position données
-    du "result set" associé à l'objet
-
-  * `complete_entity(row, col=0, skip_bytes=True)`, équivalent à `entity` mais
-    appelle également la méthode `complete()` sur l'entité avant de la retourner
-
-:Formattage de données:
-  * `format_date(date, date_format=None, time=False)`
-  * `format_time(time)`,
-
-:Et encore...:
-
-  * `external_resource(rid, default=_MARKER)`, accède à une valeur définie dans
-    le fichier de configuration `external_resource`
-    
-  * `tal_render(template, variables)`, 
-
-
-**NOTE IMPORTANTE**
-Lorsqu'on hérite d'`AppObject` (même indirectement), il faut **toujours**
-utiliser **super()** pour récupérer les méthodes et attributs des classes
-parentes, et pas passer par l'identifiant de classe parente directement.
-(sous peine de tomber sur des bugs bizarres lors du rechargement automatique
-des vues). Par exemple, plutôt que d'écrire::
-
-      class Truc(PrimaryView):
-          def f(self, arg1):
-              PrimaryView.f(self, arg1)
-
-Il faut écrire::
-      
-      class Truc(PrimaryView):
-          def f(self, arg1):
-              super(Truc, self).f(arg1)
-
-
-XXX FILLME diagramme interaction application/controller/template/view
--- a/goa/doc/devmanual_fr/chap_configuration_instance.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-Configuration d'une instance
-============================
-
-À la création d'une instance, un fichier de configuration est généré dans ::
-
-   $(CW_REGISTRY)/<instance>/<nom configuration>.conf
-
-par exemple ::
-
-   /etc/cubicweb.d/jpl/all-in-one.conf
-
-C'est un simple fichier texte au format INI. Dans la description suivante,
-chaque nom d'option est préfixé de sa section et suivi de sa valeur par défaut
-le cas échéant, e.g. "`<section>.<option>` [valeur]".
-
-
-Configuration du serveur web
-----------------------------
-:`web.auth-mode` [cookie]: 
-   mode d'authentification, cookie ou http
-:`web.realm`: 
-   realm de l'application en mode d'authentification http
-:`web.http-session-time` [0]:
-   délai d'inactivité d'une session HTTP avant sa fermeture automatique. Durée
-   en secondes, 0 signifiant pas d'expiration (ou plus exactement lors de la
-   fermeture du navigateur du client)
-
-:`main.anonymous-user`, `main.anonymous-password`:
-   login et mot de passe à utiliser pour se connecter au serveur RQL lors des
-   connexions HTTP anonymes. Il faut que le compte CWUser associé existe.
-
-:`main.base-url`:
-   url de base du site, à utiliser pour générer les urls des pages web
-
-Configuration https
-```````````````````
-Il est possible de rendre un site accessible en http pour les connections 
-anonymes et en https pour les utilisateurs authentifié. Il faut pour cela
-utiliser apache (par ex.) pour la redirection et la variable `main.https-url` du
-fichier de configuration.
-
-:Exemple:
-
-  pour une redirection apache d'un site accessible via `http://localhost/demo`
-  et `https://localhost/demo` et qui tourne en réalité sur le port 8080, il 
-  faut avoir pour la version http : ::
-
-    RewriteCond %{REQUEST_URI} ^/demo
-    RewriteRule ^/demo$ /demo/
-    RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P]
-  
-  et pour la version https : ::
-
-    RewriteCond %{REQUEST_URI} ^/demo
-    RewriteRule ^/demo$ /demo/
-    RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]
-
-
-  et on aura dans le fichier all-in-one.conf de l'instance : ::
-
-    base-url = http://localhost/demo
-    https-url = `https://localhost/demo`
-
-Configuration de l'interface web
---------------------------------
-:`web.embed-allowed`:
-   expression régulière correspondant aux sites pouvant être "incorporé" dans
-   le site (controleur 'embed')
-:`web.submit-url`:
-   url à laquelle les bugs rencontrés dans l'application peuvent être posté
-
-
-Configuration du serveur RQL
-----------------------------
-:`main.host`:
-   nom de l'hôte s'il ne peut être détecter correctement
-:`main.pid-file`:
-   fichier où sera écrit le pid du serveur
-:`main.uid`:
-   compte utilisateur à utiliser pour le lancement du serveur quand il est
-   lancé en root par init
-:`main.session-time [30*60]`:
-   temps d'expiration d'une session RQL
-:`main.query-log-file`:
-   fichier dans lequel écrire toutes les requêtes RQL éxécutées par le serveur
-
-
-Configuration Pyro pour l'instance
------------------------------------
-Coté serveur web :
-
-:`pyro-client.pyro-application-id`: 
-   identifiant pyro du serveur RQL (e.g. le nom de l'instance)
-
-Coté serveur RQL :
-
-:`pyro-server.pyro-port`:
-   numéro de port pyro. Si aucune valeur n'est spécifiée, un port est attribué
-   automatiquement.
-
-Coté serveur RQL et serveur web :
-
-:`pyro-name-server.pyro-ns-host`:
-   nom de l'hôte hébergeant le serveur de nom pyro. Si aucune valeur n'est
-   spécifié, il est localisé par une requête de broadcast
-:`pyro-name-server.pyro-ns-group` [cubicweb]:
-   groupe pyro sous lequel enregistrer l'application
-
-
-Configuration courriel
-----------------------
-Coté serveur RQL et serveur web :
-
-:`email.mangle-emails [no]`:
-   indique si les adresses email doivent être affichées telle quelle ou
-   transformée
-
-Coté serveur RQL :
-
-:`email.smtp-host [mail]`:
-   nom de l'hôte hébergeant le serveur SMTP à utiliser pour le courriel sortant
-:`email.smtp-port [25]`:
-   port du serveur SMTP à utiliser pour le courriel sortant
-:`email.sender-name`:
-   nom à utiliser pour les courriels sortant de l'application
-:`email.sender-addr`:
-   adresse à utiliser pour les courriels sortant de l'application
-:`email.default-dest-addrs`:
-   adresses de destination par défaut, si utilisé par la configuration de la 
-   diffusion du modèle (séparées par des virgules)
-:`email.supervising-addrs`:
-   addresses de destination des courriels de supervision (séparées par des 
-   virgules)
-
-
-Configuration journalisation
-----------------------------
-:`main.log-threshold`:
-   niveau de filtrage des messages (DEBUG, INFO, WARNING, ERROR)
-:`main.log-file`:
-   fichier dans lequel écrire les messages
-
-
-Configuration Eproperties
--------------------------
-D'autres paramètres de configuration sont sous la forme d'entités `CWProperty`
-dans la base de données. Il faut donc les éditer via l'interface web ou par des
-requêtes rql.
-
-:`ui.encoding`:
-   encodage de caractères à utiliser pour l'interface web
-:`navigation.short-line-size`: # XXX should be in ui
-   nombre de caractères maximum pour les affichages "courts"
-:`navigation.page-size`:
-   nombre d'entités maximum à afficher par page de résultat
-:`navigation.related-limit`:
-   nombre d'entités liées maximum à afficher sur la vue primaire d'une entité
-:`navigation.combobox-limit`:
-   nombre d'entités non liées maximum à afficher sur les listes déroulantes de
-   la vue d'édition d'une entité
--- a/goa/doc/devmanual_fr/chap_definition_schema.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-Définition du modèle de données (*schéma*)
-==========================================
-
-Le schéma est l'élément central d'une application d'CubicWeb, définissant le modèle
-de données manipulé. Il est généralement défini à partir de type d'entités
-existants dans la librairie et d'autres spécifiques, généralement décrites dans
-un ou plusieurs fichiers python dans le sous-répertoire `schema` du modèle.
-
-A ce niveau il est important de noter la différence entre type de relation et
-définition de relation : un type de relation est uniquement un nom de relation
-avec éventuellement quelques propriétés supplémentaires (voir plus bas), alors
-qu'une définition de relation est un triplet complet "<type d'entité sujet>
-<type de relation> <type d'entité objet>". Eventuellement un type de relation
-sera créé implicitement si aucun n'est associé à une définition de relation du
-schema.
-
-.. include:: sect_stdlib_schemas.txt
-.. include:: sect_definition_schema.txt
-
--- a/goa/doc/devmanual_fr/chap_definition_workflows.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-Définition de workflow
-======================
-On peut mettre une condition rql ou/et un groupe auquel doit appartenir l'utilisateur.
-
-Si on met à la fois un(ou plusieurs) groupe et une condition RQL, il faut que les deux soient respectés.
-
-Si on met plusieurs groupes, il faut que l'utilisateur soit dans un des groupes.
-
-Pour la condition RQL sur une transition, on peut y mettre les substitutions suivantes :
-
-* `%(eid)s`, eid de l'objet
-* `%(ueid)s`, eid de l'utilisateur qui fait la requête
-* `%(seid)s`, eid de l'état courant de l'objet
-
-Dans le script de création d'un workflow, penser à mettre `_()` autour des noms d'états et de transitions
-pour que ceux si soient pris en compte par les scripts de gestion des catalogues i18n.
\ No newline at end of file
--- a/goa/doc/devmanual_fr/chap_fondements_erudi.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-Fondements CubicWeb
-================
-
-Concepts et vocabulaire
------------------------
-
-*schéma*
-  le schéma définit le modèle de données d'une application sous forme d'entités
-  et de relations, grâce au package `yams`_. C'est l'élément central d'une
-  application. Il est initialement défini sur le système de fichiers et est
-  stocké dans la base de données lors de la création d'une instance. CubicWeb 
-  fournit un certain nombres de types d'entités inclus systématiquement 
-  car nécessaire au noyau CubicWeb et une librairie de composants devant être
-  inclus explicitement le cas échéant.
-
-*source*
-  une source de données est un conteneur de données quelquonque (SGBD, annuaire
-  LDAP...) intégré par l'entrepôt CubicWeb. Un entrepôt possède au moins une source
-  dite "system" contenant le schéma de l'application, l'index plein-texte et
-  d'autres informations vitales au système.
-
-*composant*
-  un composant est un modèle regroupant un ou plusieurs types de données et/ou
-  des vues afin de fournir une fonctionalité précise, ou une application CubicWeb
-  complète utilisant éventuellement d'autres composants. Les différents
-  composants disponibles sur une machine sont installés dans
-  `/usr/share/cubicweb/templates`
-
-*result set*
-  objet encaspulant les résultats d'une requête RQL et des informations sur
-  cette requête.
-
-.. _`Python Remote Object`: http://pyro.sourceforge.net/
-.. _`yams`: http://www.logilab.org/project/name/yams/
-
-
-Structure générale d'une application LAX
-----------------------------------------
-
-Un composant complexe est structuré selon le modèle suivant :
-
-::
-    
-  .
-  |-- app.yaml
-  |-- custom.py
-  |-- data
-  |-- cubicweb/
-  |-- i18n/
-  |-- logilab/
-  |-- main.py
-  |-- mx/
-  |-- rql/
-  |-- schema.py
-  |-- simplejson/
-  |-- tools/
-  |   |-- generate_schema_img.py
-  |   `-- i18ncompile.py
-  |-- views.py
-  |-- yams/
-  `-- yapps/
-        
-
-où :
-
-* ``schema.py`` contient la définition du schéma
-* ``views.py`` contient les définitions des vues
-* ``i18n`` contient les catalogues de messages pour les langues supportées (coté
-  serveur et interface web) 
-* ``data`` contient des fichiers de données arbitraires servis statiquement
-  (images, css, fichiers javascripts)... (coté interface web uniquement)
--- a/goa/doc/devmanual_fr/chap_i18n.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-Internationalisation
-====================
-
-Le système d'internationalisation de l'interface web d'cubicweb est basé sur le
-système `GNU gettext`_.
-
-.. _`GNU gettext`: http://www.gnu.org/software/gettext/
-
-Messages à internationaliser
-----------------------------
-
-Marquage des messages à internaliser
-````````````````````````````````````
-Les chaines de caractères à internationaliser sont marqués par l'appel à la
-fonction `_` *OU* par la méthode équivalent de la requête dans le code python ou
-dans les expressions python de template TAL. 
-
-Dans les templates cubicweb-tal, il est également possible d'insérer une chaine à
-traduire via les balises `i18n:content` et  `i18n:replace`.
-
-De plus des messages correspondant aux entités/relations utilisés par le schéma
-de l'application seront automatiquement ajoutés.
-
-Renvoi d'un message internationalisé lors de la construction d'une page
-```````````````````````````````````````````````````````````````````````
-La fonction *built-in* `_` ne doit servir qu'**à marquer les messages à
-traduire**, non pas à récupérer une traduction. Il faut pour cela utiliser la
-méthode `_` de l'objet requête, sans quoi vous récupérerez l'identifiant de
-message au lieu de sa traduction dans la langue propre à la requête.1
-
-
-Gestion des catalogues de traduction
-------------------------------------
-Une fois l'application rendu internationalisable coté code, reste à gérer les
-catalogues de traductions. cubicweb-ctl intègre pour cela les commandes suivantes : 
-
-* `i18ncubicweb`, met à jour les catalogues de messages *de la librairie
-  cubicweb*. Sauf si vous développez sur le framework (et non votre propre
-  application), vous ne devriez pas avoir à utiliser cette commande
-
-* `i18ncube`, met à jour les catalogues de messages *du composant* (ou de tous
-  les composants). A la suite de cette commande, vous devez mettre à jour les
-  fichiers de traduction *.po* dans le sous-répertoire "i18n" de votre
-  template. Évidemment les traductions précédentes toujours utilisées ont été
-  conservées.
-
-* `i18ninstance`, recompile les catalogues de messages *d'une instance* (ou de
-  toutes les instances) après mise à jour des catalogues de son composant. Cela
-  est effectué automatiquement lors d'une création ou d'une mise à jour. Les
-  catalogues de messages compilés se trouvent dans le répertoire
-  "i18n/<lang>/LC_MESSAGES/cubicweb.mo" de l'application où `lang` est
-  l'identifiant de la langue sur 2 lettres ('en' ou 'fr' par exemple)
-
-
-Le cas classique
-````````````````
-Vous avez ajouté et/ou modifié des messages d'un composant utilisé par votre
-application (en ajoutant une nouvelle vue ou en ayant modifié le schéma par
-exemple) :
-
-1. `cubicweb-ctl i18ncube <composant>`
-2. éditer les fichiers <composant>/xxx.po dans pour y rajouter les traductions
-   manquantes (`msgstr` vide) 
-3. `hg ci -m "updated i18n catalogs"`
-4. `cubicweb-ctl i18ninstance <monapplication>`
-
--- a/goa/doc/devmanual_fr/chap_manipulation_donnees.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-Manipulation des données stockées
-=================================
-
-Les classes `Entity` et `AnyEntity`
------------------------------------
-Pour fournir un comportement spécifique à un type d'entité, il suffit de définir
-une classe héritant de la class `cubicweb.entities.AnyEntity`. En général il faut
-définir ces classes dans un module du package `entities` d'une application pour 
-qu'elle soit disponible à la fois coté serveur et coté client.
-
-La classe `AnyEntity` est une classe chargée dynamiquement héritant de la classe
-de base `Entity` (`cubicweb.common.entity`). On définit une sous-classe pour
-ajouter des méthodes ou spécialiser les comportements d'un type d'entité donné.
-
-Des descripteurs sont ajoutés à l'enregistrement pour initialiser la classe en
-fonction du schéma :
-
-* on peut accéder aux attributs définis dans le schéma via les attributs de même
-  nom sur les instances (valeur typée)
-
-* on peut accéder aux relations définies dans le schéma via les attributs de même
-  nom sur les instances (liste d'instances d'entité)
-
-Les méthodes définies sur la classe `AnyEntity` ou `Entity` sont les suivantes :
-
-* `has_eid()`, retourne vrai si l'entité à un eid affecté (i.e. pas en cours de
-  création) 
-        
-* `check_perm(action)`, vérifie que l'utilisateur à le droit d'effectuer
-  l'action demandée sur l'entité
-
-:Formattage et génération de la sortie:
-
-  * `view(vid, **kwargs)`, applique la vue donnée à l'entité
-
-  * `absolute_url(**kwargs)`, retourne une URL absolue permettant d'accéder à la
-    vue primaire d'une entité
-
-  * `format(attr)`, retourne le format (type MIME) du champ passé en argument
-
-  * `printable_value(attr, value=_marker, attrtype=None, format='text/html')`, 
-    retourne une chaine permettant l'affichage dans un format donné de la valeur
-    d'un attribut (la valeur est automatiquement récupérée au besoin)
-
-  * `display_name(form='')`, retourne une chaîne pour afficher le type de
-    l'entité, en spécifiant éventuellement la forme désirée ('plural' pour la
-    forme plurielle) 
-
-:Gestion de données:
-
-  * `complete(skip_bytes=True)`, effectue une requête permettant de récupérer d'un
-    coup toutes les valeurs d'attributs manquant sur l'entité
-
-  * `get_value(name)`, récupere la valeur associée à l'attribut passé en argument
-
-  * `related(rtype, x='subject', limit=None, entities=False)`, retourne une liste
-    des entités liées à l'entité courant par la relation donnée en argument
-
-  * `unrelated(rtype, targettype, x='subject', limit=None)`, retourne un result set
-    des entités not liées à l'entité courante par la relation donnée en argument
-    et satisfaisants les contraintes de celle-ci
-
-  * `copy_relations(ceid)`, copie les relations de l'entité ayant l'eid passé en
-    argument sur l'entité courante
-
-  * `last_modified(view)`, retourne la date à laquelle on doit considérer
-    l'objet comme modifié (utiliser par la gestion de cache HTTP)
-
-:Meta-données standard (Dublin Core):
-
-  * `dc_title()`, retourne une chaine unicode correspondant à la méta-donnée
-    'Title' (utilise par défaut le premier attribut non 'meta' du schéma de
-    l'entité) 
-
-  * `dc_long_title()`, comme dc_title mais peut retourner un titre plus détaillé
-
-  * `dc_description(format='text/plain')`, retourne une chaine unicode
-     correspondant à la méta-donnée 'Description' (cherche un attribut
-     'description' par défaut)
-
-  * `dc_authors()`, retourne une chaine unicode correspondant à la méta-donnée
-    'Authors' (propriétaires par défaut)
-
-  * `dc_date(date_format=None)`, retourne une chaine unicode
-     correspondant à la méta-donnée 'Date' (date de modification par défaut)
-            
-:Contrôle du vocabulaire pour les relations:
-  * `vocabulary(rtype, x='subject', limit=None)`
-  * `subject_relation_vocabulary(rtype, limit=None)`
-  * `object_relation_vocabulary(rtype, limit=None)`
-  * `relation_vocabulary(rtype, targettype, x, limit=None)`
-
-
-Les *rtags*
------------
-Les *rtags* permettent de spécifier certains comportements propres aux relations
-d'un type d'entité donné (voir plus loin). Ils sont définis sur la classe 
-d'entité via l'attribut `rtags` qui est un dictionnaire dont les clés sont un 
-triplet ::
-
-  <type de relation>, <type d'entité cible>, <position du contexte ("subject" ou "object"
-
-et les valeurs un `set` ou un tuple de marqueurs définissant des propriétés 
-s'appliquant à cette relation. 
-
-Il est possible de simplifier ce dictionnaire :
-
-* si l'on veut spécifier un seul marqueur, il n'est pas nécessaire d'utiliser
-  un tuple comme valeur, le marqueur seul (chaine de caractères) suffit
-* si l'on s'intéresse uniquement à un type de relation et non à la cible et à la
-  position du contexte (ou que celui-ci n'est pas ambigüe), on peut simplement
-  utiliser le nom du type de relation comme clé
-* si l'on veut qu'un marqueur s'applique quelque soit le type d'entité cible, il
-  faut utiliser la chaine `*` comme type d'entité cible
-
-A noter également que ce dictionnaire est *traité à la création de la classe*. 
-Il est automatiquement fusionné avec celui de la ou des classe(s) parentes (pas
-besoin de copier celui de la classe parent pour le modifier). De même modifier
-celui-ci après création de la classe n'aura aucun effet...
-
-
-.. include:: sect_definition_entites.txt
--- a/goa/doc/devmanual_fr/chap_migration.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-Migration
-=========
-
-Une des idées de base d'CubicWeb est la création incrémentale d'application, et
-pour cela de nombreuses actions sont fournies afin de facilement faire évoluer
-une application et tout particulièrement le modèle de données manipulé sans
-perdre les données des instances existantes.
-
-La version courante d'un modèle d'application est données dans le fichier
-`__pkginfo__.py` sous forme d'un tuple de 3 entiers.
-
-
-Gestion des scripts de migrations
----------------------------------
-Les scripts des migrations doivent être placés dans le répertoire `migration` de
-l'application, et nommé de la manière suivante :
-
-  <n° de version X.Y.Z>[_<description>]_<mode>.py
-
-dans lequel : 
-
-* X.Y.Z correspond au n° de version du modèle vers lequel le script permet de
-  migrer,
-
-* le *mode* (entre le dernier "_" et l'extension ".py") indique à quelle partie
-  de l'application (serveur RQL, serveur web) le script s'applique en cas
-  d'installation distribuée. Il peut valoir : 
-
-  * `common`, s'applique aussi bien sur le serveur RQL que sur le serveur web,
-    et met à jour des fichiers sur le disque (migration de fichier de
-    configuration par exemple).
-
-  * `web`, s'applique uniquement sur le serveur web, et met à jour des fichiers
-    sur le disque 
-
-  * `repository`, s'applique uniquement sur le serveur RQL, et met à jour des
-    fichiers sur le disque 
-
-  * `Any`, s'applique uniquement sur le serveur RQL, et met à jour des
-    données en base (migrations de schéma et de données par ex.)
-
-
-Toujours dans le répertoire `migration`, le fichier spécial `depends.map` permet
-d'indiquer que pour migrer vers une version spécifique du modèle, il faut tout
-d'abord avoir migrer vers une version données de cubicweb. Ce fichier peut contenir
-des commentaires (lignes commençant par un "#"), et une dépendance est notée sur
-une ligne de la manière suivante : ::
-
-  <n° de version du modèle X.Y.Z> : <n° de version cubicweb X.Y.Z>
-
-Par exemple ::
-
-  0.12.0: 2.26.0
-  0.13.0: 2.27.0
-  # 0.14 works with 2.27 <= cubicweb <= 2.28 at least
-  0.15.0: 2.28.0
-
-
-Contexte de base
-----------------
-Les identifiants suivants sont préféfinis dans les scripts de migration : 
-
-* `config`, configuration de l'instance
-
-* `interactive_mode`, booléen indiquant si le script est éxécuté en mode
-  interactif ou non
-
-* `appltemplversion`, version du modèle d'application de l'instance
-
-* `applcubicwebversion`, version cubicweb de l'instance
-
-* `templversion`, version du modéle d'application installée
-
-* `cubicwebversion`, version cubicweb installée
-
-* `confirm(question)`, fonction posant une question et retournant vrai si
-  l'utilisateur a répondu oui, faux sinon (retourne toujours vrai en mode non
-  interactif) 
-
-* `_`, fonction équivalente à `unicode` permettant de marquer des chaines à
-  internationaliser dans les scripts de migration
-
-Dans les scripts "repository", les identifiants suivant sont également définis :
-
-* `checkpoint`, demande confirmant et effectue un "commit" au point d'appel
-
-* `repo_schema`, schéma persistent de l'instance (i.e. schéma de l'instance en
-  cours de migration)
-
-* `newschema`, schéma installé sur le système de fichier (i.e. schéma de la
-  version à jour du modèle et de cubicweb)
-
-* `sqlcursor`, un curseur SQL pour les très rares cas où il est réellement
-  nécessaire ou avantageux de passer par du sql
-
-* `repo`, l'objet repository
-
-                        
-Migration de schéma
--------------------
-Les fonctions de migration de schéma suivantes sont disponibles dans les scripts
-"repository" : 
-
-* `add_attribute(etype, attrname, attrtype=None, commit=True)`, ajoute un
-  nouvel attribut à un type d'entité existante. Si le type de celui-ci n'est pas
-  spécifié il est extrait du schéma à jour.
-        
-* `drop_attribute(etype, attrname, commit=True)`, supprime un
-  attribut à un type d'entité existante.
-
-* `rename_attribute(etype, oldname, newname, commit=True)`, renomme un attribut
-            
-* `add_entity_type(etype, auto=True, commit=True)`, ajoute un nouveau type
-  d'entité. Si `auto` est vrai, toutes les relations utilisant ce type d'entité
-  et ayant un type d'entité connu à l'autre extrémité vont également être
-  ajoutées.
-
-* `drop_entity_type(etype, commit=True)`, supprime un type d'entité et toutes
-  les relations l'utilisant.
-
-* `rename_entity_type(oldname, newname, commit=True)`, renomme un type d'entité
-            
-* `add_relation_type(rtype, addrdef=True, commit=True)`, ajoute un nouveau type
-  de relation. Si `addrdef` est vrai, toutes les définitions de relation de ce
-  type seront également ajoutées.
-
-* `drop_relation_type(rtype, commit=True)`, supprime un type de relation et
-  toutes les définitions de ce type.
-
-* `rename_relation(oldname, newname, commit=True)`, renomme une relation.
-
-* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, ajoute une
-  définition de relation.
-
-* `drop_relation_definition(subjtype, rtype, objtype, commit=True)`, supprime
-  une définition de relation.
-
-* `synchronize_permissions(ertype, commit=True)`, synchronise les permissions
-  d'un type d'entité ou de relation
-        
-* `synchronize_rschema(rtype, commit=True)`, synchronise les propriétés et
-  permissions d'un type de relation.
-                
-* `synchronize_eschema(etype, commit=True)`, synchronise les propriétés et
-  permissions d'un type d'entité.
-    
-* `synchronize_schema(commit=True)`, synchronise le schéma persistent avec le
-  schéma à jour (mais sans ajouter ni supprimer de nouveaux types d'entités ou
-  de relations ni de définitions de relation).
-        
-* `change_relation_props(subjtype, rtype, objtype, commit=True, **kwargs)`, change
-  les propriétés d'une definition de relation en utilisant les arguments nommés
-  pour les propriétés à changer.
-
-* `set_widget(etype, rtype, widget, commit=True)`, change le widget à utiliser
-  pour la relation <rtype> du type d'entité <etype>
-
-* `set_size_constraint(etype, rtype, size, commit=True)`, change la contrainte
-  de taille pour la relation <rtype> du type d'entité <etype>
-
-
-Migration de données
---------------------
-Les fonctions de migration de données suivantes sont disponibles dans les scripts
-"repository" : 
-
-* `rqlexec(rql, kwargs=None, cachekey=None, ask_confirm=True)`, éxécute une
-  requête rql arbitraire, d'interrogation ou de modification. Un objet result
-  set est retourné.
-
-* `rqlexecall(rqliter, cachekey=None, ask_confirm=True)`, éxécute une série
-  de requêtes rql arbitraires, d'interrogation ou de modification. rqliter est
-  un itérateur retournant des couples (rql, kwargs). Le result set de la
-  dernière requête éxécutée est retourné.
-
-* `add_entity(etype, *args, **kwargs)`, ajoute une nouvelle entité du type
-  données. La valeur des attributs et relations est spécifiée en utilisant les
-  arguments nommés et positionnels.
-
-  
-Création de workflow
---------------------
-Les fonctions de création de workflow suivantes sont disponibles dans les scripts
-"repository" : 
-
-* `add_state(name, stateof, initial=False, commit=False, **kwargs)`, ajoute un
-  nouvel état de workflow
-    
-* `add_transition(name, transitionof, fromstates, tostate, requiredgroups=(), commit=False, **kwargs)`, 
-  ajoute une nouvelle transtion de workflow
-
-Migration de configuration
---------------------------
-Les fonctions de migration de configuration suivantes sont disponibles dans tout
-les scripts : 
-
-* `option_renamed(oldname, newname)`, indique qu'une option a été renommée
-
-* `option_group_change(option, oldgroup, newgroup)`, indique qu'une option a
-  changé de groupe
-
-* `option_added(oldname, newname)`, indique qu'une option a été ajoutée
-
-* `option_removed(oldname, newname)`, indique qu'une option a été supprimée
-
-
-Autres fonctions de migration
------------------------------
-Ces fonctions ne sont utilisés que pour des opérations de bas niveau
-irréalisables autrement ou pour réparer des bases cassées lors de session
-interactive. Elles sont disponibles dans les scripts "repository".
-
-* `sqlexec(sql, args=None, ask_confirm=True)`, éxécute une requête sql
-  arbitraire, à n'utiliser 
-
-* `add_entity_type_table(etype, commit=True)`
-* `add_relation_type_table(rtype, commit=True)`
-* `uninline_relation(rtype, commit=True)`
--- a/goa/doc/devmanual_fr/chap_mise_en_place_environnement.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-Mise en place d'un environnement de développement CubicWeb
-=======================================================
-
-.. include:: sect_mercurial.txt
-.. include:: sect_installation.txt
-.. include:: sect_cubicweb-ctl.txt
-
-
-
-Création d'un composant
------------------------
-Commençons par créer un squelette qui nous servira de base au développement de
-notre composant ou application ::
-
-  cd ~/hg
-  cubicweb-ctl newtemplate moncomposant
-  # répondre aux questions
-  hg init moncomposant
-  cd moncomposant
-  hg add .
-  hg ci
-
-A partir de là si tout va bien, votre composant devrait être affiché par
-`cubicweb-ctl list` dans la section *Avaible components*, si ce n'est pas le cas
-revoir la section `Configuration de l'environnement`_.
-
-
-Création d'une instance de développement
-----------------------------------------
-
-Maintenant que nous avons notre squelette de modèle, on peut en créer une
-instance afin de voir ce que tout ça donne dans un simple navigateur web.
-Nous allons utiliser une configuration `all-in-one` afin de simplifier les
-choses ::
-
-  cubicweb-ctl create all-in-one moncomposant moninstance
-
-Une série de questions vont être posées, la réponse par défaut est généralement
-suffisante. Vous pourrez de toute façon modifier la configuration par la suite
-en éditant les fichiers générés. Lorsqu'un login/mot de passe d'accès au sgbd
-vous est demandé, il est recommandé d'utilisé l'utilisateur créé lors de la
-`Configuration Postgres`_.
-
-Il est important de distinguer ici l'utilisateur utilisé pour accéder au sgbd,
-et l'utilisateur utilisé pour s'authentifier dans l'application cubicweb. Lorsque
-l'application cubicweb démarre, elle utilise le login/mot de passe sgdb pour
-récupérer le schéma et gérer les transactions bas-niveau. En revanche, lorsque
-`cubicweb-ctl create` vous demande un login/mot de passe `manager` pour cubicweb, il
-s'agit d'un utilisateur qui sera créé dans l'application `cubicweb` pour pouvoir
-s'y connecter dans un premier temps et l'administrer. Il sera par la suite possible
-de créer des utilisateurs différents pour l'application.
-
-A l'issue de cette commande, la définition de votre instance se trouve dans
-*~/etc/cubicweb.d/moninstance/*. Pour la lancer, il suffit de taper ::
-
-  cubicweb-ctl start -D moninstance
-
-L'option `-D` indique le *debug mode* : l'instance ne passe pas en mode serveur
-et ne se déconnecte pas du terminal, ce qui simplifie le dépannage en cas de non
-démarrage de l'instance. Vous pouvez ensuite allez voir ce que ça donne en
-pointant votre navigateur sur l'url `http://localhost:8080` (le n° de port
-dépend de votre configuration). Pour vous authentifier vous pouvez utiliser le
-login/mot de passe administrateur que vous avez spécifié lors de la création de
-l'instance.
-
-Pour arrêter l'instance, un Ctrl-C dans la fenêtre où vous l'avez lancé
-suffit. Si l'option `-D` a été omise, il faut taper ::
-
-  cubicweb-ctl stop moninstance
-
-Voilà, tout est en place pour démarrer le développement du modèle...
-
-
-Utilisation de cubicweb-liveserver
--------------------------------
-
-Afin de tester rapidement un nouveau composant, on peut également
-utiliser le script `cubicweb-liveserver` qui permet de créer une
-application en mémoire (utilisant une base de données SQLite par
-défaut) et la rendre accessible via un serveur web::
-
-  cubicweb-liveserver moncomposant
-
-ou bien, pour utiliser une base de données existante (SQLite ou postgres)::
-
-  cubicweb-liveserver -s monfichier_sources moncomposant
-
--- a/goa/doc/devmanual_fr/chap_rql.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-Le langage RQL (Relation Query Language)
-========================================
-
-Présentation
-------------
-* langage mettant l'accent sur le parcours de relations.
-* Les attributs sont considérés comme des cas particuliers de relations.
-* RQL s'inspire de SQL mais se veut plus haut niveau.
-* Une connaissance du schéma CubicWeb définissant l'application est nécessaire.
-
-
-Les différents types de requêtes
---------------------------------
-Recherche (`Any`)
-  interroger l'entrepôt afin d'extraire des entités et/ou des attributs
-  d'entités.
-
-Insertion (`INSERT`)
-  insérer de nouvelles entités dans la base.
-
-Mise à jour d'entités, création de relations (`SET`)
-  mettre à jours des entités existantes dans la base, ou de créer des
-  relations entres des entités existantes.
-
-Suppression d'entités ou de relation (`DELETE`)
-  supprimer des entités et relations existantes dans la base.
-
-
-Variables et typage
--------------------
-Les entités et valeurs à parcourir et / ou séléctionner sont représentées dans
-la requête par des *variables* qui doivent être écrites en majuscule.
-
-Les types possibles pour chaque variable sont déduits à partir du schéma en
-fonction des contraintes présentes dans la requête.
-
-On peut contraindre les types possibles pour une variable à l'aide de la
-relation spéciale `is`.
-
-Types de bases
---------------
-* `String` (litéral: entre doubles ou simples quotes).
-* `Int`, `Float` (le séparateur étant le '.').
-* `Date`, `Datetime`, `Time` (litéral: chaîne YYYY/MM/DD[ hh:mm] ou mots-clés
-  `TODAY` et `NOW`).
-* `Boolean` (mots-clés `TRUE` et `FALSE`).
-* mot-clé `NULL`.
-
-Opérateurs
-----------
-* Opérateurs logiques : `AND`, `OR`, `,`.
-* Opérateurs mathématiques: `+`, `-`, `*`, `/`.
-* Operateur de comparaisons: `=`, `<`, `<=`, `>=`, `>`, `~=`, `LIKE`, `IN`.
-
-  * L'opérateur `=` est l'opérateur par défaut.
-
-  * L'opérateur `LIKE` / `~=` permet d'utiliser le caractère `%` dans une chaine
-    de caractère pour indiquer que la chaîne doit commencer ou terminer par un
-    préfix/suffixe::
-    
-      Any X WHERE X nom ~= 'Th%'
-      Any X WHERE X nom LIKE '%lt'
-
-  * L'opérateur `IN` permet de donner une liste de valeurs possibles::
-
-      Any X WHERE X nom IN ('chauvat', 'fayolle', 'di mascio', 'thenault')
-
-Requête de recherche
---------------------
-
-  [`DISTINCT`] <type d'entité> V1(, V2)\*
-  [`GROUPBY` V1(, V2)\*]  [`ORDERBY` <orderterms>]
-  [`WHERE` <restriction>] 
-  [`LIMIT` <value>] [`OFFSET` <value>]
-
-:type d'entité:
-  Type de la ou des variables séléctionnées. 
-  Le type spécial `Any`, revient à ne pas spécifier de type.
-:restriction:
-  liste des relations à parcourir sous la forme 
-    `V1 relation V2|<valeur constante>`
-:orderterms:
-  Définition de l'ordre de selection : variable ou n° de colonne suivie de la
-  méthode de tri (`ASC`, `DESC`), ASC étant la valeur par défaut.
-:note pour les requêtes groupées:
-  Pour les requêtes groupées (i.e. avec une clause `GROUPBY`), toutes les
-  variables sélectionnée doivent être soit groupée soit aggrégée.
-
-Exemples - recherche
-`````````````````````
-::
-
-      Any X WHERE X eid 53
-      Personne X
-      Personne X WHERE X travaille_pour S, S nom "logilab"
-      Any E,COUNT(X) GROUPBY E ORDERBY EN WHERE X is E, E name EN 
-      Any E,COUNT(X) GROUPBY E ORDERBY 2 WHERE X is E 
-
-
-Fonctionnalités avancées
-````````````````````````
-* Fonctions d'aggrégat : `COUNT`, `MIN`, `MAX`, `SUM`.
-* Fonctions sur les chaines :`UPPER`, `LOWER`.
-* Relations optionnelles :
-
-  * Elles permettent de sélectionner des entités liées ou non à une autre.
-
-  * Il faut utiliser le `?` derrière la variable pour spécifier que la relation
-    vers celle-ci est optionnelle :
-
-    - Anomalies d'un projet attachées ou non à une version ::
-
-        Any X,V WHERE X concerns P, P eid 42, X corrected_in V?
-
-    - Toutes les fiches et le projet qu'elles documentent le cas échéant ::
-
-        Any C,P WHERE C is Card, P? documented_by C
-
-Négation
-````````
-* Une requête du type `Document X WHERE NOT X owned_by U` revient à dire "les
-  documents n'ayant pas de relation `owned_by`". 
-* En revanche la requête `Document X WHERE NOT X owned_by U, U login "syt"`
-  revient à dire "les  documents n'ayant pas de relation `owned_by` avec
-  l'utilisateur syt". Ils peuvent avoir une relation "owned_by" avec un autre
-  utilisateur.
-
-
-Requête d'insertion
--------------------
-   `INSERT` <type d'entité> V1(, <type d'entité> V2)\* `:` <assignements>
-   [`WHERE` <restriction>] 
-
-:assignements:
-  liste des relations à assigner sous la forme `V1 relation V2|<valeur constante>`
-
-La restriction permet de définir des variables utilisées dans les assignements.
-
-Attention, si une restriction est spécifiée, l'insertion est effectuée *pour
-chaque ligne de résultat renvoyée par la restriction*.
-
-Exemples - insertion
-`````````````````````
-* Insertion d'une nouvelle personne nommée 'bidule'::
-
-       INSERT Personne X: X nom 'bidule'
-
-* Insertion d'une nouvelle personne nommée 'bidule', d'une autre nommée
-  'chouette' et d'une relation 'ami' entre eux::
-
-       INSERT Personne X, Personne Y: X nom 'bidule', Y nom 'chouette', X ami Y
-
-* Insertion d'une nouvelle personne nommée 'bidule' et d'une relation 'ami' avec
-  une personne existante nommée 'chouette'::
-
-       INSERT Personne X: X nom 'bidule', X ami Y WHERE Y nom 'chouette'
-
-
-Requête de mise à jour
-----------------------
-   `SET` <assignements>
-   [`WHERE` <restriction>] 
-
-Attention, si une restriction est spécifiée, la mise à jour est effectuée *pour
-chaque ligne de résultat renvoyée par la restriction*.
-
-Exemples - mise à jour 
-````````````````````````
-* Renommage de la personne nommée 'bidule' en 'toto', avec modification du
-  prénom::
-
-       SET X nom 'toto', X prenom 'original' WHERE X is 'Person', X nom 'bidule'
-
-* Insertion d'une relation de type 'connait' entre les objets reliés par la
-  relation de type 'ami'::
-
-       SET X know Y WHERE X ami Y
-
-Requête de suppression
-----------------------
-   `DELETE` (<type d''entité> V) | (V1 relation v2),...
-   [`WHERE` <restriction>] 
-
-Attention, si une restriction est spécifiée, la suppression est effectuée *pour
-chaque ligne de résultat renvoyée par la restriction*.
-
-Exemples
-````````
-* Suppression de la personne nommé 'toto'::
-
-       DELETE Person X WHERE X nom 'toto'
-
-* Suppression de toutes les relations de type 'ami' partant de la personne
-  nommée 'toto'::
-
-       DELETE X ami Y WHERE X is 'Person', X nom 'toto'
--- a/goa/doc/devmanual_fr/chap_serveur_crochets.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-Les crochets (*hooks*)
-======================
-
-XXX FILLME
--- a/goa/doc/devmanual_fr/chap_serveur_notification.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-Gestion de notifications
-========================
-
-XXX FILLME
\ No newline at end of file
--- a/goa/doc/devmanual_fr/chap_tests.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-Tests
-=====
-
-Écriture de tests unitaires
----------------------------
-Le framework de test fournit principalement deux classes de tests dans le module
-`cubicweb.devtools.apptest`:
-
-* `EnvBasedTC`, pour simuler un environnement complet (web + repository)
-* `RepositoryBasedTC`, pour simuler un environnement de repository uniquement
-
-Ces deux classes ont quasiment la même interface et proposent un certain nombre de méthodes
-rendant l'écriture de test puissante et rapide.
-
-XXXFILLME describe API
-
-Dans la plupart des cas, vous allez vouloir hériter de `EnvBasedTC` pour écrire des tests
-unitaires ou fonctionnels pour vos entités, vues, crochets...
-
-
-Test des courriels de notifications
-```````````````````````````````````
-Lors de l'éxécution de tests les courriels potentiellement générés ne sont pas réellement
-envoyé mais se retrouve dans la liste `MAILBOX` du module `cubicweb.devtools.apptest`. Cette
-liste est remise à zéro au *setUp* de chaque test (par le setUp des classes `EnvBasedTC`
-et `RepositoryBasedTC`).
-
-Vous pouvez donc tester vos notifications en analysant le contenu de cette liste, qui
-contient des objets ayant deux attributs :
-* `recipients`, la liste des destinataires
-* `msg`, l'objet email.Message
-
-
-Tests automatiques
-------------------
-XXXFILLME
--- a/goa/doc/devmanual_fr/chap_ui_gestion_formulaire.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-Gestion de formulaires
-======================
-
-Contrôle de la génération automatique de formulaire pour les entités manipulée
-------------------------------------------------------------------------------
-XXX FILLME
-
-* les formulaires 'edition' et 'creation'
-
-Le formulaire généré par défaut ne vous convient pas ? Vous êtes peut-être pas
-obligé de le refaire à la main ! :)
-
-* rtags primary, secondary, generated, generic,
-  `Entity.relation_category(rtype, x='subject')`
-* inline_view (now a rtag?)
-* spécification widget
-
-
-Fonctionnement du contrôleur d'édition par défaut (id: 'edit')
---------------------------------------------------------------
-
-Contrôle de l'édition
-`````````````````````
-Prérequis: les paramètres liés aux entités à éditer sont spécifiés de la forme ::
-
-  <nom de champ>:<eid de l'entité>
-
-où l'eid de l'entité pourra être une lettre dans le cas d'une entité à créer. On
-dénommera ces paramètres comme *qualifié*.
-
-1. récupération des entités à éditer en cherchant les paramètres de formulaire
-   commençant par 'eid:' ayant également un paramètre '__type' associé
-   (également *qualifié* par l'eid évidemment)
-
-2. pour tous les attributs et relations de chaque entité à éditer
-
-   1. recherche d'un paramètre 'edits-<nom relation>' ou 'edito-<nom relation>'
-      qualifié dans le cas d'une relation dont l'entité est objet
-   2. si trouvé, la valeur récupérée est considérée comme la valeur originale
-      pour cette relation, et on cherche la (ou les) nouvelle(s) valeur(s) dans
-      le paramètre <nom relation> (qualifié)
-   3. si la valeur est différente de l'originale, une requête de modification en
-      base est effectuée
-
-3. pour chaque entité à éditer
-
-   1. si un paramètre `__linkto` qualifié est spécifié, sa valeur doit être une
-      chaine (ou une liste de chaine) de la forme : ::
-
-        <relation type>:<eids>:<target>
-
-      où <target> vaut 'subject' ou 'object' et chaque eid peut-être séparé d'un
-      autre par un '_'. Target spécifie *l'entité éditée* est sujet ou objet de la
-      relation et chaque relation ainsi spécifiée sera insérée.
-
-   2. si un paramètre `__cloned_eid` qualifié est spécifié pour une entité, les
-      relations de l'entité spécifiée en valeur de cette argument sont copiées sur
-      l'entité éditée
-
-
-   3. si un paramètre `__delete` qualifié est spécifié, sa valeur doit être une
-      chaine (ou une liste de chaine) de la forme : ::
-
-	<subject eids>:<relation type>:<object eids>
-
-      où chaque eid sujet ou objet peut-être séparé d'un autre par un '_'. Chaque
-      relation ainsi spécifiée sera supprimée.
-
-   4. si un paramètre `__insert` qualifié est spécifié, sa valeur doit être de
-      même format que pour `__delete`, mais chaque relation ainsi spécifiée sera 
-      insérée.
-
-4. si les paramètres `__insert` et/ou  `__delete` sont trouvés non qualifiés,
-   ils sont interprétés comme décrit ci-dessus (quelque soit le nombre d'entité
-   édité)
-
-5. si aucune entité n'est éditée mais que le formulaire contient les paramètres
-   `__linkto` et `eid`, celui-ci est interprété en prenant la valeur spécifié
-   par le paramètre `eid` pour désigner l'entité sur laquelle ajouter les
-   relations
-
-
-A noter que :
-
-* si le paramètre `__action_delete` est trouvé, toutes les entités comme
-  spécifiées à éditer seront supprimées
-
-* si le paramètre `__action_cancel` est trouvé, aucune action n'est effectuée
-
-* si le paramètre `__action_apply` est trouvé, l'édition est effectuée
-  normalement mais la redirection sera effectuée sur le formulaire (cf `Contrôle
-  de la redirection`_)
-
-* le paramètre `__method` est également supporté comme sur le template principal
-  (XXX not very consistent, maybe __method should be dealed in the view controller) 
-
-* si aucune entité à éditer n'est trouvée et qu'il n'y a pas de paramètre
-  `__action_delete`, `__action_cancel`, `__linkto`, `__delete` ou `__insert`,
-  une erreur est levée
-
-* placer dans le formulaire le paramètre `__message` permettra d'utiliser la
-  valeur de ce paramètre comme message d'information à l'utilisateur une fois
-  l'édition effectuée.
-
-
-Contrôle de la redirection
-``````````````````````````
-Une fois que l'édition s'est bien passé, reste un problème : c'est bien beau
-tout ça, mais où qu'on va maintenant ?? Si rien n'est spécifié, le controlleur
-se débrouille, mais comme il fait pas toujours ce qu'on voudrait, on peut
-controller ça en utilisant les paramètres suivant :
-
-* `__redirectpath`: chemin de l'url (relatif à la racine du site, sans paramètre
-  de formulaire
-  
-* `__redirectparams`: paramètres de formulaires à ajouter au chemin
-  
-* `__redirectrql`: requête RQL de redirection
-
-* `__redirectvid`: identifiant de vue de redirection
-
-* `__errorurl`: url du formulaire original, utilisé pour la redirection en cas
-  d'erreur de validation pendant l'édition. Si celui-ci n'est pas spécifié, une
-  page d'erreur sera présentée plutot qu'un retour sur le formulaire (qui est le
-  cas échéant responsable d'afficher les erreurs)
-
-* `__form_id`: identifiant de vue du formulaire original, utilisée si
-  `__action_apply` est trouvé
-
-En général on utilise soit `__redirectpath et `__redirectparams` soit
-`__redirectrql` et `__redirectvid`.
--- a/goa/doc/devmanual_fr/chap_ui_js_json.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-AJAX
-====
-JSON bla  bla
-XXX FILLME
-
-
-Le contrôleur 'json'
---------------------
-XXX FILLME
-
-
-API Javascript
---------------
-XXX FILLME
--- a/goa/doc/devmanual_fr/chap_visualisation_donnees.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-Définition de vues
-==================
-
-Les classes de base des vues
-----------------------------
-
-La class `View` (`cubicweb.common.view`)
-`````````````````````````````````````
-Un vue écrit dans son flux de sortie via son attribut `w` (`UStreamIO`).
-
-L'interface de base des vues est la suivante :
-
-* `dispatch(**context)`, appelle ("rend") la vue en appellent `call` ou
-  `cell_call` en fonction des arguments passé
-* `call(**kwargs)`, appelle la vue pour un result set complet ou nul
-* `cell_call(row, col, **kwargs)`, appelle la vue pour une cellule donnée d'un
-  result set
-* `url()`, retourne l'url permettant d'obtenir cette vue avec le result set en
-  cours 
-* `view(__vid, rset, __fallback_vid=None, **kwargs)`, appelle la vue
-  d'identificant `__vid` sur le result set donné. Il est possible de données un
-  identificant de vue de "fallback" qui sera utilisé si la vue demandée n'est
-  pas applicable au result set
-  
-* `wview(__vid, rset, __fallback_vid=None, **kwargs)`, pareil que `view` mais
-  passe automatiquement le flux en argument
-  
-* `html_headers()`, retourne une liste d'en-tête HTML à placer par le template
-  principal 
-
-* `page_title()`, retourne le titre à utiliser dans l'en tête HTML `title`
-
-* `creator(eid)`, retourne l'eid et le login du créateur de l'entité ayant
-  l'eid passé en argument
-
-Autres classes de base :
-
-* `EntityView`, vue s'appliquant à aux lignes ou cellule contenant une entité
-  (eg un eid)
-* `StartupView`, vue de départ n'ayant pas besoin de result set
-* `AnyRsetView`, vue s'appliquant à n'importe quelle result set
-
-
-Les templates ou patron
------------------------
-
-Les patrons (ou *template*) sont des cas particulier de vue ne dépendant a
-priori pas d'un result set. La classe de base `Template` (`cubicweb.common.view`)
-est une classe dérivée de la classe `View`.
-
-Pour construire une page HTML, un *template principal* est utilisé. Généralement
-celui possédant l'identifiant 'main' est utilisé (ce n'est pas le cas lors
-d'erreur dans celui-ci ou pour le formulaire de login par exemple). Ce patron
-utilise d'autres patrons en plus des vues dépendants du contenu pour générer la
-page à renvoyer.
-
-C'est ce template qui est chargé :
-
-1. d'éxécuter la requête RQL des données à afficher le cas échéant
-2. éventuellement de déterminer la vue à utiliser pour l'afficher si non
-   spécifiée
-3. de composer la page à retourner
-
-
-Le patron principal par défaut (`cubicweb.web.views.basetemplates.TheMainTemplate`)
---------------------------------------------------------------------------------
-
-Le template principal par défaut construit la page selon la décomposition
-suivante :
-
-.. image:: main_template_layout.png
-
-Le rectancle contenant le `view.dispatch()` représente l'emplacement où est
-inséré la vue de contenu à afficher. Les autres représentent des sous-templates
-appelé pour construire la page. Les implémentations par défaut de tout ces
-templates sont dans le module `cubicweb.web.views.basetemplates`. Vous pouvez
-évidemment surcharger l'un des sous-templates pour modifier l'aspect visuel
-d'une partie désirée de la page.
-
-On peut également contrôler certains comportements du template principal à
-l'aide des paramètres de formulaire suivante :
-
-* `__notemplate`, si présente (quelque soit la valeur associée), seule la vue de
-  contenu est renvoyée
-* `__force_display`, si présente et contient une valeur non nulle, pas de
-  navigation quelque soit le nombre d'entités à afficher
-* `__method`, si le result set à afficher ne contient qu'une entité et que ce
-  paramètre est spécifié, celui-ci désigne une méthode à appeler sur l'entité
-  en lui donnant en argument le dictionnaire des paramètres de formulaire, avant
-  de reprendre le comportement classique (s'insère entre les étapes 1. et
-  2. décrites ci-dessus)
-
-
-.. include:: sect_stdlib_vues.txt
-
-
-Vues xml, binaires...
----------------------
-Pour les vues générants autre que du html  (une image générée dynamiquement par
-exemple), et qui ne peuvent donc généralement pas être incluse dans la page
-HTML générée par le template principal (voir ci-dessus), il faut :
-
-* placer l'attribut `templatable` de la classe à `False`
-* indiquer via l'attribut `content_type` de la classe le type MIME généré par la
-  vue 'application/octet-stream'
-
-Pour les vues générants un contenu binaire (une image générée dynamiquement par
-exemple), il faut également placer l'attribut `binary` de la classe à `True` (ce
-qui implique `templatable == False` afin que l'attribut `w` de la vue soit
-remplacé par un flux binaire plutôt que unicode.
-
-
-Quelques trucs (X)HTML à respecter
-----------------------------------
-Certains navigateurs (dont firefox) n'aime pas les `<div>` vides (par vide
-j'entend sans contenu dans la balise, il peut y avoir des attributs), faut
-toujours mettre `<div></div>` même s'il n'y a rien dedans, et non `<div/>`. 
--- a/goa/doc/devmanual_fr/index.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-=======================================
-Développement d'applications avec CubicWeb
-=======================================
-
-
-:Author: Logilab
-:Organization: Logilab
-
-.. contents::
-
-.. include:: chap_fondements_cubicweb.txt
-.. include:: chap_mise_en_place_environnement.txt
-.. include:: chap_rql.txt
-.. include:: chap_definition_schema.txt
-.. include:: chap_definition_workflows.txt
-.. include:: chap_bases_framework_cubicweb.txt
-.. include:: chap_visualisation_donnees.txt
-.. include:: chap_manipulation_donnees.txt
-.. include:: chap_ui_gestion_formulaire.txt
-.. include:: chap_ui_js_json.txt
-.. include:: chap_autres_composants_ui.txt
-.. include:: chap_serveur_crochets.txt
-.. include:: chap_serveur_notification.txt
-
-.. include:: chap_tests.txt
-.. include:: chap_i18n.txt
-.. include:: chap_migration.txt
-
-.. include:: chap_configuration_instance.txt
-
-XXX: XXX FILLME, CSS, API sécurité
Binary file goa/doc/devmanual_fr/main_template_layout.dia has changed
Binary file goa/doc/devmanual_fr/main_template_layout.png has changed
--- a/goa/doc/devmanual_fr/makefile	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-MKHTMLOPTS=--doctype book --param toc.section.depth=1  --target html --stylesheet single-file
-SRC=.
-
-MKPDFOPTS=--doctype book --param toc.section.depth=2  --target pdf --stylesheet standard
-
-TXTFILES:= $(wildcard *.txt)
-TARGET := $(TXTFILES:.txt=.html)
-
-all: index.html
-
-index.html: *.txt
-	mkdoc ${MKHTMLOPTS} index.txt
-
-index.pdf: *.txt
-	mkdoc ${MKPDFOPTS} index.txt
-
-%.html: %.txt
-	mkdoc ${MKHTMLOPTS} $<
-
-clean:
-	rm -f *.html
--- a/goa/doc/devmanual_fr/sect_definition_entites.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,166 +0,0 @@
-Paramétrages et extensions spécifiques
---------------------------------------
-
-Valeurs par défaut dynamiques
-`````````````````````````````
-Il est possible de définir dans le schéma des valeurs par défaut *statiques*.
-Il est également possible de définir des valeurs par défaut *dynamiques* en 
-définissant sur la classe d'entité une méthode `default_<nom attribut>` pour
-un attribut donnée.
-
-
-Contrôle des attributs chargés et du tri par défaut
-```````````````````````````````````````````````````
-* l'attribut de classe `fetch_attrs` permet de définir sur une classe d'entité
-  la liste des noms des attributs ou relations devant être chargés 
-  automatiquement lors de la récupération d'entité(s) de ce type. Dans le cas 
-  des relations, on est limité aux relations *sujets de cardinalité `?` ou `1`*.
-
-* la méthode de classe `fetch_order(attr, var)` prend en argument un nom 
-  d'attribut (ou de relation) et un nom de variable et doit retourner une chaine
-  à utiliser dans la close "ORDERBY" d'une requête RQL pour trier 
-  automatiquement les listes d'entités de ce type selon cet attribut, ou `None`
-  si l'on ne veut pas de tri sur l'attribut passé en argument. Par défaut les 
-  entités sont triées selon leur date de création
-
-* la méthode de classe `fetch_unrelated_order(attr, var)` est similaire à la 
-  méthode `fetch_order` mais est utilisée essentiellement pour contrôler le tri
-  des listes déroulantes permettant de créer des relations dans la vue d'édition
-  d'une entité
-
-La fonction `fetch_config(fetchattrs, mainattr=None)` permet de simplifier la 
-définition des attributs à précharger et du tri en retournant une liste des 
-attributs à précharger (en considérant ceux de la classe  `AnyEntity`
-automatiquement) et une fonction de tri sur l'attribut "principal" (le 2eme 
-argument si spécifié ou sinon le premier attribut de la liste `fetchattrs`).
-Cette fonction est définie dans le package `cubicweb.entities`.
-
-Par exemple : ::
-
-  class Transition(AnyEntity):
-    """..."""
-    id = 'Transition'
-    fetch_attrs, fetch_order = fetch_config(['name'])
-
-Indique que pour le type d'entité "Transition" il faut précharger l'attribut
-"name" et trier par défaut selon cet attribut.
-
-
-Contrôle des formulaires d'édition
-``````````````````````````````````
-Il est possible de contrôler les attributs/relations dans la vue d'édition
-simple ou multiple à l'aide des *rtags* suivants :
-
-* `primary`, indique qu'un attribut ou une relation doit être incorporé dans
-  les formulaires d'édition simple et multiple. Dans le cas d'une relation,
-  le formulaire d'édition de l'entité liée sera inclus dans le formulaire
-
-* `secondary`, indique qu'un attribut ou une relation doit être incorporé dans
-  le formulaire d'édition simple uniquement. Dans le cas d'une relation,
-  le formulaire d'édition de l'entité liée sera inclus dans le formulaire
-
-* `generic`, indique qu'une relation doit être incorporé dans le formulaire 
-  d'édition simple dans la boite générique d'ajout de relation
-
-* `generated`, indique qu'un attribut est caculé dynamiquement ou autre, et 
-  qu'il ne doit donc pas être présent dans les formulaires d'édition
-
-Au besoin il est possible de surcharger la méthode 
-`relation_category(rtype, x='subject')` pour calculer dynamiquement la catégorie
-d'édition d'une relation.
-
-
-Contrôle de la boîte "add_related"
-``````````````````````````````````
-La boite `add related` est une boite automatique proposant de créer une entité
-qui sera automatiquement liée à l'entité de départ (le contexte dans lequel 
-s'affiche la boite). Par défaut, les liens présents dans cette boite sont 
-calculés en fonction des propriétés du schéma de l'entité visualisée, mais il
-est possible de les spécifier explicitement à l'aide des *rtags* suivants :
-
-* `link`, indique qu'une relation est généralement créée vers une entité
-  existante et qu'il ne faut donc pas faire apparaitre de lien pour cette 
-  relation
-
-* `create`, indique qu'une relation est généralement créée vers de nouvelles
-  entités et qu'il faut donc faire apparaitre un lien pour créer une nouvelle
-  entité et la lier automatiquement
-
-Au besoin il est possible de surcharger la méthode  
-`relation_mode(rtype, targettype, x='subject')` pour caculer dynamiquement la
-catégorie de création d'une relation.
-
-A noter également que si au moins une action dans la catégorie "addrelated" est
-trouvée pour le contexte courant, le fonctionnement automatique est désactivé
-en faveur du fonctionnement explicite (i.e. affichage des actions de la
-catégorie "addrelated" uniquement).
-
-Contrôle des formulaires de filtrage de table
-`````````````````````````````````````````````
-La vue "table" par défaut gère dynamiquement un formulaire de filtrage du
-contenu de celle-ci. L'algorithme est le suivant : 
-
-1. on considère que la première colonne contient les entités à restreindre
-2. on recupère la première entité de la table (ligne 0) pour "représenter"
-   toutes les autres
-3. pour toutes les autres variables définies dans la requête originale :
-
-   1. si la variable est liée à la variable principale par au moins une
-      n'importe quelle relation
-   2. on appelle la méthode `filterform_vocabulary(rtype, x)` sur l'entité
-      et si rien est retourné (ou plus exactement un tuple de valeur `None`,
-      voir ci-dessous) on passe à la variable suivante, sinon un élément de
-      formulaire de filtrage sera créé avec les valeurs de vocabulaire
-      retournées
-
-4. il n'y a pas d'autres limitations sur le rql, il peut comporter des clauses
-   de tris, de groupes... Des fonctions javascripts sont utilisées pour
-   regénérer une requête à partir de la requête de départ et des valeurs
-   séléctionnées dans les filtres de formulaire.
-
-   
-La méthode `filterform_vocabulary(rtype, x, var, rqlst, args, cachekey)` prend
-en argument le nom d'une relation et la "cible", qui indique si l'entité sur
-laquelle la méthode est appellée est sujet ou objet de la relation. Elle doit
-retourner :
-
-* un 2-uple de None si elle ne sait pas gérer cette relation
-
-* un type et une liste contenant le vocabulaire
-
-  * la liste doit contenir des couples (valeur, label)
-  * le type indique si la valeur désigne un nombre entier (`type == 'int'`), une
-    chaîne de  caractères (`type == 'string'`) ou une entité non finale (`type
-    == 'eid'`)
-
-Par exemple dans notre application de gestion de tickets, on veut pouvoir
-filtrés ceux-ci par : 
-
-* type
-* priorité
-* état (in_state)
-* étiquette (tags)
-* version (done_in)
-
-On définit donc la méthode suivante : ::
-
-
-    class Ticket(AnyEntity):
-
-	...
-
-	def filterform_vocabulary(self, rtype, x, var, rqlst, args, cachekey):
-	    _ = self.req._
-	    if rtype == 'type':
-		return 'string', [(x, _(x)) for x in ('bug', 'story')]
-	    if rtype == 'priority':
-		return 'string', [(x, _(x)) for x in ('minor', 'normal', 'important')]
-	    if rtype == 'done_in':
-		rql = insert_attr_select_relation(rqlst, var, rtype, 'num')
-		return 'eid', self.req.execute(rql, args, cachekey)
-	    return super(Ticket, self).filterform_vocabulary(rtype, x, var, rqlst,
-							     args, cachekey)
-
-							     
-NOTE: Le support du filtrage sur les étiquettes et l'état est installé
-automatiquement, pas besoin de le gérer ici.
--- a/goa/doc/devmanual_fr/sect_definition_schema.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,339 +0,0 @@
-
-Définition d'un type d'entité
------------------------------
-
-Un type d'entité est définit par une classe python héritant de `EntityType`. Le
-nom de la classe correspond au nom du type. Ensuite le corps de la classe
-contient la description des attributs et des relations pour ce type d'entité,
-par exemple ::
-
-  class Personne(EntityType):
-    """une personne avec les propriétés et relations nécessaires à mon
-    application"""
-
-    nom = String(required=True, fulltextindexed=True)
-    prenom = String(required=True, fulltextindexed=True)
-    civilite = String(vocabulary=('M', 'Mme', 'Mlle'))
-    date_naiss = Date()
-    travaille_pour = SubjectRelation('Company', cardinality='?*')
-
-* le nom de l'attribut python correspond au nom de l'attribut ou de la relation
-  dans cubicweb.
-
-* tout les types de bases sont disponibles nativement : `String`, `Int`, `Float`,
-  `Boolean`, `Date`, `Datetime`, `Time`, `Byte`.
-
-* Chaque type d'entité a au moins les méta-relations suivantes :
-  - `eid` (`Int`)
-  - `creation_date` (`Datetime`)
-  - `modification_date` (`Datetime`)
-  - `owned_by` (`CWUser`)
-  - `is` (`CWEType`)
-
-* il est également possible de définir des relations dont le type d'entité est
-  l'objet en utilisant `ObjectRelation` plutôt que `SubjectRelation`
-
-* le premier argument de `SubjectRelation` et `ObjectRelation` donne
-  respectivement le type d'entité objet /sujet de la relation. Cela
-  peut être : 
-
-  * une chaine de caractères correspondant à un type d'entité
-
-  * un tuple de chaines de caractères correspondant à plusieurs types d'entité
-
-  * les chaînes de caractères spéciales suivantes :
-
-    - "**" : tout les types d'entité
-    - "*" : tout les types d'entité non méta
-    - "@" : tout les types d'entité méta mais non "système" (i.e. servant à la
-      description du schema en base)
-
-* il est possible d'utiliser l'attribut possible `meta` pour marquer un type
-  d'entité comme étant "méta" (i.e. servant à décrire / classifier d'autre
-  entités) 
-
-* propriétés optionnelles des attributs et relations : 
-
-  - `description` : chaine de caractères décrivant un attribut ou une
-    relation. Par défaut cette chaine sera utilisée dans le formulaire de saisie
-    de l'entité, elle est donc destinée à aider l'utilisateur final et doit être
-    marquée par la fonction `_` pour être correctement internationalisée.
-
-  - `constraints` : liste de contraintes devant être respecté par la relation
-    (c.f. `Contraintes`_)
-
-  - `cardinality` : chaine de 2 caractères spécifiant la cardinalité de la
-    relation. Le premier caractère donne la cardinalité de la relation sur le
-    sujet, le 2eme sur l'objet. Quand une relation possède plusieurs sujets ou
-    objets possibles, la cardinalité s'applique sur l'ensemble et non un à un (et
-    doit donc à priori être cohérente...). Les valeurs possibles sont inspirées
-    des expressions régulières :
-
-    * `1`: 1..1
-    * `?`: 0..1
-    * `+`: 1..n
-    * `*`: 0..n
-
-  - `meta` : booléen indiquant que la relation est une méta relation (faux par
-    défaut)
-
-* propriétés optionnelles des attributs : 
-
-  - `required` : booléen indiquant si l'attribut est obligatoire (faux par
-    défaut)
-
-  - `unique` : booléen indiquant si la valeur de l'attribut doit être unique
-    parmi toutes les entités de ce type (faux par défaut)
-
-  - `indexed` : booléen indiquant si un index doit être créé dans la base de
-    données sur cette attribut (faux par défaut). C'est utile uniquement si vous
-    savez que vous allez faire de nombreuses recherche sur la valeur de cet
-    attribut. 
-
-  - `default` : valeur par défaut de l'attribut. A noter que dans le cas des
-    types date, les chaines de caractères correspondant aux mots-clés RQL
-    `TODAY` et `NOW` sont utilisables.
-
-  - `vocabulary` : spécifie statiquement les valeurs possibles d'un attribut
-
-* propriétés optionnelles des attributs de type `String` : 
-
-  - `fulltextindexed` : booléen indiquant si l'attribut participe à l'index plein
-    texte (faux par défaut) (*valable également sur le type `Byte`*)
-
-  - `internationalizable` : booléen indiquant si la valeur de cet attribut est
-    internationalisable (faux par défaut) 
-
-  - `maxsize` : entier donnant la taille maximum de la chaine (pas de limite par
-    défaut)  
-
-* propriétés optionnelles des relations : 
-
-  - `composite` : chaîne indiquant que le sujet (composite == 'subject') est
-    composé de ou des objets de la relation. Pour le cas opposé (l'objet est
-    composé de ou des sujets de la relation, il suffit de mettre 'object' comme
-    valeur. La composition implique que quand la relation est supprimé (et donc
-    aussi quand le composite est supprimé), le ou les composés le sont
-    également. 
-
-
-Contraintes
-```````````
-Par défaut les types de contraintes suivant sont disponibles :
-
-* `SizeConstraint` : permet de spécifier une taille minimale et/ou maximale sur
-  les chaines de caractères (cas générique de `maxsize`)
-
-* `BoundConstraint` : permet de spécifier une valeur minimale et/ou maximale sur
-  les types numériques
-
-* `UniqueConstraint` : identique à "unique=True"
-
-* `StaticVocabularyConstraint` : identique à "vocabulary=(...)"
-
-* `RQLConstraint` : permet de spécifier une requête RQL devant être satisfaite
-  par le sujet et/ou l'objet de la relation. Dans cette requête les variables `S`
-  et `O` sont préféfinies respectivement comme l'entité sujet et objet de la
-  relation
-
-* `RQLVocabularyConstraint` : similaire à la précédente, mais exprimant une
-  contrainte "faible", i.e. servant uniquement à limiter les valeurs apparaissant
-  dans la liste déroulantes du formulaire d'édition, mais n'empêchant pas une
-  autre entité d'être séléctionnée
-
-
-Définition d'un type de relation
---------------------------------
-
-Un type de relation est définit par une classe python héritant de `RelationType`. Le
-nom de la classe correspond au nom du type. Ensuite le corps de la classe
-contient la description des propriétés de ce type de relation, ainsi
-qu'éventuellement une chaine pour le sujet et une autre pour l'objet permettant
-de créer des définitions de relations associées (auquel cas il est possibles de
-donner sur la classe les propriétés de définition de relation explicitées
-ci-dessus), par exemple ::
-
-  class verrouille_par(RelationType):
-    """relation sur toutes les entités applicatives indiquant que celles-ci sont vérouillées
-    inlined = True
-    cardinality = '?*'
-    subject = '*'
-    object = 'CWUser'
-
-En plus des permissions, les propriétés propres aux types de relation (et donc
-partagés par toutes les définitions de relation de ce type) sont :
-
-* `inlined` : booléen contrôlant l'optimisation physique consistant à stocker la
-  relation dans la table de l'entité sujet au lieu de créer une table spécifique
-  à la relation. Cela se limite donc aux relations dont la cardinalité
-  sujet->relation->objet vaut 0..1 ('?') ou 1..1 ('1')
-
-* `symmetric` : booléen indiquant que la relation est symétrique, i.e. "X relation
-   Y" implique "Y relation X"
-
-Dans le cas de définitions de relations simultanée, `sujet` et `object` peuvent
-tout deux valoir la même chose que décrite pour le 1er argument de
-`SubjectRelation` et `ObjectRelation`.
-
-A partir du moment où une relation n'est ni mise en ligne, ni symétrique, et
-ne nécessite pas de permissions particulières, sa définition (en utilisant
-`SubjectRelation` ou `ObjectRelation`) est suffisante.
-
-
-Définition des permissions
---------------------------
-
-La définition des permissions se fait à l'aide de l'attribut `permissions` des
-types d'entité ou de relation. Celui-ci est un dictionnaire dont les clés sont
-les types d'accès (action), et les valeurs les groupes ou expressions autorisées. 
-
-Pour un type d'entité, les actions possibles sont `read`, `add`, `update` et
-`delete`.
-
-Pour un type de relation, les actions possibles sont `read`, `add`, et `delete`.
-
-Pour chaque type d'accès, un tuple indique le nom des groupes autorisés et/ou
-une ou plusieurs expressions RQL devant être vérifiées pour obtenir
-l'accès. L'accès est donné à partir du moment où l'utilisateur fait parti d'un
-des groupes requis ou dès qu'une expression RQL est vérifiée.
-
-Les groupes standards sont :
-
-* `guests`
-
-* `users`
-
-* `managers`
-
-* `owners` : groupe virtuel correspondant au propriétaire d'une entité. Celui-ci
-  ne peut être utilisé que pour les actions `update` et `delete` d'un type
-  d'entité. 
-
-Il est également possible d'utiliser des groupes spécifiques devant être pour
-cela créés dans le precreate de l'application (`migration/precreate.py`).
-
-Utilisation d'expression RQL sur les droits en écriture
-```````````````````````````````````````````````````````
-Il est possible de définir des expressions RQL donnant des droits de
-modification (`add`, `delete`, `update`) sur les types d'entité et de relation.
-
-Expression RQL pour les permissions sur un type d'entité :
-
-* il faut utiliser la classe `ERQLExpression`
-
-* l'expression utilisée correspond à la clause WHERE d'une requête RQL
-
-* dans cette expression, les variables X et U sont des références prédéfinies
-  respectivement sur l'entité courante (sur laquelle l'action est vérifiée) et
-  sur l'utilisateur ayant effectué la requête
-
-* il est possible d'utiliser dans cette expression les relations spéciales
-  "has_<ACTION>_permission" dont le sujet est l'utilisateur et l'objet une
-  variable quelquonque, signifiant ainsi que l'utilisateur doit avoir la
-  permission d'effectuer l'action <ACTION> sur la ou les entités liées cette
-  variable
-
-Pour les expressions RQL sur un type de relation, les principes sont les mêmes
-avec les différences suivantes :
-
-* il faut utiliser la classe `RRQLExpression` dans le cas d'une relation non
-  finale
-
-* dans cette expression, les variables S, O et U sont des références
-  prédéfinies respectivement sur le sujet et l'objet de la relation
-  courante (sur laquelle l'action est vérifiée) et sur l'utilisateur
-  ayant effectué la requête
-
-* On peut aussi définir des droits sur les attributs d'une entité (relation non
-  finale), sachant les points suivants :
-
-  - pour définir des expressions rql, il faut utiliser la classe `ERQLExpression`
-    dans laquelle X représentera l'entité auquel appartient l'attribut
-
-  - les permissions 'add' et 'delete' sont équivalentes. En pratique seul
-    'add'/'read' son pris en considération
-
-
-En plus de cela, le type d'entité `CWPermission` de la librairie standard permet
-de construire des modèles de sécurités très complexes et dynamiques. Le schéma
-de ce type d'entité est le suivant : ::
-
-
-    class CWPermission(MetaEntityType):
-	"""entity type that may be used to construct some advanced security configuration
-	"""
-	name = String(required=True, indexed=True, internationalizable=True, maxsize=100)
-	require_group = SubjectRelation('CWGroup', cardinality='+*',
-					description=_('groups to which the permission is granted'))
-	require_state = SubjectRelation('State',
-				    description=_("entity'state in which the permission is applyable"))
-	# can be used on any entity
-	require_permission = ObjectRelation('**', cardinality='*1', composite='subject',
-					    description=_("link a permission to the entity. This "
-							  "permission should be used in the security "
-							  "definition of the entity's type to be useful."))
-
-
-Exemple de configuration extrait de *jpl* ::
-
-    ...
-
-    class Version(EntityType):
-	"""a version is defining the content of a particular project's release"""
-
-	permissions = {'read':   ('managers', 'users', 'guests',),
-		       'update': ('managers', 'logilab', 'owners',),
-		       'delete': ('managers', ),
-		       'add':    ('managers', 'logilab',
-				  ERQLExpression('X version_of PROJ, U in_group G,'
-						 'PROJ require_permission P, P name "add_version",'
-						 'P require_group G'),)}
-
-    ...
-
-    class version_of(RelationType):
-	"""link a version to its project. A version is necessarily linked to one and only one project.
-	"""
-	permissions = {'read':   ('managers', 'users', 'guests',),
-		       'delete': ('managers', ),
-		       'add':    ('managers', 'logilab',
-				  RRQLExpression('O require_permission P, P name "add_version",'
-						 'U in_group G, P require_group G'),)
-		       }
-	inlined = True
-
-Cette configuration suppose indique qu'une entité `CWPermission` de nom
-"add_version" peut-être associée à un projet et donner le droit de créer des
-versions sur ce projet à des groupes spécifiques. Il est important de noter les
-points suivants :
-
-* dans ce cas il faut protéger à la fois le type d'entité "Version" et la
-  relation liant une version à un projet ("version_of")
-
-* du fait de la généricité du type d'entité `CWPermission`, il faut effectuer
-  l'unification avec les groupes et / ou les états le cas échéant dans
-  l'expression ("U in_group G, P require_group G" dans l'exemple ci-dessus)
-
-
-Utilisation d'expression RQL sur les droits en lecture
-``````````````````````````````````````````````````````
-Les principes sont les mêmes mais avec les restrictions suivantes :
-
-* on ne peut de `RRQLExpression` sur les types de relation en lecture
-
-* les relations spéciales "has_<ACTION>_permission" ne sont pas utilisables
-
-
-Note sur l'utilisation d'expression RQL sur la permission 'add'
-```````````````````````````````````````````````````````````````
-L'utilisation d'expression RQL sur l'ajout d'entité ou de relation pose
-potentiellement un problème pour l'interface utilisateur car si l'expression
-utilise l'entité ou la relation à créer, on est pas capable de vérifier les
-droits avant d'avoir effectué l'ajout (noter que cela n'est pas un problème coté
-serveur rql car la vérification des droits est effectuée après l'ajout
-effectif). Dans ce cas les méthodes de vérification des droits (check_perm,
-has_perm) peuvent inidquer qu'un utilisateur n'a pas le droit d'ajout alors
-qu'il pourrait effectivement l'obtenir. Pour palier à ce soucis il est en général
-nécessaire dans tel cas d'utiliser une action reflétant les droits du schéma
-mais permettant de faire la vérification correctement afin qu'elle apparaisse
-bien le cas échéant.
--- a/goa/doc/devmanual_fr/sect_erudi-ctl.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-L'outil `cubicweb-ctl`
--------------------
-`cubicweb-ctl` est le couteau suisse pour la gestion d'instances CubicWeb.
-La syntaxe générale est ::
-
-  cubicweb-ctl <commande> [options commande] <arguments commandes>
-
-Pour voir les commandes disponibles ::
-
-  cubicweb-ctl
-  cubicweb-ctl --help
-
-A noter que les commandes disponibles varient en fonction des parties de CubicWeb
-qui sont installées.
-
-Pour voir l'aide pour une commande spécifiques ::
-
-  cubicweb-ctl <commande> --help
-
-Commandes pour la création d'un composant
-````````````````````````````````````````
-* ``newtemplate``, crée un nouveau composant sur le système de fichiers
-  à partir du nom passé en paramètre. Cette commande crée le composant à partir
-  d'une squelette d'application, incluant également les fichiers pour le
-  packaging debian)
-  
-Commandes pour la création d'une instance
-`````````````````````````````````````````
-* ``create``, crée les fichiers de configuration d'une instance
-* ``db-create``, crée la base de données système d'une instance (tables et
-  extensions uniquement)
-* ``db-init``, initialise la base de données système d'une instance (schéma,
-  groupes, utilisateurs, workflows...)
-
-Par défaut ces trois commandes sont enchainées.
-
-Commandes pour le lancement des instances
-`````````````````````````````````````````
-* ``start``, démarre une, plusieurs, ou toutes les instances
-* ``stop``, arrêt une, plusieurs, ou toutes les instances
-* ``restart``, redémarre une, plusieurs, ou toutes les instances
-* ``status``, donne l'état des instances
-
-Commandes pour la maintenance des instances
-```````````````````````````````````````````
-* ``upgrade``, lance la migration d'instance(s) existante(s) lorsqu'une nouvelle
-  version d'CubicWeb ou du composant est installée
-* ``shell``, ouvre un shell de migration pour maintenance manuelle d'une instance
-* ``db-dump``, crée un dump de la base de données système
-* ``db-restore``, restore un dump de la base de données système
-* ``db-check``, vérifie l'intégrité des données d'une instance. Si la correction
-  automatique est activée, il est conseillé de faire un dump avant cette
-  opération
-* ``schema-sync``, , synchronise le schéma persistent d'une instance avec le schéma
-  de l'application. Il est conseillé de faire un dump avant cette opération
-
-Commandes pour la maintenance des catalogues i18n
-`````````````````````````````````````````````````
-* ``i18ncubicweb``, regénère les catalogues de messages de la librairie CubicWeb
-* ``i18ncube``, regénère les catalogues de messages d'un composant
-* ``i18ninstance``, recompile les catalogues de messages d'une instance. Cela est
-  effectué automatiquement lors d'une upgrade
-
-Cf Internationalisation_.
-
-Autres commandes
-````````````````
-* ``list``, donne la liste des configurations, des composants et des instances
-  disponibles
-* ``delete``, supprime une instance (fichiers de configuration et base de données)
--- a/goa/doc/devmanual_fr/sect_installation.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-Installation de cubicweb et de ses dépendances
--------------------------------------------
-Tout le système CubicWeb est préparé pour l'installation sur une machine
-debian. L'installation manuelle est un peu pénible du fait des nombreuses
-dépendances à installer (twisted, postgres, autres paquets python...). Nous
-supposerons donc ici que l'installation se fait sur une machine debian ayant
-dans ses sources apt un entrepôt contenant les paquets pour CubicWeb.
-
-Pour tout installer sur le système ::
-
-  apt-get install cubicweb
-
-On peut également n'installer que les paquets cubicweb-server ou cubicweb-twisted pour
-n'avoir que la partie serveur ou client web sur une machine.
-
-Pour tout installer la documentation et les librairies/outils de développement ::
-
-  apt-get install cubicweb-documentation cubicweb-dev
-
-On pourra ensuite installer les paquets suivants :
-
-* `pyro` si vous voulez que l'entrepôt soit accessible via Pyro ou si le client
-  et le serveur ne sont pas sur la même machine (auquel cas il faut installer ce
-  paquet sur les machines clientes et serveur)
-
-* `python-ldap` si vous voulez utiliser une source ldap sur le serveur
-
-* `postgresql-8.1`, `postgresql-contrib-8.1` et `postgresql-plpython-8.1` la
-  machine devant héberger la base de données système
-
-Configuration de l'environnement
---------------------------------
-Ajouter les lignes suivantes à son `.bashrc` ou `.bash_profile` pour configurer
-votre environnement de développement ::
-
-  export CW_REGISTRY=~/etc/cubicweb.d/
-  export CW_CUBES=~/hg/
-  export CW_RUNTIME=/tmp/
-
-Cela suppose que le composant cubicweb que vous développez est dans un
-sous-répertoire de *~/hg/* et que vous avez créé le répertoire *~/etc/cubicweb.d/*
-pour que `cubicweb-ctl` y place vos instances de test.
-
-
-Configuration Postgres
-----------------------
-* création d'un super utilisateur pour la création d'instance (**root**) ::
-
-    createuser --superuser --createdb -P pgadmin
-
-  Un mot de passe de connection pour cet utilisateur vous sera demandé. Il
-  faudra utiliser ce login / mot de passe à la création d'instance via
-  `cubicweb-ctl`
-
-* installation des extensions pour l'index plein texte ::
-
-    cat /usr/share/postgresql/8.1/contrib/tsearch2.sql | psql -U pgadmin template1
-
-* installation du langage plpythonu par défaut ::
-
-    createlang -U pgadmin plpythonu template1
-
-
-Configuration Pyro
-------------------
-Si vous utilisez Pyro, il est nécessaire d'avoir un serveur de noms Pyro
-tournant sur votre réseau (par défaut celui-ci est repéré par une requête
-broadcast). Pour cela il faut soit :
-
-* le lancer à la main avant le démarrage de cubicweb avec la commande `pyro-ns`
-
-* le lancer à la main avant le démarrage de cubicweb sous forme d'un serveur avec
-  la commande `pyro-nsd start`
-
-* éditer le fichier */etc/default/pyro-nsd* pour que le serveur de nom pyro soit
-  lancé automatiquement au démarrage de la machine
--- a/goa/doc/devmanual_fr/sect_mercurial.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-Présentation de Mercurial
--------------------------
-
-Introduction
-````````````
-Mercurial_ gère un ensemble distribué d'entrepôts contenant des arbres de
-révisions (chaque révision indique les changements à effectuer pour obtenir la
-version suivante, et ainsi de suite). Localement, on dispose d'un entrepôt
-contenant un arbre de révisions, et d'un répertoire de travail. Il est possible
-de mettre dans son répertoire de travail, une des versions issue de son entrepôt
-local, de la modifier puis de la verser dans son entrepôt. Il est également
-possible de récuprer dans son entrepôt local des révisions venant d'un autre
-entrepôt, ou d'exporter ses propres révisions depuis son entrepôt local vers un
-autre entrepôt.
-
-A noter que contrairement à CVS/Subversion, on crée généralement un entrepôt par
-projet à gérer.
-
-Lors d'un développement collaboratif, on crée généralement un entrepôt central
-accessible à tout les développeurs du projet. Ces entrepôts centraux servent de
-référence. Selon ses besoins, chacun peut ensuite disposer d'un entrepôt local,
-qu'il faudra penser à synchroniser avec l'entrepôt central de temps à autre. 
-
-
-Principales commandes
-`````````````````````
-* Créer un entrepôt local ::
-
-    hg clone ssh://orion//home/src/prive/rep
-
-* Voir le contenu de l'entrepôt local (outil graphique en Tk) ::
-
-    hg view
-
-* Ajouter un sous-répertoire ou un fichier dans le répertoire courant ::
-
-    hg add rep
-
-* Placer dans son répertoire de travail une révision spécifique (ou la dernière
-  revision) issue de l'entrepôt local ::
-
-    hg update [identifiant-revision]
-    hg up [identifiant-revision]
-
-* Récupérer dans son entrepôt local, l'arbre de révisions contenu dans un
-  entrepôt distant (cette opération ne modifie pas le répertoire local) ::
-
-    hg pull ssh://orion//home/src/prive/rep
-    hg pull -u ssh://orion//home/src/prive/rep # équivalent à pull + update
-
-* Voir quelles sont les têtes de branches de l'entrepôt local si un `pull` a
-  tiré une nouvelle branche ::
-
-    hg heads
-
-* Verser le répertoire de travail dans l'entrepôt local (et créer une nouvelle
-  révision) ::
-
-    hg commit
-    hg ci
-
-* Fusionner, avec la révision mère du répertoire local, une autre révision issue
-  de l'entrepôt local (la nouvelle révision qui en résultera aura alors deux
-  révisions mères) ::
-
-    hg merge identifiant-revision
-
-* Exporter dans un entrepôt distant, l'arbre de révisions contenu dans son
-  entrepôt local (cette opération ne modifie pas le répertoire local) ::
-
-    hg push ssh://orion//home/src/prive/rep
-
-* Voir quelle sont les révisions locales non présentes dans un autre entrepôt ::
-
-    hg outgoing ssh://orion//home/src/prive/rep
-
-* Voir quelle sont les révisions d'un autre entrepôt non présentes localement ::
-
-    hg incoming ssh://orion//home/src/prive/rep
-
-* Voir quelle est la révision issue de l'entrepôt local qui a été sortie dans le
-  répertoire de travail et modifiée ::
-
-    hg parent
-
-* Voir les différences entre le répertoire de travail et la révision mère de
-  l'entrepôt local, éventuellement permettant de les verser dans l'entrepôt
-  local ::
-
-    hg diff
-    hg commit-tool
-    hg ct
-
-
-Bonnes pratiques
-````````````````
-* penser à faire un `hg pull -u` régulièrement et particulièrement avant de
-  faire un `hg commit`
-
-* penser à faire un `hg push` lorsque votre entrepôt contient une version
-  relativement stable de vos modifications
-
-* si un `hg pull -u` a créé une nouvelle tête de branche :
-
-  1. identifier l'identifiant de celle-ci avec `hg head`
-  2. fusionner avec `hg merge`
-  3. `hg ci`
-  4. `hg push`
-
-.. _Mercurial: http://www.selenic.com/mercurial/
--- a/goa/doc/devmanual_fr/sect_stdlib_schemas.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-Schémas prédéfinies dans la librairie
--------------------------------------
-
-La librairie définit un certain nombre de schémas d'entités nécessaires
-au système ou bien couramment utilisées dans les application `cubicweb`.
-Vous pouvez bien entendu étendre ces schémas au besoin.
-
-
-Schémas "systèmes"
-``````````````````
-
-* `CWUser`, utilisateurs du système
-* `CWGroup`, groupes d'utilisateurs
-* `CWEType`, types d'entité
-* `CWRType`, types de relation
-
-* `State`, état d'un workflow
-* `Transition`, transition d'un workflow
-* `TrInfo`, enregistrement d'un passage de transition pour une entité
-
-* `EmailAddress`, adresse électronique, utilisé par le système de notification
-  pour les utilisateurs et par d'autres schéma optionnels
-
-* `CWProperty`, utilisé pour configurer l'application
-* `CWPermission`, utilisé pour configurer la sécurité de l'application
-
-* `Card`, fiche documentaire générique
-* `Bookmark`, un type d'entité utilisé pour permetter à un utilisateur de
-  personnaliser ses liens de navigation dans l'application.
-
-
-Composants de la librairie
-``````````````````
-Une application est construite sur la base de plusieurs composants de base.
-Parmi les composants de base disponible, on trouve par exemple :
-
-* `ecomment`, fournit le type d'entité `Comment` permettant de commenter les
-  entités du site
-  
-* `emailinglist`, fournit le type d'entité `Mailinglist` regroupant des
-  informations sur une liste de discussion
-
-* `efile`, fournit les types d'entités `File` et `Image` utilisés pour
-  représenter des fichiers (texte ou binaire) avec quelques données
-  supplémentaires comme le type MIME ou l'encodage le cas échéant ().
-  
-* `elink`, fournit le type d'entité lien internet (`Link`)
-
-* `eblog`, fournit le type d'entité weblog (`Blog`)
-
-* `eperson`, fournit le type d'entité personne physique (`Person`)
-
-* `eaddressbook`, fournit les types d'entités utilisés pour représenter des n°
-  de téléphone (`PhoneNumber`) et des adresses postales (`PostalAddress`)
-  
-* `eclasstags`, système de classfication à base d'étiquettes (`Tag`)
-
-* `eclassfolders`, système de classification à base de dossiers hiérarchiques
-  destinés à créer des rubriques de navigation (`Folder`)
-
-* `eemail`, gestion d'archives de courriers électroniques (`Email`, `Emailpart`,
-  `Emailthread`)
-
-* `ebasket`, gestion de paniers (`Basket`) permettant de regrouper des entités
-
-Pour déclarer l'utilisation d'un composant, une fois celui-ci installé, ajoutez
-le nom du composant à la variable `__use__` du fichier `__pkginfo__.py` de
-votre propre composant.
--- a/goa/doc/devmanual_fr/sect_stdlib_vues.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-
-Vues prédéfinies dans la librairie
-----------------------------------
-Un certain nombre de vues sont utilisées pour construire l'interface web, qui
-s'appliquent à une ou plusieurs entités. On les distingue par leur identifiant,
-et les principales sont : 
-
-:primary:
-    vue principale pour une entité, elle est appelée par défaut lorsqu'il n'y a
-    qu'un seul élément correspondant à la recherche. Cette vue est censée
-    afficher le maximum d'informations à propos de l'objet.
-:secondary:
-    vue secondaire d'une entité. Par défaut, Elle affiche les deux premiers
-    attributs de l'entité sous la forme d'un lien cliquable amenant sur la vue
-    primaire.
-:oneline:
-    similaire à la vue `secondary`, mais appelée dans des cas où l'on désire que
-    la vue tient sur une ligne, ou de manière générale juste avoir une vue plus
-    abbrégée. Par défaut, cette vue utilise le paramètre de configuration
-    `MAX_LINE_CHAR` pour contrôler la taille du résultat.
-:text:
-    similaire à la vue `oneline`, mais ne devant pas contenir de html.
-:incontext, outofcontext:
-    similaire à la vue `secondary`, mais appelé si l'entité est considérée comme
-    en dehors ou dans son contexte. Par défault renvoie respectivement le
-    résultat de `textincontext` et `textoutofcontext` entouré par un lien
-    permettant d'accéder à la vue primaire de l'entité
-:textincontext, textoutofcontext:
-    similaire à la vue `text`, mais appelé si l'entité est considérée comme
-    en dehors ou dans son contexte. Par défault renvoie respectivement le
-    résultat des méthodes `.dc_title` et `.dc_long_title` de l'entité
-:list:
-    crée une liste html (<ul>) et appelle la vue `listitem` pour chaque entité
-:listitem:
-    redirige par défaut vers la vue `outofcontext`
-:rss:
-    crée unvue RSS/XML et appelle la vue `rssitem` pour chaque entité
-:rssitem:
-    crée unvue RSS/XML pour une entité à partir des résultats renvoyés par les
-    méthodes dublin core de l'objet (`dc_*`)
-
-Vues de départ :
-
-:index:
-    page d'acceuil
-:schema:
-    affiche le schéma de l'application
-
-Vues particulières :
-
-:noresult:
-    appelé si le result set est vide
-:finall:
-    affiche la valeur de la cellule sans transformation (dans le cas d'une
-    entité non finale, on voit son eid). Appelable sur n'importe quel result
-    set.
-:table:
-    crée une table html (<table>) et appelle la vue `cell` pour chaque cellule
-    du résultat. Appelable sur n'importe quel result set.
-:cell:
-    par défaut redirige sur la vue `final` si c'est une entité finale
-    ou sur la vue `outofcontext` sinon
-:null:
-    vue toujours appelable et ne retournant rien
--- a/goa/doc/quickstart.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,128 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Introduction
-=============
-
-
-Concepts et vocabulaire
------------------------
-
-*schéma*
-  le schéma définit le modèle de données d'une application sous forme
-  d'entités et de relations. C'est l'élément central d'une
-  application.
-
-*result set*
-  objet qui encaspule les résultats d'une requête adressée à l'entrepôt
-  de données et des informations sur cette requête.
-
-*vue*
-  une vue est une manière de représenter les données d'un `result set`
-  sous forme HTML, CSV, JSON, etc.
-
-
-
-Définition d'une application de Blog
-====================================
-
-La première chose à faire est de copier le squelette depuis le répertoire
-``lax/skel`` vers un nouveau répertoire qui sera votre application
-``Google AppEngine``::
-
-  $ cp -r lax/skel myapp
-
-Définition du schéma
---------------------
-
-Ouvrir le fichier ``myapp/schema.py`` afin de définir le schéma des
-données manipulées. La syntaxe de la définition est la même que celle
-proposée par `Google AppEngine`_ mais il faut remplacer la ligne
-d'import::
-
-  from google.appengine.ext import db
-
-par celle-ci::
-
-  from cubicweb.goa import db
-
-
-Un exemple de schéma de données pour un ``Blog`` pourrait être::
-
-  from cubicweb.goa import db
-
-  class Blog(db.Model):
-      # un titre à donner à l'entrée
-      title = db.StringProperty(required=True)
-      # la date à laquelle le blog est créé
-      diem = db.DateProperty(required=True, auto_now_add=True)
-      # le contenu de l'entrée
-      content = db.TextProperty()
-      # une entrée peut en citer une autre
-      cites = db.SelfReferenceProperty()
-
-
-Personnalisation des vues
--------------------------
-
-``LAX`` permet de générer directement, à partir de la définition
-du schéma, des vues de consultation, d'ajout et de modification
-pour tous les types de donées manipulés. Il est toutefois
-généralement souhaitable de personnaliser les vues de consultations.
-
-Dans ``LAX``, les vues sont représentées par des classes Python.
-
-Une vue se caractérise par :
-
-- un identifiant (tous les objets dans ``LAX`` sont enregistrés
-  dans un registre et cet identifiant sert de clé pour y retrouver
-  la vue)
-
-- une description des types de données auxquels elle s'applique
-
-Il existe dans ``LAX`` des vues prédéfinies et utilisées par le moteur
-d'affichage. Pour avoir une liste exhaustive de ces vues prédéfinies,
-vous pouvez consulter cette page. (XXX mettre le lien vers la liste).
-Par exemple, la vue ``primary`` est la vue utilisée pour générer la
-page principale de consultation d'un objet.
-
-Par exemple, si on souhaite modifier la page principale d'une entrée de
-blog, il faut surcharger la vue ``primary`` des objets ``Blog`` dans
-le fichier ``myapp/views.py``::
-
-  from cubicweb.web.views import baseviews
-
-  class BlogPrimaryView(baseviews.PrimaryView):
-      accepts = ('Blog',)
-
-      def cell_call(self, row, col):
-          entity = self.rset.get_entity(row, col)
-          self.w(u'<h1>%s</h1>' % entity.title)
-          self.w(u'<div>%s</div>' entity.content)
-
-
-Génération du graphique de schéma
----------------------------------
-
-Il existe une vue ``schema`` qui permet d'afficher un graphique
-représantant les différents types d'entités définis dans le schéma
-ainsi que les relations entre ces types. Ce graphique doit être généré
-statiquement. Le script à utiliser pour générer ce schéma est
-dans ``myapp/tools``. Ce script nécessite d'avoir accès aux
-bibliothèques fournies par le SDK de ``Google AppEngine``. Il faut
-donc modifier son PYTHONPATH::
-
-  $ export PYTHONPATH=GAE_ROOT/google:GAE_ROOT/lib/yaml
-  $ python tools/generate_schema_img.py
-
-
-Génération des fichiers de traduction
--------------------------------------
-
-Des catalogues de traduction se trouvent dans `myapp/i18n`. Il faut
-pour l'instant les mettre à jour à la main (et/ou avec les outils
-``GNU`` comme ``xgettext``) et ensuite les compiler grâce au script
-``myapp/tools/i18ncompile.py``::
-
-  $ python tools/i18ncompile.py
-
-.. _`Google AppEngine` :: http://code.google.com/appengine/docs/datastore/overview.html
--- a/goa/doc/tutorial-wine.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,158 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-=============
-LAX Tutorial
-=============
-
-Introduction
-------------
-
-LAX is a web framework on top of the Google AppEngine datastore.
-
-
-features: schema/data-model at core of app, selection/view mechanism,
-reuseable components, very fast development
-
-
-Since we are french, let us develop an example application that deals
-with wine and will allow any wine enthusiast to track the content of
-its cellar and share his tasting experiences.
-
-Schema
-------
-
-With LAX, the core of the application is the schema/datamodel.
-
-laxctl newapp ? XXX
-
-We will start by something simple and define three entities: WineMaker,
-Wine and Bottle.
-
-::
-
-  class WineMaker(EntityType):
-      name = String(maxsize=50, required=True)
-
-  class Wine(EntityType):
-      name = String(required=True, maxsize=100, fulltextindexed=True)
-      vintage = Int(required=True, constraints=[IntervalBoundConstraint(1850,2100)])
-      grown_by = SubjectRelation('WineMaker', cardinality='?*',
-                                 description=_('Winemaker who grew the wine'))
-
-  class Bottle(EntityType):
-      buy_date = Date(description=_('Date when the bottle was bought.'),
-                      default='TODAY')
-      bottle_of = SubjectRelation('Wine', cardinality='?*')
-
-A WineMaker only has a name which is a string that is required and
-must be less than 50 characters.
-
-A Wine has a name, which is a string that is required, must be less
-than 100 characters and will be indexed in the full-text index XXX
-fulltextindex marche pas encore. A Wine
-also has a vintage year which is an integer that is required and must
-be between 1850 and 2100. A Wine also has a relationship ``grown_by``
-that link it to a WineMaker. Cardinality ``?*`` means that a Wine can
-have zero or one WineMaker (``?`` means `zero or one`) and that a
-WineMaker can have any number of Wine entities (``*`` means `any number
-including zero`).
-
-A Bottle has a buy_date attribute, which is a date with a default
-value of TODAY, meaning that when a new bottle is created, it will
-have its creation date as buy_date unless the user changes it to some
-other date. A Bottle also has a relationship ``bottle_of`` that link
-it to a Wine. The cardinality of that relationship implies that a
-Bottle can be linked to zero or one Wine and that a Wine can by linked
-to any number of Bottle entities.
-
-
-Defining this simple schema is enough to get us started, launch the
-application with the command::
-
-   laxctl start Winopedia
-
-and point your browser at localhost:8080
-
-You will see the home page of your application. It lists the entity
-types: WineMaker, Wine, Bottle.
-
-Let us create a few of these. Click on the [+] at the right of the
-link WineMaker. Call this new WineMaker ``Domaine du château`` and
-validate the form by clicking on ``button_ok``. 
-
-Click on the logo at top left to get back to the home page, then
-follow the WineMaker link. You should be seeing a list with a single
-item ``Domaine du château``. Clicking on this item will get you to 
-its detailed description except that in this case, there is not much
-to display besides the name.
-
-Now get back to the home page by clicking on the top-left logo, then
-create a new WineMaker called ``Vallon de la Dame`` and get back to the
-home page again to follow the WineMaker link for the second time. The
-list now has two items.
-
-Get back to the home page and click on [+] at the right of the link
-Wine. Call this new wine ``Cuvée du Roi`` and enter 2008 as vintage,
-then click on ``button_ok``. You added a new wine without saying who
-made it. There is a box on the left entitled "actions", click on the
-menu item `modify`. You are back to the form to edit the wine entity
-you just created, except that the form now has another section with a
-combobox titled "add a relationship". Chose "grown_by" in this
-menu and a second combobox appears where you pick ``Domaine du
-château``. Validate the changes by clicking  ``button_ok``. The entity
-Wine that is displayed now includes a link to the entity WineMaker
-named ``Domaine du château``.
-
-Exercise
-~~~~~~~~
-
-Create new entities Wine and Bottle.
-
-What we learned
-~~~~~~~~~~~~~~~
-
-Creating a simple schema was enough to set up a new application that
-can store WineMaker, Wine, Bottle. 
-
-What is next ?
---------------
-
-Althought the application is fully functionnal, its look is very
-basic. We will now improve how information is displayed by writing
-views.
-
-
-Views
-======
-
-...
-
-Defining views with selection/views
-
-implementing interfaces, calendar for bottles bought and for tasting.
-calendar with export icalput attribute drink_date on bottle 
-
-add attribute wine color
-
-create view "bottle table" with color, buy_date, drink_date.
-
-in view wine, select Wine.bottles and apply view "bottle table"
-
-demo ajax with filter on bottle table
-
-Components
-===========
-
-...
-
-
-
-customize MainTemplate
-
-rss channel of new bottles or wines
-
-use URLRewriting for nice urls
-
-talk about security access rights
-
-talk about rql
\ No newline at end of file
--- a/goa/doc/tutorial.en.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,227 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-=============
-LAX Tutorial
-=============
-
-Introduction
-============
-
-LAX stands for Logilab App engine eXtension. It is a web framework
-running on top of the Google AppEngine datastore.
-
-Distinctive features include a data-model driven engine, a query
-language, a selection/view mechanism for HTML/XML/text generation,
-reuseable components, etc. It all sums up to very fast and efficient
-development.
-
-This tutorial will guide you to build a blog application step by step
-to discover the unique features of LAX. It assumes that you followed
-the installation guidelines and that both the AppEngine SDK and the
-LAX framework are setup on your computer.
-
-Creating a very simple application
-==================================
-
-Creating a new application
---------------------------
-
-When you installed lax, you saw a directory named skel. Make a copy of
-this directory and call it BlogDemo.
-
-Defining a schema
------------------
-
-With LAX, the schema/datamodel is the core of the application.
-
-Let us start with something simple and improve on it later. First, we
-make sure that in appconfig.py we have a line ::
-
-  schema_type = 'yams'
-
-Then, in schema.py, we define two entities : ``Blog`` and ``BlogEntry``.
-
-::
-				   
-  class Blog(EntityType):
-      title = String(maxsize=50, required=True)
-      description = String()
-
-  class BlogEntry(EntityType):
-      title = String(maxsize=100, required=True)
-      publish_date = Date(default='TODAY')
-      text = String(fulltextindexed=True)
-      category = String(vocabulary=('important','business'))
-      entry_of = SubjectRelation('Blog', cardinality='?*')
-
-A Blog has a title and a description. The title is a string that is
-required and must be less than 50 characters. The description is a
-string that is not constrained.
-
-A BlogEntry has a title, a publish_date and a text. The title is a
-string that is required and must be less than 100 characters. The
-publish_date is a Date with a default value of TODAY, meaning that
-when a BlogEntry is created, its publish_date will be the current day
-unless it is modified. The text is a string that will be indexed in
-the full-text index and has no constraint.
-
-A BlogEntry also has a relationship ``entry_of`` that link it to a
-Blog. The cardinality ``?*`` means that a BlogEntry can be part of
-zero or one Blog (``?`` means `zero or one`) and that a Blog can
-have any number of BlogEntry (``*`` means `any number including
-zero`). For completeness, remember that ``+`` means `one or more`.
-
-:note: in lax-0.3.0, cardinality checking is not fully ported to
-AppEngine, so cardinality limits are not enforced. This should be
-fixed in lax-0.4.0 available at the beginning of June.
-
-Using the application
----------------------
-
-Defining this simple schema is enough to get us started. Launch the
-application with the command::
-
-   python dev_appserver.py BlogDemo
-
-and point your browser at localhost:8080
-
-You will see the home page of your application. It lists the entity
-types: Blog and BlogEntry.
-
-Let us create a few of these. Click on the [+] at the right of the
-link Blog. Call this new Blog ``Tech-blog`` and type in
-``everything about technology`` as the description, then validate the
-form by clicking on ``button_ok``.
-
-Click on the logo at top left to get back to the home page, then
-follow the Blog link. If this link reads ``blog_plural`` it is because
-i18n is not working for you yet. Let us ignore this for a while. After
-following the link, you should be seeing a list with a single item
-``Tech-blog``. Clicking on this item will get you to its detailed
-description except that in this case, there is not much to display
-besides the name and the phrase ``everything about technology``.
-
-Now get back to the home page by clicking on the top-left logo, then
-create a new Blog called ``MyLife`` and get back to the home page
-again to follow the Blog link for the second time. The list now
-has two items.
-
-Get back to the home page and click on [+] at the right of the link
-BlogEntry. Call this new entry ``Hello World`` and type in some text
-before clicking on ``button_ok``. You added a new blog entry without
-saying to what blog it belongs. There is a box on the left entitled
-``actions``, click on the menu item ``modify``. You are back to the form
-to edit the blog entry you just created, except that the form now has
-another section with a combobox titled ``add relation``. Chose
-``entry_of`` in this menu and a second combobox appears where you pick
-``MyLife``. Validate the changes by clicking
-``button_ok``. The entity BlogEntry that is displayed now includes a link
-to the entity Blog named ``MyLife``.
-
-Conclusion
-----------
-
-Exercise
-~~~~~~~~
-
-Create new blog entries in ``Tech-blog``.
-
-What we learned
-~~~~~~~~~~~~~~~
-
-Creating a simple schema was enough to set up a new application that
-can store blogs and blog entries. 
-
-What is next ?
---------------
-
-Althought the application is fully functionnal, its look is very
-basic. We will now improve how information is displayed by writing
-views.
-
-
-Developing the user interface with Views
-========================================
-
-[WRITE ME]
-
-* Defining views with selection/views
-
-* implementing interfaces, calendar for blog entries.
-
-* show that a calendar view can export data to ical. 
-
-* create view "blogentry table" with title, publish_date, category.
-
-* in view blog, select blogentries and apply view "blogentry table"
-
-* demo ajax by filtering blogentry table on category
-
-Components
-===========
-
-[WRITE ME]
-
-* explain the component architecture
-
-* add comments to the blog by importing the comments component
-
-Boxes
-======
-
-[WRITE ME]
-
-* explain how to build a box
-
-* add an blogentry archives box
-
-Preferences
-============
-
-[WRITE ME]
-
-* talk about the user preferences
-
-* add an example on how to hide / display / move a component or a box
-
-MainTemplate
-============
-
-[WRITE ME]
-
-* customize MainTemplate and show that everything in the user
-  interface can be changed
-
-
-RSS Channel
-===========
-
-[WRITE ME]
-
-* show that the RSS view can be used to display an ordered selection
-  of blog entries, thus providing a RSS channel
-
-* show that a different selection (by category) means a different channel
-
-RQL
-====
-
-[WRITE ME]
-
-* talk about the Relation Query Language
-
-URL Rewriting
-=============
-
-[WRITE ME]
-
-* show how urls are mapped to selections and views and explain URLRewriting 
-
-Security
-=========
-
-[WRITE ME]
-
-* talk about security access rights and show that security is defined
-  using RQL
-
--- a/goa/gaesource.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,331 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Adapter for google appengine source.
-
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb import AuthenticationError, UnknownEid
-from cubicweb.server.sources import AbstractSource, ConnectionWrapper
-from cubicweb.server.pool import SingleOperation
-from cubicweb.server.utils import crypt_password
-from cubicweb.goa.dbinit import set_user_groups
-from cubicweb.goa.rqlinterpreter import RQLInterpreter
-
-from google.appengine.api.datastore import Key, Entity, Put, Delete
-from google.appengine.api import datastore_errors, users
-
-def _init_groups(guser, euser):
-    # set default groups
-    if guser is None:
-        groups = ['guests']
-    else:
-        groups = ['users']
-        if users.is_current_user_admin():
-            groups.append('managers')
-    set_user_groups(euser, groups)
-
-def _clear_related_cache(session, gaesubject, rtype, gaeobject):
-    subject, object = str(gaesubject.key()), str(gaeobject.key())
-    for eid, role in ((subject, 'subject'), (object, 'object')):
-        # clear related cache if necessary
-        try:
-            entity = session.entity_cache(eid)
-        except KeyError:
-            pass
-        else:
-            entity.clear_related_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')
-    if gaeobject.kind() == 'CWUser':
-        for asession in session.repo._sessions.itervalues():
-            if asession.user.eid == object:
-                asession.user.clear_related_cache(rtype, 'object')
-
-def _mark_modified(session, gaeentity):
-    modified = session.transaction_data.setdefault('modifiedentities', {})
-    modified[str(gaeentity.key())] = gaeentity
-    DatastorePutOp(session)
-
-def _rinfo(session, subject, rtype, object):
-    gaesubj = session.datastore_get(subject)
-    gaeobj = session.datastore_get(object)
-    rschema = session.vreg.schema.rschema(rtype)
-    cards = rschema.rproperty(gaesubj.kind(), gaeobj.kind(), 'cardinality')
-    return gaesubj, gaeobj, cards
-
-def _radd(session, gaeentity, targetkey, relation, card):
-    if card in '?1':
-        gaeentity[relation] = targetkey
-    else:
-        try:
-            related = gaeentity[relation]
-        except KeyError:
-            related = []
-        else:
-            if related is None:
-                related = []
-        related.append(targetkey)
-        gaeentity[relation] = related
-    _mark_modified(session, gaeentity)
-
-def _rdel(session, gaeentity, targetkey, relation, card):
-    if card in '?1':
-        gaeentity[relation] = None
-    else:
-        related = gaeentity[relation]
-        if related is not None:
-            related = [key for key in related if not key == targetkey]
-            gaeentity[relation] = related or None
-    _mark_modified(session, gaeentity)
-
-
-class DatastorePutOp(SingleOperation):
-    """delayed put of entities to have less datastore write api calls
-
-    * save all modified entities at precommit (should be the first operation
-      processed, hence the 0 returned by insert_index())
-
-    * in case others precommit operations modify some entities, resave modified
-      entities at commit. This suppose that no db changes will occurs during
-      commit event but it should be the case.
-    """
-    def insert_index(self):
-        return 0
-
-    def _put_entities(self):
-        pending = self.session.transaction_data.get('pendingeids', ())
-        modified = self.session.transaction_data.get('modifiedentities', {})
-        for eid, gaeentity in modified.iteritems():
-            assert not eid in pending
-            Put(gaeentity)
-        modified.clear()
-
-    def commit_event(self):
-        self._put_entities()
-
-    def precommit_event(self):
-        self._put_entities()
-
-
-class GAESource(AbstractSource):
-    """adapter for a system source on top of google appengine datastore"""
-
-    passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
-    auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
-    _sols = ({'X': 'CWUser', 'P': 'Password'},)
-
-    options = ()
-
-    def __init__(self, repo, appschema, source_config, *args, **kwargs):
-        AbstractSource.__init__(self, repo, appschema, source_config,
-                                *args, **kwargs)
-        if repo.config['use-google-auth']:
-            self.info('using google authentication service')
-            self.authenticate = self.authenticate_gauth
-        else:
-            self.authenticate = self.authenticate_local
-
-    def reset_caches(self):
-        """method called during test to reset potential source caches"""
-        pass
-
-    def init_creating(self):
-        pass
-
-    def init(self):
-        # XXX unregister unsupported hooks
-        from cubicweb.server.hooks import sync_owner_after_add_composite_relation
-        self.repo.hm.unregister_hook(sync_owner_after_add_composite_relation,
-                                     'after_add_relation', '')
-
-    def get_connection(self):
-        return ConnectionWrapper()
-
-    # ISource interface #######################################################
-
-    def compile_rql(self, rql):
-        rqlst = self.repo.vreg.parse(rql)
-        rqlst.restricted_vars = ()
-        rqlst.children[0].solutions = self._sols
-        return rqlst
-
-    def set_schema(self, schema):
-        """set the instance'schema"""
-        self.interpreter = RQLInterpreter(schema)
-        self.schema = schema
-        if 'CWUser' in schema and not self.repo.config['use-google-auth']:
-            # rql syntax trees used to authenticate users
-            self._passwd_rqlst = self.compile_rql(self.passwd_rql)
-            self._auth_rqlst = self.compile_rql(self.auth_rql)
-
-    def support_entity(self, etype, write=False):
-        """return true if the given entity's type is handled by this adapter
-        if write is true, return true only if it's a RW support
-        """
-        return True
-
-    def support_relation(self, rtype, write=False):
-        """return true if the given relation's type is handled by this adapter
-        if write is true, return true only if it's a RW support
-        """
-        return True
-
-    def authenticate_gauth(self, session, login, password):
-        guser = users.get_current_user()
-        # allowing or not anonymous connection should be done in the app.yaml
-        # file, suppose it's authorized if we are there
-        if guser is None:
-            login = u'anonymous'
-        else:
-            login = unicode(guser.nickname())
-        # XXX http://code.google.com/appengine/docs/users/userobjects.html
-        # use a reference property to automatically work with email address
-        # changes after the propagation feature is implemented
-        key = Key.from_path('CWUser', 'key_' + login, parent=None)
-        try:
-            euser = session.datastore_get(key)
-            # XXX fix user. Required until we find a better way to fix broken records
-            if not euser.get('s_in_group'):
-                _init_groups(guser, euser)
-                Put(euser)
-            return str(key)
-        except datastore_errors.EntityNotFoundError:
-            # create a record for this user
-            euser = Entity('CWUser', name='key_' + login)
-            euser['s_login'] = login
-            _init_groups(guser, euser)
-            Put(euser)
-            return str(euser.key())
-
-    def authenticate_local(self, session, login, password):
-        """return CWUser eid for the given login/password if this account is
-        defined in this source, else raise `AuthenticationError`
-
-        two queries are needed since passwords are stored crypted, so we have
-        to fetch the salt first
-        """
-        args = {'login': login, 'pwd' : password}
-        if password is not None:
-            rset = self.syntax_tree_search(session, self._passwd_rqlst, args)
-            try:
-                pwd = rset[0][0]
-            except IndexError:
-                raise AuthenticationError('bad login')
-            # passwords are stored using the bytea type, so we get a StringIO
-            if pwd is not None:
-                args['pwd'] = crypt_password(password, pwd[:2])
-        # get eid from login and (crypted) password
-        rset = self.syntax_tree_search(session, self._auth_rqlst, args)
-        try:
-            return rset[0][0]
-        except IndexError:
-            raise AuthenticationError('bad password')
-
-    def syntax_tree_search(self, session, union, args=None, cachekey=None,
-                           varmap=None):
-        """return result from this source for a rql query (actually from a rql
-        syntax tree and a solution dictionary mapping each used variable to a
-        possible type). If cachekey is given, the query necessary to fetch the
-        results (but not the results themselves) may be cached using this key.
-        """
-        results, description = self.interpreter.interpret(union, args,
-                                                          session.datastore_get)
-        return results # XXX description
-
-    def flying_insert(self, table, session, union, args=None, varmap=None):
-        raise NotImplementedError
-
-    def add_entity(self, session, entity):
-        """add a new entity to the source"""
-        # do not delay add_entity as other modifications, new created entity
-        # needs an eid
-        entity.put()
-
-    def update_entity(self, session, entity):
-        """replace an entity in the source"""
-        gaeentity = entity.to_gae_model()
-        _mark_modified(session, entity.to_gae_model())
-        if gaeentity.kind() == 'CWUser':
-            for asession in self.repo._sessions.itervalues():
-                if asession.user.eid == entity.eid:
-                    asession.user.update(dict(gaeentity))
-
-    def delete_entity(self, session, entity):
-        """delete an entity from the source"""
-        # do not delay delete_entity as other modifications to ensure
-        # consistency
-        eid = entity.eid
-        key = Key(eid)
-        Delete(key)
-        session.clear_datastore_cache(key)
-        session.drop_entity_cache(eid)
-        session.transaction_data.get('modifiedentities', {}).pop(eid, None)
-
-    def add_relation(self, session, subject, rtype, object):
-        """add a relation to the source"""
-        gaesubj, gaeobj, cards = _rinfo(session, subject, rtype, object)
-        _radd(session, gaesubj, gaeobj.key(), 's_' + rtype, cards[0])
-        _radd(session, gaeobj, gaesubj.key(), 'o_' + rtype, cards[1])
-        _clear_related_cache(session, gaesubj, rtype, gaeobj)
-
-    def delete_relation(self, session, subject, rtype, object):
-        """delete a relation from the source"""
-        gaesubj, gaeobj, cards = _rinfo(session, subject, rtype, object)
-        pending = session.transaction_data.setdefault('pendingeids', set())
-        if not subject in pending:
-            _rdel(session, gaesubj, gaeobj.key(), 's_' + rtype, cards[0])
-        if not object in pending:
-            _rdel(session, gaeobj, gaesubj.key(), 'o_' + rtype, cards[1])
-        _clear_related_cache(session, gaesubj, rtype, gaeobj)
-
-    # system source interface #################################################
-
-    def eid_type_source(self, session, eid):
-        """return a tuple (type, source, extid) for the entity with id <eid>"""
-        try:
-            key = Key(eid)
-        except datastore_errors.BadKeyError:
-            raise UnknownEid(eid)
-        return key.kind(), 'system', None
-
-    def create_eid(self, session):
-        return None # let the datastore generating key
-
-    def add_info(self, session, entity, source, extid=None):
-        """add type and source info for an eid into the system table"""
-        pass
-
-    def delete_info(self, session, eid, etype, uri, extid):
-        """delete system information on deletion of an entity by transfering
-        record from the entities table to the deleted_entities table
-        """
-        pass
-
-    def fti_unindex_entity(self, session, eid):
-        """remove text content for entity with the given eid from the full text
-        index
-        """
-        pass
-
-    def fti_index_entity(self, session, entity):
-        """add text content of a created/modified entity to the full text index
-        """
-        pass
--- a/goa/goaconfig.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,179 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""google appengine configuration
-
-"""
-__docformat__ = "restructuredtext en"
-
-import os
-from os.path import join
-
-from cubicweb import CW_SOFTWARE_ROOT
-from cubicweb.cwconfig import CubicWebConfiguration
-from cubicweb.web.webconfig import WebConfiguration, merge_options
-from cubicweb.server.serverconfig import ServerConfiguration
-from cubicweb.goa.dbmyams import load_schema
-
-UNSUPPORTED_OPTIONS = set(('connections-pool-size',
-                           'pyro-host', 'pyro-instance-id',
-                           'pyro-ns-host', 'pyro-ns-group',
-                           'https-url', 'host', 'pid-file', 'uid', 'base-url', 'log-file',
-                           'smtp-host', 'smtp-port',
-                           'embed-allowed',
-                           ))
-
-# XXX fix:
-# * default sender-name / sender-addr value
-# * what about *session-time
-# * check auth-mode=http + fix doc (eg require use-google-auth = False)
-
-class GAEConfiguration(ServerConfiguration, WebConfiguration):
-    """repository and web instance in Google AppEngine environment"""
-    name = 'app'
-    repo_method = 'inmemory'
-    options = merge_options((
-        ('included-cubes',
-         {'type' : 'csv',
-          'default': [],
-          'help': 'list of db model based cubes used by the instance.',
-          'group': 'main', 'level': 1,
-          }),
-        ('included-yams-cubes',
-         {'type' : 'csv',
-          'default': [],
-          'help': 'list of yams based cubes used by the instance.',
-          'group': 'main', 'level': 1,
-          }),
-        ('use-google-auth',
-         {'type' : 'yn',
-          'default': True,
-          'help': 'does this instance rely on google authentication service or not.',
-          'group': 'main', 'level': 1,
-          }),
-        ('schema-type',
-         {'type' : 'choice', 'choices': ('yams', 'dbmodel'),
-          'default': 'yams',
-          'help': 'does this instance is defining its schema using yams or db model.',
-          'group': 'main', 'level': 1,
-          }),
-        # overriden options
-        ('query-log-file',
-         {'type' : 'string',
-          'default': None,
-          'help': 'web instance query log file: DON\'T SET A VALUE HERE WHEN '
-          'UPLOADING YOUR INSTANCE. This should only be used to analyse '
-          'queries issued by your instance in the development environment.',
-          'group': 'main', 'level': 2,
-          }),
-        ('anonymous-user',
-         {'type' : 'string',
-          'default': None,
-          'help': 'login of the CubicWeb user account to use for anonymous user '
-          '(if you want to allow anonymous). This option will be ignored if '
-          'use-google-auth option is set (in which case you should control '
-          'anonymous access using the app.yaml file)',
-          'group': 'main', 'level': 1,
-          }),
-
-        ) + WebConfiguration.options + ServerConfiguration.options)
-    options = [(optname, optdict) for optname, optdict in options
-               if not optname in UNSUPPORTED_OPTIONS]
-
-    cubicweb_appobject_path = WebConfiguration.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
-    cubicweb_appobject_path = list(cubicweb_appobject_path) + ['goa/appobjects']
-    cube_appobject_path = WebConfiguration.cube_appobject_path | ServerConfiguration.cube_appobject_path
-
-    # use file system schema
-    read_instance_schema = False
-    # schema is not persistent, don't load schema hooks (unavailable)
-    schema_hooks = False
-    # no user workflow for now
-    consider_user_state = False
-
-    # deactivate some hooks during [pre|post]create scripts execution
-    # (unique values check, owned_by/created_by relations setup)
-    free_wheel = True
-
-    if not os.environ.get('APYCOT_ROOT'):
-        CUBES_DIR = join(CW_SOFTWARE_ROOT, '../cubes')
-
-    def __init__(self, appid, apphome=None):
-        if apphome is None:
-            apphome = 'data'
-        self._apphome = apphome
-        self._base_url = None
-        CubicWebConfiguration.__init__(self, appid)
-
-    def __getitem__(self, key):
-        if key == 'connections-pool-size':
-            return 4 # > 1 to allow multiple user sessions in tests
-        if key == 'base-url':
-            return self._base_url
-        return super(GAEConfiguration, self).__getitem__(key)
-
-    # overriden from cubicweb base configuration
-
-    @property
-    def apphome(self):
-        return self._apphome
-
-    def cubes(self):
-        """return the list of top level cubes used by this instance (eg
-        without dependencies)
-        """
-        if self._cubes is None:
-            cubes = self['included-cubes'] + self['included-yams-cubes']
-            cubes = self.expand_cubes(cubes)
-            return self.reorder_cubes(cubes)
-        return self._cubes
-
-    def vc_config(self):
-        """return CubicWeb's engine and instance's cube versions number"""
-        return {}
-
-    # overriden from cubicweb web configuration
-
-    def instance_md5_version(self):
-        return ''
-
-    def _init_base_url(self):
-        pass
-
-    # overriden from cubicweb server configuration
-
-    def sources(self):
-        return {'system': {'adapter': 'gae'}}
-
-    def load_schema(self, schemaclasses=None, extrahook=None):
-        try:
-            return self._schema
-        except AttributeError:
-            self._schema = load_schema(self, schemaclasses, extrahook)
-            return self._schema
-
-    # goa specific
-    def repo_session(self, sessionid):
-        return self.repository()._sessions[sessionid]
-
-    def is_anonymous_user(self, login):
-        if self['use-google-auth']:
-            from google.appengine.api import users
-            return users.get_current_user() is None
-        else:
-            return login == self.anonymous_user()[0]
-
--- a/goa/goactl.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,254 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb on appengine plugins for cubicweb-ctl
-
-"""
-__docformat__ = "restructuredtext en"
-
-from os.path import exists, join, split, basename, normpath, abspath
-from logilab.common.clcommands import register_commands
-
-from cubicweb import CW_SOFTWARE_ROOT, BadCommandUsage
-from cubicweb.toolsutils import (Command, copy_skeleton, create_symlink,
-                                 create_dir)
-from cubicweb.cwconfig import CubicWebConfiguration
-
-
-def slink_directories():
-    import rql, yams, yapps, docutils, roman
-    try:
-        import json as simplejson
-    except ImportError:
-        import simplejson
-    from logilab import common as lgc
-    from logilab import constraint as lgcstr
-    from logilab import mtconverter as lgmtc
-    dirs = [
-        (lgc.__path__[0], 'logilab/common'),
-        (lgmtc.__path__[0], 'logilab/mtconverter'),
-        (lgcstr.__path__[0], 'logilab/constraint'),
-        (rql.__path__[0], 'rql'),
-        (simplejson.__path__[0], 'simplejson'),
-        (yams.__path__[0], 'yams'),
-        (yapps.__path__[0], 'yapps'),
-        (docutils.__path__[0], 'docutils'),
-        (roman.__file__.replace('.pyc', '.py'), 'roman.py'),
-
-        ('/usr/share/fckeditor/', 'fckeditor'),
-
-        (join(CW_SOFTWARE_ROOT, 'web', 'data'), join('cubes', 'shared', 'data')),
-        (join(CW_SOFTWARE_ROOT, 'web', 'wdoc'), join('cubes', 'shared', 'wdoc')),
-        (join(CW_SOFTWARE_ROOT, 'i18n'), join('cubes', 'shared', 'i18n')),
-        (join(CW_SOFTWARE_ROOT, 'goa', 'tools'), 'tools'),
-        (join(CW_SOFTWARE_ROOT, 'goa', 'bin'), 'bin'),
-        ]
-
-    try:
-        import dateutil
-        import vobject
-        dirs.extend([ (dateutil.__path__[0], 'dateutil'),
-                      (vobject.__path__[0], 'vobject') ] )
-    except ImportError:
-        pass
-    return dirs
-
-COPY_CW_FILES = (
-    '__init__.py',
-    '__pkginfo__.py',
-    '_exceptions.py',
-    'appobject.py',
-    'dbapi.py',
-    'cwvreg.py',
-    'cwconfig.py',
-    'entity.py',
-    'interfaces.py',
-    'i18n.py',
-    'mail.py',
-    'migration.py',
-    'mixins.py',
-    'mttransforms.py',
-    'rqlrewrite.py',
-    'rset.py',
-    'schema.py',
-    'schemaviewer.py',
-    'selectors.py',
-    'uilib.py',
-    'utils.py',
-    'vregistry.py',
-    'view.py',
-
-    'ext/html4zope.py',
-    'ext/rest.py',
-
-    'server/hookhelper.py',
-    'server/hooksmanager.py',
-    'server/hooks.py',
-    'server/migractions.py',
-    'server/pool.py',
-    'server/querier.py',
-    'server/repository.py',
-    'server/securityhooks.py',
-    'server/session.py',
-    'server/serverconfig.py',
-    'server/ssplanner.py',
-    'server/utils.py',
-    'server/sources/__init__.py',
-
-    'entities/__init__.py',
-    'entities/authobjs.py',
-    'entities/lib.py',
-    'entities/schemaobjs.py',
-    'entities/wfobjs.py',
-
-    'sobjects/__init__.py',
-    'sobjects/notification.py',
-
-# XXX would be necessary for goa.testlib but require more stuff to be added
-#     such as server.serverconfig and so on (check devtools.__init__)
-#    'devtools/__init__.py',
-#    'devtools/fake.py',
-
-    'web/__init__.py',
-    'web/_exceptions.py',
-    'web/action.py',
-    'web/application.py',
-    'web/box.py',
-    'web/component.py',
-    'web/controller.py',
-    'web/form.py',
-    'web/htmlwidgets.py',
-    'web/httpcache.py',
-    'web/request.py',
-    'web/webconfig.py',
-
-    'web/views/__init__.py',
-    'web/views/actions.py',
-    'web/views/basecomponents.py',
-    'web/views/basecontrollers.py',
-    'web/views/baseforms.py',
-    'web/views/basetemplates.py',
-    'web/views/baseviews.py',
-    'web/views/boxes.py',
-    'web/views/calendar.py',
-    'web/views/error.py',
-    'web/views/editcontroller.py',
-    'web/views/ibreadcrumbs.py',
-    'web/views/idownloadable.py',
-    'web/views/magicsearch.py',
-    'web/views/management.py',
-    'web/views/navigation.py',
-    'web/views/startup.py',
-    'web/views/vcard.py',
-    'web/views/wdoc.py',
-    'web/views/urlpublishing.py',
-    'web/views/urlrewrite.py',
-    'web/views/xbel.py',
-
-    'wsgi/__init__.py',
-    'wsgi/handler.py',
-    'wsgi/request.py',
-
-    'goa/__init__.py',
-    'goa/db.py',
-    'goa/dbinit.py',
-    'goa/dbmyams.py',
-    'goa/goaconfig.py',
-    'goa/goavreg.py',
-    'goa/gaesource.py',
-    'goa/rqlinterpreter.py',
-    'goa/appobjects/__init__.py',
-    'goa/appobjects/components.py',
-    'goa/appobjects/dbmgmt.py',
-    'goa/appobjects/gauthservice.py',
-    'goa/appobjects/sessions.py',
-
-    'schemas/bootstrap.py',
-    'schemas/base.py',
-    )
-
-OVERRIDEN_FILES = (
-    ('toolsutils.py', 'toolsutils.py'),
-    ('mttransforms.py', 'mttransforms.py'),
-    ('server__init__.py', 'server/__init__.py'),
-    ('rqlannotation.py', 'server/rqlannotation.py'),
-    )
-
-
-def create_init_file(pkgdir, pkgname):
-    open(join(pkgdir, '__init__.py'), 'w').write('"""%s pkg"""' % pkgname)
-
-
-class NewGoogleAppCommand(Command):
-    """Create a new google appengine instance.
-
-    <instance directory>
-      the path to the appengine instance directory
-    """
-    name = 'newgapp'
-    arguments = '<instance directory>'
-
-    def run(self, args):
-        if len(args) != 1:
-            raise BadCommandUsage("exactly one argument is expected")
-        appldir, = args
-        appldir = normpath(abspath(appldir))
-        appid = basename(appldir)
-        context = {'appname': appid}
-        # goa instance'skeleton
-        copy_skeleton(join(CW_SOFTWARE_ROOT, 'goa', 'skel'),
-                      appldir, context, askconfirm=True)
-        # cubicweb core dependencies
-        for directory, subdirectory in slink_directories():
-            subdirectory = join(appldir, subdirectory)
-            if not exists(split(subdirectory)[0]):
-                create_dir(split(subdirectory)[0])
-            create_symlink(directory, join(appldir, subdirectory))
-        create_init_file(join(appldir, 'logilab'), 'logilab')
-        # copy supported part of cubicweb
-        create_dir(join(appldir, 'cubicweb'))
-        for fpath in COPY_CW_FILES:
-            target = join(appldir, 'cubicweb', fpath)
-            if not exists(split(target)[0]):
-                create_dir(split(target)[0])
-            create_symlink(join(CW_SOFTWARE_ROOT, fpath), target)
-        # overriden files
-        for fpath, subfpath in OVERRIDEN_FILES:
-            create_symlink(join(CW_SOFTWARE_ROOT, 'goa', 'overrides', fpath),
-                           join(appldir, 'cubicweb', subfpath))
-        # link every supported components
-        packagesdir = join(appldir, 'cubes')
-        create_init_file(join(appldir, 'cubes'), 'cubes')
-        for include in ('addressbook','basket', 'blog','folder',
-                        'tag', 'comment', 'file', 'link',
-                        'mailinglist', 'person', 'task', 'zone',
-                        ):
-            create_symlink(CubicWebConfiguration.cube_dir(include),
-                           join(packagesdir, include))
-        # generate sample config
-        from cubicweb.goa.goaconfig import GAEConfiguration
-        from cubicweb.migration import MigrationHelper
-        config = GAEConfiguration(appid, appldir)
-        if exists(config.main_config_file()):
-            mih = MigrationHelper(config)
-            mih.rewrite_configuration()
-        else:
-            config.save()
-
-
-register_commands((NewGoogleAppCommand,
-                   ))
--- a/goa/goavreg.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""goa specific registry
-
-"""
-__docformat__ = "restructuredtext en"
-
-from os import listdir
-from os.path import join, isdir
-
-from cubicweb import CW_SOFTWARE_ROOT
-from cubicweb.cwvreg import CubicWebVRegistry
-
-
-def _pkg_name(cube, module):
-    if cube is None:
-        return module
-    return 'cubes.%s.%s' % (cube, module)
-
-class GAEVRegistry(CubicWebVRegistry):
-
-    def set_schema(self, schema):
-        """disable reload hooks of cubicweb registry set_schema method"""
-        self.schema = schema
-
-    def load(self, applroot):
-        from cubicweb.goa import db
-        self.load_module(db) # AnyEntity class
-        # explicit loading, we don't want to load __init__.py
-        self.load_directory(join(CW_SOFTWARE_ROOT, 'entities'),
-                            'cubicweb.entities', skip=('__init__.py',))
-        self.load_directory(join(CW_SOFTWARE_ROOT, 'web', 'views'),
-                            'cubicweb.web.views')
-        self.load_directory(join(CW_SOFTWARE_ROOT, 'goa', 'appobjects'),
-                            'cubicweb.goa.appobjects')
-        for cube in reversed(self.config.cubes()):
-            self.load_cube(cube)
-        self.load_instance(applroot)
-
-    def load_directory(self, directory, cube, skip=()):
-        for filename in listdir(directory):
-            if filename[-3:] == '.py' and not filename in skip:
-                self._import('%s.%s' % (cube, filename[:-3]))
-
-    def load_cube(self, cube):
-        self._auto_load(self.config.cube_dir(cube),
-                        cube in self.config['included-cubes'],
-                        cube)
-
-    def load_instance(self, applroot):
-        self._auto_load(applroot, self.config['schema-type'] == 'dbmodel')
-
-    def _import(self, modname):
-        obj = __import__(modname)
-        for attr in modname.split('.')[1:]:
-            obj = getattr(obj, attr)
-        self.load_module(obj)
-
-    def _auto_load(self, path, loadschema, cube=None):
-        vobjpath = self.config.cube_appobject_path
-        for filename in listdir(path):
-            if filename[-3:] == '.py' and filename[:-3] in vobjpath:
-                self._import(_pkg_name(cube, filename[:-3]))
-            else:
-                abspath = join(path, filename)
-                if isdir(abspath) and filename in vobjpath:
-                    self.load_directory(abspath, _pkg_name(cube, filename))
-        if loadschema:
-            # when using db.Model defined schema, the defined class is used as
-            # entity class as well and so have to be registered
-            self._import(_pkg_name(cube, 'schema'))
--- a/goa/overrides/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-# server.__init__
-# 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/>.
-"""
-
-"""
--- a/goa/overrides/mttransforms.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""mime type transformation engine for cubicweb, based on mtconverter
-
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab import mtconverter
-
-from logilab.mtconverter.engine import TransformEngine
-from logilab.mtconverter.transform import Transform
-from cubicweb.uilib import rest_publish, html_publish, remove_html_tags
-
-HTML_MIMETYPES = ('text/html', 'text/xhtml', 'application/xhtml+xml')
-# CubicWeb specific transformations
-
-class rest_to_html(Transform):
-    inputs = ('text/rest', 'text/x-rst')
-    output = 'text/html'
-    def _convert(self, trdata):
-        return rest_publish(trdata.appobject, trdata.decode())
-
-class html_to_html(Transform):
-    inputs = HTML_MIMETYPES
-    output = 'text/html'
-    def _convert(self, trdata):
-        return html_publish(trdata.appobject, trdata.data)
-
-
-# Instantiate and configure the transformation engine
-
-mtconverter.UNICODE_POLICY = 'replace'
-
-ENGINE = TransformEngine()
-ENGINE.add_transform(rest_to_html())
-ENGINE.add_transform(html_to_html())
-
-HAS_PIL_TRANSFORMS = False
-HAS_PYGMENTS_TRANSFORMS = False
-
-class html_to_text(Transform):
-    inputs = HTML_MIMETYPES
-    output = 'text/plain'
-    def _convert(self, trdata):
-        return remove_html_tags(trdata.data)
-ENGINE.add_transform(html_to_text())
--- a/goa/overrides/rqlannotation.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-"""
-__docformat__ = "restructuredtext en"
-
-def set_qdata(getrschema, union, noinvariant):
-    pass
-
-class SQLGenAnnotator(object):
-    def __init__(self, schema):
-        self.schema = schema
-        self.nfdomain = frozenset(eschema.type for eschema in schema.entities()
-                                  if not eschema.final)
-    def annotate(self, rqlst):
-        rqlst.has_text_query = False
-        rqlst.need_distinct = False
-
-
--- a/goa/overrides/server__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-# server debugging flag
-# 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/>.
-"""
-
-"""
-DEBUG = False
-
-# sqlite'stored procedures have to be registered at connexion opening time
-SQL_CONNECT_HOOKS = {}
-
-# add to this set relations which should have their add security checking done
-# *BEFORE* adding the actual relation (done after by default)
-BEFORE_ADD_RELATIONS = set(('owned_by',))
-
-# add to this set relations which should have their add security checking done
-# *at COMMIT TIME* (done after by default)
-ON_COMMIT_ADD_RELATIONS = set(())
-
-# available sources registry
-SOURCE_TYPES = {}
--- a/goa/overrides/server_utils.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-
-class RepoThread(object):
-    def __init__(self, *args):
-        pass # XXX raise
-    def start(self):
-        pass
-    def join(self):
-        pass
-
-class LoopTask(RepoThread):
-    def cancel(self):
-        pass
--- a/goa/overrides/toolsutils.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-import sys
-from cubicweb import warning
-
-def lines(path, comments=None):
-    result = []
-    for line in open(path, 'U'):
-        line = line.strip()
-        if line and (comments is None or not line.startswith(comments)):
-            result.append(line)
-    return result
-
-def read_config(config_file):
-    """read the instance configuration from a file and return it as a
-    dictionnary
-
-    :type config_file: str
-    :param config_file: path to the configuration file
-
-    :rtype: dict
-    :return: a dictionary with specified values associated to option names
-    """
-    config = current = {}
-    try:
-        for line in lines(config_file, comments='#'):
-            try:
-                option, value = line.split('=', 1)
-            except ValueError:
-                option = line.strip().lower()
-                if option[0] == '[':
-                    # start a section
-                    section = option[1:-1]
-                    assert not config.has_key(section), \
-                           'Section %s is defined more than once' % section
-                    config[section] = current = {}
-                    continue
-                print >> sys.stderr, 'ignoring malformed line\n%r' % line
-                continue
-            option = option.strip().replace(' ', '_')
-            value = value.strip()
-            current[option] = value or None
-    except IOError, ex:
-        warning('missing or non readable configuration file %s (%s)',
-                config_file, ex)
-    return config
-
-def env_path(env_var, default, name):
-    return default
-
-def create_dir(*args):
-    raise RuntimeError()
--- a/goa/rqlinterpreter.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,684 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""provide a minimal RQL support for google appengine dbmodel
-
-"""
-__docformat__ = "restructuredtext en"
-
-from datetime import datetime
-
-from rql import RQLHelper, nodes
-
-from cubicweb import Binary
-from cubicweb.rset import ResultSet
-from cubicweb.server import SQL_CONNECT_HOOKS
-
-from google.appengine.api.datastore import Key, Get, Query, Entity
-from google.appengine.api.datastore_types import Text, Blob
-from google.appengine.api.datastore_errors import EntityNotFoundError, BadKeyError
-
-
-def etype_from_key(key):
-    return Key(key).kind()
-
-def poss_var_types(myvar, ovar, kind, solutions):
-    return frozenset(etypes[myvar] for etypes in solutions
-                     if etypes[ovar] == kind)
-
-def expand_result(results, result, myvar, values, dsget=None):
-    values = map(dsget, values)
-    if values:
-        result[myvar] = values.pop(0)
-        for value in values:
-            newresult = result.copy()
-            newresult[myvar] = value
-            results.append(newresult)
-    else:
-        results.remove(result)
-
-def _resolve(restrictions, solutions, fixed):
-    varname = restrictions[0].searched_var
-    objs = []
-    for etype in frozenset(etypes[varname] for etypes in solutions):
-        gqlargs = {}
-        query = Query(etype)
-        for restriction in restrictions:
-            restriction.fill_query(fixed, query)
-        pobjs = query.Run()
-        if varname in fixed:
-            value = fixed[varname]
-            objs += (x for x in pobjs if x == value)
-        else:
-            objs += pobjs
-    if varname in fixed and not objs:
-        raise EidMismatch(varname, value)
-    return objs
-
-def _resolve_not(restrictions, solutions, fixed):
-    restr = restrictions[0]
-    constrvarname = restr.constraint_var
-    if len(restrictions) > 1 or not constrvarname in fixed:
-        raise NotImplementedError()
-    varname = restr.searched_var
-    objs = []
-    for etype in frozenset(etypes[varname] for etypes in solutions):
-        gqlargs = {}
-        for operator in ('<', '>'):
-            query = Query(etype)
-            restr.fill_query(fixed, query, operator)
-            objs += query.Run()
-    return objs
-
-def _print_results(rlist):
-    return '[%s]' % ', '.join(_print_result(r) for r in rlist)
-
-def _print_result(rdict):
-    string = []
-    for k, v in rdict.iteritems():
-        if isinstance(v, Entity):
-            string.append('%s: %s' % (k, v.key()))#_print_result(v)))
-        elif isinstance(v, list):
-            string.append('%s: [%s]' % (k, ', '.join(str(i) for i in v)))
-        else:
-            string.append('%s: %s' % (k, v))
-    return '{%s}' % ', '.join(string)
-
-
-class EidMismatch(Exception):
-    def __init__(self, varname, value):
-        self.varname = varname
-        self.value = value
-
-
-class Restriction(object):
-    supported_operators = ('=',)
-    def __init__(self, rel):
-        operator = rel.children[1].operator
-        if not operator in self.supported_operators:
-            raise NotImplementedError('unsupported operator')
-        self.rel = rel
-        self.operator = operator
-        self.rtype = rel.r_type
-        self.var = rel.children[0]
-
-    def __repr__(self):
-        return '<%s for %s>' % (self.__class__.__name__, self.rel)
-
-    @property
-    def rhs(self):
-        return self.rel.children[1].children[0]
-
-
-class MultipleRestriction(object):
-    def __init__(self, restrictions):
-        self.restrictions = restrictions
-
-    def resolve(self, solutions, fixed):
-        return _resolve(self.restrictions, solutions, fixed)
-
-
-class VariableSelection(Restriction):
-    def __init__(self, rel, dsget, prefix='s'):
-        Restriction.__init__(self, rel)
-        self._dsget = dsget
-        self._not = self.rel.neged(strict=True)
-        self._prefix = prefix + '_'
-
-    def __repr__(self):
-        return '<%s%s for %s>' % (self._prefix[0], self.__class__.__name__, self.rel)
-
-    @property
-    def searched_var(self):
-        if self._prefix == 's_':
-            return self.var.name
-        return self.rhs.name
-
-    @property
-    def constraint_var(self):
-        if self._prefix == 's_':
-            return self.rhs.name
-        return self.var.name
-
-    def _possible_values(self, myvar, ovar, entity, solutions, dsprefix):
-        if self.rtype == 'identity':
-            return (entity.key(),)
-        value = entity.get(dsprefix + self.rtype)
-        if value is None:
-            return []
-        if not isinstance(value, list):
-            value = [value]
-        vartypes = poss_var_types(myvar, ovar, entity.kind(), solutions)
-        return (v for v in value if v.kind() in vartypes)
-
-    def complete_and_filter(self, solutions, results):
-        myvar = self.rhs.name
-        ovar = self.var.name
-        rtype = self.rtype
-        if self.schema.rschema(rtype).final:
-            # should be detected by rql.stcheck: "Any C WHERE NOT X attr C" doesn't make sense
-            #if self._not:
-            #    raise NotImplementedError()
-            for result in results:
-                result[myvar] = result[ovar].get('s_'+rtype)
-        elif self.var.name in results[0]:
-            if self.rhs.name in results[0]:
-                self.filter(solutions, results)
-            else:
-                if self._not:
-                    raise NotImplementedError()
-                for result in results[:]:
-                    values = self._possible_values(myvar, ovar, result[ovar],
-                                                   solutions, 's_')
-                    expand_result(results, result, myvar, values, self._dsget)
-        else:
-            assert self.rhs.name in results[0]
-            self.object_complete_and_filter(solutions, results)
-
-    def filter(self, solutions, results):
-        myvar = self.rhs.name
-        ovar = self.var.name
-        newsols = {}
-        for result in results[:]:
-            entity = result[ovar]
-            key = entity.key()
-            if not key in newsols:
-                values = self._possible_values(myvar, ovar, entity, solutions, 's_')
-                newsols[key] = frozenset(v for v in values)
-            if self._not:
-                if result[myvar].key() in newsols[key]:
-                    results.remove(result)
-            elif not result[myvar].key() in newsols[key]:
-                results.remove(result)
-
-    def object_complete_and_filter(self, solutions, results):
-        if self._not:
-            raise NotImplementedError()
-        myvar = self.var.name
-        ovar = self.rhs.name
-        for result in results[:]:
-            values = self._possible_values(myvar, ovar, result[ovar],
-                                           solutions, 'o_')
-            expand_result(results, result, myvar, values, self._dsget)
-
-
-class EidRestriction(Restriction):
-    def __init__(self, rel, dsget):
-        Restriction.__init__(self, rel)
-        self._dsget = dsget
-
-    def resolve(self, kwargs):
-        value = self.rel.children[1].children[0].eval(kwargs)
-        return self._dsget(value)
-
-
-class RelationRestriction(VariableSelection):
-
-    def _get_value(self, fixed):
-        return fixed[self.constraint_var].key()
-
-    def fill_query(self, fixed, query, operator=None):
-        restr = '%s%s %s' % (self._prefix, self.rtype, operator or self.operator)
-        query[restr] = self._get_value(fixed)
-
-    def resolve(self, solutions, fixed):
-        if self.rtype == 'identity':
-            if self._not:
-                raise NotImplementedError()
-            return [fixed[self.constraint_var]]
-        if self._not:
-            return _resolve_not([self], solutions, fixed)
-        return _resolve([self], solutions, fixed)
-
-
-class NotRelationRestriction(RelationRestriction):
-
-    def _get_value(self, fixed):
-        return None
-
-    def resolve(self, solutions, fixed):
-        if self.rtype == 'identity':
-            raise NotImplementedError()
-        return _resolve([self], solutions, fixed)
-
-
-class AttributeRestriction(RelationRestriction):
-    supported_operators = ('=', '>', '>=', '<', '<=', 'ILIKE')
-    def __init__(self, rel, kwargs):
-        RelationRestriction.__init__(self, rel, None)
-        value = self.rhs.eval(kwargs)
-        self.value = value
-        if self.operator == 'ILIKE':
-            if value.startswith('%'):
-                raise NotImplementedError('LIKE is only supported for prefix search')
-            if not value.endswith('%'):
-                raise NotImplementedError('LIKE is only supported for prefix search')
-            self.operator = '>'
-            self.value = value[:-1]
-
-    def complete_and_filter(self, solutions, results):
-        # check lhs var first in case this is a restriction
-        assert self._not
-        myvar, rtype, value = self.var.name, self.rtype, self.value
-        for result in results[:]:
-            if result[myvar].get('s_'+rtype) == value:
-                results.remove(result)
-
-    def _get_value(self, fixed):
-        return self.value
-
-
-class DateAttributeRestriction(AttributeRestriction):
-    """just a thin layer on top af `AttributeRestriction` that
-    tries to convert date strings such as in :
-    Any X WHERE X creation_date >= '2008-03-04'
-    """
-    def __init__(self, rel, kwargs):
-        super(DateAttributeRestriction, self).__init__(rel, kwargs)
-        if isinstance(self.value, basestring):
-#             try:
-            self.value = datetime.strptime(self.value, '%Y-%m-%d')
-#             except Exception, exc:
-#                 from logging import error
-#                 error('unable to parse date %s with format %%Y-%%m-%%d (exc=%s)', value, exc)
-
-
-class AttributeInRestriction(AttributeRestriction):
-    def __init__(self, rel, kwargs):
-        RelationRestriction.__init__(self, rel, None)
-        values = []
-        for c in self.rel.children[1].iget_nodes(nodes.Constant):
-            values.append(c.eval(kwargs))
-        self.value = values
-
-    @property
-    def operator(self):
-        return 'in'
-
-
-class TypeRestriction(AttributeRestriction):
-    def __init__(self, var):
-        self.var = var
-
-    def __repr__(self):
-        return '<%s for %s>' % (self.__class__.__name__, self.var)
-
-    def resolve(self, solutions, fixed):
-        objs = []
-        for etype in frozenset(etypes[self.var.name] for etypes in solutions):
-            objs += Query(etype).Run()
-        return objs
-
-
-def append_result(res, descr, i, j, value, etype):
-    if value is not None:
-        if isinstance(value, Text):
-            value = unicode(value)
-        elif isinstance(value, Blob):
-            value = Binary(str(value))
-    if j == 0:
-        res.append([value])
-        descr.append([etype])
-    else:
-        res[i].append(value)
-        descr[i].append(etype)
-
-
-class ValueResolver(object):
-    def __init__(self, functions, args, term):
-        self.functions = functions
-        self.args = args
-        self.term = term
-        self._solution = self.term.stmt.solutions[0]
-
-    def compute(self, result):
-        """return (entity type, value) to which self.term is evaluated according
-        to the given result dictionnary and to query arguments (self.args)
-        """
-        return self.term.accept(self, result)
-
-    def visit_function(self, node, result):
-        args = tuple(n.accept(self, result)[1] for n in node.children)
-        value = self.functions[node.name](*args)
-        return node.get_type(self._solution, self.args), value
-
-    def visit_variableref(self, node, result):
-        value = result[node.name]
-        try:
-            etype = value.kind()
-            value = str(value.key())
-        except AttributeError:
-            etype = self._solution[node.name]
-        return etype, value
-
-    def visit_constant(self, node, result):
-        return node.get_type(kwargs=self.args), node.eval(self.args)
-
-
-class RQLInterpreter(object):
-    """algorithm:
-    1. visit the restriction clauses and collect restriction for each subject
-       of a relation. Different restriction types are:
-       * EidRestriction
-       * AttributeRestriction
-       * RelationRestriction
-       * VariableSelection (not really a restriction)
-       -> dictionary {<variable>: [restriction...], ...}
-    2. resolve eid restrictions
-    3. for each select in union:
-           for each solution in select'solutions:
-               1. resolve variables which have attribute restriction
-               2. resolve relation restriction
-               3. resolve selection and add to global results
-    """
-    def __init__(self, schema):
-        self.schema = schema
-        Restriction.schema = schema # yalta!
-        self.rqlhelper = RQLHelper(schema, {'eid': etype_from_key})
-        self._stored_proc = {'LOWER': lambda x: x.lower(),
-                             'UPPER': lambda x: x.upper()}
-        for cb in SQL_CONNECT_HOOKS.get('sqlite', []):
-            cb(self)
-
-    # emulate sqlite connection interface so we can reuse stored procedures
-    def create_function(self, name, nbargs, func):
-        self._stored_proc[name] = func
-
-    def create_aggregate(self, name, nbargs, func):
-        self._stored_proc[name] = func
-
-
-    def execute(self, operation, parameters=None, eid_key=None, build_descr=True):
-        rqlst = self.rqlhelper.parse(operation, annotate=True)
-        try:
-            self.rqlhelper.compute_solutions(rqlst, kwargs=parameters)
-        except BadKeyError:
-            results, description = [], []
-        else:
-            results, description = self.interpret(rqlst, parameters)
-        return ResultSet(results, operation, parameters, description, rqlst=rqlst)
-
-    def interpret(self, node, kwargs, dsget=None):
-        if dsget is None:
-            self._dsget = Get
-        else:
-            self._dsget = dsget
-        try:
-            return node.accept(self, kwargs)
-        except NotImplementedError:
-            self.critical('support for query not implemented: %s', node)
-            raise
-
-    def visit_union(self, node, kwargs):
-        results, description = [], []
-        extra = {'kwargs': kwargs}
-        for child in node.children:
-            pres, pdescr = self.visit_select(child, extra)
-            results += pres
-            description += pdescr
-        return results, description
-
-    def visit_select(self, node, extra):
-        constraints = {}
-        if node.where is not None:
-            node.where.accept(self, constraints, extra)
-        fixed, toresolve, postresolve, postfilters = {}, {}, {}, []
-        # extract NOT filters
-        for vname, restrictions in constraints.items():
-            for restr in restrictions[:]:
-                if isinstance(restr, AttributeRestriction) and restr._not:
-                    postfilters.append(restr)
-                    restrictions.remove(restr)
-                    if not restrictions:
-                        del constraints[vname]
-        # add TypeRestriction for variable which have no restrictions at all
-        for varname, var in node.defined_vars.iteritems():
-            if not varname in constraints:
-                constraints[varname] = [TypeRestriction(var)]
-        #print node, constraints
-        # compute eid restrictions
-        kwargs = extra['kwargs']
-        for varname, restrictions in constraints.iteritems():
-            for restr in restrictions[:]:
-                if isinstance(restr, EidRestriction):
-                    assert not varname in fixed
-                    try:
-                        value = restr.resolve(kwargs)
-                        fixed[varname] = value
-                    except EntityNotFoundError:
-                        return [], []
-                    restrictions.remove(restr)
-        #print 'fixed', fixed.keys()
-        # combine remaining restrictions
-        for varname, restrictions in constraints.iteritems():
-            for restr in restrictions:
-                if isinstance(restr, AttributeRestriction):
-                    toresolve.setdefault(varname, []).append(restr)
-                elif isinstance(restr, NotRelationRestriction) or (
-                    isinstance(restr, RelationRestriction) and
-                    not restr.searched_var in fixed and restr.constraint_var in fixed):
-                    toresolve.setdefault(varname, []).append(restr)
-                else:
-                    postresolve.setdefault(varname, []).append(restr)
-            try:
-                if len(toresolve[varname]) > 1:
-                    toresolve[varname] = MultipleRestriction(toresolve[varname])
-                else:
-                    toresolve[varname] = toresolve[varname][0]
-            except KeyError:
-                pass
-        #print 'toresolve %s' % toresolve
-        #print 'postresolve %s' % postresolve
-        # resolve additional restrictions
-        if fixed:
-            partres = [fixed.copy()]
-        else:
-            partres = []
-        for varname, restr in toresolve.iteritems():
-            varpartres = partres[:]
-            try:
-                values = tuple(restr.resolve(node.solutions, fixed))
-            except EidMismatch, ex:
-                varname = ex.varname
-                value = ex.value
-                partres = [res for res in partres if res[varname] != value]
-                if partres:
-                    continue
-                # some join failed, no possible results
-                return [], []
-            if not values:
-                # some join failed, no possible results
-                return [], []
-            if not varpartres:
-                # init results
-                for value in values:
-                    partres.append({varname: value})
-            elif not varname in partres[0]:
-                # cartesian product
-                for res in partres:
-                    res[varname] = values[0]
-                for res in partres[:]:
-                    for value in values[1:]:
-                        res = res.copy()
-                        res[varname] = value
-                        partres.append(res)
-            else:
-                # union
-                for res in varpartres:
-                    for value in values:
-                        res = res.copy()
-                        res[varname] = value
-                        partres.append(res)
-        #print 'partres', len(partres)
-        #print partres
-        # Note: don't check for empty partres since constant selection may still
-        # produce result at this point
-        # sort to get RelationRestriction before AttributeSelection
-        restrictions = sorted((restr for restrictions in postresolve.itervalues()
-                               for restr in restrictions),
-                              key=lambda x: not isinstance(x, RelationRestriction))
-        # compute stuff not doable in the previous step using datastore queries
-        for restr in restrictions + postfilters:
-            restr.complete_and_filter(node.solutions, partres)
-            if not partres:
-                # some join failed, no possible results
-                return [], []
-        if extra.pop('has_exists', False):
-            # remove potential duplicates introduced by exists
-            toremovevars = [v.name for v in node.defined_vars.itervalues()
-                            if not v.scope is node]
-            if toremovevars:
-                newpartres = []
-                for result in partres:
-                    for var in toremovevars:
-                        del result[var]
-                    if not result in newpartres:
-                        newpartres.append(result)
-                if not newpartres:
-                    # some join failed, no possible results
-                    return [], []
-                partres = newpartres
-        if node.orderby:
-            for sortterm in reversed(node.orderby):
-                resolver = ValueResolver(self._stored_proc, kwargs, sortterm.term)
-                partres.sort(reverse=not sortterm.asc,
-                             key=lambda x: resolver.compute(x)[1])
-        if partres:
-            if node.offset:
-                partres = partres[node.offset:]
-            if node.limit:
-                partres = partres[:node.limit]
-            if not partres:
-                return [], []
-        #print 'completed partres', _print_results(partres)
-        # compute results
-        res, descr = [], []
-        for j, term in enumerate(node.selection):
-            resolver = ValueResolver(self._stored_proc, kwargs, term)
-            if not partres:
-                etype, value = resolver.compute({})
-                # only constant selected
-                if not res:
-                    res.append([])
-                    descr.append([])
-                    res[0].append(value)
-                    descr[0].append(etype)
-            else:
-                for i, sol in enumerate(partres):
-                    etype, value = resolver.compute(sol)
-                    append_result(res, descr, i, j, value, etype)
-        #print '--------->', res
-        return res, descr
-
-    def visit_and(self, node, constraints, extra):
-        for child in node.children:
-            child.accept(self, constraints, extra)
-    def visit_exists(self, node, constraints, extra):
-        extra['has_exists'] = True
-        self.visit_and(node, constraints, extra)
-
-    def visit_not(self, node, constraints, extra):
-        for child in node.children:
-            child.accept(self, constraints, extra)
-        try:
-            extra.pop(node)
-        except KeyError:
-            raise NotImplementedError()
-
-    def visit_relation(self, node, constraints, extra):
-        if node.is_types_restriction():
-            return
-        rschema = self.schema.rschema(node.r_type)
-        neged = node.neged(strict=True)
-        if neged:
-            # ok, we *may* process this Not node (not implemented error will be
-            # raised later if we can't)
-            extra[node.parent] = True
-        if rschema.final:
-            self._visit_final_relation(rschema, node, constraints, extra)
-        elif neged:
-            self._visit_non_final_neged_relation(rschema, node, constraints)
-        else:
-            self._visit_non_final_relation(rschema, node, constraints)
-
-    def _visit_non_final_relation(self, rschema, node, constraints, not_=False):
-        lhs, rhs = node.get_variable_parts()
-        for v1, v2, prefix in ((lhs, rhs, 's'), (rhs, lhs, 'o')):
-            #if not_:
-            nbrels = len(v2.variable.stinfo['relations'])
-            #else:
-            #    nbrels = len(v2.variable.stinfo['relations']) - len(v2.variable.stinfo['uidrels'])
-            if nbrels > 1:
-                constraints.setdefault(v1.name, []).append(
-                    RelationRestriction(node, self._dsget, prefix))
-                # just init an empty list for v2 variable to avoid a
-                # TypeRestriction being added for it
-                constraints.setdefault(v2.name, [])
-                break
-        else:
-            constraints.setdefault(rhs.name, []).append(
-                VariableSelection(node, self._dsget, 's'))
-
-    def _visit_non_final_neged_relation(self, rschema, node, constraints):
-        lhs, rhs = node.get_variable_parts()
-        for v1, v2, prefix in ((lhs, rhs, 's'), (rhs, lhs, 'o')):
-            stinfo = v2.variable.stinfo
-            if not stinfo['selected'] and len(stinfo['relations']) == 1:
-                constraints.setdefault(v1.name, []).append(
-                    NotRelationRestriction(node, self._dsget, prefix))
-                constraints.setdefault(v2.name, [])
-                break
-        else:
-            self._visit_non_final_relation(rschema, node, constraints, True)
-
-    def _visit_final_relation(self, rschema, node, constraints, extra):
-        varname = node.children[0].name
-        if rschema.type == 'eid':
-            constraints.setdefault(varname, []).append(
-                EidRestriction(node, self._dsget))
-        else:
-            rhs = node.children[1].children[0]
-            if isinstance(rhs, nodes.VariableRef):
-                constraints.setdefault(rhs.name, []).append(
-                    VariableSelection(node, self._dsget))
-            elif isinstance(rhs, nodes.Constant):
-                if rschema.objects()[0] in ('Datetime', 'Date'): # XXX
-                    constraints.setdefault(varname, []).append(
-                        DateAttributeRestriction(node, extra['kwargs']))
-                else:
-                    constraints.setdefault(varname, []).append(
-                        AttributeRestriction(node, extra['kwargs']))
-            elif isinstance(rhs, nodes.Function) and rhs.name == 'IN':
-                constraints.setdefault(varname, []).append(
-                    AttributeInRestriction(node, extra['kwargs']))
-            else:
-                raise NotImplementedError()
-
-    def _not_implemented(self, *args, **kwargs):
-        raise NotImplementedError()
-
-    visit_or = _not_implemented
-    # shouldn't occurs
-    visit_set = _not_implemented
-    visit_insert = _not_implemented
-    visit_delete = _not_implemented
-
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(RQLInterpreter, getLogger('cubicweb.goa.rqlinterpreter'))
-set_log_methods(Restriction, getLogger('cubicweb.goa.rqlinterpreter'))
--- a/goa/skel/app.yaml.tmpl	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-application: %(appname)s
-version: 0-1
-runtime: python
-api_version: 1
-
-handlers:
-- url: /admin/.*
-  script: $PYTHON_LIB/google/appengine/ext/admin
-  login: admin
-- url: /data
-  static_dir: cubes/shared/data
-- url: /fckeditor
-  static_dir: fckeditor
-- url: /_load
-  script: loader.py
-  login: admin
-- url: .*
-  script: main.py
-  # comment the line below to allow anonymous access or if you don't want to use
-  # google authentication service
-  login: required
-
-skip_files: |
- ^(.*/)?(
- (app\.yaml)|
- (app\.yml)|
- (index\.yaml)|
- (index\.yml)|
- (#.*#)|
- (.*~)|
- (.*\.py[co])|
- (.*\.xcf)|
- (.*\.asp)|
- (.*\.aspx)|
- (.*\.cfm)|
- (.*\.po)|
- (.*/RCS/.*)|
- (\..*)|
- (.*ChangeLog)|
- (.*README)|
- (.*TODO)|
- (.*DEPENDS)|
- (.*MANIFEST)|
- (.*MANIFEST.in)|
- (.*setup\.py)|
- (.*,cover)|
- (.*\.orig)|
- (.*/test/.*)|
- (.*/tests/.*)|
- (.*/bin/.*)|
- (.*/build/.*)|
- (.*/debian/.*)|
- (.*/doc/.*)|
- (.*/skins/office2003/.*)|
- (.*/editor/skins/silver/.*)|
- (.*/editor/filemanager/.*)|
- (.*/editor/plugins/.*)|
- (.*/editor/images/smiley/.*)|
- (.*/editor/.*spellerpages.*)|
- (.*/docutils/writers/s5_html/.*)|
- (.*/docutils/writers/latex2e/.*)|
- (.*/docutils/writers/newlatex2e/.*)|
- (.*/docutils/writers/pep_html/.*)|
- (bin/.*)|
- (tools/.*)|
- (cubicweb.*/data/.*\.js)|
- (cubicweb.*/data/.*\.css)|
- (cubicweb.*/data/.*\.png)|
- (cubicweb.*/data/.*\.gif)|
- (cubicweb.*/data/.*\.gif)|
- )$
-
--- a/goa/skel/custom.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-def postinit(vreg):
-    """this callback is called at the end of initialization process
-    and can be used to load explicit modules (views or entities).
-
-    For instance :
-    import someviews
-    vreg.load_module(someviws)
-    """
-    # from migration import migrate
-    # migrate(vreg)
--- a/goa/skel/cw-cubes/README.txt	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-This directory is where you should put your lax components.
-
-For your application to actually use a component, you also 
-have to modify the ``INCLUDED_COMPONENTS`` variable in 
-the ``custom.py`` module.
-
-
--- a/goa/skel/i18n/en.po	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-# LAX application po file
-
-msgid ""
-msgstr ""
-"Project-Id-Version: erudi 2.48.2\n"
-"PO-Revision-Date: 2008-03-28 18:14+0100\n"
-"Last-Translator: Logilab Team <contact@logilab.fr>\n"
-"Language-Team: fr <contact@logilab.fr>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: ginco-devtools\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-
--- a/goa/skel/i18n/fr.po	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-# LAX application po file
-
-msgid ""
-msgstr ""
-"Project-Id-Version: erudi 2.48.2\n"
-"PO-Revision-Date: 2008-03-28 18:14+0100\n"
-"Last-Translator: Logilab Team <contact@logilab.fr>\n"
-"Language-Team: fr <contact@logilab.fr>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: ginco-devtools\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-
-
--- a/goa/skel/loader.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-if __name__ == '__main__':
-
-    from os.path import dirname, abspath
-    from cubicweb import goa
-    from cubicweb.goa.goaconfig import GAEConfiguration
-    from cubicweb.goa.dbinit import create_user, create_groups
-
-    # compute instance's root directory
-    APPLROOT = dirname(abspath(__file__))
-    # 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')
-    config = GAEConfiguration('toto', APPLROOT)
-    # create default groups
-    create_groups()
-    if not config['use-google-auth']:
-        # create default admin
-        create_user('admin', 'admin', ('managers', 'users'))
-        # create anonymous user if specified
-        anonlogin = config['anonymous-user']
-        if anonlogin:
-            create_user(anonlogin, config['anonymous-password'], ('guests',))
-    print 'content initialized'
--- a/goa/skel/main.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""module defining the root handler for a lax instance. You should not have
-to change anything here.
-
-"""
-__docformat__ = "restructuredtext en"
-
-# compute instance's root directory
-from os.path import dirname, abspath
-APPLROOT = dirname(abspath(__file__))
-
-# apply monkey patches first
-from cubicweb import goa
-goa.do_monkey_patch()
-
-# 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')
-config = GAEConfiguration('toto', APPLROOT)
-
-# dynamic objects registry
-from cubicweb.goa.goavreg import GAEVregistry
-vreg = GAEVregistry(config, debug=goa.MODE == 'dev')
-
-# trigger automatic classes registration (metaclass magic), should be done
-# before schema loading
-import custom
-
-# load instance'schema
-vreg.schema = config.load_schema()
-
-# load dynamic objects
-vreg.load(APPLROOT)
-
-# call the postinit so custom get a chance to do instance specific stuff
-custom.postinit(vreg)
-
-from cubicweb.wsgi.handler import CubicWebWSGIApplication
-application = CubicWebWSGIApplication(config, vreg=vreg)
-
-# main function so this handler module is cached
-def main():
-    from wsgiref.handlers import CGIHandler
-    CGIHandler().run(application)
-
-if __name__ == "__main__":
-    main()
--- a/goa/skel/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-
-class Blog(EntityType):
-    title = String(maxsize=50, required=True)
-    description = String()
-
-class BlogEntry(EntityType):
-    title = String(maxsize=100, required=True)
-    publish_date = Date(default='TODAY')
-    text = RichString(fulltextindexed=True)
-    category = String(vocabulary=('important','business'))
-    entry_of = SubjectRelation('Blog', cardinality='?*')
--- a/goa/skel/views.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-# custom application views
-# 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/>.
-"""
-
-"""
-from datetime import date
-
-from logilab.common.date import last_day
-
-from cubicweb.web.views import baseviews, boxes, calendar
-from cubicweb.web.htmlwidgets import BoxLink, BoxWidget
-
-_ = unicode
-
-
-class BlogEntryPrimaryView(baseviews.PrimaryView):
-    accepts = ('BlogEntry',)
-
-    def cell_call(self, row, col):
-        entity = self.rset.get_entity(row, col)
-        self.w(u'<h1>%s</h1>' % entity.dc_title())
-        entity.view('metadata', w=self.w)
-        self.w(entity.printable_value('text'))
-
-
-class BlogArchiveBox(boxes.BoxTemplate):
-    """side box usually displaying some related entities in a primary view"""
-    id = 'blog_archives_box'
-    title = _('blog archives')
-
-    def call(self, **kwargs):
-        """display a list of entities by calling their <item_vid> view
-        """
-        _ = self.req._
-        rset = self.req.execute('Any CD ORDERBY CD DESC WHERE B is Blog, B creation_date CD')
-        blogmonths = []
-        for (blogdate,) in rset:
-            year, month = blogdate.year, blogdate.month
-            if (year, month) not in blogmonths:
-                blogmonths.append( (year, month) )
-        box = BoxWidget(_('Blog archives'), id=self.id)
-        for year, month in blogmonths:
-            firstday = date(year, month, 1)
-            lastday = last_day(firstday)
-            rql = ('Any B WHERE B is BlogEntry, B creation_date >= "%s", B creation_date <= "%s"'
-                   % (firstday.strftime('%Y-%m-%d'), lastday.strftime('%Y-%m-%d')))
-            url = self.build_url(rql=rql)
-            label = u'%s %s' % (_(calendar.MONTHNAMES[month-1]), year)
-            box.append( BoxLink(url, label) )
-        box.render(self.w)
-
-
-
-
--- a/goa/test/data/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""zou
-
-"""
--- a/goa/test/data/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-
-
-class YamsEntity(EntityType):
-    if 'Blog' in defined_types and 'Article' in defined_types:
-        ambiguous_relation = SubjectRelation(('Blog', 'Article'))
-    if 'Blog' in defined_types:
-        inlined_relation = SubjectRelation('Blog', cardinality='?*')
-
-class inlined_relation(RelationType):
-    inlined = True
-
--- a/goa/test/data/settings.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-TEMPLATE_DEBUG = False
--- a/goa/test/data/views.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-# -*- coding: utf-8 -*-
-# 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/>.
-"""
-
-"""
-import os
-os.environ["DJANGO_SETTINGS_MODULE"] = 'data.settings'
-
-from django import template
-
-
-def encode_output(self, output):
-    # Check type so that we don't run str() on a Unicode object
-    if not isinstance(output, basestring):
-        return unicode(output)
-    return output
-
-template.VariableNode.encode_output = encode_output
-
-from cubicweb.view import StartupView
-
-INDEX_TEMPLATE = template.Template(u'''
- <h1>hellô {{ user.login }}</h1>
-''')
-
-class MyIndex(StartupView):
-    id = 'index'
-
-    def call(self):
-        ctx = template.Context({'user': self.req.user})
-        return INDEX_TEMPLATE.render(ctx)
--- a/goa/test/unittest_db.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-# -*- coding: utf-8 -*-
-# 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/>.
-"""
-
-"""
-from cubicweb.goa.testlib import *
-
-from cubicweb import Binary
-from cubicweb.goa.goaconfig import GAEConfiguration
-from cubicweb.server.utils import crypt_password
-
-from google.appengine.api.datastore_types import Text, Blob
-
-
-class Blog(db.Model):
-    data = db.BlobProperty()
-
-class DBTest(GAEBasedTC):
-    config = GAEConfiguration('toto')
-    config.global_set_option('use-google-auth', False)
-
-    MODEL_CLASSES = (Blog,)
-
-    def test_set_none_relation(self):
-        eprop = self.add_entity('CWProperty', pkey=u'ui.language', value=u'en')
-        self.failUnless('s_for_user' in eprop._dbmodel)
-        self.assertEquals(eprop._dbmodel['s_for_user'], None)
-
-    def test_euser_key(self):
-        euser = self.add_entity('CWUser', login=u'toto', upassword='toto')
-        self.assertEquals(euser.key().name(), 'key_toto')
-
-    def test_egroup_key(self):
-        egroup = self.add_entity('CWGroup', name=u'toto')
-        self.assertEquals(egroup.key().name(), 'key_toto')
-
-    def test_password_encryption(self):
-        euser = self.add_entity('CWUser', login=u'toto', upassword='toto')
-        self.failUnless(euser.upassword != 'toto', euser.upassword)
-        self.assertEquals(crypt_password('toto', euser.upassword[:2]), euser.upassword)
-
-    def test_long_text(self):
-        # datastore string type is limited to 500 bytes
-        text = u'e'*501
-        entity = self.add_entity('State', name=u'test', description=text)
-        self.assertIsInstance(entity.description, unicode)
-        self.failIf(isinstance(entity.description, Text))
-        self.assertEquals(entity.description, text)
-
-    def test_long_accentued_text(self):
-        # datastore string type is limited to 500 bytes
-        text = u'é'*500
-        entity = self.add_entity('State', name=u'test', description=text)
-        self.assertIsInstance(entity.description, unicode)
-        self.failIf(isinstance(entity.description, Text))
-        self.assertEquals(entity.description, text)
-
-    def test_blob(self):
-        data = 'e'*501
-        entity = self.add_entity('Blog', data=data)
-        self.assertIsInstance(entity.data, Binary)
-        value = entity.data.getvalue()
-        self.failIf(isinstance(value, Blob))
-        self.assertEquals(value, data)
-
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/goa/test/unittest_editcontroller.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,430 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-from cubicweb.goa.testlib import *
-
-from urllib import unquote
-
-from cubicweb import ValidationError
-from cubicweb.uilib import rql_for_eid
-
-from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect
-
-from cubicweb.goa.goaconfig import GAEConfiguration
-from cubicweb.entities.authobjs import CWUser
-
-
-class EditControllerTC(GAEBasedTC):
-
-    config = GAEConfiguration('toto')
-    config.global_set_option('use-google-auth', False)
-    config.global_set_option('schema-type', 'yams')
-    config.global_set_option('included-cubes', ())
-    config.global_set_option('included-yams-cubes', ('blog',))
-
-    MODEL_CLASSES = ()
-    from cubicweb.web.views import editcontroller
-    from cubicweb.entities import lib
-    LOAD_APP_MODULES = (editcontroller, lib)
-
-    def setUp(self):
-        GAEBasedTC.setUp(self)
-        self.req = self.request()
-        self.ctrl = self.get_ctrl(self.req)
-
-    def get_ctrl(self, req):
-        return self.vreg.select('controllers', 'edit', req=req, appli=self)
-
-    def publish(self, req):
-        assert req is self.ctrl.req
-        try:
-            result = self.ctrl.publish()
-            req.cnx.commit()
-        except Redirect:
-            req.cnx.commit()
-            raise
-        except:
-            req.cnx.rollback()
-            raise
-        return result
-
-    def expect_redirect_publish(self, req=None):
-        if req is not None:
-            self.ctrl = self.get_ctrl(req)
-        else:
-            req = self.req
-        try:
-            res = self.publish(req)
-        except Redirect, ex:
-            try:
-                path, params = ex.location.split('?', 1)
-            except:
-                path, params = ex.location, ""
-            req._url = path
-            cleanup = lambda p: (p[0], unquote(p[1]))
-            params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
-            return req.relative_path(False), params # path.rsplit('/', 1)[-1], params
-        else:
-            self.fail('expected a Redirect exception')
-
-    def test_noparam_edit(self):
-        """check behaviour of this controller without any form parameter"""
-        self.req.form = {}
-        self.assertRaises(ValidationError, self.publish, self.req)
-
-    def test_validation_unique(self):
-        """test creation of two linked entities"""
-        user = self.user
-        self.req.form = {'eid': 'X', '__type:X': 'CWUser',
-                         'login:X': self.user.login, 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
-
-
-    def test_user_editing_itself(self):
-        """checking that a manager user can edit itself"""
-        self.skip('missing actual gae support, retry latter')
-        user = self.user
-        basegroups = [str(eid) for eid, in self.req.execute('CWGroup G WHERE X in_group G, X eid %(x)s', {'x': user.eid})]
-        groupeids = [eid for eid, in self.req.execute('CWGroup G WHERE G name in ("managers", "users")')]
-        groups = [str(eid) for eid in groupeids]
-        stateeid = [eid for eid, in self.req.execute('State S WHERE S name "activated"')][0]
-        self.req.form = {
-            'eid':       user.eid,
-            '__type:'+user.eid:    'CWUser',
-            'login:'+user.eid:     unicode(user.login),
-            'firstname:'+user.eid: u'Th\xe9nault',
-            'surname:'+user.eid:   u'Sylvain',
-            'in_group:'+user.eid:  groups,
-            'in_state:'+user.eid:  stateeid,
-            #
-            'edits-login:'+user.eid:     unicode(user.login),
-            'edits-firstname:'+user.eid: u'',
-            'edits-surname:'+user.eid:   u'',
-            'edits-in_group:'+user.eid:  basegroups,
-            'edits-in_state:'+user.eid:  stateeid,
-            }
-        path, params = self.expect_redirect_publish()
-        e = self.req.execute('Any X WHERE X eid %(x)s', {'x': user.eid}, 'x').get_entity(0, 0)
-        self.assertEquals(e.firstname, u'Th\xe9nault')
-        self.assertEquals(e.surname, u'Sylvain')
-        self.assertEquals(e.login, user.login)
-        self.assertEquals([g.eid for g in e.in_group], groupeids)
-        self.assertEquals(e.in_state[0].eid, stateeid)
-
-    def test_user_can_change_its_password(self):
-        user = self.create_user('user')
-        cnx = self.login('user')
-        req = self.request()
-        #self.assertEquals(self.ctrl.schema['CWUser']._groups['read'],
-        #                  ('managers', 'users'))
-        req.form = {
-            'eid': user.eid, '__type:'+user.eid: 'CWUser',
-            '__maineid' : str(user.eid),
-            'upassword:'+user.eid: 'tournicoton',
-            'upassword-confirm:'+user.eid: 'tournicoton',
-            'edits-upassword:'+user.eid:  '',
-            }
-        path, params = self.expect_redirect_publish(req)
-        cnx.commit() # commit to check we don't get late validation error for instance
-        self.assertEquals(path, 'euser/user')
-        self.failIf('vid' in params)
-
-    def test_user_editing_itself_no_relation(self):
-        """checking we can edit an entity without specifying some required
-        relations (meaning no changes)
-        """
-        user = self.user
-        groupeids = [eid for eid, in self.req.execute('CWGroup G WHERE X in_group G, X eid %(x)s', {'x': user.eid})]
-        self.req.form = {
-            'eid':       user.eid,
-            '__type:'+user.eid:    'CWUser',
-            'login:'+user.eid:     unicode(user.login),
-            'firstname:'+user.eid: u'Th\xe9nault',
-            'surname:'+user.eid:   u'Sylvain',
-            #
-            'edits-login:'+user.eid:     unicode(user.login),
-            'edits-firstname:'+user.eid: u'',
-            'edits-surname:'+user.eid:   u'',
-            }
-        path, params = self.expect_redirect_publish()
-        self.req.drop_entity_cache(user.eid)
-        e = self.req.execute('Any X WHERE X eid %(x)s', {'x': user.eid}, 'x').get_entity(0, 0)
-        self.assertEquals(e.login, user.login)
-        self.assertEquals(e.firstname, u'Th\xe9nault')
-        self.assertEquals(e.surname, u'Sylvain')
-        self.assertUnorderedIterableEquals([g.eid for g in e.in_group], groupeids)
-        #stateeids = [eid for eid, in self.req.execute('State S WHERE S name "activated"')]
-        #self.assertEquals([s.eid for s in e.in_state], stateeids)
-
-
-    def test_create_multiple_linked(self):
-        gueid = self.req.execute('CWGroup G WHERE G name "users"')[0][0]
-        self.req.form = {'eid': ['X', 'Y'],
-
-                         '__type:X': 'CWUser',
-                         '__maineid' : 'X',
-                         'login:X': u'adim', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
-                         'surname:X': u'Di Mascio', 'edits-surname:X': '',
-
-                         'in_group:X': gueid, 'edits-in_group:X': INTERNAL_FIELD_VALUE,
-
-                         '__type:Y': 'EmailAddress',
-                         'address:Y': u'dima@logilab.fr', 'edits-address:Y': '',
-                         'use_email:X': 'Y', 'edits-use_email:X': INTERNAL_FIELD_VALUE,
-                         }
-        path, params = self.expect_redirect_publish()
-        # should be redirected on the created person
-        self.assertEquals(path, 'euser/adim')
-        e = self.req.execute('Any P WHERE P surname "Di Mascio"').get_entity(0, 0)
-        self.assertEquals(e.surname, 'Di Mascio')
-        email = e.use_email[0]
-        self.assertEquals(email.address, 'dima@logilab.fr')
-
-    def test_edit_multiple_linked(self):
-        peid = self.create_user('adim').eid
-        self.req.form = {'eid': [peid, 'Y'],
-                         '__type:%s'%peid: 'CWUser',
-                         'surname:%s'%peid: u'Di Masci', 'edits-surname:%s'%peid: '',
-
-                         '__type:Y': 'EmailAddress',
-                         'address:Y': u'dima@logilab.fr', 'edits-address:Y': '',
-                         'use_email:%s'%peid: 'Y', 'edits-use_email:%s'%peid: INTERNAL_FIELD_VALUE,
-
-                         '__redirectrql': 'Any X WHERE X eid %s'%peid,
-                         }
-        path, params = self.expect_redirect_publish()
-        # should be redirected on the created person
-        eid = params['rql'].split()[-1]
-        e = self.req.execute('Any X WHERE X eid %(x)s', {'x': eid}, 'x').get_entity(0, 0)
-        self.assertEquals(e.surname, 'Di Masci')
-        email = e.use_email[0]
-        self.assertEquals(email.address, 'dima@logilab.fr')
-
-        emaileid = email.eid
-        self.req.form = {'eid': [peid, emaileid],
-                         '__type:%s'%peid: 'CWUser',
-                         'surname:%s'%peid: u'Di Masci', 'edits-surname:%s'%peid: 'Di Masci',
-                         '__type:%s'%emaileid: 'EmailAddress',
-                         'address:%s'%emaileid: u'adim@logilab.fr', 'edits-address:%s'%emaileid: 'dima@logilab.fr',
-                         'use_email:%s'%peid: emaileid, 'edits-use_email:%s'%peid: emaileid,
-                         '__redirectrql': 'Any X WHERE X eid %s'%peid,
-                         }
-        path, params = self.expect_redirect_publish()
-        # should be redirected on the created person
-        eid = params['rql'].split()[-1]
-        # XXX this should not be necessary, it isn't with regular cubicweb
-        self.req._eid_cache = {}
-        e = self.req.execute('Any X WHERE X eid %(x)s', {'x': eid}, 'x').get_entity(0, 0)
-        self.assertEquals(e.surname, 'Di Masci')
-        email = e.use_email[0]
-        self.assertEquals(email.address, 'adim@logilab.fr')
-
-
-    def test_password_confirm(self):
-        """test creation of two linked entities
-        """
-        user = self.user
-        self.req.form = {'__cloned_eid:X': user.eid,
-                         'eid': 'X', '__type:X': 'CWUser',
-                         'login:X': u'toto', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'edits-upassword:X': u'',
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
-        self.req.form = {'__cloned_eid:X': user.eid,
-                         'eid': 'X', '__type:X': 'CWUser',
-                         'login:X': u'toto', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'tutu', 'edits-upassword:X': u'',
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
-
-
-    def test_req_pending_insert(self):
-        """make sure req's pending insertions are taken into account"""
-        tmpgroup = self.add_entity('CWGroup', name=u"test")
-        user = self.user
-        self.req.set_session_data('pending_insert', set([(user.eid, 'in_group', tmpgroup.eid)]))
-        path, params = self.expect_redirect_publish()
-        usergroups = [gname for gname, in
-                      self.req.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
-        self.assertUnorderedIterableEquals(usergroups, ['managers', 'users', 'test'])
-        self.assertEquals(self.req.get_pending_inserts(), [])
-
-
-    def test_req_pending_delete(self):
-        """make sure req's pending deletions are taken into account"""
-        user = self.user
-        groupeid = self.req.execute('INSERT CWGroup G: G name "test", U in_group G WHERE U eid %(x)s',
-                                    {'x': user.eid})[0][0]
-        usergroups = [gname for gname, in
-                      self.req.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
-        # just make sure everything was set correctly
-        self.assertUnorderedIterableEquals(usergroups, ['managers', 'users', 'test'])
-        # now try to delete the relation
-        self.req.set_session_data('pending_delete', set([(user.eid, 'in_group', groupeid)]))
-        path, params = self.expect_redirect_publish()
-        usergroups = [gname for gname, in
-                      self.req.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
-        self.assertUnorderedIterableEquals(usergroups, ['managers', 'users'])
-        #self.assertUnorderedIterableEquals(usergroups, ['managers'])
-        self.assertEquals(self.req.get_pending_deletes(), [])
-
-    def test_custom_attribute_handler(self):
-        def custom_login_edit(self, formparams, value, relations):
-            formparams['login'] = value.upper()
-            relations.append('X login %(login)s')
-        CWUser.custom_login_edit = custom_login_edit
-        try:
-            user = self.user
-            eid = repr(user.eid)
-            self.req.form = {
-                'eid': eid,
-                '__type:'+eid:  'CWUser',
-                'login:'+eid: u'foo',
-                'edits-login:'+eid:  unicode(user.login),
-                }
-            path, params = self.expect_redirect_publish()
-            rset = self.req.execute('Any L WHERE X eid %(x)s, X login L', {'x': user.eid}, 'x')
-            self.assertEquals(rset[0][0], 'FOO')
-        finally:
-            del CWUser.custom_login_edit
-
-    def test_redirect_apply_button(self):
-        redirectrql = rql_for_eid(4012) # whatever
-        self.req.form = {
-                         'eid': 'A', '__type:A': 'BlogEntry',
-                         '__maineid' : 'A',
-                         'content:A': u'"13:03:43"', 'edits-content:A': '',
-                         'title:A': u'huuu', 'edits-title:A': '',
-                         '__redirectrql': redirectrql,
-                         '__redirectvid': 'primary',
-                         '__redirectparams': 'toto=tutu&tata=titi',
-                         '__form_id': 'edition',
-                         '__action_apply': '',
-                         }
-        path, params = self.expect_redirect_publish()
-        self.failUnless(path.startswith('blogentry/'))
-        eid = path.split('/')[1]
-        self.assertEquals(params['vid'], 'edition')
-        self.assertNotEquals(eid, '4012')
-        self.assertEquals(params['__redirectrql'], redirectrql)
-        self.assertEquals(params['__redirectvid'], 'primary')
-        self.assertEquals(params['__redirectparams'], 'toto=tutu&tata=titi')
-
-    def test_redirect_ok_button(self):
-        redirectrql = rql_for_eid(4012) # whatever
-        self.req.form = {
-                         'eid': 'A', '__type:A': 'BlogEntry',
-                         '__maineid' : 'A',
-                         'content:A': u'"13:03:43"', 'edits-content:A': '',
-                         'title:A': u'huuu', 'edits-title:A': '',
-                         '__redirectrql': redirectrql,
-                         '__redirectvid': 'primary',
-                         '__redirectparams': 'toto=tutu&tata=titi',
-                         '__form_id': 'edition',
-                         }
-        path, params = self.expect_redirect_publish()
-        self.assertEquals(path, 'view')
-        self.assertEquals(params['rql'], redirectrql)
-        self.assertEquals(params['vid'], 'primary')
-        self.assertEquals(params['tata'], 'titi')
-        self.assertEquals(params['toto'], 'tutu')
-
-    def test_redirect_delete_button(self):
-        eid = self.add_entity('BlogEntry', title=u'hop', content=u'hop').eid
-        self.req.form = {'eid': str(eid), '__type:%s'%eid: 'BlogEntry',
-                         '__action_delete': ''}
-        path, params = self.expect_redirect_publish()
-        self.assertEquals(path, 'blogentry')
-        self.assertEquals(params, {u'__message': u'entity deleted'})
-        eid = self.add_entity('EmailAddress', address=u'hop@logilab.fr').eid
-        self.req.execute('SET X use_email E WHERE E eid %(e)s, X eid %(x)s',
-                         {'x': self.user.eid, 'e': eid}, 'x')
-        self.commit()
-        self.req.form = {'eid': str(eid), '__type:%s'%eid: 'EmailAddress',
-                         '__action_delete': ''}
-        path, params = self.expect_redirect_publish()
-        self.assertEquals(unquote(path), 'euser/'+self.user.login)
-        self.assertEquals(params, {u'__message': u'entity deleted'})
-        eid1 = self.add_entity('BlogEntry', title=u'hop', content=u'hop').eid
-        eid2 = self.add_entity('EmailAddress', address=u'hop@logilab.fr').eid
-        self.req.form = {'eid': [str(eid1), str(eid2)],
-                         '__type:%s'%eid1: 'BlogEntry',
-                         '__type:%s'%eid2: 'EmailAddress',
-                         '__action_delete': ''}
-        path, params = self.expect_redirect_publish()
-        self.assertEquals(path, 'view')
-        self.assertEquals(params, {u'__message': u'entities deleted'})
-
-
-    def test_nonregr_multiple_empty_email_addr(self):
-        gueid = self.req.execute('CWGroup G WHERE G name "users"')[0][0]
-        self.req.form = {'eid': ['X', 'Y'],
-
-                         '__type:X': 'CWUser',
-                         'login:X': u'adim', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
-                         'in_group:X': gueid, 'edits-in_group:X': INTERNAL_FIELD_VALUE,
-
-                         '__type:Y': 'EmailAddress',
-                         'address:Y': u'', 'edits-address:Y': '',
-                         'alias:Y': u'', 'edits-alias:Y': '',
-                         'use_email:X': 'Y', 'edits-use_email:X': INTERNAL_FIELD_VALUE,
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
-
-
-    def test_nonregr_rollback_on_validation_error(self):
-        self.skip('lax fix me')
-        p = self.create_user("doe")
-        # do not try to skip 'primary_email' for this test
-        old_skips = p.__class__.skip_copy_for
-        p.__class__.skip_copy_for = ()
-        try:
-            e = self.add_entity('EmailAddress', address=u'doe@doe.com')
-            self.req.execute('SET P use_email E, P primary_email E WHERE P eid %(p)s, E eid %(e)s',
-                         {'p' : p.eid, 'e' : e.eid})
-            self.req.form = {'__cloned_eid:X': p.eid,
-                             'eid': 'X', '__type:X': 'CWUser',
-                             'login': u'dodo', 'edits-login': u'dodo',
-                             'surname:X': u'Boom', 'edits-surname:X': u'',
-                             '__errorurl' : "whatever but required",
-                             }
-            # try to emulate what really happens in the web application
-            # 1/ validate form => EditController.publish raises a ValidationError
-            #    which fires a Redirect
-            # 2/ When re-publishing the copy form, the publisher implicitly commits
-            try:
-                self.app.publish('edit', self.req)
-            except Redirect:
-                self.req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
-                self.req.form['vid'] = 'copy'
-                self.app.publish('view', self.req)
-            rset = self.req.execute('CWUser P WHERE P surname "Boom"')
-            self.assertEquals(len(rset), 0)
-        finally:
-            p.__class__.skip_copy_for = old_skips
-
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/goa/test/unittest_metadata.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-from cubicweb.goa.testlib import *
-
-import time
-from mx.DateTime import DateTimeType
-from datetime import datetime
-from cubicweb.goa import db
-
-from google.appengine.api import datastore
-
-class Article(db.Model):
-    content = db.TextProperty()
-    synopsis = db.StringProperty(default='hello')
-
-class Blog(db.Model):
-    diem = db.DateProperty(required=True, auto_now_add=True)
-    title = db.StringProperty(required=True)
-    content = db.TextProperty()
-    talks_about = db.ReferenceProperty(Article)
-    cites = db.SelfReferenceProperty()
-
-
-class MetaDataTC(GAEBasedTC):
-    MODEL_CLASSES = (Article, Blog)
-
-    def setUp(self):
-        GAEBasedTC.setUp(self)
-        self.req = self.request()
-        self.a = self.add_entity('Article')
-        self.p = self.add_entity('CWProperty', pkey=u'ui.language', value=u'en')
-        self.session.commit()
-
-    def _test_timestamp(self, entity, attr, sleep=0.1):
-        timestamp = getattr(entity, attr)
-        self.failUnless(timestamp)
-        self.assertIsInstance(timestamp, DateTimeType)
-        self.assertIsInstance(entity.to_gae_model()['s_'+attr], datetime)
-        time.sleep(sleep)
-        if entity.id == 'Article':
-            entity.set_attributes(content=u'zou')
-        else:
-            entity.set_attributes(value=u'en')
-        self.session.commit()
-        return timestamp
-
-    def test_creation_date_dbmodel(self):
-        cdate = self._test_timestamp(self.a, 'creation_date')
-        self.assertEquals(cdate, self.a.creation_date)
-
-    def test_creation_date_yams(self):
-        cdate = self._test_timestamp(self.p, 'creation_date')
-        self.assertEquals(cdate, self.p.creation_date)
-
-    def test_modification_date_dbmodel(self):
-        mdate = self._test_timestamp(self.a, 'modification_date', sleep=1)
-        a = self.execute('Any X WHERE X eid %(x)s', {'x': self.a.eid}, 'x').get_entity(0, 0)
-        self.failUnless(mdate < a.modification_date, (mdate, a.modification_date))
-
-    def test_modification_date_yams(self):
-        mdate = self._test_timestamp(self.p, 'modification_date', sleep=1)
-        p = self.execute('Any X WHERE X eid %(x)s', {'x': self.p.eid}, 'x').get_entity(0, 0)
-        self.failUnless(mdate < p.modification_date, (mdate, p.modification_date))
-
-    def _test_owned_by(self, entity):
-        self.assertEquals(len(entity.owned_by), 1)
-        owner = entity.owned_by[0]
-        self.assertIsInstance(owner, db.Model)
-        dbmodel = entity.to_gae_model()
-        self.assertEquals(len(dbmodel['s_owned_by']), 1)
-        self.assertIsInstance(dbmodel['s_owned_by'][0], datastore.Key)
-
-    def test_owned_by_dbmodel(self):
-        self._test_owned_by(self.a)
-
-    def test_owned_by_yams(self):
-        self._test_owned_by(self.p)
-
-    def _test_created_by(self, entity):
-        self.assertEquals(len(entity.created_by), 1)
-        creator = entity.created_by[0]
-        self.assertIsInstance(creator, db.Model)
-        self.assertIsInstance(entity.to_gae_model()['s_created_by'], datastore.Key)
-
-    def test_created_by_dbmodel(self):
-        self._test_created_by(self.a)
-
-    def test_created_by_dbmodel(self):
-        self._test_created_by(self.p)
-
-    def test_user_owns_dbmodel(self):
-        self.failUnless(self.req.user.owns(self.a.eid))
-
-    def test_user_owns_yams(self):
-        self.failUnless(self.req.user.owns(self.p.eid))
-
-    def test_is_relation(self):
-        en = self.execute('Any EN WHERE E name EN, X is E, X eid %(x)s', {'x': self.a.eid}, 'x')[0][0]
-        self.assertEquals(en, 'Article')
-        en = self.execute('Any EN WHERE E name EN, X is E, X eid %(x)s', {'x': self.p.eid}, 'x')[0][0]
-        self.assertEquals(en, 'CWProperty')
-        en = self.execute('Any EN WHERE E name EN, X is E, X eid %(x)s', {'x': self.req.user.eid}, 'x')[0][0]
-        self.assertEquals(en, 'CWUser')
-
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/goa/test/unittest_rql.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,628 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-from cubicweb.goa.testlib import *
-
-from cubicweb import Binary
-
-from logilab.common.testlib import unittest_main
-from mx.DateTime import now, today, DateTimeType
-import rql
-
-from google.appengine.api.datastore_types import Blob, Text
-
-# stored procedure definition #################################################
-
-from rql.utils import register_function, FunctionDescr
-
-class itemtype_sort_value(FunctionDescr):
-    supported_backends = ('sqlite',)
-    rtype = 'Int'
-
-try:
-    register_function(itemtype_sort_value)
-except AssertionError:
-    pass
-
-def init_sqlite_connexion(cnx):
-    def itemtype_sort_value(text):
-        return {"personal":2, "business":1}[text]
-    cnx.create_function("ITEMTYPE_SORT_VALUE", 1, itemtype_sort_value)
-
-from cubicweb.server import SQL_CONNECT_HOOKS
-sqlite_hooks = SQL_CONNECT_HOOKS.setdefault('sqlite', [])
-sqlite_hooks.append(init_sqlite_connexion)
-
-# end stored procedure definition #############################################
-
-class Article(db.Model):
-    content = db.TextProperty()
-    synopsis = db.StringProperty(default=u'hello')
-
-class Blog(db.Model):
-    diem = db.DateProperty(required=True, auto_now_add=True)
-    content = db.TextProperty()
-    itemtype = db.StringProperty(required=True, choices=(u'personal', u'business'))
-    talks_about = db.ReferenceProperty(Article)
-    cites = db.SelfReferenceProperty()
-    data = db.BlobProperty()
-
-
-class RQLTest(GAEBasedTC):
-    MODEL_CLASSES = (Article, Blog)
-
-    def setUp(self):
-        GAEBasedTC.setUp(self)
-        # hack to make talks_about cardinality to ** instead of ?*
-        self.schema.rschema('talks_about').set_rproperty('Blog', 'Article',
-                                                         'cardinality', '**')
-        self.req = self.request()
-        self.article = self.add_entity('Article', content=u'very interesting')
-        self.blog = self.add_entity('Blog', itemtype=u'personal', content=u'hop')
-        self.execute('SET X talks_about Y WHERE X eid %(x)s, Y eid %(y)s',
-                     {'x': self.blog.eid, 'y': self.article.eid})
-        self.commit()
-
-    def _check_rset_size(self, rset, row, col):
-        self.assertEquals(len(rset), row)
-        self.assertEquals(len(rset[0]), col)
-        self.assertEquals(len(rset.description), row)
-        self.assertEquals(len(rset.description[0]), col)
-
-    def _check_blog_rset(self, rset):
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.description[0][0], 'Blog')
-        self.assertEquals(rset[0][0], self.blog.eid)
-        self.assertEquals(rset.get_entity(0, 0).eid, self.blog.eid)
-
-    def test_0_const(self):
-        rset = self.req.execute('Any 1')
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset[0][0], 1)
-        self.assertEquals(rset.description[0][0], 'Int')
-
-    def test_0_now_const(self):
-        rset = self.req.execute('Any NOW')
-        self._check_rset_size(rset, 1, 1)
-        self.assertIsInstance(rset[0][0], DateTimeType)
-        self.assertEquals(rset.description[0][0], 'Datetime')
-
-    def test_0_today_const(self):
-        rset = self.req.execute('Any TODAY')
-        self._check_rset_size(rset, 1, 1)
-        self.assertIsInstance(rset[0][0], DateTimeType)
-        self.assertEquals(rset[0][0], today())
-        self.assertEquals(rset.description[0][0], 'Date')
-
-
-    def test_1_eid(self):
-        rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': self.blog.eid})
-        self._check_blog_rset(rset)
-        rset = self.req.execute('Any X WHERE X eid "%s"' % self.blog.eid)
-        self._check_blog_rset(rset)
-
-    def test_1_eid_eid(self):
-        rset = self.req.execute('Any X,Y WHERE X eid %(x)s, Y eid %(y)s', {'x': self.blog.eid,
-                                                                           'y': self.article.eid})
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset.description[0], ('Blog', 'Article'))
-        self.assertEquals(rset[0][0], self.blog.eid)
-        self.assertEquals(rset[0][1], self.article.eid)
-
-    def test_1_eid_with_is(self):
-        self.assertRaises(rql.TypeResolverException,
-                          self.req.execute, 'Any X WHERE X eid %(x)s, X is Article', {'x': self.blog.eid})
-        rset = self.req.execute('Any X WHERE X eid %(x)s, X is Blog', {'x': self.blog.eid})
-        self._check_blog_rset(rset)
-
-    def test_1_is(self):
-        rset = self.req.execute('Any X WHERE X is Blog')
-        self._check_blog_rset(rset)
-        blog2 = Blog(itemtype=u'personal', content=u'hop')
-        blog2.put()
-        rset = self.req.execute('Any X WHERE X is Blog')
-        self.assertEquals(len(rset), 2)
-        self.assertEquals(rset.description, [('Blog',), ('Blog',)])
-
-
-    def test_2_attribute_selection_1(self):
-        rset = self.req.execute('Any X,D,C WHERE X is Blog, X diem D, X content C')
-        self._check_rset_size(rset, 1, 3)
-        self.assertEquals(rset[0], [self.blog.eid, today(), u'hop'])
-        self.assertEquals(rset.description[0], ('Blog', 'Date', 'String'))
-        self.assertIsInstance(rset[0][1], DateTimeType)
-
-    def test_2_attribute_selection_2(self):
-        rset = self.req.execute('Any D,C WHERE X is Blog, X diem D, X content C')
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset[0], [today(), u'hop'])
-        self.assertEquals(rset.description[0], ('Date', 'String'))
-
-    def test_2_attribute_selection_binary(self):
-        rset = self.req.execute('Any D WHERE X is Blog, X data D')
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset[0], [None])
-        self.assertEquals(rset.description[0], ('Bytes',))
-        self.blog['data'] = Binary('raw data')
-        self.blog.put()
-        rset = self.req.execute('Any D WHERE X is Blog, X data D')
-        self._check_rset_size(rset, 1, 1)
-        self.assertIsInstance(rset[0][0], Binary)
-        value = rset[0][0].getvalue()
-        self.assertIsInstance(value, str)
-        self.failIf(isinstance(value, Blob))
-        self.assertEquals(value, 'raw data')
-        self.assertEquals(rset.description[0], ('Bytes',))
-
-    def test_2_attribute_selection_long_text(self):
-        self.blog['content'] = text = 'a'*501
-        self.blog.put()
-        rset = self.req.execute('Any C WHERE X is Blog, X content C')
-        self._check_rset_size(rset, 1, 1)
-        self.assertIsInstance(rset[0][0], unicode)
-        self.failIf(isinstance(rset[0][0], Text))
-        self.assertEquals(rset[0][0], text)
-
-    def test_2_attribute_selection_transformation(self):
-        rset = self.req.execute('Any X,UPPER(C) WHERE X is Blog, X content C')
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset[0], [self.blog.eid, u'HOP'])
-        self.assertEquals(rset.description[0], ('Blog', 'String',))
-
-
-    def test_3_attribute_restriction(self):
-        rset = self.req.execute('Any X WHERE X itemtype "personal"')
-        self._check_blog_rset(rset)
-        rset = self.req.execute('Any X WHERE X itemtype "business"')
-        self.assertEquals(len(rset), 0)
-
-    def test_3_ambigous_attribute_restriction_1(self):
-        rset = self.req.execute('Any X WHERE X content "hello"')
-        self.assertEquals(len(rset), 0)
-
-    def test_3_ambigous_attribute_restriction_2(self):
-        rset = self.req.execute('Any X WHERE X content "hop"')
-        self._check_blog_rset(rset)
-
-    def test_3_ambigous_attribute_restriction_3(self):
-        article = Article(content=u'hop')
-        article.put()
-        rset = self.req.execute('Any X WHERE X content "hop"')
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([r[0] for r in rset], [self.blog.eid, article.eid])
-        self.assertUnorderedIterableEquals([r[0] for r in rset.description], ['Blog', 'Article'])
-
-    def test_3_incoherant_attribute_restriction(self):
-        rset = self.req.execute('Any X WHERE X eid %(x)s, X content "hola"',
-                                {'x': self.blog.eid})
-        self.assertEquals(len(rset), 0)
-
-    def test_3_multiple_attribute_restriction(self):
-        rset = self.req.execute('Any X WHERE X content "hop", X itemtype "personal"')
-        self._check_blog_rset(rset)
-
-    def test_3_incoherant_multiple_attribute_restriction(self):
-        rset = self.req.execute('Any X WHERE X content "hip", X itemtype "personal"')
-        self.assertEquals(len(rset), 0)
-
-    def test_3_today_attribute_restriction(self):
-        rset = self.req.execute('Any X WHERE X diem < TODAY')
-        self.assertEquals(len(rset), 0)
-        rset = self.req.execute('Any X WHERE X diem <= TODAY')
-        self._check_blog_rset(rset)
-        rset = self.req.execute('Any X WHERE X diem > TODAY')
-        self.assertEquals(len(rset), 0)
-        rset = self.req.execute('Any X WHERE X diem >= TODAY')
-        self._check_blog_rset(rset)
-
-    def test_3_now_attribute_restriction(self):
-        rset = self.req.execute('Any X WHERE X diem < NOW')
-        self._check_blog_rset(rset)
-        rset = self.req.execute('Any X WHERE X diem <= NOW')
-        self._check_blog_rset(rset)
-        rset = self.req.execute('Any X WHERE X diem > NOW')
-        self.assertEquals(len(rset), 0)
-        rset = self.req.execute('Any X WHERE X diem >= NOW')
-        self.assertEquals(len(rset), 0)
-
-    def test_3_in_attribute_restriction(self):
-        self.skip('missing actual gae support, retry latter')
-        article2 = Article(content=u'hip')
-        rset = self.req.execute('Any X WHERE X content IN ("hop", "hip")')
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([r[0] for r in rset], [self.blog.eid, article.eid])
-        self.assertUnorderedIterableEquals([r[0] for r in rset.description], ['Blog', 'Article'])
-
-    def test_3_like(self):
-        repo = self.config.repository()
-        versions = repo.get_versions()
-        self.assertEquals(versions.keys(), ['cubicweb'])
-
-    def _setup_relation_description(self):
-        self.article2 = self.add_entity('Article', content=u'hop')
-        self.blog2 = self.add_entity('Blog', itemtype=u'personal', content=u'hip')
-        self.execute('SET X talks_about Y WHERE X eid %(x)s, Y eid %(y)s',
-                     {'x': self.blog2.eid, 'y': self.article2.eid})
-        self.blog3 = self.add_entity('Blog', itemtype=u'business', content=u'hep')
-        self.commit()
-
-    def test_4_relation_restriction_1(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X WHERE X talks_about Y')
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([r[0] for r in rset],
-                             [self.blog.eid, self.blog2.eid])
-        self.assertUnorderedIterableEquals([r[0] for r in rset.description], ['Blog', 'Blog'])
-
-    def test_4_relation_restriction_2(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any Y WHERE X talks_about Y')
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([r[0] for r in rset],
-                             [self.article.eid, self.article2.eid])
-        self.assertUnorderedIterableEquals([r[0] for r in rset.description],
-                             ['Article', 'Article'])
-
-    def test_4_relation_restriction_3(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,Y WHERE X talks_about Y')
-        self._check_rset_size(rset, 2, 2)
-        self.assertUnorderedIterableEquals([tuple(r) for r in rset],
-                             [(self.blog.eid, self.article.eid),
-                              (self.blog2.eid, self.article2.eid)])
-        self.assertUnorderedIterableEquals([tuple(r) for r in rset.description],
-                             [('Blog', 'Article'),
-                              ('Blog', 'Article')])
-
-    def test_4_relation_restriction_4(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,Y WHERE X talks_about Y, X eid %(x)s',
-                                {'x': self.blog.eid})
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset[0], [self.blog.eid, self.article.eid])
-        self.assertUnorderedIterableEquals(rset.description[0], ['Blog', 'Article'])
-
-    def test_4_relation_restriction_5(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,Y WHERE X talks_about Y, Y eid %(x)s',
-                                {'x': self.article.eid})
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset[0], [self.blog.eid, self.article.eid])
-        self.assertUnorderedIterableEquals(rset.description[0], ['Blog', 'Article'])
-
-    def test_4_relation_subject_restriction(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,Y WHERE X talks_about Y, X content %(c)s',
-                                {'c': 'hop'})
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset[0], [self.blog.eid, self.article.eid])
-        self.assertUnorderedIterableEquals(rset.description[0], ['Blog', 'Article'])
-
-    def test_4_relation_object_restriction(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X WHERE X is Blog, X talks_about Y, Y content %(c)s',
-                                {'c': 'very interesting'})
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset[0], [self.blog.eid])
-        self.assertUnorderedIterableEquals(rset.description[0], ['Blog'])
-
-    def test_4_relation_subject_object_restriction(self):
-        article2 = self.add_entity('Article', content=u'very interesting')
-        rset = self.req.execute('Any X,XC WHERE X is Blog, X content XC, X content %(xc)s, '
-                                'X talks_about Y, Y content %(c)s',
-                                {'xc': 'hop', 'c': 'very interesting'})
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset[0], [self.blog.eid, self.blog.content])
-        self.assertUnorderedIterableEquals(rset.description[0], ['Blog', 'String'])
-
-    def test_4_relation_subject_object_restriction_no_res(self):
-        article2 = self.add_entity('Article', content=u'very interesting')
-        rset = self.req.execute('Any X,XC WHERE X is Blog, X content XC, X content %(xc)s, '
-                                'X talks_about Y, Y content %(c)s',
-                                {'xc': 'hip', 'c': 'very interesting'})
-        self.assertEquals(len(rset), 0)
-
-    def test_4_relation_subject_object_restriction_no_res_2(self):
-        rset = self.req.execute('Any X,XC WHERE X is Blog, X content XC, X content %(xc)s, '
-                                'X talks_about Y, Y content %(c)s',
-                                {'xc': 'hop', 'c': 'not interesting'})
-        self.assertEquals(len(rset), 0)
-
-    def test_4_relation_restriction_7(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any XC,XD,YC WHERE X talks_about Y, Y eid %(x)s,'
-                                'X content XC, X diem XD, Y content YC',
-                                {'x': self.article.eid})
-        self._check_rset_size(rset, 1, 3)
-        self.assertEquals(rset[0], [self.blog.content, self.blog.diem, self.article.content])
-        self.assertUnorderedIterableEquals(rset.description[0], ['String', 'Date', 'String'])
-
-    def test_4_relation_restriction_8(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,Y WHERE X cites Y, Y eid %(x)s', {'x': self.blog.eid})
-        self.assertEquals(len(rset), 0)
-
-    def test_4_relation_restriction_9(self):
-        article2 = self.add_entity('Article', content=u'hop')
-        self.req.execute('SET X talks_about Y WHERE X eid %(x)s, Y eid %(y)s',
-                         {'x': self.blog.eid, 'y': article2.eid})
-        rset = self.req.execute('Any X,Y WHERE X talks_about Y, X eid %(x)s, Y eid %(y)s',
-                                {'x': self.blog.eid, 'y': article2.eid})
-        self._check_rset_size(rset, 1, 2)
-
-    def test_4_ambiguous_subject_relation(self):
-        ye = self.add_entity('YamsEntity')
-        self.req.execute('SET X ambiguous_relation Y WHERE X eid %(x)s, Y eid %(y)s',
-                         {'x': ye.eid, 'y': self.blog.eid})
-        self.req.execute('SET X ambiguous_relation Y WHERE X eid %(x)s, Y eid %(y)s',
-                         {'x': ye.eid, 'y': self.article.eid})
-        self.commit()
-        #ye = self.vreg.etype_class('YamsEntity ')(req, None)
-        #ye.to_gae_model()['s_ambiguous_relation'] = [self.blog.key(), self.article.key()]
-        #ye.put()
-        rset = self.req.execute('Any X WHERE Y ambiguous_relation X')
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([r[0] for r in rset], [self.blog.eid, self.article.eid])
-        self.assertUnorderedIterableEquals([r[0] for r in rset.description], ['Blog', 'Article'])
-        rset = self.req.execute('Any X WHERE Y ambiguous_relation X, Y eid %(x)s', {'x': ye.eid})
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([r[0] for r in rset], [self.blog.eid, self.article.eid])
-        self.assertUnorderedIterableEquals([r[0] for r in rset.description], ['Blog', 'Article'])
-
-    def test_4_relation_selection(self):
-        req = self.request()
-        rset = req.execute('Any N WHERE G content N, U talks_about G, U eid %(u)s', {'u': self.blog.eid})
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset[0][0], 'very interesting')
-
-
-    def test_5_orderby(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,XC ORDERBY XC WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 3, 2)
-        self.assertEquals(rset.rows,
-                          [[self.blog3.eid, 'hep'],
-                           [self.blog2.eid, 'hip'],
-                           [self.blog.eid, 'hop']])
-
-    def test_5_orderby_desc(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,XC ORDERBY XC DESC WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 3, 2)
-        self.assertEquals(rset.rows,
-                          [[self.blog.eid, 'hop'],
-                           [self.blog2.eid, 'hip'],
-                           [self.blog3.eid, 'hep']])
-
-    def test_5_orderby_several_terms(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,XC,XI ORDERBY XI,XC WHERE X is Blog, X content XC, X itemtype XI')
-        self._check_rset_size(rset, 3, 3)
-        self.assertEquals(rset.rows,
-                          [[self.blog3.eid, 'hep', 'business'],
-                           [self.blog2.eid, 'hip', 'personal'],
-                           [self.blog.eid, 'hop', 'personal']])
-
-    def test_5_orderby_several_terms_mixed_implicit(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,XC,XI ORDERBY XI,XC DESC WHERE X is Blog, X content XC, X itemtype XI')
-        self._check_rset_size(rset, 3, 3)
-        self.assertEquals(rset.rows,
-                          [[self.blog3.eid, 'hep', 'business'],
-                           [self.blog.eid, 'hop', 'personal'],
-                           [self.blog2.eid, 'hip', 'personal']])
-
-    def test_5_orderby_several_terms_explicit_order(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,XC,XI ORDERBY XI DESC,XC DESC WHERE X is Blog, X content XC, X itemtype XI')
-        self._check_rset_size(rset, 3, 3)
-        self.assertEquals(rset.rows,
-                          [[self.blog.eid, 'hop', 'personal'],
-                           [self.blog2.eid, 'hip', 'personal'],
-                           [self.blog3.eid, 'hep', 'business']])
-
-    def test_5_orderby_several_terms_mixed_order(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X,XC,XI ORDERBY XI ASC,XC DESC WHERE X is Blog, X content XC, X itemtype XI')
-        self._check_rset_size(rset, 3, 3)
-        self.assertEquals(rset.rows,
-                          [[self.blog3.eid, 'hep', 'business'],
-                           [self.blog.eid, 'hop', 'personal'],
-                           [self.blog2.eid, 'hip', 'personal']])
-
-
-    def test_5_orderby_lower(self):
-        blog2 = self.add_entity('Blog', itemtype=u'business', content=u'Hup')
-        rset = self.req.execute('Any X ORDERBY LOWER(XC) '
-                                'WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [[self.blog.eid], [blog2.eid]])
-        rset = self.req.execute('Any X ORDERBY LOWER(XC) DESC'
-                                'WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [[blog2.eid], [self.blog.eid]])
-
-    def test_5_orderby_stored_proc(self):
-        blog2 = self.add_entity('Blog', itemtype=u'business', content=u'hop')
-        rset = self.req.execute('Any X ORDERBY ITEMTYPE_SORT_VALUE(XIT) '
-                                'WHERE X is Blog, X itemtype XIT')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [[blog2.eid], [self.blog.eid]])
-        rset = self.req.execute('Any X ORDERBY ITEMTYPE_SORT_VALUE(XIT) DESC'
-                                'WHERE X is Blog, X itemtype XIT')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [[self.blog.eid], [blog2.eid]])
-
-
-    def test_6_limit(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any X LIMIT 2 WHERE X is Blog')
-        self._check_rset_size(rset, 2, 1)
-
-    def test_6_offset(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any XC ORDERBY XC DESC OFFSET 1 WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [['hip'], ['hep']])
-
-    def test_6_limit_and_orderby(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any XC ORDERBY XC LIMIT 2 WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [['hep'], ['hip']])
-
-    def test_6_limit_offset_and_orderby(self):
-        self._setup_relation_description()
-        rset = self.req.execute('Any XC ORDERBY XC LIMIT 2 OFFSET 0 WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [['hep'], ['hip']])
-        rset = self.req.execute('Any XC ORDERBY XC LIMIT 2 OFFSET 1 WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 2, 1)
-        self.assertEquals(rset.rows, [['hip'], ['hop']])
-        rset = self.req.execute('Any XC ORDERBY XC LIMIT 2 OFFSET 2 WHERE X is Blog, X content XC')
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [['hop']])
-        rset = self.req.execute('Any XC ORDERBY XC LIMIT 2 OFFSET 3 WHERE X is Blog, X content XC')
-        self.failIf(rset)
-
-
-    def test_7_simple_datetimecast(self):
-        self._setup_relation_description()
-        _today = today()
-        _tomorrow = _today + 1
-        rset = self.req.execute('Any X WHERE X is Blog, X creation_date >= "%s"'
-                                % _tomorrow.strftime('%Y-%m-%d'))
-        self.failUnless(len(rset) == 0)
-        rset = self.req.execute('Any X WHERE X is Blog, X creation_date >= "%s"'
-                                % _today.strftime('%Y-%m-%d'))
-        self._check_rset_size(rset, 3, 1)
-        rset = self.req.execute('Any X WHERE X is Blog, X creation_date <= "%s"'
-                                % _tomorrow.strftime('%Y-%m-%d'))
-        self._check_rset_size(rset, 3, 1)
-
-    def test_7_identity_relation(self):
-        rset = self.req.execute('Any X WHERE X identity Y, X eid %(x)s, Y eid %(y)s',
-                                {'x': self.user.eid, 'y': self.user.eid})
-        self._check_rset_size(rset, 1, 1)
-        rset = self.req.execute('Any Y WHERE X identity Y, X eid %(x)s',
-                                {'x': self.user.eid})
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [[self.user.eid]])
-        blog2 = self.add_entity('Blog', itemtype=u'personal', content=u'hip')
-        rset = self.req.execute('Any X WHERE X identity Y, X eid %(x)s, Y eid %(y)s',
-                                {'x': self.blog.eid, 'y': blog2.eid})
-        self.failIf(rset)
-
-    def test_8_not_relation_1(self):
-        rset = self.req.execute('Any X WHERE X identity U, NOT U in_group G, '
-                                'G name "guests", X eid %(x)s, U eid %(u)s',
-                                {'x': self.user.eid, 'u': self.user.eid})
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [[self.user.eid]])
-
-    def test_8_not_relation_linked_subject(self):
-        rset = self.req.execute('Any X WHERE NOT X talks_about Y, Y eid %(y)s',
-                                {'y': self.article.eid})
-        self.failIf(rset)
-        blog2 = self.add_entity('Blog', content=u'hop', itemtype=u'personal')
-        self.commit()
-        rset = self.req.execute('Any X WHERE NOT X talks_about Y, Y eid %(y)s',
-                                {'y': self.article.eid})
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [[blog2.eid]])
-
-    def test_8_not_relation_linked_object(self):
-        rset = self.req.execute('Any Y WHERE NOT X talks_about Y, X eid %(x)s',
-                                {'x': self.blog.eid})
-        self.failIf(rset)
-        article2 = self.add_entity('Article', content=u'hop')
-        self.commit()
-        rset = self.req.execute('Any Y WHERE NOT X talks_about Y, X eid %(x)s',
-                                {'x': self.blog.eid})
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [[article2.eid]])
-
-    def test_8_not_relation_linked_attr(self):
-        self.skip('not yet implemented')
-        # TODO: this should generated
-        # Query(X)[s_talks_about] > "hop" || Query(X)[s_talks_about] < "hop"
-        article2 = self.add_entity('Article', content=u'hop')
-        self.req.execute('SET X talks_about Y WHERE X eid %(x)s, Y eid %(y)s',
-                         {'x': self.blog.eid, 'y': article2.eid})
-        self.commit()
-        rset = self.req.execute('Any X WHERE NOT X talks_about Y, Y content "hop"')
-        self._check_rset_size(rset, 1, 2)
-        self.assertEquals(rset.rows, [[self.blog.eid, self.article.eid]])
-
-    def test_8_not_relation_unlinked_subject(self):
-        blog2 = self.add_entity('Blog', content=u'hop', itemtype=u'personal')
-        self.commit()
-        rset = self.req.execute('Any X WHERE NOT X talks_about Y')
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [[blog2.eid]])
-
-    def test_8_not_relation_unlinked_object(self):
-        article2 = self.add_entity('Article', content=u'hop')
-        self.commit()
-        rset = self.req.execute('Any Y WHERE NOT X talks_about Y')
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [[article2.eid]])
-
-    def test_8_not_relation_final_1(self):
-        rset = self.req.execute('Any G WHERE G is CWGroup, NOT G name "guests"')
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([g.name for g in rset.entities()],
-                                           ['users', 'managers'])
-
-    def test_8_not_relation_final_2(self):
-        rset = self.req.execute('Any GN WHERE G is CWGroup, NOT G name "guests", G name GN')
-        self._check_rset_size(rset, 2, 1)
-        self.assertUnorderedIterableEquals([gn for gn, in rset.rows],
-                                           ['users', 'managers'])
-
-
-    def test_9_exists(self):
-        blog2 = self.add_entity('Article', content=u'hop')
-        article2 = self.add_entity('Article', content=u'hop')
-        self.req.execute('SET X talks_about Y WHERE X eid %(x)s, Y eid %(y)s',
-                         {'x': self.blog.eid, 'y': article2.eid})
-        self.commit()
-        rset = self.req.execute('Any X WHERE X is Blog, EXISTS(X talks_about Y)')
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset.rows, [[self.blog.eid]])
-
-
-    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()
-        rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': self.blog.eid})
-        self.assertEquals(len(rset), 0)
-
-    def test_nonregr_inlined_relation(self):
-        eid = self.execute('INSERT YamsEntity X: X inlined_relation Y WHERE Y eid %(y)s',
-                           {'y': self.blog.eid})[0][0]
-        self.commit()
-        rset = self.execute('Any X WHERE Y inlined_relation X, Y eid %(y)s', {'y': eid})
-        self._check_rset_size(rset, 1, 1)
-        self.assertEquals(rset[0][0], self.blog.eid)
-
-if __name__ == '__main__':
-    unittest_main()
--- a/goa/test/unittest_schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-from cubicweb.goa.testlib import *
-
-class Article(db.Model):
-    content = db.TextProperty()
-    synopsis = db.StringProperty(default='hello')
-
-class Blog(db.Model):
-    diem = db.DateProperty(required=True, auto_now_add=True)
-    title = db.StringProperty(required=True)
-    content = db.TextProperty()
-    talks_about = db.ReferenceProperty(Article)
-    cites = db.SelfReferenceProperty()
-
-
-class SomeViewsTC(GAEBasedTC):
-    MODEL_CLASSES = (Article, Blog)
-
-    def test_entities_and_relation(self):
-        schema = self.schema
-        self.assertSetEquals(set(str(e) for e in schema.entities()),
-                             set(('Boolean', 'Bytes', 'Date', 'Datetime', 'Float',
-                              'Decimal',
-                              'Int', 'Interval', 'Password', 'String', 'Time',
-                              'CWEType', 'CWGroup', 'CWPermission', 'CWProperty', 'CWRType',
-                              'CWUser', 'EmailAddress',
-                              'RQLExpression', 'State', 'Transition', 'TrInfo',
-                              'Article', 'Blog', 'YamsEntity')))
-        self.assertSetEquals(set(str(e) for e in schema.relations()),
-                             set(('add_permission', 'address', 'alias', 'allowed_transition',
-                                  'ambiguous_relation', 'canonical', 'cites',
-                                  'comment', 'comment_format', 'condition', 'content',
-                                  'created_by', 'creation_date', 'delete_permission',
-                                  'description', 'description_format', 'destination_state',
-                                  'diem', 'eid', 'expression', 'exprtype', 'final', 'firstname',
-                                  'for_user', 'from_state', 'fulltext_container', 'has_text',
-                                  'identical_to', 'identity', 'in_group', 'initial_state',
-                                  'inlined', 'inlined_relation', 'is', 'is_instance_of',
-                                  'label', 'last_login_time', 'login',
-                                  'mainvars', 'meta', 'modification_date', 'name', 'owned_by', 'pkey', 'primary_email',
-                                  'read_permission', 'require_group', 'state_of', 'surname', 'symmetric',
-                                  'synopsis', 'talks_about', 'title', 'to_state', 'transition_of',
-                                  'update_permission', 'use_email', 'value')))
-
-    def test_dbmodel_imported(self):
-        eschema = self.schema['Blog']
-        orels = [str(e) for e in eschema.ordered_relations()]
-        # only relations defined in the class are actually ordered
-        orels, others = orels[:5], orels[5:]
-        self.assertEquals(orels,
-                          ['diem', 'title', 'content', 'talks_about', 'cites'])
-        self.assertUnorderedIterableEquals(others,
-                             ['eid', 'identity', 'owned_by', 'modification_date',
-                              'created_by', 'creation_date', 'is', 'is_instance_of'])
-        self.assertUnorderedIterableEquals((str(e) for e in eschema.object_relations()),
-                             ('ambiguous_relation', 'cites', 'identity', 'inlined_relation'))
-        eschema = self.schema['Article']
-        orels = [str(e) for e in eschema.ordered_relations()]
-        # only relations defined in the class are actually ordered
-        orels, others = orels[:2], orels[2:]
-        self.assertEquals(orels,
-                          ['content', 'synopsis'])
-        self.assertUnorderedIterableEquals(others,
-                             ['eid', 'identity', 'owned_by', 'modification_date',
-                              'created_by', 'creation_date', 'is', 'is_instance_of'])
-        self.assertUnorderedIterableEquals((str(e) for e in eschema.object_relations()),
-                             ('ambiguous_relation', 'talks_about', 'identity'))
-
-    def test_yams_imported(self):
-        eschema = self.schema['CWProperty']
-        # only relations defined in the class are actually ordered
-        orels = [str(e) for e in eschema.ordered_relations()]
-        orels, others = orels[:3], orels[3:]
-        self.assertEquals(orels,
-                          ['pkey', 'value', 'for_user'])
-        self.assertEquals(others,
-                          ['created_by', 'creation_date', 'eid', 'identity',
-                           'is', 'is_instance_of', 'modification_date', 'owned_by'])
-        self.assertUnorderedIterableEquals((str(e) for e in eschema.object_relations()),
-                             ('identity',))
-
-    def test_yams_ambiguous_relation(self):
-        rschema = self.schema['ambiguous_relation']
-        # only relations defined in the class are actually ordered
-        self.assertUnorderedIterableEquals((str(e) for e in rschema.subjects()),
-                             ('YamsEntity',))
-        self.assertUnorderedIterableEquals((str(e) for e in rschema.objects()),
-                             ('Blog', 'Article'))
-
-    def test_euser(self):
-        eschema = self.schema['CWUser']
-        # XXX pretend to have some relations it has not
-        self.assertEquals([str(e) for e in eschema.ordered_relations()],
-                          ['login', 'firstname', 'surname', 'last_login_time',
-                           'primary_email', 'use_email', 'in_group', 'created_by',
-                           'creation_date', 'eid', 'has_text', 'identity',
-                           'is', 'is_instance_of', 'modification_date',
-                           'owned_by'])
-        self.assertUnorderedIterableEquals((str(e) for e in eschema.object_relations()),
-                             ('owned_by', 'created_by', 'identity', 'for_user'))
-
-    def test_eid(self):
-        rschema = self.schema['eid']
-        self.assertEquals(rschema.objects(), ('Bytes',))
-        self.assertEquals(rschema.rproperty('Blog', 'Bytes', 'cardinality'), '?1')
-
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/goa/test/unittest_views.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-from cubicweb.goa.testlib import *
-
-from cubicweb.interfaces import ICalendarable
-
-
-class Blog(db.Model):
-    diem = db.DateProperty(required=True, auto_now_add=True)
-    title = db.StringProperty(required=True)
-    content = db.TextProperty()
-
-    __implements__ = (ICalendarable,)
-
-    @property
-    def start(self):
-        return self.diem
-
-    @property
-    def stop(self):
-        return self.diem
-
-    def matching_dates(self, begin, end):
-        """calendar views interface"""
-        mydate = self.diem
-        if mydate:
-            return [mydate]
-        return []
-
-
-class SomeViewsTC(GAEBasedTC):
-    MODEL_CLASSES = (Blog, )
-    from cubicweb.web.views import basecontrollers, baseviews, navigation, boxes, calendar
-    from data import views
-    LOAD_APP_MODULES = (basecontrollers, baseviews, navigation, boxes, calendar, views)
-
-    def setUp(self):
-        GAEBasedTC.setUp(self)
-        self.req = self.request()
-        self.blog = Blog(title=u'a blog', content=u'hop')
-        self.blog.put(self.req)
-
-    def test_hcal(self):
-        self.vreg['views'].render('hcal', self.req, rset=self.blog.rset)
-
-    def test_django_index(self):
-        self.vreg['views'].render('index', self.req, rset=None)
-
-for vid in ('primary', 'oneline', 'incontext', 'outofcontext', 'text'):
-    setattr(SomeViewsTC, 'test_%s'%vid, lambda self, vid=vid: self.blog.view(vid))
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/goa/testlib.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,198 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.common.testlib import TestCase, TestSkipped
-try:
-    import google.appengine
-except ImportError:
-    raise TestSkipped('Can not import google.appengine. Skip this module')
-
-import os, os.path as osp
-import time
-from shutil import copy
-
-# additional monkey patches necessary in regular cubicweb environment
-from cubicweb.server import rqlannotation
-from cubicweb.goa.overrides import rqlannotation as goarqlannotation
-rqlannotation.SQLGenAnnotator = goarqlannotation.SQLGenAnnotator
-rqlannotation.set_qdata = goarqlannotation.set_qdata
-
-from google.appengine.api import apiproxy_stub_map
-from google.appengine.api import datastore_file_stub
-from google.appengine.ext import db as gdb
-
-from cubicweb.devtools.fake import FakeRequest
-
-from cubicweb.goa import db, do_monkey_patch
-from cubicweb.goa.goavreg import GAEVRegistry
-from cubicweb.goa.goaconfig import GAEConfiguration
-from cubicweb.goa.dbinit import (create_user, create_groups, fix_entities,
-                                 init_persistent_schema, insert_versions)
-
-import logging
-logger = logging.getLogger()
-logger.setLevel(logging.CRITICAL)
-
-do_monkey_patch()
-
-class GAEBasedTC(TestCase):
-    APP_ID = u'test_app'
-    AUTH_DOMAIN = 'gmail.com'
-    LOGGED_IN_USER = u't...@example.com'  # set to '' for no logged in user
-    MODEL_CLASSES = None
-    LOAD_APP_MODULES = None
-    config = None
-    _DS_TEMPL_FILE = 'tmpdb-template'
-
-    def load_schema_hook(self, loader):
-        loader.import_yams_cube_schema('data')
-
-    @property
-    def DS_FILE(self):
-        return self.DS_TEMPL_FILE.replace('-template', '')
-
-    @property
-    def DS_TEMPL_FILE(self):
-        return self._DS_TEMPL_FILE + '_'.join(sorted(cls.__name__ for cls in self.MODEL_CLASSES))
-
-    def _set_ds_file(self, dsfile):
-        # Start with a fresh api proxy.
-        apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
-        # Use a fresh stub datastore.
-        stub = datastore_file_stub.DatastoreFileStub(self.APP_ID, dsfile,
-                                                     dsfile+'.history')
-        apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub)
-
-    def setUp(self):
-        # Ensure we're in UTC.
-        os.environ['TZ'] = 'UTC'
-        time.tzset()
-        if osp.exists(self.DS_TEMPL_FILE):
-            copy(self.DS_TEMPL_FILE, self.DS_FILE)
-            need_ds_init = False
-            self._set_ds_file(self.DS_FILE)
-        else:
-            need_ds_init = True
-            self._set_ds_file(self.DS_TEMPL_FILE)
-#         from google.appengine.api import mail_stub
-#         from google3.apphosting.api import urlfetch_stub
-#         from google3.apphosting.api import user_service_stub
-#         # Use a fresh stub UserService.
-#         apiproxy_stub_map.apiproxy.RegisterStub(
-#             'user', user_service_stub.UserServiceStub())
-        os.environ['AUTH_DOMAIN'] = self.AUTH_DOMAIN
-        os.environ['USER_EMAIL'] = self.LOGGED_IN_USER
-#         # Use a fresh urlfetch stub.
-#         apiproxy_stub_map.apiproxy.RegisterStub(
-#             'urlfetch', urlfetch_stub.URLFetchServiceStub())
-#         # Use a fresh mail stub.
-#         apiproxy_stub_map.apiproxy.RegisterStub(
-#             'mail', mail_stub.MailServiceStub())
-        if self.MODEL_CLASSES is None:
-            raise Exception('GAEBasedTC should set MODEL_CLASSES class attribute')
-        gdb._kind_map = {}
-        self.config = self.config or GAEConfiguration('toto')
-        self.config.init_log(logging.CRITICAL)
-        self.schema = self.config.load_schema(self.MODEL_CLASSES,
-                                              self.load_schema_hook)
-        self.vreg = GAEVregistry(self.config)
-        self.vreg.schema = self.schema
-        self.vreg.load_module(db)
-        from cubicweb.goa.appobjects import sessions
-        self.vreg.load_module(sessions)
-        from cubicweb.entities import authobjs, schemaobjs
-        self.vreg.load_module(authobjs)
-        self.vreg.load_module(schemaobjs)
-        if self.config['use-google-auth']:
-            from cubicweb.goa.appobjects import gauthservice
-            self.vreg.load_module(gauthservice)
-        if self.LOAD_APP_MODULES is not None:
-            for module in self.LOAD_APP_MODULES:
-                self.vreg.load_module(module)
-        for cls in self.MODEL_CLASSES:
-            self.vreg.register(cls)
-        self.session_manager = self.vreg.select('components', 'sessionmanager')
-        if need_ds_init:
-            # create default groups and create entities according to the schema
-            create_groups()
-            if not self.config['use-google-auth']:
-                create_user(self.LOGGED_IN_USER, 'toto', ('users', 'managers'))
-                self.session = self.login(self.LOGGED_IN_USER, 'toto')
-            else:
-                req = FakeRequest(vreg=self.vreg)
-                self.session = self.session_manager.open_session(req)
-            self.user = self.session.user()
-            ssession = self.config.repo_session(self.session.sessionid)
-            ssession.set_pool()
-            init_persistent_schema(ssession, self.schema)
-            insert_versions(ssession, self.config)
-            ssession.commit()
-            fix_entities(self.schema)
-            copy(self.DS_TEMPL_FILE, self.DS_FILE)
-            self._set_ds_file(self.DS_FILE)
-        else:
-            if not self.config['use-google-auth']:
-                self.session = self.login(self.LOGGED_IN_USER, 'toto')
-            else:
-                req = FakeRequest(vreg=self.vreg)
-                self.session = self.session_manager.open_session(req)
-            self.user = self.session.user()
-
-    def tearDown(self):
-        self.session.close()
-
-    def request(self):
-        req = FakeRequest(vreg=self.vreg)
-        req.set_connection(self.session, self.user)
-        return req
-
-    def add_entity(self, etype, **kwargs):
-        cu = self.session.cursor()
-        rql = 'INSERT %s X' % etype
-        if kwargs:
-            rql += ': %s' % ', '.join('X %s %%(%s)s' % (key, key) for key in kwargs)
-        rset = cu.execute(rql, kwargs)
-        return rset.get_entity(0, 0)
-
-    def execute(self, *args):
-        return self.session.cursor().execute(*args)
-
-    def commit(self):
-        self.session.commit()
-
-    def rollback(self):
-        self.session.rollback()
-
-    def create_user(self, login, groups=('users',), req=None):
-        assert not self.config['use-google-auth']
-        user = self.add_entity('CWUser', upassword=str(login), login=unicode(login))
-        cu = self.session.cursor()
-        cu.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}, 'x')
-        return user
-
-    def login(self, login, password=None):
-        assert not self.config['use-google-auth']
-        req = FakeRequest(vreg=self.vreg)
-        req.form['__login'] = login
-        req.form['__password'] = password or login
-        return self.session_manager.open_session(req)
--- a/goa/tools/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""lax tools cube
-
-"""
--- a/goa/tools/generate_schema_img.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-import sys
-from os.path import dirname, abspath, join
-from yams import schema2dot
-from cubicweb.web.views.schema import SKIP_TYPES
-
-APPLROOT = abspath(join(dirname(abspath(__file__)), '..'))
-
-try:
-    import custom
-except ImportError:
-    sys.path.insert(0, APPLROOT)
-    import custom
-
-
-schema = custom.SCHEMA
-skip_rels = ('owned_by', 'created_by', 'identity', 'is', 'is_instance_of')
-path = join(APPLROOT, 'data', 'schema.png')
-schema2dot.schema2dot(schema, path, #size=size,
-                      skiptypes=SKIP_TYPES)
-print 'generated', path
-path = join(APPLROOT, 'data', 'metaschema.png')
-schema2dot.schema2dot(schema, path)
-print 'generated', path
--- a/goa/tools/laxctl.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,269 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""provides all lax instances management commands into a single utility script
-
-"""
-__docformat__ = "restructuredtext en"
-
-import sys
-import os
-import os.path as osp
-import time
-import re
-import urllib2
-from urllib import urlencode
-from Cookie import SimpleCookie
-
-from logilab.common.clcommands import Command, register_commands, main_run
-
-from cubicweb.uilib import remove_html_tags
-from cubicweb.web.views.schema import SKIP_TYPES
-
-APPLROOT = osp.abspath(osp.join(osp.dirname(osp.abspath(__file__)), '..'))
-
-
-def initialize_vregistry(applroot):
-    # apply monkey patches first
-    from cubicweb.goa import do_monkey_patch
-    do_monkey_patch()
-    from cubicweb.goa.goavreg import GAEVregistry
-    from cubicweb.goa.goaconfig import GAEConfiguration
-    #WebConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
-    config = GAEConfiguration('toto', applroot)
-    vreg = GAEVregistry(config)
-    vreg.set_schema(config.load_schema())
-    return vreg
-
-def alistdir(directory):
-    return [osp.join(directory, f) for f in os.listdir(directory)]
-
-
-class LaxCommand(Command):
-    """base command class for all lax commands
-    creates vreg, schema and calls
-    """
-    min_args = max_args = 0
-
-    def run(self, args):
-        self.vreg = initialize_vregistry(APPLROOT)
-        self._run(args)
-
-
-class GenerateSchemaCommand(LaxCommand):
-    """generates the schema's png file"""
-    name = 'genschema'
-
-    def _run(self, args):
-        assert not args, 'no argument expected'
-        from yams import schema2dot
-        schema = self.vreg.schema
-        path = osp.join(APPLROOT, 'data', 'schema.png')
-        schema2dot.schema2dot(schema, path, #size=size,
-                              skiptypes=SKIP_TYPES)
-        print 'generated', path
-        path = osp.join(APPLROOT, 'data', 'metaschema.png')
-        schema2dot.schema2dot(schema, path)
-        print 'generated', path
-
-
-class PopulateDataDirCommand(LaxCommand):
-    """populate instance's data directory according to used cubes"""
-    name = 'populatedata'
-
-    def _run(self, args):
-        assert not args, 'no argument expected'
-        # first clean everything which is a symlink from the data directory
-        datadir = osp.join(APPLROOT, 'data')
-        if not osp.exists(datadir):
-            print 'created data directory'
-            os.mkdir(datadir)
-        for filepath in alistdir(datadir):
-            if osp.islink(filepath):
-                print 'removing', filepath
-                os.remove(filepath)
-        cubes = list(self.vreg.config.cubes()) + ['shared']
-        for templ in cubes:
-            templpath = self.vreg.config.cube_dir(templ)
-            templdatadir = osp.join(templpath, 'data')
-            if not osp.exists(templdatadir):
-                print 'no data provided by', templ
-                continue
-            for resource in os.listdir(templdatadir):
-                if resource == 'external_resources':
-                    continue
-                if not osp.exists(osp.join(datadir, resource)):
-                    print 'symlinked %s from %s' % (resource, templ)
-                    os.symlink(osp.join(templdatadir, resource),
-                               osp.join(datadir, resource))
-
-
-class NoRedirectHandler(urllib2.HTTPRedirectHandler):
-    def http_error_302(self, req, fp, code, msg, headers):
-        raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
-    http_error_301 = http_error_303 = http_error_307 = http_error_302
-
-
-class GetSessionIdHandler(urllib2.HTTPRedirectHandler):
-    def __init__(self, config):
-        self.config = config
-
-    def http_error_303(self, req, fp, code, msg, headers):
-        cookie = SimpleCookie(headers['Set-Cookie'])
-        sessionid = cookie['__session'].value
-        print 'session id', sessionid
-        setattr(self.config, 'cookie', '__session=' + sessionid)
-        return 1 # on exception should be raised
-
-
-class URLCommand(LaxCommand):
-    """abstract class for commands doing stuff by accessing the web instance
-    """
-    min_args = max_args = 1
-    arguments = '<site url>'
-
-    options = (
-        ('cookie',
-         {'short': 'C', 'type' : 'string', 'metavar': 'key=value',
-          'default': None,
-          'help': 'session/authentication cookie.'}),
-        ('user',
-         {'short': 'u', 'type' : 'string', 'metavar': 'login',
-          'default': None,
-          'help': 'user login instead of giving raw cookie string (require lax '
-          'based authentication).'}),
-        ('password',
-         {'short': 'p', 'type' : 'string', 'metavar': 'password',
-          'default': None,
-          'help': 'user password instead of giving raw cookie string (require '
-          'lax based authentication).'}),
-        )
-
-    def _run(self, args):
-        baseurl = args[0]
-        if not baseurl.startswith('http'):
-            baseurl = 'http://' + baseurl
-        if not baseurl.endswith('/'):
-            baseurl += '/'
-        self.base_url = baseurl
-        if not self.config.cookie and self.config.user:
-            # no cookie specified but a user is. Try to open a session using
-            # given authentication info
-            print 'opening session for', self.config.user
-            opener = urllib2.build_opener(GetSessionIdHandler(self.config))
-            urllib2.install_opener(opener)
-            data = urlencode(dict(__login=self.config.user,
-                                  __password=self.config.password))
-            self.open_url(urllib2.Request(baseurl, data))
-        opener = urllib2.build_opener(NoRedirectHandler())
-        urllib2.install_opener(opener)
-        self.do_base_url(baseurl)
-
-    def build_req(self, url):
-        req = urllib2.Request(url)
-        if self.config.cookie:
-            req.headers['Cookie'] = self.config.cookie
-        return req
-
-    def open_url(self, req):
-        try:
-            return urllib2.urlopen(req)
-        except urllib2.HTTPError, ex:
-            if ex.code == 302:
-                self.error_302(req, ex)
-            elif ex.code == 500:
-                self.error_500(req, ex)
-            else:
-                raise
-
-    def error_302(self, req, ex):
-        print 'authentication required'
-        print ('visit %s?vid=authinfo with your browser to get '
-               'authentication info' % self.base_url)
-        sys.exit(1)
-
-    def error_500(self, req, ex):
-        print 'an unexpected error occured on the server'
-        print ('you may get more information by visiting '
-               '%s' % req.get_full_url())
-        sys.exit(1)
-
-    def extract_message(self, data):
-        match = re.search(r'<div class="message">(.*?)</div>', data.read(), re.M|re.S)
-        if match:
-            msg = remove_html_tags(match.group(1))
-            print msg
-            return msg
-
-    def do_base_url(self, baseurl):
-        raise NotImplementedError()
-
-
-class DSInitCommand(URLCommand):
-    """initialize the datastore"""
-    name = 'db-init'
-
-    options = URLCommand.options + (
-        ('sleep',
-         {'short': 's', 'type' : 'int', 'metavar': 'nb seconds',
-          'default': None,
-          'help': 'number of seconds to wait between each request to avoid '
-          'going out of quota.'}),
-        )
-
-    def do_base_url(self, baseurl):
-        req = self.build_req(baseurl + '?vid=contentinit')
-        while True:
-            try:
-                data = self.open_url(req)
-            except urllib2.HTTPError, ex:
-                if ex.code == 303: # redirect
-                    print 'process completed'
-                    break
-                raise
-            msg = self.extract_message(data)
-            if msg and msg.startswith('error: '):
-                print ('you may to cleanup datastore by visiting '
-                       '%s?vid=contentclear (ALL ENTITIES WILL BE DELETED)'
-                       % baseurl)
-                break
-            if self.config.sleep:
-                time.sleep(self.config.sleep)
-
-
-class CleanSessionsCommand(URLCommand):
-    """cleanup sessions on the server. This command should usually be called
-    regularly by a cron job or equivalent.
-    """
-    name = "cleansessions"
-    def do_base_url(self, baseurl):
-        req = self.build_req(baseurl + '?vid=cleansessions')
-        data = self.open_url(req)
-        self.extract_message(data)
-
-
-register_commands([GenerateSchemaCommand,
-                   PopulateDataDirCommand,
-                   DSInitCommand,
-                   CleanSessionsCommand,
-                   ])
-
-def run():
-    main_run(sys.argv[1:])
-
-if __name__ == '__main__':
-    run()
--- a/hooks/bookmark.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/bookmark.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""bookmark related hooks
+"""bookmark related hooks"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.server import hook
@@ -28,7 +27,7 @@
     def precommit_event(self):
         if not self.session.deleted_in_transaction(self.bookmark.eid):
             if not self.bookmark.bookmarked_by:
-                self.bookmark.delete()
+                self.bookmark.cw_delete()
 
 
 class DelBookmarkedByHook(hook.Hook):
--- a/hooks/integrity.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/integrity.py	Wed Nov 03 16:38:28 2010 +0100
@@ -26,19 +26,17 @@
 from yams.schema import role_name
 
 from cubicweb import ValidationError
-from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
-from cubicweb.selectors import implements
+from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
+                             RQLConstraint, RQLUniqueConstraint)
+from cubicweb.selectors import is_instance
 from cubicweb.uilib import soup2xhtml
 from cubicweb.server import hook
 from cubicweb.server.hook import set_operation
 
 # special relations that don't have to be checked for integrity, usually
 # because they are handled internally by hooks (so we trust ourselves)
-DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
-                                'is', 'is_instance_of',
-                                'wf_info_for', 'from_state', 'to_state'))
-DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
-                                'wf_info_for', 'from_state', 'to_state'))
+DONT_CHECK_RTYPES_ON_ADD = META_RTYPES | WORKFLOW_RTYPES
+DONT_CHECK_RTYPES_ON_DEL = META_RTYPES | WORKFLOW_RTYPES
 
 _UNIQUE_CONSTRAINTS_LOCK = Lock()
 _UNIQUE_CONSTRAINTS_HOLDER = None
@@ -253,7 +251,7 @@
     """delete the composed of a composite relation when this relation is deleted
     """
     __regid__ = 'checkownersgroup'
-    __select__ = IntegrityHook.__select__ & implements('CWGroup')
+    __select__ = IntegrityHook.__select__ & is_instance('CWGroup')
     events = ('before_delete_entity', 'before_update_entity')
 
     def __call__(self):
@@ -293,7 +291,7 @@
 class StripCWUserLoginHook(IntegrityHook):
     """ensure user logins are stripped"""
     __regid__ = 'stripuserlogin'
-    __select__ = IntegrityHook.__select__ & implements('CWUser')
+    __select__ = IntegrityHook.__select__ & is_instance('CWUser')
     events = ('before_add_entity', 'before_update_entity',)
 
     def __call__(self):
--- a/hooks/metadata.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/metadata.py	Wed Nov 03 16:38:28 2010 +0100
@@ -21,7 +21,7 @@
 
 from datetime import datetime
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server import hook
 from cubicweb.server.utils import eschema_eid
 
@@ -140,7 +140,7 @@
 class FixUserOwnershipHook(MetaDataHook):
     """when a user has been created, add owned_by relation on itself"""
     __regid__ = 'fixuserowner'
-    __select__ = MetaDataHook.__select__ & implements('CWUser')
+    __select__ = MetaDataHook.__select__ & is_instance('CWUser')
     events = ('after_add_entity',)
 
     def __call__(self):
--- a/hooks/notification.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/notification.py	Wed Nov 03 16:38:28 2010 +0100
@@ -22,7 +22,7 @@
 
 from logilab.common.textutils import normalize_text
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server import hook
 from cubicweb.sobjects.supervising import SupervisionMailOp
 
@@ -49,7 +49,7 @@
 class StatusChangeHook(NotificationHook):
     """notify when a workflowable entity has its state modified"""
     __regid__ = 'notifystatuschange'
-    __select__ = NotificationHook.__select__ & implements('TrInfo')
+    __select__ = NotificationHook.__select__ & is_instance('TrInfo')
     events = ('after_add_entity',)
 
     def __call__(self):
--- a/hooks/security.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/security.py	Wed Nov 03 16:38:28 2010 +0100
@@ -29,9 +29,9 @@
 def check_entity_attributes(session, entity, editedattrs=None, creation=False):
     eid = entity.eid
     eschema = entity.e_schema
-    # .skip_security_attributes is there to bypass security for attributes
+    # ._cw_skip_security_attributes is there to bypass security for attributes
     # set by hooks by modifying the entity's dictionnary
-    dontcheck = entity.skip_security_attributes
+    dontcheck = entity._cw_skip_security_attributes
     if editedattrs is None:
         try:
             editedattrs = entity.edited_attributes
@@ -59,7 +59,7 @@
         for values in session.transaction_data.pop('check_entity_perm_op'):
             entity = session.entity_from_eid(values[0])
             action = values[1]
-            entity.check_perm(action)
+            entity.cw_check_perm(action)
             check_entity_attributes(session, entity, values[2:],
                                     creation=self.creation)
 
@@ -110,10 +110,10 @@
     def __call__(self):
         try:
             # check user has permission right now, if not retry at commit time
-            self.entity.check_perm('update')
+            self.entity.cw_check_perm('update')
             check_entity_attributes(self._cw, self.entity)
         except Unauthorized:
-            self.entity.clear_local_perm_cache('update')
+            self.entity._cw_clear_local_perm_cache('update')
             # save back editedattrs in case the entity is reedited later in the
             # same transaction, which will lead to edited_attributes being
             # overwritten
@@ -127,7 +127,7 @@
     events = ('before_delete_entity',)
 
     def __call__(self):
-        self.entity.check_perm('delete')
+        self.entity.cw_check_perm('delete')
 
 
 class BeforeAddRelationSecurityHook(SecurityHook):
--- a/hooks/syncschema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/syncschema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -33,8 +33,9 @@
 from logilab.common.testlib import mock_object
 
 from cubicweb import ValidationError
-from cubicweb.selectors import implements
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS, display_name
+from cubicweb.selectors import is_instance
+from cubicweb.schema import (SCHEMA_TYPES, 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
 
@@ -51,16 +52,9 @@
     }
 
 # core entity and relation types which can't be removed
-CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
-                                  'CWConstraint', 'CWAttribute', 'CWRelation']
-CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
-               'login', 'upassword', 'name',
-               'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
-               'relation_type', 'from_entity', 'to_entity',
-               'constrainted_by',
-               'read_permission', 'add_permission',
-               'delete_permission', 'updated_permission',
-               ]
+CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
+    ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group'))
+
 
 def get_constraints(session, entity):
     constraints = []
@@ -80,6 +74,11 @@
 
 def add_inline_relation_column(session, etype, rtype):
     """add necessary column and index for an inlined relation"""
+    attrkey = '%s.%s' % (etype, rtype)
+    createdattrs = session.transaction_data.setdefault('createdattrs', set())
+    if attrkey in createdattrs:
+        return
+    createdattrs.add(attrkey)
     table = SQL_PREFIX + etype
     column = SQL_PREFIX + rtype
     try:
@@ -96,8 +95,27 @@
     # is done by the dbhelper)
     session.pool.source('system').create_index(session, table, column)
     session.info('added index on %s(%s)', table, column)
-    session.transaction_data.setdefault('createdattrs', []).append(
-        '%s.%s' % (etype, rtype))
+
+
+def insert_rdef_on_subclasses(session, eschema, rschema, rdefdef, props):
+    # XXX 'infered': True/False, not clear actually
+    props.update({'constraints': rdefdef.constraints,
+                  'description': rdefdef.description,
+                  'cardinality': rdefdef.cardinality,
+                  'permissions': rdefdef.get_permissions(),
+                  'order': rdefdef.order,
+                  'infered': False, 'eid': None
+                  })
+    cstrtypemap = ss.cstrtype_mapping(session)
+    groupmap = group_mapping(session)
+    object = rschema.schema.eschema(rdefdef.object)
+    for specialization in eschema.specialized_by(False):
+        if (specialization, rdefdef.object) in rschema.rdefs:
+            continue
+        sperdef = RelationDefinitionSchema(specialization, rschema,
+                                           object, props)
+        ss.execschemarql(session.execute, sperdef,
+                         ss.rdef2rql(sperdef, cstrtypemap, groupmap))
 
 
 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
@@ -115,6 +133,14 @@
         raise ValidationError(entity.eid, errors)
 
 
+class SyncSchemaHook(hook.Hook):
+    """abstract class for schema synchronization hooks (in the `syncschema`
+    category)
+    """
+    __abstract__ = True
+    category = 'syncschema'
+
+
 # operations for low-level database alteration  ################################
 
 class DropTable(hook.Operation):
@@ -129,6 +155,8 @@
         self.session.system_sql('DROP TABLE %s' % self.table)
         self.info('dropped table %s', self.table)
 
+    # XXX revertprecommit_event
+
 
 class DropRelationTable(DropTable):
     def __init__(self, session, rtype):
@@ -156,6 +184,8 @@
             self.error('dropping column not supported by the backend, handle '
                        'it yourself (%s.%s)', table, column)
 
+    # XXX revertprecommit_event
+
 
 # base operations for in-memory schema synchronization  ########################
 
@@ -175,7 +205,7 @@
             if not eschema.final:
                 clear_cache(eschema, 'ordered_relations')
 
-    def commit_event(self):
+    def postcommit_event(self):
         rebuildinfered = self.session.data.get('rebuild-infered', True)
         repo = self.session.repo
         # commit event should not raise error, while set_schema has chances to
@@ -195,60 +225,88 @@
 
 class MemSchemaOperation(hook.Operation):
     """base class for schema operations"""
-    def __init__(self, session, kobj=None, **kwargs):
-        self.kobj = kobj
-        # once Operation.__init__ has been called, event may be triggered, so
-        # do this last !
+    def __init__(self, session, **kwargs):
         hook.Operation.__init__(self, session, **kwargs)
         # every schema operation is triggering a schema update
         MemSchemaNotifyChanges(session)
 
-    def prepare_constraints(self, rdef):
-        # if constraints is already a list, reuse it (we're updating multiple
-        # constraints of the same rdef in the same transactions)
-        if not isinstance(rdef.constraints, list):
-            rdef.constraints = list(rdef.constraints)
-        self.constraints = rdef.constraints
-
-
-class MemSchemaEarlyOperation(MemSchemaOperation):
-    def insert_index(self):
-        """schema operation which are inserted at the begining of the queue
-        (typically to add/remove entity or relation types)
-        """
-        i = -1
-        for i, op in enumerate(self.session.pending_operations):
-            if not isinstance(op, MemSchemaEarlyOperation):
-                return i
-        return i + 1
-
 
 # operations for high-level source database alteration  ########################
 
-class SourceDbCWETypeRename(hook.Operation):
+class CWETypeAddOp(MemSchemaOperation):
+    """after adding a CWEType entity:
+    * add it to the instance's schema
+    * create the necessary table
+    * set creation_date and modification_date by creating the necessary
+      CWAttribute entities
+    * add owned_by relation by creating the necessary CWRelation entity
+    """
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        schema = session.vreg.schema
+        etype = ybo.EntityType(eid=entity.eid, name=entity.name,
+                               description=entity.description)
+        eschema = schema.add_entity_type(etype)
+        # create the necessary table
+        tablesql = y2sql.eschema2sql(session.pool.source('system').dbhelper,
+                                     eschema, prefix=SQL_PREFIX)
+        for sql in tablesql.split(';'):
+            if sql.strip():
+                session.system_sql(sql)
+        # add meta relations
+        gmap = group_mapping(session)
+        cmap = ss.cstrtype_mapping(session)
+        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
+            rschema = schema[rtype]
+            sampletype = rschema.subjects()[0]
+            desttype = rschema.objects()[0]
+            rdef = copy(rschema.rdef(sampletype, desttype))
+            rdef.subject = mock_object(eid=entity.eid)
+            mock = mock_object(eid=None)
+            ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.session.vreg.schema.del_entity_type(self.entity.name)
+        # revert changes on database
+        self.session.system_sql('DROP TABLE %s%s' % (SQL_PREFIX, self.entity.name))
+
+
+class CWETypeRenameOp(MemSchemaOperation):
     """this operation updates physical storage accordingly"""
     oldname = newname = None # make pylint happy
 
-    def precommit_event(self):
+    def rename(self, oldname, newname):
+        self.session.vreg.schema.rename_entity_type(oldname, newname)
         # we need sql to operate physical changes on the system database
         sqlexec = self.session.system_sql
-        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
-                                                     SQL_PREFIX, self.newname))
-        self.info('renamed table %s to %s', self.oldname, self.newname)
+        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, oldname,
+                                                     SQL_PREFIX, newname))
+        self.info('renamed table %s to %s', oldname, newname)
         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
+                (newname, oldname))
         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
+                (newname, oldname))
+        # XXX transaction records
+
+    def precommit_event(self):
+        self.rename(self.oldname, self.newname)
+
+    def revertprecommit_event(self):
+        self.rename(self.newname, self.oldname)
 
 
-class SourceDbCWRTypeUpdate(hook.Operation):
+class CWRTypeUpdateOp(MemSchemaOperation):
     """actually update some properties of a relation definition"""
     rschema = entity = values = None # make pylint happy
+    oldvalus = None
 
     def precommit_event(self):
         rschema = self.rschema
         if rschema.final:
-            return
+            return # watched changes to final relation type are unexpected
         session = self.session
         if 'fulltext_container' in self.values:
             for subjtype, objtype in rschema.rdefs:
@@ -256,10 +314,14 @@
                                    UpdateFTIndexOp)
                 hook.set_operation(session, 'fti_update_etypes', objtype,
                                    UpdateFTIndexOp)
+        # update the in-memory schema first
+        self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
+        self.rschema.__dict__.update(self.values)
+        # then make necessary changes to the system source database
         if not 'inlined' in self.values:
             return # nothing to do
         inlined = self.values['inlined']
-        # check in-lining is necessary / possible
+        # check in-lining is possible when inlined
         if inlined:
             self.entity.check_inlined_allowed()
         # inlined changed, make necessary physical changes!
@@ -295,7 +357,7 @@
                 except Exception, ex:
                     # the column probably already exists. this occurs when the
                     # entity's type has just been added or if the column has not
-                    # been previously dropped
+                    # been previously dropped (eg sqlite)
                     self.error('error while altering table %s: %s', etype, ex)
                 # copy existant data.
                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
@@ -315,8 +377,13 @@
                 # drop existant table
                 DropRelationTable(session, rtype)
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.rschema.__dict__.update(self.oldvalues)
+        # XXX revert changes on database
 
-class SourceDbCWAttributeAdd(hook.Operation):
+
+class CWAttributeAddOp(MemSchemaOperation):
     """an attribute relation (CWAttribute) has been added:
     * add the necessary column
     * set default on this column if any and possible
@@ -330,24 +397,18 @@
     def init_rdef(self, **kwargs):
         entity = self.entity
         fromentity = entity.stype
+        rdefdef = self.rdefdef = ybo.RelationDefinition(
+            str(fromentity.name), entity.rtype.name, str(entity.otype.name),
+            description=entity.description, cardinality=entity.cardinality,
+            constraints=get_constraints(self.session, entity),
+            order=entity.ordernum, eid=entity.eid, **kwargs)
+        self.session.vreg.schema.add_relation_def(rdefdef)
         self.session.execute('SET X ordernum Y+1 '
                              'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
                              'X ordernum >= %(order)s, NOT X eid %(x)s',
                              {'x': entity.eid, 'se': fromentity.eid,
                               'order': entity.ordernum or 0})
-        subj = str(fromentity.name)
-        rtype = entity.rtype.name
-        obj = str(entity.otype.name)
-        constraints = get_constraints(self.session, entity)
-        rdef = ybo.RelationDefinition(subj, rtype, obj,
-                                      description=entity.description,
-                                      cardinality=entity.cardinality,
-                                      constraints=constraints,
-                                      order=entity.ordernum,
-                                      eid=entity.eid,
-                                      **kwargs)
-        MemSchemaRDefAdd(self.session, rdef)
-        return rdef
+        return rdefdef
 
     def precommit_event(self):
         session = self.session
@@ -361,22 +422,24 @@
                  'indexed': entity.indexed,
                  'fulltextindexed': entity.fulltextindexed,
                  'internationalizable': entity.internationalizable}
-        rdef = self.init_rdef(**props)
-        sysource = session.pool.source('system')
+        # update the in-memory schema first
+        rdefdef = self.init_rdef(**props)
+        # then make necessary changes to the system source database
+        syssource = session.pool.source('system')
         attrtype = y2sql.type_from_constraints(
-            sysource.dbhelper, rdef.object, rdef.constraints)
+            syssource.dbhelper, rdefdef.object, rdefdef.constraints)
         # XXX should be moved somehow into lgdb: sqlite doesn't support to
         # add a new column with UNIQUE, it should be added after the ALTER TABLE
         # using ADD INDEX
-        if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
+        if syssource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
             extra_unique_index = True
             attrtype = attrtype.replace(' UNIQUE', '')
         else:
             extra_unique_index = False
         # added some str() wrapping query since some backend (eg psycopg) don't
         # allow unicode queries
-        table = SQL_PREFIX + rdef.subject
-        column = SQL_PREFIX + rdef.name
+        table = SQL_PREFIX + rdefdef.subject
+        column = SQL_PREFIX + rdefdef.name
         try:
             session.system_sql(str('ALTER TABLE %s ADD %s %s'
                                    % (table, column, attrtype)),
@@ -389,7 +452,7 @@
             self.error('error while altering table %s: %s', table, ex)
         if extra_unique_index or entity.indexed:
             try:
-                sysource.create_index(session, table, column,
+                syssource.create_index(session, table, column,
                                       unique=extra_unique_index)
             except Exception, ex:
                 self.error('error while creating index for %s.%s: %s',
@@ -397,47 +460,36 @@
         # final relations are not infered, propagate
         schema = session.vreg.schema
         try:
-            eschema = schema.eschema(rdef.subject)
+            eschema = schema.eschema(rdefdef.subject)
         except KeyError:
             return # entity type currently being added
         # propagate attribute to children classes
-        rschema = schema.rschema(rdef.name)
+        rschema = schema.rschema(rdefdef.name)
         # if relation type has been inserted in the same transaction, its final
         # attribute is still set to False, so we've to ensure it's False
         rschema.final = True
-        # XXX 'infered': True/False, not clear actually
-        props.update({'constraints': rdef.constraints,
-                      'description': rdef.description,
-                      'cardinality': rdef.cardinality,
-                      'constraints': rdef.constraints,
-                      'permissions': rdef.get_permissions(),
-                      'order': rdef.order,
-                      'infered': False, 'eid': None
-                      })
-        cstrtypemap = ss.cstrtype_mapping(session)
-        groupmap = group_mapping(session)
-        object = schema.eschema(rdef.object)
-        for specialization in eschema.specialized_by(False):
-            if (specialization, rdef.object) in rschema.rdefs:
-                continue
-            sperdef = RelationDefinitionSchema(specialization, rschema,
-                                               object, props)
-            ss.execschemarql(session.execute, sperdef,
-                             ss.rdef2rql(sperdef, cstrtypemap, groupmap))
+        insert_rdef_on_subclasses(session, eschema, rschema, rdefdef, props)
         # set default value, using sql for performance and to avoid
         # modification_date update
         if default:
             session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
                                {'default': default})
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.session.vreg.schema.del_relation_def(
+            self.rdefdef.subject, self.rdefdef.name, self.rdefdef.object)
+        # XXX revert changes on database
 
-class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
+
+class CWRelationAddOp(CWAttributeAddOp):
     """an actual relation has been added:
-    * if this is an inlined relation, add the necessary column
-      else if it's the first instance of this relation type, add the
-      necessary table and set default permissions
-    * register an operation to add the relation definition to the
-      instance's schema on commit
+
+    * add the relation definition to the instance's schema
+
+    * if this is an inlined relation, add the necessary column else if it's the
+      first instance of this relation type, add the necessary table and set
+      default permissions
 
     constraints are handled by specific hooks
     """
@@ -446,280 +498,281 @@
     def precommit_event(self):
         session = self.session
         entity = self.entity
-        rdef = self.init_rdef(composite=entity.composite)
+        # update the in-memory schema first
+        rdefdef = self.init_rdef(composite=entity.composite)
+        # then make necessary changes to the system source database
         schema = session.vreg.schema
-        rtype = rdef.name
+        rtype = rdefdef.name
         rschema = schema.rschema(rtype)
         # this have to be done before permissions setting
         if rschema.inlined:
             # need to add a column if the relation is inlined and if this is the
             # first occurence of "Subject relation Something" whatever Something
-            # and if it has not been added during other event of the same
-            # transaction
-            key = '%s.%s' % (rdef.subject, rtype)
-            try:
-                alreadythere = bool(rschema.objects(rdef.subject))
-            except KeyError:
-                alreadythere = False
-            if not (alreadythere or
-                    key in session.transaction_data.get('createdattrs', ())):
-                add_inline_relation_column(session, rdef.subject, rtype)
+            if len(rschema.objects(rdefdef.subject)) == 1:
+                add_inline_relation_column(session, rdefdef.subject, rtype)
+            eschema = schema[rdefdef.subject]
+            insert_rdef_on_subclasses(session, eschema, rschema, rdefdef,
+                                      {'composite': entity.composite})
         else:
+            if rschema.symmetric:
+                # for symmetric relations, rdefs will store relation definitions
+                # in both ways (i.e. (subj -> obj) and (obj -> subj))
+                relation_already_defined = len(rschema.rdefs) > 2
+            else:
+                relation_already_defined = len(rschema.rdefs) > 1
             # need to create the relation if no relation definition in the
             # schema and if it has not been added during other event of the same
             # transaction
-            if not (rschema.subjects() or
+            if not (relation_already_defined or
                     rtype in session.transaction_data.get('createdtables', ())):
-                try:
-                    rschema = schema.rschema(rtype)
-                    tablesql = y2sql.rschema2sql(rschema)
-                except KeyError:
-                    # fake we add it to the schema now to get a correctly
-                    # initialized schema but remove it before doing anything
-                    # more dangerous...
-                    rschema = schema.add_relation_type(rdef)
-                    tablesql = y2sql.rschema2sql(rschema)
-                    schema.del_relation_type(rtype)
+                rschema = schema.rschema(rtype)
                 # create the necessary table
-                for sql in tablesql.split(';'):
+                for sql in y2sql.rschema2sql(rschema).split(';'):
                     if sql.strip():
                         session.system_sql(sql)
                 session.transaction_data.setdefault('createdtables', []).append(
                     rtype)
 
+    # XXX revertprecommit_event
 
-class SourceDbRDefUpdate(hook.Operation):
+
+class RDefDelOp(MemSchemaOperation):
+    """an actual relation has been removed"""
+    rdef = None # make pylint happy
+
+    def precommit_event(self):
+        session = self.session
+        rdef = self.rdef
+        rschema = rdef.rtype
+        # make necessary changes to the system source database first
+        rdeftype = rschema.final and 'CWAttribute' or 'CWRelation'
+        execute = session.execute
+        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
+                       'R eid %%(x)s' % rdeftype, {'x': rschema.eid})
+        lastrel = rset[0][0] == 0
+        # we have to update physical schema systematically for final and inlined
+        # relations, but only if it's the last instance for this relation type
+        # for other relations
+        if (rschema.final or rschema.inlined):
+            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
+                           'R eid %%(r)s, X from_entity E, E eid %%(e)s'
+                           % rdeftype,
+                           {'r': rschema.eid, 'e': rdef.subject.eid})
+            if rset[0][0] == 0 and not session.deleted_in_transaction(rdef.subject.eid):
+                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
+                ptypes.add(rschema.type)
+                DropColumn(session, table=SQL_PREFIX + str(rdef.subject),
+                           column=SQL_PREFIX + str(rschema))
+        elif lastrel:
+            DropRelationTable(session, str(rschema))
+        # then update the in-memory schema
+        rschema.del_relation_def(rdef.subject, rdef.object)
+        # if this is the last relation definition of this type, drop associated
+        # relation type
+        if lastrel and not session.deleted_in_transaction(rschema.eid):
+            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rschema.eid})
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        #
+        # Note: add_relation_def takes a RelationDefinition, not a
+        # RelationDefinitionSchema, needs to fake it
+        self.rdef.name = str(self.rdef.rtype)
+        self.session.vreg.schema.add_relation_def(self.rdef)
+
+
+
+class RDefUpdateOp(MemSchemaOperation):
     """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
+    rschema = rdefkey = values = None # make pylint happy
+    rdef = oldvalues = None
+    indexed_changed = null_allowed_changed = False
 
     def precommit_event(self):
         session = self.session
-        etype = self.kobj[0]
-        table = SQL_PREFIX + etype
-        column = SQL_PREFIX + self.rschema.type
+        rdef = self.rdef = self.rschema.rdefs[self.rdefkey]
+        # update the in-memory schema first
+        self.oldvalues = dict( (attr, getattr(rdef, attr)) for attr in self.values)
+        rdef.update(self.values)
+        # then make necessary changes to the system source database
+        syssource = session.pool.source('system')
         if 'indexed' in self.values:
-            sysource = session.pool.source('system')
-            if self.values['indexed']:
-                sysource.create_index(session, table, column)
-            else:
-                sysource.drop_index(session, table, column)
-        if 'cardinality' in self.values and self.rschema.final:
-            syssource = session.pool.source('system')
-            if not syssource.dbhelper.alter_column_support:
-                # not supported (and NOT NULL not set by yams in that case, so
-                # no worry) XXX (syt) then should we set NOT NULL below ??
-                return
-            atype = self.rschema.objects(etype)[0]
-            constraints = self.rschema.rdef(etype, atype).constraints
-            coltype = y2sql.type_from_constraints(syssource.dbhelper, atype, constraints,
-                                                  creating=False)
-            # XXX check self.values['cardinality'][0] actually changed?
-            syssource.set_null_allowed(self.session, table, column, coltype,
-                                       self.values['cardinality'][0] != '1')
+            syssource.update_rdef_indexed(session, rdef)
+            self.indexed_changed = True
+        if 'cardinality' in self.values and (rdef.rtype.final or
+                                             rdef.rtype.inlined) \
+              and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
+            syssource.update_rdef_null_allowed(self.session, rdef)
+            self.null_allowed_changed = True
         if 'fulltextindexed' in self.values:
-            hook.set_operation(session, 'fti_update_etypes', etype,
+            hook.set_operation(session, 'fti_update_etypes', rdef.subject,
                                UpdateFTIndexOp)
 
+    def revertprecommit_event(self):
+        if self.rdef is None:
+            return
+        # revert changes on in memory schema
+        self.rdef.update(self.oldvalues)
+        # revert changes on database
+        syssource = self.session.pool.source('system')
+        if self.indexed_changed:
+            syssource.update_rdef_indexed(self.session, self.rdef)
+        if self.null_allowed_changed:
+            syssource.update_rdef_null_allowed(self.session, self.rdef)
 
-class SourceDbCWConstraintAdd(hook.Operation):
+
+def _set_modifiable_constraints(rdef):
+    # for proper in-place modification of in-memory schema: if rdef.constraints
+    # is already a list, reuse it (we're updating multiple constraints of the
+    # same rdef in the same transactions)
+    if not isinstance(rdef.constraints, list):
+        rdef.constraints = list(rdef.constraints)
+
+
+class CWConstraintDelOp(MemSchemaOperation):
+    """actually remove a constraint of a relation definition"""
+    rdef = oldcstr = newcstr = None # make pylint happy
+    size_cstr_changed = unique_changed = False
+
+    def precommit_event(self):
+        session = self.session
+        rdef = self.rdef
+        # in-place modification of in-memory schema first
+        _set_modifiable_constraints(rdef)
+        rdef.constraints.remove(self.oldcstr)
+        # then update database: alter the physical schema on size/unique
+        # constraint changes
+        syssource = session.pool.source('system')
+        cstrtype = self.oldcstr.type()
+        if cstrtype == 'SizeConstraint':
+            syssource.update_rdef_column(session, rdef)
+            self.size_cstr_changed = True
+        elif cstrtype == 'UniqueConstraint':
+            syssource.update_rdef_unique(session, rdef)
+            self.unique_changed = True
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        if self.newcstr is not None:
+            self.rdef.constraints.remove(self.newcstr)
+        if self.oldcstr is not None:
+            self.rdef.constraints.append(self.oldcstr)
+        # revert changes on database
+        syssource = self.session.pool.source('system')
+        if self.size_cstr_changed:
+            syssource.update_rdef_column(self.session, self.rdef)
+        if self.unique_changed:
+            syssource.update_rdef_unique(self.session, self.rdef)
+
+
+class CWConstraintAddOp(CWConstraintDelOp):
     """actually update constraint of a relation definition"""
     entity = None # make pylint happy
-    cancelled = False
 
     def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
         session = self.session
+        rdefentity = self.entity.reverse_constrained_by[0]
         # when the relation is added in the same transaction, the constraint
         # object is created by the operation adding the attribute or relation,
         # so there is nothing to do here
-        if session.added_in_transaction(rdef.eid):
+        if session.added_in_transaction(rdefentity.eid):
             return
-        rdefschema = session.vreg.schema.schema_by_eid(rdef.eid)
-        subjtype, rtype, objtype = rdefschema.as_triple()
+        rdef = self.rdef = session.vreg.schema.schema_by_eid(rdefentity.eid)
         cstrtype = self.entity.type
-        oldcstr = rtype.rdef(subjtype, objtype).constraint_by_type(cstrtype)
-        newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        table = SQL_PREFIX + str(subjtype)
-        column = SQL_PREFIX + str(rtype)
-        # alter the physical schema on size constraint changes
-        if newcstr.type() == 'SizeConstraint' and (
-            oldcstr is None or oldcstr.max != newcstr.max):
-            syssource = self.session.pool.source('system')
-            card = rtype.rdef(subjtype, objtype).cardinality
-            coltype = y2sql.type_from_constraints(syssource.dbhelper, objtype,
-                                                  [newcstr], creating=False)
-            try:
-                syssource.change_col_type(session, table, column, coltype, card[0] != '1')
-                self.info('altered column %s of table %s: now %s',
-                          column, table, coltype)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
+        oldcstr = self.oldcstr = rdef.constraint_by_type(cstrtype)
+        newcstr = self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+        # in-place modification of in-memory schema first
+        _set_modifiable_constraints(rdef)
+        newcstr.eid = self.entity.eid
+        if oldcstr is not None:
+            rdef.constraints.remove(oldcstr)
+        rdef.constraints.append(newcstr)
+        # then update database: alter the physical schema on size/unique
+        # constraint changes
+        syssource = session.pool.source('system')
+        if cstrtype == 'SizeConstraint' and (oldcstr is None or
+                                             oldcstr.max != newcstr.max):
+            syssource.update_rdef_column(session, rdef)
+            self.size_cstr_changed = True
         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
-            session.pool.source('system').create_index(
-                self.session, table, column, unique=True)
+            syssource.update_rdef_unique(session, rdef)
+            self.unique_changed = True
 
-
-class SourceDbCWConstraintDel(hook.Operation):
-    """actually remove a constraint of a relation definition"""
-    rtype = subjtype = None # make pylint happy
-
+class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
+    entity = None # make pylint happy
     def precommit_event(self):
-        cstrtype = self.cstr.type()
-        table = SQL_PREFIX + str(self.rdef.subject)
-        column = SQL_PREFIX + str(self.rdef.rtype)
-        # alter the physical schema on size/unique constraint changes
-        if cstrtype == 'SizeConstraint':
-            syssource = self.session.pool.source('system')
-            coltype = y2sql.type_from_constraints(syssource.dbhelper,
-                                                  self.rdef.object, [],
-                                                  creating=False)
-            try:
-                syssource.change_col_type(session, table, column, coltype,
-                                          self.rdef.cardinality[0] != '1')
-                self.info('altered column %s of table %s: now %s',
-                          column, table, coltype)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
-        elif cstrtype == 'UniqueConstraint':
-            self.session.pool.source('system').drop_index(
-                self.session, table, column, unique=True)
+        session = self.session
+        prefix = SQL_PREFIX
+        table = '%s%s' % (prefix, self.entity.constraint_of[0].name)
+        cols = ['%s%s' % (prefix, r.rtype.name)
+                for r in self.entity.relations]
+        dbhelper= session.pool.source('system').dbhelper
+        sqls = dbhelper.sqls_create_multicol_unique_index(table, cols)
+        for sql in sqls:
+            session.system_sql(sql)
+
+    # XXX revertprecommit_event
+
+    def postcommit_event(self):
+        eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid)
+        attrs = [r.rtype.name for r in self.entity.relations]
+        eschema._unique_together.append(attrs)
 
+class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
+    entity = oldcstr = None # for pylint
+    cols = [] # for pylint
+    def precommit_event(self):
+        session = self.session
+        prefix = SQL_PREFIX
+        table = '%s%s' % (prefix, self.entity.type)
+        dbhelper= session.pool.source('system').dbhelper
+        cols = ['%s%s' % (prefix, c) for c in self.cols]
+        sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols)
+        for sql in sqls:
+            session.system_sql(sql)
+
+    # XXX revertprecommit_event
+
+    def postcommit_event(self):
+        eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid)
+        cols = set(self.cols)
+        unique_together = [ut for ut in eschema._unique_together
+                           if set(ut) != cols]
+        eschema._unique_together = unique_together
 
 # operations for in-memory schema synchronization  #############################
 
-class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
-    """actually add the entity type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.session.vreg.schema.add_entity_type(self.kobj)
-
-
-class MemSchemaCWETypeRename(MemSchemaOperation):
-    """this operation updates physical storage accordingly"""
-    oldname = newname = None # make pylint happy
-
-    def commit_event(self):
-        self.session.vreg.schema.rename_entity_type(self.oldname, self.newname)
-
-
 class MemSchemaCWETypeDel(MemSchemaOperation):
     """actually remove the entity type from the instance's schema"""
-    def commit_event(self):
-        try:
-            # del_entity_type also removes entity's relations
-            self.session.vreg.schema.del_entity_type(self.kobj)
-        except KeyError:
-            # s/o entity type have already been deleted
-            pass
+    def postcommit_event(self):
+        # del_entity_type also removes entity's relations
+        self.session.vreg.schema.del_entity_type(self.etype)
 
 
-class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
+class MemSchemaCWRTypeAdd(MemSchemaOperation):
     """actually add the relation type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.session.vreg.schema.add_relation_type(self.kobj)
-
+    def precommit_event(self):
+        self.session.vreg.schema.add_relation_type(self.rtypedef)
 
-class MemSchemaCWRTypeUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.__dict__.update(self.values)
+    def revertprecommit_event(self):
+        self.session.vreg.schema.del_relation_type(self.rtypedef.name)
 
 
 class MemSchemaCWRTypeDel(MemSchemaOperation):
     """actually remove the relation type from the instance's schema"""
-    def commit_event(self):
+    def postcommit_event(self):
         try:
-            self.session.vreg.schema.del_relation_type(self.kobj)
+            self.session.vreg.schema.del_relation_type(self.rtype)
         except KeyError:
             # s/o entity type have already been deleted
             pass
 
 
-class MemSchemaRDefAdd(MemSchemaEarlyOperation):
-    """actually add the attribute relation definition to the instance's
-    schema
-    """
-    def commit_event(self):
-        self.session.vreg.schema.add_relation_def(self.kobj)
-
-
-class MemSchemaRDefUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.rdefs[self.kobj].update(self.values)
-
-
-class MemSchemaRDefDel(MemSchemaOperation):
-    """actually remove the relation definition from the instance's schema"""
-    def commit_event(self):
-        subjtype, rtype, objtype = self.kobj
-        try:
-            self.session.vreg.schema.del_relation_def(subjtype, rtype, objtype)
-        except KeyError:
-            # relation type may have been already deleted
-            pass
-
-
-class MemSchemaCWConstraintAdd(MemSchemaOperation):
-    """actually update constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintAdd
-    """
-    cancelled = False
-
-    def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
-        # when the relation is added in the same transaction, the constraint
-        # object is created by the operation adding the attribute or relation,
-        # so there is nothing to do here
-        if self.session.added_in_transaction(rdef.eid):
-            self.cancelled = True
-            return
-        rdef = self.session.vreg.schema.schema_by_eid(rdef.eid)
-        self.prepare_constraints(rdef)
-        cstrtype = self.entity.type
-        self.cstr = rdef.constraint_by_type(cstrtype)
-        self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        self.newcstr.eid = self.entity.eid
-
-    def commit_event(self):
-        if self.cancelled:
-            return
-        # in-place modification
-        if not self.cstr is None:
-            self.constraints.remove(self.cstr)
-        self.constraints.append(self.newcstr)
-
-
-class MemSchemaCWConstraintDel(MemSchemaOperation):
-    """actually remove a constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintDel
-    """
-    rtype = subjtype = objtype = None # make pylint happy
-    def precommit_event(self):
-        self.prepare_constraints(self.rdef)
-
-    def commit_event(self):
-        self.constraints.remove(self.cstr)
-
-
 class MemSchemaPermissionAdd(MemSchemaOperation):
     """synchronize schema when a *_permission relation has been added on a group
     """
 
-    def commit_event(self):
+    def precommit_event(self):
         """the observed connections pool has been commited"""
         try:
             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
@@ -740,13 +793,15 @@
             perms.append(perm)
             erschema.set_action_permissions(self.action, perms)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaPermissionDel(MemSchemaPermissionAdd):
     """synchronize schema when a *_permission relation has been deleted from a
     group
     """
 
-    def commit_event(self):
+    def precommit_event(self):
         """the observed connections pool has been commited"""
         try:
             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
@@ -771,19 +826,23 @@
             self.error('can\'t remove permission %s for %s on %s',
                        perm, self.action, erschema)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaSpecializesAdd(MemSchemaOperation):
 
-    def commit_event(self):
+    def precommit_event(self):
         eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
         parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
         eschema._specialized_type = parenteschema.type
         parenteschema._specialized_by.append(eschema.type)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaSpecializesDel(MemSchemaOperation):
 
-    def commit_event(self):
+    def precommit_event(self):
         try:
             eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
             parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
@@ -793,10 +852,7 @@
         eschema._specialized_type = None
         parenteschema._specialized_by.remove(eschema.type)
 
-
-class SyncSchemaHook(hook.Hook):
-    __abstract__ = True
-    category = 'syncschema'
+    # XXX revertprecommit_event
 
 
 # CWEType hooks ################################################################
@@ -808,18 +864,19 @@
     * instantiate an operation to delete the entity type on commit
     """
     __regid__ = 'syncdelcwetype'
-    __select__ = SyncSchemaHook.__select__ & implements('CWEType')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWEType')
     events = ('before_delete_entity',)
 
     def __call__(self):
         # final entities can't be deleted, don't care about that
         name = self.entity.name
-        if name in CORE_ETYPES:
+        if name in CORE_TYPES:
             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
         # delete every entities of this type
-        self._cw.execute('DELETE %s X' % name)
+        if not name in ETYPE_NAME_MAP:
+            self._cw.execute('DELETE %s X' % name)
+            MemSchemaCWETypeDel(self._cw, etype=name)
         DropTable(self._cw, table=SQL_PREFIX + name)
-        MemSchemaCWETypeDel(self._cw, name)
 
 
 class AfterDelCWETypeHook(DelCWETypeHook):
@@ -847,42 +904,7 @@
         entity = self.entity
         if entity.get('final'):
             return
-        schema = self._cw.vreg.schema
-        name = entity['name']
-        etype = ybo.EntityType(name=name, description=entity.get('description'),
-                               meta=entity.get('meta')) # don't care about final
-        # fake we add it to the schema now to get a correctly initialized schema
-        # but remove it before doing anything more dangerous...
-        schema = self._cw.vreg.schema
-        eschema = schema.add_entity_type(etype)
-        # generate table sql and rql to add metadata
-        tablesql = y2sql.eschema2sql(self._cw.pool.source('system').dbhelper,
-                                     eschema, prefix=SQL_PREFIX)
-        rdefrqls = []
-        gmap = group_mapping(self._cw)
-        cmap = ss.cstrtype_mapping(self._cw)
-        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
-            rschema = schema[rtype]
-            sampletype = rschema.subjects()[0]
-            desttype = rschema.objects()[0]
-            rdef = copy(rschema.rdef(sampletype, desttype))
-            rdef.subject = mock_object(eid=entity.eid)
-            mock = mock_object(eid=None)
-            rdefrqls.append( (mock, tuple(ss.rdef2rql(rdef, cmap, gmap))) )
-        # now remove it !
-        schema.del_entity_type(name)
-        # create the necessary table
-        for sql in tablesql.split(';'):
-            if sql.strip():
-                self._cw.system_sql(sql)
-        # register operation to modify the schema on commit
-        # this have to be done before adding other relations definitions
-        # or permission settings
-        etype.eid = entity.eid
-        MemSchemaCWETypeAdd(self._cw, etype)
-        # add meta relations
-        for rdef, relrqls in rdefrqls:
-            ss.execschemarql(self._cw.execute, rdef, relrqls)
+        CWETypeAddOp(self._cw, entity=entity)
 
 
 class BeforeUpdateCWETypeHook(DelCWETypeHook):
@@ -895,12 +917,9 @@
         check_valid_changes(self._cw, entity, ro_attrs=('final',))
         # don't use getattr(entity, attr), we would get the modified value if any
         if 'name' in entity.edited_attributes:
-            newname = entity.pop('name')
-            oldname = entity.name
+            oldname, newname = hook.entity_oldnewvalue(entity, 'name')
             if newname.lower() != oldname.lower():
-                SourceDbCWETypeRename(self._cw, oldname=oldname, newname=newname)
-                MemSchemaCWETypeRename(self._cw, oldname=oldname, newname=newname)
-            entity['name'] = newname
+                CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
 
 
 # CWRType hooks ################################################################
@@ -912,19 +931,19 @@
     * instantiate an operation to delete the relation type on commit
     """
     __regid__ = 'syncdelcwrtype'
-    __select__ = SyncSchemaHook.__select__ & implements('CWRType')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
     events = ('before_delete_entity',)
 
     def __call__(self):
         name = self.entity.name
-        if name in CORE_RTYPES:
+        if name in CORE_TYPES:
             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
         # delete relation definitions using this relation type
         self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
                         {'x': self.entity.eid})
         self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
                         {'x': self.entity.eid})
-        MemSchemaCWRTypeDel(self._cw, name)
+        MemSchemaCWRTypeDel(self._cw, rtype=name)
 
 
 class AfterAddCWRTypeHook(DelCWRTypeHook):
@@ -939,13 +958,12 @@
 
     def __call__(self):
         entity = self.entity
-        rtype = ybo.RelationType(name=entity.name,
-                                 description=entity.get('description'),
-                                 meta=entity.get('meta', False),
-                                 inlined=entity.get('inlined', False),
-                                 symmetric=entity.get('symmetric', False),
-                                 eid=entity.eid)
-        MemSchemaCWRTypeAdd(self._cw, rtype)
+        rtypedef = ybo.RelationType(name=entity.name,
+                                    description=entity.description,
+                                    inlined=entity.get('inlined', False),
+                                    symmetric=entity.get('symmetric', False),
+                                    eid=entity.eid)
+        MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
 
 
 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
@@ -964,9 +982,8 @@
                     newvalues[prop] = entity[prop]
         if newvalues:
             rschema = self._cw.vreg.schema.rschema(entity.name)
-            SourceDbCWRTypeUpdate(self._cw, rschema=rschema, entity=entity,
-                                  values=newvalues)
-            MemSchemaCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues)
+            CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
+                            values=newvalues)
 
 
 class AfterDelRelationTypeHook(SyncSchemaHook):
@@ -984,9 +1001,12 @@
 
     def __call__(self):
         session = self._cw
-        rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
+        try:
+            rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
+        except KeyError:
+            self.critical('cant get schema rdef associated to %s', self.eidfrom)
+            return
         subjschema, rschema, objschema = rdef.as_triple()
-        pendings = session.transaction_data.get('pendingeids', ())
         pendingrdefs = session.transaction_data.setdefault('pendingrdefs', set())
         # first delete existing relation if necessary
         if rschema.final:
@@ -995,107 +1015,89 @@
         else:
             rdeftype = 'CWRelation'
             pendingrdefs.add((subjschema, rschema, objschema))
-            if not (subjschema.eid in pendings or objschema.eid in pendings):
+            if not (session.deleted_in_transaction(subjschema.eid) or
+                    session.deleted_in_transaction(objschema.eid)):
                 session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
                                 % (rschema, subjschema, objschema))
-        execute = session.execute
-        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
-                       'R eid %%(x)s' % rdeftype, {'x': self.eidto})
-        lastrel = rset[0][0] == 0
-        # we have to update physical schema systematically for final and inlined
-        # relations, but only if it's the last instance for this relation type
-        # for other relations
-
-        if (rschema.final or rschema.inlined):
-            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
-                           'R eid %%(x)s, X from_entity E, E name %%(name)s'
-                           % rdeftype, {'x': self.eidto, 'name': str(subjschema)})
-            if rset[0][0] == 0 and not subjschema.eid in pendings:
-                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
-                ptypes.add(rschema.type)
-                DropColumn(session, table=SQL_PREFIX + subjschema.type,
-                           column=SQL_PREFIX + rschema.type)
-        elif lastrel:
-            DropRelationTable(session, rschema.type)
-        # if this is the last instance, drop associated relation type
-        if lastrel and not self.eidto in pendings:
-            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': self.eidto})
-        MemSchemaRDefDel(session, (subjschema, rschema, objschema))
+        RDefDelOp(session, rdef=rdef)
 
 
 # CWAttribute / CWRelation hooks ###############################################
 
 class AfterAddCWAttributeHook(SyncSchemaHook):
     __regid__ = 'syncaddcwattribute'
-    __select__ = SyncSchemaHook.__select__ & implements('CWAttribute')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute')
     events = ('after_add_entity',)
 
     def __call__(self):
-        SourceDbCWAttributeAdd(self._cw, entity=self.entity)
+        CWAttributeAddOp(self._cw, entity=self.entity)
 
 
 class AfterAddCWRelationHook(AfterAddCWAttributeHook):
     __regid__ = 'syncaddcwrelation'
-    __select__ = SyncSchemaHook.__select__ & implements('CWRelation')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWRelation')
 
     def __call__(self):
-        SourceDbCWRelationAdd(self._cw, entity=self.entity)
+        CWRelationAddOp(self._cw, entity=self.entity)
 
 
 class AfterUpdateCWRDefHook(SyncSchemaHook):
     __regid__ = 'syncaddcwattribute'
-    __select__ = SyncSchemaHook.__select__ & implements('CWAttribute',
-                                                        'CWRelation')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute',
+                                                         'CWRelation')
     events = ('before_update_entity',)
 
     def __call__(self):
         entity = self.entity
         if self._cw.deleted_in_transaction(entity.eid):
             return
-        desttype = entity.otype.name
+        subjtype = entity.stype.name
+        objtype = entity.otype.name
         rschema = self._cw.vreg.schema[entity.rtype.name]
+        # note: do not access schema rdef here, it may be added later by an
+        # operation
         newvalues = {}
-        for prop in RelationDefinitionSchema.rproperty_defs(desttype):
+        for prop in RelationDefinitionSchema.rproperty_defs(objtype):
             if prop == 'constraints':
                 continue
             if prop == 'order':
-                prop = 'ordernum'
-            if prop in entity.edited_attributes:
-                old, new = hook.entity_oldnewvalue(entity, prop)
+                attr = 'ordernum'
+            else:
+                attr = prop
+            if attr in entity.edited_attributes:
+                old, new = hook.entity_oldnewvalue(entity, attr)
                 if old != new:
-                    newvalues[prop] = entity[prop]
+                    newvalues[prop] = new
         if newvalues:
-            subjtype = entity.stype.name
-            MemSchemaRDefUpdate(self._cw, kobj=(subjtype, desttype),
-                                rschema=rschema, values=newvalues)
-            SourceDbRDefUpdate(self._cw, kobj=(subjtype, desttype),
-                               rschema=rschema, values=newvalues)
+            RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
+                         values=newvalues)
 
 
 # constraints synchronization hooks ############################################
 
 class AfterAddCWConstraintHook(SyncSchemaHook):
     __regid__ = 'syncaddcwconstraint'
-    __select__ = SyncSchemaHook.__select__ & implements('CWConstraint')
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
     events = ('after_add_entity', 'after_update_entity')
 
     def __call__(self):
-        MemSchemaCWConstraintAdd(self._cw, entity=self.entity)
-        SourceDbCWConstraintAdd(self._cw, entity=self.entity)
+        CWConstraintAddOp(self._cw, entity=self.entity)
 
 
 class AfterAddConstrainedByHook(SyncSchemaHook):
-    __regid__ = 'syncdelconstrainedby'
+    __regid__ = 'syncaddconstrainedby'
     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
     events = ('after_add_relation',)
 
     def __call__(self):
         if self._cw.added_in_transaction(self.eidfrom):
+            # used by get_constraints() which is called in CWAttributeAddOp
             self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
 
 
-class BeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):
+class BeforeDeleteConstrainedByHook(SyncSchemaHook):
     __regid__ = 'syncdelconstrainedby'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
     events = ('before_delete_relation',)
 
     def __call__(self):
@@ -1109,8 +1111,33 @@
         except IndexError:
             self._cw.critical('constraint type no more accessible')
         else:
-            SourceDbCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
-            MemSchemaCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
+            CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr)
+
+# unique_together constraints
+# XXX: use setoperations and before_add_relation here (on constraint_of and relations)
+class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook):
+    __regid__ = 'syncadd_cwuniquetogether_constraint'
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint')
+    events = ('after_add_entity', 'after_update_entity')
+
+    def __call__(self):
+        CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity)
+
+
+class BeforeDeleteConstraintOfHook(SyncSchemaHook):
+    __regid__ = 'syncdelconstraintof'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constraint_of')
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        if self._cw.deleted_in_transaction(self.eidto):
+            return
+        schema = self._cw.vreg.schema
+        cstr = self._cw.entity_from_eid(self.eidfrom)
+        entity = schema.schema_by_eid(self.eidto)
+        cols = [r.rtype.name
+                for r in cstr.relations]
+        CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols)
 
 
 # permissions synchronization hooks ############################################
@@ -1176,7 +1203,7 @@
             still_fti = list(schema[etype].indexable_attributes())
             for entity in rset.entities():
                 source.fti_unindex_entity(session, entity.eid)
-                for container in entity.fti_containers():
+                for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
                     if still_fti or container is not entity:
                         source.fti_unindex_entity(session, container.eid)
                         source.fti_index_entity(session, container)
--- a/hooks/syncsession.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/syncsession.py	Wed Nov 03 16:38:28 2010 +0100
@@ -22,7 +22,7 @@
 
 from yams.schema import role_name
 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server import hook
 
 
@@ -108,7 +108,7 @@
 
 class CloseDeletedUserSessionsHook(SyncSessionHook):
     __regid__ = 'closession'
-    __select__ = SyncSessionHook.__select__ & implements('CWUser')
+    __select__ = SyncSessionHook.__select__ & is_instance('CWUser')
     events = ('after_delete_entity',)
 
     def __call__(self):
@@ -152,7 +152,7 @@
 
 class AddCWPropertyHook(SyncSessionHook):
     __regid__ = 'addcwprop'
-    __select__ = SyncSessionHook.__select__ & implements('CWProperty')
+    __select__ = SyncSessionHook.__select__ & is_instance('CWProperty')
     events = ('after_add_entity',)
 
     def __call__(self):
--- a/hooks/test/unittest_hooks.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/test/unittest_hooks.py	Wed Nov 03 16:38:28 2010 +0100
@@ -49,7 +49,7 @@
         self.commit()
 
     def test_delete_required_relations_object(self):
-        self.skip('no sample in the schema ! YAGNI ? Kermaat ?')
+        self.skipTest('no sample in the schema ! YAGNI ? Kermaat ?')
 
     def test_static_vocabulary_check(self):
         self.assertRaises(ValidationError,
@@ -63,14 +63,14 @@
                           self.commit)
 
     def test_inlined(self):
-        self.assertEquals(self.repo.schema['sender'].inlined, True)
+        self.assertEqual(self.repo.schema['sender'].inlined, True)
         self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')
         self.execute('INSERT EmailPart X: X content_format "text/plain", X ordernum 1, X content "this is a test"')
         eeid = self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y, X parts P '
                             'WHERE Y is EmailAddress, P is EmailPart')[0][0]
         self.execute('SET X sender Y WHERE X is Email, Y is EmailAddress')
         rset = self.execute('Any S WHERE X sender S, X eid %s' % eeid)
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
 
     def test_composite_1(self):
         self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')
@@ -81,10 +81,10 @@
         self.commit()
         self.execute('DELETE Email X')
         rset = self.execute('Any X WHERE X is EmailPart')
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
         self.commit()
         rset = self.execute('Any X WHERE X is EmailPart')
-        self.assertEquals(len(rset), 0)
+        self.assertEqual(len(rset), 0)
 
     def test_composite_2(self):
         self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')
@@ -96,7 +96,7 @@
         self.execute('DELETE EmailPart X')
         self.commit()
         rset = self.execute('Any X WHERE X is EmailPart')
-        self.assertEquals(len(rset), 0)
+        self.assertEqual(len(rset), 0)
 
     def test_composite_redirection(self):
         self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')
@@ -110,77 +110,74 @@
         self.execute('SET X parts Y WHERE X messageid "<2345>"')
         self.commit()
         rset = self.execute('Any X WHERE X is EmailPart')
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset.get_entity(0, 0).reverse_parts[0].messageid, '<2345>')
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.get_entity(0, 0).reverse_parts[0].messageid, '<2345>')
 
     def test_unsatisfied_constraints(self):
-        releid = self.execute('INSERT CWRelation X: X from_entity FE, X relation_type RT, X to_entity TE '
-                              'WHERE FE name "CWUser", RT name "in_group", TE name "String"')[0][0]
-        self.execute('SET X read_permission Y WHERE X eid %(x)s, Y name "managers"',
-                     {'x': releid}, 'x')
+        releid = self.execute('SET U in_group G WHERE G name "owners", U login "admin"')[0][0]
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors,
-                          {'to_entity-object': 'RQLConstraint O final FALSE failed'})
+        self.assertEqual(ex.errors,
+                          {'in_group-object': u'RQLConstraint NOT O name "owners" failed'})
 
     def test_html_tidy_hook(self):
         req = self.request()
         entity = req.create_entity('Workflow', name=u'wf1', description_format=u'text/html',
                                  description=u'yo')
-        self.assertEquals(entity.description, u'yo')
+        self.assertEqual(entity.description, u'yo')
         entity = req.create_entity('Workflow', name=u'wf2', description_format=u'text/html',
                                  description=u'<b>yo')
-        self.assertEquals(entity.description, u'<b>yo</b>')
+        self.assertEqual(entity.description, u'<b>yo</b>')
         entity = req.create_entity('Workflow', name=u'wf3', description_format=u'text/html',
                                  description=u'<b>yo</b>')
-        self.assertEquals(entity.description, u'<b>yo</b>')
+        self.assertEqual(entity.description, u'<b>yo</b>')
         entity = req.create_entity('Workflow', name=u'wf4', description_format=u'text/html',
                                  description=u'<b>R&D</b>')
-        self.assertEquals(entity.description, u'<b>R&amp;D</b>')
+        self.assertEqual(entity.description, u'<b>R&amp;D</b>')
         entity = req.create_entity('Workflow', name=u'wf5', description_format=u'text/html',
                                  description=u"<div>c&apos;est <b>l'ét&eacute;")
-        self.assertEquals(entity.description, u"<div>c'est <b>l'été</b></div>")
+        self.assertEqual(entity.description, u"<div>c'est <b>l'été</b></div>")
 
     def test_nonregr_html_tidy_hook_no_update(self):
         entity = self.request().create_entity('Workflow', name=u'wf1', description_format=u'text/html',
                                  description=u'yo')
         entity.set_attributes(name=u'wf2')
-        self.assertEquals(entity.description, u'yo')
+        self.assertEqual(entity.description, u'yo')
         entity.set_attributes(description=u'R&D<p>yo')
         entity.pop('description')
-        self.assertEquals(entity.description, u'R&amp;D<p>yo</p>')
+        self.assertEqual(entity.description, u'R&amp;D<p>yo</p>')
 
 
     def test_metadata_cwuri(self):
         entity = self.request().create_entity('Workflow', name=u'wf1')
-        self.assertEquals(entity.cwuri, self.repo.config['base-url'] + 'eid/%s' % entity.eid)
+        self.assertEqual(entity.cwuri, self.repo.config['base-url'] + 'eid/%s' % entity.eid)
 
     def test_metadata_creation_modification_date(self):
         _now = datetime.now()
         entity = self.request().create_entity('Workflow', name=u'wf1')
-        self.assertEquals((entity.creation_date - _now).seconds, 0)
-        self.assertEquals((entity.modification_date - _now).seconds, 0)
+        self.assertEqual((entity.creation_date - _now).seconds, 0)
+        self.assertEqual((entity.modification_date - _now).seconds, 0)
 
     def test_metadata_created_by(self):
         entity = self.request().create_entity('Bookmark', title=u'wf1', path=u'/view')
         self.commit() # fire operations
-        self.assertEquals(len(entity.created_by), 1) # make sure we have only one creator
-        self.assertEquals(entity.created_by[0].eid, self.session.user.eid)
+        self.assertEqual(len(entity.created_by), 1) # make sure we have only one creator
+        self.assertEqual(entity.created_by[0].eid, self.session.user.eid)
 
     def test_metadata_owned_by(self):
         entity = self.request().create_entity('Bookmark', title=u'wf1', path=u'/view')
         self.commit() # fire operations
-        self.assertEquals(len(entity.owned_by), 1) # make sure we have only one owner
-        self.assertEquals(entity.owned_by[0].eid, self.session.user.eid)
+        self.assertEqual(len(entity.owned_by), 1) # make sure we have only one owner
+        self.assertEqual(entity.owned_by[0].eid, self.session.user.eid)
 
     def test_user_login_stripped(self):
         u = self.create_user('  joe  ')
         tname = self.execute('Any L WHERE E login L, E eid %(e)s',
                              {'e': u.eid})[0][0]
-        self.assertEquals(tname, 'joe')
+        self.assertEqual(tname, 'joe')
         self.execute('SET X login " jijoe " WHERE X eid %(x)s', {'x': u.eid})
         tname = self.execute('Any L WHERE E login L, E eid %(e)s',
                              {'e': u.eid})[0][0]
-        self.assertEquals(tname, 'jijoe')
+        self.assertEqual(tname, 'jijoe')
 
 
 
@@ -201,15 +198,15 @@
 
     def test_user_group_synchronization(self):
         user = self.session.user
-        self.assertEquals(user.groups, set(('managers',)))
+        self.assertEqual(user.groups, set(('managers',)))
         self.execute('SET X in_group G WHERE X eid %s, G name "guests"' % user.eid)
-        self.assertEquals(user.groups, set(('managers',)))
+        self.assertEqual(user.groups, set(('managers',)))
         self.commit()
-        self.assertEquals(user.groups, set(('managers', 'guests')))
+        self.assertEqual(user.groups, set(('managers', 'guests')))
         self.execute('DELETE X in_group G WHERE X eid %s, G name "guests"' % user.eid)
-        self.assertEquals(user.groups, set(('managers', 'guests')))
+        self.assertEqual(user.groups, set(('managers', 'guests')))
         self.commit()
-        self.assertEquals(user.groups, set(('managers',)))
+        self.assertEqual(user.groups, set(('managers',)))
 
     def test_user_composite_owner(self):
         ueid = self.create_user('toto').eid
@@ -217,7 +214,7 @@
         self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", U use_email X '
                      'WHERE U login "toto"')
         self.commit()
-        self.assertEquals(self.execute('Any A WHERE X owned_by U, U use_email X,'
+        self.assertEqual(self.execute('Any A WHERE X owned_by U, U use_email X,'
                                        'U login "toto", X address A')[0][0],
                           'toto@logilab.fr')
 
@@ -233,23 +230,23 @@
     def test_unexistant_eproperty(self):
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'pkey-subject': 'unknown property key'})
+        self.assertEqual(ex.errors, {'pkey-subject': 'unknown property key'})
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
-        self.assertEquals(ex.errors, {'pkey-subject': 'unknown property key'})
+        self.assertEqual(ex.errors, {'pkey-subject': 'unknown property key'})
 
     def test_site_wide_eproperty(self):
         ex = self.assertRaises(ValidationError,
                                self.execute, 'INSERT CWProperty X: X pkey "ui.site-title", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'for_user-subject': "site-wide property can't be set for user"})
+        self.assertEqual(ex.errors, {'for_user-subject': "site-wide property can't be set for user"})
 
     def test_bad_type_eproperty(self):
         ex = self.assertRaises(ValidationError,
                                self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'value-subject': u'unauthorized value'})
+        self.assertEqual(ex.errors, {'value-subject': u'unauthorized value'})
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop"')
-        self.assertEquals(ex.errors, {'value-subject': u'unauthorized value'})
+        self.assertEqual(ex.errors, {'value-subject': u'unauthorized value'})
 
 
 class SchemaHooksTC(CubicWebTC):
@@ -269,7 +266,7 @@
             self.execute('INSERT CWUser X: X login "admin"')
         except ValidationError, ex:
             self.assertIsInstance(ex.entity, int)
-            self.assertEquals(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
+            self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
 
 if __name__ == '__main__':
--- a/hooks/test/unittest_syncschema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/test/unittest_syncschema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,6 +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.server.hooks.syncschema unit and functional tests"""
+
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb import ValidationError
@@ -98,7 +100,7 @@
         self.execute('Societe2 X WHERE X name "logilab"')
         self.execute('SET X concerne2 X WHERE X name "logilab"')
         rset = self.execute('Any X WHERE X concerne2 Y')
-        self.assertEquals(rset.rows, [[s2eid]])
+        self.assertEqual(rset.rows, [[s2eid]])
         # check that when a relation definition is deleted, existing relations are deleted
         rdefeid = self.execute('INSERT CWRelation X: X cardinality "**", X relation_type RT, '
                                '   X from_entity E, X to_entity E '
@@ -125,9 +127,9 @@
     def test_is_instance_of_insertions(self):
         seid = self.execute('INSERT Transition T: T name "subdiv"')[0][0]
         is_etypes = [etype for etype, in self.execute('Any ETN WHERE X eid %s, X is ET, ET name ETN' % seid)]
-        self.assertEquals(is_etypes, ['Transition'])
+        self.assertEqual(is_etypes, ['Transition'])
         instanceof_etypes = [etype for etype, in self.execute('Any ETN WHERE X eid %s, X is_instance_of ET, ET name ETN' % seid)]
-        self.assertEquals(sorted(instanceof_etypes), ['BaseTransition', 'Transition'])
+        self.assertEqual(sorted(instanceof_etypes), ['BaseTransition', 'Transition'])
         snames = [name for name, in self.execute('Any N WHERE S is BaseTransition, S name N')]
         self.failIf('subdiv' in snames)
         snames = [name for name, in self.execute('Any N WHERE S is_instance_of BaseTransition, S name N')]
@@ -136,27 +138,27 @@
 
     def test_perms_synchronization_1(self):
         schema = self.repo.schema
-        self.assertEquals(schema['CWUser'].get_groups('read'), set(('managers', 'users')))
+        self.assertEqual(schema['CWUser'].get_groups('read'), set(('managers', 'users')))
         self.failUnless(self.execute('Any X, Y WHERE X is CWEType, X name "CWUser", Y is CWGroup, Y name "users"')[0])
         self.execute('DELETE X read_permission Y WHERE X is CWEType, X name "CWUser", Y name "users"')
-        self.assertEquals(schema['CWUser'].get_groups('read'), set(('managers', 'users', )))
+        self.assertEqual(schema['CWUser'].get_groups('read'), set(('managers', 'users', )))
         self.commit()
-        self.assertEquals(schema['CWUser'].get_groups('read'), set(('managers',)))
+        self.assertEqual(schema['CWUser'].get_groups('read'), set(('managers',)))
         self.execute('SET X read_permission Y WHERE X is CWEType, X name "CWUser", Y name "users"')
         self.commit()
-        self.assertEquals(schema['CWUser'].get_groups('read'), set(('managers', 'users',)))
+        self.assertEqual(schema['CWUser'].get_groups('read'), set(('managers', 'users',)))
 
     def test_perms_synchronization_2(self):
         schema = self.repo.schema['in_group'].rdefs[('CWUser', 'CWGroup')]
-        self.assertEquals(schema.get_groups('read'), set(('managers', 'users', 'guests')))
+        self.assertEqual(schema.get_groups('read'), set(('managers', 'users', 'guests')))
         self.execute('DELETE X read_permission Y WHERE X relation_type RT, RT name "in_group", Y name "guests"')
-        self.assertEquals(schema.get_groups('read'), set(('managers', 'users', 'guests')))
+        self.assertEqual(schema.get_groups('read'), set(('managers', 'users', 'guests')))
         self.commit()
-        self.assertEquals(schema.get_groups('read'), set(('managers', 'users')))
+        self.assertEqual(schema.get_groups('read'), set(('managers', 'users')))
         self.execute('SET X read_permission Y WHERE X relation_type RT, RT name "in_group", Y name "guests"')
-        self.assertEquals(schema.get_groups('read'), set(('managers', 'users')))
+        self.assertEqual(schema.get_groups('read'), set(('managers', 'users')))
         self.commit()
-        self.assertEquals(schema.get_groups('read'), set(('managers', 'users', 'guests')))
+        self.assertEqual(schema.get_groups('read'), set(('managers', 'users', 'guests')))
 
     def test_nonregr_user_edit_itself(self):
         ueid = self.session.user.eid
@@ -187,7 +189,10 @@
             self.failIf(self.schema['state_of'].inlined)
             self.failIf(self.index_exists('State', 'state_of'))
             rset = self.execute('Any X, Y WHERE X state_of Y')
-            self.assertEquals(len(rset), 2) # user states
+            self.assertEqual(len(rset), 2) # user states
+        except:
+            import traceback
+            traceback.print_exc()
         finally:
             self.execute('SET X inlined TRUE WHERE X name "state_of"')
             self.failIf(self.schema['state_of'].inlined)
@@ -195,7 +200,7 @@
             self.failUnless(self.schema['state_of'].inlined)
             self.failUnless(self.index_exists('State', 'state_of'))
             rset = self.execute('Any X, Y WHERE X state_of Y')
-            self.assertEquals(len(rset), 2)
+            self.assertEqual(len(rset), 2)
 
     def test_indexed_change(self):
         self.session.set_pool()
@@ -255,6 +260,7 @@
         self.commit()
         # should not be able anymore to add cwuser without surname
         self.assertRaises(ValidationError, self.create_user, "toto")
+        self.rollback()
         self.execute('SET DEF cardinality "?1" '
                      'WHERE DEF relation_type RT, DEF from_entity E,'
                      'RT name "surname", E name "CWUser"')
@@ -314,7 +320,7 @@
         rdef = self.schema['Transition'].rdef('type')
         cstr = rdef.constraint_by_type('StaticVocabularyConstraint')
         if not getattr(cstr, 'eid', None):
-            self.skip('start me alone') # bug in schema reloading, constraint's eid not restored
+            self.skipTest('start me alone') # bug in schema reloading, constraint's eid not restored
         self.execute('SET X value %(v)s WHERE X eid %(x)s',
                      {'x': cstr.eid, 'v': u"u'normal', u'auto', u'new'"})
         self.execute('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
@@ -322,7 +328,7 @@
                      {'ct': 'SizeConstraint', 'value': u'max=10', 'x': rdef.eid})
         self.commit()
         cstr = rdef.constraint_by_type('StaticVocabularyConstraint')
-        self.assertEquals(cstr.values, (u'normal', u'auto', u'new'))
+        self.assertEqual(cstr.values, (u'normal', u'auto', u'new'))
         self.execute('INSERT Transition T: T name "hop", T type "new"')
 
 if __name__ == '__main__':
--- a/hooks/workflow.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/hooks/workflow.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Core hooks: workflow related hooks
+"""Core hooks: workflow related hooks"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from datetime import datetime
@@ -25,8 +24,7 @@
 from yams.schema import role_name
 
 from cubicweb import RepositoryError, ValidationError
-from cubicweb.interfaces import IWorkflowable
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance, adaptable
 from cubicweb.server import hook
 
 
@@ -51,11 +49,12 @@
     def precommit_event(self):
         session = self.session
         entity = self.entity
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         # if there is an initial state and the entity's state is not set,
         # use the initial state as a default state
         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
-               and entity.current_workflow:
-            state = entity.current_workflow.initial
+               and iworkflowable.current_workflow:
+            state = iworkflowable.current_workflow.initial
             if state:
                 session.add_relation(entity.eid, 'in_state', state.eid)
                 _FireAutotransitionOp(session, entity=entity)
@@ -65,10 +64,11 @@
 
     def precommit_event(self):
         entity = self.entity
-        autotrs = list(entity.possible_transitions('auto'))
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
+        autotrs = list(iworkflowable.possible_transitions('auto'))
         if autotrs:
             assert len(autotrs) == 1
-            entity.fire_transition(autotrs[0])
+            iworkflowable.fire_transition(autotrs[0])
 
 
 class _WorkflowChangedOp(hook.Operation):
@@ -82,29 +82,30 @@
         if self.eid in pendingeids:
             return
         entity = session.entity_from_eid(self.eid)
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         # check custom workflow has not been rechanged to another one in the same
         # transaction
-        mainwf = entity.main_workflow
+        mainwf = iworkflowable.main_workflow
         if mainwf.eid == self.wfeid:
             deststate = mainwf.initial
             if not deststate:
                 qname = role_name('custom_workflow', 'subject')
                 msg = session._('workflow has no initial state')
                 raise ValidationError(entity.eid, {qname: msg})
-            if mainwf.state_by_eid(entity.current_state.eid):
+            if mainwf.state_by_eid(iworkflowable.current_state.eid):
                 # nothing to do
                 return
             # if there are no history, simply go to new workflow's initial state
-            if not entity.workflow_history:
-                if entity.current_state.eid != deststate.eid:
+            if not iworkflowable.workflow_history:
+                if iworkflowable.current_state.eid != deststate.eid:
                     _change_state(session, entity.eid,
-                                  entity.current_state.eid, deststate.eid)
+                                  iworkflowable.current_state.eid, deststate.eid)
                     _FireAutotransitionOp(session, entity=entity)
                 return
             msg = session._('workflow changed to "%s"')
             msg %= session._(mainwf.name)
             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
-            entity.change_state(deststate, msg, u'text/plain')
+            iworkflowable.change_state(deststate, msg, u'text/plain')
 
 
 class _CheckTrExitPoint(hook.Operation):
@@ -125,9 +126,10 @@
     def precommit_event(self):
         session = self.session
         forentity = self.forentity
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
         trinfo = self.trinfo
         # we're in a subworkflow, check if we've reached an exit point
-        wftr = forentity.subworkflow_input_transition()
+        wftr = iworkflowable.subworkflow_input_transition()
         if wftr is None:
             # inconsistency detected
             qname = role_name('to_state', 'subject')
@@ -137,9 +139,9 @@
         if tostate is not None:
             # reached an exit point
             msg = session._('exiting from subworkflow %s')
-            msg %= session._(forentity.current_workflow.name)
+            msg %= session._(iworkflowable.current_workflow.name)
             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
-            forentity.change_state(tostate, msg, u'text/plain', tr=wftr)
+            iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
 
 
 # hooks ########################################################################
@@ -151,7 +153,7 @@
 
 class SetInitialStateHook(WorkflowHook):
     __regid__ = 'wfsetinitial'
-    __select__ = WorkflowHook.__select__ & implements(IWorkflowable)
+    __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -175,7 +177,7 @@
     * by_transition or to_state (managers only) inlined relation is set
     """
     __regid__ = 'wffiretransition'
-    __select__ = WorkflowHook.__select__ & implements('TrInfo')
+    __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
     events = ('before_add_entity',)
 
     def __call__(self):
@@ -189,18 +191,19 @@
             msg = session._('mandatory relation')
             raise ValidationError(entity.eid, {qname: msg})
         forentity = session.entity_from_eid(foreid)
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
         # then check it has a workflow set, unless we're in the process of changing
         # entity's workflow
         if session.transaction_data.get((forentity.eid, 'customwf')):
             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
             wf = session.entity_from_eid(wfeid)
         else:
-            wf = forentity.current_workflow
+            wf = iworkflowable.current_workflow
         if wf is None:
             msg = session._('related entity has no workflow set')
             raise ValidationError(entity.eid, {None: msg})
         # then check it has a state set
-        fromstate = forentity.current_state
+        fromstate = iworkflowable.current_state
         if fromstate is None:
             msg = session._('related entity has no state')
             raise ValidationError(entity.eid, {None: msg})
@@ -270,7 +273,7 @@
 class FiredTransitionHook(WorkflowHook):
     """change related entity state"""
     __regid__ = 'wffiretransition'
-    __select__ = WorkflowHook.__select__ & implements('TrInfo')
+    __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -278,8 +281,9 @@
         _change_state(self._cw, trinfo['wf_info_for'],
                       trinfo['from_state'], trinfo['to_state'])
         forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
-        assert forentity.current_state.eid == trinfo['to_state']
-        if forentity.main_workflow.eid != forentity.current_workflow.eid:
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
+        assert iworkflowable.current_state.eid == trinfo['to_state']
+        if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
             _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
 
 
@@ -297,7 +301,8 @@
             # state changed through TrInfo insertion, so we already know it's ok
             return
         entity = session.entity_from_eid(self.eidfrom)
-        mainwf = entity.main_workflow
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
+        mainwf = iworkflowable.main_workflow
         if mainwf is None:
             msg = session._('entity has no workflow set')
             raise ValidationError(entity.eid, {None: msg})
@@ -309,7 +314,7 @@
             msg = session._("state doesn't belong to entity's workflow. You may "
                             "want to set a custom workflow for this entity first.")
             raise ValidationError(self.eidfrom, {qname: msg})
-        if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+        if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
             qname = role_name('in_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
             raise ValidationError(self.eidfrom, {qname: msg})
@@ -359,7 +364,7 @@
 
     def __call__(self):
         entity = self._cw.entity_from_eid(self.eidfrom)
-        typewf = entity.cwetype_workflow()
+        typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow()
         if typewf is not None:
             _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
 
--- a/i18n.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/i18n.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""Some i18n/gettext utilities.
+"""Some i18n/gettext utilities."""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import re
--- a/i18n/en.po	Tue Jul 27 12:36:03 2010 +0200
+++ b/i18n/en.po	Wed Nov 03 16:38:28 2010 +0100
@@ -5,9 +5,10 @@
 msgstr ""
 "Project-Id-Version: 2.0\n"
 "POT-Creation-Date: 2006-01-12 17:35+CET\n"
-"PO-Revision-Date: 2010-05-16 18:58+0200\n"
+"PO-Revision-Date: 2010-09-15 14:55+0200\n"
 "Last-Translator: Sylvain Thenault <sylvain.thenault@logilab.fr>\n"
 "Language-Team: English <devel@logilab.fr.org>\n"
+"Language: en\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -181,6 +182,9 @@
 "can also display a <a href=\"%s\">complete schema with meta-data</a>.</div>"
 msgstr ""
 
+msgid "<not specified>"
+msgstr ""
+
 msgid "?*"
 msgstr "0..1 0..n"
 
@@ -235,6 +239,9 @@
 msgid "Browse by category"
 msgstr ""
 
+msgid "Browse by entity type"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -313,6 +320,12 @@
 msgid "CWRelation_plural"
 msgstr "Relations"
 
+msgid "CWUniqueTogetherConstraint"
+msgstr ""
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr ""
+
 msgid "CWUser"
 msgstr "User"
 
@@ -355,6 +368,10 @@
 "supported"
 msgstr ""
 
+#, python-format
+msgid "Data connection graph for %s"
+msgstr ""
+
 msgid "Date"
 msgstr "Date"
 
@@ -379,10 +396,10 @@
 msgid "Download schema as OWL"
 msgstr ""
 
-msgctxt "inlined:CWUser.use_email.subject"
 msgid "EmailAddress"
 msgstr "Email address"
 
+msgctxt "inlined:CWUser.use_email.subject"
 msgid "EmailAddress"
 msgstr "Email address"
 
@@ -419,6 +436,9 @@
 msgid "Garbage collection information"
 msgstr ""
 
+msgid "Got rhythm?"
+msgstr ""
+
 msgid "Help"
 msgstr ""
 
@@ -485,6 +505,9 @@
 msgid "New CWRelation"
 msgstr "New relation"
 
+msgid "New CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "New CWUser"
 msgstr "New user"
 
@@ -515,6 +538,10 @@
 msgid "New WorkflowTransition"
 msgstr "New workflow-transition"
 
+#, python-format
+msgid "No account? Try public access at %s"
+msgstr ""
+
 msgid "No result matching query"
 msgstr ""
 
@@ -524,7 +551,7 @@
 msgid "OR"
 msgstr ""
 
-msgid "Parent classes:"
+msgid "Parent class:"
 msgstr ""
 
 msgid "Password"
@@ -624,9 +651,6 @@
 msgid "Submit bug report by mail"
 msgstr ""
 
-msgid "The repository holds the following entities"
-msgstr ""
-
 #, python-format
 msgid "The view %s can not be applied to this query"
 msgstr ""
@@ -674,6 +698,9 @@
 msgid "This CWRelation"
 msgstr "This relation"
 
+msgid "This CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "This CWUser"
 msgstr "This user"
 
@@ -868,6 +895,9 @@
 msgid "add CWRelation relation_type CWRType object"
 msgstr "relation definition"
 
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr ""
+
 msgid "add CWUser in_group CWGroup object"
 msgstr "user"
 
@@ -926,9 +956,6 @@
 msgid "add a new permission"
 msgstr ""
 
-msgid "add_perm"
-msgstr "add permission"
-
 # subject and object forms for each relation type
 # (no object form for final relation types)
 msgid "add_permission"
@@ -944,6 +971,9 @@
 msgid "add_permission"
 msgstr "add permission"
 
+msgid "add_permission_object"
+msgstr "has permission to add"
+
 msgctxt "CWGroup"
 msgid "add_permission_object"
 msgstr "can add"
@@ -952,8 +982,8 @@
 msgid "add_permission_object"
 msgstr "used to define add permission on"
 
-msgid "add_permission_object"
-msgstr "has permission to add"
+msgid "add_relation"
+msgstr "add"
 
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
@@ -961,8 +991,8 @@
 
 #, python-format
 msgid ""
-"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%"
-"(eidto)s"
+"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #"
+"%(eidto)s"
 msgstr ""
 
 msgid "addrelated"
@@ -995,6 +1025,9 @@
 msgid "allowed_transition"
 msgstr "allowed transition"
 
+msgid "allowed_transition_object"
+msgstr "incoming states"
+
 msgctxt "BaseTransition"
 msgid "allowed_transition_object"
 msgstr "incoming states"
@@ -1007,9 +1040,6 @@
 msgid "allowed_transition_object"
 msgstr "incoming states"
 
-msgid "allowed_transition_object"
-msgstr "incoming states"
-
 msgid "am/pm calendar (month)"
 msgstr ""
 
@@ -1025,13 +1055,13 @@
 msgid "an electronic mail address associated to a short alias"
 msgstr ""
 
-msgid "an error occured"
-msgstr ""
-
-msgid "an error occured while processing your request"
-msgstr ""
-
-msgid "an error occured, the request cannot be fulfilled"
+msgid "an error occurred"
+msgstr ""
+
+msgid "an error occurred while processing your request"
+msgstr ""
+
+msgid "an error occurred, the request cannot be fulfilled"
 msgstr ""
 
 msgid "an integer is expected"
@@ -1093,13 +1123,13 @@
 msgid "bookmarked_by"
 msgstr "bookmarked by"
 
+msgid "bookmarked_by_object"
+msgstr "has bookmarks"
+
 msgctxt "CWUser"
 msgid "bookmarked_by_object"
 msgstr "uses bookmarks"
 
-msgid "bookmarked_by_object"
-msgstr "has bookmarks"
-
 msgid "bookmarks"
 msgstr ""
 
@@ -1185,6 +1215,9 @@
 msgid "by_transition"
 msgstr "by transition"
 
+msgid "by_transition_object"
+msgstr "transition information"
+
 msgctxt "BaseTransition"
 msgid "by_transition_object"
 msgstr "transition information"
@@ -1197,9 +1230,6 @@
 msgid "by_transition_object"
 msgstr "transition information"
 
-msgid "by_transition_object"
-msgstr "transition information"
-
 msgid "calendar"
 msgstr ""
 
@@ -1282,6 +1312,12 @@
 msgid "click on the box to cancel the deletion"
 msgstr ""
 
+msgid "click to add a value"
+msgstr ""
+
+msgid "click to delete this value"
+msgstr ""
+
 msgid "click to edit this field"
 msgstr ""
 
@@ -1375,10 +1411,10 @@
 msgid "condition"
 msgstr "condition"
 
-msgctxt "RQLExpression"
 msgid "condition_object"
 msgstr "condition of"
 
+msgctxt "RQLExpression"
 msgid "condition_object"
 msgstr "condition of"
 
@@ -1405,16 +1441,30 @@
 msgid "constrained_by"
 msgstr "constrained by"
 
-msgctxt "CWConstraint"
 msgid "constrained_by_object"
 msgstr "constraints"
 
+msgctxt "CWConstraint"
 msgid "constrained_by_object"
 msgstr "constraints"
 
 msgid "constraint factory"
 msgstr ""
 
+msgid "constraint_of"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr ""
+
+msgid "constraint_of_object"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr ""
+
 msgid "constraints"
 msgstr ""
 
@@ -1470,6 +1520,9 @@
 msgid "context where this component should be displayed"
 msgstr ""
 
+msgid "context where this facet should be displayed, leave empty for both"
+msgstr ""
+
 msgid "control subject entity's relations order"
 msgstr ""
 
@@ -1535,6 +1588,11 @@
 msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
 msgstr "creating relation %(linkto)s"
 
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr ""
+
 msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
 msgstr "creating a new user in group %(linkto)s"
 
@@ -1616,8 +1674,8 @@
 msgstr "creating workflow-transition leading to state %(linkto)s"
 
 msgid ""
-"creating WorkflowTransition (WorkflowTransition transition_of Workflow %"
-"(linkto)s)"
+"creating WorkflowTransition (WorkflowTransition transition_of Workflow "
+"%(linkto)s)"
 msgstr "creating workflow-transition of workflow %(linkto)s"
 
 msgid "creation"
@@ -1639,13 +1697,13 @@
 msgid "cstrtype"
 msgstr "constraint type"
 
+msgid "cstrtype_object"
+msgstr "used by"
+
 msgctxt "CWConstraintType"
 msgid "cstrtype_object"
 msgstr "constraint type of"
 
-msgid "cstrtype_object"
-msgstr "used by"
-
 msgid "csv entities export"
 msgstr ""
 
@@ -1728,10 +1786,10 @@
 msgid "default_workflow"
 msgstr "default workflow"
 
-msgctxt "Workflow"
 msgid "default_workflow_object"
 msgstr "default workflow of"
 
+msgctxt "Workflow"
 msgid "default_workflow_object"
 msgstr "default workflow of"
 
@@ -1776,6 +1834,9 @@
 msgid "define how we get out from a sub-workflow"
 msgstr ""
 
+msgid "defines a sql-level multicolumn unique index"
+msgstr ""
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -1793,9 +1854,6 @@
 msgid "delete this relation"
 msgstr ""
 
-msgid "delete_perm"
-msgstr "delete permission"
-
 msgid "delete_permission"
 msgstr "can be deleted by"
 
@@ -1807,6 +1865,9 @@
 msgid "delete_permission"
 msgstr "delete_permission"
 
+msgid "delete_permission_object"
+msgstr "has permission to delete"
+
 msgctxt "CWGroup"
 msgid "delete_permission_object"
 msgstr "has permission to delete"
@@ -1815,17 +1876,14 @@
 msgid "delete_permission_object"
 msgstr "has permission to delete"
 
-msgid "delete_permission_object"
-msgstr "has permission to delete"
-
 #, python-format
 msgid "deleted %(etype)s #%(eid)s (%(title)s)"
 msgstr ""
 
 #, python-format
 msgid ""
-"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%"
-"(eidto)s"
+"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #"
+"%(eidto)s"
 msgstr ""
 
 msgid "depends on the constraint type"
@@ -1834,15 +1892,7 @@
 msgid "description"
 msgstr "description"
 
-msgctxt "CWEType"
-msgid "description"
-msgstr "description"
-
-msgctxt "CWRelation"
-msgid "description"
-msgstr "description"
-
-msgctxt "Workflow"
+msgctxt "BaseTransition"
 msgid "description"
 msgstr "description"
 
@@ -1850,15 +1900,7 @@
 msgid "description"
 msgstr "description"
 
-msgctxt "Transition"
-msgid "description"
-msgstr "description"
-
-msgctxt "WorkflowTransition"
-msgid "description"
-msgstr "description"
-
-msgctxt "State"
+msgctxt "CWEType"
 msgid "description"
 msgstr "description"
 
@@ -1866,10 +1908,34 @@
 msgid "description"
 msgstr "description"
 
-msgctxt "BaseTransition"
+msgctxt "CWRelation"
+msgid "description"
+msgstr "description"
+
+msgctxt "State"
+msgid "description"
+msgstr "description"
+
+msgctxt "Transition"
 msgid "description"
 msgstr "description"
 
+msgctxt "Workflow"
+msgid "description"
+msgstr "description"
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr "description"
+
+msgid "description_format"
+msgstr "format"
+
+msgctxt "BaseTransition"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWAttribute"
 msgid "description_format"
 msgstr "format"
 
@@ -1877,38 +1943,30 @@
 msgid "description_format"
 msgstr "format"
 
+msgctxt "CWRType"
+msgid "description_format"
+msgstr "format"
+
 msgctxt "CWRelation"
 msgid "description_format"
 msgstr "format"
 
+msgctxt "State"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "Transition"
+msgid "description_format"
+msgstr "format"
+
 msgctxt "Workflow"
 msgid "description_format"
 msgstr "format"
 
-msgctxt "CWAttribute"
-msgid "description_format"
-msgstr "format"
-
-msgctxt "Transition"
-msgid "description_format"
-msgstr "format"
-
 msgctxt "WorkflowTransition"
 msgid "description_format"
 msgstr "format"
 
-msgctxt "State"
-msgid "description_format"
-msgstr "format"
-
-msgctxt "CWRType"
-msgid "description_format"
-msgstr "format"
-
-msgctxt "BaseTransition"
-msgid "description_format"
-msgstr "format"
-
 msgid "destination state for this transition"
 msgstr ""
 
@@ -1926,21 +1984,21 @@
 msgid "destination_state"
 msgstr "destination state"
 
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr "destination state"
+
 msgctxt "Transition"
 msgid "destination_state"
 msgstr "destination state"
 
-msgctxt "SubWorkflowExitPoint"
-msgid "destination_state"
-msgstr "destination state"
+msgid "destination_state_object"
+msgstr "destination of"
 
 msgctxt "State"
 msgid "destination_state_object"
 msgstr "destination of"
 
-msgid "destination_state_object"
-msgstr "destination of"
-
 msgid "detach attached file"
 msgstr ""
 
@@ -1950,12 +2008,18 @@
 msgid "display order of the component"
 msgstr ""
 
+msgid "display order of the facet"
+msgstr ""
+
 msgid "display the box or not"
 msgstr ""
 
 msgid "display the component or not"
 msgstr ""
 
+msgid "display the facet or not"
+msgstr ""
+
 msgid ""
 "distinct label to distinguate between other permission entity of the same "
 "name"
@@ -1986,9 +2050,6 @@
 msgid "editable-table"
 msgstr ""
 
-msgid "edition"
-msgstr ""
-
 msgid "eid"
 msgstr ""
 
@@ -2001,6 +2062,9 @@
 msgid "embed"
 msgstr ""
 
+msgid "embedded html"
+msgstr ""
+
 msgid "embedding this url is forbidden"
 msgstr ""
 
@@ -2062,7 +2126,7 @@
 msgid "eta_date"
 msgstr "ETA date"
 
-msgid "exit state must a subworkflow state"
+msgid "exit state must be a subworkflow state"
 msgstr ""
 
 msgid "exit_point"
@@ -2185,13 +2249,13 @@
 msgid "for_user"
 msgstr "for user"
 
+msgid "for_user_object"
+msgstr "use properties"
+
 msgctxt "CWUser"
 msgid "for_user_object"
 msgstr "property of"
 
-msgid "for_user_object"
-msgstr "use properties"
-
 msgid "friday"
 msgstr ""
 
@@ -2213,13 +2277,13 @@
 msgid "from_entity"
 msgstr "from entity"
 
+msgid "from_entity_object"
+msgstr "subjet relation"
+
 msgctxt "CWEType"
 msgid "from_entity_object"
 msgstr "subjec relation"
 
-msgid "from_entity_object"
-msgstr "subjet relation"
-
 msgid "from_interval_start"
 msgstr "from"
 
@@ -2230,10 +2294,10 @@
 msgid "from_state"
 msgstr "from state"
 
-msgctxt "State"
 msgid "from_state_object"
 msgstr "transitions from this state"
 
+msgctxt "State"
 msgid "from_state_object"
 msgstr "transitions from this state"
 
@@ -2271,15 +2335,20 @@
 msgid "granted to groups"
 msgstr ""
 
-msgid "graphical representation of the instance'schema"
+#, python-format
+msgid "graphical representation of %(appid)s data model"
 msgstr ""
 
 #, python-format
-msgid "graphical schema for %s"
+msgid ""
+"graphical representation of the %(etype)s entity type from %(appid)s data "
+"model"
 msgstr ""
 
 #, python-format
-msgid "graphical workflow for %s"
+msgid ""
+"graphical representation of the %(rtype)s relation type from %(appid)s data "
+"model"
 msgstr ""
 
 msgid "group in which a user should be to be allowed to pass this transition"
@@ -2386,10 +2455,10 @@
 msgid "in_group"
 msgstr "in group"
 
-msgctxt "CWGroup"
 msgid "in_group_object"
 msgstr "contains"
 
+msgctxt "CWGroup"
 msgid "in_group_object"
 msgstr "contains"
 
@@ -2444,10 +2513,10 @@
 msgid "initial_state"
 msgstr "initial state"
 
-msgctxt "State"
 msgid "initial_state_object"
 msgstr "initial state of"
 
+msgctxt "State"
 msgid "initial_state_object"
 msgstr "initial state of"
 
@@ -2679,6 +2748,9 @@
 msgid "missing parameters for entity %s"
 msgstr ""
 
+msgid "modification"
+msgstr ""
+
 msgid "modification_date"
 msgstr "modification date"
 
@@ -2703,15 +2775,19 @@
 msgid "name"
 msgstr ""
 
-msgctxt "CWEType"
+msgctxt "BaseTransition"
+msgid "name"
+msgstr "name"
+
+msgctxt "CWCache"
 msgid "name"
-msgstr ""
-
-msgctxt "Transition"
+msgstr "name"
+
+msgctxt "CWConstraintType"
 msgid "name"
 msgstr ""
 
-msgctxt "Workflow"
+msgctxt "CWEType"
 msgid "name"
 msgstr ""
 
@@ -2719,18 +2795,6 @@
 msgid "name"
 msgstr ""
 
-msgctxt "CWConstraintType"
-msgid "name"
-msgstr ""
-
-msgctxt "WorkflowTransition"
-msgid "name"
-msgstr ""
-
-msgctxt "State"
-msgid "name"
-msgstr "name"
-
 msgctxt "CWPermission"
 msgid "name"
 msgstr "name"
@@ -2739,13 +2803,21 @@
 msgid "name"
 msgstr "name"
 
-msgctxt "BaseTransition"
+msgctxt "State"
 msgid "name"
 msgstr "name"
 
-msgctxt "CWCache"
+msgctxt "Transition"
+msgid "name"
+msgstr ""
+
+msgctxt "Workflow"
 msgid "name"
-msgstr "name"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "name"
+msgstr ""
 
 msgid "name of the cache"
 msgstr ""
@@ -2801,6 +2873,9 @@
 msgid "no edited fields specified for entity %s"
 msgstr ""
 
+msgid "no related entity"
+msgstr ""
+
 msgid "no related project"
 msgstr ""
 
@@ -2926,9 +3001,6 @@
 msgid "permissions for this entity"
 msgstr ""
 
-msgid "personnal informations"
-msgstr ""
-
 msgid "pick existing bookmarks"
 msgstr ""
 
@@ -2958,10 +3030,10 @@
 msgid "prefered_form"
 msgstr "prefered form"
 
-msgctxt "EmailAddress"
 msgid "prefered_form_object"
 msgstr "prefered over"
 
+msgctxt "EmailAddress"
 msgid "prefered_form_object"
 msgstr "prefered over"
 
@@ -2981,12 +3053,15 @@
 msgid "primary_email"
 msgstr "primary email"
 
+msgid "primary_email_object"
+msgstr "primary email of"
+
 msgctxt "EmailAddress"
 msgid "primary_email_object"
 msgstr "primary email of"
 
-msgid "primary_email_object"
-msgstr "primary email of"
+msgid "profile"
+msgstr ""
 
 msgid "progress"
 msgstr ""
@@ -3006,17 +3081,14 @@
 msgid "read"
 msgstr ""
 
-msgid "read_perm"
-msgstr "read permission"
-
 msgid "read_permission"
 msgstr "can be read by"
 
-msgctxt "CWEType"
+msgctxt "CWAttribute"
 msgid "read_permission"
 msgstr "read permission"
 
-msgctxt "CWAttribute"
+msgctxt "CWEType"
 msgid "read_permission"
 msgstr "read permission"
 
@@ -3024,6 +3096,9 @@
 msgid "read_permission"
 msgstr "read permission"
 
+msgid "read_permission_object"
+msgstr "has permission to read"
+
 msgctxt "CWGroup"
 msgid "read_permission_object"
 msgstr "can be read by"
@@ -3032,9 +3107,6 @@
 msgid "read_permission_object"
 msgstr "can be read by"
 
-msgid "read_permission_object"
-msgstr "has permission to delete"
-
 msgid "registry"
 msgstr ""
 
@@ -3068,16 +3140,34 @@
 msgid "relation_type"
 msgstr "relation type"
 
+msgid "relation_type_object"
+msgstr "relation definitions"
+
 msgctxt "CWRType"
 msgid "relation_type_object"
 msgstr "relation definitions"
 
-msgid "relation_type_object"
-msgstr "relation definitions"
+msgid "relations"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr ""
 
 msgid "relations deleted"
 msgstr ""
 
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr ""
+
 msgid "relative url of the bookmarked page"
 msgstr ""
 
@@ -3091,11 +3181,11 @@
 msgid "require_group"
 msgstr "require group"
 
-msgctxt "Transition"
+msgctxt "CWPermission"
 msgid "require_group"
 msgstr "require group"
 
-msgctxt "CWPermission"
+msgctxt "Transition"
 msgid "require_group"
 msgstr "require group"
 
@@ -3103,10 +3193,10 @@
 msgid "require_group"
 msgstr "require group"
 
-msgctxt "CWGroup"
 msgid "require_group_object"
 msgstr "required by"
 
+msgctxt "CWGroup"
 msgid "require_group_object"
 msgstr "required by"
 
@@ -3148,7 +3238,7 @@
 msgstr ""
 
 msgid "same_as"
-msgstr ""
+msgstr "same as"
 
 msgid "sample format"
 msgstr ""
@@ -3277,7 +3367,7 @@
 msgid "site-wide property can't be set for user"
 msgstr ""
 
-msgid "some errors occured:"
+msgid "some errors occurred:"
 msgstr ""
 
 msgid "some later transaction(s) touch entity, undo them first"
@@ -3299,10 +3389,10 @@
 msgid "specializes"
 msgstr "specializes"
 
-msgctxt "CWEType"
 msgid "specializes_object"
 msgstr "specialized by"
 
+msgctxt "CWEType"
 msgid "specializes_object"
 msgstr "specialized by"
 
@@ -3339,10 +3429,10 @@
 msgid "state_of"
 msgstr "state of"
 
-msgctxt "Workflow"
 msgid "state_of_object"
 msgstr "use states"
 
+msgctxt "Workflow"
 msgid "state_of_object"
 msgstr "use states"
 
@@ -3386,20 +3476,20 @@
 msgid "subworkflow_exit"
 msgstr "subworkflow exit"
 
+msgid "subworkflow_exit_object"
+msgstr "subworkflow exit of"
+
 msgctxt "SubWorkflowExitPoint"
 msgid "subworkflow_exit_object"
 msgstr "subworkflow exit of"
 
-msgid "subworkflow_exit_object"
-msgstr "subworkflow exit of"
+msgid "subworkflow_object"
+msgstr "subworkflow of"
 
 msgctxt "Workflow"
 msgid "subworkflow_object"
 msgstr "subworkflow of"
 
-msgid "subworkflow_object"
-msgstr "subworkflow of"
-
 msgid "subworkflow_state"
 msgstr "subworkflow state"
 
@@ -3407,10 +3497,10 @@
 msgid "subworkflow_state"
 msgstr "subworkflow state"
 
-msgctxt "State"
 msgid "subworkflow_state_object"
 msgstr "exit point"
 
+msgctxt "State"
 msgid "subworkflow_state_object"
 msgstr "exit point"
 
@@ -3524,10 +3614,10 @@
 msgid "to_entity"
 msgstr "to entity"
 
-msgctxt "CWEType"
 msgid "to_entity_object"
 msgstr "object relations"
 
+msgctxt "CWEType"
 msgid "to_entity_object"
 msgstr "object relations"
 
@@ -3541,10 +3631,10 @@
 msgid "to_state"
 msgstr "to state"
 
-msgctxt "State"
 msgid "to_state_object"
 msgstr "transitions to this state"
 
+msgctxt "State"
 msgid "to_state_object"
 msgstr "transitions to this state"
 
@@ -3585,10 +3675,10 @@
 msgid "transition_of"
 msgstr "transition of"
 
-msgctxt "Workflow"
 msgid "transition_of_object"
 msgstr "use transitions"
 
+msgctxt "Workflow"
 msgid "transition_of_object"
 msgstr "use transitions"
 
@@ -3686,9 +3776,10 @@
 msgid "update"
 msgstr ""
 
-msgid "update_perm"
-msgstr "update permission"
-
+msgid "update_permission"
+msgstr "can be updated by"
+
+msgctxt "CWAttribute"
 msgid "update_permission"
 msgstr "can be updated by"
 
@@ -3696,9 +3787,8 @@
 msgid "update_permission"
 msgstr "can be updated by"
 
-msgctxt "CWAttribute"
-msgid "update_permission"
-msgstr "can be updated by"
+msgid "update_permission_object"
+msgstr "has permission to update"
 
 msgctxt "CWGroup"
 msgid "update_permission_object"
@@ -3708,8 +3798,8 @@
 msgid "update_permission_object"
 msgstr "has permission to update"
 
-msgid "update_permission_object"
-msgstr "has permission to update"
+msgid "update_relation"
+msgstr "update"
 
 msgid "updated"
 msgstr ""
@@ -3741,10 +3831,10 @@
 msgid "use_email"
 msgstr "use email"
 
-msgctxt "EmailAddress"
 msgid "use_email_object"
 msgstr "used by"
 
+msgctxt "EmailAddress"
 msgid "use_email_object"
 msgstr "used by"
 
@@ -3854,6 +3944,10 @@
 msgid "view_index"
 msgstr "index"
 
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr "violates unique_together constraints (%s)"
+
 msgid "visible"
 msgstr ""
 
@@ -3923,10 +4017,10 @@
 msgid "workflow_of"
 msgstr "workflow of"
 
-msgctxt "CWEType"
 msgid "workflow_of_object"
 msgstr "may use workflow"
 
+msgctxt "CWEType"
 msgid "workflow_of_object"
 msgstr "may use workflow"
 
@@ -3951,3 +4045,15 @@
 
 msgid "you should probably delete that property"
 msgstr ""
+
+#~ msgid "add_perm"
+#~ msgstr "add permission"
+
+#~ msgid "delete_perm"
+#~ msgstr "delete permission"
+
+#~ msgid "read_perm"
+#~ msgstr "read permission"
+
+#~ msgid "update_perm"
+#~ msgstr "update permission"
--- a/i18n/es.po	Tue Jul 27 12:36:03 2010 +0200
+++ b/i18n/es.po	Wed Nov 03 16:38:28 2010 +0100
@@ -5,8 +5,10 @@
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
 "PO-Revision-Date: 2010-11-27 07:59+0100\n"
-"Last-Translator: Celso Flores<jcelsoflores@gmail.com>\n"
+"Last-Translator: Celso Flores<celso.flores@crealibre.com>, Carlos Balderas "
+"<carlos.balderas@crealibre.com>\n"
 "Language-Team: es <contact@logilab.fr>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -23,8 +25,8 @@
 "url: %(url)s\n"
 msgstr ""
 "\n"
-"%(user)s ha cambiado su estado de <%(previous_state)s> hacia <%"
-"(current_state)s> por la entidad\n"
+"%(user)s ha cambiado su estado de <%(previous_state)s> hacia <"
+"%(current_state)s> por la entidad\n"
 "'%(title)s'\n"
 "\n"
 "%(comment)s\n"
@@ -36,15 +38,15 @@
 msgstr "  del estado %(fromstate)s hacia el estado %(tostate)s\n"
 
 msgid " :"
-msgstr ""
+msgstr ":"
 
 #, python-format
 msgid "%(attr)s set to %(newvalue)s"
-msgstr ""
+msgstr "%(attr)s modificado a %(newvalue)s"
 
 #, python-format
 msgid "%(attr)s updated from %(oldvalue)s to %(newvalue)s"
-msgstr ""
+msgstr "%(attr)s modificado de %(oldvalue)s a %(newvalue)s"
 
 #, python-format
 msgid "%(cstr)s constraint failed for value %(value)r"
@@ -60,7 +62,7 @@
 
 #, python-format
 msgid "%(value)r doesn't match the %(regexp)r regular expression"
-msgstr ""
+msgstr "%(value)r no corresponde a la expresión regular %(regexp)r"
 
 #, python-format
 msgid "%d days"
@@ -132,10 +134,10 @@
 
 #, python-format
 msgid "%s updated"
-msgstr ""
+msgstr "%s actualizado"
 
 msgid "(UNEXISTANT EID)"
-msgstr ""
+msgstr "(EID INEXISTENTE"
 
 msgid "(loading ...)"
 msgstr "(Cargando ...)"
@@ -178,7 +180,7 @@
 
 #, python-format
 msgid "<%s not specified>"
-msgstr ""
+msgstr "<%s no especificado>"
 
 #, python-format
 msgid ""
@@ -189,6 +191,9 @@
 "pero se puede ver a un <a href=\"%s\">modelo completo con meta-datos</a>.</"
 "div>"
 
+msgid "<not specified>"
+msgstr ""
+
 msgid "?*"
 msgstr "0..1 0..n"
 
@@ -208,19 +213,19 @@
 msgstr "Cualquiera"
 
 msgid "Attributes permissions:"
-msgstr ""
+msgstr "Permisos de atributos:"
 
 msgid "Attributes with non default permissions:"
-msgstr ""
+msgstr "Atributos con permisos no estándares"
 
 # schema pot file, generated on 2009-09-16 16:46:55
 #
 # singular and plural forms for each entity type
 msgid "BaseTransition"
-msgstr ""
+msgstr "Transición (abstracta)"
 
 msgid "BaseTransition_plural"
-msgstr ""
+msgstr "Transiciones (abstractas)"
 
 msgid "Bookmark"
 msgstr "Favorito"
@@ -235,14 +240,17 @@
 msgstr "Booleanos"
 
 msgid "BoundConstraint"
-msgstr ""
+msgstr "Restricción de límite"
 
 msgid "BoundaryConstraint"
-msgstr ""
+msgstr "Restricción de límite"
 
 msgid "Browse by category"
 msgstr "Busca por categoría"
 
+msgid "Browse by entity type"
+msgstr "Busca por tipo de entidad"
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -278,11 +286,11 @@
 
 msgctxt "inlined:CWRelation.from_entity.subject"
 msgid "CWEType"
-msgstr ""
+msgstr "Tipo de entidad"
 
 msgctxt "inlined:CWRelation.to_entity.subject"
 msgid "CWEType"
-msgstr ""
+msgstr "Tipo de entidad"
 
 msgid "CWEType_plural"
 msgstr "Tipos de entidades"
@@ -291,7 +299,7 @@
 msgstr "Groupo"
 
 msgid "CWGroup_plural"
-msgstr "Groupos"
+msgstr "Grupos"
 
 msgid "CWPermission"
 msgstr "Autorización"
@@ -310,7 +318,7 @@
 
 msgctxt "inlined:CWRelation.relation_type.subject"
 msgid "CWRType"
-msgstr ""
+msgstr "Tipo de relación"
 
 msgid "CWRType_plural"
 msgstr "Tipos de relación"
@@ -321,6 +329,12 @@
 msgid "CWRelation_plural"
 msgstr "Relaciones"
 
+msgid "CWUniqueTogetherConstraint"
+msgstr ""
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr ""
+
 msgid "CWUser"
 msgstr "Usuario"
 
@@ -332,36 +346,52 @@
 "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which is already "
 "linked using this relation."
 msgstr ""
+"No puede restaurar la relación %(role)s %(rtype)s en la entidad %(eid)s pues "
+"ya esta ligada a otra entidad usando esa relación."
 
 #, python-format
 msgid ""
 "Can't restore relation %(rtype)s between %(subj)s and %(obj)s, that relation "
 "does not exists anymore in the schema."
 msgstr ""
+"No puede restaurar la relación %(rtype)s entre %(subj)s y  %(obj)s, esta "
+"relación ya no existe en el esquema."
 
 #, python-format
 msgid ""
 "Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
 "exists anymore in the schema."
 msgstr ""
+"No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
+"relación ya no existe en el esquema."
 
 #, python-format
 msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
+"No puede restaurar la relación %(rtype)s, la entidad %(role)s %(eid)s ya no "
+"existe."
 
 #, python-format
 msgid ""
 "Can't undo addition of relation %(rtype)s from %(subj)s to %(obj)s, doesn't "
 "exist anymore"
 msgstr ""
+"No puede anular el agregar la relación %(rtype)s de %(subj)s a %(obj)s, esta "
+"relación ya no existe"
 
 #, python-format
 msgid ""
 "Can't undo creation of entity %(eid)s of type %(etype)s, type no more "
 "supported"
 msgstr ""
+"No puede anular la creación de la entidad %(eid)s de tipo %(etype)s, este "
+"tipo ya no existe"
+
+#, python-format
+msgid "Data connection graph for %s"
+msgstr ""
 
 msgid "Date"
 msgstr "Fecha"
@@ -382,16 +412,16 @@
 msgstr "Decimales"
 
 msgid "Do you want to delete the following element(s) ?"
-msgstr "Desea suprimir el(los) elemento(s) siguiente(s)"
+msgstr "Desea eliminar el(los) elemento(s) siguiente(s)"
 
 msgid "Download schema as OWL"
-msgstr ""
+msgstr "Descargar el esquema en formato OWL"
+
+msgid "EmailAddress"
+msgstr "Correo Electrónico"
 
 msgctxt "inlined:CWUser.use_email.subject"
 msgid "EmailAddress"
-msgstr ""
-
-msgid "EmailAddress"
 msgstr "Correo Electrónico"
 
 msgid "EmailAddress_plural"
@@ -401,13 +431,13 @@
 msgstr "Entidades"
 
 msgid "Entity types"
-msgstr ""
+msgstr "Tipos de entidades"
 
 msgid "ExternalUri"
-msgstr ""
+msgstr "Uri externo"
 
 msgid "ExternalUri_plural"
-msgstr ""
+msgstr "Uris externos"
 
 msgid "Float"
 msgstr "Número flotante"
@@ -419,22 +449,25 @@
 #
 # singular and plural forms for each entity type
 msgid "FormatConstraint"
-msgstr ""
+msgstr "Restricción de Formato"
 
 msgid "From:"
 msgstr "De: "
 
 msgid "Garbage collection information"
+msgstr "Recolector de basura en memoria"
+
+msgid "Got rhythm?"
 msgstr ""
 
 msgid "Help"
-msgstr ""
+msgstr "Ayuda"
 
 msgid "Index"
-msgstr ""
+msgstr "Ãndice"
 
 msgid "Instance"
-msgstr ""
+msgstr "Instancia"
 
 msgid "Int"
 msgstr "Número entero"
@@ -446,19 +479,19 @@
 msgstr "Duración"
 
 msgid "IntervalBoundConstraint"
-msgstr ""
+msgstr "Restricción de intervalo"
 
 msgid "Interval_plural"
 msgstr "Duraciones"
 
 msgid "Looked up classes"
-msgstr ""
+msgstr "Clases buscadas"
 
 msgid "Most referenced classes"
-msgstr ""
+msgstr "Clases más referenciadas"
 
 msgid "New BaseTransition"
-msgstr ""
+msgstr "XXX"
 
 msgid "New Bookmark"
 msgstr "Agregar a Favoritos"
@@ -467,7 +500,7 @@
 msgstr "Nueva definición de relación final"
 
 msgid "New CWCache"
-msgstr "Agregar Cache"
+msgstr "Agregar Caché"
 
 msgid "New CWConstraint"
 msgstr "Agregar Restricción"
@@ -493,6 +526,9 @@
 msgid "New CWRelation"
 msgstr "Nueva definición de relación final"
 
+msgid "New CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "New CWUser"
 msgstr "Agregar usuario"
 
@@ -500,7 +536,7 @@
 msgstr "Agregar Email"
 
 msgid "New ExternalUri"
-msgstr ""
+msgstr "Agregar Uri externa"
 
 msgid "New RQLExpression"
 msgstr "Agregar expresión rql"
@@ -509,7 +545,7 @@
 msgstr "Agregar Estado"
 
 msgid "New SubWorkflowExitPoint"
-msgstr ""
+msgstr "Agregar salida de sub-Workflow"
 
 msgid "New TrInfo"
 msgstr "Agregar Información de Transición"
@@ -518,22 +554,26 @@
 msgstr "Agregar transición"
 
 msgid "New Workflow"
-msgstr ""
+msgstr "Agregar Workflow"
 
 msgid "New WorkflowTransition"
-msgstr ""
+msgstr "Agregar transición de Workflow"
+
+#, python-format
+msgid "No account? Try public access at %s"
+msgstr "No esta registrado? Use el acceso público en %s"
 
 msgid "No result matching query"
 msgstr "Ningún resultado corresponde a su búsqueda"
 
 msgid "Non exhaustive list of views that may apply to entities of this type"
-msgstr ""
+msgstr "Lista no exhaustiva de vistas aplicables a este tipo de entidad"
 
 msgid "OR"
 msgstr "O"
 
-msgid "Parent classes:"
-msgstr ""
+msgid "Parent class:"
+msgstr "Clase padre:"
 
 msgid "Password"
 msgstr "Contraseña"
@@ -542,16 +582,16 @@
 msgstr "Contraseñas"
 
 msgid "Permissions for entity types"
-msgstr ""
+msgstr "Permisos por tipos de entidad"
 
 msgid "Permissions for relations"
-msgstr ""
+msgstr "Permisos por las relaciones"
 
 msgid "Please note that this is only a shallow copy"
-msgstr "Recuerde que no es más que una copia superficial"
+msgstr "Recuerde que sólo es una copia superficial"
 
 msgid "RQLConstraint"
-msgstr ""
+msgstr "Restricción RQL"
 
 msgid "RQLExpression"
 msgstr "Expresión RQL"
@@ -560,28 +600,28 @@
 msgstr "Expresiones RQL"
 
 msgid "RQLUniqueConstraint"
-msgstr ""
+msgstr "Restricción RQL de Unicidad"
 
 msgid "RQLVocabularyConstraint"
-msgstr ""
+msgstr "Restricción RQL de Vocabulario"
 
 msgid "Recipients:"
-msgstr "Destinatarios"
+msgstr "Destinatarios :"
 
 msgid "RegexpConstraint"
-msgstr ""
+msgstr "restricción expresión regular"
 
 msgid "Registry's content"
-msgstr ""
+msgstr "Contenido del registro"
 
 msgid "Relation types"
-msgstr ""
+msgstr "Tipos de relación"
 
 msgid "Relations"
 msgstr "Relaciones"
 
 msgid "Repository"
-msgstr ""
+msgstr "Repositorio"
 
 #, python-format
 msgid "Schema %s"
@@ -594,10 +634,10 @@
 msgstr "Buscar"
 
 msgid "SizeConstraint"
-msgstr ""
+msgstr "Restricción de tamaño"
 
 msgid "Startup views"
-msgstr "Vistas de Inicio"
+msgstr "Vistas de inicio"
 
 msgid "State"
 msgstr "Estado"
@@ -606,7 +646,7 @@
 msgstr "Estados"
 
 msgid "StaticVocabularyConstraint"
-msgstr ""
+msgstr "Restricción de vocabulario"
 
 msgid "String"
 msgstr "Cadena de caracteres"
@@ -615,13 +655,13 @@
 msgstr "Cadenas de caracteres"
 
 msgid "Sub-classes:"
-msgstr ""
+msgstr "Clases hijas:"
 
 msgid "SubWorkflowExitPoint"
-msgstr ""
+msgstr "Salida de sub-workflow"
 
 msgid "SubWorkflowExitPoint_plural"
-msgstr ""
+msgstr "Salidas de sub-workflow"
 
 msgid "Subject:"
 msgstr "Sujeto:"
@@ -632,9 +672,6 @@
 msgid "Submit bug report by mail"
 msgstr "Enviar este reporte por email"
 
-msgid "The repository holds the following entities"
-msgstr "El repositorio contiene las entidades siguientes"
-
 #, python-format
 msgid "The view %s can not be applied to this query"
 msgstr "La vista %s no puede ser aplicada a esta búsqueda"
@@ -644,10 +681,10 @@
 msgstr "La vista %s no ha podido ser encontrada"
 
 msgid "There is no default workflow"
-msgstr ""
+msgstr "Esta entidad no posee workflow por defecto"
 
 msgid "This BaseTransition"
-msgstr ""
+msgstr "Esta transición abstracta"
 
 msgid "This Bookmark"
 msgstr "Este favorito"
@@ -656,7 +693,7 @@
 msgstr "Esta definición de relación final"
 
 msgid "This CWCache"
-msgstr "Este Cache"
+msgstr "Este Caché"
 
 msgid "This CWConstraint"
 msgstr "Esta Restricción"
@@ -671,7 +708,7 @@
 msgstr "Este grupo"
 
 msgid "This CWPermission"
-msgstr "Esta autorización"
+msgstr "Este permiso"
 
 msgid "This CWProperty"
 msgstr "Esta propiedad"
@@ -682,6 +719,9 @@
 msgid "This CWRelation"
 msgstr "Esta definición de relación no final"
 
+msgid "This CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "This CWUser"
 msgstr "Este usuario"
 
@@ -689,7 +729,7 @@
 msgstr "Esta dirección electrónica"
 
 msgid "This ExternalUri"
-msgstr ""
+msgstr "Este Uri externo"
 
 msgid "This RQLExpression"
 msgstr "Esta expresión RQL"
@@ -698,7 +738,7 @@
 msgstr "Este estado"
 
 msgid "This SubWorkflowExitPoint"
-msgstr ""
+msgstr "Esta Salida de sub-workflow"
 
 msgid "This TrInfo"
 msgstr "Esta información de transición"
@@ -707,13 +747,13 @@
 msgstr "Esta transición"
 
 msgid "This Workflow"
-msgstr ""
+msgstr "Este Workflow"
 
 msgid "This WorkflowTransition"
-msgstr ""
+msgstr "Esta transición de Workflow"
 
 msgid "This entity type permissions:"
-msgstr ""
+msgstr "Permisos para este tipo de entidad:"
 
 msgid "Time"
 msgstr "Hora"
@@ -734,34 +774,34 @@
 msgstr "Transiciones"
 
 msgid "UniqueConstraint"
-msgstr ""
+msgstr "Restricción de Unicidad"
 
 msgid "Unreachable objects"
-msgstr ""
+msgstr "Objetos inaccesibles"
 
 msgid "Used by:"
 msgstr "Utilizado por :"
 
 msgid "Web server"
-msgstr ""
+msgstr "Servidor web"
 
 msgid "What's new?"
-msgstr "Lo último en el sitio"
+msgstr "Lo más reciente"
 
 msgid "Workflow"
-msgstr ""
+msgstr "Workflow"
 
 msgid "Workflow history"
 msgstr "Histórico del Workflow"
 
 msgid "WorkflowTransition"
-msgstr ""
+msgstr "Transición de Workflow"
 
 msgid "WorkflowTransition_plural"
-msgstr ""
+msgstr "Transiciones de Workflow"
 
 msgid "Workflow_plural"
-msgstr ""
+msgstr "work flows"
 
 msgid ""
 "You can either submit a new file using the browse button above, or choose to "
@@ -790,14 +830,15 @@
 "You have no access to this view or it can not be used to display the current "
 "data."
 msgstr ""
-"No tiene acceso a esta vista o No se puede utilizare para los datos actuales."
+"No tiene permisos para accesar esta vista o No puede utilizarse para "
+"desplegar los datos seleccionados."
 
 msgid ""
 "You're not authorized to access this page. If you think you should, please "
 "contact the site administrator."
 msgstr ""
 "Usted no esta autorizado a acceder a esta página. Si Usted cree que \n"
-"hay un error, favor de contactar al administrador del sitio."
+"hay un error, favor de contactar al administrador del Sistema."
 
 #, python-format
 msgid "[%s supervision] changes summary"
@@ -813,10 +854,10 @@
 "representan respectivamente la entidad en transición y el usuario actual. "
 
 msgid "a URI representing an object in external data store"
-msgstr ""
+msgstr "una URI designando un objeto en un repositorio de datos externo"
 
 msgid "a float is expected"
-msgstr ""
+msgstr "un número flotante es requerido"
 
 msgid ""
 "a simple cache entity characterized by a name and a validity date. The "
@@ -824,18 +865,23 @@
 "invalidate the cache (typically in hooks). Also, checkout the AppObject."
 "get_cache() method."
 msgstr ""
+"un caché simple caracterizado por un nombre y una fecha de validez. Es\n"
+"el código de la instancia quién es responsable de actualizar la fecha de\n"
+"validez mientras el caché debe ser invalidado (en general en un hook).\n"
+"Para recuperar un caché, hace falta utilizar el método\n"
+"get_cache(cachename)."
 
 msgid "about this site"
-msgstr "Sobre este Espacio"
+msgstr "Información del Sistema"
 
 msgid "abstract base class for transitions"
-msgstr ""
+msgstr "Clase de base abstracta para la transiciones"
 
 msgid "action(s) on this selection"
-msgstr "acción(es) en esta selección"
+msgstr "Acción(es) en esta selección"
 
 msgid "actions"
-msgstr "acciones"
+msgstr "Acciones"
 
 msgid "activate"
 msgstr "Activar"
@@ -853,13 +899,13 @@
 msgstr "Restricción"
 
 msgid "add CWAttribute read_permission RQLExpression subject"
-msgstr ""
+msgstr "Expresión RQL de lectura"
 
 msgid "add CWAttribute relation_type CWRType object"
 msgstr "Definición de atributo"
 
 msgid "add CWAttribute update_permission RQLExpression subject"
-msgstr ""
+msgstr "Permiso de actualización"
 
 msgid "add CWEType add_permission RQLExpression subject"
 msgstr "Expresión RQL de agregación"
@@ -877,20 +923,23 @@
 msgstr "Propiedad"
 
 msgid "add CWRelation add_permission RQLExpression subject"
-msgstr ""
+msgstr "Expresión RQL de agregar"
 
 msgid "add CWRelation constrained_by CWConstraint subject"
 msgstr "Restricción"
 
 msgid "add CWRelation delete_permission RQLExpression subject"
-msgstr ""
+msgstr "Expresión RQL de supresión"
 
 msgid "add CWRelation read_permission RQLExpression subject"
-msgstr ""
+msgstr "Expresión RQL de lectura"
 
 msgid "add CWRelation relation_type CWRType object"
 msgstr "Definición de relación"
 
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr ""
+
 msgid "add CWUser in_group CWGroup object"
 msgstr "Usuario"
 
@@ -904,10 +953,10 @@
 msgstr "Transición en salida"
 
 msgid "add State allowed_transition WorkflowTransition subject"
-msgstr ""
+msgstr "Transición workflow en salida"
 
 msgid "add State state_of Workflow object"
-msgstr ""
+msgstr "Estado"
 
 msgid "add Transition condition RQLExpression subject"
 msgstr "Restricción"
@@ -919,39 +968,36 @@
 msgstr "Estado de salida"
 
 msgid "add Transition transition_of Workflow object"
-msgstr ""
+msgstr "Transición"
 
 msgid "add WorkflowTransition condition RQLExpression subject"
-msgstr ""
+msgstr "Condición"
 
 msgid "add WorkflowTransition subworkflow_exit SubWorkflowExitPoint subject"
-msgstr ""
+msgstr "Salida de sub-workflow"
 
 msgid "add WorkflowTransition transition_of Workflow object"
-msgstr ""
+msgstr "Transición Workflow"
 
 msgctxt "inlined:CWRelation.from_entity.subject"
 msgid "add a CWEType"
-msgstr ""
+msgstr "Agregar un tipo de entidad"
 
 msgctxt "inlined:CWRelation.to_entity.subject"
 msgid "add a CWEType"
-msgstr ""
+msgstr "Agregar un tipo de entidad"
 
 msgctxt "inlined:CWRelation.relation_type.subject"
 msgid "add a CWRType"
-msgstr ""
+msgstr "Agregar un tipo de relación"
 
 msgctxt "inlined:CWUser.use_email.subject"
 msgid "add a EmailAddress"
-msgstr ""
+msgstr "Agregar correo electrónico"
 
 msgid "add a new permission"
 msgstr "Agregar una autorización"
 
-msgid "add_perm"
-msgstr "Agregado"
-
 # subject and object forms for each relation type
 # (no object form for final relation types)
 msgid "add_permission"
@@ -961,77 +1007,82 @@
 # (no object form for final relation types)
 msgctxt "CWEType"
 msgid "add_permission"
-msgstr ""
+msgstr "Permiso de agregar"
 
 msgctxt "CWRelation"
 msgid "add_permission"
-msgstr ""
+msgstr "Permiso de agregar"
+
+msgid "add_permission_object"
+msgstr "tiene permiso de agregar"
 
 msgctxt "CWGroup"
 msgid "add_permission_object"
-msgstr ""
+msgstr "tiene permiso de agregar"
 
 msgctxt "RQLExpression"
 msgid "add_permission_object"
-msgstr ""
-
-msgid "add_permission_object"
-msgstr "tiene la autorización para agregar"
+msgstr "tiene permiso de agregar"
+
+msgid "add_relation"
+msgstr "agregar"
 
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
-msgstr "Agregado %(etype)s #%(eid)s (%(title)s)"
+msgstr "se agregó %(etype)s #%(eid)s (%(title)s)"
 
 #, python-format
 msgid ""
-"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%"
-"(eidto)s"
+"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #"
+"%(eidto)s"
 msgstr ""
+"la relación %(rtype)s de %(frometype)s #%(eidfrom)s a %(toetype)s #%(eidto)s "
+"ha sido agregada"
 
 msgid "addrelated"
-msgstr ""
+msgstr "Agregar"
 
 msgid "address"
-msgstr "dirección"
+msgstr "correo electrónico"
 
 msgctxt "EmailAddress"
 msgid "address"
-msgstr ""
+msgstr "correo electrónico"
 
 msgid "alias"
 msgstr "alias"
 
 msgctxt "EmailAddress"
 msgid "alias"
-msgstr ""
+msgstr "alias"
 
 msgid "allow to set a specific workflow for an entity"
-msgstr ""
+msgstr "permite definir un Workflow específico para una entidad"
 
 msgid "allowed transitions from this state"
 msgstr "transiciones autorizadas desde este estado"
 
 msgid "allowed_transition"
-msgstr "transición autorizada"
+msgstr "transiciones autorizadas"
 
 msgctxt "State"
 msgid "allowed_transition"
-msgstr ""
+msgstr "transiciones autorizadas"
+
+msgid "allowed_transition_object"
+msgstr "Estados de entrada"
 
 msgctxt "BaseTransition"
 msgid "allowed_transition_object"
-msgstr ""
+msgstr "transición autorizada de"
 
 msgctxt "Transition"
 msgid "allowed_transition_object"
-msgstr ""
+msgstr "transición autorizada de"
 
 msgctxt "WorkflowTransition"
 msgid "allowed_transition_object"
-msgstr ""
-
-msgid "allowed_transition_object"
-msgstr "Estados de entrada"
+msgstr "transición autorizada de"
 
 msgid "am/pm calendar (month)"
 msgstr "calendario am/pm (mes)"
@@ -1048,26 +1099,26 @@
 msgid "an electronic mail address associated to a short alias"
 msgstr "una dirección electrónica asociada a este alias"
 
-msgid "an error occured"
-msgstr "ha ocurrido un error"
-
-msgid "an error occured while processing your request"
+msgid "an error occurred"
+msgstr "Ha ocurrido un error"
+
+msgid "an error occurred while processing your request"
 msgstr "un error ocurrió al procesar su demanda"
 
-msgid "an error occured, the request cannot be fulfilled"
+msgid "an error occurred, the request cannot be fulfilled"
 msgstr "un error ha ocurrido, la búsqueda no ha podido ser realizada"
 
 msgid "an integer is expected"
-msgstr ""
+msgstr "un número entero es esperado"
 
 msgid "and linked"
-msgstr "y ligada"
+msgstr "y relacionada"
 
 msgid "and/or between different values"
 msgstr "y/o entre los diferentes valores"
 
 msgid "anonymous"
-msgstr "Anónimo"
+msgstr "anónimo"
 
 msgid "application entities"
 msgstr "Entidades de la aplicación"
@@ -1078,8 +1129,8 @@
 #, python-format
 msgid "at least one relation %(rtype)s is required on %(etype)s (%(eid)s)"
 msgstr ""
-"La entidad #%(eid)s de tipo %(etype)s debe necesariamente estar ligada a \n"
-"otra via la relación %(rtype)s"
+"La entidad #%(eid)s de tipo %(etype)s debe necesariamente tener almenos una "
+"relación de tipo %(rtype)s"
 
 msgid "attribute"
 msgstr "Atributo"
@@ -1091,7 +1142,7 @@
 msgstr "Usuario o contraseña incorrecta"
 
 msgid "auto"
-msgstr ""
+msgstr "Automático"
 
 msgid "automatic"
 msgstr "Automático"
@@ -1103,33 +1154,33 @@
 msgstr "Url de base"
 
 msgid "bookmark has been removed"
-msgstr "ha sido eliminado de sus favoritos"
+msgstr "Ha sido eliminado de sus favoritos"
 
 msgid "bookmark this page"
-msgstr "Agregar esta página a sus favoritos"
+msgstr "Agregar esta página a los favoritos"
 
 msgid "bookmark this search"
 msgstr "Guardar esta búsqueda"
 
 msgid "bookmarked_by"
-msgstr "está en los favoritos de"
+msgstr "está en los Favoritos de"
 
 msgctxt "Bookmark"
 msgid "bookmarked_by"
-msgstr ""
+msgstr "está en los Favoritos de"
+
+msgid "bookmarked_by_object"
+msgstr "tiene como Favoritos"
 
 msgctxt "CWUser"
 msgid "bookmarked_by_object"
-msgstr ""
-
-msgid "bookmarked_by_object"
-msgstr "selecciona en sus favoritos a"
+msgstr "tiene como Favoritos"
 
 msgid "bookmarks"
 msgstr "Favoritos"
 
 msgid "bookmarks are used to have user's specific internal links"
-msgstr "favoritos son usados para que un usuario recorde ligas"
+msgstr "los Favoritos son ligas directas a espacios guardados por el usuario"
 
 msgid "boxes"
 msgstr "Cajas"
@@ -1138,50 +1189,51 @@
 msgstr "Caja de Favoritos"
 
 msgid "boxes_bookmarks_box_description"
-msgstr "Caja que contiene los espacios favoritos del usuario"
+msgstr "Muestra y permite administrar los favoritos del usuario"
 
 msgid "boxes_download_box"
-msgstr "Caja de download"
+msgstr "Configuración de caja de descargas"
 
 msgid "boxes_download_box_description"
-msgstr "Caja que contiene los elementos bajados"
+msgstr "Caja que contiene los elementos descargados"
 
 msgid "boxes_edit_box"
-msgstr "Caja de acciones"
+msgstr "Caja de Acciones"
 
 msgid "boxes_edit_box_description"
-msgstr ""
-"Caja que muestra las diferentes acciones posibles sobre los datos presentes"
+msgstr "Muestra las acciones posibles a ejecutar para los datos seleccionados"
 
 msgid "boxes_filter_box"
 msgstr "Filtros"
 
 msgid "boxes_filter_box_description"
-msgstr "Caja que permite realizar filtros sobre los resultados de una búsqueda"
+msgstr "Muestra los filtros aplicables a una búsqueda realizada"
 
 msgid "boxes_possible_views_box"
 msgstr "Caja de Vistas Posibles"
 
 msgid "boxes_possible_views_box_description"
-msgstr "Caja mostrando las vistas posibles para los datos actuales"
+msgstr "Muestra las vistas posibles a aplicar a los datos seleccionados"
 
 msgid "boxes_rss"
-msgstr "ícono RSS"
+msgstr "Ãcono RSS"
 
 msgid "boxes_rss_description"
-msgstr "El ícono RSS permite recuperar las vistas RSS de los datos presentes"
+msgstr "Muestra el ícono RSS para vistas RSS"
 
 msgid "boxes_search_box"
 msgstr "Caja de búsqueda"
 
 msgid "boxes_search_box_description"
-msgstr "Caja con un espacio de búsqueda simple"
+msgstr ""
+"Permite realizar una búsqueda simple para cualquier tipo de dato en la "
+"aplicación"
 
 msgid "boxes_startup_views_box"
 msgstr "Caja Vistas de inicio"
 
 msgid "boxes_startup_views_box_description"
-msgstr "Caja mostrando las vistas de inicio de la aplicación"
+msgstr "Muestra las vistas de inicio de la aplicación"
 
 msgid "bug report sent"
 msgstr "Reporte de error enviado"
@@ -1202,29 +1254,29 @@
 msgstr "por"
 
 msgid "by relation"
-msgstr "por relación"
+msgstr "por la relación"
 
 msgid "by_transition"
-msgstr ""
+msgstr "transición"
 
 msgctxt "TrInfo"
 msgid "by_transition"
-msgstr ""
+msgstr "transición"
+
+msgid "by_transition_object"
+msgstr "cambio de estados"
 
 msgctxt "BaseTransition"
 msgid "by_transition_object"
-msgstr ""
+msgstr "tiene como información"
 
 msgctxt "Transition"
 msgid "by_transition_object"
-msgstr ""
+msgstr "tiene como información"
 
 msgctxt "WorkflowTransition"
 msgid "by_transition_object"
-msgstr ""
-
-msgid "by_transition_object"
-msgstr ""
+msgstr "tiene como información"
 
 msgid "calendar"
 msgstr "mostrar un calendario"
@@ -1242,13 +1294,13 @@
 msgstr "calendario (anual)"
 
 msgid "can not resolve entity types:"
-msgstr ""
+msgstr "Imposible de interpretar los tipos de entidades:"
 
 msgid "can't be changed"
-msgstr ""
+msgstr "No puede ser modificado"
 
 msgid "can't be deleted"
-msgstr ""
+msgstr "No puede ser eliminado"
 
 #, python-format
 msgid "can't change the %s attribute"
@@ -1263,11 +1315,11 @@
 msgstr "imposible de mostrar los datos, a causa del siguiente error: %s"
 
 msgid "can't have multiple exits on the same state"
-msgstr ""
+msgstr "no puede tener varias salidas en el mismo estado"
 
 #, python-format
 msgid "can't parse %(value)r (expected %(format)s)"
-msgstr ""
+msgstr "no puede analizar %(value)r (formato requerido : %(format)s)"
 
 #, python-format
 msgid ""
@@ -1288,14 +1340,14 @@
 
 msgctxt "CWAttribute"
 msgid "cardinality"
-msgstr ""
+msgstr "cardinalidad"
 
 msgctxt "CWRelation"
 msgid "cardinality"
-msgstr ""
+msgstr "cardinalidad"
 
 msgid "category"
-msgstr "categoria"
+msgstr "categoría"
 
 #, python-format
 msgid "changed state of %(etype)s #%(eid)s (%(title)s)"
@@ -1305,56 +1357,60 @@
 msgstr "Cambios realizados"
 
 msgid "click here to see created entity"
-msgstr "ver la entidad creada"
+msgstr "Ver la entidad creada"
 
 msgid "click on the box to cancel the deletion"
 msgstr "Seleccione la zona de edición para cancelar la eliminación"
 
+msgid "click to add a value"
+msgstr "seleccione para agregar un valor"
+
+msgid "click to delete this value"
+msgstr "seleccione para eliminar este valor"
+
 msgid "click to edit this field"
-msgstr ""
+msgstr "seleccione para editar este campo"
 
 msgid "comment"
 msgstr "Comentario"
 
 msgctxt "TrInfo"
 msgid "comment"
-msgstr ""
+msgstr "Comentario"
 
 msgid "comment_format"
 msgstr "Formato"
 
 msgctxt "TrInfo"
 msgid "comment_format"
-msgstr ""
+msgstr "Formato"
 
 msgid "components"
 msgstr "Componentes"
 
 msgid "components_appliname"
-msgstr "Título de la aplicación"
+msgstr "Nombre de la aplicación"
 
 msgid "components_appliname_description"
-msgstr "Muestra el título de la aplicación en el encabezado de la página"
+msgstr "Muestra el nombre de la aplicación en el encabezado de la página"
 
 msgid "components_breadcrumbs"
 msgstr "Ruta de Navegación"
 
 msgid "components_breadcrumbs_description"
-msgstr ""
-"Muestra un camino que permite identificar el lugar donde se encuentra la "
-"página en el sitio"
+msgstr "Muestra el lugar donde se encuentra la página actual en el Sistema"
 
 msgid "components_etypenavigation"
-msgstr "Filtro por tipo"
+msgstr "Filtar por tipo"
 
 msgid "components_etypenavigation_description"
-msgstr "Permite filtrar por tipo de entidad los resultados de búsqueda"
+msgstr "Permite filtrar por tipo de entidad los resultados de una búsqueda"
 
 msgid "components_help"
 msgstr "Botón de ayuda"
 
 msgid "components_help_description"
-msgstr "El botón de ayuda, en el encabezado de página"
+msgstr "El botón de ayuda, en el encabezado de la página"
 
 msgid "components_loggeduserlink"
 msgstr "Liga usuario"
@@ -1362,10 +1418,10 @@
 msgid "components_loggeduserlink_description"
 msgstr ""
 "Muestra un enlace hacia el formulario de conexión para los usuarios "
-"anónimos, o una caja que contiene las ligas propias a el usuarioconectado. "
+"anónimos, o una caja que contiene los enlaces del usuario conectado. "
 
 msgid "components_logo"
-msgstr "Logo"
+msgstr "logo"
 
 msgid "components_logo_description"
 msgstr "El logo de la aplicación, en el encabezado de página"
@@ -1375,76 +1431,90 @@
 
 msgid "components_navigation_description"
 msgstr ""
-"Componente que permite distribuir sobre varias páginas las búsquedas que "
-"arrojan mayores resultados que un número previamente elegido"
+"Componente que permite presentar en varias páginas los resultados de  "
+"búsqueda  cuando son mayores a un número predeterminado "
 
 msgid "components_rqlinput"
-msgstr "Barra rql"
+msgstr "Barra RQL"
 
 msgid "components_rqlinput_description"
-msgstr "La barra de demanda rql, en el encabezado de página"
+msgstr "La barra para realizar consultas en RQL, en el encabezado de página"
 
 msgid "composite"
 msgstr "composite"
 
 msgctxt "CWRelation"
 msgid "composite"
-msgstr ""
+msgstr "composite"
 
 msgid "condition"
 msgstr "condición"
 
 msgctxt "BaseTransition"
 msgid "condition"
-msgstr ""
+msgstr "condición"
 
 msgctxt "Transition"
 msgid "condition"
-msgstr ""
+msgstr "condición"
 
 msgctxt "WorkflowTransition"
 msgid "condition"
-msgstr ""
+msgstr "condición"
+
+msgid "condition_object"
+msgstr "condición de"
 
 msgctxt "RQLExpression"
 msgid "condition_object"
-msgstr ""
-
-msgid "condition_object"
 msgstr "condición de"
 
 msgid "conditions"
-msgstr ""
+msgstr "condiciones"
 
 msgid "config mode"
-msgstr ""
+msgstr "Modo de configuración"
 
 msgid "config type"
-msgstr ""
+msgstr "Tipo de configuración"
 
 msgid "confirm password"
 msgstr "Confirmar contraseña"
 
 msgid "constrained_by"
-msgstr "Restricción hecha por"
+msgstr "Restricción impuesta por"
 
 msgctxt "CWAttribute"
 msgid "constrained_by"
-msgstr ""
+msgstr "Restricción impuesta por"
 
 msgctxt "CWRelation"
 msgid "constrained_by"
-msgstr ""
+msgstr "Restricción impuesta por"
+
+msgid "constrained_by_object"
+msgstr "Restricción de"
 
 msgctxt "CWConstraint"
 msgid "constrained_by_object"
-msgstr ""
-
-msgid "constrained_by_object"
-msgstr "ha restringido"
+msgstr "Restricción de"
 
 msgid "constraint factory"
-msgstr "FAbrica de restricciones"
+msgstr "Fábrica de restricciones"
+
+msgid "constraint_of"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr ""
+
+msgid "constraint_of_object"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr ""
 
 msgid "constraints"
 msgstr "Restricciones"
@@ -1453,7 +1523,7 @@
 msgstr "Restricciones que se aplican a esta relación"
 
 msgid "content type"
-msgstr ""
+msgstr "tipo MIME"
 
 msgid "contentnavigation"
 msgstr "Componentes contextuales"
@@ -1462,10 +1532,10 @@
 msgstr "Ruta de Navegación"
 
 msgid "contentnavigation_breadcrumbs_description"
-msgstr "Muestra un camino que permite localizar la página actual en el sitio"
+msgstr "Muestra la ruta que permite localizar la página actual en el Sistema"
 
 msgid "contentnavigation_metadata"
-msgstr ""
+msgstr "Metadatos de la Entidad"
 
 msgid "contentnavigation_metadata_description"
 msgstr ""
@@ -1475,7 +1545,7 @@
 
 msgid "contentnavigation_prevnext_description"
 msgstr ""
-"Muestra las ligas que permiten pasar de una entidad a otra en lasentidades "
+"Muestra las ligas que permiten pasar de una entidad a otra en las entidades "
 "que implementan la interface \"anterior/siguiente\"."
 
 msgid "contentnavigation_seealso"
@@ -1483,16 +1553,16 @@
 
 msgid "contentnavigation_seealso_description"
 msgstr ""
-"sección que muestra las entidades ligadas por la relación \"vea también\" , "
-"si la entidad soporta esta relación."
+"sección que muestra las entidades relacionadas por la relación \"vea también"
+"\" , si la entidad soporta esta relación."
 
 msgid "contentnavigation_wfhistory"
 msgstr "Histórico del workflow."
 
 msgid "contentnavigation_wfhistory_description"
 msgstr ""
-"Sección que ofrece el reporte histórico del workflow para las entidades que "
-"posean un workflow."
+"Sección que muestra el reporte histórico de las transiciones del workflow. "
+"Aplica solo en entidades con workflow."
 
 msgid "context"
 msgstr "Contexto"
@@ -1503,6 +1573,10 @@
 msgid "context where this component should be displayed"
 msgstr "Contexto en el cual el componente debe aparecer en el sistema"
 
+msgid "context where this facet should be displayed, leave empty for both"
+msgstr ""
+"Contexto en el cual esta faceta debe ser mostrada, dejar vacia para ambos"
+
 msgid "control subject entity's relations order"
 msgstr "Controla el orden de relaciones de la entidad sujeto"
 
@@ -1519,13 +1593,13 @@
 msgstr ""
 "Relación sistema que indica el(los) propietario(s) de una entidad. Esta "
 "relación pone de manera implícita al propietario en el grupo de propietarios "
-"de una entidad"
+"de una entidad."
 
 msgid "core relation indicating the original creator of an entity"
 msgstr "Relación sistema que indica el creador de una entidad."
 
 msgid "core relation indicating the type of an entity"
-msgstr "Relación sistema que indica el tipo de entidad"
+msgstr "Relación sistema que indica el tipo de entidad."
 
 msgid ""
 "core relation indicating the types (including specialized types) of an entity"
@@ -1546,7 +1620,7 @@
 msgstr "Crear una página de inicio"
 
 msgid "created on"
-msgstr "Creado el"
+msgstr "creado el"
 
 msgid "created_by"
 msgstr "creado por"
@@ -1574,6 +1648,11 @@
 msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
 msgstr "Creación de la relación %(linkto)s"
 
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr ""
+
 msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
 msgstr "Creación de un usuario para agregar al grupo %(linkto)s"
 
@@ -1582,12 +1661,13 @@
 
 msgid ""
 "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)"
-msgstr ""
+msgstr "creación de una expresión RQL por el derecho de lectura de %(linkto)s"
 
 msgid ""
 "creating RQLExpression (CWAttribute %(linkto)s update_permission "
 "RQLExpression)"
 msgstr ""
+"creación de una expresión RQL por el derecho de actualización de %(linkto)s"
 
 msgid ""
 "creating RQLExpression (CWEType %(linkto)s add_permission RQLExpression)"
@@ -1601,24 +1681,24 @@
 
 msgid ""
 "creating RQLExpression (CWEType %(linkto)s read_permission RQLExpression)"
-msgstr "Creación de una expresión RQL para la autorización de leer %(linkto)s"
+msgstr "Creación de una expresión RQL para permitir leer %(linkto)s"
 
 msgid ""
 "creating RQLExpression (CWEType %(linkto)s update_permission RQLExpression)"
-msgstr "Creación de una expresión RQL para autorizar actualizar %(linkto)s"
+msgstr "Creación de una expresión RQL para permitir actualizar %(linkto)s"
 
 msgid ""
 "creating RQLExpression (CWRelation %(linkto)s add_permission RQLExpression)"
-msgstr ""
+msgstr "Creación de una expresión RQL para permitir agregar %(linkto)s"
 
 msgid ""
 "creating RQLExpression (CWRelation %(linkto)s delete_permission "
 "RQLExpression)"
-msgstr ""
+msgstr "Creación de una expresión RQL para permitir eliminar %(linkto)s"
 
 msgid ""
 "creating RQLExpression (CWRelation %(linkto)s read_permission RQLExpression)"
-msgstr ""
+msgstr "Creación de una expresión RQL para permitir leer %(linkto)s"
 
 msgid "creating RQLExpression (Transition %(linkto)s condition RQLExpression)"
 msgstr "Creación de una expresión RQL para la transición %(linkto)s"
@@ -1626,111 +1706,112 @@
 msgid ""
 "creating RQLExpression (WorkflowTransition %(linkto)s condition "
 "RQLExpression)"
-msgstr ""
+msgstr "Creación de una expresión RQL para la transición Workflow %(linkto)s"
 
 msgid "creating State (State allowed_transition Transition %(linkto)s)"
-msgstr "Creación de un estado que pueda ir hacia la transición %(linkto)s"
+msgstr "Creación de un estado que puede ir hacia la transición %(linkto)s"
 
 msgid "creating State (State state_of Workflow %(linkto)s)"
-msgstr ""
+msgstr "Creando un Estado del Workflow"
 
 msgid "creating State (Transition %(linkto)s destination_state State)"
-msgstr "Creación de un estado destinación de la transición %(linkto)s"
+msgstr "Creación de un Estado Destinación de la Transición %(linkto)s"
 
 msgid ""
 "creating SubWorkflowExitPoint (WorkflowTransition %(linkto)s "
 "subworkflow_exit SubWorkflowExitPoint)"
-msgstr ""
+msgstr "creación de un punto de Salida de la Transición Workflow %(linkto)s"
 
 msgid "creating Transition (State %(linkto)s allowed_transition Transition)"
-msgstr "Creación de una transición autorizada desde el estado %(linkto)s"
+msgstr "Creación de una transición autorizada desde el Estado %(linkto)s"
 
 msgid "creating Transition (Transition destination_state State %(linkto)s)"
-msgstr "Creación de un transición hacia el estado %(linkto)s"
+msgstr "Creación de un transición hacia el Estado %(linkto)s"
 
 msgid "creating Transition (Transition transition_of Workflow %(linkto)s)"
-msgstr ""
+msgstr "Creación de una Transición Workflow %(linkto)s"
 
 msgid ""
 "creating WorkflowTransition (State %(linkto)s allowed_transition "
 "WorkflowTransition)"
 msgstr ""
+"Creación de una Transición Workflow permitida desde el estado %(linkto)s"
 
 msgid ""
-"creating WorkflowTransition (WorkflowTransition transition_of Workflow %"
-"(linkto)s)"
-msgstr ""
+"creating WorkflowTransition (WorkflowTransition transition_of Workflow "
+"%(linkto)s)"
+msgstr "Creación de una Transición Workflow del Workflow %(linkto)s"
 
 msgid "creation"
 msgstr "Creación"
 
 msgid "creation date"
-msgstr ""
+msgstr "Fecha de Creación"
 
 msgid "creation time of an entity"
 msgstr "Fecha de creación de una entidad"
 
 msgid "creation_date"
-msgstr "fecha de creación"
+msgstr "Fecha de Creación"
 
 msgid "cstrtype"
-msgstr "Tipo de condición"
+msgstr "Tipo de restricción"
 
 msgctxt "CWConstraint"
 msgid "cstrtype"
-msgstr ""
+msgstr "Tipo"
+
+msgid "cstrtype_object"
+msgstr "utilizado por"
 
 msgctxt "CWConstraintType"
 msgid "cstrtype_object"
-msgstr ""
-
-msgid "cstrtype_object"
-msgstr "utilizado por"
+msgstr "Tipo de restricciones"
 
 msgid "csv entities export"
 msgstr "Exportar entidades en csv"
 
 msgid "csv export"
-msgstr "Exportar CSV"
+msgstr "Exportar en CSV"
 
 msgid "ctxtoolbar"
-msgstr ""
+msgstr "Barra de herramientas"
 
 msgid "custom_workflow"
-msgstr ""
+msgstr "Workflow específico"
 
 msgid "custom_workflow_object"
-msgstr ""
+msgstr "Workflow de"
 
 msgid "cwetype-box"
-msgstr ""
+msgstr "Vista \"caja\""
 
 msgid "cwetype-description"
-msgstr ""
+msgstr "Descripción"
 
 msgid "cwetype-permissions"
-msgstr ""
+msgstr "Permisos"
 
 msgid "cwetype-views"
-msgstr ""
+msgstr "Vistas"
 
 msgid "cwetype-workflow"
-msgstr ""
+msgstr "Workflow"
 
 msgid "cwgroup-main"
-msgstr ""
+msgstr "Descripción"
 
 msgid "cwgroup-permissions"
-msgstr ""
+msgstr "Permisos"
 
 msgid "cwrtype-description"
-msgstr ""
+msgstr "Descripción"
 
 msgid "cwrtype-permissions"
-msgstr ""
+msgstr "Permisos"
 
 msgid "cwuri"
-msgstr ""
+msgstr "Uri Interna"
 
 msgid "data directory url"
 msgstr "Url del repertorio de datos"
@@ -1751,37 +1832,38 @@
 msgstr "Valor por defecto"
 
 msgid "default text format for rich text fields."
-msgstr "Formato de texto como opción por defecto para los campos texto"
+msgstr ""
+"Formato de texto que se utilizará por defecto para los campos de tipo texto"
 
 msgid "default user workflow"
-msgstr ""
+msgstr "Workflow por defecto de los usuarios"
 
 msgid "default value"
-msgstr ""
+msgstr "Valor por defecto"
 
 msgid "default workflow for an entity type"
-msgstr ""
+msgstr "Workflow por defecto para un tipo de entidad"
 
 msgid "default_workflow"
-msgstr ""
+msgstr "Workflow por defecto"
 
 msgctxt "CWEType"
 msgid "default_workflow"
-msgstr ""
+msgstr "Workflow por defecto"
+
+msgid "default_workflow_object"
+msgstr "Workflow por defecto de"
 
 msgctxt "Workflow"
 msgid "default_workflow_object"
-msgstr ""
-
-msgid "default_workflow_object"
-msgstr ""
+msgstr "Workflow por defecto de"
 
 msgid "defaultval"
 msgstr "Valor por defecto"
 
 msgctxt "CWAttribute"
 msgid "defaultval"
-msgstr ""
+msgstr "Valor por defecto"
 
 msgid "define a CubicWeb user"
 msgstr "Define un usuario CubicWeb"
@@ -1793,14 +1875,21 @@
 "define a final relation: link a final relation type from a non final entity "
 "to a final entity type. used to build the instance schema"
 msgstr ""
+"Define una relación final: liga un tipo de relación final desde una entidad "
+"NO final hacia un tipo de entidad final. Se usa para crear el esquema de la "
+"instancia."
 
 msgid ""
 "define a non final relation: link a non final relation type from a non final "
 "entity to a non final entity type. used to build the instance schema"
 msgstr ""
+"Define una relación NO final: liga un tipo de relación NO final desde una  "
+"entidad NO final hacia un tipo de entidad NO final. Se usa para crear el "
+"esquema de la instancia."
 
 msgid "define a relation type, used to build the instance schema"
 msgstr ""
+"Define un tipo de relación, usado para construir el esquema de la instancia."
 
 msgid "define a rql expression used to define permissions"
 msgstr "Expresión RQL utilizada para definir los derechos de acceso"
@@ -1813,16 +1902,20 @@
 
 msgid "define an entity type, used to build the instance schema"
 msgstr ""
+"Define un tipo de entidad, usado para construir el esquema de la instancia."
 
 msgid "define how we get out from a sub-workflow"
+msgstr "Define como salir de un sub-Workflow"
+
+msgid "defines a sql-level multicolumn unique index"
 msgstr ""
 
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
 msgstr ""
-"Define a que se aplica la propiedad . Usted debe seleccionar esto antes de "
-"poder fijar un valor"
+"Define a que se aplica la propiedad . Debe de seleccionar esto antes de "
+"establecer un valor"
 
 msgid "delete"
 msgstr "Eliminar"
@@ -1836,30 +1929,27 @@
 msgid "delete this relation"
 msgstr "Eliminar esta relación"
 
-msgid "delete_perm"
-msgstr "Eliminar"
-
 msgid "delete_permission"
-msgstr "Autorización de eliminar"
+msgstr "Permiso de eliminar"
 
 msgctxt "CWEType"
 msgid "delete_permission"
-msgstr ""
+msgstr "Permiso de eliminar"
 
 msgctxt "CWRelation"
 msgid "delete_permission"
-msgstr ""
+msgstr "Permiso de eliminar"
+
+msgid "delete_permission_object"
+msgstr "posee permiso para eliminar"
 
 msgctxt "CWGroup"
 msgid "delete_permission_object"
-msgstr ""
+msgstr "puede eliminar"
 
 msgctxt "RQLExpression"
 msgid "delete_permission_object"
-msgstr ""
-
-msgid "delete_permission_object"
-msgstr "posee la autorización de eliminar"
+msgstr "puede eliminar"
 
 #, python-format
 msgid "deleted %(etype)s #%(eid)s (%(title)s)"
@@ -1867,96 +1957,100 @@
 
 #, python-format
 msgid ""
-"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%"
-"(eidto)s"
+"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #"
+"%(eidto)s"
 msgstr ""
+"La relación %(rtype)s de %(frometype)s #%(eidfrom)s a %(toetype)s #%(eidto)s "
+"ha sido suprimida."
 
 msgid "depends on the constraint type"
-msgstr "Depende del tipo de condición"
-
+msgstr "Depende del tipo de restricción"
+
+msgid "description"
+msgstr "Descripción"
+
+msgctxt "BaseTransition"
+msgid "description"
+msgstr "Descripción"
+
+msgctxt "CWAttribute"
 msgid "description"
 msgstr "Descripción"
 
 msgctxt "CWEType"
 msgid "description"
-msgstr ""
+msgstr "Descripción"
+
+msgctxt "CWRType"
+msgid "description"
+msgstr "Descripción"
 
 msgctxt "CWRelation"
 msgid "description"
-msgstr ""
-
-msgctxt "Workflow"
+msgstr "Descripción"
+
+msgctxt "State"
 msgid "description"
-msgstr ""
-
-msgctxt "CWAttribute"
-msgid "description"
-msgstr ""
+msgstr "Descripción"
 
 msgctxt "Transition"
 msgid "description"
-msgstr ""
+msgstr "Descripción"
+
+msgctxt "Workflow"
+msgid "description"
+msgstr "Descripción"
 
 msgctxt "WorkflowTransition"
 msgid "description"
-msgstr ""
-
-msgctxt "State"
-msgid "description"
-msgstr ""
-
-msgctxt "CWRType"
-msgid "description"
-msgstr ""
+msgstr "Descripción"
+
+msgid "description_format"
+msgstr "Formato"
 
 msgctxt "BaseTransition"
-msgid "description"
-msgstr ""
-
+msgid "description_format"
+msgstr "Formato"
+
+msgctxt "CWAttribute"
 msgid "description_format"
 msgstr "Formato"
 
 msgctxt "CWEType"
 msgid "description_format"
-msgstr ""
+msgstr "Formato"
+
+msgctxt "CWRType"
+msgid "description_format"
+msgstr "Formato"
 
 msgctxt "CWRelation"
 msgid "description_format"
-msgstr ""
+msgstr "Formato"
+
+msgctxt "State"
+msgid "description_format"
+msgstr "Formato"
+
+msgctxt "Transition"
+msgid "description_format"
+msgstr "Formato"
 
 msgctxt "Workflow"
 msgid "description_format"
-msgstr ""
-
-msgctxt "CWAttribute"
-msgid "description_format"
-msgstr ""
-
-msgctxt "Transition"
-msgid "description_format"
-msgstr ""
+msgstr "Formato"
 
 msgctxt "WorkflowTransition"
 msgid "description_format"
-msgstr ""
-
-msgctxt "State"
-msgid "description_format"
-msgstr ""
-
-msgctxt "CWRType"
-msgid "description_format"
-msgstr ""
-
-msgctxt "BaseTransition"
-msgid "description_format"
-msgstr ""
+msgstr "Formato"
 
 msgid "destination state for this transition"
-msgstr "Estado destino para esta transición"
+msgstr "Estados accesibles para esta transición"
 
 msgid "destination state must be in the same workflow as our parent transition"
 msgstr ""
+"El estado de destino debe pertenecer al mismo Workflow que la transición "
+"padre."
 
 msgid "destination state of a transition"
 msgstr "Estado destino de una transición"
@@ -1965,24 +2059,27 @@
 "destination state. No destination state means that transition should go back "
 "to the state from which we've entered the subworkflow."
 msgstr ""
-
+"Estado destino de la transición. Si el Estado destino no ha sido "
+"especificado, la transición regresará hacia el estado que tenía la entidad "
+"al entrar en el Sub-Workflow."
+
+msgid "destination_state"
+msgstr "Estado destino"
+
+msgctxt "SubWorkflowExitPoint"
 msgid "destination_state"
 msgstr "Estado destino"
 
 msgctxt "Transition"
 msgid "destination_state"
-msgstr ""
-
-msgctxt "SubWorkflowExitPoint"
-msgid "destination_state"
-msgstr ""
+msgstr "Estado destino"
+
+msgid "destination_state_object"
+msgstr "Destino de"
 
 msgctxt "State"
 msgid "destination_state_object"
-msgstr ""
-
-msgid "destination_state_object"
-msgstr "Destino de"
+msgstr "Estado final de"
 
 msgid "detach attached file"
 msgstr "soltar el archivo existente"
@@ -1993,11 +2090,17 @@
 msgid "display order of the component"
 msgstr "Orden de aparición del componente"
 
+msgid "display order of the facet"
+msgstr "Orden de aparición de la faceta"
+
 msgid "display the box or not"
-msgstr "Mostrar la caja o no"
+msgstr "Mostrar o no la caja"
 
 msgid "display the component or not"
-msgstr "Mostrar el componente o no"
+msgstr "Mostrar o no el componente"
+
+msgid "display the facet or not"
+msgstr "Mostrar o no la faceta"
 
 msgid ""
 "distinct label to distinguate between other permission entity of the same "
@@ -2011,19 +2114,19 @@
 
 #, python-format
 msgid "download %s"
-msgstr ""
+msgstr "Descargar %s"
 
 msgid "download icon"
 msgstr "ícono de descarga"
 
 msgid "download schema as owl"
-msgstr "Descargar esquema en OWL"
+msgstr "Descargar esquema en formato OWL"
 
 msgid "edit bookmarks"
 msgstr "Editar favoritos"
 
 msgid "edit canceled"
-msgstr ""
+msgstr "Edición cancelada"
 
 msgid "edit the index page"
 msgstr "Modificar la página de inicio"
@@ -2031,9 +2134,6 @@
 msgid "editable-table"
 msgstr "Tabla modificable"
 
-msgid "edition"
-msgstr "Edición"
-
 msgid "eid"
 msgstr "eid"
 
@@ -2044,7 +2144,10 @@
 msgstr "Mensajes enviados con éxito"
 
 msgid "embed"
-msgstr "Incrustrado"
+msgstr "Incrustado"
+
+msgid "embedded html"
+msgstr "Html incrustado"
 
 msgid "embedding this url is forbidden"
 msgstr "La inclusión de este url esta prohibida"
@@ -2053,28 +2156,28 @@
 msgstr "Entidades eliminadas"
 
 msgid "entity copied"
-msgstr "entidad copiada"
+msgstr "Entidad copiada"
 
 msgid "entity created"
-msgstr "entidad creada"
+msgstr "Entidad creada"
 
 msgid "entity creation"
-msgstr ""
+msgstr "Creación de entidad"
 
 msgid "entity deleted"
 msgstr "Entidad eliminada"
 
 msgid "entity deletion"
-msgstr ""
+msgstr "Eliminación de entidad"
 
 msgid "entity edited"
-msgstr "entidad modificada"
+msgstr "Entidad modificada"
 
 msgid "entity has no workflow set"
-msgstr ""
+msgstr "La entidad no tiene Workflow"
 
 msgid "entity linked"
-msgstr "entidad asociada"
+msgstr "Entidad asociada"
 
 msgid "entity type"
 msgstr "Tipo de entidad"
@@ -2087,10 +2190,10 @@
 "avanzada"
 
 msgid "entity types which may use this workflow"
-msgstr ""
+msgstr "Tipos de entidades que pueden utilizar este Workflow"
 
 msgid "entity update"
-msgstr ""
+msgstr "Actualización de la Entidad"
 
 msgid "error while embedding page"
 msgstr "Error durante la inclusión de la página"
@@ -2101,7 +2204,7 @@
 
 msgid "error while publishing ReST text"
 msgstr ""
-"Se ha producido un error durante la interpretación del texto en formatoReST"
+"Se ha producido un error durante la interpretación del texto en formato ReST"
 
 #, python-format
 msgid "error while querying source %s, some data may be missing"
@@ -2110,20 +2213,20 @@
 "datos visibles se encuentren incompletos"
 
 msgid "eta_date"
-msgstr "fecha de fin"
-
-msgid "exit state must a subworkflow state"
-msgstr ""
+msgstr "Fecha de fin"
+
+msgid "exit state must be a subworkflow state"
+msgstr "El estado de salida debe de ser un estado del Sub-Workflow"
 
 msgid "exit_point"
-msgstr ""
+msgstr "Estado de Salida"
 
 msgid "exit_point_object"
-msgstr ""
+msgstr "Estado de Salida de"
 
 #, python-format
 msgid "exiting from subworkflow %s"
-msgstr ""
+msgstr "Salida del subworkflow %s"
 
 msgid "expected:"
 msgstr "Previsto :"
@@ -2133,14 +2236,14 @@
 
 msgctxt "RQLExpression"
 msgid "expression"
-msgstr ""
+msgstr "RQL de la expresión"
 
 msgid "exprtype"
 msgstr "Tipo de la expresión"
 
 msgctxt "RQLExpression"
 msgid "exprtype"
-msgstr ""
+msgstr "Tipo"
 
 msgid "external page"
 msgstr "Página externa"
@@ -2149,71 +2252,71 @@
 msgstr "Caja de facetas"
 
 msgid "facets_created_by-facet"
-msgstr "faceta \"creada por\""
+msgstr "Faceta \"creada por\""
 
 msgid "facets_created_by-facet_description"
-msgstr "faceta creado por"
+msgstr "Faceta creada por"
 
 msgid "facets_cwfinal-facet"
-msgstr "faceta \"final\""
+msgstr "Faceta \"final\""
 
 msgid "facets_cwfinal-facet_description"
-msgstr "faceta para las entidades \"finales\""
+msgstr "Faceta para las entidades \"finales\""
 
 msgid "facets_etype-facet"
-msgstr "faceta \"es de tipo\""
+msgstr "Faceta \"es de tipo\""
 
 msgid "facets_etype-facet_description"
-msgstr "faceta es de tipo"
+msgstr "Faceta es de tipo"
 
 msgid "facets_has_text-facet"
-msgstr "faceta \"contiene el texto\""
+msgstr "Faceta \"contiene el texto\""
 
 msgid "facets_has_text-facet_description"
-msgstr "faceta contiene el texto"
+msgstr "Faceta contiene el texto"
 
 msgid "facets_in_group-facet"
-msgstr "faceta \"forma parte del grupo\""
+msgstr "Faceta \"forma parte del grupo\""
 
 msgid "facets_in_group-facet_description"
-msgstr "faceta en grupo"
+msgstr "Faceta en grupo"
 
 msgid "facets_in_state-facet"
-msgstr "faceta \"en el estado\""
+msgstr "Faceta \"en el estado\""
 
 msgid "facets_in_state-facet_description"
-msgstr "faceta en el estado"
+msgstr "Faceta en el estado"
 
 #, python-format
 msgid "failed to uniquify path (%s, %s)"
-msgstr ""
+msgstr "No se pudo obtener un dato único (%s, %s)"
 
 msgid "february"
 msgstr "Febrero"
 
 msgid "file tree view"
-msgstr "File Vista Arborescencia"
+msgstr "Arborescencia (archivos)"
 
 msgid "final"
 msgstr "Final"
 
 msgctxt "CWEType"
 msgid "final"
-msgstr ""
+msgstr "Final"
 
 msgctxt "CWRType"
 msgid "final"
-msgstr ""
+msgstr "Final"
 
 msgid "first name"
-msgstr ""
+msgstr "Nombre"
 
 msgid "firstname"
 msgstr "Nombre"
 
 msgctxt "CWUser"
 msgid "firstname"
-msgstr ""
+msgstr "Nombre"
 
 msgid "foaf"
 msgstr "Amigo de un Amigo, FOAF"
@@ -2223,24 +2326,24 @@
 
 #, python-format
 msgid "follow this link for more information on this %s"
-msgstr ""
+msgstr "Seleccione esta liga para obtener mayor información sobre %s"
 
 msgid "follow this link if javascript is deactivated"
-msgstr ""
+msgstr "Seleccione esta liga si javascript esta desactivado"
 
 msgid "for_user"
 msgstr "Para el usuario"
 
 msgctxt "CWProperty"
 msgid "for_user"
-msgstr ""
+msgstr "Propiedad del Usuario"
+
+msgid "for_user_object"
+msgstr "Utiliza las propiedades"
 
 msgctxt "CWUser"
 msgid "for_user_object"
-msgstr ""
-
-msgid "for_user_object"
-msgstr "Utiliza las propiedades"
+msgstr "Tiene como preferencia"
 
 msgid "friday"
 msgstr "Viernes"
@@ -2257,35 +2360,35 @@
 
 msgctxt "CWAttribute"
 msgid "from_entity"
-msgstr ""
+msgstr "Atributo de la entidad"
 
 msgctxt "CWRelation"
 msgid "from_entity"
-msgstr ""
-
-msgctxt "CWEType"
-msgid "from_entity_object"
-msgstr ""
+msgstr "Relación de la entidad"
 
 msgid "from_entity_object"
 msgstr "Relación sujeto"
 
+msgctxt "CWEType"
+msgid "from_entity_object"
+msgstr "Entidad de"
+
 msgid "from_interval_start"
-msgstr ""
+msgstr "De"
 
 msgid "from_state"
-msgstr "De el estado"
+msgstr "Del Estado"
 
 msgctxt "TrInfo"
 msgid "from_state"
-msgstr ""
+msgstr "Estado de Inicio"
+
+msgid "from_state_object"
+msgstr "Transiciones desde este estado"
 
 msgctxt "State"
 msgid "from_state_object"
-msgstr ""
-
-msgid "from_state_object"
-msgstr "Transiciones desde este estado"
+msgstr "Estado de Inicio de"
 
 msgid "full text or RQL query"
 msgstr "Texto de búsqueda o demanda RQL"
@@ -2295,25 +2398,27 @@
 
 msgctxt "CWRType"
 msgid "fulltext_container"
-msgstr ""
+msgstr "Objeto a indexar"
 
 msgid "fulltextindexed"
 msgstr "Indexación de texto"
 
 msgctxt "CWAttribute"
 msgid "fulltextindexed"
-msgstr ""
+msgstr "Texto indexado"
 
 msgid "generic plot"
-msgstr "Trazado de curbas estándares"
+msgstr "Gráfica Genérica"
 
 msgid "generic relation to link one entity to another"
-msgstr "relación generica para ligar entidades"
+msgstr "Relación genérica para ligar entidades"
 
 msgid ""
 "generic relation to specify that an external entity represent the same "
 "object as a local one: http://www.w3.org/TR/owl-ref/#sameAs-def"
 msgstr ""
+"Relación genérica que indicar que una entidad es idéntica a otro recurso web "
+"(ver http://www.w3.org/TR/owl-ref/#sameAs-def)."
 
 msgid "go back to the index page"
 msgstr "Regresar a la página de inicio"
@@ -2321,25 +2426,34 @@
 msgid "granted to groups"
 msgstr "Otorgado a los grupos"
 
-msgid "graphical representation of the instance'schema"
-msgstr ""
+#, python-format
+msgid "graphical representation of %(appid)s data model"
+msgstr "Representación gráfica del modelo de datos de %(appid)s"
 
 #, python-format
-msgid "graphical schema for %s"
-msgstr "Gráfica del esquema por %s"
+msgid ""
+"graphical representation of the %(etype)s entity type from %(appid)s data "
+"model"
+msgstr ""
+"Representación gráfica del modelo de datos para el tipo de entidad %(etype)s "
+"de %(appid)s"
 
 #, python-format
-msgid "graphical workflow for %s"
-msgstr "Gráfica del workflow por %s"
+msgid ""
+"graphical representation of the %(rtype)s relation type from %(appid)s data "
+"model"
+msgstr ""
+"Representación gráfica del modelo de datos para el tipo de relación "
+"%(rtype)s de %(appid)s"
 
 msgid "group in which a user should be to be allowed to pass this transition"
-msgstr "Grupo en el cual el usuario debe estar para poder pasar la transición"
+msgstr "Grupo en el cual el usuario debe estar lograr la transición"
 
 msgid "groups"
 msgstr "Grupos"
 
 msgid "groups grant permissions to the user"
-msgstr "Los grupos otorgan las autorizaciones al usuario"
+msgstr "Los grupos otorgan los permisos al usuario"
 
 msgid "groups to which the permission is granted"
 msgstr "Grupos quienes tienen otorgada esta autorización"
@@ -2363,42 +2477,43 @@
 "how to format date and time in the ui (\"man strftime\" for format "
 "description)"
 msgstr ""
-"Como formatear la fecha en la interface (\"man strftime\" por la descripción "
-"del formato)"
+"Formato de fecha y hora que se utilizará por defecto en la interfaz (\"man "
+"strftime\" para mayor información del formato)"
 
 msgid "how to format date in the ui (\"man strftime\" for format description)"
 msgstr ""
-"Como formatear la fecha en la interface (\"man strftime\" por la descripción "
-"del formato)"
+"Formato de fecha que se utilizará por defecto en la interfaz (\"man strftime"
+"\" para mayor información  del formato)"
 
 msgid "how to format float numbers in the ui"
-msgstr "Como formatear los números flotantes en la interface"
+msgstr ""
+"Formato de números flotantes que se utilizará por defecto en la interfaz"
 
 msgid "how to format time in the ui (\"man strftime\" for format description)"
 msgstr ""
-"Como formatear la hora en la interface (\"man strftime\" por la descripción "
-"del formato)"
+"Formato de hora que se utilizará por defecto en la interfaz (\"man strftime"
+"\" para mayor información del formato)"
 
 msgid "i18n_bookmark_url_fqs"
-msgstr ""
+msgstr "Parámetros"
 
 msgid "i18n_bookmark_url_path"
-msgstr ""
+msgstr "Ruta"
 
 msgid "i18n_login_popup"
 msgstr "Identificarse"
 
 msgid "i18ncard_*"
-msgstr ""
+msgstr "0..n"
 
 msgid "i18ncard_+"
-msgstr ""
+msgstr "1..n"
 
 msgid "i18ncard_1"
-msgstr ""
+msgstr "1"
 
 msgid "i18ncard_?"
-msgstr ""
+msgstr "0..1"
 
 msgid "i18nprevnext_next"
 msgstr "Siguiente"
@@ -2416,7 +2531,7 @@
 msgstr "ID del template principal"
 
 msgid "identical to"
-msgstr ""
+msgstr "Idéntico a"
 
 msgid "identical_to"
 msgstr "idéntico a"
@@ -2432,7 +2547,7 @@
 "entity (the container)."
 msgstr ""
 "Si el texto indexado de la entidad sujeto/objeto debe ser agregado a la "
-"entidad a el otro extremo de la relación (el contenedor)."
+"entidad al otro extremo de la relación (el contenedor)."
 
 msgid "image"
 msgstr "Imagen"
@@ -2442,26 +2557,26 @@
 
 msgctxt "CWUser"
 msgid "in_group"
-msgstr ""
+msgstr "Forma parte del grupo"
+
+msgid "in_group_object"
+msgstr "Miembros"
 
 msgctxt "CWGroup"
 msgid "in_group_object"
-msgstr ""
-
-msgid "in_group_object"
-msgstr "Miembros"
+msgstr "Contiene los usuarios"
 
 msgid "in_state"
-msgstr "estado"
+msgstr "Estado"
 
 msgid "in_state_object"
-msgstr "estado de"
+msgstr "Estado de"
 
 msgid "incontext"
 msgstr "En el contexto"
 
 msgid "incorrect captcha value"
-msgstr ""
+msgstr "Valor del Captcha incorrecto"
 
 #, python-format
 msgid "incorrect value (%(value)s) for type \"%(type)s\""
@@ -2475,7 +2590,7 @@
 
 msgctxt "CWAttribute"
 msgid "indexed"
-msgstr ""
+msgstr "Indexado"
 
 msgid "indicate the current state of an entity"
 msgstr "Indica el estado actual de una entidad"
@@ -2487,51 +2602,51 @@
 "Indica cual estado deberá ser utilizado por defecto al crear una entidad"
 
 msgid "info"
-msgstr ""
+msgstr "Información del Sistema"
 
 #, python-format
 msgid "initial estimation %s"
 msgstr "Estimación inicial %s"
 
 msgid "initial state for this workflow"
-msgstr ""
+msgstr "Estado inicial para este Workflow"
 
 msgid "initial_state"
-msgstr "estado inicial"
+msgstr "Estado inicial"
 
 msgctxt "Workflow"
 msgid "initial_state"
-msgstr ""
+msgstr "Estado inicial"
+
+msgid "initial_state_object"
+msgstr "Estado inicial de"
 
 msgctxt "State"
 msgid "initial_state_object"
-msgstr ""
-
-msgid "initial_state_object"
-msgstr "es el estado inicial de"
+msgstr "Estado inicial de"
 
 msgid "inlined"
-msgstr "Puesto en línea"
+msgstr "Inlined"
 
 msgctxt "CWRType"
 msgid "inlined"
-msgstr ""
+msgstr "Inlined"
 
 msgid "instance home"
-msgstr ""
+msgstr "Repertorio de la Instancia"
 
 msgid "instance schema"
-msgstr ""
+msgstr "Esquema de la Instancia"
 
 msgid "internal entity uri"
-msgstr ""
+msgstr "Uri Interna"
 
 msgid "internationalizable"
 msgstr "Internacionalizable"
 
 msgctxt "CWAttribute"
 msgid "internationalizable"
-msgstr ""
+msgstr "Internacionalizable"
 
 #, python-format
 msgid "invalid action %r"
@@ -2539,7 +2654,7 @@
 
 #, python-format
 msgid "invalid value %(value)s, it must be one of %(choices)s"
-msgstr ""
+msgstr "Valor %(value)s incorrecto, debe estar entre %(choices)s"
 
 msgid "is"
 msgstr "es"
@@ -2567,7 +2682,7 @@
 "is this relation physically inlined? you should know what you're doing if "
 "you are changing this!"
 msgstr ""
-"Es esta relación puesta en línea en la base de datos  ? Usted debe saber lo "
+"Es esta relación estilo INLINED en la base de datos  ? Usted debe saber lo "
 "que hace si cambia esto !"
 
 msgid "is_instance_of"
@@ -2593,35 +2708,35 @@
 
 msgctxt "CWPermission"
 msgid "label"
-msgstr ""
+msgstr "Etiqueta"
 
 msgid "language of the user interface"
-msgstr "Idioma para la interface del usuario"
+msgstr "Idioma que se utilizará por defecto en la interfaz usuario"
 
 msgid "last connection date"
-msgstr "Ultima fecha de conexión"
+msgstr "Ultima conexión"
 
 msgid "last login time"
-msgstr ""
+msgstr "Ultima conexión"
 
 msgid "last name"
-msgstr ""
+msgstr "Apellido"
 
 msgid "last usage"
-msgstr ""
+msgstr "Ultimo uso"
 
 msgid "last_login_time"
 msgstr "Ultima fecha de conexión"
 
 msgctxt "CWUser"
 msgid "last_login_time"
-msgstr ""
+msgstr "Ultima conexión"
 
 msgid "latest modification time of an entity"
 msgstr "Fecha de la última modificación de una entidad "
 
 msgid "latest update on"
-msgstr "actualizado el"
+msgstr "Actualizado el"
 
 msgid "left"
 msgstr "izquierda"
@@ -2630,56 +2745,56 @@
 "link a permission to the entity. This permission should be used in the "
 "security definition of the entity's type to be useful."
 msgstr ""
-"relaciónar una autorización con la entidad. Este autorización debe ser usada "
-"en la definición de la entidad para ser utíl."
+"Relacionar un permiso con la entidad. Este permiso debe ser integrado en la "
+"definición de seguridad de la entidad para poder ser utilizado."
 
 msgid ""
 "link a property to the user which want this property customization. Unless "
 "you're a site manager, this relation will be handled automatically."
 msgstr ""
-"Liga una propiedad a el usuario que desea esta personalización. Salvo que "
-"usted sea un administrador del sistema, esta relación es gestionada "
-"automáticamente."
+"Liga una propiedad al usuario que desea esta personalización. Salvo que "
+"usted sea un administrador del sistema, esta relación será administrada de "
+"forma automática."
 
 msgid "link a relation definition to its object entity type"
-msgstr "liga una definición de relación a su tipo de entidad objeto"
+msgstr "Liga una definición de relación a su tipo de entidad objeto"
 
 msgid "link a relation definition to its relation type"
-msgstr "liga una definición de relación a su tipo de relación"
+msgstr "Liga una definición de relación a su tipo de relación"
 
 msgid "link a relation definition to its subject entity type"
-msgstr "liga una definición de relación a su tipo de entidad"
+msgstr "Liga una definición de relación a su tipo de entidad"
 
 msgid "link a state to one or more workflow"
-msgstr ""
+msgstr "Liga un estado a uno o más Workflow"
 
 msgid "link a transition information to its object"
-msgstr "liga una transcion de informacion a los objetos asociados"
+msgstr "Liga una transición de informacion hacia los objetos asociados"
 
 msgid "link a transition to one or more workflow"
-msgstr ""
+msgstr "Liga una transición a uno o más Workflow"
 
 msgid "link a workflow to one or more entity type"
-msgstr ""
+msgstr "Liga un Workflow a uno a más tipos de entidad"
 
 msgid "list"
 msgstr "Lista"
 
 msgid "log in"
-msgstr "Identificarse"
+msgstr "Acceder"
 
 msgid "log out first"
-msgstr ""
+msgstr "Desconéctese primero"
 
 msgid "login"
-msgstr "Clave de acesso"
+msgstr "Usuario"
 
 msgctxt "CWUser"
 msgid "login"
-msgstr ""
+msgstr "Usuario"
 
 msgid "login or email"
-msgstr "Clave de acesso o dirección de correo"
+msgstr "Usuario o dirección de correo"
 
 msgid "login_action"
 msgstr "Ingresa tus datos"
@@ -2695,144 +2810,147 @@
 msgstr "Informaciones Generales"
 
 msgid "mainvars"
-msgstr "Principales variables"
+msgstr "Variables principales"
 
 msgctxt "RQLExpression"
 msgid "mainvars"
-msgstr ""
+msgstr "Variables principales"
 
 msgid "manage"
-msgstr "Administracion del Sitio"
+msgstr "Administración Sistema"
 
 msgid "manage bookmarks"
-msgstr "Administra tus favoritos"
+msgstr "Gestión de favoritos"
 
 msgid "manage permissions"
-msgstr "Administración de Autorizaciones"
+msgstr "Gestión de permisos"
 
 msgid "manage security"
-msgstr "Administración de la Seguridad"
+msgstr "Gestión de seguridad"
 
 msgid "managers"
-msgstr "editores"
+msgstr "Administradores"
 
 msgid "mandatory relation"
-msgstr ""
+msgstr "Relación obligatoria"
 
 msgid "march"
 msgstr "Marzo"
 
 msgid "maximum number of characters in short description"
-msgstr "Numero maximo de caracteres en las descripciones cortas"
+msgstr "Máximo de caracteres en las descripciones cortas"
 
 msgid "maximum number of entities to display in related combo box"
-msgstr "Numero maximo de entidades a mostrar en las listas dinamicas"
+msgstr "Máximo de entidades a mostrar en las listas dinámicas"
 
 msgid "maximum number of objects displayed by page of results"
-msgstr "Numero maximo de objetos mostrados por pagina de resultados"
+msgstr "Máximo de elementos mostrados por página de resultados"
 
 msgid "maximum number of related entities to display in the primary view"
-msgstr "Numero maximo de entidades ligadas a mostrar en la vista primaria"
+msgstr "Máximo de entidades relacionadas a mostrar en la vista primaria"
 
 msgid "may"
 msgstr "Mayo"
 
 msgid "memory leak debugging"
-msgstr ""
+msgstr "depuración (debugging) de fuga de memoria"
 
 msgid "milestone"
 msgstr "Milestone"
 
 #, python-format
 msgid "missing parameters for entity %s"
-msgstr "Parametros faltantes a la entidad %s"
+msgstr "Parámetros faltantes a la entidad %s"
+
+msgid "modification"
+msgstr ""
 
 msgid "modification_date"
-msgstr "Fecha de modificacion"
+msgstr "Fecha de modificación"
 
 msgid "modify"
 msgstr "Modificar"
 
 msgid "monday"
-msgstr "Lundi"
+msgstr "Lunes"
 
 msgid "more actions"
-msgstr "mas acciones"
+msgstr "Más acciones"
 
 msgid "more info about this workflow"
-msgstr ""
+msgstr "Más información acerca de este workflow"
 
 msgid "multiple edit"
-msgstr "Edicion multiple"
+msgstr "Edición multiple"
 
 msgid "my custom search"
-msgstr "Mi busqueda personalizada"
-
+msgstr "Mi búsqueda personalizada"
+
+msgid "name"
+msgstr "Nombre"
+
+msgctxt "BaseTransition"
+msgid "name"
+msgstr "Nombre"
+
+msgctxt "CWCache"
+msgid "name"
+msgstr "Nombre"
+
+msgctxt "CWConstraintType"
 msgid "name"
 msgstr "Nombre"
 
 msgctxt "CWEType"
 msgid "name"
-msgstr ""
+msgstr "Nombre"
+
+msgctxt "CWGroup"
+msgid "name"
+msgstr "Nombre"
+
+msgctxt "CWPermission"
+msgid "name"
+msgstr "Nombre"
+
+msgctxt "CWRType"
+msgid "name"
+msgstr "Nombre"
+
+msgctxt "State"
+msgid "name"
+msgstr "Nombre"
 
 msgctxt "Transition"
 msgid "name"
-msgstr ""
+msgstr "Nombre"
 
 msgctxt "Workflow"
 msgid "name"
-msgstr ""
-
-msgctxt "CWGroup"
-msgid "name"
-msgstr ""
-
-msgctxt "CWConstraintType"
-msgid "name"
-msgstr ""
+msgstr "Nombre"
 
 msgctxt "WorkflowTransition"
 msgid "name"
-msgstr ""
-
-msgctxt "State"
-msgid "name"
-msgstr ""
-
-msgctxt "CWPermission"
-msgid "name"
-msgstr ""
-
-msgctxt "CWRType"
-msgid "name"
-msgstr ""
-
-msgctxt "BaseTransition"
-msgid "name"
-msgstr ""
-
-msgctxt "CWCache"
-msgid "name"
-msgstr ""
+msgstr "Nombre"
 
 msgid "name of the cache"
-msgstr "Nombre del Cache"
+msgstr "Nombre del Caché"
 
 msgid ""
 "name of the main variables which should be used in the selection if "
 "necessary (comma separated)"
 msgstr ""
-"Nombre de las variables principales que deberian se utilizadas en la "
-"selecciónde ser necesario (separarlas con comas)"
+"Nombre de las variables principales que deberían ser utilizadas en la "
+"selección de ser necesario (separarlas con comas)"
 
 msgid "name or identifier of the permission"
-msgstr "Nombre o indentificador de la autorización"
+msgstr "Nombre o identificador del permiso"
 
 msgid "navbottom"
-msgstr "Pie de pagina"
+msgstr "Pie de página"
 
 msgid "navcontentbottom"
-msgstr "Pie de pagina del contenido principal"
+msgstr "Pie de página del contenido principal"
 
 msgid "navcontenttop"
 msgstr "Encabezado"
@@ -2841,21 +2959,17 @@
 msgstr "Navegación"
 
 msgid "navigation.combobox-limit"
-msgstr ""
-
-# msgstr "Navegación: numero maximo de elementos en una caja de elección (combobox)"
+msgstr "ComboBox"
+
 msgid "navigation.page-size"
-msgstr ""
-
-# msgstr "Navegación: numero maximo de elementos por pagina"
+msgstr "Paginación"
+
 msgid "navigation.related-limit"
-msgstr ""
-
-# msgstr "Navegación: numero maximo de elementos relacionados"
+msgstr "Entidades relacionadas"
+
 msgid "navigation.short-line-size"
-msgstr ""
-
-# msgstr "Navegación: numero maximo de caracteres en una linéa corta"
+msgstr "Descripción corta"
+
 msgid "navtop"
 msgstr "Encabezado del contenido principal"
 
@@ -2866,331 +2980,349 @@
 msgstr "Siguientes resultados"
 
 msgid "no"
-msgstr "no"
+msgstr "No"
 
 msgid "no associated permissions"
-msgstr "no autorización relacionada"
+msgstr "No existe permiso asociado"
 
 #, python-format
 msgid "no edited fields specified for entity %s"
-msgstr ""
+msgstr "Ningún campo editable especificado para la entidad %s"
+
+msgid "no related entity"
+msgstr "No posee entidad asociada"
 
 msgid "no related project"
-msgstr "no hay proyecto relacionado"
+msgstr "No tiene proyecto relacionado"
 
 msgid "no repository sessions found"
-msgstr ""
+msgstr "Ninguna sesión encontrada"
 
 msgid "no selected entities"
-msgstr "no hay entidades seleccionadas"
+msgstr "No hay entidades seleccionadas"
 
 #, python-format
 msgid "no such entity type %s"
-msgstr "el tipo de entidad '%s' no existe"
+msgstr "El tipo de entidad '%s' no existe"
 
 msgid "no version information"
-msgstr "no información de version"
+msgstr "No existe la información de version"
 
 msgid "no web sessions found"
-msgstr ""
+msgstr "Ninguna sesión web encontrada"
 
 msgid "normal"
-msgstr ""
+msgstr "Normal"
 
 msgid "not authorized"
-msgstr "no autorizado"
+msgstr "No autorizado"
 
 msgid "not selected"
-msgstr "no seleccionado"
+msgstr "No seleccionado"
 
 msgid "november"
-msgstr "noviembre"
+msgstr "Noviembre"
 
 msgid "object"
-msgstr "objeto"
+msgstr "Objeto"
 
 msgid "object type"
-msgstr ""
+msgstr "Tipo de Objeto"
 
 msgid "october"
-msgstr "octubre"
+msgstr "Octubre"
 
 msgid "one month"
-msgstr "un mes"
+msgstr "Un mes"
 
 msgid "one week"
-msgstr "una semana"
+msgstr "Una semana"
 
 msgid "oneline"
-msgstr "una linea"
+msgstr "En una línea"
 
 msgid "only select queries are authorized"
-msgstr "solo estan permitidas consultas de lectura"
+msgstr "Solo están permitidas consultas de lectura"
 
 msgid "open all"
-msgstr "abrir todos"
+msgstr "Abrir todos"
 
 msgid "opened sessions"
-msgstr ""
+msgstr "Sesiones abiertas"
 
 msgid "opened web sessions"
-msgstr ""
+msgstr "Sesiones Web abiertas"
 
 msgid "options"
-msgstr ""
+msgstr "Opciones"
 
 msgid "order"
-msgstr "orden"
+msgstr "Orden"
 
 msgid "ordernum"
-msgstr "orden"
+msgstr "Orden"
 
 msgctxt "CWAttribute"
 msgid "ordernum"
-msgstr ""
+msgstr "Número de Orden"
 
 msgctxt "CWRelation"
 msgid "ordernum"
-msgstr ""
+msgstr "Número de Orden"
 
 msgid "owl"
-msgstr "owl"
+msgstr "OWL"
 
 msgid "owlabox"
-msgstr "owlabox"
+msgstr "OWLabox"
 
 msgid "owned_by"
-msgstr "pertenece a"
+msgstr "Pertenece a"
 
 msgid "owned_by_object"
-msgstr "pertenece al objeto"
+msgstr "Pertenece al objeto"
 
 msgid "owners"
-msgstr "proprietarios"
+msgstr "Proprietarios"
 
 msgid "ownership"
-msgstr "pertenencia"
+msgstr "Propiedad"
 
 msgid "ownerships have been changed"
-msgstr "la pertenencia ha sido modificada"
+msgstr "Derechos de propiedad modificados"
 
 msgid "pageid-not-found"
-msgstr "pagina no encontrada."
+msgstr "Página no encontrada."
 
 msgid "password"
-msgstr "Clave de acceso"
+msgstr "Contraseña"
 
 msgid "password and confirmation don't match"
-msgstr "La clave de acceso y la confirmación no concuerdan"
+msgstr "Su contraseña y confirmación no concuerdan"
 
 msgid "path"
 msgstr "Ruta"
 
 msgctxt "Bookmark"
 msgid "path"
-msgstr ""
+msgstr "Ruta"
 
 msgid "permission"
 msgstr "Permiso"
 
 msgid "permissions"
-msgstr ""
+msgstr "Permisos"
 
 msgid "permissions for this entity"
 msgstr "Permisos para esta entidad"
 
-msgid "personnal informations"
-msgstr "Información personal"
-
 msgid "pick existing bookmarks"
-msgstr "Seleccione los favoritos existentes"
+msgstr "Seleccionar favoritos existentes"
 
 msgid "pkey"
-msgstr "pkey"
+msgstr "Clave"
 
 msgctxt "CWProperty"
 msgid "pkey"
-msgstr ""
+msgstr "Código de la Propiedad"
 
 msgid "please correct errors below"
-msgstr "Favor de corregir errores"
+msgstr "Por favor corregir los errores señalados en la parte inferior"
 
 msgid "please correct the following errors:"
-msgstr "Favor de corregir los siguientes errores :"
+msgstr "Por favor corregir los siguientes errores:"
 
 msgid "possible views"
 msgstr "Vistas posibles"
 
 msgid "powered by CubicWeb"
-msgstr ""
+msgstr "Potenciado en CubicWeb"
 
 msgid "prefered_form"
-msgstr ""
+msgstr "Forma preferida"
 
 msgctxt "EmailAddress"
 msgid "prefered_form"
-msgstr ""
+msgstr "Email principal"
+
+msgid "prefered_form_object"
+msgstr "Formato preferido sobre"
 
 msgctxt "EmailAddress"
 msgid "prefered_form_object"
-msgstr ""
-
-msgid "prefered_form_object"
-msgstr ""
+msgstr "Email principal de"
 
 msgid "preferences"
 msgstr "Preferencias"
 
 msgid "previous_results"
-msgstr "Resultados anteriores"
+msgstr "Resultados Anteriores"
 
 msgid "primary"
 msgstr "Primaria"
 
 msgid "primary_email"
-msgstr "Dirección de email principal"
+msgstr "Dirección principal de correo electrónico"
 
 msgctxt "CWUser"
 msgid "primary_email"
-msgstr ""
+msgstr "Dirección principal de correo electrónico"
+
+msgid "primary_email_object"
+msgstr "Dirección de email principal (objeto)"
 
 msgctxt "EmailAddress"
 msgid "primary_email_object"
+msgstr "Dirección principal de correo electrónico de"
+
+msgid "profile"
 msgstr ""
 
-msgid "primary_email_object"
-msgstr "Dirección de email principal (objeto)"
-
 msgid "progress"
-msgstr "Avance"
+msgstr "Progreso"
 
 msgid "progress bar"
-msgstr "Barra de progreso de avance"
+msgstr "Barra de Progreso"
 
 msgid "project"
 msgstr "Proyecto"
 
 msgid "rdef-description"
-msgstr ""
+msgstr "Descripción"
 
 msgid "rdef-permissions"
-msgstr ""
+msgstr "Permisos"
 
 msgid "read"
 msgstr "Lectura"
 
-msgid "read_perm"
-msgstr "Lectura"
-
 msgid "read_permission"
 msgstr "Permiso de lectura"
 
+msgctxt "CWAttribute"
+msgid "read_permission"
+msgstr "Permiso de Lectura"
+
 msgctxt "CWEType"
 msgid "read_permission"
-msgstr ""
-
-msgctxt "CWAttribute"
-msgid "read_permission"
-msgstr ""
+msgstr "Permiso de Lectura"
 
 msgctxt "CWRelation"
 msgid "read_permission"
-msgstr ""
+msgstr "Permiso de Lectura"
+
+msgid "read_permission_object"
+msgstr "Tiene acceso de lectura a"
 
 msgctxt "CWGroup"
 msgid "read_permission_object"
-msgstr ""
+msgstr "Puede leer"
 
 msgctxt "RQLExpression"
 msgid "read_permission_object"
-msgstr ""
-
-msgid "read_permission_object"
-msgstr "Objeto_permiso_lectura"
+msgstr "Puede leer"
 
 msgid "registry"
-msgstr ""
+msgstr "Registro"
 
 msgid "related entity has no state"
-msgstr ""
+msgstr "La entidad relacionada no posee Estado"
 
 msgid "related entity has no workflow set"
-msgstr ""
+msgstr "La entidad relacionada no posee Workflow definido"
 
 msgid "relation"
-msgstr ""
+msgstr "relación"
 
 #, python-format
 msgid "relation %(relname)s of %(ent)s"
 msgstr "relación %(relname)s de %(ent)s"
 
 msgid "relation add"
-msgstr ""
+msgstr "Agregar Relación"
 
 msgid "relation removal"
-msgstr ""
+msgstr "Eliminar Relación"
 
 msgid "relation_type"
-msgstr "tipo de relación"
+msgstr "Tipo de Relación"
 
 msgctxt "CWAttribute"
 msgid "relation_type"
-msgstr ""
+msgstr "Tipo de Relación"
 
 msgctxt "CWRelation"
 msgid "relation_type"
-msgstr ""
+msgstr "Tipo de Relación"
+
+msgid "relation_type_object"
+msgstr "Definición de Relaciones"
 
 msgctxt "CWRType"
 msgid "relation_type_object"
+msgstr "Definición de Relaciones"
+
+msgid "relations"
 msgstr ""
 
-msgid "relation_type_object"
-msgstr "Definición"
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr ""
 
 msgid "relations deleted"
-msgstr "Relaciones eliminadas"
+msgstr "Relaciones Eliminadas"
+
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr ""
 
 msgid "relative url of the bookmarked page"
-msgstr "Url relativa de la pagina"
+msgstr "Url relativa de la página"
 
 msgid "remove-inlined-entity-form"
-msgstr ""
+msgstr "Eliminar"
 
 msgid "require_group"
-msgstr "Requiere grupo"
+msgstr "Requiere el grupo"
 
 msgctxt "BaseTransition"
 msgid "require_group"
-msgstr ""
+msgstr "Restringida al Grupo"
+
+msgctxt "CWPermission"
+msgid "require_group"
+msgstr "Restringida al Grupo"
 
 msgctxt "Transition"
 msgid "require_group"
-msgstr ""
-
-msgctxt "CWPermission"
-msgid "require_group"
-msgstr ""
+msgstr "Restringida al Grupo"
 
 msgctxt "WorkflowTransition"
 msgid "require_group"
-msgstr ""
+msgstr "Restringida al Grupo"
+
+msgid "require_group_object"
+msgstr "Posee derechos sobre"
 
 msgctxt "CWGroup"
 msgid "require_group_object"
-msgstr ""
-
-msgid "require_group_object"
-msgstr "Requerido por grupo"
+msgstr "Posee derechos sobre"
 
 msgid "require_permission"
-msgstr "Requiere autorización"
+msgstr "Requiere Permisos"
 
 msgid "require_permission_object"
 msgstr "Requerido por autorización"
 
 msgid "required"
-msgstr ""
+msgstr "Requerido"
 
 msgid "required attribute"
 msgstr "Atributo requerido"
@@ -3199,7 +3331,7 @@
 msgstr "Campo requerido"
 
 msgid "resources usage"
-msgstr ""
+msgstr "Recursos utilizados"
 
 msgid ""
 "restriction part of a rql query. For entity rql expression, X and U are "
@@ -3207,382 +3339,387 @@
 "relation rql expression, S, O and U are predefined respectivly to the "
 "current relation'subject, object and to the request user. "
 msgstr ""
-"restriction part of a rql query. For entity rql expression, X and U are "
-"predefined respectivly to the current object and to the request user. For "
-"relation rql expression, S, O and U are predefined respectivly to the "
-"current relation'subject, object and to the request user. "
+"Parte restrictiva de una consulta RQL. En una expresión ligada a una "
+"entidad, X y U son respectivamente asignadas a la Entidad y el Usuario en "
+"curso.En una expresión ligada a una relación, S, O y U son respectivamente "
+"asignados al Sujeto/Objeto de la relación y al Usuario actual."
 
 msgid "revert changes"
-msgstr "Revertir cambios"
+msgstr "Anular modificación"
 
 msgid "right"
 msgstr "Derecha"
 
 msgid "rql expressions"
-msgstr "expresiones rql"
+msgstr "Expresiones RQL"
 
 msgid "rss"
 msgstr "RSS"
 
 msgid "same_as"
-msgstr ""
+msgstr "Idéntico a"
 
 msgid "sample format"
-msgstr "ejemplo"
+msgstr "Ejemplo"
 
 msgid "saturday"
-msgstr "sabado"
+msgstr "Sábado"
 
 msgid "schema entities"
-msgstr "entidades del esquema"
+msgstr "Entidades del esquema"
 
 msgid "schema's permissions definitions"
-msgstr "definiciones de permisos del esquema"
+msgstr "Definiciones de permisos del esquema"
 
 msgid "schema-diagram"
-msgstr ""
+msgstr "Gráfica"
 
 msgid "schema-entity-types"
-msgstr ""
+msgstr "Entidades"
 
 msgid "schema-relation-types"
-msgstr ""
+msgstr "Relaciones"
 
 msgid "schema-security"
-msgstr ""
+msgstr "Seguridad"
 
 msgid "search"
-msgstr "buscar"
+msgstr "Buscar"
 
 msgid "search for association"
-msgstr "buscar por asociación"
+msgstr "Búsqueda por asociación"
 
 msgid "searching for"
-msgstr "buscando "
+msgstr "Buscando"
 
 msgid "secondary"
-msgstr "secundario"
+msgstr "Secundaria"
 
 msgid "security"
-msgstr "seguridad"
+msgstr "Seguridad"
 
 msgid "see them all"
 msgstr "Ver todos"
 
 msgid "see_also"
-msgstr "Ver tambíen"
+msgstr "Ver además"
 
 msgid "select"
 msgstr "Seleccionar"
 
 msgid "select a"
-msgstr "seleccione un"
+msgstr "Seleccione un"
 
 msgid "select a key first"
-msgstr "seleccione una clave"
+msgstr "Seleccione una clave"
 
 msgid "select a relation"
-msgstr "seleccione una relación"
+msgstr "Seleccione una relación"
 
 msgid "select this entity"
-msgstr "seleccionar esta entidad"
+msgstr "Seleccionar esta entidad"
 
 msgid "selected"
-msgstr "seleccionado"
+msgstr "Seleccionado"
 
 msgid "semantic description of this attribute"
-msgstr "descripción semantica de este atributo"
+msgstr "Descripción semántica de este atributo"
 
 msgid "semantic description of this entity type"
-msgstr "descripción semantica de este tipo de entidad"
+msgstr "Descripción semántica de este tipo de entidad"
 
 msgid "semantic description of this relation"
-msgstr "descripción semantica de esta relación"
+msgstr "Descripción semántica de esta relación"
 
 msgid "semantic description of this relation type"
-msgstr "descripción semantica de este tipo de relación"
+msgstr "Descripción semántica de este tipo de relación"
 
 msgid "semantic description of this state"
-msgstr "descripción semantica de este estado"
+msgstr "Descripción semántica de este estado"
 
 msgid "semantic description of this transition"
-msgstr "descripcion semantica de esta transición"
+msgstr "Descripcion semántica de esta transición"
 
 msgid "semantic description of this workflow"
-msgstr ""
+msgstr "Descripcion semántica de este Workflow"
 
 msgid "send email"
-msgstr "enviar email"
+msgstr "Enviar email"
 
 msgid "september"
-msgstr "septiembre"
+msgstr "Septiembre"
 
 msgid "server information"
-msgstr "server information"
+msgstr "Información del servidor"
 
 msgid ""
 "should html fields being edited using fckeditor (a HTML WYSIWYG editor).  "
 "You should also select text/html as default text format to actually get "
 "fckeditor."
 msgstr ""
-"indique si los campos deberan ser editados usando fckeditor (un\n"
-"editor HTML WYSIWYG). Debera tambien elegir text/html\n"
-"como formato de texto por default para poder utilizar fckeditor."
+"Indica si los campos de tipo texto deberán ser editados usando fckeditor "
+"(un\n"
+"editor HTML WYSIWYG). Deberá también elegir text/html\n"
+"como formato de texto por defecto para poder utilizar fckeditor."
 
 #, python-format
 msgid "show %s results"
-msgstr "mostrar %s resultados"
+msgstr "Mostrar %s resultados"
 
 msgid "show advanced fields"
-msgstr "mostrar campos avanzados"
+msgstr "Mostrar campos avanzados"
 
 msgid "show filter form"
-msgstr "afficher le filtre"
+msgstr "Mostrar el Filtro"
 
 msgid "sioc"
-msgstr ""
+msgstr "SIOC"
 
 msgid "site configuration"
-msgstr "configuracion del sitio"
+msgstr "Configuración Sistema"
 
 msgid "site documentation"
-msgstr "documentacion del sitio"
+msgstr "Documentación Sistema"
 
 msgid "site schema"
-msgstr "esquema del sitio"
+msgstr "Esquema del Sistema"
 
 msgid "site title"
-msgstr "titulo del sitio"
+msgstr "Nombre del Sistema"
 
 msgid "site-wide property can't be set for user"
-msgstr ""
-"una propiedad especifica para el sitio no puede establecerse para el usuario"
-
-msgid "some errors occured:"
-msgstr ""
+msgstr "Una propiedad específica al Sistema no puede ser propia al usuario"
+
+msgid "some errors occurred:"
+msgstr "Algunos errores encontrados :"
 
 msgid "some later transaction(s) touch entity, undo them first"
 msgstr ""
+"Las transacciones más recientes modificaron esta entidad, anúlelas primero"
 
 msgid "sorry, the server is unable to handle this query"
-msgstr "lo sentimos, el servidor no puede manejar esta consulta"
+msgstr "Lo sentimos, el servidor no puede manejar esta consulta"
 
 msgid "sparql xml"
-msgstr ""
+msgstr "XML Sparql"
 
 msgid "special transition allowing to go through a sub-workflow"
-msgstr ""
+msgstr "Transición especial que permite ir en un Sub-Workflow"
 
 msgid "specializes"
-msgstr "derivado de"
+msgstr "Deriva de"
 
 msgctxt "CWEType"
 msgid "specializes"
-msgstr ""
+msgstr "Especializa"
+
+msgid "specializes_object"
+msgstr "Especializado por"
 
 msgctxt "CWEType"
 msgid "specializes_object"
-msgstr ""
-
-msgid "specializes_object"
-msgstr "objeto_derivado"
+msgstr "Especializado por"
 
 msgid "startup views"
-msgstr "vistas de inicio"
+msgstr "Vistas de inicio"
 
 msgid "state"
-msgstr "estado"
+msgstr "Estado"
 
 msgid "state and transition don't belong the the same workflow"
-msgstr ""
+msgstr "El Estado y la Transición no pertenecen al mismo Workflow"
 
 msgid "state doesn't apply to this entity's type"
-msgstr ""
+msgstr "Este Estado no aplica a este tipo de Entidad"
 
 msgid "state doesn't belong to entity's current workflow"
-msgstr ""
+msgstr "El Estado no pertenece al Workflow actual de la Entidad"
 
 msgid "state doesn't belong to entity's workflow"
-msgstr ""
+msgstr "El Estado no pertenece al Workflow de la Entidad"
 
 msgid ""
 "state doesn't belong to entity's workflow. You may want to set a custom "
 "workflow for this entity first."
 msgstr ""
+"El Estado no pertenece al Workflow Actual de la Entidad. Usted deseaquizás "
+"especificar que esta entidad debe utilizar este Workflow"
 
 msgid "state doesn't belong to this workflow"
-msgstr ""
+msgstr "El Estado no pertenece a este Workflow"
 
 msgid "state_of"
-msgstr "estado_de"
+msgstr "Estado de"
 
 msgctxt "State"
 msgid "state_of"
-msgstr ""
+msgstr "Estado de"
+
+msgid "state_of_object"
+msgstr "Tiene por Estado"
 
 msgctxt "Workflow"
 msgid "state_of_object"
-msgstr ""
-
-msgid "state_of_object"
-msgstr "objeto_estado_de"
+msgstr "Tiene por Estado"
 
 msgid "status change"
-msgstr "cambio de estatus"
+msgstr "Cambio de Estatus"
 
 msgid "status changed"
-msgstr "estatus cambiado"
+msgstr "Estatus cambiado"
 
 #, python-format
 msgid "status will change from %(st1)s to %(st2)s"
-msgstr "estatus cambiara de %(st1)s a %(st2)s"
+msgstr "El estatus cambiará de %(st1)s a %(st2)s"
 
 msgid "subject"
-msgstr "sujeto"
+msgstr "Sujeto"
 
 msgid "subject type"
-msgstr ""
+msgstr "Tipo del sujeto"
 
 msgid "subject/object cardinality"
-msgstr "cardinalidad sujeto/objeto"
+msgstr "Cardinalidad Sujeto/Objeto"
 
 msgid "subworkflow"
-msgstr ""
+msgstr "Sub-Workflow"
 
 msgctxt "WorkflowTransition"
 msgid "subworkflow"
-msgstr ""
+msgstr "Sub-Workflow"
 
 msgid ""
 "subworkflow isn't a workflow for the same types as the transition's workflow"
 msgstr ""
+"Le Sub-Workflow no se aplica a los mismos tipos que el Workflow de esta "
+"transición"
 
 msgid "subworkflow state"
-msgstr ""
+msgstr "Estado de Sub-Workflow"
 
 msgid "subworkflow_exit"
-msgstr ""
+msgstr "Salida del Sub-Workflow"
 
 msgctxt "WorkflowTransition"
 msgid "subworkflow_exit"
-msgstr ""
+msgstr "Salida del Sub-Workflow"
+
+msgid "subworkflow_exit_object"
+msgstr "Salida Sub-Workflow de"
 
 msgctxt "SubWorkflowExitPoint"
 msgid "subworkflow_exit_object"
-msgstr ""
-
-msgid "subworkflow_exit_object"
-msgstr ""
+msgstr "Salida Sub-Workflow de"
+
+msgid "subworkflow_object"
+msgstr "Sub-Workflow de"
 
 msgctxt "Workflow"
 msgid "subworkflow_object"
-msgstr ""
-
-msgid "subworkflow_object"
-msgstr ""
+msgstr "Sub-Workflow de"
 
 msgid "subworkflow_state"
-msgstr ""
+msgstr "Estado de Sub-Workflow"
 
 msgctxt "SubWorkflowExitPoint"
 msgid "subworkflow_state"
-msgstr ""
+msgstr "Estado de Sub-Workflow"
+
+msgid "subworkflow_state_object"
+msgstr "Estado de Salida de"
 
 msgctxt "State"
 msgid "subworkflow_state_object"
-msgstr ""
-
-msgid "subworkflow_state_object"
-msgstr ""
+msgstr "Estado de Salida de"
 
 msgid "sunday"
-msgstr "domingo"
+msgstr "Domingo"
 
 msgid "surname"
-msgstr "apellido"
+msgstr "Apellido"
 
 msgctxt "CWUser"
 msgid "surname"
-msgstr ""
+msgstr "Apellido"
 
 msgid "symmetric"
-msgstr "simetrico"
+msgstr "Simétrico"
 
 msgctxt "CWRType"
 msgid "symmetric"
-msgstr ""
+msgstr "Simétrico"
 
 msgid "system entities"
-msgstr "entidades de sistema"
+msgstr "Entidades del sistema"
 
 msgid "table"
-msgstr "tabla"
+msgstr "Tabla"
 
 msgid "tablefilter"
-msgstr "filtro de tabla"
+msgstr "Tablero de Filtrado"
 
 msgid "task progression"
-msgstr "progreso de la tarea"
+msgstr "Progreso de la Acción"
 
 msgid "text"
-msgstr "text"
+msgstr "Texto"
 
 msgid "text/cubicweb-page-template"
-msgstr "text/cubicweb-page-template"
+msgstr "Usar Page Templates"
 
 msgid "text/html"
-msgstr "html"
+msgstr "Usar HTML"
 
 msgid "text/plain"
-msgstr "text/plain"
+msgstr "Usar Texto simple"
 
 msgid "text/rest"
-msgstr "text/rest"
+msgstr "Texto en REST"
 
 msgid "the URI of the object"
-msgstr ""
+msgstr "El URI del Objeto"
 
 msgid "the prefered email"
-msgstr "dirección principal de email"
+msgstr "Dirección principal de email"
 
 #, python-format
 msgid "the value \"%s\" is already used, use another one"
-msgstr "el valor \"%s\" ya esta en uso, favor de utilizar otro"
+msgstr "El valor \"%s\" ya esta en uso, favor de utilizar otro"
 
 msgid "this action is not reversible!"
-msgstr "esta acción es irreversible!."
+msgstr "Esta acción es irreversible!."
 
 msgid "this entity is currently owned by"
-msgstr "esta entidad es propiedad de"
+msgstr "Esta Entidad es propiedad de"
 
 msgid "this resource does not exist"
-msgstr "este recurso no existe"
+msgstr "Este recurso no existe"
 
 msgid "thursday"
-msgstr "jueves"
+msgstr "Jueves"
 
 msgid "timeline"
-msgstr ""
+msgstr "Escala de Tiempo"
 
 msgid "timestamp"
-msgstr "fecha"
+msgstr "Fecha"
 
 msgctxt "CWCache"
 msgid "timestamp"
-msgstr ""
+msgstr "Válido desde"
 
 msgid "timestamp of the latest source synchronization."
-msgstr "fecha de la ultima sincronización de la fuente."
+msgstr "Fecha de la última sincronización de la fuente."
 
 msgid "timetable"
-msgstr "tabla de tiempos"
+msgstr "Tablero de tiempos"
 
 msgid "title"
-msgstr "titulo"
+msgstr "Nombre"
 
 msgctxt "Bookmark"
 msgid "title"
-msgstr ""
+msgstr "Nombre"
 
 msgid "to"
 msgstr "a"
@@ -3592,434 +3729,444 @@
 msgstr "a %(date)s"
 
 msgid "to associate with"
-msgstr "a asociar con"
+msgstr "Para asociar con"
 
 msgid "to_entity"
-msgstr "hacia entidad"
+msgstr "Hacia la entidad"
 
 msgctxt "CWAttribute"
 msgid "to_entity"
-msgstr ""
+msgstr "Por la entidad"
 
 msgctxt "CWRelation"
 msgid "to_entity"
-msgstr ""
+msgstr "Por la entidad"
+
+msgid "to_entity_object"
+msgstr "Objeto de la Relación"
 
 msgctxt "CWEType"
 msgid "to_entity_object"
-msgstr ""
-
-msgid "to_entity_object"
-msgstr "hacia entidad objeto"
+msgstr "Objeto de la Relación"
 
 msgid "to_interval_end"
-msgstr ""
+msgstr "a"
 
 msgid "to_state"
-msgstr "hacia el estado"
+msgstr "Hacia el Estado"
 
 msgctxt "TrInfo"
 msgid "to_state"
-msgstr ""
+msgstr "Hacia el Estado"
+
+msgid "to_state_object"
+msgstr "Transición hacia este Estado"
 
 msgctxt "State"
 msgid "to_state_object"
-msgstr ""
-
-msgid "to_state_object"
-msgstr "hacia objeto estado"
+msgstr "Transición hacia este Estado"
 
 msgid "todo_by"
-msgstr "a hacer por"
+msgstr "Asignada a"
 
 msgid "toggle check boxes"
-msgstr "cambiar valor"
+msgstr "Cambiar valor"
 
 msgid "transaction undoed"
-msgstr ""
+msgstr "Transacciones Anuladas"
 
 #, python-format
 msgid "transition %(tr)s isn't allowed from %(st)s"
-msgstr ""
+msgstr "La transición %(tr)s no esta permitida desde el Estado %(st)s"
 
 msgid "transition doesn't belong to entity's workflow"
-msgstr ""
+msgstr "La transición no pertenece al Workflow de la Entidad"
 
 msgid "transition isn't allowed"
-msgstr ""
+msgstr "La transición no esta permitida"
 
 msgid "transition may not be fired"
-msgstr ""
+msgstr "La transición no puede ser lanzada"
 
 msgid "transition_of"
-msgstr "transicion de"
+msgstr "Transición de"
 
 msgctxt "BaseTransition"
 msgid "transition_of"
-msgstr ""
+msgstr "Transición de"
 
 msgctxt "Transition"
 msgid "transition_of"
-msgstr ""
+msgstr "Transición de"
 
 msgctxt "WorkflowTransition"
 msgid "transition_of"
-msgstr ""
+msgstr "Transición de"
+
+msgid "transition_of_object"
+msgstr "Utiliza las transiciones"
 
 msgctxt "Workflow"
 msgid "transition_of_object"
-msgstr ""
-
-msgid "transition_of_object"
-msgstr "objeto de transición"
+msgstr "Utiliza las transiciones"
 
 msgid "tree view"
-msgstr ""
+msgstr "Vista Jerárquica"
 
 msgid "tuesday"
-msgstr "martes"
+msgstr "Martes"
 
 msgid "type"
-msgstr "type"
+msgstr "Tipo"
 
 msgctxt "BaseTransition"
 msgid "type"
-msgstr ""
+msgstr "Tipo"
 
 msgctxt "Transition"
 msgid "type"
-msgstr ""
+msgstr "Tipo"
 
 msgctxt "WorkflowTransition"
 msgid "type"
-msgstr ""
+msgstr "Tipo"
 
 msgid "type here a sparql query"
-msgstr ""
+msgstr "Escriba aquí su consulta en Sparql"
 
 msgid "ui"
-msgstr "interfaz de usuario"
+msgstr "Interfaz Genérica"
 
 msgid "ui.date-format"
-msgstr ""
+msgstr "Formato de Fecha"
 
 msgid "ui.datetime-format"
-msgstr ""
+msgstr "Formato de Fecha y Hora"
 
 msgid "ui.default-text-format"
-msgstr ""
+msgstr "Formato de texto"
 
 msgid "ui.encoding"
-msgstr ""
+msgstr "Codificación"
 
 msgid "ui.fckeditor"
-msgstr ""
+msgstr "Editor de texto FCK"
 
 msgid "ui.float-format"
-msgstr ""
+msgstr "Números flotantes"
 
 msgid "ui.language"
-msgstr ""
+msgstr "Lenguaje"
 
 msgid "ui.main-template"
-msgstr ""
+msgstr "Plantilla Principal"
 
 msgid "ui.site-title"
-msgstr ""
+msgstr "Nombre del Sistema"
 
 msgid "ui.time-format"
-msgstr ""
+msgstr "Formato de hora"
 
 msgid "unable to check captcha, please try again"
-msgstr ""
+msgstr "Imposible de verificar el Captcha, inténtelo otra vez"
 
 msgid "unaccessible"
-msgstr "inaccesible"
+msgstr "Inaccesible"
 
 msgid "unauthorized value"
-msgstr "valor no permitido"
+msgstr "Valor no permitido"
 
 msgid "undo"
-msgstr ""
+msgstr "Anular"
 
 msgid "unique identifier used to connect to the application"
-msgstr "identificador unico utilizado para conectar a la aplicación"
+msgstr "Identificador único utilizado para conectarse al Sistema"
 
 msgid "unknown external entity"
-msgstr "entidad externa desconocida"
+msgstr "Entidad externa desconocida"
 
 msgid "unknown property key"
-msgstr "propiedad desconocida"
+msgstr "Clave de Propiedad desconocida"
 
 msgid "unknown vocabulary:"
-msgstr ""
+msgstr "Vocabulario desconocido: "
 
 msgid "up"
-msgstr "arriba"
+msgstr "Arriba"
 
 msgid "upassword"
-msgstr "clave de acceso"
+msgstr "Contraseña"
 
 msgctxt "CWUser"
 msgid "upassword"
-msgstr ""
+msgstr "Contraseña"
 
 msgid "update"
-msgstr "modificación"
-
-msgid "update_perm"
-msgstr "modificación"
+msgstr "Modificación"
 
 msgid "update_permission"
-msgstr "Permiso de modificación"
+msgstr "Puede ser modificado por"
+
+msgctxt "CWAttribute"
+msgid "update_permission"
+msgstr "Puede ser modificado por"
 
 msgctxt "CWEType"
 msgid "update_permission"
-msgstr ""
-
-msgctxt "CWAttribute"
-msgid "update_permission"
-msgstr ""
+msgstr "Puede ser modificado por"
+
+msgid "update_permission_object"
+msgstr "Tiene permiso de modificar"
 
 msgctxt "CWGroup"
 msgid "update_permission_object"
-msgstr ""
+msgstr "Puede modificar"
 
 msgctxt "RQLExpression"
 msgid "update_permission_object"
-msgstr ""
-
-msgid "update_permission_object"
-msgstr "objeto de autorización de modificaciones"
+msgstr "Puede modificar"
+
+msgid "update_relation"
+msgstr "Modificar"
 
 msgid "updated"
-msgstr ""
+msgstr "Actualizado"
 
 #, python-format
 msgid "updated %(etype)s #%(eid)s (%(title)s)"
 msgstr "actualización de la entidad %(etype)s #%(eid)s (%(title)s)"
 
 msgid "uri"
-msgstr ""
+msgstr "URI"
 
 msgctxt "ExternalUri"
 msgid "uri"
-msgstr ""
+msgstr "URI"
 
 msgid "use template languages"
-msgstr "utilizar plantillas de lenguaje"
+msgstr "Utilizar plantillas de lenguaje"
 
 msgid ""
 "use to define a transition from one or multiple states to a destination "
 "states in workflow's definitions. Transition without destination state will "
 "go back to the state from which we arrived to the current state."
 msgstr ""
+"Se utiliza en una definición de procesos para agregar una transición desde "
+"uno o varios estados hacia un estado destino. Una transición sin Estado "
+"destino regresará al Estado anterior del Estado actual"
 
 msgid "use_email"
-msgstr "correo electrónico"
+msgstr "Correo electrónico"
 
 msgctxt "CWUser"
 msgid "use_email"
-msgstr ""
+msgstr "Usa el Correo Electrónico"
+
+msgid "use_email_object"
+msgstr "Email utilizado por"
 
 msgctxt "EmailAddress"
 msgid "use_email_object"
-msgstr ""
-
-msgid "use_email_object"
-msgstr "objeto email utilizado"
+msgstr "Utilizado por"
 
 msgid "use_template_format"
-msgstr "utilización del formato 'cubicweb template'"
+msgstr "Utilización del formato 'cubicweb template'"
 
 msgid ""
 "used for cubicweb configuration. Once a property has been created you can't "
 "change the key."
 msgstr ""
-"utilizado para la configuración de cubicweb. Una vez que la propiedad ha "
-"sido creada no puede cambiar la llave"
+"Se utiliza para la configuración de CubicWeb. Una vez que la propiedad ha "
+"sido creada no puede cambiar la clave"
 
 msgid ""
 "used to associate simple states to an entity type and/or to define workflows"
 msgstr ""
-"utilizado para asociar estados simples a un tipo de entidad y/o para definir "
-"workflows"
+"Se utiliza para asociar estados simples a un tipo de entidad y/o para "
+"definir Workflows"
 
 msgid "used to grant a permission to a group"
-msgstr "utilizado para otorgar permisos a un grupo"
+msgstr "Se utiliza para otorgar permisos a un grupo"
 
 msgid "user"
-msgstr ""
+msgstr "Usuario"
 
 #, python-format
 msgid ""
 "user %s has made the following change(s):\n"
 "\n"
 msgstr ""
-"el usuario %s ha efectuado los siguentes cambios:\n"
+"El usuario %s ha efectuado los siguentes cambios:\n"
 "\n"
 
 msgid "user interface encoding"
-msgstr "codificación de la interfaz de usuario"
+msgstr "Encoding de la interfaz de usuario"
 
 msgid "user preferences"
-msgstr "preferencias del usuario"
+msgstr "Preferencias"
 
 msgid "users"
-msgstr "usuarios"
+msgstr "Usuarios"
 
 msgid "users using this bookmark"
-msgstr "usuarios en este favorito"
+msgstr "Usuarios utilizando este Favorito"
 
 msgid "validate modifications on selected items"
-msgstr "valida modificaciones sobre elementos seleccionados"
+msgstr "Valida modificaciones sobre elementos seleccionados"
 
 msgid "validating..."
-msgstr "validando ..."
+msgstr "Validando ..."
 
 msgid "value"
-msgstr "valor"
+msgstr "Valor"
 
 msgctxt "CWConstraint"
 msgid "value"
-msgstr ""
+msgstr "Valor"
 
 msgctxt "CWProperty"
 msgid "value"
-msgstr ""
+msgstr "Vampr"
 
 msgid "value associated to this key is not editable manually"
-msgstr "el valor asociado a este elemento no es editable manualmente"
+msgstr "El valor asociado a este elemento no es editable manualmente"
 
 #, python-format
 msgid "value must be %(op)s %(boundary)s"
-msgstr ""
+msgstr "El valor debe ser %(op)s %(boundary)s"
 
 #, python-format
 msgid "value must be <= %(boundary)s"
-msgstr ""
+msgstr "El valor debe ser <= %(boundary)s"
 
 #, python-format
 msgid "value must be >= %(boundary)s"
-msgstr ""
+msgstr "El valor debe ser >= %(boundary)s"
 
 #, python-format
 msgid "value should have maximum size of %s"
-msgstr ""
+msgstr "El valor no debe exceder de %s"
 
 #, python-format
 msgid "value should have minimum size of %s"
-msgstr ""
+msgstr "El valor no puede ser menor a %s"
 
 msgid "vcard"
 msgstr "vcard"
 
 msgid "versions configuration"
-msgstr ""
+msgstr "Configuración de Versión"
 
 msgid "view"
-msgstr "ver"
+msgstr "Ver"
 
 msgid "view all"
-msgstr "ver todos"
+msgstr "Ver todos"
 
 msgid "view detail for this entity"
-msgstr "ver detalle de esta entidad"
+msgstr "Ver a detalle esta entidad"
 
 msgid "view history"
-msgstr ""
+msgstr "Ver histórico"
 
 msgid "view identifier"
-msgstr ""
+msgstr "Identificador"
 
 msgid "view title"
-msgstr ""
+msgstr "Nombre"
 
 msgid "view workflow"
-msgstr "ver workflow"
+msgstr "Ver Workflow"
 
 msgid "view_index"
+msgstr "Inicio"
+
+#, python-format
+msgid "violates unique_together constraints (%s)"
 msgstr ""
 
 msgid "visible"
-msgstr "visible"
+msgstr "Visible"
 
 msgid "we are not yet ready to handle this query"
-msgstr ""
+msgstr "Aún no podemos manejar este tipo de consulta Sparql"
 
 msgid "wednesday"
-msgstr "miercoles"
+msgstr "Miércoles"
 
 msgid "week"
 msgstr "sem."
 
 #, python-format
 msgid "welcome %s !"
-msgstr "bienvenido %s !"
+msgstr "¡ Bienvenido %s  !"
 
 msgid "wf_info_for"
-msgstr "historial de"
+msgstr "Histórico de"
 
 msgid "wf_info_for_object"
-msgstr "historial de transiciones"
+msgstr "Histórico de transiciones"
 
 msgid "wf_tab_info"
-msgstr ""
+msgstr "Descripción"
 
 msgid "wfgraph"
-msgstr ""
+msgstr "Gráfica del Workflow"
 
 msgid ""
 "when multiple addresses are equivalent (such as python-projects@logilab.org "
 "and python-projects@lists.logilab.org), set this to indicate which is the "
 "preferred form."
 msgstr ""
+"Cuando varias direcciones email son equivalentes (como python-"
+"projects@logilab.org y python-projects@lists.logilab.org), aquí se indica "
+"cual es la forma preferida."
 
 msgid "workflow"
-msgstr ""
+msgstr "Workflow"
 
 msgid "workflow already have a state of that name"
-msgstr ""
+msgstr "El Workflow ya tiene un Estado con ese nombre"
 
 msgid "workflow already have a transition of that name"
-msgstr ""
+msgstr "El Workflow ya tiene una transición con ese nombre"
 
 #, python-format
 msgid "workflow changed to \"%s\""
-msgstr ""
+msgstr "Workflow cambiado a \"%s\""
 
 msgid "workflow has no initial state"
-msgstr ""
+msgstr "El Workflow no posee Estado Inicial"
 
 msgid "workflow history item"
-msgstr ""
+msgstr "Elemento histórico del Workflow"
 
 msgid "workflow isn't a workflow for this type"
-msgstr ""
+msgstr "El Workflow no se aplica a este Tipo de Entidad"
 
 msgid "workflow to which this state belongs"
-msgstr ""
+msgstr "Workflow al cual pertenece este estado"
 
 msgid "workflow to which this transition belongs"
-msgstr ""
+msgstr "Workflow al cual pertenece esta transición"
 
 msgid "workflow_of"
-msgstr ""
+msgstr "Workflow de"
 
 msgctxt "Workflow"
 msgid "workflow_of"
-msgstr ""
+msgstr "Workflow de"
+
+msgid "workflow_of_object"
+msgstr "Utiliza el Workflow"
 
 msgctxt "CWEType"
 msgid "workflow_of_object"
-msgstr ""
-
-msgid "workflow_of_object"
-msgstr ""
+msgstr "Utiliza el Workflow"
 
 #, python-format
 msgid "wrong query parameter line %s"
-msgstr ""
+msgstr "Parámetro erróneo de consulta línea %s"
 
 msgid "xbel"
 msgstr "xbel"
@@ -4028,16 +4175,34 @@
 msgstr "xml"
 
 msgid "xml export"
-msgstr ""
+msgstr "Exportar XML"
 
 msgid "yes"
-msgstr "si"
+msgstr "Sí"
 
 msgid "you have been logged out"
-msgstr "ha terminado la sesion"
+msgstr "Ha terminado la sesión"
 
 msgid "you should probably delete that property"
-msgstr "deberia probablamente suprimir esta propriedad"
-
-#~ msgid "schema-image"
-#~ msgstr "esquema imagen"
+msgstr "Debería probablamente suprimir esta propriedad"
+
+#~ msgid "add_perm"
+#~ msgstr "Agregado"
+
+#~ msgid "delete_perm"
+#~ msgstr "Eliminar"
+
+#~ msgid "edition"
+#~ msgstr "Edición"
+
+#~ msgid "graphical workflow for %s"
+#~ msgstr "Gráfica del workflow por %s"
+
+#~ msgid "personnal informations"
+#~ msgstr "Información personal"
+
+#~ msgid "read_perm"
+#~ msgstr "Lectura"
+
+#~ msgid "update_perm"
+#~ msgstr "Permiso de Modificar"
--- a/i18n/fr.po	Tue Jul 27 12:36:03 2010 +0200
+++ b/i18n/fr.po	Wed Nov 03 16:38:28 2010 +0100
@@ -4,9 +4,10 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2010-05-16 18:59+0200\n"
+"PO-Revision-Date: 2010-09-15 15:12+0200\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -36,7 +37,7 @@
 msgstr "  de l'état %(fromstate)s vers l'état %(tostate)s\n"
 
 msgid " :"
-msgstr ""
+msgstr " :"
 
 #, python-format
 msgid "%(attr)s set to %(newvalue)s"
@@ -188,6 +189,9 @@
 "<div>Ce schéma du modèle de données <em>exclue</em> les méta-données, mais "
 "vous pouvez afficher un <a href=\"%s\">schéma complet</a>.</div>"
 
+msgid "<not specified>"
+msgstr "<non spécifié>"
+
 msgid "?*"
 msgstr "0..1 0..n"
 
@@ -242,6 +246,9 @@
 msgid "Browse by category"
 msgstr "Naviguer par catégorie"
 
+msgid "Browse by entity type"
+msgstr "Naviguer par type d'entité"
+
 msgid "Bytes"
 msgstr "Donnée binaires"
 
@@ -320,6 +327,12 @@
 msgid "CWRelation_plural"
 msgstr "Relations"
 
+msgid "CWUniqueTogetherConstraint"
+msgstr "Contrainte unique_together"
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr "Contraintes unique_together"
+
 msgid "CWUser"
 msgstr "Utilisateur"
 
@@ -374,6 +387,10 @@
 "Ne peut annuler la création de l'entité %(eid)s de type %(etype)s, ce type "
 "n'existe plus"
 
+#, python-format
+msgid "Data connection graph for %s"
+msgstr "Graphique de connection des données pour %s"
+
 msgid "Date"
 msgstr "Date"
 
@@ -398,10 +415,10 @@
 msgid "Download schema as OWL"
 msgstr "Télécharger le schéma au format OWL"
 
-msgctxt "inlined:CWUser.use_email.subject"
 msgid "EmailAddress"
 msgstr "Adresse électronique"
 
+msgctxt "inlined:CWUser.use_email.subject"
 msgid "EmailAddress"
 msgstr "Adresse électronique"
 
@@ -438,6 +455,9 @@
 msgid "Garbage collection information"
 msgstr "Information sur le ramasse-miette"
 
+msgid "Got rhythm?"
+msgstr "T'as le rythme ?"
+
 msgid "Help"
 msgstr "Aide"
 
@@ -504,6 +524,9 @@
 msgid "New CWRelation"
 msgstr "Nouvelle définition de relation non finale"
 
+msgid "New CWUniqueTogetherConstraint"
+msgstr "Nouvelle contrainte unique_together"
+
 msgid "New CWUser"
 msgstr "Nouvel utilisateur"
 
@@ -534,17 +557,21 @@
 msgid "New WorkflowTransition"
 msgstr "Nouvelle transition workflow"
 
+#, python-format
+msgid "No account? Try public access at %s"
+msgstr "Pas de compte ? Accédez au site public : %s"
+
 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é"
+msgstr "Liste non exhaustive des vues s'appliquant à ce type d'entité"
 
 msgid "OR"
 msgstr "OU"
 
-msgid "Parent classes:"
-msgstr "Classes parentes :"
+msgid "Parent class:"
+msgstr "Classe parente"
 
 msgid "Password"
 msgstr "Mot de passe"
@@ -643,9 +670,6 @@
 msgid "Submit bug report by mail"
 msgstr "Soumettre ce rapport par email"
 
-msgid "The repository holds the following entities"
-msgstr "Le dépot contient les entités suivantes"
-
 #, python-format
 msgid "The view %s can not be applied to this query"
 msgstr "La vue %s ne peut être appliquée à cette requête"
@@ -693,6 +717,9 @@
 msgid "This CWRelation"
 msgstr "Cette définition de relation"
 
+msgid "This CWUniqueTogetherConstraint"
+msgstr "Cette contrainte unique_together"
+
 msgid "This CWUser"
 msgstr "Cet utilisateur"
 
@@ -724,7 +751,7 @@
 msgstr "Cette transition workflow"
 
 msgid "This entity type permissions:"
-msgstr "Permissions pour ce type d'endité"
+msgstr "Permissions pour ce type d'entité"
 
 msgid "Time"
 msgstr "Heure"
@@ -748,7 +775,7 @@
 msgstr "contrainte d'unicité"
 
 msgid "Unreachable objects"
-msgstr "Objets inacessible"
+msgstr "Objets inaccessibles"
 
 msgid "Used by:"
 msgstr "Utilisé par :"
@@ -908,6 +935,9 @@
 msgid "add CWRelation relation_type CWRType object"
 msgstr "définition de relation"
 
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr "contrainte unique_together"
+
 msgid "add CWUser in_group CWGroup object"
 msgstr "utilisateur"
 
@@ -966,9 +996,6 @@
 msgid "add a new permission"
 msgstr "ajouter une permission"
 
-msgid "add_perm"
-msgstr "ajout"
-
 # subject and object forms for each relation type
 # (no object form for final relation types)
 msgid "add_permission"
@@ -984,6 +1011,9 @@
 msgid "add_permission"
 msgstr "permission d'ajout"
 
+msgid "add_permission_object"
+msgstr "a la permission d'ajouter"
+
 msgctxt "CWGroup"
 msgid "add_permission_object"
 msgstr "a la permission d'ajouter"
@@ -992,8 +1022,8 @@
 msgid "add_permission_object"
 msgstr "a la permission d'ajouter"
 
-msgid "add_permission_object"
-msgstr "a la permission d'ajouter"
+msgid "add_relation"
+msgstr "ajouter"
 
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
@@ -1001,11 +1031,11 @@
 
 #, python-format
 msgid ""
-"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%"
-"(eidto)s"
+"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #"
+"%(eidto)s"
 msgstr ""
-"la relation %(rtype)s de %(frometype)s #%(eidfrom)s vers %(toetype)s #%"
-"(eidto)s a été ajoutée"
+"la relation %(rtype)s de %(frometype)s #%(eidfrom)s vers %(toetype)s #"
+"%(eidto)s a été ajoutée"
 
 msgid "addrelated"
 msgstr "ajouter"
@@ -1037,6 +1067,9 @@
 msgid "allowed_transition"
 msgstr "transitions autorisées"
 
+msgid "allowed_transition_object"
+msgstr "états en entrée"
+
 msgctxt "BaseTransition"
 msgid "allowed_transition_object"
 msgstr "transition autorisée de"
@@ -1049,9 +1082,6 @@
 msgid "allowed_transition_object"
 msgstr "transition autorisée de"
 
-msgid "allowed_transition_object"
-msgstr "états en entrée"
-
 msgid "am/pm calendar (month)"
 msgstr "calendrier am/pm (mois)"
 
@@ -1067,13 +1097,13 @@
 msgid "an electronic mail address associated to a short alias"
 msgstr "une adresse électronique associée à un alias"
 
-msgid "an error occured"
+msgid "an error occurred"
 msgstr "une erreur est survenue"
 
-msgid "an error occured while processing your request"
+msgid "an error occurred while processing your request"
 msgstr "une erreur est survenue pendant le traitement de votre requête"
 
-msgid "an error occured, the request cannot be fulfilled"
+msgid "an error occurred, the request cannot be fulfilled"
 msgstr "une erreur est survenue, la requête ne peut être complétée"
 
 msgid "an integer is expected"
@@ -1137,13 +1167,13 @@
 msgid "bookmarked_by"
 msgstr "utilisé par"
 
+msgid "bookmarked_by_object"
+msgstr "utilise le(s) signet(s)"
+
 msgctxt "CWUser"
 msgid "bookmarked_by_object"
 msgstr "utilise le(s) signet(s)"
 
-msgid "bookmarked_by_object"
-msgstr "a pour signets"
-
 msgid "bookmarks"
 msgstr "signets"
 
@@ -1231,6 +1261,9 @@
 msgid "by_transition"
 msgstr "transition"
 
+msgid "by_transition_object"
+msgstr "changement d'états"
+
 msgctxt "BaseTransition"
 msgid "by_transition_object"
 msgstr "a pour information"
@@ -1243,9 +1276,6 @@
 msgid "by_transition_object"
 msgstr "a pour information"
 
-msgid "by_transition_object"
-msgstr "changement d'états"
-
 msgid "calendar"
 msgstr "afficher un calendrier"
 
@@ -1330,6 +1360,12 @@
 msgid "click on the box to cancel the deletion"
 msgstr "cliquez dans la zone d'édition pour annuler la suppression"
 
+msgid "click to add a value"
+msgstr "cliquer pour ajouter une valeur"
+
+msgid "click to delete this value"
+msgstr "cliquer pour supprimer cette valeur"
+
 msgid "click to edit this field"
 msgstr "cliquez pour éditer ce champ"
 
@@ -1426,10 +1462,10 @@
 msgid "condition"
 msgstr "condition"
 
-msgctxt "RQLExpression"
 msgid "condition_object"
 msgstr "condition de"
 
+msgctxt "RQLExpression"
 msgid "condition_object"
 msgstr "condition de"
 
@@ -1456,16 +1492,30 @@
 msgid "constrained_by"
 msgstr "contraint par"
 
-msgctxt "CWConstraint"
 msgid "constrained_by_object"
 msgstr "contrainte de"
 
+msgctxt "CWConstraint"
 msgid "constrained_by_object"
 msgstr "contrainte de"
 
 msgid "constraint factory"
 msgstr "fabrique de contraintes"
 
+msgid "constraint_of"
+msgstr "contrainte de"
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr "contrainte de"
+
+msgid "constraint_of_object"
+msgstr "contraint par"
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr "contraint par"
+
 msgid "constraints"
 msgstr "contraintes"
 
@@ -1524,6 +1574,11 @@
 msgid "context where this component should be displayed"
 msgstr "contexte où ce composant doit être affiché"
 
+msgid "context where this facet should be displayed, leave empty for both"
+msgstr ""
+"contexte où cette facette doit être affichée. Laissez ce champ vide pour "
+"l'avoir dans les deux."
+
 msgid "control subject entity's relations order"
 msgstr "contrôle l'ordre des relations de l'entité sujet"
 
@@ -1595,6 +1650,11 @@
 msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
 msgstr "création relation %(linkto)s"
 
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr "création d'une contrainte unique_together sur %(linkto)s"
+
 msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
 msgstr "création d'un utilisateur à rajouter au groupe %(linkto)s"
 
@@ -1680,8 +1740,8 @@
 msgstr "création d'une transition workflow autorisée depuis l'état %(linkto)s"
 
 msgid ""
-"creating WorkflowTransition (WorkflowTransition transition_of Workflow %"
-"(linkto)s)"
+"creating WorkflowTransition (WorkflowTransition transition_of Workflow "
+"%(linkto)s)"
 msgstr "création d'une transition workflow du workflow %(linkto)s"
 
 msgid "creation"
@@ -1697,19 +1757,19 @@
 msgstr "date de création"
 
 msgid "cstrtype"
-msgstr "type de constrainte"
+msgstr "type de contrainte"
 
 msgctxt "CWConstraint"
 msgid "cstrtype"
 msgstr "type"
 
+msgid "cstrtype_object"
+msgstr "utilisé par"
+
 msgctxt "CWConstraintType"
 msgid "cstrtype_object"
 msgstr "type des contraintes"
 
-msgid "cstrtype_object"
-msgstr "utilisé par"
-
 msgid "csv entities export"
 msgstr "export d'entités en CSV"
 
@@ -1792,10 +1852,10 @@
 msgid "default_workflow"
 msgstr "workflow par défaut"
 
-msgctxt "Workflow"
 msgid "default_workflow_object"
 msgstr "workflow par défaut de"
 
+msgctxt "Workflow"
 msgid "default_workflow_object"
 msgstr "workflow par défaut de"
 
@@ -1816,7 +1876,7 @@
 "define a final relation: link a final relation type from a non final entity "
 "to a final entity type. used to build the instance schema"
 msgstr ""
-"définit une relation non finale: lie un type de relation non finaledepuis "
+"définit une relation non finale: lie un type de relation non finale depuis "
 "une entité vers un type d'entité non final. Utilisé pour construire le "
 "schéma de l'instance"
 
@@ -1845,6 +1905,9 @@
 msgid "define how we get out from a sub-workflow"
 msgstr "définit comment sortir d'un sous-workflow"
 
+msgid "defines a sql-level multicolumn unique index"
+msgstr "définit un index SQL unique sur plusieurs colonnes"
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -1864,9 +1927,6 @@
 msgid "delete this relation"
 msgstr "supprimer cette relation"
 
-msgid "delete_perm"
-msgstr "suppression"
-
 msgid "delete_permission"
 msgstr "permission de supprimer"
 
@@ -1878,6 +1938,9 @@
 msgid "delete_permission"
 msgstr "permission de supprimer"
 
+msgid "delete_permission_object"
+msgstr "a la permission de supprimer"
+
 msgctxt "CWGroup"
 msgid "delete_permission_object"
 msgstr "peut supprimer"
@@ -1886,17 +1949,14 @@
 msgid "delete_permission_object"
 msgstr "peut supprimer"
 
-msgid "delete_permission_object"
-msgstr "a la permission de supprimer"
-
 #, python-format
 msgid "deleted %(etype)s #%(eid)s (%(title)s)"
 msgstr "suppression de l'entité %(etype)s #%(eid)s (%(title)s)"
 
 #, python-format
 msgid ""
-"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%"
-"(eidto)s"
+"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #"
+"%(eidto)s"
 msgstr ""
 "relation %(rtype)s de %(frometype)s #%(eidfrom)s vers %(toetype)s #%(eidto)s "
 "supprimée"
@@ -1907,15 +1967,7 @@
 msgid "description"
 msgstr "description"
 
-msgctxt "CWEType"
-msgid "description"
-msgstr "description"
-
-msgctxt "CWRelation"
-msgid "description"
-msgstr "description"
-
-msgctxt "Workflow"
+msgctxt "BaseTransition"
 msgid "description"
 msgstr "description"
 
@@ -1923,15 +1975,7 @@
 msgid "description"
 msgstr "description"
 
-msgctxt "Transition"
-msgid "description"
-msgstr "description"
-
-msgctxt "WorkflowTransition"
-msgid "description"
-msgstr "description"
-
-msgctxt "State"
+msgctxt "CWEType"
 msgid "description"
 msgstr "description"
 
@@ -1939,10 +1983,34 @@
 msgid "description"
 msgstr "description"
 
-msgctxt "BaseTransition"
+msgctxt "CWRelation"
+msgid "description"
+msgstr "description"
+
+msgctxt "State"
+msgid "description"
+msgstr "description"
+
+msgctxt "Transition"
 msgid "description"
 msgstr "description"
 
+msgctxt "Workflow"
+msgid "description"
+msgstr "description"
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr "description"
+
+msgid "description_format"
+msgstr "format"
+
+msgctxt "BaseTransition"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWAttribute"
 msgid "description_format"
 msgstr "format"
 
@@ -1950,38 +2018,30 @@
 msgid "description_format"
 msgstr "format"
 
+msgctxt "CWRType"
+msgid "description_format"
+msgstr "format"
+
 msgctxt "CWRelation"
 msgid "description_format"
 msgstr "format"
 
+msgctxt "State"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "Transition"
+msgid "description_format"
+msgstr "format"
+
 msgctxt "Workflow"
 msgid "description_format"
 msgstr "format"
 
-msgctxt "CWAttribute"
-msgid "description_format"
-msgstr "format"
-
-msgctxt "Transition"
-msgid "description_format"
-msgstr "format"
-
 msgctxt "WorkflowTransition"
 msgid "description_format"
 msgstr "format"
 
-msgctxt "State"
-msgid "description_format"
-msgstr "format"
-
-msgctxt "CWRType"
-msgid "description_format"
-msgstr "format"
-
-msgctxt "BaseTransition"
-msgid "description_format"
-msgstr "format"
-
 msgid "destination state for this transition"
 msgstr "états accessibles par cette transition"
 
@@ -2004,21 +2064,21 @@
 msgid "destination_state"
 msgstr "état de destination"
 
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr "état de destination"
+
 msgctxt "Transition"
 msgid "destination_state"
 msgstr "état de destination"
 
-msgctxt "SubWorkflowExitPoint"
-msgid "destination_state"
-msgstr "état de destination"
+msgid "destination_state_object"
+msgstr "destination de"
 
 msgctxt "State"
 msgid "destination_state_object"
 msgstr "état final de"
 
-msgid "destination_state_object"
-msgstr "destination de"
-
 msgid "detach attached file"
 msgstr "détacher le fichier existant"
 
@@ -2028,12 +2088,18 @@
 msgid "display order of the component"
 msgstr "ordre d'affichage du composant"
 
+msgid "display order of the facet"
+msgstr "ordre d'affichage de la facette"
+
 msgid "display the box or not"
 msgstr "afficher la boîte ou non"
 
 msgid "display the component or not"
 msgstr "afficher le composant ou non"
 
+msgid "display the facet or not"
+msgstr "afficher la facette ou non"
+
 msgid ""
 "distinct label to distinguate between other permission entity of the same "
 "name"
@@ -2066,9 +2132,6 @@
 msgid "editable-table"
 msgstr "table éditable"
 
-msgid "edition"
-msgstr "édition"
-
 msgid "eid"
 msgstr "eid"
 
@@ -2081,6 +2144,9 @@
 msgid "embed"
 msgstr "embarqué"
 
+msgid "embedded html"
+msgstr "HTML contenu"
+
 msgid "embedding this url is forbidden"
 msgstr "l'inclusion de cette url est interdite"
 
@@ -2146,7 +2212,7 @@
 msgid "eta_date"
 msgstr "date de fin"
 
-msgid "exit state must a subworkflow state"
+msgid "exit state must be a subworkflow state"
 msgstr "l'état de sortie doit être un état du sous-workflow"
 
 msgid "exit_point"
@@ -2269,13 +2335,13 @@
 msgid "for_user"
 msgstr "propriété de l'utilisateur"
 
+msgid "for_user_object"
+msgstr "utilise les propriétés"
+
 msgctxt "CWUser"
 msgid "for_user_object"
 msgstr "a pour préférence"
 
-msgid "for_user_object"
-msgstr "utilise les propriétés"
-
 msgid "friday"
 msgstr "vendredi"
 
@@ -2297,13 +2363,13 @@
 msgid "from_entity"
 msgstr "relation de l'entité"
 
+msgid "from_entity_object"
+msgstr "relation sujet"
+
 msgctxt "CWEType"
 msgid "from_entity_object"
 msgstr "entité de"
 
-msgid "from_entity_object"
-msgstr "relation sujet"
-
 msgid "from_interval_start"
 msgstr "De"
 
@@ -2314,13 +2380,13 @@
 msgid "from_state"
 msgstr "état de départ"
 
+msgid "from_state_object"
+msgstr "transitions depuis cet état"
+
 msgctxt "State"
 msgid "from_state_object"
 msgstr "état de départ de"
 
-msgid "from_state_object"
-msgstr "transitions depuis cet état"
-
 msgid "full text or RQL query"
 msgstr "texte à rechercher ou requête RQL"
 
@@ -2357,16 +2423,25 @@
 msgid "granted to groups"
 msgstr "accordée aux groupes"
 
-msgid "graphical representation of the instance'schema"
-msgstr "représentation graphique du schéma de l'instance"
+#, python-format
+msgid "graphical representation of %(appid)s data model"
+msgstr "réprésentation graphique du modèle de données de %(appid)s"
 
 #, python-format
-msgid "graphical schema for %s"
-msgstr "graphique du schéma pour %s"
+msgid ""
+"graphical representation of the %(etype)s entity type from %(appid)s data "
+"model"
+msgstr ""
+"réprésentation graphique du modèle de données pour le type d'entité "
+"%(etype)s de %(appid)s"
 
 #, python-format
-msgid "graphical workflow for %s"
-msgstr "graphique du workflow pour %s"
+msgid ""
+"graphical representation of the %(rtype)s relation type from %(appid)s data "
+"model"
+msgstr ""
+"réprésentation graphique du modèle de données pour le type de relation "
+"%(rtype)s de %(appid)s"
 
 msgid "group in which a user should be to be allowed to pass this transition"
 msgstr ""
@@ -2481,13 +2556,13 @@
 msgid "in_group"
 msgstr "fait partie du groupe"
 
+msgid "in_group_object"
+msgstr "membres"
+
 msgctxt "CWGroup"
 msgid "in_group_object"
 msgstr "contient les utilisateurs"
 
-msgid "in_group_object"
-msgstr "membres"
-
 msgid "in_state"
 msgstr "état"
 
@@ -2540,10 +2615,10 @@
 msgid "initial_state"
 msgstr "état initial"
 
-msgctxt "State"
 msgid "initial_state_object"
 msgstr "état initial de"
 
+msgctxt "State"
 msgid "initial_state_object"
 msgstr "état initial de"
 
@@ -2785,6 +2860,9 @@
 msgid "missing parameters for entity %s"
 msgstr "paramètres manquants pour l'entité %s"
 
+msgid "modification"
+msgstr "modification"
+
 msgid "modification_date"
 msgstr "date de modification"
 
@@ -2809,10 +2887,38 @@
 msgid "name"
 msgstr "nom"
 
+msgctxt "BaseTransition"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWCache"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWConstraintType"
+msgid "name"
+msgstr "nom"
+
 msgctxt "CWEType"
 msgid "name"
 msgstr "nom"
 
+msgctxt "CWGroup"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWPermission"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWRType"
+msgid "name"
+msgstr "nom"
+
+msgctxt "State"
+msgid "name"
+msgstr "nom"
+
 msgctxt "Transition"
 msgid "name"
 msgstr "nom"
@@ -2821,38 +2927,10 @@
 msgid "name"
 msgstr "nom"
 
-msgctxt "CWGroup"
-msgid "name"
-msgstr "nom"
-
-msgctxt "CWConstraintType"
-msgid "name"
-msgstr "nom"
-
 msgctxt "WorkflowTransition"
 msgid "name"
 msgstr "nom"
 
-msgctxt "State"
-msgid "name"
-msgstr "nom"
-
-msgctxt "CWPermission"
-msgid "name"
-msgstr "nom"
-
-msgctxt "CWRType"
-msgid "name"
-msgstr "nom"
-
-msgctxt "BaseTransition"
-msgid "name"
-msgstr "nom"
-
-msgctxt "CWCache"
-msgid "name"
-msgstr "nom"
-
 msgid "name of the cache"
 msgstr "nom du cache applicatif"
 
@@ -2860,8 +2938,8 @@
 "name of the main variables which should be used in the selection if "
 "necessary (comma separated)"
 msgstr ""
-"nom des variables principaes qui devrait être utilisées dans la sélection si "
-"nécessaire (les séparer par des virgules)"
+"nom des variables principales qui devrait être utilisées dans la sélection "
+"si nécessaire (les séparer par des virgules)"
 
 msgid "name or identifier of the permission"
 msgstr "nom (identifiant) de la permission"
@@ -2909,6 +2987,9 @@
 msgid "no edited fields specified for entity %s"
 msgstr "aucun champ à éditer spécifié pour l'entité %s"
 
+msgid "no related entity"
+msgstr "pas d'entité liée"
+
 msgid "no related project"
 msgstr "pas de projet rattaché"
 
@@ -3035,9 +3116,6 @@
 msgid "permissions for this entity"
 msgstr "permissions pour cette entité"
 
-msgid "personnal informations"
-msgstr "informations personnelles"
-
 msgid "pick existing bookmarks"
 msgstr "récupérer des signets existants"
 
@@ -3067,13 +3145,13 @@
 msgid "prefered_form"
 msgstr "forme préférée"
 
+msgid "prefered_form_object"
+msgstr "forme préférée à"
+
 msgctxt "EmailAddress"
 msgid "prefered_form_object"
 msgstr "forme préférée de"
 
-msgid "prefered_form_object"
-msgstr "forme préférée à"
-
 msgid "preferences"
 msgstr "préférences"
 
@@ -3090,12 +3168,15 @@
 msgid "primary_email"
 msgstr "email principal"
 
+msgid "primary_email_object"
+msgstr "adresse email principale (object)"
+
 msgctxt "EmailAddress"
 msgid "primary_email_object"
 msgstr "adresse principale de"
 
-msgid "primary_email_object"
-msgstr "adresse email principale (object)"
+msgid "profile"
+msgstr "profil"
 
 msgid "progress"
 msgstr "avancement"
@@ -3115,24 +3196,24 @@
 msgid "read"
 msgstr "lecture"
 
-msgid "read_perm"
-msgstr "lecture"
-
+msgid "read_permission"
+msgstr "permission de lire"
+
+msgctxt "CWAttribute"
 msgid "read_permission"
 msgstr "permission de lire"
 
 msgctxt "CWEType"
 msgid "read_permission"
-msgstr "permission d'ajouter"
-
-msgctxt "CWAttribute"
-msgid "read_permission"
 msgstr "permission de lire"
 
 msgctxt "CWRelation"
 msgid "read_permission"
 msgstr "permission de lire"
 
+msgid "read_permission_object"
+msgstr "a la permission de lire"
+
 msgctxt "CWGroup"
 msgid "read_permission_object"
 msgstr "peut lire"
@@ -3141,9 +3222,6 @@
 msgid "read_permission_object"
 msgstr "peut lire"
 
-msgid "read_permission_object"
-msgstr "a la permission de lire"
-
 msgid "registry"
 msgstr "registre"
 
@@ -3177,16 +3255,34 @@
 msgid "relation_type"
 msgstr "type de relation"
 
+msgid "relation_type_object"
+msgstr "définition"
+
 msgctxt "CWRType"
 msgid "relation_type_object"
 msgstr "définition"
 
-msgid "relation_type_object"
-msgstr "définition"
+msgid "relations"
+msgstr "relations"
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr "relations"
 
 msgid "relations deleted"
 msgstr "relations supprimées"
 
+msgid "relations_object"
+msgstr "relations de"
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr "contraint par"
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr "contraint par"
+
 msgid "relative url of the bookmarked page"
 msgstr "url relative de la page"
 
@@ -3200,11 +3296,11 @@
 msgid "require_group"
 msgstr "restreinte au groupe"
 
-msgctxt "Transition"
+msgctxt "CWPermission"
 msgid "require_group"
 msgstr "restreinte au groupe"
 
-msgctxt "CWPermission"
+msgctxt "Transition"
 msgid "require_group"
 msgstr "restreinte au groupe"
 
@@ -3212,11 +3308,11 @@
 msgid "require_group"
 msgstr "restreinte au groupe"
 
+msgid "require_group_object"
+msgstr "a les droits"
+
 msgctxt "CWGroup"
 msgid "require_group_object"
-msgstr "de"
-
-msgid "require_group_object"
 msgstr "a les droits"
 
 msgid "require_permission"
@@ -3244,9 +3340,9 @@
 "current relation'subject, object and to the request user. "
 msgstr ""
 "partie restriction de la requête rql. Pour une expression s'appliquant à une "
-"entité, X et U sont respectivement préféfinis à l'entité et à l'utilisateur "
+"entité, X et U sont respectivement prédéfinis à l'entité et à l'utilisateur "
 "courant. Pour une expression s'appliquant à une relation, S, O et U sont "
-"respectivement préféfinis au sujet/objet de la relation et à l'utilisateur "
+"respectivement prédéfinis au sujet/objet de la relation et à l'utilisateur "
 "courant."
 
 msgid "revert changes"
@@ -3262,7 +3358,7 @@
 msgstr "RSS"
 
 msgid "same_as"
-msgstr "identique à l'entité externe"
+msgstr "identique à"
 
 msgid "sample format"
 msgstr "exemple"
@@ -3394,7 +3490,7 @@
 msgid "site-wide property can't be set for user"
 msgstr "une propriété spécifique au site ne peut être propre à un utilisateur"
 
-msgid "some errors occured:"
+msgid "some errors occurred:"
 msgstr "des erreurs sont survenues"
 
 msgid "some later transaction(s) touch entity, undo them first"
@@ -3417,10 +3513,10 @@
 msgid "specializes"
 msgstr "spécialise"
 
-msgctxt "CWEType"
 msgid "specializes_object"
 msgstr "parent de"
 
+msgctxt "CWEType"
 msgid "specializes_object"
 msgstr "parent de"
 
@@ -3459,13 +3555,13 @@
 msgid "state_of"
 msgstr "état de"
 
+msgid "state_of_object"
+msgstr "a pour état"
+
 msgctxt "Workflow"
 msgid "state_of_object"
 msgstr "contient les états"
 
-msgid "state_of_object"
-msgstr "a pour état"
-
 msgid "status change"
 msgstr "changer l'état"
 
@@ -3508,20 +3604,20 @@
 msgid "subworkflow_exit"
 msgstr "sortie du sous-workflow"
 
+msgid "subworkflow_exit_object"
+msgstr "états de sortie"
+
 msgctxt "SubWorkflowExitPoint"
 msgid "subworkflow_exit_object"
 msgstr "états de sortie"
 
-msgid "subworkflow_exit_object"
-msgstr "états de sortie"
+msgid "subworkflow_object"
+msgstr "utilisé par la transition"
 
 msgctxt "Workflow"
 msgid "subworkflow_object"
 msgstr "sous workflow de"
 
-msgid "subworkflow_object"
-msgstr "utilisé par la transition"
-
 msgid "subworkflow_state"
 msgstr "état du sous-workflow"
 
@@ -3529,10 +3625,10 @@
 msgid "subworkflow_state"
 msgstr "état"
 
-msgctxt "State"
 msgid "subworkflow_state_object"
 msgstr "état de sortie de"
 
+msgctxt "State"
 msgid "subworkflow_state_object"
 msgstr "état de sortie de"
 
@@ -3647,12 +3743,12 @@
 msgid "to_entity"
 msgstr "pour l'entité"
 
+msgid "to_entity_object"
+msgstr "objet de la relation"
+
 msgctxt "CWEType"
 msgid "to_entity_object"
-msgstr "relation objet"
-
-msgid "to_entity_object"
-msgstr "relation objet"
+msgstr "objet de la relation"
 
 msgid "to_interval_end"
 msgstr "à"
@@ -3664,13 +3760,13 @@
 msgid "to_state"
 msgstr "état de destination"
 
+msgid "to_state_object"
+msgstr "transitions vers cet état"
+
 msgctxt "State"
 msgid "to_state_object"
 msgstr "transition vers cet état"
 
-msgid "to_state_object"
-msgstr "transitions vers cet état"
-
 msgid "todo_by"
 msgstr "à faire par"
 
@@ -3708,10 +3804,10 @@
 msgid "transition_of"
 msgstr "transition de"
 
-msgctxt "Workflow"
 msgid "transition_of_object"
 msgstr "a pour transition"
 
+msgctxt "Workflow"
 msgid "transition_of_object"
 msgstr "a pour transition"
 
@@ -3809,19 +3905,19 @@
 msgid "update"
 msgstr "modification"
 
-msgid "update_perm"
-msgstr "modification"
-
 msgid "update_permission"
 msgstr "permission de modification"
 
+msgctxt "CWAttribute"
+msgid "update_permission"
+msgstr "permission de modifier"
+
 msgctxt "CWEType"
 msgid "update_permission"
 msgstr "permission de modifier"
 
-msgctxt "CWAttribute"
-msgid "update_permission"
-msgstr "permission de modifier"
+msgid "update_permission_object"
+msgstr "a la permission de modifier"
 
 msgctxt "CWGroup"
 msgid "update_permission_object"
@@ -3831,8 +3927,8 @@
 msgid "update_permission_object"
 msgstr "peut modifier"
 
-msgid "update_permission_object"
-msgstr "a la permission de modifier"
+msgid "update_relation"
+msgstr "modifier"
 
 msgid "updated"
 msgstr "mis à jour"
@@ -3867,13 +3963,13 @@
 msgid "use_email"
 msgstr "utilise l'adresse électronique"
 
+msgid "use_email_object"
+msgstr "adresse utilisée par"
+
 msgctxt "EmailAddress"
 msgid "use_email_object"
 msgstr "utilisée par"
 
-msgid "use_email_object"
-msgstr "adresse utilisée par"
-
 msgid "use_template_format"
 msgstr "utilisation du format 'cubicweb template'"
 
@@ -3984,6 +4080,10 @@
 msgid "view_index"
 msgstr "accueil"
 
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr "violation de contrainte unique_together (%s)"
+
 msgid "visible"
 msgstr "visible"
 
@@ -4057,10 +4157,10 @@
 msgid "workflow_of"
 msgstr "workflow de"
 
-msgctxt "CWEType"
 msgid "workflow_of_object"
 msgstr "a pour workflow"
 
+msgctxt "CWEType"
 msgid "workflow_of_object"
 msgstr "a pour workflow"
 
@@ -4085,3 +4185,27 @@
 
 msgid "you should probably delete that property"
 msgstr "vous devriez probablement supprimer cette propriété"
+
+#~ msgid "add_perm"
+#~ msgstr "ajout"
+
+#~ msgid "delete_perm"
+#~ msgstr "suppression"
+
+#~ msgid "edition"
+#~ msgstr "édition"
+
+#~ msgid "graphical workflow for %s"
+#~ msgstr "graphique du workflow pour %s"
+
+#~ msgid "personnal informations"
+#~ msgstr "informations personnelles"
+
+#~ msgid "read_perm"
+#~ msgstr "lecture"
+
+#~ msgid "update_perm"
+#~ msgstr "modification"
+
+#~ msgid "yams type, rdf type or mime type of the object"
+#~ msgstr "type yams, vocabulaire rdf ou type mime de l'objet"
--- a/i18n/static-messages.pot	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-msgid "read_perm"
-msgstr ""
-
-msgid "add_perm"
-msgstr ""
-
-msgid "update_perm"
-msgstr ""
-
-msgid "delete_perm"
-msgstr ""
--- a/interfaces.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/interfaces.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,68 +15,24 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-Standard interfaces.
+"""Standard interfaces. Deprecated in favor of adapters.
 
 .. note::
 
-  The `implements` selector matches not only entity classes but also
-  their interfaces. Writing __select__ = implements('IGeocodable') is
-  a perfectly fine thing to do.
+  The `implements` selector used to match not only entity classes but also their
+  interfaces. This will disappear in a future version. You should define an
+  adapter for that interface and use `adaptable('MyIFace')` selector on appobjects
+  that require that interface.
+
 """
 __docformat__ = "restructuredtext en"
 
 from logilab.common.interface import Interface
 
-class IEmailable(Interface):
-    """interface for emailable entities"""
 
-    def get_email(self):
-        """return email address"""
-
-    @classmethod
-    def allowed_massmail_keys(cls):
-        """returns a set of allowed email substitution keys
-
-        The default is to return the entity's attribute list but an
-        entity class might override this method to allow extra keys.
-        For instance, the Person class might want to return a `companyname`
-        key.
-        """
-
-    def as_email_context(self):
-        """returns the dictionary as used by the sendmail controller to
-        build email bodies.
-
-        NOTE: the dictionary keys should match the list returned by the
-        `allowed_massmail_keys` method.
-        """
-
-
-class IWorkflowable(Interface):
-    """interface for entities dealing with a specific workflow"""
-    # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn
-
-    @property
-    def state(self):
-        """return current state name"""
-
-    def change_state(self, stateeid, trcomment=None, trcommentformat=None):
-        """change the entity's state to the state of the given name in entity's
-        workflow
-        """
-
-    def latest_trinfo(self):
-        """return the latest transition information for this entity
-        """
-
-
+# XXX deprecates in favor of IProgressAdapter
 class IProgress(Interface):
-    """something that has a cost, a state and a progression
-
-    Take a look at cubicweb.mixins.ProgressMixIn for some
-    default implementations
-    """
+    """something that has a cost, a state and a progression"""
 
     @property
     def cost(self):
@@ -112,7 +68,7 @@
     def progress(self):
         """returns the % progress of the task item"""
 
-
+# XXX deprecates in favor of IMileStoneAdapter
 class IMileStone(IProgress):
     """represents an ITask's item"""
 
@@ -135,7 +91,132 @@
     def contractors(self):
         """returns the list of persons supposed to work on this task"""
 
+# XXX deprecates in favor of IEmbedableAdapter
+class IEmbedable(Interface):
+    """interface for embedable entities"""
 
+    def embeded_url(self):
+        """embed action interface"""
+
+# XXX deprecates in favor of ICalendarViewsAdapter
+class ICalendarViews(Interface):
+    """calendar views interface"""
+    def matching_dates(self, begin, end):
+        """
+        :param begin: day considered as begin of the range (`DateTime`)
+        :param end: day considered as end of the range (`DateTime`)
+
+        :return:
+          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+          this entity apply
+        """
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ICalendarable(Interface):
+    """interface for items that do have a begin date 'start' and an end date 'stop'
+    """
+
+    @property
+    def start(self):
+        """return start date"""
+
+    @property
+    def stop(self):
+        """return stop state"""
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ITimetableViews(Interface):
+    """timetable views interface"""
+    def timetable_date(self):
+        """XXX explain
+
+        :return: date (`DateTime`)
+        """
+
+# XXX deprecates in favor of IGeocodableAdapter
+class IGeocodable(Interface):
+    """interface required by geocoding views such as gmap-view"""
+
+    @property
+    def latitude(self):
+        """returns the latitude of the entity"""
+
+    @property
+    def longitude(self):
+        """returns the longitude of the entity"""
+
+    def marker_icon(self):
+        """returns the icon that should be used as the marker"""
+
+# XXX deprecates in favor of ISIOCItemAdapter
+class ISiocItem(Interface):
+    """interface for entities which may be represented as an ISIOC item"""
+
+    def isioc_content(self):
+        """return item's content"""
+
+    def isioc_container(self):
+        """return container entity"""
+
+    def isioc_type(self):
+        """return container type (post, BlogPost, MailMessage)"""
+
+    def isioc_replies(self):
+        """return replies items"""
+
+    def isioc_topics(self):
+        """return topics items"""
+
+# XXX deprecates in favor of ISIOCContainerAdapter
+class ISiocContainer(Interface):
+    """interface for entities which may be represented as an ISIOC container"""
+
+    def isioc_type(self):
+        """return container type (forum, Weblog, MailingList)"""
+
+    def isioc_items(self):
+        """return contained items"""
+
+# XXX deprecates in favor of IEmailableAdapter
+class IFeed(Interface):
+    """interface for entities with rss flux"""
+
+    def rss_feed_url(self):
+        """"""
+
+# XXX deprecates in favor of IDownloadableAdapter
+class IDownloadable(Interface):
+    """interface for downloadable entities"""
+
+    def download_url(self): # XXX not really part of this interface
+        """return an url to download entity's content"""
+    def download_content_type(self):
+        """return MIME type of the downloadable content"""
+    def download_encoding(self):
+        """return encoding of the downloadable content"""
+    def download_file_name(self):
+        """return file name of the downloadable content"""
+    def download_data(self):
+        """return actual data of the downloadable content"""
+
+# XXX deprecates in favor of IPrevNextAdapter
+class IPrevNext(Interface):
+    """interface for entities which can be linked to a previous and/or next
+    entity
+    """
+
+    def next_entity(self):
+        """return the 'next' entity"""
+    def previous_entity(self):
+        """return the 'previous' entity"""
+
+# XXX deprecates in favor of IBreadCrumbsAdapter
+class IBreadCrumbs(Interface):
+
+    def breadcrumbs(self, view, recurs=False):
+        pass
+
+# XXX deprecates in favor of ITreeAdapter
 class ITree(Interface):
 
     def parent(self):
@@ -159,141 +240,3 @@
     def root(self):
         """returns the root object"""
 
-
-## web specific interfaces ####################################################
-
-
-class IPrevNext(Interface):
-    """interface for entities which can be linked to a previous and/or next
-    entity
-    """
-
-    def next_entity(self):
-        """return the 'next' entity"""
-    def previous_entity(self):
-        """return the 'previous' entity"""
-
-
-class IBreadCrumbs(Interface):
-    """interface for entities which can be "located" on some path"""
-
-    # XXX fix recurs !
-    def breadcrumbs(self, view, recurs=False):
-        """return a list containing some:
-
-        * tuple (url, label)
-        * entity
-        * simple label string
-
-        defining path from a root to the current view
-
-        the main view is given as argument so breadcrumbs may vary according
-        to displayed view (may be None). When recursing on a parent entity,
-        the `recurs` argument should be set to True.
-        """
-
-
-class IDownloadable(Interface):
-    """interface for downloadable entities"""
-
-    def download_url(self): # XXX not really part of this interface
-        """return an url to download entity's content"""
-    def download_content_type(self):
-        """return MIME type of the downloadable content"""
-    def download_encoding(self):
-        """return encoding of the downloadable content"""
-    def download_file_name(self):
-        """return file name of the downloadable content"""
-    def download_data(self):
-        """return actual data of the downloadable content"""
-
-
-class IEmbedable(Interface):
-    """interface for embedable entities"""
-
-    def embeded_url(self):
-        """embed action interface"""
-
-class ICalendarable(Interface):
-    """interface for items that do have a begin date 'start' and an end date 'stop'
-    """
-
-    @property
-    def start(self):
-        """return start date"""
-
-    @property
-    def stop(self):
-        """return stop state"""
-
-class ICalendarViews(Interface):
-    """calendar views interface"""
-    def matching_dates(self, begin, end):
-        """
-        :param begin: day considered as begin of the range (`DateTime`)
-        :param end: day considered as end of the range (`DateTime`)
-
-        :return:
-          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
-          this entity apply
-        """
-
-class ITimetableViews(Interface):
-    """timetable views interface"""
-    def timetable_date(self):
-        """XXX explain
-
-        :return: date (`DateTime`)
-        """
-
-class IGeocodable(Interface):
-    """interface required by geocoding views such as gmap-view"""
-
-    @property
-    def latitude(self):
-        """returns the latitude of the entity"""
-
-    @property
-    def longitude(self):
-        """returns the longitude of the entity"""
-
-    def marker_icon(self):
-        """returns the icon that should be used as the marker
-        (returns None for default)
-        """
-
-class IFeed(Interface):
-    """interface for entities with rss flux"""
-
-    def rss_feed_url(self):
-        """return an url which layout sub-entities item
-        """
-
-class ISiocItem(Interface):
-    """interface for entities (which are item
-    in sioc specification) with sioc views"""
-
-    def isioc_content(self):
-        """return content entity"""
-
-    def isioc_container(self):
-        """return container entity"""
-
-    def isioc_type(self):
-        """return container type (post, BlogPost, MailMessage)"""
-
-    def isioc_replies(self):
-        """return replies items"""
-
-    def isioc_topics(self):
-        """return topics items"""
-
-class ISiocContainer(Interface):
-    """interface for entities (which are container
-    in sioc specification) with sioc views"""
-
-    def isioc_type(self):
-        """return container type (forum, Weblog, MailingList)"""
-
-    def isioc_items(self):
-        """return contained items"""
--- a/mail.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/mail.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/migration.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""utilities for instances migration
+"""utilities for instances migration"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -25,14 +24,16 @@
 import logging
 import tempfile
 from os.path import exists, join, basename, splitext
+from itertools import chain
 
+from logilab.common import IGNORED_EXTENSIONS
 from logilab.common.decorators import cached
 from logilab.common.configuration import REQUIRED, read_old_config
 from logilab.common.shellutils import ASK
 from logilab.common.changelog import Version
 
-from cubicweb import ConfigurationError
-
+from cubicweb import ConfigurationError, ExecutionError
+from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
 
 def filter_scripts(config, directory, fromversion, toversion, quiet=True):
     """return a list of paths of migration files to consider to upgrade
@@ -52,8 +53,7 @@
         return []
     result = []
     for fname in os.listdir(directory):
-        if fname.endswith('.pyc') or fname.endswith('.pyo') \
-               or fname.endswith('~'):
+        if fname.endswith(IGNORED_EXTENSIONS):
             continue
         fpath = join(directory, fname)
         try:
@@ -76,9 +76,6 @@
     return sorted(result)
 
 
-IGNORED_EXTENSIONS = ('.swp', '~')
-
-
 def execscript_confirm(scriptpath):
     """asks for confirmation before executing a script and provides the
     ability to show the script's content
@@ -111,7 +108,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
@@ -125,13 +122,15 @@
                           'config': self.config,
                           'interactive_mode': interactive,
                           }
+        self._context_stack = []
 
     def __getattribute__(self, name):
         try:
             return object.__getattribute__(self, name)
         except AttributeError:
             cmd = 'cmd_%s' % name
-            if hasattr(self, cmd):
+            # search self.__class__ to avoid infinite recursion
+            if hasattr(self.__class__, cmd):
                 meth = getattr(self, cmd)
                 return lambda *args, **kwargs: self.interact(args, kwargs,
                                                              meth=meth)
@@ -202,7 +201,8 @@
         if not ask_confirm or self.confirm(msg):
             return meth(*args, **kwargs)
 
-    def confirm(self, question, shell=True, abort=True, retry=False, default='y'):
+    def confirm(self, question, shell=True, abort=True, retry=False, pdb=False,
+                default='y'):
         """ask for confirmation and return true on positive answer
 
         if `retry` is true the r[etry] answer may return 2
@@ -210,6 +210,8 @@
         possibleanswers = ['y', 'n']
         if abort:
             possibleanswers.append('abort')
+        if pdb:
+            possibleanswers.append('pdb')
         if shell:
             possibleanswers.append('shell')
         if retry:
@@ -224,9 +226,13 @@
             return 2
         if answer == 'abort':
             raise SystemExit(1)
-        if shell and answer == 'shell':
+        if answer == 'shell':
             self.interactive_shell()
-            return self.confirm(question)
+            return self.confirm(question, shell, abort, retry, pdb, default)
+        if answer == 'pdb':
+            import pdb
+            pdb.set_trace()
+            return self.confirm(question, shell, abort, retry, pdb, default)
         return True
 
     def interactive_shell(self):
@@ -280,27 +286,62 @@
                     context[attr[4:]] = getattr(self, attr)
         return context
 
+    def update_context(self, key, value):
+        for context in self._context_stack:
+            context[key] = value
+        self.__context[key] = value
+
     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
+
+        Allowed input file formats for migration scripts:
+        - `python` (.py)
+        - `sql` (.sql)
+        - `doctest` (.txt or .rst)
+
+        .. warning:: sql migration scripts are not available in web-only instance
+
+        You can pass script parameters with using double dash (--) in the
+        command line
+
+        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
         """
+        ftypes = {'python':  ('.py',),
+                  'doctest': ('.txt', '.rst'),
+                  'sql':     ('.sql',)}
+        # sql migration scripts are not available in web-only instance
+        if not hasattr(self, "session"):
+            ftypes.pop('sql')
         migrscript = os.path.normpath(migrscript)
-        if migrscript.endswith('.py'):
-            script_mode = 'python'
-        elif migrscript.endswith('.txt') or migrscript.endswith('.rst'):
-            script_mode = 'doctest'
+        for (script_mode, ftype) in ftypes.items():
+            if migrscript.endswith(ftype):
+                break
         else:
-            raise Exception('This is not a valid cubicweb shell input')
+            ftypes = ', '.join(chain(*ftypes.values()))
+            msg = 'ignoring %s, not a valid script extension (%s)'
+            raise ExecutionError(msg % (migrscript, ftypes))
         if not self.execscript_confirm(migrscript):
             return
         scriptlocals = self._create_context().copy()
+        self._context_stack.append(scriptlocals)
         if script_mode == 'python':
             if funcname is None:
                 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:
@@ -311,10 +352,15 @@
                     self.critical('no %s in script %s', funcname, migrscript)
                     return None
                 return func(*args, **kwargs)
+        elif script_mode == 'sql':
+            from cubicweb.server.sqlutils import sqlexec
+            sqlexec(open(migrscript).read(), self.session.system_sql)
+            self.commit()
         else: # script_mode == 'doctest'
             import doctest
             doctest.testfile(migrscript, module_relative=False,
                              optionflags=doctest.ELLIPSIS, globs=scriptlocals)
+        self._context_stack.pop()
 
     def cmd_option_renamed(self, oldname, newname):
         """a configuration option has been renamed"""
@@ -345,10 +391,8 @@
             cubes = (cubes,)
         origcubes = self.config.cubes()
         newcubes = [p for p in self.config.expand_cubes(cubes)
-                       if not p in origcubes]
+                    if not p in origcubes]
         if newcubes:
-            for cube in cubes:
-                assert cube in newcubes
             self.config.add_cubes(newcubes)
         return newcubes
 
@@ -410,8 +454,8 @@
     """
 
     def __init__(self, config):
-        self.cubes = {}
         self.config = config
+        self.cubes = {'cubicweb': cwcfg.cubicweb_version()}
 
     def add_cube(self, name, version):
         self.cubes[name] = version
@@ -419,44 +463,50 @@
     def solve(self):
         self.warnings = []
         self.errors = []
-        self.read_constraints()
-        for cube, versions in sorted(self.constraints.items()):
-            oper, version = None, None
+        self.dependencies = {}
+        self.reverse_dependencies = {}
+        self.constraints = {}
+        # read dependencies
+        for cube in self.cubes:
+            if cube == 'cubicweb': continue
+            self.dependencies[cube] = dict(self.config.cube_dependencies(cube))
+            self.dependencies[cube]['cubicweb'] = self.config.cube_depends_cubicweb_version(cube)
+        # compute reverse dependencies
+        for cube, dependencies in self.dependencies.iteritems():
+            for name, constraint in dependencies.iteritems():
+                self.reverse_dependencies.setdefault(name,set())
+                if constraint:
+                    try:
+                        oper, version = constraint.split()
+                        self.reverse_dependencies[name].add( (oper, version, cube) )
+                    except:
+                        self.warnings.append(
+                            'cube %s depends on %s but constraint badly '
+                            'formatted: %s' % (cube, name, constraint))
+        # check consistency
+        for cube, versions in sorted(self.reverse_dependencies.items()):
+            oper, version, source = None, None, None
             # simplify constraints
             if versions:
                 for constraint in versions:
-                    op, ver = constraint
+                    op, ver, src = constraint
                     if oper is None:
                         oper = op
                         version = ver
+                        source = src
                     elif op == '>=' and oper == '>=':
-                        version = max_version(ver, version)
+                        if version_strictly_lower(version, ver):
+                            version = ver
+                            source = src
                     else:
                         print 'unable to handle this case', oper, version, op, ver
             # "solve" constraint satisfaction problem
             if cube not in self.cubes:
-                self.errors.append( ('add', cube, version) )
+                self.errors.append( ('add', cube, version, source) )
             elif versions:
                 lower_strict = version_strictly_lower(self.cubes[cube], version)
                 if oper in ('>=','='):
                     if lower_strict:
-                        self.errors.append( ('update', cube, version) )
+                        self.errors.append( ('update', cube, version, source) )
                 else:
                     print 'unknown operator', oper
-
-    def read_constraints(self):
-        self.constraints = {}
-        self.reverse_constraints = {}
-        for cube in self.cubes:
-            use = self.config.cube_dependencies(cube)
-            for name, constraint in use.iteritems():
-                self.constraints.setdefault(name,set())
-                if constraint:
-                    try:
-                        oper, version = constraint.split()
-                        self.constraints[name].add( (oper, version) )
-                    except:
-                        self.warnings.append(
-                            'cube %s depends on %s but constraint badly '
-                            'formatted: %s' % (cube, name, constraint))
-                self.reverse_constraints.setdefault(name, set()).add(cube)
--- a/misc/migration/3.6.0_Any.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-sync_schema_props_perms('read_permission', syncperms=False) # fix read_permission cardinality
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.9.0_Any.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,3 @@
+if repo.system_source.dbdriver == 'postgres':
+    sql('ALTER TABLE appears ADD COLUMN weight float')
+    sql('UPDATE appears SET weight=1.0 ')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.9.5_Any.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,4 @@
+if not rql('CWConstraintType X WHERE X name "RQLUniqueConstraint"',
+           ask_confirm=False):
+    rql('INSERT CWConstraintType X: X name "RQLUniqueConstraint"',
+        ask_confirm=False)
--- a/misc/migration/bootstrapmigration_repository.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/misc/migration/bootstrapmigration_repository.py	Wed Nov 03 16:38:28 2010 +0100
@@ -18,7 +18,6 @@
 """allways executed before all others in server migration
 
 it should only include low level schema changes
-
 """
 from __future__ import with_statement
 
@@ -93,6 +92,10 @@
         for action in ('read', 'add', 'delete'):
             drop_relation_definition('CWRType', '%s_permission' % action, 'CWGroup', commit=False)
             drop_relation_definition('CWRType', '%s_permission' % action, 'RQLExpression')
+    sync_schema_props_perms('read_permission', syncperms=False) # fix read_permission cardinality
+
+if applcubicwebversion < (3, 9, 6) and cubicwebversion >= (3, 9, 6):
+    add_entity_type('CWUniqueTogetherConstraint')
 
 if applcubicwebversion < (3, 4, 0) and cubicwebversion >= (3, 4, 0):
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/detect_cycle.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,15 @@
+
+try:
+    rtype, = __args__
+except ValueError:
+    print 'USAGE: cubicweb-ctl shell <instance> detect_cycle.py -- <relation type>'
+    print
+
+graph = {}
+for fromeid, toeid in rql('Any X,Y WHERE X %s Y' % rtype):
+    graph.setdefault(fromeid, []).append(toeid)
+
+from logilab.common.graph import get_cycles
+
+for cycle in get_cycles(graph):
+    print 'cycle', '->'.join(str(n) for n in cycle)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/ldap_change_base_dn.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,24 @@
+from base64 import b64decode, b64encode
+try:
+    uri, newdn = __args__
+except ValueError:
+    print 'USAGE: cubicweb-ctl shell <instance> ldap_change_base_dn.py -- <ldap source uri> <new dn>'
+    print
+    print 'you should not have updated your sources file yet'
+
+olddn = repo.config.sources()[uri]['user-base-dn']
+
+assert olddn != newdn
+
+raw_input("Ensure you've stopped the instance, type enter when done.")
+
+for eid, extid in sql("SELECT eid, extid FROM entities WHERE source='%s'" % uri):
+    olduserdn = b64decode(extid)
+    newuserdn = olduserdn.replace(olddn, newdn)
+    if newuserdn != olduserdn:
+        print olduserdn, '->', newuserdn
+        sql("UPDATE entities SET extid='%s' WHERE eid=%s" % (b64encode(newuserdn), eid))
+
+commit()
+
+print 'you can now update the sources file to the new dn and restart the instance'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/repair_file_1-9_migration.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,52 @@
+"""execute this script if you've migration to file >= 1.9.0 with cubicweb <= 3.9.2
+
+FYI, this migration occurred :
+* on our intranet on July 07 2010
+* on our extranet on July 16 2010
+"""
+from __future__ import with_statement
+
+try:
+    backupinstance, = __args__
+except ValueError:
+    print 'USAGE: cubicweb-ctl shell <instance> repair_file_1-9_migration.py -- <backup instance id>'
+    print
+    print 'you should restored the backup on a new instance, accessible through pyro'
+
+from cubicweb import cwconfig, dbapi
+from cubicweb.server.session import hooks_control
+
+sourcescfg = repo.config.sources()
+backupcfg = cwconfig.instance_configuration(backupinstance)
+backupcfg.repairing = True
+backuprepo, backupcnx = dbapi.in_memory_cnx(backupcfg, sourcescfg['admin']['login'],
+                                            password=sourcescfg['admin']['password'],
+                                            host='localhost')
+backupcu = backupcnx.cursor()
+
+with hooks_control(session, session.HOOKS_DENY_ALL):
+    rql('SET X is Y WHERE X is File, Y name "File", NOT X is Y')
+    rql('SET X is_instance_of Y WHERE X is File, Y name "File", NOT X is_instance_of Y')
+    for rtype, in backupcu.execute('DISTINCT Any RTN WHERE X relation_type RT, RT name RTN,'
+                                   'X from_entity Y, Y name "Image", X is CWRelation, '
+                                   'EXISTS(XX is CWRelation, XX relation_type RT, '
+                                   'XX from_entity YY, YY name "File")'):
+        if rtype in ('is', 'is_instance_of'):
+            continue
+        print rtype
+        for feid, xeid in backupcu.execute('Any F,X WHERE F %s X, F is IN (File,Image)' % rtype):
+            print 'restoring relation %s between file %s and %s' % (rtype, feid, xeid),
+            print rql('SET F %s X WHERE F eid %%(f)s, X eid %%(x)s, NOT F %s X' % (rtype, rtype),
+                      {'f': feid, 'x': xeid})
+
+    for rtype, in backupcu.execute('DISTINCT Any RTN WHERE X relation_type RT, RT name RTN,'
+                                   'X to_entity Y, Y name "Image", X is CWRelation, '
+                                   'EXISTS(XX is CWRelation, XX relation_type RT, '
+                                   'XX to_entity YY, YY name "File")'):
+        print rtype
+        for feid, xeid in backupcu.execute('Any F,X WHERE X %s F, F is IN (File,Image)' % rtype):
+            print 'restoring relation %s between %s and file %s' % (rtype, xeid, feid),
+            print rql('SET X %s F WHERE F eid %%(f)s, X eid %%(x)s, NOT X %s F' % (rtype, rtype),
+                      {'f': feid, 'x': xeid})
+
+commit()
--- a/mixins.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/mixins.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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'
@@ -67,7 +71,7 @@
             _done = set()
         for child in self.children():
             if child.eid in _done:
-                self.error('loop in %s tree', self.__regid__.lower())
+                self.error('loop in %s tree: %s', self.__regid__.lower(), child)
                 continue
             yield child
             _done.add(child.eid)
@@ -90,7 +94,7 @@
         parent = self
         while parent:
             if parent.eid in path:
-                self.error('loop in %s tree', self.__regid__.lower())
+                self.error('loop in %s tree: %s', self.__regid__.lower(), parent)
                 break
             path.append(parent.eid)
             try:
@@ -117,16 +121,6 @@
             return chain([self], _uptoroot(self))
         return _uptoroot(self)
 
-    def notification_references(self, view):
-        """used to control References field of email send on notification
-        for this entity. `view` is the notification view.
-
-        Should return a list of eids which can be used to generate message ids
-        of previously sent email
-        """
-        return self.path()[:-1]
-
-
     ## ITree interface ########################################################
     def parent(self):
         """return the parent entity if any, else None (e.g. if we are on the
@@ -151,7 +145,7 @@
                                 entities=entities)
 
     def children_rql(self):
-        return self.related_rql(self.tree_attribute, self.children_target)
+        return self.cw_related_rql(self.tree_attribute, self.children_target)
 
     def is_leaf(self):
         return len(self.children()) == 0
@@ -171,8 +165,7 @@
     NOTE: The default implementation is based on the
     primary_email / use_email scheme
     """
-    __implements__ = (IEmailable,)
-
+    @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()")
     def get_email(self):
         if getattr(self, 'primary_email', None):
             return self.primary_email[0].address
@@ -180,28 +173,6 @@
             return self.use_email[0].address
         return None
 
-    @classmethod
-    def allowed_massmail_keys(cls):
-        """returns a set of allowed email substitution keys
-
-        The default is to return the entity's attribute list but an
-        entity class might override this method to allow extra keys.
-        For instance, the Person class might want to return a `companyname`
-        key.
-        """
-        return set(rschema.type
-                   for rschema, attrtype in cls.e_schema.attribute_definitions()
-                   if attrtype.type not in ('Password', 'Bytes'))
-
-    def as_email_context(self):
-        """returns the dictionary as used by the sendmail controller to
-        build email bodies.
-
-        NOTE: the dictionary keys should match the list returned by the
-        `allowed_massmail_keys` method.
-        """
-        return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
-
 
 """pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
 classes which have the relation described by the dict's key.
@@ -215,7 +186,7 @@
     }
 
 
-
+# XXX move to cubicweb.web.views.treeview once we delete usage from this file
 def _done_init(done, view, row, col):
     """handle an infinite recursion safety belt"""
     if done is None:
@@ -223,7 +194,7 @@
     entity = view.cw_rset.get_entity(row, col)
     if entity.eid in done:
         msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
-            'rel': entity.tree_attribute,
+            'rel': entity.cw_adapt_to('ITree').tree_relation,
             'eid': entity.eid
             }
         return None, msg
@@ -233,16 +204,20 @@
 
 class TreeViewMixIn(object):
     """a recursive tree view"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead'
+
     __regid__ = 'tree'
+    __select__ = implements(ITree, warn=False)
     item_vid = 'treeitem'
-    __select__ = implements(ITree)
 
     def call(self, done=None, **kwargs):
         if done is None:
             done = set()
         super(TreeViewMixIn, self).call(done=done, **kwargs)
 
-    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+    def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs):
+        assert maxlevel is None or maxlevel > 0
         done, entity = _done_init(done, self, row, col)
         if done is None:
             # entity is actually an error message
@@ -250,8 +225,14 @@
             return
         self.open_item(entity)
         entity.view(vid or self.item_vid, w=self.w, **kwargs)
+        if maxlevel is not None:
+            maxlevel -= 1
+            if maxlevel == 0:
+                self.close_item(entity)
+                return
         relatedrset = entity.children(entities=False)
-        self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
+        self.wview(self.__regid__, relatedrset, 'null', done=done,
+                   maxlevel=maxlevel, **kwargs)
         self.close_item(entity)
 
     def open_item(self, entity):
@@ -262,6 +243,8 @@
 
 class TreePathMixIn(object):
     """a recursive path view"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead'
     __regid__ = 'path'
     item_vid = 'oneline'
     separator = u'&#160;&gt;&#160;'
@@ -286,6 +269,8 @@
 
 class ProgressMixIn(object):
     """provide a default implementations for IProgress interface methods"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead'
 
     @property
     def cost(self):
--- a/mttransforms.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/mttransforms.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/req.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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 class for request/session
+"""Base class for request/session"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -133,7 +132,7 @@
         Example (in a shell session):
 
         >>> c = create_entity('Company', name=u'Logilab')
-        >>> create_entity('Person', firstname=u'John', lastname=u'Doe',
+        >>> create_entity('Person', firstname=u'John', surname=u'Doe',
         ...               works_for=c)
 
         """
@@ -175,6 +174,8 @@
         """return an absolute URL using params dictionary key/values as URL
         parameters. Values are automatically URL quoted, and the
         publishing method to use may be specified or will be guessed.
+
+        raises :exc:`ValueError` if None is found in arguments
         """
         # use *args since we don't want first argument to be "anonymous" to
         # avoid potential clash with kwargs
@@ -202,7 +203,6 @@
             return u'%s%s' % (base_url, path)
         return u'%s%s?%s' % (base_url, path, self.build_url_params(**kwargs))
 
-
     def build_url_params(self, **kwargs):
         """return encoded params to incorporate them in an URL"""
         args = []
@@ -210,6 +210,8 @@
             if not isinstance(values, (list, tuple)):
                 values = (values,)
             for value in values:
+                if value is None:
+                    raise ValueError(_('unauthorized value'))
                 args.append(u'%s=%s' % (param, self.url_quote(value)))
         return '&'.join(args)
 
@@ -279,7 +281,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 +375,11 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
-    # abstract methods to override according to the web front-end #############
-
     def base_url(self):
         """return the root url of the instance"""
-        raise NotImplementedError
+        return self.vreg.config['base-url']
+
+    # abstract methods to override according to the web front-end #############
 
     def describe(self, eid):
         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
--- a/rqlrewrite.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/rqlrewrite.py	Wed Nov 03 16:38:28 2010 +0100
@@ -19,8 +19,8 @@
 tree.
 
 This is used for instance for read security checking in the repository.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from rql import nodes as n, stmts, TypeResolverException
@@ -45,14 +45,12 @@
     allpossibletypes = {}
     for solution in solutions:
         for varname, etype in solution.iteritems():
-            if not varname in newroot.defined_vars or eschema(etype).final:
+            # XXX not considering aliases by design, right ?
+            if varname not in newroot.defined_vars or eschema(etype).final:
                 continue
             allpossibletypes.setdefault(varname, set()).add(etype)
     for varname in sorted(allpossibletypes):
-        try:
-            var = newroot.defined_vars[varname]
-        except KeyError:
-            continue
+        var = newroot.defined_vars[varname]
         stinfo = var.stinfo
         if stinfo.get('uidrel') is not None:
             continue # eid specified, no need for additional type specification
@@ -64,7 +62,7 @@
         if newroot is rqlst and typerel is not None:
             mytyperel = typerel
         else:
-            for vref in newroot.defined_vars[varname].references():
+            for vref in var.references():
                 rel = vref.relation()
                 if rel and rel.is_types_restriction():
                     mytyperel = rel
@@ -79,12 +77,6 @@
             for cst in mytyperel.get_nodes(n.Constant):
                 if not cst.value in possibletypes:
                     cst.parent.remove(cst)
-                    try:
-                        stinfo['possibletypes'].remove(cst.value)
-                    except KeyError:
-                        # restriction on a type not used by this query, may
-                        # occurs with X is IN(...)
-                        pass
         else:
             # we have to add types restriction
             if stinfo.get('scope') is not None:
@@ -94,7 +86,7 @@
                 # to the root
                 rel = newroot.add_type_restriction(var, possibletypes)
             stinfo['typerel'] = rel
-            stinfo['possibletypes'] = possibletypes
+        stinfo['possibletypes'] = possibletypes
 
 
 def remove_solutions(origsolutions, solutions, defined):
--- a/rset.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/rset.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -563,7 +569,8 @@
                     if i == col:
                         continue
                     coletype = self.description[row][i]
-                    # None description possible on column resulting from an outer join
+                    # None description possible on column resulting from an
+                    # outer join
                     if coletype is None or eschema(coletype).final:
                         continue
                     try:
@@ -582,11 +589,20 @@
 
     @cached
     def related_entity(self, row, col):
-        """try to get the related entity to extract format information if any"""
+        """given an cell of the result set, try to return a (entity, relation
+        name) tuple to which this cell is linked.
+
+        This is especially useful when the cell is an attribute of an entity,
+        to get the entity to which this attribute belongs to.
+        """
         rqlst = self.syntax_tree()
+        # UNION query, we've first to find a 'pivot' column to use to get the
+        # actual query from which the row is coming
         etype, locate_query_col = self._locate_query_params(rqlst, row, col)
-        # UNION query, find the subquery from which this entity has been found
+        # now find the query from which this entity has been found. Returned
+        # select node may be a subquery with different column indexes.
         select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
+        # then get the index of root query's col in the subquery
         col = rqlst.subquery_selection_index(select, col)
         if col is None:
             # XXX unexpected, should fix subquery_selection_index ?
--- a/rtags.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/rtags.py	Wed Nov 03 16:38:28 2010 +0100
@@ -34,8 +34,6 @@
    * ``tag_subject_of`` tag a relation in the subject's context
    * ``tag_object_of`` tag a relation in the object's context
    * ``tag_attribute`` shortcut for tag_subject_of
-
-
 """
 __docformat__ = "restructuredtext en"
 
@@ -212,4 +210,27 @@
     _allowed_values = frozenset((True, False))
 
 
+class NoTargetRelationTagsDict(RelationTagsDict):
+
+    @property
+    def name(self):
+        return self.__class__.name
+
+    def tag_subject_of(self, key, tag):
+        subj, rtype, obj = key
+        if obj != '*':
+            self.warning('using explict target type in %s.tag_subject_of() '
+                         'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
+                         self.name, subj, rtype, subj, rtype, obj)
+        super(NoTargetRelationTagsDict, self).tag_subject_of((subj, rtype, '*'), tag)
+
+    def tag_object_of(self, key, tag):
+        subj, rtype, obj = key
+        if subj != '*':
+            self.warning('using explict subject type in %s.tag_object_of() '
+                         'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
+                         self.name, rtype, obj, subj, rtype, obj)
+        super(NoTargetRelationTagsDict, self).tag_object_of(('*', rtype, obj), tag)
+
+
 set_log_methods(RelationTags, logging.getLogger('cubicweb.rtags'))
--- a/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/schema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""classes to define schemas for CubicWeb
+"""classes to define schemas for CubicWeb"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -52,15 +51,19 @@
     'owned_by', 'created_by', 'is', 'is_instance_of', 'identity',
     'eid', 'creation_date', 'modification_date', 'has_text', 'cwuri',
     ))
-SYSTEM_RTYPES = set(('require_permission', 'custom_workflow', 'in_state',
-                     'wf_info_for'))
+WORKFLOW_RTYPES = set(('custom_workflow', 'in_state', 'wf_info_for'))
+SYSTEM_RTYPES = set(('require_permission',)) | WORKFLOW_RTYPES
 
 # set of entity and relation types used to build the schema
 SCHEMA_TYPES = set((
     'CWEType', 'CWRType', 'CWAttribute', 'CWRelation',
-    'CWConstraint', 'CWConstraintType', 'RQLExpression',
+    'CWConstraint', 'CWConstraintType', 'CWUniqueTogetherConstraint',
+    'RQLExpression',
     'relation_type', 'from_entity', 'to_entity',
     'constrained_by', 'cstrtype',
+    'constraint_of', 'relations',
+    'read_permission', 'add_permission',
+    'delete_permission', 'update_permission',
     ))
 
 WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow',
@@ -417,7 +420,7 @@
             # avoid deleting the relation type accidentally...
             self.schema['has_text'].del_relation_def(self, self.schema['String'])
 
-    def schema_entity(self):
+    def schema_entity(self): # XXX @property for consistency with meta
         """return True if this entity type is used to build the schema"""
         return self.type in SCHEMA_TYPES
 
@@ -441,7 +444,7 @@
     def meta(self):
         return self.type in META_RTYPES
 
-    def schema_relation(self):
+    def schema_relation(self): # XXX @property for consistency with meta
         """return True if this relation type is used to build the schema"""
         return self.type in SCHEMA_TYPES
 
@@ -612,6 +615,7 @@
 class BaseRQLConstraint(BaseConstraint):
     """base class for rql constraints
     """
+    distinct_query = None
 
     def __init__(self, restriction, mainvars=None):
         self.restriction = normalize_expression(restriction)
@@ -651,8 +655,12 @@
         pass # this is a vocabulary constraint, not enforce XXX why?
 
     def __str__(self):
-        return '%s(Any %s WHERE %s)' % (self.__class__.__name__, self.mainvars,
-                                        self.restriction)
+        if self.distinct_query:
+            selop = 'Any'
+        else:
+            selop = 'DISTINCT Any'
+        return '%s(%s %s WHERE %s)' % (self.__class__.__name__, selop,
+                                       self.mainvars, self.restriction)
 
     def __repr__(self):
         return '<%s @%#x>' % (self.__str__(), id(self))
@@ -699,7 +707,7 @@
         """
         if not self.match_condition(session, eidfrom, eidto):
             # XXX at this point if both or neither of S and O are in mainvar we
-            # dunno if the validation error `occured` on eidfrom or eidto (from
+            # dunno if the validation error `occurred` on eidfrom or eidto (from
             # user interface point of view)
             #
             # possible enhancement: check entity being created, it's probably
@@ -744,13 +752,14 @@
 
 class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
     """the unique rql constraint check that the result of the query isn't
-    greater than one
-    """
-    distinct_query = True
+    greater than one.
 
-    # XXX turns mainvars into a required argument in __init__, since we've no
-    #     way to guess it correctly (eg if using S,O or U the constraint will
-    #     always be satisfied since we've to use a DISTINCT query)
+    You *must* specify mainvars when instantiating the constraint since there is
+    no way to guess it correctly (e.g. if using S,O or U the constraint will
+    always be satisfied because we've to use a DISTINCT query).
+    """
+    # XXX turns mainvars into a required argument in __init__
+    distinct_query = True
 
     def match_condition(self, session, eidfrom, eidto):
         return len(self.exec_query(session, eidfrom, eidto)) <= 1
--- a/schemas/base.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/schemas/base.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""core CubicWeb schema, but not necessary at bootstrap time
+"""core CubicWeb schema, but not necessary at bootstrap time"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
--- a/schemas/bootstrap.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/schemas/bootstrap.py	Wed Nov 03 16:38:28 2010 +0100
@@ -154,6 +154,17 @@
     value = String(description=_('depends on the constraint type'))
 
 
+class CWUniqueTogetherConstraint(EntityType):
+    """defines a sql-level multicolumn unique index"""
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+    constraint_of = SubjectRelation('CWEType', cardinality='1*', composite='object',
+                                    inlined=True)
+    relations = SubjectRelation(('CWAttribute', 'CWRelation'), cardinality='+*',
+                                 constraints=[RQLConstraint(
+           'O from_entity X, S constraint_of X, O relation_type T, '
+           'T final TRUE OR (T final FALSE AND T inlined TRUE)')])
+
+
 class CWConstraintType(EntityType):
     """define a schema constraint type"""
     __permissions__ = PUB_SYSTEM_ENTITY_PERMS
--- a/schemas/workflow.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/schemas/workflow.py	Wed Nov 03 16:38:28 2010 +0100
@@ -139,7 +139,7 @@
     subworkflow_state = SubjectRelation(
         'State', cardinality='1*',
         constraints=[RQLConstraint('T subworkflow_exit S, T subworkflow WF, O state_of WF',
-                                   msg=_('exit state must a subworkflow state'))],
+                                   msg=_('exit state must be a subworkflow state'))],
         description=_('subworkflow state'))
     destination_state = SubjectRelation(
         'State', cardinality='?*',
--- a/selectors.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/selectors.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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,127 +183,53 @@
 
 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*
 """
 
 __docformat__ = "restructuredtext en"
 
 import logging
 from warnings import warn
+from operator import eq
 
 from logilab.common.deprecation import class_renamed
 from logilab.common.compat import all, any
 from logilab.common.interface import implements as implements_iface
 
-from yams import BASE_TYPES
+from yams.schema import BASE_TYPES, role_name
+from rql.nodes import Function
 
-from cubicweb import Unauthorized, NoSelectableObject, NotAnEntity, role
+from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
+                      CW_EVENT_MANAGER, role)
 # even if not used, let yes here so it's importable through this module
-from cubicweb.appobject import Selector, objectify_selector, yes
-from cubicweb.vregistry import class_regid
-from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.uilib import eid_param
+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.appobject import traced_selection # XXX for bw compat
 
-        >>> 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
-
-
-def score_interface(etypesreg, cls_or_inst, cls, iface):
+def score_interface(etypesreg, eclass, iface):
     """Return XXX if the give object (maybe an instance or class) implements
     the interface.
     """
     if getattr(iface, '__registry__', None) == 'etypes':
         # adjust score if the interface is an entity class
-        parents = etypesreg.parent_classes(cls_or_inst.__regid__)
-        if iface is cls:
+        parents, any = etypesreg.parent_classes(eclass.__regid__)
+        if iface is eclass:
             return len(parents) + 4
-        if iface is parents[-1]: # Any
+        if iface is any: # Any
             return 1
-        for index, basecls in enumerate(reversed(parents[:-1])):
+        for index, basecls in enumerate(reversed(parents)):
             if iface is basecls:
                 return index + 3
         return 0
-    if implements_iface(cls_or_inst, iface):
-        # implenting an interface takes precedence other special Any interface
+    # XXX iface in implements deprecated in 3.9
+    if implements_iface(eclass, iface):
+        # implementing an interface takes precedence other special Any interface
         return 2
     return 0
 
@@ -321,31 +247,6 @@
         return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
 
 
-class ImplementsMixIn(object):
-    """mix-in class for selectors checking implemented interfaces of something
-    """
-    def __init__(self, *expected_ifaces, **kwargs):
-        super(ImplementsMixIn, self).__init__(**kwargs)
-        self.expected_ifaces = expected_ifaces
-
-    def __str__(self):
-        return '%s(%s)' % (self.__class__.__name__,
-                           ','.join(str(s) for s in self.expected_ifaces))
-
-    def score_interfaces(self, req, cls_or_inst, cls):
-        score = 0
-        etypesreg = req.vreg['etypes']
-        for iface in self.expected_ifaces:
-            if isinstance(iface, basestring):
-                # entity type
-                try:
-                    iface = etypesreg.etype_class(iface)
-                except KeyError:
-                    continue # entity type not in the schema
-            score += score_interface(etypesreg, cls_or_inst, cls, iface)
-        return score
-
-
 class EClassSelector(Selector):
     """abstract class for selectors working on *entity class(es)* specified
     explicitly or found of the result set.
@@ -375,14 +276,17 @@
         self.accept_none = accept_none
 
     @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+    def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+                 **kwargs):
         if kwargs.get('entity'):
             return self.score_class(kwargs['entity'].__class__, req)
         if not rset:
             return 0
         score = 0
         if row is None:
-            if not self.accept_none:
+            if accept_none is None:
+                accept_none = self.accept_none
+            if not accept_none:
                 if any(rset[i][col] is None for i in xrange(len(rset))):
                     return 0
             for etype in rset.column_types(col):
@@ -442,7 +346,8 @@
     """
 
     @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+    def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+                 **kwargs):
         if not rset and not kwargs.get('entity'):
             return 0
         score = 0
@@ -450,9 +355,11 @@
             score = self.score_entity(kwargs['entity'])
         elif row is None:
             col = col or 0
+            if accept_none is None:
+                accept_none = self.accept_none
             for row, rowvalue in enumerate(rset.rows):
                 if rowvalue[col] is None: # outer join
-                    if not self.accept_none:
+                    if not accept_none:
                         return 0
                     continue
                 escore = self.score(req, rset, row, col)
@@ -482,7 +389,7 @@
     """Take a list of expected values as initializer argument and store them
     into the :attr:`expected` set attribute.
 
-    You should implements the :meth:`_get_value(cls, req, **kwargs)` method
+    You should implement the :meth:`_get_value(cls, req, **kwargs)` method
     which should return the value for the given context. The selector will then
     return 1 if the value is expected, else 0.
     """
@@ -528,19 +435,49 @@
 
     * `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:
+                continue
+        return 0
+
+
+class adaptable(appobject_selectable):
+    """Return 1 if another appobject is selectable using the same input context.
+
+    Initializer arguments:
+
+    * `regids`, adapter identifiers (e.g. interface names) to which the context
+      (usually entities) should be adaptable. One of them should be selectable
+      when multiple identifiers are given.
+    """
+    def __init__(self, *regids):
+        super(adaptable, self).__init__('adapters', *regids)
 
     def __call__(self, cls, req, **kwargs):
-        try:
-            req.vreg[self.registry].select(self.regid, req, **kwargs)
-            return 1
-        except NoSelectableObject:
-            return 0
-
+        kwargs.setdefault('accept_none', False)
+        # being adaptable to an interface should takes precedence other is_instance('Any'),
+        # but not other explicit is_instance('SomeEntityType'), and:
+        # * is_instance('Any') score is 1
+        # * is_instance('SomeEntityType') score is at least 2
+        score = super(adaptable, self).__call__(cls, req, **kwargs)
+        if score >= 2:
+            return score - 0.5
+        if score == 1:
+            return score + 0.5
+        return score
 
 # rset selectors ##############################################################
 
@@ -586,8 +523,8 @@
 @objectify_selector
 @lltrace
 def one_line_rset(cls, req, rset=None, row=None, **kwargs):
-    """Return 1 if the result set is of size 1 or if a specific row in the
-    result set is specified ('row' argument).
+    """Return 1 if the result set is of size 1, or greater but a specific row in
+      the result set is specified ('row' argument).
     """
     if rset is not None and (row is not None or rset.rowcount == 1):
         return 1
@@ -595,25 +532,34 @@
 
 
 class multi_lines_rset(Selector):
-    """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*
+    """Return 1 if the operator expression matches between `num` elements
+    in the result set and the `expected` value if defined.
+    
+    By default, multi_lines_rset(expected) matches equality expression:
+        `nb` row(s) in result set equals to expected value
+    But, you can perform richer comparisons by overriding default operator:
+        multi_lines_rset(expected, operator.gt)
+    
+    If `expected` is None, return 1 if the result set contains *at least*
     two rows.
+    If rset is None, return 0.
     """
-    def __init__(self, nb=None):
-        self.expected = nb
+    def __init__(self, expected=None, operator=eq):
+        self.expected = expected
+        self.operator = operator
 
     def match_expected(self, num):
         if self.expected is None:
             return num > 1
-        return num == self.expected
+        return self.operator(num, self.expected)
 
     @lltrace
     def __call__(self, cls, req, rset=None, **kwargs):
-        return rset is not None and self.match_expected(rset.rowcount)
+        return int(rset is not None and self.match_expected(rset.rowcount))
 
 
 class multi_columns_rset(multi_lines_rset):
-    """If `nb`is specified, return 1 if the result set has exactly `nb` column
+    """If `nb` is specified, return 1 if the result set has exactly `nb` column
     per row. Else (`nb` is None), return 1 if the result set contains *at least*
     two columns per row. Return 0 for empty result set.
     """
@@ -659,12 +605,17 @@
 @lltrace
 def sorted_rset(cls, req, rset=None, **kwargs):
     """Return 1 for sorted result set (e.g. from an RQL query containing an
-    :ref:ORDERBY clause.
+    :ref:ORDERBY clause), with exception that it will return 0 if the rset is
+    'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
     """
     if rset is None:
         return 0
-    rqlst = rset.syntax_tree()
-    if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
+    selects = rset.syntax_tree().children
+    if (len(selects) > 1 or
+        not selects[0].orderby or
+        (isinstance(selects[0].orderby[0].term, Function) and
+         selects[0].orderby[0].term.name == 'FTIRANK')
+        ):
         return 0
     return 2
 
@@ -712,7 +663,7 @@
 class non_final_entity(EClassSelector):
     """Return 1 for entity of a non final entity type(s). Remember, "final"
     entity types are String, Int, etc... This is equivalent to
-    `implements('Any')` but more optimized.
+    `is_instance('Any')` but more optimized.
 
     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
     class lookup / score rules according to the input context.
@@ -726,7 +677,7 @@
         return 1 # necessarily true if we're there
 
 
-class implements(ImplementsMixIn, EClassSelector):
+class implements(EClassSelector):
     """Return non-zero score for entity that are of the given type(s) or
     implements at least one of the given interface(s). If multiple arguments are
     given, matching one of them is enough.
@@ -739,9 +690,95 @@
 
     .. note:: when interface is an entity class, the score will reflect class
               proximity so the most specific object will be selected.
+
+    .. note:: deprecated in cubicweb >= 3.9, use either
+              :class:`~cubicweb.selectors.is_instance` or
+              :class:`~cubicweb.selectors.adaptable`.
     """
+
+    def __init__(self, *expected_ifaces, **kwargs):
+        emit_warn = kwargs.pop('warn', True)
+        super(implements, self).__init__(**kwargs)
+        self.expected_ifaces = expected_ifaces
+        if emit_warn:
+            warn('[3.9] implements selector is deprecated, use either '
+                 'is_instance or adaptable', DeprecationWarning, stacklevel=2)
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                           ','.join(str(s) for s in self.expected_ifaces))
+
     def score_class(self, eclass, req):
-        return self.score_interfaces(req, eclass, eclass)
+        score = 0
+        etypesreg = req.vreg['etypes']
+        for iface in self.expected_ifaces:
+            if isinstance(iface, basestring):
+                # entity type
+                try:
+                    iface = etypesreg.etype_class(iface)
+                except KeyError:
+                    continue # entity type not in the schema
+            score += score_interface(etypesreg, eclass, iface)
+        return score
+
+def _reset_is_instance_cache(vreg):
+    vreg._is_instance_selector_cache = {}
+
+CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
+
+class is_instance(EClassSelector):
+    """Return non-zero score for entity that is an instance of the one of given
+    type(s). If multiple arguments are given, matching one of them is enough.
+
+    Entity types should be given as string, the corresponding class will be
+    fetched from the registry at selection time.
+
+    See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
+    class lookup / score rules according to the input context.
+
+    .. note:: the score will reflect class proximity so the most specific object
+              will be selected.
+    """
+
+    def __init__(self, *expected_etypes, **kwargs):
+        super(is_instance, self).__init__(**kwargs)
+        self.expected_etypes = expected_etypes
+        for etype in self.expected_etypes:
+            assert isinstance(etype, basestring), etype
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                           ','.join(str(s) for s in self.expected_etypes))
+
+    def score_class(self, eclass, req):
+        # cache on vreg to avoid reloading issues
+        cache = req.vreg._is_instance_selector_cache
+        try:
+            expected_eclasses = cache[self]
+        except KeyError:
+            # turn list of entity types as string into a list of
+            #  (entity class, parent classes)
+            etypesreg = req.vreg['etypes']
+            expected_eclasses = cache[self] = []
+            for etype in self.expected_etypes:
+                try:
+                    expected_eclasses.append(etypesreg.etype_class(etype))
+                except KeyError:
+                    continue # entity type not in the schema
+        parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
+        score = 0
+        for expectedcls in expected_eclasses:
+            # adjust score according to class proximity
+            if expectedcls is eclass:
+                score += len(parents) + 4
+            elif expectedcls is any: # Any
+                score += 1
+            else:
+                for index, basecls in enumerate(reversed(parents)):
+                    if expectedcls is basecls:
+                        score += index + 3
+                        break
+        return score
 
 
 class score_entity(EntitySelector):
@@ -765,6 +802,41 @@
             return 1
         self.score_entity = intscore
 
+class attribute_edited(EntitySelector):
+    """Scores if the specified attribute has been edited
+    This is useful for selection of forms by the edit controller.
+    The initial use case is on a form, in conjunction with match_transition,
+    which will not score at edit time::
+
+     is_instance('Version') & (match_transition('ready') |
+                               attribute_edited('publication_date'))
+    """
+    def __init__(self, attribute, once_is_enough=False):
+        super(attribute_edited, self).__init__(once_is_enough)
+        self._attribute = attribute
+
+    def score_entity(self, entity):
+        return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
+
+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
@@ -978,12 +1050,12 @@
             return self.score_entity(kwargs['entity'])
         if rset is None:
             return 0
-        user = req.user
-        action = self.action
         if row is None:
             score = 0
             need_local_check = []
             geteschema = req.vreg.schema.eschema
+            user = req.user
+            action = self.action
             for etype in rset.column_types(0):
                 if etype in BASE_TYPES:
                     return 0
@@ -1000,16 +1072,18 @@
             if need_local_check:
                 # check local role for entities of necessary types
                 for i, row in enumerate(rset):
-                    if not rset.description[i][0] in need_local_check:
+                    if not rset.description[i][col] in need_local_check:
                         continue
-                    if not self.score(req, rset, i, col):
+                    # micro-optimisation instead of calling self.score(req,
+                    # rset, i, col): rset may be large
+                    if not rset.get_entity(i, col).cw_has_perm(action):
                         return 0
                 score += 1
             return score
         return self.score(req, rset, row, col)
 
     def score_entity(self, entity):
-        if entity.has_perm(self.action):
+        if entity.cw_has_perm(self.action):
             return 1
         return 0
 
@@ -1233,18 +1307,15 @@
         return len(self.expected)
 
 
-class specified_etype_implements(implements):
+class specified_etype_implements(is_instance):
     """Return non-zero score if the entity type specified by an 'etype' key
     searched in (by priority) input context kwargs and request form parameters
     match a known entity type (case insensitivly), and it's associated entity
-    class is of one of the type(s) given to the initializer or implements at
-    least one of the given interfaces. If multiple arguments are given, matching
-    one of them is enough.
+    class is of one of the type(s) given to the initializer. If multiple
+    arguments are given, matching one of them is enough.
 
-    Entity types should be given as string, the corresponding class will be
-    fetched from the entity types registry at selection time.
-
-    .. note:: when interface is an entity class, the score will reflect class
+    .. note:: as with :class:`~cubicweb.selectors.is_instance`, entity types
+              should be given as string and the score will reflect class
               proximity so the most specific object will be selected.
 
     This selector is usually used by views holding entity creation forms (since
@@ -1280,19 +1351,13 @@
 
 
 class match_transition(ExpectedValueSelector):
-    """Return 1 if:
-
-    * a `transition` argument is found in the input context which
-      has a `.name` attribute matching one of the expected names given to the
-      initializer
-
-    * no transition specified.
+    """Return 1 if `transition` argument is found in the input context
+      which has a `.name` attribute matching one of the expected names
+      given to the initializer
     """
     @lltrace
     def __call__(self, cls, req, transition=None, **kwargs):
         # XXX check this is a transition that apply to the object?
-        if transition is None:
-            return 1
         if transition is not None and getattr(transition, 'name', None) in self.expected:
             return 1
         return 0
@@ -1300,25 +1365,30 @@
 class is_in_state(score_entity):
     """return 1 if entity is in one of the states given as argument list
 
-    you should use this instead of your own score_entity x: x.state == 'bla'
-    selector to avoid some gotchas:
+    you should use this instead of your own :class:`score_entity` selector to
+    avoid some gotchas:
 
     * possible views gives a fake entity with no state
-    * you must use the latest tr info, not entity.state for repository side
+    * you must use the latest tr info, not entity.in_state for repository side
       checking of the current state
     """
     def __init__(self, *states):
         def score(entity, states=set(states)):
+            trinfo = entity.cw_adapt_to('IWorkflowable').latest_trinfo()
             try:
-                return entity.latest_trinfo().new_state.name in states
+                return trinfo.new_state.name in states
             except AttributeError:
                 return None
         super(is_in_state, self).__init__(score)
 
+@objectify_selector
+def debug_mode(cls, req, rset=None, **kwargs):
+    """Return 1 if running in debug mode"""
+    return req.vreg.config.debugmode and 1 or 0
 
 ## deprecated stuff ############################################################
 
-entity_implements = class_renamed('entity_implements', implements)
+entity_implements = class_renamed('entity_implements', is_instance)
 
 class _but_etype(EntitySelector):
     """accept if the given entity types are not found in the result set.
@@ -1336,7 +1406,7 @@
             return 0
         return 1
 
-but_etype = class_renamed('but_etype', _but_etype, 'use ~implements(*etypes) instead')
+but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
 
 
 # XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
--- a/server/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -131,11 +131,9 @@
     config.creating = True
     config.consider_user_state = False
     config.set_language = False
-    # only enable the system source at initialization time + admin which is not
-    # an actual source but contains initial manager account information
-    config.enabled_sources = ('system', 'admin')
+    # only enable the system source at initialization time
+    config.enabled_sources = ('system',)
     repo = Repository(config, vreg=vreg)
-    assert len(repo.sources) == 1, repo.sources
     schema = repo.schema
     sourcescfg = config.sources()
     _title = '-> creating tables '
--- a/server/checkintegrity.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/checkintegrity.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,10 +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/>.
-"""Check integrity of a CubicWeb repository. Hum actually only the system database
-is checked.
+"""Integrity checking tool for instances:
 
+* integrity of a CubicWeb repository. Hum actually only the system database is
+  checked.
+
+* consistency of multi-sources instance mapping file
 """
+
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
@@ -28,7 +32,7 @@
 
 from logilab.common.shellutils import ProgressBar
 
-from cubicweb.schema import PURE_VIRTUAL_RTYPES
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.session import security_enabled
 
@@ -99,8 +103,6 @@
         print 'no text index table'
         dbhelper.init_fti(cursor)
     repo.system_source.do_fti = True  # ensure full-text indexation is activated
-    if withpb:
-        pb = ProgressBar(len(etypes) + 1)
     if etypes is None:
         print 'Reindexing entities'
         etypes = set()
@@ -123,6 +125,7 @@
                                dbhelper.fti_table, dbhelper.fti_uid_attr,
                                ','.join("'%s'" % etype for etype in etypes)))
     if withpb:
+        pb = ProgressBar(len(etypes) + 1)
         pb.update()
     # reindex entities by generating rql queries which set all indexable
     # attribute to their current value
@@ -237,7 +240,12 @@
                                 table, column, column, eid)
                             session.system_sql(sql)
             continue
-        cursor = session.system_sql('SELECT eid_from FROM %s_relation;' % rschema)
+        try:
+            cursor = session.system_sql('SELECT eid_from FROM %s_relation;' % rschema)
+        except Exception, ex:
+            # usually because table doesn't exist
+            print 'ERROR', ex
+            continue
         for row in cursor.fetchall():
             eid = row[0]
             if not has_eid(session, cursor, eid, eids):
@@ -326,3 +334,98 @@
         session.set_pool()
         reindex_entities(repo.schema, session, withpb=withpb)
         cnx.commit()
+
+
+def warning(msg, *args):
+    if args:
+        msg = msg % args
+    print 'WARNING: %s' % msg
+
+def error(msg, *args):
+    if args:
+        msg = msg % args
+    print 'ERROR: %s' % msg
+
+def check_mapping(schema, mapping, warning=warning, error=error):
+    # first check stuff found in mapping file exists in the schema
+    for attr in ('support_entities', 'support_relations'):
+        for ertype in mapping[attr].keys():
+            try:
+                mapping[attr][ertype] = erschema = schema[ertype]
+            except KeyError:
+                error('reference to unknown type %s in %s', ertype, attr)
+                del mapping[attr][ertype]
+            else:
+                if erschema.final or erschema in META_RTYPES:
+                    error('type %s should not be mapped in %s', ertype, attr)
+                    del mapping[attr][ertype]
+    for attr in ('dont_cross_relations', 'cross_relations'):
+        for rtype in list(mapping[attr]):
+            try:
+                rschema = schema.rschema(rtype)
+            except KeyError:
+                error('reference to unknown relation type %s in %s', rtype, attr)
+                mapping[attr].remove(rtype)
+            else:
+                if rschema.final or rschema in VIRTUAL_RTYPES:
+                    error('relation type %s should not be mapped in %s',
+                          rtype, attr)
+                    mapping[attr].remove(rtype)
+    # check relation in dont_cross_relations aren't in support_relations
+    for rschema in mapping['dont_cross_relations']:
+        if rschema in mapping['support_relations']:
+            warning('relation %s is in dont_cross_relations and in support_relations',
+                    rschema)
+    # check relation in cross_relations are in support_relations
+    for rschema in mapping['cross_relations']:
+        if rschema not in mapping['support_relations']:
+            warning('relation %s is in cross_relations but not in support_relations',
+                    rschema)
+    # check for relation in both cross_relations and dont_cross_relations
+    for rschema in mapping['cross_relations'] & mapping['dont_cross_relations']:
+        error('relation %s is in both cross_relations and dont_cross_relations',
+              rschema)
+    # now check for more handy things
+    seen = set()
+    for eschema in mapping['support_entities'].values():
+        for rschema, ttypes, role in eschema.relation_definitions():
+            if rschema in META_RTYPES:
+                continue
+            ttypes = [ttype for ttype in ttypes if ttype in mapping['support_entities']]
+            if not rschema in mapping['support_relations']:
+                somethingprinted = False
+                for ttype in ttypes:
+                    rdef = rschema.role_rdef(eschema, ttype, role)
+                    seen.add(rdef)
+                    if rdef.role_cardinality(role) in '1+':
+                        error('relation %s with %s as %s and target type %s is '
+                              'mandatory but not supported',
+                              rschema, eschema, role, ttype)
+                        somethingprinted = True
+                    elif ttype in mapping['support_entities']:
+                        if rdef not in seen:
+                            warning('%s could be supported', rdef)
+                        somethingprinted = True
+                if rschema not in mapping['dont_cross_relations']:
+                    if role == 'subject' and rschema.inlined:
+                        error('inlined relation %s of %s should be supported',
+                              rschema, eschema)
+                    elif not somethingprinted and rschema not in seen:
+                        print 'you may want to specify something for %s' % rschema
+                        seen.add(rschema)
+            else:
+                if not ttypes:
+                    warning('relation %s with %s as %s is supported but no target '
+                            'type supported', rschema, role, eschema)
+                if rschema in mapping['cross_relations'] and rschema.inlined:
+                    error('you should unline relation %s which is supported and '
+                          'may be crossed ', rschema)
+    for rschema in mapping['support_relations'].values():
+        if rschema in META_RTYPES:
+            continue
+        for subj, obj in rschema.rdefs:
+            if subj in mapping['support_entities'] and obj in mapping['support_entities']:
+                break
+        else:
+            error('relation %s is supported but none if its definitions '
+                  'matches supported entities', rschema)
--- a/server/hook.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/hook.py	Wed Nov 03 16:38:28 2010 +0100
@@ -57,13 +57,13 @@
 from itertools import chain
 
 from logilab.common.decorators import classproperty
-from logilab.common.deprecation import deprecated
+from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.logging_ext import set_log_methods
 
 from cubicweb import RegistryNotFound
 from cubicweb.cwvreg import CWRegistry, VRegistry
 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
-                                implements)
+                                is_instance)
 from cubicweb.appobject import AppObject
 from cubicweb.server.session import security_enabled
 
@@ -246,7 +246,7 @@
                 if ertype.islower():
                     rtypes.append(ertype)
                 else:
-                    cls.__select__ = cls.__select__ & implements(ertype)
+                    cls.__select__ = cls.__select__ & is_instance(ertype)
             if rtypes:
                 cls.__select__ = cls.__select__ & match_rtype(*rtypes)
         return cls
@@ -262,7 +262,7 @@
     def __call__(self):
         if hasattr(self, 'call'):
             cls = self.__class__
-            warn('[3.6] %s.%s: call is deprecated, implements __call__'
+            warn('[3.6] %s.%s: call is deprecated, implement __call__'
                  % (cls.__module__, cls.__name__), DeprecationWarning)
             if self.event.endswith('_relation'):
                 self.call(self._cw, self.eidfrom, self.rtype, self.eidto)
@@ -278,15 +278,21 @@
 set_log_methods(Hook, getLogger('cubicweb.hook'))
 
 
-# base classes for relation propagation ########################################
+# abtract hooks for relation propagation #######################################
+# See example usage in hooks of the nosylist cube
 
-class PropagateSubjectRelationHook(Hook):
+class PropagateRelationHook(Hook):
     """propagate some `main_rtype` relation on entities linked as object of
     `subject_relations` or as subject of `object_relations` (the watched
     relations).
 
     This hook ensure that when one of the watched relation is added, the
     `main_rtype` relation is added to the target entity of the relation.
+    Notice there are no default behaviour defined when a watched relation is
+    deleted, you'll have to handle this by yourself.
+
+    You usually want to use the :class:`match_rtype_sets` selector on concret
+    classes.
     """
     events = ('after_add_relation',)
 
@@ -312,56 +318,77 @@
             {'x': meid, 'e': seid})
 
 
-class PropagateSubjectRelationAddHook(Hook):
-    """propagate to entities at the end of watched relations when a `main_rtype`
-    relation is added
+class PropagateRelationAddHook(Hook):
+    """Propagate to entities at the end of watched relations when a `main_rtype`
+    relation is added.
+
+    `subject_relations` and `object_relations` attributes should be specified on
+    subclasses and are usually shared references with attributes of the same
+    name on :class:`PropagateRelationHook`.
+
+    Because of those shared references, you can use `skip_subject_relations` and
+    `skip_object_relations` attributes when you don't want to propagate to
+    entities linked through some particular relations.
     """
     events = ('after_add_relation',)
 
-    # to set in concrete class
+    # to set in concrete class (mandatory)
     subject_relations = None
     object_relations = None
+    # to set in concrete class (optionaly)
+    skip_subject_relations = ()
+    skip_object_relations = ()
 
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
         execute = self._cw.execute
         for rel in self.subject_relations:
-            if rel in eschema.subjrels:
+            if rel in eschema.subjrels and not rel in self.skip_subject_relations:
                 execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'X %s R, NOT R %s P' % (self.rtype, rel, self.rtype),
                         {'x': self.eidfrom, 'p': self.eidto})
         for rel in self.object_relations:
-            if rel in eschema.objrels:
+            if rel in eschema.objrels and not rel in self.skip_object_relations:
                 execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'R %s X, NOT R %s P' % (self.rtype, rel, self.rtype),
                         {'x': self.eidfrom, 'p': self.eidto})
 
 
-class PropagateSubjectRelationDelHook(Hook):
-    """propagate to entities at the end of watched relations when a `main_rtype`
-    relation is deleted
+class PropagateRelationDelHook(PropagateRelationAddHook):
+    """Propagate to entities at the end of watched relations when a `main_rtype`
+    relation is deleted.
+
+    This is the opposite of the :class:`PropagateRelationAddHook`, see its
+    documentation for how to use this class.
     """
     events = ('after_delete_relation',)
 
-    # to set in concrete class
-    subject_relations = None
-    object_relations = None
-
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
         execute = self._cw.execute
         for rel in self.subject_relations:
-            if rel in eschema.subjrels:
+            if rel in eschema.subjrels and not rel in self.skip_subject_relations:
                 execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'X %s R' % (self.rtype, rel),
                         {'x': self.eidfrom, 'p': self.eidto})
         for rel in self.object_relations:
-            if rel in eschema.objrels:
+            if rel in eschema.objrels and not rel in self.skip_object_relations:
                 execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'R %s X' % (self.rtype, rel),
                         {'x': self.eidfrom, 'p': self.eidto})
 
 
+PropagateSubjectRelationHook = class_renamed(
+    'PropagateSubjectRelationHook', PropagateRelationHook,
+    '[3.9] PropagateSubjectRelationHook has been renamed to PropagateRelationHook')
+PropagateSubjectRelationAddHook = class_renamed(
+    'PropagateSubjectRelationAddHook', PropagateRelationAddHook,
+    '[3.9] PropagateSubjectRelationAddHook has been renamed to PropagateRelationAddHook')
+PropagateSubjectRelationDelHook = class_renamed(
+    'PropagateSubjectRelationDelHook', PropagateRelationDelHook,
+    '[3.9] PropagateSubjectRelationDelHook has been renamed to PropagateRelationDelHook')
+
+
 # abstract classes for operation ###############################################
 
 class Operation(object):
--- a/server/migractions.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/migractions.py	Wed Nov 03 16:38:28 2010 +0100
@@ -44,17 +44,20 @@
 
 from logilab.common.deprecation import deprecated
 from logilab.common.decorators import cached, clear_cache
-from logilab.common.testlib import mock_object
 
 from yams.constraints import SizeConstraint
 from yams.schema2sql import eschema2sql, rschema2sql
 
-from cubicweb import AuthenticationError
-from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES,
+from cubicweb import AuthenticationError, ExecutionError
+from cubicweb.selectors import is_instance
+from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES,
+                             PURE_VIRTUAL_RTYPES,
                              CubicWebRelationSchema, order_eschemas)
+from cubicweb.cwvreg import CW_EVENT_MANAGER
 from cubicweb.dbapi import get_repository, repo_connect
 from cubicweb.migration import MigrationHelper, yes
 from cubicweb.server.session import hooks_control
+from cubicweb.server import hook
 try:
     from cubicweb.server import SOURCE_TYPES, schemaserial as ss
     from cubicweb.server.utils import manager_userpasswd, ask_source_config
@@ -63,6 +66,24 @@
     pass
 
 
+def mock_object(**params):
+    return type('Mock', (), params)()
+
+class ClearGroupMap(hook.Hook):
+    __regid__ = 'cw.migration.clear_group_mapping'
+    __select__ = hook.Hook.__select__ & is_instance('CWGroup')
+    events = ('after_add_entity', 'after_update_entity',)
+    def __call__(self):
+        clear_cache(self.mih, 'group_mapping')
+        self.mih._synchronized.clear()
+
+    @classmethod
+    def mih_register(cls, repo):
+        # may be already registered in tests (e.g. unittest_migractions at
+        # least)
+        if not cls.__regid__ in repo.vreg['after_add_entity_hooks']:
+            repo.vreg.register(ClearGroupMap)
+
 class ServerMigrationHelper(MigrationHelper):
     """specific migration helper for server side  migration scripts,
     providind actions related to schema/data migration
@@ -82,8 +103,17 @@
             self.repo_connect()
         # no config on shell to a remote instance
         if config is not None and (cnx or connect):
+            repo = self.repo
             self.session.data['rebuild-infered'] = False
-            self.repo.hm.call_hooks('server_maintenance', repo=self.repo)
+            # register a hook to clear our group_mapping cache and the
+            # self._synchronized set when some group is added or updated
+            ClearGroupMap.mih = self
+            ClearGroupMap.mih_register(repo)
+            CW_EVENT_MANAGER.bind('after-registry-reload',
+                                  ClearGroupMap.mih_register, repo)
+            # notify we're starting maintenance (called instead of server_start
+            # which is called on regular start
+            repo.hm.call_hooks('server_maintenance', repo=repo)
         if not schema and not getattr(config, 'quick_start', False):
             schema = config.load_schema(expand_cubes=True)
         self.fs_schema = schema
@@ -116,27 +146,18 @@
             super(ServerMigrationHelper, self).migrate(vcconf, toupgrade, options)
 
     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
-        """
         try:
-            if migrscript.endswith('.sql'):
-                if self.execscript_confirm(migrscript):
-                    sqlexec(open(migrscript).read(), self.session.system_sql)
-            elif migrscript.endswith('.py') or migrscript.endswith('.txt'):
-                return super(ServerMigrationHelper, self).cmd_process_script(
-                    migrscript, funcname, *args, **kwargs)
-            else:
-                print >> sys.stderr
-                print >> sys.stderr, ('-> ignoring %s, only .py .sql and .txt scripts are considered' %
-                       migrscript)
-                print >> sys.stderr
-            self.commit()
+            return super(ServerMigrationHelper, self).cmd_process_script(
+                  migrscript, funcname, *args, **kwargs)
+        except ExecutionError, err:
+            print >> sys.stderr, "-> %s" % err
         except:
             self.rollback()
             raise
 
+    # Adjust docstring
+    cmd_process_script.__doc__ = MigrationHelper.cmd_process_script.__doc__
+
     # server specific migration methods ########################################
 
     def backup_database(self, backupfile=None, askconfirm=True):
@@ -187,7 +208,7 @@
                          askconfirm=True):
         # check
         if not osp.exists(backupfile):
-            raise Exception("Backup file %s doesn't exist" % backupfile)
+            raise ExecutionError("Backup file %s doesn't exist" % backupfile)
         if askconfirm and not self.confirm('Restore %s database from %s ?'
                                            % (self.config.appid, backupfile)):
             return
@@ -201,7 +222,7 @@
         else:
             for name in bkup.getnames():
                 if name[0] in '/.':
-                    raise Exception('Security check failed, path starts with "/" or "."')
+                    raise ExecutionError('Security check failed, path starts with "/" or "."')
             bkup.close() # XXX seek error if not close+open !?!
             bkup = tarfile.open(backupfile, 'r|gz')
             bkup.extractall(path=tmpdir)
@@ -280,7 +301,7 @@
         if self.session:
             self.session.set_pool()
 
-    def rqlexecall(self, rqliter, ask_confirm=True):
+    def rqlexecall(self, rqliter, ask_confirm=False):
         for rql, kwargs in rqliter:
             self.rqlexec(rql, kwargs, ask_confirm=ask_confirm)
 
@@ -386,9 +407,13 @@
             for gname in newgroups:
                 if not confirm or self.confirm('Grant %s permission of %s to %s?'
                                                % (action, erschema, gname)):
-                    self.rqlexec('SET T %s G WHERE G eid %%(x)s, T eid %s'
-                                 % (perm, teid),
-                                 {'x': gm[gname]}, ask_confirm=False)
+                    try:
+                        self.rqlexec('SET T %s G WHERE G eid %%(x)s, T eid %s'
+                                     % (perm, teid),
+                                     {'x': gm[gname]}, ask_confirm=False)
+                    except KeyError:
+                        self.error('can grant %s perm to unexistant group %s',
+                                   action, gname)
             # handle rql expressions
             newexprs = dict((expr.expression, expr) for expr in erschema.get_rqlexprs(action))
             for expreid, expression in self.rqlexec('Any E, EX WHERE T %s E, E expression EX, '
@@ -455,6 +480,7 @@
         * description
         * internationalizable, fulltextindexed, indexed, meta
         * relations from/to this entity
+        * __unique_together__
         * permissions if `syncperms`
         """
         etype = str(etype)
@@ -502,6 +528,44 @@
                             continue
                         self._synchronize_rdef_schema(subj, rschema, obj,
                                                       syncprops=syncprops, syncperms=syncperms)
+        if syncprops: # need to process __unique_together__ after rdefs were processed
+            repo_unique_together = set([frozenset(ut)
+                                        for ut in repoeschema._unique_together])
+            unique_together = set([frozenset(ut)
+                                   for ut in eschema._unique_together])
+            for ut in repo_unique_together - unique_together:
+                restrictions  = ', '.join(['C relations R%(i)d, '
+                                           'R%(i)d relation_type T%(i)d, '
+                                           'R%(i)d from_entity X, '
+                                           'T%(i)d name %%(T%(i)d)s' % {'i': i,
+                                                                        'col':col}
+                                           for (i, col) in enumerate(ut)])
+                substs = {'etype': etype}
+                for i, col in enumerate(ut):
+                    substs['T%d'%i] = col
+                self.rqlexec('DELETE CWUniqueTogetherConstraint C '
+                             'WHERE C constraint_of E, '
+                             '      E name %%(etype)s,'
+                             '      %s' % restrictions,
+                             substs)
+            for ut in unique_together - repo_unique_together:
+                relations = ', '.join(['C relations R%d' % i
+                                       for (i, col) in enumerate(ut)])
+                restrictions  = ', '.join(['R%(i)d relation_type T%(i)d, '
+                                           'R%(i)d from_entity E, '
+                                           'T%(i)d name %%(T%(i)d)s' % {'i': i,
+                                                                        'col':col}
+                                           for (i, col) in enumerate(ut)])
+                substs = {'etype': etype}
+                for i, col in enumerate(ut):
+                    substs['T%d'%i] = col
+                self.rqlexec('INSERT CWUniqueTogetherConstraint C:'
+                             '       C constraint_of E, '
+                             '       %s '
+                             'WHERE '
+                             '      E name %%(etype)s,'
+                             '      %s' % (relations, restrictions),
+                             substs)
 
     def _synchronize_rdef_schema(self, subjtype, rtype, objtype,
                                  syncperms=True, syncprops=True):
@@ -596,7 +660,8 @@
         newcubes_schema = self.config.load_schema(construction_mode='non-strict')
         # XXX we have to replace fs_schema, used in cmd_add_relation_type
         # etc. and fsschema of migration script contexts
-        self.fs_schema = self._create_context()['fsschema'] = newcubes_schema
+        self.fs_schema = newcubes_schema
+        self.update_context('fsschema', self.fs_schema)
         new = set()
         # execute pre-create files
         driver = self.repo.system_source.dbdriver
@@ -714,13 +779,8 @@
         targeted type is known
         """
         instschema = self.repo.schema
-        assert not etype in instschema
-        #     # XXX (syt) plz explain: if we're adding an entity type, it should
-        #     # not be there...
-        #     eschema = instschema[etype]
-        #     if eschema.final:
-        #         instschema.del_entity_type(etype)
-        # else:
+        assert not etype in instschema, \
+               '%s already defined in the instance schema' % etype
         eschema = self.fs_schema.eschema(etype)
         confirm = self.verbosity >= 2
         groupmap = self.group_mapping()
@@ -734,8 +794,8 @@
             try:
                 specialized.eid = instschema[specialized].eid
             except KeyError:
-                raise Exception('trying to add entity type but parent type is '
-                                'not yet in the database schema')
+                raise ExecutionError('trying to add entity type but parent type is '
+                                     'not yet in the database schema')
             self.rqlexecall(ss.eschemaspecialize2rql(eschema), ask_confirm=confirm)
         # register entity's attributes
         for rschema, attrschema in eschema.attribute_definitions():
@@ -849,15 +909,96 @@
         if commit:
             self.commit()
 
-    def cmd_rename_entity_type(self, oldname, newname, commit=True):
+    def cmd_rename_entity_type(self, oldname, newname, attrs=None, commit=True):
         """rename an existing entity type in the persistent schema
 
         `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 mapped to %s in ETYPE_NAME_MAP' % (oldname,
+                                                                    newname)
+            if attrs is None:
+                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)
+            else:
+                attrs += ('eid', 'creation_date', 'modification_date', 'cwuri')
+                attrs = ','.join(SQL_PREFIX + attr for attr in attrs)
+            self.sqlexec('INSERT INTO %s%s(%s) SELECT %s FROM %s%s' % (
+                SQL_PREFIX, newname, attrs, attrs, SQL_PREFIX, oldname),
+                         ask_confirm=False)
+            # old entity type has not been added to the schema, can't gather it
+            new = schema.eschema(newname)
+            oldeid = self.rqlexec('CWEType ET WHERE ET name %(on)s',
+                                  {'on': oldname}, ask_confirm=False)[0][0]
+            # backport old type relations to new type
+            # XXX workflows, other relations?
+            for r1, rr1 in [('from_entity', 'to_entity'),
+                            ('to_entity', 'from_entity')]:
+                self.rqlexec('SET X %(r1)s NET WHERE X %(r1)s OET, '
+                             'NOT EXISTS(X2 %(r1)s NET, X relation_type XRT, '
+                             'X2 relation_type XRT, X %(rr1)s XTE, X2 %(rr1)s XTE), '
+                             'OET eid %%(o)s, NET eid %%(n)s' % locals(),
+                             {'o': oldeid, 'n': new.eid}, ask_confirm=False)
+            # backport is / is_instance_of relation to new type
+            for rtype in ('is', 'is_instance_of'):
+                self.sqlexec('UPDATE %s_relation SET eid_to=%s WHERE eid_to=%s'
+                             % (rtype, new.eid, oldeid), ask_confirm=False)
+            # delete relations using SQL to avoid relations content removal
+            # triggered by schema synchronization hooks.
+            session = self.session
+            for rdeftype in ('CWRelation', 'CWAttribute'):
+                thispending = set()
+                for eid, in self.sqlexec('SELECT cw_eid FROM cw_%s '
+                                         'WHERE cw_from_entity=%%(eid)s OR '
+                                         ' cw_to_entity=%%(eid)s' % rdeftype,
+                                         {'eid': oldeid}, ask_confirm=False):
+                    # we should add deleted eids into pending eids else we may
+                    # get some validation error on commit since integrity hooks
+                    # may think some required relation is missing... This also ensure
+                    # repository caches are properly cleanup
+                    hook.set_operation(session, 'pendingeids', eid,
+                                       hook.CleanupDeletedEidsCacheOp)
+                    # and don't forget to remove record from system tables
+                    self.repo.system_source.delete_info(
+                        session, session.entity_from_eid(eid, rdeftype),
+                        'system', None)
+                    thispending.add(eid)
+                self.sqlexec('DELETE FROM cw_%s '
+                             'WHERE cw_from_entity=%%(eid)s OR '
+                             'cw_to_entity=%%(eid)s' % rdeftype,
+                             {'eid': oldeid}, ask_confirm=False)
+                # now we have to manually cleanup relations pointing to deleted
+                # entities
+                thiseids = ','.join(str(eid) for eid in thispending)
+                for rschema, ttypes, role in schema[rdeftype].relation_definitions():
+                    if rschema.type in VIRTUAL_RTYPES:
+                        continue
+                    sqls = []
+                    if role == 'object':
+                        if rschema.inlined:
+                            for eschema in ttypes:
+                                sqls.append('DELETE FROM cw_%s WHERE cw_%s IN(%%s)'
+                                            % (eschema, rschema))
+                        else:
+                            sqls.append('DELETE FROM %s_relation WHERE eid_to IN(%%s)'
+                                        % rschema)
+                    elif not rschema.inlined:
+                        sqls.append('DELETE FROM %s_relation WHERE eid_from IN(%%s)'
+                                    % rschema)
+                    for sql in sqls:
+                        self.sqlexec(sql % thiseids, ask_confirm=False)
+            # remove the old type: use rql to propagate deletion
+            self.rqlexec('DELETE CWEType ET WHERE ET name %(on)s', {'on': oldname},
+                         ask_confirm=False)
+        else:
+            self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(on)s',
+                         {'newname' : unicode(newname), 'on' : oldname},
+                         ask_confirm=False)
         if commit:
             self.commit()
 
@@ -882,10 +1023,15 @@
             self.commit()
             gmap = self.group_mapping()
             cmap = self.cstrtype_mapping()
+            done = set()
             for rdef in rschema.rdefs.itervalues():
                 if not (reposchema.has_entity(rdef.subject)
                         and reposchema.has_entity(rdef.object)):
                     continue
+                # symmetric relations appears twice
+                if (rdef.subject, rdef.object) in done:
+                    continue
+                done.add( (rdef.subject, rdef.object) )
                 self._set_rdef_eid(rdef)
                 ss.execschemarql(execute, rdef,
                                  ss.rdef2rql(rdef, cmap, gmap))
@@ -1152,10 +1298,10 @@
         if commit:
             self.commit()
 
-    @deprecated('[3.5] use entity.fire_transition("transition") or entity.change_state("state")',
-                stacklevel=3)
+    @deprecated('[3.5] use iworkflowable.fire_transition("transition") or '
+                'iworkflowable.change_state("state")', stacklevel=3)
     def cmd_set_state(self, eid, statename, commit=False):
-        self._cw.entity_from_eid(eid).change_state(statename)
+        self._cw.entity_from_eid(eid).cw_adapt_to('IWorkflowable').change_state(statename)
         if commit:
             self.commit()
 
@@ -1215,6 +1361,13 @@
             self.commit()
         return entity
 
+    def cmd_update_etype_fti_weight(self, etype, weight):
+        if self.repo.system_source.dbdriver == 'postgres':
+            self.sqlexec('UPDATE appears SET weight=%(weight)s '
+                         'FROM entities as X '
+                         'WHERE X.eid=appears.uid AND X.type=%(type)s',
+                         {'type': etype, 'weight': weight}, ask_confirm=False)
+
     def cmd_reindex_entities(self, etypes=None):
         """force reindexaction of entities of the given types or of all
         indexable entity types
@@ -1238,7 +1391,7 @@
                 cu = self.session.system_sql(sql, args)
             except:
                 ex = sys.exc_info()[1]
-                if self.confirm('Error: %s\nabort?' % ex):
+                if self.confirm('Error: %s\nabort?' % ex, pdb=True):
                     raise
                 return
             try:
@@ -1248,7 +1401,7 @@
                 return
 
     def rqlexec(self, rql, kwargs=None, cachekey=None, build_descr=True,
-                ask_confirm=True):
+                ask_confirm=False):
         """rql action"""
         if cachekey is not None:
             warn('[3.8] cachekey is deprecated, you can safely remove this argument',
@@ -1266,7 +1419,7 @@
                 try:
                     res = execute(rql, kwargs, build_descr=build_descr)
                 except Exception, ex:
-                    if self.confirm('Error: %s\nabort?' % ex):
+                    if self.confirm('Error: %s\nabort?' % ex, pdb=True):
                         raise
         return res
 
@@ -1344,9 +1497,7 @@
     def __iter__(self):
         return self
 
-    def next(self):
-        if self._rsetit is not None:
-            return self._rsetit.next()
+    def _get_rset(self):
         rql, kwargs = self.rql, self.kwargs
         if kwargs:
             msg = '%s (%s)' % (rql, kwargs)
@@ -1356,11 +1507,23 @@
             if not self._h.confirm('Execute rql: %s ?' % msg):
                 raise StopIteration
         try:
-            rset = self._h._cw.execute(rql, kwargs)
+            return self._h._cw.execute(rql, kwargs)
         except Exception, ex:
             if self._h.confirm('Error: %s\nabort?' % ex):
                 raise
             else:
                 raise StopIteration
+
+    def next(self):
+        if self._rsetit is not None:
+            return self._rsetit.next()
+        rset = self._get_rset()
         self._rsetit = iter(rset)
         return self._rsetit.next()
+
+    def entities(self):
+        try:
+            rset = self._get_rset()
+        except StopIteration:
+            return []
+        return rset.entities()
--- a/server/msplanner.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/msplanner.py	Wed Nov 03 16:38:28 2010 +0100
@@ -96,13 +96,13 @@
 
 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
+from cubicweb.rqlrewrite import add_types_restriction
 from cubicweb.server.utils import cleanup_solutions
-from cubicweb.server.ssplanner import (SSPlanner, OneFetchStep,
-                                       add_types_restriction)
+from cubicweb.server.ssplanner import SSPlanner, OneFetchStep
 from cubicweb.server.mssteps import *
 
 Variable._ms_table_key = lambda x: x.name
@@ -110,6 +110,11 @@
 # str() Constant.value to ensure generated table name won't be unicode
 Constant._ms_table_key = lambda x: str(x.value)
 
+Variable._ms_may_be_processed = lambda x, terms, linkedterms: any(
+    t for t in terms if t in linkedterms.get(x, ()))
+Relation._ms_may_be_processed = lambda x, terms, linkedterms: all(
+    getattr(hs, 'variable', hs) in terms for hs in x.get_variable_parts())
+
 def ms_scope(term):
     rel = None
     scope = term.scope
@@ -411,7 +416,8 @@
                 for const in vconsts:
                     self._set_source_for_term(source, const)
             elif not self._sourcesterms:
-                self._set_source_for_term(source, const)
+                for const in vconsts:
+                    self._set_source_for_term(source, const)
             elif source in self._sourcesterms:
                 source_scopes = frozenset(ms_scope(t) for t in self._sourcesterms[source])
                 for const in vconsts:
@@ -419,9 +425,9 @@
                         self._set_source_for_term(source, const)
                         # if system source is used, add every rewritten constant
                         # to its supported terms even when associated entity
-                        # doesn't actually come from it so we get a changes
-                        # that allequals will return True as expected when
-                        # computing needsplit
+                        # doesn't actually come from it so we get a changes that
+                        # allequals will return True as expected when computing
+                        # needsplit
                         # check const is used in a relation restriction
                         if const.relation() and self.system_source in sourcesterms:
                             self._set_source_for_term(self.system_source, const)
@@ -432,14 +438,16 @@
             # process non final relations only
             # note: don't try to get schema for 'is' relation (not available
             # during bootstrap)
-            if not rel.is_types_restriction() and not rschema(rel.r_type).final:
+            if not (rel.is_types_restriction() or rschema(rel.r_type).final):
                 # nothing to do if relation is not supported by multiple sources
                 # or if some source has it listed in its cross_relations
                 # attribute
                 #
                 # XXX code below don't deal if some source allow relation
                 #     crossing but not another one
-                relsources = repo.rel_type_sources(rel.r_type)
+                relsources = [s for s in repo.rel_type_sources(rel.r_type)
+                               if s is self.system_source
+                               or s in self._sourcesterms]
                 if len(relsources) < 2:
                     # filter out sources being there because they have this
                     # relation in their dont_cross_relations attribute
@@ -478,6 +486,7 @@
                     # not supported by the source, so we can stop here
                     continue
                 self._sourcesterms.setdefault(ssource, {})[rel] = set(self._solindices)
+                solindices = None
                 for term in crossvars:
                     if len(termssources[term]) == 1 and iter(termssources[term]).next()[0].uri == 'system':
                         for ov in crossvars:
@@ -485,8 +494,14 @@
                                 ssset = frozenset((ssource,))
                                 self._remove_sources(ov, termssources[ov] - ssset)
                         break
+                    if solindices is None:
+                        solindices = set(sol for s, sol in termssources[term]
+                                         if s is source)
+                    else:
+                        solindices &= set(sol for s, sol in termssources[term]
+                                          if s is source)
                 else:
-                    self._sourcesterms.setdefault(source, {})[rel] = set(self._solindices)
+                    self._sourcesterms.setdefault(source, {})[rel] = solindices
 
     def _remove_invalid_sources(self, termssources):
         """removes invalid sources from `sourcesterms` member according to
@@ -799,10 +814,13 @@
                                     rhsvar = rhs.variable
                                 except AttributeError:
                                     rhsvar = rhs
-                                if lhsvar in terms and not rhsvar in terms:
-                                    needsel.add(lhsvar.name)
-                                elif rhsvar in terms and not lhsvar in terms:
-                                    needsel.add(rhsvar.name)
+                                try:
+                                    if lhsvar in terms and not rhsvar in terms:
+                                        needsel.add(lhsvar.name)
+                                    elif rhsvar in terms and not lhsvar in terms:
+                                        needsel.add(rhsvar.name)
+                                except AttributeError:
+                                    continue # not an attribute, no selection needed
                 if final and source.uri != 'system':
                     # check rewritten constants
                     for vconsts in select.stinfo['rewritten'].itervalues():
@@ -937,13 +955,14 @@
                 exclude[vars[1]] = vars[0]
             except IndexError:
                 pass
-        accept_term = lambda x: (not any(s for s in sources if not x in sourcesterms.get(s, ()))
-                                 and any(t for t in terms if t in linkedterms.get(x, ()))
+        accept_term = lambda x: (not any(s for s in sources
+                                         if not x in sourcesterms.get(s, ()))
+                                 and x._ms_may_be_processed(terms, linkedterms)
                                  and not exclude.get(x) in terms)
         if isinstance(term, Relation) and term in cross_rels:
             cross_terms = cross_rels.pop(term)
             base_accept_term = accept_term
-            accept_term = lambda x: (base_accept_term(x) or x in cross_terms)
+            accept_term = lambda x: (accept_term(x) or x in cross_terms)
             for refed in cross_terms:
                 if not refed in candidates:
                     terms.append(refed)
@@ -954,7 +973,11 @@
             modified = False
             for term in candidates[:]:
                 if isinstance(term, Constant):
-                    if sorted(set(x[0] for x in self._term_sources(term))) != sources:
+                    termsources = set(x[0] for x in self._term_sources(term))
+                    # ensure system source is there for constant
+                    if self.system_source in sources:
+                        termsources.add(self.system_source)
+                    if sorted(termsources) != sources:
                         continue
                     terms.append(term)
                     candidates.remove(term)
@@ -1076,14 +1099,14 @@
 
         the rqlst should not be tagged at this point
         """
-        if server.DEBUG & server.DBG_MS:
-            print '-'*80
-            print 'PLANNING', rqlst
         # preprocess deals with security insertion and returns a new syntax tree
         # which have to be executed to fulfill the query: according
         # to permissions for variable's type, different rql queries may have to
         # be executed
         plan.preprocess(rqlst)
+        if server.DEBUG & server.DBG_MS:
+            print '-'*80
+            print 'PLANNING', rqlst
         ppis = [PartPlanInformation(plan, select, self.rqlhelper)
                 for select in rqlst.children]
         steps = self._union_plan(plan, ppis)
@@ -1213,11 +1236,16 @@
                     sources, terms, scope, solindices, needsel, final)
                 if final:
                     solsinputmaps = ppi.merge_input_maps(solindices)
+                    if len(solsinputmaps) > 1:
+                        refrqlst = minrqlst
                     for solindices, inputmap in solsinputmaps:
                         if inputmap is None:
                             inputmap = subinputmap
                         else:
                             inputmap.update(subinputmap)
+                        if len(solsinputmaps) > 1:
+                            minrqlst = refrqlst.copy()
+                            sources = sources[:]
                         if inputmap and len(sources) > 1:
                             sources.remove(ppi.system_source)
                             steps.append(ppi.build_final_part(minrqlst, solindices, None,
@@ -1330,6 +1358,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
@@ -1423,8 +1457,8 @@
         if not node.is_types_restriction():
             if node in self.skip and self.solindices.issubset(self.skip[node]):
                 if not self.schema.rschema(node.r_type).final:
-                    # can't really skip the relation if one variable is selected and only
-                    # referenced by this relation
+                    # can't really skip the relation if one variable is selected
+                    # and only referenced by this relation
                     for vref in node.iget_nodes(VariableRef):
                         stinfo = vref.variable.stinfo
                         if stinfo['selected'] and len(stinfo['relations']) == 1:
@@ -1435,13 +1469,14 @@
                     return None, node
             if not self._relation_supported(node):
                 raise UnsupportedBranch()
-        # don't copy type restriction unless this is the only relation for the
-        # rhs variable, else they'll be reinserted later as needed (else we may
-        # copy a type restriction while the variable is not actually used)
-        elif not any(self._relation_supported(rel)
-                     for rel in node.children[0].variable.stinfo['relations']):
-            rel, node = self.visit_default(node, newroot, terms)
-            return rel, node
+        # don't copy type restriction unless this is the only supported relation
+        # for the lhs variable, else they'll be reinserted later as needed (in
+        # other cases we may copy a type restriction while the variable is not
+        # actually used)
+        elif not (node.neged(strict=True) or
+                  any(self._relation_supported(rel)
+                      for rel in node.children[0].variable.stinfo['relations'])):
+            return self.visit_default(node, newroot, terms)
         else:
             raise UnsupportedBranch()
         rschema = self.schema.rschema(node.r_type)
@@ -1530,12 +1565,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 = []
@@ -1574,6 +1635,8 @@
                 for vref in supportedvars:
                     if not vref in newroot.get_selected_variables():
                         newroot.append_selected(VariableRef(newroot.get_variable(vref.name)))
+            elif term in self.terms:
+                newroot.append_selected(term.copy(newroot))
 
     def add_necessary_selection(self, newroot, terms):
         selected = tuple(newroot.get_selected_variables())
--- a/server/mssteps.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/mssteps.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/querier.py	Wed Nov 03 16:38:28 2010 +0100
@@ -29,10 +29,11 @@
 from logilab.common.compat import any
 from rql import RQLSyntaxError
 from rql.stmts import Union, Select
-from rql.nodes import Relation, VariableRef, Constant, SubQuery, Exists, Not
+from rql.nodes import (Relation, VariableRef, Constant, SubQuery, Function,
+                       Exists, Not)
 
-from cubicweb import Unauthorized, QueryError, UnknownEid, typed_eid
-from cubicweb import server
+from cubicweb import ValidationError, Unauthorized, QueryError, UnknownEid
+from cubicweb import server, typed_eid
 from cubicweb.rset import ResultSet
 
 from cubicweb.server.utils import cleanup_solutions
@@ -50,7 +51,8 @@
         key = term.as_string()
         value = '%s.C%s' % (table, i)
         if varmap.get(key, value) != value:
-            raise Exception('variable name conflict on %s' % key)
+            raise Exception('variable name conflict on %s: got %s / %s'
+                            % (key, value, varmap))
         varmap[key] = value
 
 # permission utilities ########################################################
@@ -294,7 +296,26 @@
                     for term in origselection:
                         newselect.append_selected(term.copy(newselect))
                     if select.orderby:
-                        newselect.set_orderby([s.copy(newselect) for s in select.orderby])
+                        sortterms = []
+                        for sortterm in select.orderby:
+                            sortterms.append(sortterm.copy(newselect))
+                            for fnode in sortterm.get_nodes(Function):
+                                if fnode.name == 'FTIRANK':
+                                    # we've to fetch the has_text relation as well
+                                    var = fnode.children[0].variable
+                                    rel = iter(var.stinfo['ftirels']).next()
+                                    assert not rel.ored(), 'unsupported'
+                                    newselect.add_restriction(rel.copy(newselect))
+                                    # remove relation from the orig select and
+                                    # cleanup variable stinfo
+                                    rel.parent.remove(rel)
+                                    var.stinfo['ftirels'].remove(rel)
+                                    var.stinfo['relations'].remove(rel)
+                                    # XXX not properly re-annotated after security insertion?
+                                    newvar = newselect.get_variable(var.name)
+                                    newvar.stinfo.setdefault('ftirels', set()).add(rel)
+                                    newvar.stinfo.setdefault('relations', set()).add(rel)
+                        newselect.set_orderby(sortterms)
                         _expand_selection(select.orderby, selected, aliases, select, newselect)
                         select.orderby = () # XXX dereference?
                     if select.groupby:
@@ -339,6 +360,7 @@
                     select.set_possible_types(localchecks[()])
                     add_types_restriction(self.schema, select)
                     add_noinvariant(noinvariant, restricted, select, nbtrees)
+                self.rqlhelper.annotate(union)
 
     def _check_permissions(self, rqlst):
         """return a dict defining "local checks", e.g. RQLExpression defined in
@@ -400,7 +422,9 @@
         # raise Unautorized exception if the user can't access to any solution
         if not newsolutions:
             raise Unauthorized('\n'.join(msgs))
-        rqlst.set_possible_types(newsolutions)
+        if msgs:
+            # (else solutions have not been modified)
+            rqlst.set_possible_types(newsolutions)
         return localchecks, restricted_vars
 
     def finalize(self, select, solutions, insertedvars):
@@ -411,6 +435,7 @@
             for sol in solutions:
                 sol[newvarname] = nvartype
         select.clean_solutions(solutions)
+        add_types_restriction(self.schema, select)
         self.rqlhelper.annotate(rqlst)
         self.preprocess(rqlst, security=False)
         return rqlst
@@ -571,6 +596,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
@@ -674,15 +701,9 @@
         # execute the plan
         try:
             results = plan.execute()
-        except Unauthorized:
-            # XXX this could be done in security's after_add_relation hooks
-            # since it's actually realy only needed there (other relations
-            # security is done *before* actual changes, and add/update entity
-            # security is done after changes but in an operation, and exception
-            # generated in operation's events properly generate a rollback on
-            # the session). Even though, this is done here for a better
-            # consistency: getting an Unauthorized exception means the
-            # transaction has been rollbacked
+        except (Unauthorized, ValidationError):
+            # getting an Unauthorized/ValidationError exception means the
+            # transaction must been rollbacked
             #
             # notes:
             # * we should not reset the pool here, since we don't want the
@@ -690,7 +711,7 @@
             # * don't rollback if we're in the commit process, will be handled
             #   by the session
             if session.commit_state is None:
-                session.rollback(reset_pool=False)
+                session.commit_state = 'uncommitable'
             raise
         # build a description for the results if necessary
         descr = ()
--- a/server/repository.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/repository.py	Wed Nov 03 16:38:28 2010 +0100
@@ -50,12 +50,12 @@
                       UnknownEid, AuthenticationError, ExecutionError,
                       ETypeNotSupportedBySources, MultiSourcesError,
                       BadConnectionId, Unauthorized, ValidationError,
-                      RepositoryError, typed_eid, onevent)
+                      RepositoryError, UniqueTogetherError, typed_eid, onevent)
 from cubicweb import cwvreg, schema, server
 from cubicweb.server import utils, hook, pool, querier, sources
 from cubicweb.server.session import Session, InternalSession, InternalManager, \
      security_enabled
-
+_ = unicode
 
 def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
     """delete existing relation when adding a new one if card is 1 or ?
@@ -81,14 +81,14 @@
     # not expected for this).  So: don't do it, we pretend to ensure repository
     # consistency.
     #
-    # XXX we don't want read permissions to be applied but we want delete
-    # permission to be checked
-    rschema = session.repo.schema.rschema(rtype)
-    if card[0] in '1?':
-        if not rschema.inlined: # inlined relations will be implicitly deleted
-            with security_enabled(session, read=False):
-                session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
-                                'NOT Y eid %%(y)s' % rtype,
+    # notes:
+    # * inlined relations will be implicitly deleted for the subject entity
+    # * we don't want read permissions to be applied but we want delete
+    #   permission to be checked
+    if card[0] in '1?' and not session.repo.schema.rschema(rtype).inlined:
+        with security_enabled(session, read=False):
+            session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
+                            'NOT Y eid %%(y)s' % rtype,
                                 {'x': eidfrom, 'y': eidto})
     if card[1] in '1?':
         with security_enabled(session, read=False):
@@ -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)
@@ -135,7 +135,8 @@
                 continue
             source = self.get_source(uri, source_config)
             self.sources_by_uri[uri] = source
-            self.sources.append(source)
+            if config.source_enabled(uri):
+                self.sources.append(source)
         self.system_source = self.sources_by_uri['system']
         # ensure system source is the first one
         self.sources.remove(self.system_source)
@@ -154,13 +155,6 @@
                 if not isinstance(session.user, InternalManager):
                     session.user.__class__ = usercls
 
-    def _bootstrap_hook_registry(self):
-        """called during bootstrap since we need the metadata hooks"""
-        hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
-        self.vreg.init_registration([hooksdirectory])
-        self.vreg.load_file(join(hooksdirectory, 'metadata.py'),
-                            'cubicweb.hooks.metadata')
-
     def open_connections_pools(self):
         config = self.config
         self._available_pools = Queue.Queue()
@@ -186,7 +180,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()
@@ -205,8 +201,8 @@
             for source in self.sources:
                 source.init()
         else:
-            # call init_creating so for instance native source can configurate
-            # tsearch according to postgres version
+            # call init_creating so that for instance native source can
+            # configurate tsearch according to postgres version
             for source in self.sources:
                 source.init_creating()
         # close initialization pool and reopen fresh ones for proper
@@ -234,13 +230,14 @@
         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)
         self.querier.set_schema(schema)
-        for source in self.sources:
+        # don't use self.sources, we may want to give schema even to disabled
+        # sources
+        for source in self.sources_by_uri.values():
             source.set_schema(schema)
         self.schema = schema
 
@@ -392,7 +389,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
 
@@ -413,6 +410,11 @@
     # public (dbapi) interface ################################################
 
     def stats(self): # XXX restrict to managers session?
+        """Return a dictionary containing some statistics about the repository
+        resources usage.
+
+        This is a public method, not requiring a session id.
+        """
         results = {}
         querier = self.querier
         source = self.system_source
@@ -435,8 +437,9 @@
         return results
 
     def get_schema(self):
-        """return the instance schema. This is a public method, not
-        requiring a session id
+        """Return the instance schema.
+
+        This is a public method, not requiring a session id.
         """
         try:
             # necessary to support pickling used by pyro
@@ -446,8 +449,9 @@
             self.schema.__hashmode__ = None
 
     def get_cubes(self):
-        """return the list of cubes used by this instance. This is a
-        public method, not requiring a session id.
+        """Return the list of cubes used by this instance.
+
+        This is a public method, not requiring a session id.
         """
         versions = self.get_versions(not (self.config.creating
                                           or self.config.repairing
@@ -457,11 +461,31 @@
         cubes.remove('cubicweb')
         return cubes
 
+    def get_option_value(self, option, foreid=None):
+        """Return the value for `option` in the configuration. If `foreid` is
+        specified, the actual repository to which this entity belongs is
+        derefenced and the option value retrieved from it.
+
+        This is a public method, not requiring a session id.
+        """
+        # XXX we may want to check we don't give sensible information
+        if foreid is None:
+            return self.config[option]
+        _, sourceuri, extid = self.type_and_source_from_eid(foreid)
+        if sourceuri == 'system':
+            return self.config[option]
+        pool = self._get_pool()
+        try:
+            return pool.connection(sourceuri).get_option_value(option, extid)
+        finally:
+            self._free_pool(pool)
+
     @cached
     def get_versions(self, checkversions=False):
-        """return the a dictionary containing cubes used by this instance
-        as key with their version as value, including cubicweb version. This is a
-        public method, not requiring a session id.
+        """Return the a dictionary containing cubes used by this instance
+        as key with their version as value, including cubicweb version.
+
+        This is a public method, not requiring a session id.
         """
         from logilab.common.changelog import Version
         vcconf = {}
@@ -491,6 +515,11 @@
 
     @cached
     def source_defs(self):
+        """Return the a dictionary containing source uris as value and a
+        dictionary describing each source as value.
+
+        This is a public method, not requiring a session id.
+        """
         sources = self.config.sources().copy()
         # remove manager information
         sources.pop('admin', None)
@@ -502,7 +531,10 @@
         return sources
 
     def properties(self):
-        """return a result set containing system wide properties"""
+        """Return a result set containing system wide properties.
+
+        This is a public method, not requiring a session id.
+        """
         session = self.internal_session()
         try:
             # don't use session.execute, we don't want rset.req set
@@ -573,7 +605,7 @@
             session.close()
         session = Session(user, self, cnxprops)
         user._cw = user.cw_rset.req = session
-        user.clear_related_cache()
+        user.cw_clear_relation_cache()
         self._sessions[session.id] = session
         self.info('opened session %s for user %s', session.id, login)
         self.hm.call_hooks('session_open', session)
@@ -932,7 +964,7 @@
             self._extid_cache[cachekey] = eid
             self._type_source_cache[eid] = (etype, source.uri, extid)
             entity = source.before_entity_insertion(session, extid, etype, eid)
-            entity.edited_attributes = set(entity)
+            entity.edited_attributes = set(entity.cw_attr_cache)
             if source.should_call_hooks:
                 self.hm.call_hooks('before_add_entity', session, entity=entity)
             # XXX call add_info with complete=False ?
@@ -1042,37 +1074,39 @@
         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)
-        source.add_entity(session, entity)
+            entity._cw_check(creation=True)
+        try:
+            source.add_entity(session, entity)
+        except UniqueTogetherError, exc:
+            etype, rtypes = exc.args
+            problems = {}
+            for col in rtypes:
+                problems[col] = _('violates unique_together constraints (%s)') % (','.join(rtypes))
+            raise ValidationError(entity.eid, problems)
         self.add_info(session, entity, source, extid, complete=False)
-        entity._is_saved = True # entity has an eid and is saved
+        entity._cw_is_saved = True # entity has an eid and is saved
         # prefill entity relation caches
         for rschema in eschema.subject_relations():
             rtype = str(rschema)
@@ -1081,15 +1115,17 @@
             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())
-        # set inline relation cache before call to after_add_entity
+            entity.cw_set_relation_cache(rtype, 'object', session.empty_rset())
+        # set inlined relation cache before call to after_add_entity
         for attr, value in relations:
             session.update_rel_cache_add(entity.eid, attr, value)
+            del_existing_rel_if_needed(session, entity.eid, attr, value)
         # trigger after_add_entity after after_add_relation
         if source.should_call_hooks:
             self.hm.call_hooks('after_add_entity', session, entity=entity)
@@ -1107,7 +1143,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)
@@ -1139,21 +1175,29 @@
                     relations.append((attr, entity[attr], previous_value))
             if source.should_call_hooks:
                 # call hooks for inlined relations
-                for attr, value, _ in relations:
+                for attr, value, _t in relations:
                     hm.call_hooks('before_add_relation', session,
                                   eidfrom=entity.eid, rtype=attr, eidto=value)
                 if not only_inline_rels:
                     hm.call_hooks('before_update_entity', session, entity=entity)
             if session.is_hook_category_activated('integrity'):
-                entity.check()
-            source.update_entity(session, entity)
+                entity._cw_check()
+            try:
+                source.update_entity(session, entity)
+            except UniqueTogetherError, exc:
+                etype, rtypes = exc.args
+                problems = {}
+                for col in rtypes:
+                    problems[col] = _('violates unique_together constraints (%s)') % (','.join(rtypes))
+                raise ValidationError(entity.eid, problems)
+
             self.system_source.update_info(session, entity, need_fti_update)
             if source.should_call_hooks:
                 if not only_inline_rels:
                     hm.call_hooks('after_update_entity', session, entity=entity)
                 for attr, value, prevvalue in relations:
                     # if the relation is already cached, update existant cache
-                    relcache = entity.relation_cached(attr, 'subject')
+                    relcache = entity.cw_relation_cached(attr, 'subject')
                     if prevvalue is not None:
                         hm.call_hooks('after_delete_relation', session,
                                       eidfrom=entity.eid, rtype=attr, eidto=prevvalue)
@@ -1163,8 +1207,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:
@@ -1226,15 +1270,17 @@
 
     def pyro_register(self, host=''):
         """register the repository as a pyro object"""
-        import tempfile
-        from logilab.common.pyro_ext import register_object, config
-        config.PYRO_STORAGE = tempfile.gettempdir() # XXX until lgc > 0.45.1 is out
-        appid = self.config['pyro-instance-id'] or self.config.appid
-        daemon = register_object(self, appid, self.config['pyro-ns-group'],
-                                 self.config['pyro-host'],
-                                 self.config['pyro-ns-host'])
-        msg = 'repository registered as a pyro object using group %s and id %s'
-        self.info(msg, self.config['pyro-ns-group'], appid)
+        from logilab.common import pyro_ext as pyro
+        config = self.config
+        appid = '%s.%s' % pyro.ns_group_and_id(
+            config['pyro-instance-id'] or config.appid,
+            config['pyro-ns-group'])
+        # ensure config['pyro-instance-id'] is a full qualified pyro name
+        config['pyro-instance-id'] = appid
+        daemon = pyro.register_object(self, appid,
+                                      daemonhost=config['pyro-host'],
+                                      nshost=config['pyro-ns-host'])
+        self.info('repository registered as a pyro object %s', appid)
         self.pyro_registered = True
         return daemon
 
@@ -1242,15 +1288,15 @@
 
     @cached
     def rel_type_sources(self, rtype):
-        return [source for source in self.sources
-                if source.support_relation(rtype)
-                or rtype in source.dont_cross_relations]
+        return tuple([source for source in self.sources
+                      if source.support_relation(rtype)
+                      or rtype in source.dont_cross_relations])
 
     @cached
     def can_cross_relation(self, rtype):
-        return [source for source in self.sources
-                if source.support_relation(rtype)
-                and rtype in source.cross_relations]
+        return tuple([source for source in self.sources
+                      if source.support_relation(rtype)
+                      and rtype in source.cross_relations])
 
     @cached
     def is_multi_sources_relation(self, rtype):
--- a/server/rqlannotation.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/rqlannotation.py	Wed Nov 03 16:38:28 2010 +0100
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Functions to add additional annotations on a rql syntax tree to ease later
 code generation.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.common.compat import any
--- a/server/schemaserial.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/schemaserial.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""functions for schema / permissions (de)serialization using RQL
+"""functions for schema / permissions (de)serialization using RQL"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import os
@@ -25,9 +24,11 @@
 
 from logilab.common.shellutils import ProgressBar
 
-from yams import schema as schemamod, buildobjs as ybo
+from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo
 
-from cubicweb.schema import CONSTRAINTS, ETYPE_NAME_MAP, VIRTUAL_RTYPES
+from cubicweb import CW_SOFTWARE_ROOT, typed_eid
+from cubicweb.schema import (CONSTRAINTS, ETYPE_NAME_MAP,
+                             VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES)
 from cubicweb.server import sqlutils
 
 def group_mapping(cursor, interactive=True):
@@ -57,10 +58,18 @@
                 if not value:
                     continue
                 try:
-                    res[group] = int(value)
+                    eid = typed_eid(value)
                 except ValueError:
                     print 'eid should be an integer'
                     continue
+                for eid_ in res.values():
+                    if eid == eid_:
+                        break
+                else:
+                    print 'eid is not a group eid'
+                    continue
+                res[name] = eid
+                break
     return res
 
 def cstrtype_mapping(cursor):
@@ -78,7 +87,7 @@
     """
     repo = session.repo
     dbhelper = repo.system_source.dbhelper
-    # 3.6 migration
+    # XXX bw compat (3.6 migration)
     sqlcu = session.pool['system']
     sqlcu.execute("SELECT * FROM cw_CWRType WHERE cw_name='symetric'")
     if sqlcu.fetchall():
@@ -86,8 +95,10 @@
                                       dbhelper.TYPE_MAPPING['Boolean'], True)
         sqlcu.execute(sql)
         sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'")
-    sidx = {}
-    permsdict = deserialize_ertype_permissions(session)
+        session.commit(False)
+    ertidx = {}
+    copiedeids = set()
+    permsidx = deserialize_ertype_permissions(session)
     schema.reading_from_database = True
     for eid, etype, desc in session.execute(
         'Any X, N, D WHERE X is CWEType, X name N, X description D',
@@ -97,20 +108,32 @@
             # just set the eid
             eschema = schema.eschema(etype)
             eschema.eid = eid
-            sidx[eid] = eschema
+            ertidx[eid] = etype
             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 copy (eg make existing
+                # instances of the old type instances of the new type)
+                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,56 +141,102 @@
                           if etype == eidetype)
             repo.clear_caches(tocleanup)
             session.commit(False)
+            if needcopy:
+                ertidx[eid] = netype
+                copiedeids.add(eid)
+                # 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)
-        sidx[eid] = eschema
-        set_perms(eschema, permsdict)
+        ertidx[eid] = etype
+        eschema = schema.add_entity_type(
+            ybo.EntityType(name=etype, description=desc, eid=eid))
+        set_perms(eschema, permsidx)
     for etype, stype in session.execute(
         'Any XN, ETN WHERE X is CWEType, X name XN, X specializes ET, ET name ETN',
         build_descr=False):
+        etype = ETYPE_NAME_MAP.get(etype, etype)
+        stype = ETYPE_NAME_MAP.get(stype, stype)
         schema.eschema(etype)._specialized_type = stype
         schema.eschema(stype)._specialized_by.append(etype)
     for eid, rtype, desc, sym, il, ftc in session.execute(
         'Any X,N,D,S,I,FTC WHERE X is CWRType, X name N, X description D, '
         'X symmetric S, X inlined I, X fulltext_container FTC', build_descr=False):
-        rtype = ybo.RelationType(name=rtype, description=desc,
-                                 symmetric=bool(sym), inlined=bool(il),
-                                 fulltext_container=ftc, eid=eid)
-        rschema = schema.add_relation_type(rtype)
-        sidx[eid] = rschema
-    cstrsdict = deserialize_rdef_constraints(session)
+        ertidx[eid] = rtype
+        rschema = schema.add_relation_type(
+            ybo.RelationType(name=rtype, description=desc,
+                             symmetric=bool(sym), inlined=bool(il),
+                             fulltext_container=ftc, eid=eid))
+    cstrsidx = deserialize_rdef_constraints(session)
+    pendingrdefs = []
+    # closure to factorize common code of attribute/relation rdef addition
+    def _add_rdef(rdefeid, seid, reid, oeid, **kwargs):
+        rdef = ybo.RelationDefinition(ertidx[seid], ertidx[reid], ertidx[oeid],
+                                      constraints=cstrsidx.get(rdefeid, ()),
+                                      eid=rdefeid, **kwargs)
+        if seid in copiedeids or oeid in copiedeids:
+            # delay addition of this rdef. We'll insert them later if needed. We
+            # have to do this because:
+            #
+            # * on etype renaming, we want relation of the old entity type being
+            #   redirected to the new type during migration
+            #
+            # * in the case of a copy, we've to take care that rdef already
+            #   existing in the schema are not overwritten by a redirected one,
+            #   since we want correct eid on them (redirected rdef will be
+            #   removed in rename_entity_type)
+            pendingrdefs.append(rdef)
+        else:
+            # add_relation_def return a RelationDefinitionSchema if it has been
+            # actually added (can be None on duplicated relation definitions,
+            # e.g. if the relation type is marked as beeing symmetric)
+            rdefs = schema.add_relation_def(rdef)
+            if rdefs is not None:
+                ertidx[rdefeid] = rdefs
+                set_perms(rdefs, permsidx)
+
     for values in session.execute(
         'Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT WHERE X is CWAttribute,'
         'X relation_type RT, X cardinality CARD, X ordernum ORD, X indexed IDX,'
         'X description DESC, X internationalizable I18N, X defaultval DFLT,'
         'X fulltextindexed FTIDX, X from_entity SE, X to_entity OE',
         build_descr=False):
-        rdefeid, seid, reid, teid, card, ord, desc, idx, ftidx, i18n, default = values
-        rdef = ybo.RelationDefinition(sidx[seid].type, sidx[reid].type, sidx[teid].type,
-                                      cardinality=card,
-                                      constraints=cstrsdict.get(rdefeid, ()),
-                                      order=ord, description=desc,
-                                      indexed=idx, fulltextindexed=ftidx,
-                                      internationalizable=i18n,
-                                      default=default, eid=rdefeid)
-        rdefs = schema.add_relation_def(rdef)
-        # rdefs can be None on duplicated relation definitions (e.g. symmetrics)
-        if rdefs is not None:
-            set_perms(rdefs, permsdict)
+        rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values
+        _add_rdef(rdefeid, seid, reid, oeid,
+                  cardinality=card, description=desc, order=ord,
+                  indexed=idx, fulltextindexed=ftidx, internationalizable=i18n,
+                  default=default)
     for values in session.execute(
         'Any X,SE,RT,OE,CARD,ORD,DESC,C WHERE X is CWRelation, X relation_type RT,'
         'X cardinality CARD, X ordernum ORD, X description DESC, '
         'X from_entity SE, X to_entity OE, X composite C', build_descr=False):
-        rdefeid, seid, reid, teid, card, ord, desc, c = values
-        rdef = ybo.RelationDefinition(sidx[seid].type, sidx[reid].type, sidx[teid].type,
-                                      constraints=cstrsdict.get(rdefeid, ()),
-                                      cardinality=card, order=ord, description=desc,
-                                      composite=c,  eid=rdefeid)
-        rdefs = schema.add_relation_def(rdef)
-        # rdefs can be None on duplicated relation definitions (e.g. symmetrics)
+        rdefeid, seid, reid, oeid, card, ord, desc, comp = values
+        _add_rdef(rdefeid, seid, reid, oeid,
+                  cardinality=card, description=desc, order=ord,
+                  composite=comp)
+    for rdef in pendingrdefs:
+        try:
+            rdefs = schema.add_relation_def(rdef)
+        except BadSchemaDefinition:
+            continue
         if rdefs is not None:
-            set_perms(rdefs, permsdict)
+            set_perms(rdefs, permsidx)
+    unique_togethers = {}
+    try:
+        rset = session.execute(
+        'Any X,E,R WHERE '
+        'X is CWUniqueTogetherConstraint, '
+        'X constraint_of E, X relations R', build_descr=False)
+    except Exception:
+        session.rollback() # first migration introducing CWUniqueTogetherConstraint cw 3.9.6
+    else:
+        for values in rset:
+            uniquecstreid, eeid, releid = values
+            eschema = schema.schema_by_eid(eeid)
+            relations = unique_togethers.setdefault(uniquecstreid, (eschema, []))
+            relations[1].append(ertidx[releid].rtype.type)
+        for eschema, unique_together in unique_togethers.itervalues():
+            eschema._unique_together.append(tuple(sorted(unique_together)))
     schema.infer_specialization_rules()
     session.commit()
     schema.reading_from_database = False
@@ -204,7 +273,7 @@
         res.setdefault(rdefeid, []).append(cstr)
     return res
 
-def set_perms(erschema, permsdict):
+def set_perms(erschema, permsidx):
     """set permissions on the given erschema according to the permission
     definition dictionary as built by deserialize_ertype_permissions for a
     given erschema's eid
@@ -212,7 +281,7 @@
     # reset erschema permissions here to avoid getting yams default anyway
     erschema.permissions = dict((action, ()) for action in erschema.ACTIONS)
     try:
-        thispermsdict = permsdict[erschema.eid]
+        thispermsdict = permsidx[erschema.eid]
     except KeyError:
         return
     for action, somethings in thispermsdict.iteritems():
@@ -281,6 +350,10 @@
                           rdef2rql(rdef, cstrtypemap, groupmap))
         if pb is not None:
             pb.update()
+    # serialize unique_together constraints
+    for eschema in eschemas:
+        for unique_together in eschema._unique_together:
+            execschemarql(execute, eschema, [uniquetogether2rql(eschema, unique_together)])
     for rql, kwargs in specialize2rql(schema):
         execute(rql, kwargs, build_descr=False)
         if pb is not None:
@@ -338,6 +411,31 @@
         values = {'x': eschema.eid, 'et': specialized_type.eid}
         yield 'SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', values
 
+def uniquetogether2rql(eschema, unique_together):
+    relations = []
+    restrictions = []
+    substs = {}
+    for i, name in enumerate(unique_together):
+        rschema = eschema.rdef(name)
+        var = 'R%d' % i
+        rtype = 'T%d' % i
+        substs[rtype] = rschema.rtype.type
+        relations.append('C relations %s' % var)
+        restrictions.append('%(var)s from_entity X, '
+                            '%(var)s relation_type %(rtype)s, '
+                            '%(rtype)s name %%(%(rtype)s)s' \
+                            % {'var': var,
+                               'rtype':rtype})
+    relations = ', '.join(relations)
+    restrictions = ', '.join(restrictions)
+    rql = ('INSERT CWUniqueTogetherConstraint C: '
+           '    C constraint_of X, %s  '
+           'WHERE '
+           '    X eid %%(x)s, %s' )
+
+    return rql % (relations, restrictions), substs
+
+
 def _ervalues(erschema):
     try:
         type_ = unicode(erschema.type)
--- a/server/server.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/server.py	Wed Nov 03 16:38:28 2010 +0100
@@ -74,10 +74,10 @@
 
 class RepositoryServer(object):
 
-    def __init__(self, config, debug=False):
+    def __init__(self, config):
         """make the repository available as a PyRO object"""
         self.config = config
-        self.repo = Repository(config, debug=debug)
+        self.repo = Repository(config)
         self.ns = None
         self.quiting = None
         # event queue
--- a/server/serverconfig.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/serverconfig.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,16 +15,15 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""server.serverconfig definition
+"""server.serverconfig definition"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from os.path import join, exists
 
 from logilab.common.configuration import REQUIRED, Method, Configuration, \
      ini_format_section
-from logilab.common.decorators import wproperty, cached, clear_cache
+from logilab.common.decorators import wproperty, cached
 
 from cubicweb.toolsutils import read_config, restrict_perms_to_user
 from cubicweb.cwconfig import CubicWebConfiguration, merge_options
@@ -46,15 +45,27 @@
     )
 
 class SourceConfiguration(Configuration):
-    def __init__(self, appid, options):
-        self.appid = appid # has to be done before super call
+    def __init__(self, appconfig, options):
+        self.appconfig = appconfig # has to be done before super call
         super(SourceConfiguration, self).__init__(options=options)
 
     # make Method('default_instance_id') usable in db option defs (in native.py)
     def default_instance_id(self):
-        return self.appid
+        return self.appconfig.appid
 
-def generate_sources_file(appid, sourcesfile, sourcescfg, keys=None):
+    def input_option(self, option, optdict, inputlevel):
+        if self['db-driver'] == 'sqlite':
+            if option in ('db-user', 'db-password'):
+                return
+            if option == 'db-name':
+                optdict = optdict.copy()
+                optdict['help'] = 'path to the sqlite database'
+                optdict['default'] = join(self.appconfig.appdatahome,
+                                          self.appconfig.appid + '.sqlite')
+        super(SourceConfiguration, self).input_option(option, optdict, inputlevel)
+
+
+def generate_sources_file(appconfig, sourcesfile, sourcescfg, keys=None):
     """serialize repository'sources configuration into a INI like file
 
     the `keys` parameter may be used to sort sections
@@ -74,7 +85,7 @@
                 options = USER_OPTIONS
             else:
                 options = SOURCE_TYPES[sconfig['adapter']].options
-            _sconfig = SourceConfiguration(appid, options=options)
+            _sconfig = SourceConfiguration(appconfig, options=options)
             for attr, val in sconfig.items():
                 if attr == 'uri':
                     continue
@@ -157,7 +168,7 @@
         ('multi-sources-etypes',
          {'type' : 'csv', 'default': (),
           'help': 'defines which entity types from this repository are used \
-by some other instances. You should set this properly so those instances to \
+by some other instances. You should set this properly for these instances to \
 detect updates / deletions.',
           'group': 'main', 'level': 3,
           }),
@@ -228,11 +239,7 @@
 
     # list of enables sources when sources restriction is necessary
     # (eg repository initialization at least)
-    _enabled_sources = None
-    @wproperty
-    def enabled_sources(self, sourceuris=None):
-        self._enabled_sources = sourceuris
-        clear_cache(self, 'sources')
+    enabled_sources = None
 
     def bootstrap_cubes(self):
         from logilab.common.textutils import splitstrip
@@ -267,18 +274,17 @@
         """return a dictionnaries containing sources definitions indexed by
         sources'uri
         """
-        allsources = self.read_sources_file()
-        if self._enabled_sources is None:
-            return allsources
-        return dict((uri, config) for uri, config in allsources.items()
-                    if uri in self._enabled_sources or uri == 'admin')
+        return self.read_sources_file()
+
+    def source_enabled(self, uri):
+        return not self.enabled_sources or uri in self.enabled_sources
 
     def write_sources_file(self, sourcescfg):
         sourcesfile = self.sources_file()
         if exists(sourcesfile):
             import shutil
             shutil.copy(sourcesfile, sourcesfile + '.bak')
-        generate_sources_file(self.appid, sourcesfile, sourcescfg,
+        generate_sources_file(self, sourcesfile, sourcescfg,
                               ['admin', 'system'])
         restrict_perms_to_user(sourcesfile)
 
@@ -326,8 +332,7 @@
             for uri in sources:
                 assert uri in known_sources, uri
             enabled_sources = sources
-        self._enabled_sources = enabled_sources
-        clear_cache(self, 'sources')
+        self.enabled_sources = enabled_sources
 
     def migration_handler(self, schema=None, interactive=True,
                           cnx=None, repo=None, connect=True, verbosity=None):
--- a/server/serverctl.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/serverctl.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-ctl commands and command handlers specific to the server.serverconfig
+"""cubicweb-ctl commands and command handlers specific to the repository"""
 
-"""
 __docformat__ = 'restructuredtext en'
 
 # *ctl module should limit the number of import to be imported as quickly as
@@ -27,11 +26,11 @@
 import os
 
 from logilab.common.configuration import Configuration
-from logilab.common.clcommands import register_commands, cmd_run, pop_arg
 from logilab.common.shellutils import ASK
 
 from cubicweb import AuthenticationError, ExecutionError, ConfigurationError
 from cubicweb.toolsutils import Command, CommandHandler, underline_title
+from cubicweb.cwctl import CWCTL
 from cubicweb.server import SOURCE_TYPES
 from cubicweb.server.serverconfig import (USER_OPTIONS, ServerConfiguration,
                                           SourceConfiguration)
@@ -43,37 +42,44 @@
     given server.serverconfig
     """
     from getpass import getpass
-    from logilab.database import get_connection
+    from logilab.database import get_connection, get_db_helper
     dbhost = source.get('db-host')
     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 not verbose or (not special_privs and source.get('db-user')):
-        user = source['db-user']
-        print 'as', user
-        if source.get('db-password'):
-            password = source['db-password']
+    dbhelper = get_db_helper(driver)
+    if verbose:
+        print '-> connecting to %s database' % driver,
+        if dbhost:
+            print '%s@%s' % (dbname, dbhost),
         else:
-            password = getpass('password: ')
+            print dbname,
+    if dbhelper.users_support:
+        if not verbose or (not special_privs and source.get('db-user')):
+            user = source['db-user']
+            if verbose:
+                print 'as', user
+            if source.get('db-password'):
+                password = source['db-password']
+            else:
+                password = getpass('password: ')
+        else:
+            print
+            if special_privs:
+                print 'WARNING'
+                print ('the user will need the following special access rights '
+                       'on the database:')
+                print special_privs
+                print
+            default_user = source.get('db-user', os.environ.get('USER', ''))
+            user = raw_input('Connect as user ? [%r]: ' % default_user)
+            user = user or default_user
+            if user == source.get('db-user') and source.get('db-password'):
+                password = source['db-password']
+            else:
+                password = getpass('password: ')
     else:
-        print
-        if special_privs:
-            print 'WARNING'
-            print 'the user will need the following special access rights on the database:'
-            print special_privs
-            print
-        default_user = source.get('db-user', os.environ.get('USER', ''))
-        user = raw_input('Connect as user ? [%r]: ' % default_user)
-        user = user or default_user
-        if user == source.get('db-user') and source.get('db-password'):
-            password = source['db-password']
-        else:
-            password = getpass('password: ')
+        user = password = None
     extra_args = source.get('db-extra-arguments')
     extra = extra_args and {'extra_args': extra_args} or {}
     cnx = get_connection(driver, dbhost, dbname, user, password=password,
@@ -152,8 +158,8 @@
     cfgname = 'repository'
 
     def bootstrap(self, cubes, inputlevel=0):
-        """create an instance by copying files from the given cube and by
-        asking information necessary to build required configuration files
+        """create an instance by copying files from the given cube and by asking
+        information necessary to build required configuration files
         """
         from cubicweb.server.utils import ask_source_config
         config = self.config
@@ -168,7 +174,7 @@
         sourcesfile = config.sources_file()
         # XXX hack to make Method('default_instance_id') usable in db option
         # defs (in native.py)
-        sconfig = SourceConfiguration(config.appid,
+        sconfig = SourceConfiguration(config,
                                       options=SOURCE_TYPES['native'].options)
         sconfig.adapter = 'native'
         sconfig.input_config(inputlevel=inputlevel)
@@ -211,7 +217,7 @@
     def postcreate(self):
         if ASK.confirm('Run db-create to create the system database ?'):
             verbosity = (self.config.mode == 'installed') and 'y' or 'n'
-            cmd_run('db-create', self.config.appid, '--verbose=%s' % verbosity)
+            CWCTL.run(['db-create', self.config.appid, '--verbose=%s' % verbosity])
         else:
             print ('-> nevermind, you can do it later with '
                    '"cubicweb-ctl db-create %s".' % self.config.appid)
@@ -228,6 +234,9 @@
         dbname = source['db-name']
         helper = get_db_helper(source['db-driver'])
         if ASK.confirm('Delete database %s ?' % dbname):
+            if source['db-driver'] == 'sqlite':
+                os.unlink(source['db-name'])
+                return
             user = source['db-user'] or None
             cnx = _db_sys_cnx(source, 'DROP DATABASE', user=user)
             cursor = cnx.cursor()
@@ -249,11 +258,12 @@
     cmdname = 'start'
     cfgname = 'repository'
 
-    def start_server(self, ctlconf, debug):
+    def start_server(self, config):
         command = ['cubicweb-ctl start-repository ']
-        if debug:
+        if config.debugmode:
             command.append('--debug')
-        command.append(self.config.appid)
+        command.append('--loglevel %s' % config['log-threshold'].lower())
+        command.append(config.appid)
         os.system(' '.join(command))
 
 
@@ -262,8 +272,7 @@
     cfgname = 'repository'
 
     def poststop(self):
-        """if pyro is enabled, ensure the repository is correctly
-        unregistered
+        """if pyro is enabled, ensure the repository is correctly unregistered
         """
         if self.config.pyro_enabled():
             from cubicweb.server.repository import pyro_unregister
@@ -272,6 +281,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').
 
@@ -285,7 +302,7 @@
     """
     name = 'db-create'
     arguments = '<instance>'
-
+    min_args = max_args = 1
     options = (
         ('create-db',
          {'short': 'c', 'type': 'yn', 'metavar': '<y or n>',
@@ -309,19 +326,18 @@
         from logilab.database import get_db_helper
         verbose = self.get('verbose')
         automatic = self.get('automatic')
-        appid = pop_arg(args, msg='No instance specified !')
+        appid = args.pop()
         config = ServerConfiguration.config_for(appid)
         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 +354,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:
@@ -363,7 +374,7 @@
         print '-> database for instance %s created and necessary extensions installed.' % appid
         print
         if automatic or ASK.confirm('Run db-init to initialize the system database ?'):
-            cmd_run('db-init', config.appid)
+            CWCTL.run(['db-init', config.appid])
         else:
             print ('-> nevermind, you can do it later with '
                    '"cubicweb-ctl db-init %s".' % config.appid)
@@ -381,7 +392,7 @@
     """
     name = 'db-init'
     arguments = '<instance>'
-
+    min_args = max_args = 1
     options = (
         ('drop',
          {'short': 'd', 'action': 'store_true',
@@ -394,7 +405,7 @@
         print '\n'+underline_title('Initializing the system database')
         from cubicweb.server import init_repository
         from logilab.database import get_connection
-        appid = pop_arg(args, msg='No instance specified !')
+        appid = args[0]
         config = ServerConfiguration.config_for(appid)
         try:
             system = config.sources()['system']
@@ -423,7 +434,7 @@
     """
     name = 'db-grant-user'
     arguments = '<instance> <user>'
-
+    min_args = max_args = 2
     options = (
         ('set-owner',
          {'short': 'o', 'type' : 'yn', 'metavar' : '<yes or no>',
@@ -434,8 +445,7 @@
     def run(self, args):
         """run the command with its specific arguments"""
         from cubicweb.server.sqlutils import sqlexec, sqlgrants
-        appid = pop_arg(args, 1, msg='No instance specified !')
-        user = pop_arg(args, msg='No user specified !')
+        appid, user = args
         config = ServerConfiguration.config_for(appid)
         source = config.sources()['system']
         set_owner = self.config.set_owner
@@ -449,7 +459,7 @@
             cnx.rollback()
             import traceback
             traceback.print_exc()
-            print '-> an error occured:', ex
+            print '-> an error occurred:', ex
         else:
             cnx.commit()
             print '-> rights granted to %s on instance %s.' % (appid, user)
@@ -467,7 +477,7 @@
     def run(self, args):
         """run the command with its specific arguments"""
         from cubicweb.server.utils import crypt_password, manager_userpasswd
-        appid = pop_arg(args, 1, msg='No instance specified !')
+        appid = args[0]
         config = ServerConfiguration.config_for(appid)
         sourcescfg = config.read_sources_file()
         try:
@@ -491,7 +501,7 @@
                                        passwdmsg='new password for %s' % adminlogin)
         try:
             cursor.execute("UPDATE cw_CWUser SET cw_upassword=%(p)s WHERE cw_login=%(l)s",
-                           {'p': crypt_password(passwd), 'l': adminlogin})
+                           {'p': buffer(crypt_password(passwd)), 'l': adminlogin})
             sconfig = Configuration(options=USER_OPTIONS)
             sconfig['login'] = adminlogin
             sconfig['password'] = passwd
@@ -501,7 +511,7 @@
             cnx.rollback()
             import traceback
             traceback.print_exc()
-            print '-> an error occured:', ex
+            print '-> an error occurred:', ex
         else:
             cnx.commit()
             print '-> password reset, sources file regenerated.'
@@ -518,27 +528,33 @@
     """
     name = 'start-repository'
     arguments = '<instance>'
-
+    min_args = max_args = 1
     options = (
         ('debug',
          {'short': 'D', 'action' : 'store_true',
           'help': 'start server in debug mode.'}),
+        ('loglevel',
+         {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
+          'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
+          'help': 'debug if -D is set, error otherwise',
+          }),
         )
 
     def run(self, args):
         from logilab.common.daemon import daemonize
+        from cubicweb.cwctl import init_cmdline_log_threshold
         from cubicweb.server.server import RepositoryServer
-        appid = pop_arg(args, msg='No instance specified !')
-        config = ServerConfiguration.config_for(appid)
-        if sys.platform == 'win32':
-            if not self.config.debug:
-                from logging import getLogger
-                logger = getLogger('cubicweb.ctl')
-                logger.info('Forcing debug mode on win32 platform')
-                self.config.debug = True
-        debug = self.config.debug
+        appid = args[0]
+        debug = self['debug']
+        if sys.platform == 'win32' and not debug:
+            from logging import getLogger
+            logger = getLogger('cubicweb.ctl')
+            logger.info('Forcing debug mode on win32 platform')
+            debug = True
+        config = ServerConfiguration.config_for(appid, debugmode=debug)
+        init_cmdline_log_threshold(config, self['loglevel'])
         # create the server
-        server = RepositoryServer(config, debug)
+        server = RepositoryServer(config)
         # ensure the directory where the pid-file should be set exists (for
         # instance /var/run/cubicweb may be deleted on computer restart)
         pidfile = config['pid-file']
@@ -581,7 +597,7 @@
     rmcmd = 'ssh -t %s "rm -f /tmp/%s"' % (host, filename)
     print rmcmd
     if os.system(rmcmd) and not ASK.confirm(
-        'An error occured while deleting remote dump at /tmp/%s. '
+        'An error occurred while deleting remote dump at /tmp/%s. '
         'Continue anyway?' % filename):
         raise ExecutionError('Error while deleting remote dump at /tmp/%s' % filename)
 
@@ -659,7 +675,7 @@
     """
     name = 'db-dump'
     arguments = '<instance>'
-
+    min_args = max_args = 1
     options = (
         ('output',
          {'short': 'o', 'type' : 'string', 'metavar' : '<file>',
@@ -674,7 +690,7 @@
         )
 
     def run(self, args):
-        appid = pop_arg(args, 1, msg='No instance specified !')
+        appid = args[0]
         if ':' in appid:
             host, appid = appid.split(':')
             _remote_dump(host, appid, self.config.output, self.config.sudo)
@@ -690,6 +706,7 @@
     """
     name = 'db-restore'
     arguments = '<instance> <backupfile>'
+    min_args = max_args = 2
 
     options = (
         ('no-drop',
@@ -707,8 +724,7 @@
         )
 
     def run(self, args):
-        appid = pop_arg(args, 1, msg='No instance specified !')
-        backupfile = pop_arg(args, msg='No backup file or timestamp specified !')
+        appid, backupfile = args
         _local_restore(appid, backupfile,
                        drop=not self.config.no_drop,
                        systemonly=not self.config.restore_all)
@@ -726,7 +742,7 @@
     """
     name = 'db-copy'
     arguments = '<src-instance> <dest-instance>'
-
+    min_args = max_args = 2
     options = (
         ('no-drop',
          {'short': 'n', 'action' : 'store_true',
@@ -748,8 +764,7 @@
 
     def run(self, args):
         import tempfile
-        srcappid = pop_arg(args, 1, msg='No source instance specified !')
-        destappid = pop_arg(args, msg='No destination instance specified !')
+        srcappid, destappid = args
         fd, output = tempfile.mkstemp()
         os.close(fd)
         if ':' in srcappid:
@@ -772,7 +787,7 @@
     """
     name = 'db-check'
     arguments = '<instance>'
-
+    min_args = max_args = 1
     options = (
         ('checks',
          {'short': 'c', 'type' : 'csv', 'metavar' : '<check list>',
@@ -803,7 +818,7 @@
 
     def run(self, args):
         from cubicweb.server.checkintegrity import check
-        appid = pop_arg(args, 1, msg='No instance specified !')
+        appid = args[0]
         config = ServerConfiguration.config_for(appid)
         config.repairing = self.config.force
         repo, cnx = repo_cnx(config)
@@ -819,12 +834,11 @@
     """
     name = 'db-rebuild-fti'
     arguments = '<instance>'
-
-    options = ()
+    min_args = max_args = 1
 
     def run(self, args):
         from cubicweb.server.checkintegrity import reindex_entities
-        appid = pop_arg(args, 1, msg='No instance specified !')
+        appid = args[0]
         config = ServerConfiguration.config_for(appid)
         repo, cnx = repo_cnx(config)
         session = repo._get_session(cnx.sessionid, setpool=True)
@@ -843,23 +857,48 @@
     """
     name = 'schema-sync'
     arguments = '<instance>'
+    min_args = max_args = 1
 
     def run(self, args):
-        appid = pop_arg(args, msg='No instance specified !')
+        appid = args[0]
         config = ServerConfiguration.config_for(appid)
         mih = config.migration_handler()
         mih.cmd_synchronize_schema()
 
 
-register_commands( (CreateInstanceDBCommand,
-                    InitInstanceCommand,
-                    GrantUserOnInstanceCommand,
-                    ResetAdminPasswordCommand,
-                    StartRepositoryCommand,
-                    DBDumpCommand,
-                    DBRestoreCommand,
-                    DBCopyCommand,
-                    CheckRepositoryCommand,
-                    RebuildFTICommand,
-                    SynchronizeInstanceSchemaCommand,
-                    ) )
+class CheckMappingCommand(Command):
+    """Check content of the mapping file of an external source.
+
+    The mapping is checked against the instance's schema, searching for
+    inconsistencies or stuff you may have forgotten. It's higly recommanded to
+    run it when you setup a multi-sources instance.
+
+    <instance>
+      the identifier of the instance.
+
+    <mapping file>
+      the mapping file to check.
+    """
+    name = 'check-mapping'
+    arguments = '<instance> <mapping file>'
+    min_args = max_args = 2
+
+    def run(self, args):
+        from cubicweb.server.checkintegrity import check_mapping
+        from cubicweb.server.sources.pyrorql import load_mapping_file
+        appid, mappingfile = args
+        config = ServerConfiguration.config_for(appid)
+        config.quick_start = True
+        mih = config.migration_handler(connect=False, verbosity=1)
+        repo = mih.repo_connect() # necessary to get cubes
+        check_mapping(config.load_schema(), load_mapping_file(mappingfile))
+
+for cmdclass in (CreateInstanceDBCommand, InitInstanceCommand,
+                 GrantUserOnInstanceCommand, ResetAdminPasswordCommand,
+                 StartRepositoryCommand,
+                 DBDumpCommand, DBRestoreCommand, DBCopyCommand,
+                 CheckRepositoryCommand, RebuildFTICommand,
+                 SynchronizeInstanceSchemaCommand,
+                 CheckMappingCommand,
+                 ):
+    CWCTL.register(cmdclass)
--- a/server/session.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/session.py	Wed Nov 03 16:38:28 2010 +0100
@@ -31,7 +31,7 @@
 from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
 from yams import BASE_TYPES
 
-from cubicweb import Binary, UnknownEid, schema
+from cubicweb import Binary, UnknownEid, QueryError, schema
 from cubicweb.req import RequestSessionBase
 from cubicweb.dbapi import ConnectionProperties
 from cubicweb.utils import make_uid, RepeatList
@@ -250,7 +250,7 @@
             entity = self.entity_cache(eid)
         except KeyError:
             return
-        rcache = entity.relation_cached(rtype, role)
+        rcache = entity.cw_relation_cached(rtype, role)
         if rcache is not None:
             rset, entities = rcache
             rset = rset.copy()
@@ -266,14 +266,15 @@
                 targetentity.cw_col = 0
             rset.rowcount += 1
             entities.append(targetentity)
-            entity._related_cache['%s_%s' % (rtype, role)] = (rset, tuple(entities))
+            entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+                rset, tuple(entities))
 
     def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid):
         try:
             entity = self.entity_cache(eid)
         except KeyError:
             return
-        rcache = entity.relation_cached(rtype, role)
+        rcache = entity.cw_relation_cached(rtype, role)
         if rcache is not None:
             rset, entities = rcache
             for idx, row in enumerate(rset.rows):
@@ -292,7 +293,8 @@
                 del rset.description[idx]
             del entities[idx]
             rset.rowcount -= 1
-            entity._related_cache['%s_%s' % (rtype, role)] = (rset, tuple(entities))
+            entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+                rset, tuple(entities))
 
     # resource accessors ######################################################
 
@@ -312,16 +314,15 @@
 
     def set_language(self, language):
         """i18n configuration for translation"""
-        vreg = self.vreg
         language = language or self.user.property_value('ui.language')
         try:
-            gettext, pgettext = vreg.config.translations[language]
+            gettext, pgettext = self.vreg.config.translations[language]
             self._ = self.__ = gettext
             self.pgettext = pgettext
         except KeyError:
-            language = vreg.property_value('ui.language')
+            language = self.vreg.property_value('ui.language')
             try:
-                gettext, pgettext = vreg.config.translations[language]
+                gettext, pgettext = self.vreg.config.translations[language]
                 self._ = self.__ = gettext
                 self.pgettext = pgettext
             except KeyError:
@@ -661,16 +662,6 @@
         else:
             del self.transaction_data['ecache'][eid]
 
-    def base_url(self):
-        url = self.repo.config['base-url']
-        if not url:
-            try:
-                url = self.repo.config.default_base_url()
-            except AttributeError: # default_base_url() might not be available
-                self.warning('missing base-url definition in server config')
-                url = u''
-        return url
-
     def from_controller(self):
         """return the id (string) of the controller issuing the request (no
         sense here, always return 'view')
@@ -735,7 +726,10 @@
             self._touch()
             self.debug('commit session %s done (no db activity)', self.id)
             return
-        if self.commit_state:
+        cstate = self.commit_state
+        if cstate == 'uncommitable':
+            raise QueryError('transaction must be rollbacked')
+        if cstate is not None:
             return
         # on rollback, an operation should have the following state
         # information:
@@ -756,7 +750,6 @@
                         self.pending_operations[:] = processed
                         self.debug('%s session %s done', trstate, self.id)
                     except:
-                        self.exception('error while %sing', trstate)
                         # if error on [pre]commit:
                         #
                         # * set .failed = True on the operation causing the failure
@@ -768,8 +761,12 @@
                         # instead of having to implements rollback, revertprecommit
                         # and revertcommit, that will be enough in mont case.
                         operation.failed = True
-                        for operation in processed:
-                            operation.handle_event('revert%s_event' % trstate)
+                        for operation in reversed(processed):
+                            try:
+                                operation.handle_event('revert%s_event' % trstate)
+                            except:
+                                self.critical('error while reverting %sing', trstate,
+                                              exc_info=True)
                         # XXX use slice notation since self.pending_operations is a
                         # read-only property.
                         self.pending_operations[:] = processed + self.pending_operations
@@ -785,7 +782,7 @@
                     except:
                         self.critical('error while %sing', trstate,
                                       exc_info=sys.exc_info())
-                self.info('%s session %s done', trstate, self.id)
+                self.debug('%s session %s done', trstate, self.id)
                 return self.transaction_uuid(set=False)
         finally:
             self._touch()
@@ -1027,7 +1024,7 @@
     def __init__(self, repo, cnxprops=None):
         super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
                                               _id='internal')
-        self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone
+        self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
         self.cnxtype = 'inmemory'
         self.disable_hook_categories('integrity')
 
--- a/server/sources/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/sources/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -182,7 +182,7 @@
             wsupport = self.support_relations[rtype]
         except KeyError:
             rschema = self.schema.rschema(rtype)
-            if not rschema.final or rschema == 'has_text':
+            if not rschema.final or rschema.type == 'has_text':
                 return False
             for etype in rschema.subjects():
                 try:
@@ -306,7 +306,7 @@
         pass
 
     def authenticate(self, session, login, **kwargs):
-        """if the source support CWUser entity type, it should implements
+        """if the source support CWUser entity type, it should implement
         this method which should return CWUser eid for the given login/password
         if this account is defined in this source and valid login / password is
         given. Else raise `AuthenticationError`
@@ -342,7 +342,7 @@
         entity.
         """
         entity = self.repo.vreg['etypes'].etype_class(etype)(session)
-        entity.set_eid(eid)
+        entity.eid = eid
         return entity
 
     def after_entity_insertion(self, session, lid, entity):
--- a/server/sources/ldapuser.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/sources/ldapuser.py	Wed Nov 03 16:38:28 2010 +0100
@@ -232,6 +232,8 @@
                 if res:
                     ldapemailaddr = res[0].get(ldap_emailattr)
                     if ldapemailaddr:
+                        if isinstance(ldapemailaddr, list):
+                            ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list
                         rset = execute('Any X,A WHERE '
                                        'X address A, U use_email X, U eid %(u)s',
                                        {'u': eid})
@@ -522,7 +524,7 @@
                              eid, base)
                 entity = session.entity_from_eid(eid, 'CWUser')
                 self.repo.delete_info(session, entity, self.uri, base)
-                self.reset_cache()
+                self.reset_caches()
             return []
         # except ldap.REFERRAL, e:
         #     cnx = self.handle_referral(e)
@@ -589,6 +591,8 @@
             emailaddr = self._cache[dn][self.user_rev_attrs['email']]
         except KeyError:
             return
+        if isinstance(emailaddr, list):
+            emailaddr = emailaddr[0] # XXX consider only the first email in the list
         rset = session.execute('EmailAddress X WHERE X address %(addr)s',
                                {'addr': emailaddr})
         if rset:
--- a/server/sources/native.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/sources/native.py	Wed Nov 03 16:38:28 2010 +0100
@@ -34,6 +34,7 @@
 from base64 import b64decode, b64encode
 from contextlib import contextmanager
 from os.path import abspath
+import re
 
 from logilab.common.compat import any
 from logilab.common.cache import Cache
@@ -42,7 +43,9 @@
 from logilab.common.shellutils import getlogin
 from logilab.database import get_db_helper
 
-from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary
+from yams import schema2sql as y2sql
+
+from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary, UniqueTogetherError
 from cubicweb import transaction as tx, server, neg_role
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
@@ -127,6 +130,25 @@
         restr = '(%s)' % ' OR '.join(clauses)
     return '%s WHERE %s' % (select, restr)
 
+def rdef_table_column(rdef):
+    """return table and column used to store the given relation definition in
+    the database
+    """
+    return (SQL_PREFIX + str(rdef.subject),
+            SQL_PREFIX + str(rdef.rtype))
+
+def rdef_physical_info(dbhelper, rdef):
+    """return backend type and a boolean flag if NULL values should be allowed
+    for a given relation definition
+    """
+    if rdef.object.final:
+        ttype = rdef.object
+    else:
+        ttype = 'Int' # eid type
+    coltype = y2sql.type_from_constraints(dbhelper, ttype,
+                                          rdef.constraints, creating=False)
+    allownull = rdef.cardinality[0] != '1'
+    return coltype, allownull
 
 class UndoException(Exception):
     """something went wrong during undoing"""
@@ -193,7 +215,7 @@
           'default': 'postgres',
           # XXX use choice type
           'help': 'database driver (postgres, mysql, sqlite, sqlserver2005)',
-          'group': 'native-source', 'level': 1,
+          'group': 'native-source', 'level': 0,
           }),
         ('db-host',
          {'type' : 'string',
@@ -488,7 +510,7 @@
     def manual_insert(self, results, table, session):
         """insert given result into a temporary table on the system source"""
         if server.DEBUG & server.DBG_RQL:
-            print '  manual insertion of', results, 'into', table
+            print '  manual insertion of', len(results), 'results into', table
         if not results:
             return
         query_args = ['%%(%s)s' % i for i in xrange(len(results[0]))]
@@ -649,6 +671,21 @@
                         self.critical('transaction has been rollbacked')
                 except:
                     pass
+            if ex.__class__.__name__ == 'IntegrityError':
+                # need string comparison because of various backends
+                for arg in ex.args:
+                    mo = re.search('unique_cw_[^ ]+_idx', arg)
+                    if mo is not None:
+                        index_name = mo.group(0)
+                        elements = index_name.rstrip('_idx').split('_cw_')[1:]
+                        etype = elements[0]
+                        rtypes = elements[1:]
+                        raise UniqueTogetherError(etype, rtypes)
+                    mo = re.search('columns (.*) are not unique', arg)
+                    if mo is not None: # sqlite in use
+                        rtypes = [c.strip().lstrip('cw_') for c in mo.group(1).split(',')]
+                        etype = '???'
+                        raise UniqueTogetherError(etype, rtypes)
             raise
         return cursor
 
@@ -678,6 +715,47 @@
 
     # short cut to method requiring advanced db helper usage ##################
 
+    def update_rdef_column(self, session, rdef):
+        """update physical column for a relation definition (final or inlined)
+        """
+        table, column = rdef_table_column(rdef)
+        coltype, allownull = rdef_physical_info(self.dbhelper, rdef)
+        if not self.dbhelper.alter_column_support:
+            self.error("backend can't alter %s.%s to %s%s", table, column, coltype,
+                       not allownull and 'NOT NULL' or '')
+            return
+        self.dbhelper.change_col_type(LogCursor(session.pool[self.uri]),
+                                      table, column, coltype, allownull)
+        self.info('altered %s.%s: now %s%s', table, column, coltype,
+                  not allownull and 'NOT NULL' or '')
+
+    def update_rdef_null_allowed(self, session, rdef):
+        """update NULL / NOT NULL of physical column for a relation definition
+        (final or inlined)
+        """
+        if not self.dbhelper.alter_column_support:
+            # not supported (and NOT NULL not set by yams in that case, so no
+            # worry)
+            return
+        table, column = rdef_table_column(rdef)
+        coltype, allownull = rdef_physical_info(self.dbhelper, rdef)
+        self.dbhelper.set_null_allowed(LogCursor(session.pool[self.uri]),
+                                       table, column, coltype, allownull)
+
+    def update_rdef_indexed(self, session, rdef):
+        table, column = rdef_table_column(rdef)
+        if rdef.indexed:
+            self.create_index(session, table, column)
+        else:
+            self.drop_index(session, table, column)
+
+    def update_rdef_unique(self, session, rdef):
+        table, column = rdef_table_column(rdef)
+        if rdef.constraint_by_type('UniqueConstraint'):
+            self.create_index(session, table, column, unique=True)
+        else:
+            self.drop_index(session, table, column, unique=True)
+
     def create_index(self, session, table, column, unique=False):
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.create_index(cursor, table, column, unique)
@@ -686,14 +764,6 @@
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.drop_index(cursor, table, column, unique)
 
-    def change_col_type(self, session, table, column, coltype, null_allowed):
-        cursor = LogCursor(session.pool[self.uri])
-        self.dbhelper.change_col_type(cursor, table, column, coltype, null_allowed)
-
-    def set_null_allowed(self, session, table, column, coltype, null_allowed):
-        cursor = LogCursor(session.pool[self.uri])
-        self.dbhelper.set_null_allowed(cursor, table, column, coltype, null_allowed)
-
     # system source interface #################################################
 
     def eid_type_source(self, session, eid):
@@ -801,7 +871,6 @@
             cnx.commit()
             return eid
 
-
     def add_info(self, session, entity, source, extid, complete):
         """add type and source info for an eid into the system table"""
         # begin by inserting eid/type/source/extid into the entities table
@@ -1079,10 +1148,10 @@
                 entity[rtype] = unicode(value, session.encoding, 'replace')
             else:
                 entity[rtype] = value
-        entity.set_eid(eid)
+        entity.eid = eid
         session.repo.init_entity_caches(session, entity, self)
         entity.edited_attributes = set(entity)
-        entity.check()
+        entity._cw_check()
         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
         # restore the entity
         action.changes['cw_eid'] = eid
@@ -1149,7 +1218,7 @@
             return [session._(
                 "Can't undo creation of entity %(eid)s of type %(etype)s, type "
                 "no more supported" % {'eid': eid, 'etype': etype})]
-        entity.set_eid(eid)
+        entity.eid = eid
         # for proper eid/type cache update
         hook.set_operation(session, 'pendingeids', eid,
                            CleanupDeletedEidsCacheOp)
@@ -1237,7 +1306,8 @@
         try:
             # use cursor_index_object, not cursor_reindex_object since
             # unindexing done in the FTIndexEntityOp
-            self.dbhelper.cursor_index_object(entity.eid, entity,
+            self.dbhelper.cursor_index_object(entity.eid,
+                                              entity.cw_adapt_to('IFTIndexable'),
                                               session.pool['system'])
         except Exception: # let KeyboardInterrupt / SystemExit propagate
             self.exception('error while reindexing %s', entity)
@@ -1262,7 +1332,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/pyrorql.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/sources/pyrorql.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""Source to query another RQL repository using pyro
+"""Source to query another RQL repository using pyro"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import threading
@@ -44,6 +43,34 @@
     select, col = union.locate_subquery(col, etype, args)
     return getattr(select.selection[col], 'uidtype', None)
 
+def load_mapping_file(mappingfile):
+    mapping = {}
+    execfile(mappingfile, mapping)
+    for junk in ('__builtins__', '__doc__'):
+        mapping.pop(junk, None)
+    mapping.setdefault('support_relations', {})
+    mapping.setdefault('dont_cross_relations', set())
+    mapping.setdefault('cross_relations', set())
+
+    # do some basic checks of the mapping content
+    assert 'support_entities' in mapping, \
+           'mapping file should at least define support_entities'
+    assert isinstance(mapping['support_entities'], dict)
+    assert isinstance(mapping['support_relations'], dict)
+    assert isinstance(mapping['dont_cross_relations'], set)
+    assert isinstance(mapping['cross_relations'], set)
+    unknown = set(mapping) - set( ('support_entities', 'support_relations',
+                                   'dont_cross_relations', 'cross_relations') )
+    assert not unknown, 'unknown mapping attribute(s): %s' % unknown
+    # relations that are necessarily not crossed
+    mapping['dont_cross_relations'] |= set(('owned_by', 'created_by'))
+    for rtype in ('is', 'is_instance_of'):
+        assert rtype not in mapping['dont_cross_relations'], \
+               '%s relation should not be in dont_cross_relations' % rtype
+        assert rtype not in mapping['support_relations'], \
+               '%s relation should not be in support_relations' % rtype
+    return mapping
+
 
 class ReplaceByInOperator(Exception):
     def __init__(self, eids):
@@ -59,8 +86,6 @@
     # migration
     connect_for_migration = False
 
-    support_entities = None
-
     options = (
         # XXX pyro-ns host/port
         ('pyro-ns-id',
@@ -127,12 +152,11 @@
         mappingfile = source_config['mapping-file']
         if not mappingfile[0] == '/':
             mappingfile = join(repo.config.apphome, mappingfile)
-        mapping = {}
-        execfile(mappingfile, mapping)
+        mapping = load_mapping_file(mappingfile)
         self.support_entities = mapping['support_entities']
-        self.support_relations = mapping.get('support_relations', {})
-        self.dont_cross_relations = mapping.get('dont_cross_relations', ())
-        self.cross_relations = mapping.get('cross_relations', ())
+        self.support_relations = mapping['support_relations']
+        self.dont_cross_relations = mapping['dont_cross_relations']
+        self.cross_relations = mapping['cross_relations']
         baseurl = source_config.get('base-url')
         if baseurl and not baseurl.endswith('/'):
             source_config['base-url'] += '/'
@@ -173,7 +197,8 @@
         """method called by the repository once ready to handle request"""
         interval = int(self.config.get('synchronization-interval', 5*60))
         self.repo.looping_task(interval, self.synchronize)
-        self.repo.looping_task(self._query_cache.ttl.seconds/10, self._query_cache.clear_expired)
+        self.repo.looping_task(self._query_cache.ttl.seconds/10,
+                               self._query_cache.clear_expired)
 
     def synchronize(self, mtime=None):
         """synchronize content known by this repository with content in the
--- a/server/sources/rql2sql.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/sources/rql2sql.py	Wed Nov 03 16:38:28 2010 +0100
@@ -101,15 +101,12 @@
     subquery. This function check this and rewrite the rql syntax tree if
     necessary (in place). Return a boolean telling if the tree has been modified
     """
-    torewrite = set()
     modified = False
     for varname in tuple(unstable):
         var = select.defined_vars[varname]
         if not var.stinfo.get('optrelations'):
             continue
-        modified = True
         unstable.remove(varname)
-        torewrite.add(var)
         newselect = Select()
         newselect.need_distinct = False
         myunion = Union()
@@ -139,10 +136,17 @@
                 var.stinfo['rhsrelations'].add(newrel)
                 if rel.optional in ('right', 'both'):
                     var.add_optional_relation(newrel)
+        if not select.where and not modified:
+            # oops, generated the same thing as the original select....
+            # restore original query, else we'll indefinitly loop
+            for var, rel in towrap_rels:
+                select.add_restriction(rel)
+            continue
+        modified = True
         # extract subquery solutions
         mysolutions = [sol.copy() for sol in solutions]
         cleanup_solutions(newselect, mysolutions)
-        newselect.set_possible_types(solutions)
+        newselect.set_possible_types(mysolutions)
         # full sub-query
         aliases = [VariableRef(select.get_variable(avar.name, i))
                    for i, avar in enumerate(newselect.selection)]
@@ -611,12 +615,14 @@
                 sql += '\nHAVING %s' % having
             # sort
             if sorts:
-                sql += '\nORDER BY %s' % ','.join(self._sortterm_sql(sortterm,
-                                                                     fselectidx)
-                                                  for sortterm in sorts)
-                if fneedwrap:
-                    selection = ['T1.C%s' % i for i in xrange(len(origselection))]
-                    sql = 'SELECT %s FROM (%s) AS T1' % (','.join(selection), sql)
+                sqlsortterms = [self._sortterm_sql(sortterm, fselectidx)
+                                for sortterm in sorts]
+                sqlsortterms = [x for x in sqlsortterms if x is not None]
+                if sqlsortterms:
+                    sql += '\nORDER BY %s' % ','.join(sqlsortterms)
+                    if sorts and fneedwrap:
+                        selection = ['T1.C%s' % i for i in xrange(len(origselection))]
+                        sql = 'SELECT %s FROM (%s) AS T1' % (','.join(selection), sql)
             state.finalize_source_cbs()
         finally:
             select.selection = origselection
@@ -696,12 +702,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
 
@@ -753,6 +761,8 @@
             restrictions.append(restriction)
         restriction = ' AND '.join(restrictions)
         if not restriction:
+            if tables:
+                return 'SELECT 1 FROM %s' % ', '.join(tables)
             return ''
         if not tables:
             # XXX could leave surrounding EXISTS() in this case no?
@@ -814,26 +824,35 @@
 
     def _visit_inlined_relation(self, relation):
         lhsvar, _, rhsvar, rhsconst = relation_info(relation)
-        # we are sure here to have a lhsvar
-        assert lhsvar is not None
-        if isinstance(relation.parent, Not) \
-               and len(lhsvar.stinfo['relations']) > 1 \
-               and (rhsvar is not None and rhsvar._q_invariant):
+        # we are sure lhsvar is not None
+        lhssql = self._inlined_var_sql(lhsvar, relation.r_type)
+        if rhsvar is None:
+            moresql = None
+        else:
+            moresql = self._extra_join_sql(relation, lhssql, rhsvar)
+        if isinstance(relation.parent, Not):
             self._state.done.add(relation.parent)
-            return '%s IS NULL' % self._inlined_var_sql(lhsvar, relation.r_type)
-        lhssql = self._inlined_var_sql(lhsvar, relation.r_type)
-        if rhsconst is not None:
-            return '%s=%s' % (lhssql, rhsconst.accept(self))
-        if isinstance(rhsvar, Variable) and not rhsvar.name in self._varmap:
+            if rhsvar is not None and rhsvar._q_invariant:
+                sql = '%s IS NULL' % lhssql
+            else:
+                # column != 1234 may not get back rows where column is NULL...
+                sql = '(%s IS NULL OR %s!=%s)' % (
+                    lhssql, lhssql, (rhsvar or rhsconst).accept(self))
+        elif rhsconst is not None:
+            sql = '%s=%s' % (lhssql, rhsconst.accept(self))
+        elif isinstance(rhsvar, Variable) and rhsvar._q_invariant and \
+                 not rhsvar.name in self._varmap:
             # if the rhs variable is only linked to this relation, this mean we
             # only want the relation to exists, eg NOT NULL in case of inlined
             # relation
-            if rhsvar._q_invariant:
-                sql = self._extra_join_sql(relation, lhssql, rhsvar)
-                if sql:
-                    return sql
-                return '%s IS NOT NULL' % lhssql
-        return '%s=%s' % (lhssql, rhsvar.accept(self))
+            if moresql is not None:
+                return moresql
+            return '%s IS NOT NULL' % lhssql
+        else:
+            sql = '%s=%s' % (lhssql, rhsvar.accept(self))
+        if moresql is None:
+            return sql
+        return '%s AND %s' % (sql, moresql)
 
     def _process_relation_term(self, relation, rid, termvar, termconst, relfield):
         if termconst or not termvar._q_invariant:
@@ -845,7 +864,7 @@
                 termsql = termvar.accept(self)
                 yield '%s.%s=%s' % (rid, relfield, termsql)
             extrajoin = self._extra_join_sql(relation, '%s.%s' % (rid, relfield), termvar)
-            if extrajoin:
+            if extrajoin is not None:
                 yield extrajoin
 
     def _visit_relation(self, relation, rschema):
@@ -1060,7 +1079,8 @@
             not_ = True
         else:
             not_ = False
-        return self.dbhelper.fti_restriction_sql(alias, const.eval(self._args),
+        query = const.eval(self._args)
+        return self.dbhelper.fti_restriction_sql(alias, query,
                                                  jointo, not_) + restriction
 
     def visit_comparison(self, cmp):
@@ -1104,6 +1124,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
@@ -1114,11 +1143,11 @@
 
     def visit_constant(self, constant):
         """generate SQL name for a constant"""
-        value = constant.value
         if constant.type is None:
             return 'NULL'
+        value = constant.value
         if constant.type == 'Int' and  isinstance(constant.parent, SortTerm):
-            return constant.value
+            return value
         if constant.type in ('Date', 'Datetime'):
             rel = constant.relation()
             if rel is not None:
@@ -1127,13 +1156,15 @@
         if constant.type == 'Boolean':
             value = self.dbhelper.boolean_value(value)
         if constant.type == 'Substitute':
-            _id = constant.value
-            if isinstance(_id, unicode):
-                _id = _id.encode()
+            try:
+                # we may found constant from simplified var in varmap
+                return self._mapped_term(constant, '%%(%s)s' % value)[0]
+            except KeyError:
+                _id = value
+                if isinstance(_id, unicode):
+                    _id = _id.encode()
         else:
             _id = str(id(constant)).replace('-', '', 1)
-            if isinstance(value, unicode):
-                value = value.encode(self.dbencoding)
             self._query_attrs[_id] = value
         return '%%(%s)s' % _id
 
@@ -1222,7 +1253,7 @@
             # no principal defined, relation is necessarily the principal and
             # so nothing to return here
             pass
-        return ''
+        return None
 
     def _temp_table_scope(self, select, table):
         scope = 9999
@@ -1237,12 +1268,19 @@
                     break
         return scope
 
+    def _mapped_term(self, term, key):
+        """return sql and table alias to the `term`, mapped as `key` or raise
+        KeyError when the key is not found in the varmap
+        """
+        sql = self._varmap[key]
+        tablealias = sql.split('.', 1)[0]
+        scope = self._temp_table_scope(term.stmt, tablealias)
+        self.add_table(tablealias, scope=scope)
+        return sql, tablealias
+
     def _var_info(self, var):
         try:
-            sql = self._varmap[var.name]
-            tablealias = sql.split('.', 1)[0]
-            scope = self._temp_table_scope(var.stmt, tablealias)
-            self.add_table(tablealias, scope=scope)
+            return self._mapped_term(var, var.name)
         except KeyError:
             scope = self._state.scopes[var.scope]
             etype = self._state.solution[var.name]
--- a/server/sources/storages.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/sources/storages.py	Wed Nov 03 16:38:28 2010 +0100
@@ -18,6 +18,7 @@
 """custom storages for the system source"""
 
 from os import unlink, path as osp
+from contextlib import contextmanager
 
 from yams.schema import role_name
 
@@ -93,6 +94,17 @@
             return path
     return None
 
+@contextmanager
+def fsimport(session):
+    present = 'fs_importing' in session.transaction_data
+    old_value = session.transaction_data.get('fs_importing')
+    session.transaction_data['fs_importing'] = True
+    yield
+    if present:
+        session.transaction_data['fs_importing'] = old_value
+    else:
+        del session.transaction_data['fs_importing']
+
 
 class BytesFileSystemStorage(Storage):
     """store Bytes attribute value on the file system"""
@@ -174,7 +186,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/sqlutils.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -263,8 +262,7 @@
         eschema = entity.e_schema
         for attr in entity.edited_attributes:
             value = entity[attr]
-            rschema = eschema.subjrels[attr]
-            if rschema.final:
+            if value is not None and eschema.subjrels[attr].final:
                 atype = str(entity.e_schema.destination(attr))
                 if atype == 'Boolean':
                     value = self.dbhelper.boolean_value(value)
--- a/server/ssplanner.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/ssplanner.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,15 +15,12 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""plan execution of rql queries on a single source
+"""plan execution of rql queries on a single source"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
 
-from copy import copy
-
 from rql.stmts import Union, Select
 from rql.nodes import Constant, Relation
 
@@ -479,7 +476,7 @@
             result = [[]]
         for row in result:
             # get a new entity definition for this row
-            edef = copy(base_edef)
+            edef = base_edef.cw_copy()
             # complete this entity def using row values
             index = 0
             for rtype, rorder, value in self.rdefs:
@@ -487,7 +484,7 @@
                     value = row[index]
                     index += 1
                 if rorder == InsertRelationsStep.FINAL:
-                    edef.rql_set_value(rtype, value)
+                    edef._cw_rql_set_value(rtype, value)
                 elif rorder == InsertRelationsStep.RELATION:
                     self.plan.add_relation_def( (edef, rtype, value) )
                     edef.querier_pending_relations[(rtype, 'subject')] = value
@@ -584,7 +581,7 @@
                         edef = edefs[eid]
                     except KeyError:
                         edefs[eid] = edef = session.entity_from_eid(eid)
-                    edef.rql_set_value(str(rschema), rhsval)
+                    edef._cw_rql_set_value(str(rschema), rhsval)
                 else:
                     repo.glob_add_relation(session, lhsval, str(rschema), rhsval)
             result[i] = newrow
--- a/server/test/data/migratedapp/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/data/migratedapp/schema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -101,6 +101,7 @@
 
 
 class Personne(EntityType):
+    __unique_together__ = [('nom', 'prenom', 'datenaiss')]
     nom    = String(fulltextindexed=True, required=True, maxsize=64)
     prenom = String(fulltextindexed=True, maxsize=64)
     civility   = String(maxsize=1, default='M', fulltextindexed=True)
@@ -126,7 +127,6 @@
         'delete': ('managers', 'owners'),
         'add': ('managers', 'users',)
         }
-
     nom  = String(maxsize=64, fulltextindexed=True)
     web  = String(maxsize=128)
     tel  = Int()
@@ -138,6 +138,9 @@
     cp   = String(maxsize=12)
     ville= String(maxsize=32)
 
+class same_as(RelationDefinition):
+    subject = ('Societe',)
+    object = 'ExternalUri'
 
 class evaluee(RelationDefinition):
     subject = ('Personne', 'CWUser', 'Societe')
--- a/server/test/data/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/data/schema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -19,7 +19,8 @@
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
                             SubjectRelation, RichString, String, Int, Boolean, Datetime)
 from yams.constraints import SizeConstraint
-from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
+from cubicweb.schema import (WorkflowableEntityType,
+                             RQLConstraint, RQLUniqueConstraint,
                              ERQLExpression, RRQLExpression)
 
 class Affaire(WorkflowableEntityType):
@@ -92,11 +93,15 @@
                       })
 
     migrated_from = SubjectRelation('Note')
-    attachment = SubjectRelation(('File', 'Image'))
-    inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
+    attachment = SubjectRelation('File')
+    inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*',
+                              constraints=[RQLUniqueConstraint('S type T, S inline1 A1, A1 todo_by C, '
+                                                              'Y type T, Y inline1 A2, A2 todo_by C',
+                                                               'S,Y')])
     todo_by = SubjectRelation('CWUser')
 
 class Personne(EntityType):
+    __unique_together__ = [('nom', 'prenom', 'inline2')]
     nom    = String(fulltextindexed=True, required=True, maxsize=64)
     prenom = String(fulltextindexed=True, maxsize=64)
     sexe   = String(maxsize=1, default='M', fulltextindexed=True)
--- a/server/test/data/site_cubicweb.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/data/site_cubicweb.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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/data/sources_ldap1	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,35 @@
+[system]
+adapter=native
+# database driver (postgres or sqlite)
+db-driver=sqlite
+# database host
+db-host=
+# database name
+db-name=tmpdb
+# database user
+db-user=admin
+# database password
+db-password=gingkow
+# database encoding
+db-encoding=utf8
+
+[admin]
+login = admin
+password = gingkow
+
+[ldapuser]
+adapter=ldapuser
+# ldap host
+host=ldap1
+# base DN to lookup for usres
+user-base-dn=ou=People,dc=logilab,dc=fr
+# user search scope
+user-scope=ONELEVEL
+# classes of user
+user-classes=top,posixAccount
+# attribute used as login on authentication
+user-login-attr=uid
+# name of a group in which ldap users will be by default
+user-default-group=users
+# map from ldap user attributes to cubicweb attributes
+user-attrs-map=gecos:email,uid:login
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data/sources_ldap2	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,35 @@
+[system]
+adapter=native
+# database driver (postgres or sqlite)
+db-driver=sqlite
+# database host
+db-host=
+# database name
+db-name=tmpdb
+# database user
+db-user=admin
+# database password
+db-password=gingkow
+# database encoding
+db-encoding=utf8
+
+[admin]
+login = admin
+password = gingkow
+
+[ldapuser]
+adapter=ldapuser
+# ldap host
+host=ldap1
+# base DN to lookup for usres
+user-base-dn=ou=People,dc=logilab,dc=net
+# user search scope
+user-scope=ONELEVEL
+# classes of user
+user-classes=top,OpenLDAPperson
+# attribute used as login on authentication
+user-login-attr=uid
+# name of a group in which ldap users will be by default
+user-default-group=users
+# map from ldap user attributes to cubicweb attributes
+user-attrs-map=mail:email,uid:login
--- a/server/test/data/sourcesldap	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-[system]
-adapter=native
-# database driver (postgres or sqlite)
-db-driver=sqlite
-# database host
-db-host=
-# database name
-db-name=tmpdb
-# database user
-db-user=admin
-# database password
-db-password=gingkow
-# database encoding
-db-encoding=utf8
-
-[admin]
-login = admin
-password = gingkow
-
-[ldapuser]
-adapter=ldapuser
-# ldap host
-host=ldap1
-# base DN to lookup for usres
-user-base-dn=ou=People,dc=logilab,dc=fr
-# user search scope
-user-scope=ONELEVEL
-# classes of user
-user-classes=top,posixAccount
-# attribute used as login on authentication
-user-login-attr=uid
-# name of a group in which ldap users will be by default
-user-default-group=users
-# map from ldap user attributes to cubicweb attributes
-user-attrs-map=gecos:email,uid:login
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_fti.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,63 @@
+from __future__ import with_statement
+
+import socket
+
+from cubicweb.devtools import ApptestConfiguration
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.selectors import is_instance
+from cubicweb.entities.adapters import IFTIndexableAdapter
+
+class PostgresFTITC(CubicWebTC):
+    config = ApptestConfiguration('data', sourcefile='sources_fti')
+
+    def setUp(self):
+        if not socket.gethostname().endswith('.logilab.fr'):
+            self.skipTest('XXX require logilab configuration')
+        super(PostgresFTITC, self).setUp()
+
+    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.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
+                          [[c1.eid], [c3.eid], [c2.eid]])
+
+
+    def test_attr_weight(self):
+        class CardIFTIndexableAdapter(IFTIndexableAdapter):
+            __select__ = is_instance('Card')
+            attr_weight = {'title': 'A'}
+        with self.temporary_appobjects(CardIFTIndexableAdapter):
+            req = self.request()
+            c1 = req.create_entity('Card', title=u'c1',
+                                   content=u'cubicweb cubicweb cubicweb')
+            c2 = req.create_entity('Card', title=u'c2',
+                                   content=u'cubicweb cubicweb')
+            c3 = req.create_entity('Card', title=u'cubicweb',
+                                   content=u'autre chose')
+            self.commit()
+            self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
+                              [[c3.eid], [c1.eid], [c2.eid]])
+
+
+    def test_entity_weight(self):
+        class PersonneIFTIndexableAdapter(IFTIndexableAdapter):
+            __select__ = is_instance('Personne')
+            entity_weight = 2.0
+        with self.temporary_appobjects(PersonneIFTIndexableAdapter):
+            req = self.request()
+            c1 = req.create_entity('Personne', nom=u'c1', prenom=u'cubicweb')
+            c2 = req.create_entity('Comment', content=u'cubicweb cubicweb', comments=c1)
+            c3 = req.create_entity('Comment', content=u'cubicweb cubicweb cubicweb', comments=c1)
+            self.commit()
+            self.assertEqual(req.execute('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
+                              [[c1.eid], [c3.eid], [c2.eid]])
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/server/test/unittest_hook.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_hook.py	Wed Nov 03 16:38:28 2010 +0100
@@ -23,7 +23,6 @@
 
 from cubicweb.devtools import TestServerConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.selectors import implements
 from cubicweb.server import hook
 from cubicweb.hooks import integrity, syncschema
 
@@ -48,7 +47,7 @@
         l1 = hook.LateOperation(session)
         l2 = hook.LateOperation(session)
         l3 = hook.Operation(session)
-        self.assertEquals(session.pending_operations, [l3, l1, l2])
+        self.assertEqual(session.pending_operations, [l3, l1, l2])
 
     @clean_session_ops
     def test_single_last_operation(self):
@@ -57,21 +56,21 @@
         l1 = hook.LateOperation(session)
         l2 = hook.LateOperation(session)
         l3 = hook.Operation(session)
-        self.assertEquals(session.pending_operations, [l3, l1, l2, l0])
+        self.assertEqual(session.pending_operations, [l3, l1, l2, l0])
         l4 = hook.SingleLastOperation(session)
-        self.assertEquals(session.pending_operations, [l3, l1, l2, l4])
+        self.assertEqual(session.pending_operations, [l3, l1, l2, l4])
 
     @clean_session_ops
     def test_global_operation_order(self):
         session = self.session
         op1 = integrity._DelayedDeleteOp(session)
-        op2 = syncschema.MemSchemaRDefDel(session)
+        op2 = syncschema.RDefDelOp(session)
         # equivalent operation generated by op2 but replace it here by op3 so we
         # can check the result...
         op3 = syncschema.MemSchemaNotifyChanges(session)
         op4 = integrity._DelayedDeleteOp(session)
         op5 = integrity._CheckORelationOp(session)
-        self.assertEquals(session.pending_operations, [op1, op2, op4, op5, op3])
+        self.assertEqual(session.pending_operations, [op1, op2, op4, op5, op3])
 
 
 class HookCalled(Exception): pass
@@ -103,19 +102,19 @@
         class _Hook(hook.Hook):
             events = ('before_add_entiti',)
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad event before_add_entiti on %s._Hook' % __name__)
+        self.assertEqual(str(ex), 'bad event before_add_entiti on %s._Hook' % __name__)
 
     def test_register_bad_hook2(self):
         class _Hook(hook.Hook):
             events = None
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad .events attribute None on %s._Hook' % __name__)
+        self.assertEqual(str(ex), 'bad .events attribute None on %s._Hook' % __name__)
 
     def test_register_bad_hook3(self):
         class _Hook(hook.Hook):
             events = 'before_add_entity'
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad event b on %s._Hook' % __name__)
+        self.assertEqual(str(ex), 'bad event b on %s._Hook' % __name__)
 
     def test_call_hook(self):
         self.o.register(AddAnyHook)
@@ -139,17 +138,17 @@
 
     def test_startup_shutdown(self):
         import hooks # cubicweb/server/test/data/hooks.py
-        self.assertEquals(hooks.CALLED_EVENTS['server_startup'], True)
+        self.assertEqual(hooks.CALLED_EVENTS['server_startup'], True)
         # don't actually call repository.shutdown !
         self.repo.hm.call_hooks('server_shutdown', repo=self.repo)
-        self.assertEquals(hooks.CALLED_EVENTS['server_shutdown'], True)
+        self.assertEqual(hooks.CALLED_EVENTS['server_shutdown'], True)
 
     def test_session_open_close(self):
         import hooks # cubicweb/server/test/data/hooks.py
         cnx = self.login('anon')
-        self.assertEquals(hooks.CALLED_EVENTS['session_open'], 'anon')
+        self.assertEqual(hooks.CALLED_EVENTS['session_open'], 'anon')
         cnx.close()
-        self.assertEquals(hooks.CALLED_EVENTS['session_close'], 'anon')
+        self.assertEqual(hooks.CALLED_EVENTS['session_close'], 'anon')
 
 
 # class RelationHookTC(TestCase):
@@ -163,30 +162,30 @@
 #         """make sure before_xxx_relation hooks are called directly"""
 #         self.o.register(self._before_relation_hook,
 #                              'before_add_relation', 'concerne')
-#         self.assertEquals(self.called, [])
+#         self.assertEqual(self.called, [])
 #         self.o.call_hooks('before_add_relation', 'concerne', 'USER',
 #                           1, 'concerne', 2)
-#         self.assertEquals(self.called, [(1, 'concerne', 2)])
+#         self.assertEqual(self.called, [(1, 'concerne', 2)])
 
 #     def test_after_add_relation(self):
 #         """make sure after_xxx_relation hooks are deferred"""
 #         self.o.register(self._after_relation_hook,
 #                              'after_add_relation', 'concerne')
-#         self.assertEquals(self.called, [])
+#         self.assertEqual(self.called, [])
 #         self.o.call_hooks('after_add_relation', 'concerne', 'USER',
 #                           1, 'concerne', 2)
 #         self.o.call_hooks('after_add_relation', 'concerne', 'USER',
 #                           3, 'concerne', 4)
-#         self.assertEquals(self.called, [(1, 'concerne', 2), (3, 'concerne', 4)])
+#         self.assertEqual(self.called, [(1, 'concerne', 2), (3, 'concerne', 4)])
 
 #     def test_before_delete_relation(self):
 #         """make sure before_xxx_relation hooks are called directly"""
 #         self.o.register(self._before_relation_hook,
 #                              'before_delete_relation', 'concerne')
-#         self.assertEquals(self.called, [])
+#         self.assertEqual(self.called, [])
 #         self.o.call_hooks('before_delete_relation', 'concerne', 'USER',
 #                           1, 'concerne', 2)
-#         self.assertEquals(self.called, [(1, 'concerne', 2)])
+#         self.assertEqual(self.called, [(1, 'concerne', 2)])
 
 #     def test_after_delete_relation(self):
 #         """make sure after_xxx_relation hooks are deferred"""
@@ -196,7 +195,7 @@
 #                           1, 'concerne', 2)
 #         self.o.call_hooks('after_delete_relation', 'concerne', 'USER',
 #                           3, 'concerne', 4)
-#         self.assertEquals(self.called, [(1, 'concerne', 2), (3, 'concerne', 4)])
+#         self.assertEqual(self.called, [(1, 'concerne', 2), (3, 'concerne', 4)])
 
 
 #     def _before_relation_hook(self, pool, subject, r_type, object):
--- a/server/test/unittest_ldapuser.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_ldapuser.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb.server.sources.ldapusers unit and functional tests
-
-"""
+"""cubicweb.server.sources.ldapusers unit and functional tests"""
 
 import socket
 
@@ -30,10 +28,14 @@
 
 if '17.1' in socket.gethostbyname('ldap1'):
     SYT = 'syt'
+    SYT_EMAIL = 'Sylvain Thenault'
     ADIM = 'adim'
+    SOURCESFILE = 'data/sources_ldap1'
 else:
     SYT = 'sthenault'
+    SYT_EMAIL = 'sylvain.thenault@logilab.fr'
     ADIM = 'adimascio'
+    SOURCESFILE = 'data/sources_ldap2'
 
 
 def nopwd_authenticate(self, session, login, password):
@@ -59,7 +61,7 @@
 
 class LDAPUserSourceTC(CubicWebTC):
     config = TestServerConfiguration('data')
-    config.sources_file = lambda : 'data/sourcesldap'
+    config.sources_file = lambda: SOURCESFILE
 
     def patch_authenticate(self):
         self._orig_authenticate = LDAPUserSource.authenticate
@@ -92,19 +94,19 @@
     def test_base(self):
         # check a known one
         e = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT}).get_entity(0, 0)
-        self.assertEquals(e.login, SYT)
+        self.assertEqual(e.login, SYT)
         e.complete()
-        self.assertEquals(e.creation_date, None)
-        self.assertEquals(e.modification_date, None)
-        self.assertEquals(e.firstname, None)
-        self.assertEquals(e.surname, None)
-        self.assertEquals(e.in_group[0].name, 'users')
-        self.assertEquals(e.owned_by[0].login, SYT)
-        self.assertEquals(e.created_by, ())
-        self.assertEquals(e.primary_email[0].address, 'Sylvain Thenault')
+        self.assertEqual(e.creation_date, None)
+        self.assertEqual(e.modification_date, None)
+        self.assertEqual(e.firstname, None)
+        self.assertEqual(e.surname, None)
+        self.assertEqual(e.in_group[0].name, 'users')
+        self.assertEqual(e.owned_by[0].login, SYT)
+        self.assertEqual(e.created_by, ())
+        self.assertEqual(e.primary_email[0].address, SYT_EMAIL)
         # email content should be indexed on the user
         rset = self.sexecute('CWUser X WHERE X has_text "thenault"')
-        self.assertEquals(rset.rows, [[e.eid]])
+        self.assertEqual(rset.rows, [[e.eid]])
 
     def test_not(self):
         eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
@@ -117,16 +119,16 @@
         aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM})[0][0]
         rset = self.sexecute('CWUser X, Y WHERE X login %(syt)s, Y login %(adim)s',
                             {'syt': SYT, 'adim': ADIM})
-        self.assertEquals(rset.rows, [[seid, aeid]])
+        self.assertEqual(rset.rows, [[seid, aeid]])
         rset = self.sexecute('Any X,Y,L WHERE X login L, X login %(syt)s, Y login %(adim)s',
                             {'syt': SYT, 'adim': ADIM})
-        self.assertEquals(rset.rows, [[seid, aeid, SYT]])
+        self.assertEqual(rset.rows, [[seid, aeid, SYT]])
 
     def test_in(self):
         seid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
         aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM})[0][0]
         rset = self.sexecute('Any X,L ORDERBY L WHERE X login IN("%s", "%s"), X login L' % (SYT, ADIM))
-        self.assertEquals(rset.rows, [[aeid, ADIM], [seid, SYT]])
+        self.assertEqual(rset.rows, [[aeid, ADIM], [seid, SYT]])
 
     def test_relations(self):
         eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
@@ -144,28 +146,28 @@
     def test_upper(self):
         eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
         rset = self.sexecute('Any UPPER(L) WHERE X eid %s, X login L' % eid)
-        self.assertEquals(rset[0][0], SYT.upper())
+        self.assertEqual(rset[0][0], SYT.upper())
 
     def test_unknown_attr(self):
         eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
         rset = self.sexecute('Any L,C,M WHERE X eid %s, X login L, '
                             'X creation_date C, X modification_date M' % eid)
-        self.assertEquals(rset[0][0], SYT)
-        self.assertEquals(rset[0][1], None)
-        self.assertEquals(rset[0][2], None)
+        self.assertEqual(rset[0][0], SYT)
+        self.assertEqual(rset[0][1], None)
+        self.assertEqual(rset[0][2], None)
 
     def test_sort(self):
         logins = [l for l, in self.sexecute('Any L ORDERBY L WHERE X login L')]
-        self.assertEquals(logins, sorted(logins))
+        self.assertEqual(logins, sorted(logins))
 
     def test_lower_sort(self):
         logins = [l for l, in self.sexecute('Any L ORDERBY lower(L) WHERE X login L')]
-        self.assertEquals(logins, sorted(logins))
+        self.assertEqual(logins, sorted(logins))
 
     def test_or(self):
         rset = self.sexecute('DISTINCT Any X WHERE X login %(login)s OR (X in_group G, G name "managers")',
                             {'login': SYT})
-        self.assertEquals(len(rset), 2, rset.rows) # syt + admin
+        self.assertEqual(len(rset), 2, rset.rows) # syt + admin
 
     def test_nonregr_set_owned_by(self):
         # test that when a user coming from ldap is triggering a transition
@@ -173,29 +175,30 @@
         self.sexecute('SET X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': SYT})
         self.commit()
         syt = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT}).get_entity(0, 0)
-        self.assertEquals([g.name for g in syt.in_group], ['managers', 'users'])
+        self.assertEqual([g.name for g in syt.in_group], ['managers', 'users'])
         self.patch_authenticate()
         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()
-            self.assertEquals(trinfo.owned_by[0].login, SYT)
+            self.assertEqual(adim.in_state[0].name, 'deactivated')
+            trinfo = iworkflowable.latest_trinfo()
+            self.assertEqual(trinfo.owned_by[0].login, SYT)
             # select from_state to skip the user's creation TrInfo
             rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
                                 'WF creation_date D, WF from_state FS,'
                                 'WF owned_by U?, X eid %(x)s',
                                 {'x': adim.eid})
-            self.assertEquals(rset.rows, [[syt.eid]])
+            self.assertEqual(rset.rows, [[syt.eid]])
         finally:
             # restore db state
             self.restore_connection()
             adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
-            adim.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):
@@ -212,14 +215,14 @@
         self.sexecute('SET U in_group G WHERE G name ~= "bougloup%", U login "admin"')
         self.sexecute('SET U in_group G WHERE G name = "bougloup1", U login %(syt)s', {'syt': SYT})
         rset = self.sexecute('Any L,SN ORDERBY L WHERE X in_state S, S name SN, X login L, EXISTS(X in_group G, G name ~= "bougloup%")')
-        self.assertEquals(rset.rows, [['admin', 'activated'], [SYT, 'activated']])
+        self.assertEqual(rset.rows, [['admin', 'activated'], [SYT, 'activated']])
 
     def test_exists2(self):
         self.create_user('comme')
         self.create_user('cochon')
         self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
         rset = self.sexecute('Any GN ORDERBY GN WHERE X in_group G, G name GN, (G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon")))')
-        self.assertEquals(rset.rows, [['managers'], ['users']])
+        self.assertEqual(rset.rows, [['managers'], ['users']])
 
     def test_exists3(self):
         self.create_user('comme')
@@ -229,7 +232,7 @@
         self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "cochon"', {'syt': SYT})
         self.failUnless(self.sexecute('Any X, Y WHERE X copain Y, X login %(syt)s, Y login "cochon"', {'syt': SYT}))
         rset = self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon"))')
-        self.assertEquals(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', SYT]])
+        self.assertEqual(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', SYT]])
 
     def test_exists4(self):
         self.create_user('comme')
@@ -251,7 +254,7 @@
         all = self.sexecute('Any GN, L WHERE X in_group G, X login L, G name GN')
         all.rows.remove(['users', 'comme'])
         all.rows.remove(['users', SYT])
-        self.assertEquals(sorted(rset.rows), sorted(all.rows))
+        self.assertEqual(sorted(rset.rows), sorted(all.rows))
 
     def test_exists5(self):
         self.create_user('comme')
@@ -264,25 +267,25 @@
         rset= self.sexecute('Any L WHERE X login L, '
                            'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
                            'NOT EXISTS(X copain T2, T2 login "billy")')
-        self.assertEquals(sorted(rset.rows), [['cochon'], [SYT]])
+        self.assertEqual(sorted(rset.rows), [['cochon'], [SYT]])
         rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, '
                            'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
                            'NOT EXISTS(X copain T2, T2 login "billy")')
-        self.assertEquals(sorted(rset.rows), [['guests', 'cochon'],
+        self.assertEqual(sorted(rset.rows), [['guests', 'cochon'],
                                               ['users', 'cochon'],
                                               ['users', SYT]])
 
     def test_cd_restriction(self):
         rset = self.sexecute('CWUser X WHERE X creation_date > "2009-02-01"')
         # admin/anon but no ldap user since it doesn't support creation_date
-        self.assertEquals(sorted(e.login for e in rset.entities()),
+        self.assertEqual(sorted(e.login for e in rset.entities()),
                           ['admin', 'anon'])
 
     def test_union(self):
         afeids = self.sexecute('State X')
         ueids = self.sexecute('CWUser X')
         rset = self.sexecute('(Any X WHERE X is State) UNION (Any X WHERE X is CWUser)')
-        self.assertEquals(sorted(r[0] for r in rset.rows),
+        self.assertEqual(sorted(r[0] for r in rset.rows),
                           sorted(r[0] for r in afeids + ueids))
 
     def _init_security_test(self):
@@ -293,23 +296,23 @@
     def test_security1(self):
         cu = self._init_security_test()
         rset = cu.execute('CWUser X WHERE X login %(login)s', {'login': SYT})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         rset = cu.execute('Any X WHERE X login "iaminguestsgrouponly"')
-        self.assertEquals(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows), 1)
 
     def test_security2(self):
         cu = self._init_security_test()
         rset = cu.execute('Any X WHERE X has_text %(syt)s', {'syt': SYT})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         rset = cu.execute('Any X WHERE X has_text "iaminguestsgrouponly"')
-        self.assertEquals(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows), 1)
 
     def test_security3(self):
         cu = self._init_security_test()
         rset = cu.execute('Any F WHERE X has_text %(syt)s, X firstname F', {'syt': SYT})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         rset = cu.execute('Any F WHERE X has_text "iaminguestsgrouponly", X firstname F')
-        self.assertEquals(rset.rows, [[None]])
+        self.assertEqual(rset.rows, [[None]])
 
     def test_nonregr1(self):
         self.sexecute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E owned_by X, '
@@ -350,34 +353,34 @@
     def test_count(self):
         trfunc = GlobTrFunc('count', 0)
         res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEquals(res, [[4]])
+        self.assertEqual(res, [[4]])
         trfunc = GlobTrFunc('count', 1)
         res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEquals(res, [[1, 2], [2, 1], [3, 1]])
+        self.assertEqual(res, [[1, 2], [2, 1], [3, 1]])
 
     def test_sum(self):
         trfunc = GlobTrFunc('sum', 0)
         res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEquals(res, [[10]])
+        self.assertEqual(res, [[10]])
         trfunc = GlobTrFunc('sum', 1)
         res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEquals(res, [[1, 7], [2, 4], [3, 6]])
+        self.assertEqual(res, [[1, 7], [2, 4], [3, 6]])
 
     def test_min(self):
         trfunc = GlobTrFunc('min', 0)
         res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEquals(res, [[1]])
+        self.assertEqual(res, [[1]])
         trfunc = GlobTrFunc('min', 1)
         res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEquals(res, [[1, 2], [2, 4], [3, 6]])
+        self.assertEqual(res, [[1, 2], [2, 4], [3, 6]])
 
     def test_max(self):
         trfunc = GlobTrFunc('max', 0)
         res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEquals(res, [[4]])
+        self.assertEqual(res, [[4]])
         trfunc = GlobTrFunc('max', 1)
         res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEquals(res, [[1, 5], [2, 4], [3, 6]])
+        self.assertEqual(res, [[1, 5], [2, 4], [3, 6]])
 
 # XXX
 LDAPUserSourceTC._init_repo()
@@ -397,6 +400,8 @@
         self.pool = repo._get_pool()
         session = mock_object(pool=self.pool)
         self.o = RQL2LDAPFilter(ldapsource, session)
+        self.ldapclasses = ''.join('(objectClass=%s)' % ldapcls
+                                   for ldapcls in ldapsource.user_classes)
 
     def tearDown(self):
         repo._free_pool(self.pool)
@@ -404,14 +409,14 @@
 
     def test_base(self):
         rqlst = self._prepare('CWUser X WHERE X login "toto"').children[0]
-        self.assertEquals(self.o.generate(rqlst, 'X')[1],
-                          '(&(objectClass=top)(objectClass=posixAccount)(uid=toto))')
+        self.assertEqual(self.o.generate(rqlst, 'X')[1],
+                          '(&%s(uid=toto))' % self.ldapclasses)
 
     def test_kwargs(self):
         rqlst = self._prepare('CWUser X WHERE X login %(x)s').children[0]
         self.o._args = {'x': "toto"}
-        self.assertEquals(self.o.generate(rqlst, 'X')[1],
-                          '(&(objectClass=top)(objectClass=posixAccount)(uid=toto))')
+        self.assertEqual(self.o.generate(rqlst, 'X')[1],
+                          '(&%s(uid=toto))' % self.ldapclasses)
 
     def test_get_attr(self):
         rqlst = self._prepare('Any X WHERE E firstname X, E eid 12').children[0]
--- a/server/test/unittest_migractions.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_migractions.py	Wed Nov 03 16:38:28 2010 +0100
@@ -80,11 +80,11 @@
                                          'RDEF relation_type RT, RDEF ordernum O, RT name RTN'))
         self.mh.cmd_add_attribute('Note', 'whatever')
         self.failUnless('whatever' in self.schema)
-        self.assertEquals(self.schema['whatever'].subjects(), ('Note',))
-        self.assertEquals(self.schema['whatever'].objects(), ('Int',))
-        self.assertEquals(self.schema['Note'].default('whatever'), 2)
+        self.assertEqual(self.schema['whatever'].subjects(), ('Note',))
+        self.assertEqual(self.schema['whatever'].objects(), ('Int',))
+        self.assertEqual(self.schema['Note'].default('whatever'), 2)
         note = self.execute('Note X').get_entity(0, 0)
-        self.assertEquals(note.whatever, 2)
+        self.assertEqual(note.whatever, 2)
         orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, '
                                           'RDEF relation_type RT, RDEF ordernum O, RT name RTN'))
         whateverorder = migrschema['whatever'].rdef('Note', 'Int').order
@@ -92,8 +92,8 @@
             if v >= whateverorder:
                 orderdict[k] = v+1
         orderdict['whatever'] = whateverorder
-        self.assertDictEquals(orderdict, orderdict2)
-        #self.assertEquals([r.type for r in self.schema['Note'].ordered_relations()],
+        self.assertDictEqual(orderdict, orderdict2)
+        #self.assertEqual([r.type for r in self.schema['Note'].ordered_relations()],
         #                  ['modification_date', 'creation_date', 'owned_by',
         #                   'eid', 'ecrit_par', 'inline1', 'date', 'type',
         #                   'whatever', 'date', 'in_basket'])
@@ -106,12 +106,12 @@
         self.failIf('shortpara' in self.schema)
         self.mh.cmd_add_attribute('Note', 'shortpara')
         self.failUnless('shortpara' in self.schema)
-        self.assertEquals(self.schema['shortpara'].subjects(), ('Note', ))
-        self.assertEquals(self.schema['shortpara'].objects(), ('String', ))
+        self.assertEqual(self.schema['shortpara'].subjects(), ('Note', ))
+        self.assertEqual(self.schema['shortpara'].objects(), ('String', ))
         # test created column is actually a varchar(64)
         notesql = self.mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' and name='%sNote'" % SQL_PREFIX)[0][0]
         fields = dict(x.strip().split()[:2] for x in notesql.split('(', 1)[1].rsplit(')', 1)[0].split(','))
-        self.assertEquals(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
+        self.assertEqual(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
         self.mh.rollback()
 
     def test_add_datetime_with_default_value_attribute(self):
@@ -119,15 +119,15 @@
         self.failIf('shortpara' in self.schema)
         self.mh.cmd_add_attribute('Note', 'mydate')
         self.failUnless('mydate' in self.schema)
-        self.assertEquals(self.schema['mydate'].subjects(), ('Note', ))
-        self.assertEquals(self.schema['mydate'].objects(), ('Date', ))
+        self.assertEqual(self.schema['mydate'].subjects(), ('Note', ))
+        self.assertEqual(self.schema['mydate'].objects(), ('Date', ))
         testdate = date(2005, 12, 13)
         eid1 = self.mh.rqlexec('INSERT Note N')[0][0]
         eid2 = self.mh.rqlexec('INSERT Note N: N mydate %(mydate)s', {'mydate' : testdate})[0][0]
         d1 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid1})[0][0]
         d2 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid2})[0][0]
-        self.assertEquals(d1, date.today())
-        self.assertEquals(d2, testdate)
+        self.assertEqual(d1, date.today())
+        self.assertEqual(d2, testdate)
         self.mh.rollback()
 
     def test_rename_attribute(self):
@@ -149,10 +149,10 @@
         for etype in ('Personne', 'Email'):
             s1 = self.mh.rqlexec('Any N WHERE WF workflow_of ET, ET name "%s", WF name N' %
                                  etype)[0][0]
-            self.assertEquals(s1, "foo")
+            self.assertEqual(s1, "foo")
             s1 = self.mh.rqlexec('Any N WHERE ET default_workflow WF, ET name "%s", WF name N' %
                                  etype)[0][0]
-            self.assertEquals(s1, "foo")
+            self.assertEqual(s1, "foo")
 
     def test_add_entity_type(self):
         self.failIf('Folder2' in self.schema)
@@ -163,18 +163,18 @@
         self.failUnless('filed_under2' in self.schema)
         self.failUnless(self.execute('CWRType X WHERE X name "filed_under2"'))
         self.schema.rebuild_infered_relations()
-        self.assertEquals(sorted(str(rs) for rs in self.schema['Folder2'].subject_relations()),
+        self.assertEqual(sorted(str(rs) for rs in self.schema['Folder2'].subject_relations()),
                           ['created_by', 'creation_date', 'cwuri',
                            'description', 'description_format',
                            'eid',
                            'filed_under2', 'has_text',
                            'identity', 'in_basket', 'is', 'is_instance_of',
                            'modification_date', 'name', 'owned_by'])
-        self.assertEquals([str(rs) for rs in self.schema['Folder2'].object_relations()],
+        self.assertEqual([str(rs) for rs in self.schema['Folder2'].object_relations()],
                           ['filed_under2', 'identity'])
-        self.assertEquals(sorted(str(e) for e in self.schema['filed_under2'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['filed_under2'].subjects()),
                           sorted(str(e) for e in self.schema.entities() if not e.final))
-        self.assertEquals(self.schema['filed_under2'].objects(), ('Folder2',))
+        self.assertEqual(self.schema['filed_under2'].objects(), ('Folder2',))
         eschema = self.schema.eschema('Folder2')
         for cstr in eschema.rdef('name').constraints:
             self.failUnless(hasattr(cstr, 'eid'))
@@ -201,22 +201,22 @@
         self.mh.cmd_add_relation_type('filed_under2')
         self.schema.rebuild_infered_relations()
         self.failUnless('filed_under2' in self.schema)
-        self.assertEquals(sorted(str(e) for e in self.schema['filed_under2'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['filed_under2'].subjects()),
                           sorted(str(e) for e in self.schema.entities() if not e.final))
-        self.assertEquals(self.schema['filed_under2'].objects(), ('Folder2',))
+        self.assertEqual(self.schema['filed_under2'].objects(), ('Folder2',))
         self.mh.cmd_drop_relation_type('filed_under2')
         self.failIf('filed_under2' in self.schema)
 
     def test_add_relation_definition_nortype(self):
         self.mh.cmd_add_relation_definition('Personne', 'concerne2', 'Affaire')
-        self.assertEquals(self.schema['concerne2'].subjects(),
+        self.assertEqual(self.schema['concerne2'].subjects(),
                           ('Personne',))
-        self.assertEquals(self.schema['concerne2'].objects(),
+        self.assertEqual(self.schema['concerne2'].objects(),
                           ('Affaire', ))
-        self.assertEquals(self.schema['concerne2'].rdef('Personne', 'Affaire').cardinality,
+        self.assertEqual(self.schema['concerne2'].rdef('Personne', 'Affaire').cardinality,
                           '1*')
         self.mh.cmd_add_relation_definition('Personne', 'concerne2', 'Note')
-        self.assertEquals(sorted(self.schema['concerne2'].objects()), ['Affaire', 'Note'])
+        self.assertEqual(sorted(self.schema['concerne2'].objects()), ['Affaire', 'Note'])
         self.mh.create_entity('Personne', nom=u'tot')
         self.mh.create_entity('Affaire')
         self.mh.rqlexec('SET X concerne2 Y WHERE X is Personne, Y is Affaire')
@@ -227,59 +227,59 @@
         self.failIf('concerne2' in self.schema)
 
     def test_drop_relation_definition_existant_rtype(self):
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire', 'Personne'])
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Division', 'Note', 'Societe', 'SubDivision'])
         self.mh.cmd_drop_relation_definition('Personne', 'concerne', 'Affaire')
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire'])
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Division', 'Note', 'Societe', 'SubDivision'])
         self.mh.cmd_add_relation_definition('Personne', 'concerne', 'Affaire')
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire', 'Personne'])
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Division', 'Note', 'Societe', 'SubDivision'])
         # trick: overwrite self.maxeid to avoid deletion of just reintroduced types
         self.maxeid = self.execute('Any MAX(X)')[0][0]
 
     def test_drop_relation_definition_with_specialization(self):
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire', 'Personne'])
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Division', 'Note', 'Societe', 'SubDivision'])
         self.mh.cmd_drop_relation_definition('Affaire', 'concerne', 'Societe')
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire', 'Personne'])
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Division', 'Note', 'SubDivision'])
         self.schema.rebuild_infered_relations() # need to be explicitly called once everything is in place
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Note'])
         self.mh.cmd_add_relation_definition('Affaire', 'concerne', 'Societe')
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire', 'Personne'])
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Note', 'Societe'])
         self.schema.rebuild_infered_relations() # need to be explicitly called once everything is in place
-        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+        self.assertEqual(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Division', 'Note', 'Societe', 'SubDivision'])
         # trick: overwrite self.maxeid to avoid deletion of just reintroduced types
         self.maxeid = self.execute('Any MAX(X)')[0][0]
 
     def test_rename_relation(self):
-        self.skip('implement me')
+        self.skipTest('implement me')
 
     def test_change_relation_props_non_final(self):
         rschema = self.schema['concerne']
         card = rschema.rdef('Affaire', 'Societe').cardinality
-        self.assertEquals(card, '**')
+        self.assertEqual(card, '**')
         try:
             self.mh.cmd_change_relation_props('Affaire', 'concerne', 'Societe',
                                               cardinality='?*')
             card = rschema.rdef('Affaire', 'Societe').cardinality
-            self.assertEquals(card, '?*')
+            self.assertEqual(card, '?*')
         finally:
             self.mh.cmd_change_relation_props('Affaire', 'concerne', 'Societe',
                                               cardinality='**')
@@ -287,12 +287,12 @@
     def test_change_relation_props_final(self):
         rschema = self.schema['adel']
         card = rschema.rdef('Personne', 'String').fulltextindexed
-        self.assertEquals(card, False)
+        self.assertEqual(card, False)
         try:
             self.mh.cmd_change_relation_props('Personne', 'adel', 'String',
                                               fulltextindexed=True)
             card = rschema.rdef('Personne', 'String').fulltextindexed
-            self.assertEquals(card, True)
+            self.assertEqual(card, True)
         finally:
             self.mh.cmd_change_relation_props('Personne', 'adel', 'String',
                                               fulltextindexed=False)
@@ -309,13 +309,14 @@
         migrschema['titre'].rdefs[('Personne', 'String')].description = 'title for this person'
         delete_concerne_rqlexpr = self._rrqlexpr_rset('delete', 'concerne')
         add_concerne_rqlexpr = self._rrqlexpr_rset('add', 'concerne')
+        
         self.mh.cmd_sync_schema_props_perms(commit=False)
 
-        self.assertEquals(cursor.execute('Any D WHERE X name "Personne", X description D')[0][0],
+        self.assertEqual(cursor.execute('Any D WHERE X name "Personne", X description D')[0][0],
                           'blabla bla')
-        self.assertEquals(cursor.execute('Any D WHERE X name "titre", X description D')[0][0],
+        self.assertEqual(cursor.execute('Any D WHERE X name "titre", X description D')[0][0],
                           'usually a title')
-        self.assertEquals(cursor.execute('Any D WHERE X relation_type RT, RT name "titre",'
+        self.assertEqual(cursor.execute('Any D WHERE X relation_type RT, RT name "titre",'
                                          'X from_entity FE, FE name "Personne",'
                                          'X description D')[0][0],
                           'title for this person')
@@ -326,29 +327,29 @@
         expected = [u'nom', u'prenom', u'sexe', u'promo', u'ass', u'adel', u'titre',
                     u'web', u'tel', u'fax', u'datenaiss', u'test', 'description', u'firstname',
                     u'creation_date', 'cwuri', u'modification_date']
-        self.assertEquals(rinorder, expected)
+        self.assertEqual(rinorder, expected)
 
         # test permissions synchronization ####################################
         # new rql expr to add note entity
         eexpr = self._erqlexpr_entity('add', 'Note')
-        self.assertEquals(eexpr.expression,
+        self.assertEqual(eexpr.expression,
                           'X ecrit_part PE, U in_group G, '
                           'PE require_permission P, P name "add_note", P require_group G')
-        self.assertEquals([et.name for et in eexpr.reverse_add_permission], ['Note'])
-        self.assertEquals(eexpr.reverse_read_permission, ())
-        self.assertEquals(eexpr.reverse_delete_permission, ())
-        self.assertEquals(eexpr.reverse_update_permission, ())
+        self.assertEqual([et.name for et in eexpr.reverse_add_permission], ['Note'])
+        self.assertEqual(eexpr.reverse_read_permission, ())
+        self.assertEqual(eexpr.reverse_delete_permission, ())
+        self.assertEqual(eexpr.reverse_update_permission, ())
         # no more rqlexpr to delete and add para attribute
         self.failIf(self._rrqlexpr_rset('add', 'para'))
         self.failIf(self._rrqlexpr_rset('delete', 'para'))
         # new rql expr to add ecrit_par relation
         rexpr = self._rrqlexpr_entity('add', 'ecrit_par')
-        self.assertEquals(rexpr.expression,
+        self.assertEqual(rexpr.expression,
                           'O require_permission P, P name "add_note", '
                           'U in_group G, P require_group G')
-        self.assertEquals([rdef.rtype.name for rdef in rexpr.reverse_add_permission], ['ecrit_par'])
-        self.assertEquals(rexpr.reverse_read_permission, ())
-        self.assertEquals(rexpr.reverse_delete_permission, ())
+        self.assertEqual([rdef.rtype.name for rdef in rexpr.reverse_add_permission], ['ecrit_par'])
+        self.assertEqual(rexpr.reverse_read_permission, ())
+        self.assertEqual(rexpr.reverse_delete_permission, ())
         # no more rqlexpr to delete and add travaille relation
         self.failIf(self._rrqlexpr_rset('add', 'travaille'))
         self.failIf(self._rrqlexpr_rset('delete', 'travaille'))
@@ -359,13 +360,13 @@
         self.failIf(self._erqlexpr_rset('read', 'Affaire'))
         # rqlexpr to update Affaire entity has been updated
         eexpr = self._erqlexpr_entity('update', 'Affaire')
-        self.assertEquals(eexpr.expression, 'X concerne S, S owned_by U')
+        self.assertEqual(eexpr.expression, 'X concerne S, S owned_by U')
         # no change for rqlexpr to add and delete Affaire entity
-        self.assertEquals(len(self._erqlexpr_rset('delete', 'Affaire')), 1)
-        self.assertEquals(len(self._erqlexpr_rset('add', 'Affaire')), 1)
+        self.assertEqual(len(self._erqlexpr_rset('delete', 'Affaire')), 1)
+        self.assertEqual(len(self._erqlexpr_rset('add', 'Affaire')), 1)
         # no change for rqlexpr to add and delete concerne relation
-        self.assertEquals(len(self._rrqlexpr_rset('delete', 'concerne')), len(delete_concerne_rqlexpr))
-        self.assertEquals(len(self._rrqlexpr_rset('add', 'concerne')), len(add_concerne_rqlexpr))
+        self.assertEqual(len(self._rrqlexpr_rset('delete', 'concerne')), len(delete_concerne_rqlexpr))
+        self.assertEqual(len(self._rrqlexpr_rset('add', 'concerne')), len(add_concerne_rqlexpr))
         # * migrschema involve:
         #   * 7 rqlexprs deletion (2 in (Affaire read + Societe + travaille) + 1
         #     in para attribute)
@@ -373,29 +374,36 @@
         #   * 2 new (Note add, ecrit_par add)
         #   * 2 implicit new for attributes update_permission (Note.para, Personne.test)
         # remaining orphan rql expr which should be deleted at commit (composite relation)
-        self.assertEquals(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, '
+        self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, '
                                          'NOT ET1 read_permission X, NOT ET2 add_permission X, '
                                          'NOT ET3 delete_permission X, NOT ET4 update_permission X')[0][0],
                           7+1)
         # finally
-        self.assertEquals(cursor.execute('Any COUNT(X) WHERE X is RQLExpression')[0][0],
+        self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression')[0][0],
                           nbrqlexpr_start + 1 + 2 + 2)
-
-        self.mh.rollback()
+        self.mh.commit()
+        # unique_together test
+        self.assertEqual(len(self.schema.eschema('Personne')._unique_together), 1)
+        self.assertItemsEqual(self.schema.eschema('Personne')._unique_together[0],
+                                           ('nom', 'prenom', 'datenaiss'))
+        rset = cursor.execute('Any C WHERE C is CWUniqueTogetherConstraint')
+        self.assertEqual(len(rset), 1)
+        relations = [r.rtype.name for r in rset.get_entity(0,0).relations]
+        self.assertItemsEqual(relations, ('nom', 'prenom', 'datenaiss'))
 
     def _erqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action
         return self.mh.session.execute(rql, {'name': ertype})
     def _erqlexpr_entity(self, action, ertype):
         rset = self._erqlexpr_rset(action, ertype)
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
         return rset.get_entity(0, 0)
     def _rrqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE RT is CWRType, RDEF %s_permission X, RT name %%(name)s, RDEF relation_type RT' % action
         return self.mh.session.execute(rql, {'name': ertype})
     def _rrqlexpr_entity(self, action, ertype):
         rset = self._rrqlexpr_rset(action, ertype)
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
         return rset.get_entity(0, 0)
 
     def test_set_size_constraint(self):
@@ -413,7 +421,7 @@
     def test_add_remove_cube_and_deps(self):
         cubes = set(self.config.cubes())
         schema = self.repo.schema
-        self.assertEquals(sorted((str(s), str(o)) for s, o in schema['see_also'].rdefs.keys()),
+        self.assertEqual(sorted((str(s), str(o)) for s, o in schema['see_also'].rdefs.keys()),
                           sorted([('EmailThread', 'EmailThread'), ('Folder', 'Folder'),
                                   ('Bookmark', 'Bookmark'), ('Bookmark', 'Note'),
                                   ('Note', 'Note'), ('Note', 'Bookmark')]))
@@ -425,19 +433,19 @@
                 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()),
+                self.assertEqual(sorted(schema['see_also'].rdefs.keys()),
                                   sorted([('Folder', 'Folder'),
                                           ('Bookmark', 'Bookmark'),
                                           ('Bookmark', 'Note'),
                                           ('Note', 'Note'),
                                           ('Note', 'Bookmark')]))
-                self.assertEquals(sorted(schema['see_also'].subjects()), ['Bookmark', 'Folder', 'Note'])
-                self.assertEquals(sorted(schema['see_also'].objects()), ['Bookmark', 'Folder', 'Note'])
-                self.assertEquals(self.execute('Any X WHERE X pkey "system.version.email"').rowcount, 0)
-                self.assertEquals(self.execute('Any X WHERE X pkey "system.version.file"').rowcount, 0)
+                self.assertEqual(sorted(schema['see_also'].subjects()), ['Bookmark', 'Folder', 'Note'])
+                self.assertEqual(sorted(schema['see_also'].objects()), ['Bookmark', 'Folder', 'Note'])
+                self.assertEqual(self.execute('Any X WHERE X pkey "system.version.email"').rowcount, 0)
+                self.assertEqual(self.execute('Any X WHERE X pkey "system.version.file"').rowcount, 0)
             except :
                 import traceback
                 traceback.print_exc()
@@ -448,22 +456,22 @@
             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()),
+            self.assertEqual(sorted(schema['see_also'].rdefs.keys()),
                               sorted([('EmailThread', 'EmailThread'), ('Folder', 'Folder'),
                                       ('Bookmark', 'Bookmark'),
                                       ('Bookmark', 'Note'),
                                       ('Note', 'Note'),
                                       ('Note', 'Bookmark')]))
-            self.assertEquals(sorted(schema['see_also'].subjects()), ['Bookmark', 'EmailThread', 'Folder', 'Note'])
-            self.assertEquals(sorted(schema['see_also'].objects()), ['Bookmark', 'EmailThread', 'Folder', 'Note'])
+            self.assertEqual(sorted(schema['see_also'].subjects()), ['Bookmark', 'EmailThread', 'Folder', 'Note'])
+            self.assertEqual(sorted(schema['see_also'].objects()), ['Bookmark', 'EmailThread', 'Folder', 'Note'])
             from cubes.email.__pkginfo__ import version as email_version
             from cubes.file.__pkginfo__ import version as file_version
-            self.assertEquals(self.execute('Any V WHERE X value V, X pkey "system.version.email"')[0][0],
+            self.assertEqual(self.execute('Any V WHERE X value V, X pkey "system.version.email"')[0][0],
                               email_version)
-            self.assertEquals(self.execute('Any V WHERE X value V, X pkey "system.version.file"')[0][0],
+            self.assertEqual(self.execute('Any V WHERE X value V, X pkey "system.version.file"')[0][0],
                               file_version)
             # trick: overwrite self.maxeid to avoid deletion of just reintroduced
             #        types (and their associated tables!)
@@ -501,19 +509,19 @@
 
     def test_remove_dep_cube(self):
         ex = self.assertRaises(ConfigurationError, self.mh.cmd_remove_cube, 'file')
-        self.assertEquals(str(ex), "can't remove cube file, used as a dependency")
+        self.assertEqual(str(ex), "can't remove cube file, used as a dependency")
 
     def test_introduce_base_class(self):
         self.mh.cmd_add_entity_type('Para')
         self.mh.repo.schema.rebuild_infered_relations()
-        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+        self.assertEqual(sorted(et.type for et in self.schema['Para'].specialized_by()),
                           ['Note'])
-        self.assertEquals(self.schema['Note'].specializes().type, 'Para')
+        self.assertEqual(self.schema['Note'].specializes().type, 'Para')
         self.mh.cmd_add_entity_type('Text')
         self.mh.repo.schema.rebuild_infered_relations()
-        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+        self.assertEqual(sorted(et.type for et in self.schema['Para'].specialized_by()),
                           ['Note', 'Text'])
-        self.assertEquals(self.schema['Text'].specializes().type, 'Para')
+        self.assertEqual(self.schema['Text'].specializes().type, 'Para')
         # test columns have been actually added
         text = self.execute('INSERT Text X: X para "hip", X summary "hop", X newattr "momo"').get_entity(0, 0)
         note = self.execute('INSERT Note X: X para "hip", X shortpara "hop", X newattr "momo"').get_entity(0, 0)
@@ -540,10 +548,20 @@
             self.commit()
         finally:
             self.session.data['rebuild-infered'] = False
-        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+        self.assertEqual(sorted(et.type for et in self.schema['Para'].specialized_by()),
                           [])
-        self.assertEquals(self.schema['Note'].specializes(), None)
-        self.assertEquals(self.schema['Text'].specializes(), None)
+        self.assertEqual(self.schema['Note'].specializes(), None)
+        self.assertEqual(self.schema['Text'].specializes(), None)
+
+
+    def test_add_symmetric_relation_type(self):
+        same_as_sql = self.mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
+                                      "and name='same_as_relation'")
+        self.failIf(same_as_sql)
+        self.mh.cmd_add_relation_type('same_as')
+        same_as_sql = self.mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
+                                      "and name='same_as_relation'")
+        self.failUnless(same_as_sql)
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_msplanner.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_msplanner.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,6 +15,9 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+from logilab.common.decorators import clear_cache
+
 from cubicweb.devtools import init_test_database
 from cubicweb.devtools.repotest import BasePlannerTC, test_plan
 
@@ -45,7 +48,7 @@
     uri = 'ccc'
     support_entities = {'Card': True, 'Note': True, 'State': True}
     support_relations = {'in_state': True, 'multisource_rel': True, 'multisource_inlined_rel': True,
-                         'multisource_crossed_rel': True}
+                         'multisource_crossed_rel': True,}
     dont_cross_relations = set(('fiche', 'state_of'))
     cross_relations = set(('multisource_crossed_rel',))
 
@@ -57,10 +60,11 @@
                      {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, {'X': 'CWEType'},
                      {'X': 'CWGroup'}, {'X': 'CWPermission'}, {'X': 'CWProperty'},
                      {'X': 'CWRType'}, {'X': 'CWRelation'}, {'X': 'CWUser'},
+                     {'X': 'CWUniqueTogetherConstraint'},
                      {'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'},
@@ -137,8 +141,8 @@
             for var in sourcevars.keys():
                 solindices = sourcevars.pop(var)
                 sourcevars[var._ms_table_key()] = solindices
-        self.assertEquals(ppi._sourcesterms, sourcesterms)
-        self.assertEquals(ppi.needsplit, needsplit)
+        self.assertEqual(ppi._sourcesterms, sourcesterms)
+        self.assertEqual(ppi.needsplit, needsplit)
 
 
     def test_simple_system_only(self):
@@ -364,6 +368,8 @@
     def setUp(self):
         BaseMSPlannerTC.setUp(self)
         self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
+        for cached in ('rel_type_sources', 'can_cross_relation', 'is_multi_sources_relation'):
+            clear_cache(self.repo, cached)
 
     _test = test_plan
 
@@ -413,7 +419,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 +429,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 +504,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 +513,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 +599,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 +663,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 +790,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 +816,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 +829,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,18 +894,19 @@
                                           [{'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, CWUniqueTogetherConstraint, 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'},
                               {'X': 'CWEType'}, {'X': 'CWGroup'},
                               {'X': 'CWPermission'}, {'X': 'CWProperty'},
                               {'X': 'CWRType'}, {'X': 'CWRelation'},
+                              {'X': 'CWUniqueTogetherConstraint'},
                               {'X': 'Comment'}, {'X': 'Division'},
                               {'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,19 +956,21 @@
                        [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, CWUniqueTogetherConstraint, 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'},
                                          {'X': 'CWConstraintType', 'ET': 'CWEType'}, {'X': 'CWEType', 'ET': 'CWEType'},
                                          {'X': 'CWGroup', 'ET': 'CWEType'}, {'X': 'CWPermission', 'ET': 'CWEType'},
                                          {'X': 'CWProperty', 'ET': 'CWEType'}, {'X': 'CWRType', 'ET': 'CWEType'},
-                                         {'X': 'CWRelation', 'ET': 'CWEType'}, {'X': 'Comment', 'ET': 'CWEType'},
+                                         {'X': 'CWRelation', 'ET': 'CWEType'},
+                                         {'X': 'CWUniqueTogetherConstraint', 'ET': 'CWEType'},
+                                         {'X': 'Comment', 'ET': 'CWEType'},
                                          {'X': 'Division', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'},
                                          {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'},
                                          {'X': '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'},
@@ -1026,7 +1035,7 @@
                      [self.cards, self.system], None, {'X': 'table1.C0', 'X.title': 'table1.C1', 'XT': 'table1.C1'}, []),
                     ('OneFetchStep',
                      [('Any X,XT,U WHERE X owned_by U?, X title XT, X is Card',
-                       [{'X': 'Card', 'XT': 'String'}])],
+                       [{'X': 'Card', 'U': 'CWUser', 'XT': 'String'}])],
                      None, None, [self.system], {'L': 'table0.C1',
                                                  'U': 'table0.C0',
                                                  'X': 'table1.C0',
@@ -1299,9 +1308,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 +1376,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], {},
@@ -1380,7 +1445,7 @@
                     ('FetchStep',
                      [('Any B,C WHERE B login C, B is CWUser', [{'B': 'CWUser', 'C': 'String'}])],
                      [self.ldap, self.system], None, {'B': 'table1.C0', 'B.login': 'table1.C1', 'C': 'table1.C1'}, []),
-                    ('OneFetchStep', [('DISTINCT Any B,C ORDERBY C WHERE A created_by B, B login C, EXISTS(B owned_by 5), B is CWUser',
+                    ('OneFetchStep', [('DISTINCT Any B,C ORDERBY C WHERE A created_by B, B login C, EXISTS(B owned_by 5), B is CWUser, A is IN(Bookmark, Tag)',
                                        [{'A': 'Bookmark', 'B': 'CWUser', 'C': 'String'},
                                         {'A': 'Tag', 'B': 'CWUser', 'C': 'String'}])],
                      None, None, [self.system],
@@ -1414,7 +1479,7 @@
                     ('FetchStep',
                      [('Any B,C WHERE B login C, B is CWUser', [{'B': 'CWUser', 'C': 'String'}])],
                      [self.ldap, self.system], None, {'B': 'table1.C0', 'B.login': 'table1.C1', 'C': 'table1.C1'}, []),
-                    ('OneFetchStep', [('DISTINCT Any B,C ORDERBY C WHERE A created_by B, B login C, EXISTS(B owned_by 5), B is CWUser',
+                    ('OneFetchStep', [('DISTINCT Any B,C ORDERBY C WHERE A created_by B, B login C, EXISTS(B owned_by 5), B is CWUser, A is IN(Card, Tag)',
                                        [{'A': 'Card', 'B': 'CWUser', 'C': 'String'},
                                         {'A': 'Tag', 'B': 'CWUser', 'C': 'String'}])],
                      None, None, [self.system],
@@ -1480,20 +1545,11 @@
     def test_crossed_relation_eid_2_needattr(self):
         repo._type_source_cache[999999] = ('Note', 'cards', 999999)
         self._test('Any Y,T WHERE X eid %(x)s, X multisource_crossed_rel Y, Y type T',
-                   [('FetchStep', [('Any Y,T WHERE Y type T, Y is Note', [{'T': 'String', 'Y': 'Note'}])],
-                     [self.cards, self.system], None,
-                     {'T': 'table0.C1', 'Y': 'table0.C0', 'Y.type': 'table0.C1'}, []),
-                    ('UnionStep', None, None,
-                     [('OneFetchStep', [('Any Y,T WHERE 999999 multisource_crossed_rel Y, Y type T, Y is Note',
-                                         [{'T': 'String', 'Y': 'Note'}])],
-                       None, None, [self.cards], None,
-                       []),
-                      ('OneFetchStep', [('Any Y,T WHERE 999999 multisource_crossed_rel Y, Y type T, Y is Note',
-                                         [{'T': 'String', 'Y': 'Note'}])],
-                       None, None, [self.system],
-                       {'T': 'table0.C1', 'Y': 'table0.C0', 'Y.type': 'table0.C1'},
-                       [])]
-                     )],
+                   [('OneFetchStep', [('Any Y,T WHERE 999999 multisource_crossed_rel Y, Y type T, Y is Note',
+                                       [{'T': 'String', 'Y': 'Note'}])],
+                     None, None, [self.cards, self.system], {},
+                     []),
+                    ],
                    {'x': 999999,})
 
     def test_crossed_relation_eid_not_1(self):
@@ -1701,6 +1757,54 @@
 #                        ]),
 #                     ])
 
+    def test_ldap_user_related_to_invariant_and_dont_cross_rel(self):
+        self.repo._type_source_cache[999999] = ('Note', 'cards', 999999)
+        self.cards.dont_cross_relations.add('created_by')
+        try:
+            self._test('Any X,XL WHERE E eid %(x)s, E created_by X, X login XL',
+                   [('FetchStep', [('Any X,XL WHERE X login XL, X is CWUser',
+                                    [{'X': 'CWUser', 'XL': 'String'}])],
+                     [self.ldap, self.system], None,
+                     {'X': 'table0.C0', 'X.login': 'table0.C1', 'XL': 'table0.C1'},
+                     []),
+                    ('OneFetchStep',
+                     [('Any X,XL WHERE 999999 created_by X, X login XL, X is CWUser',
+                       [{'X': 'CWUser', 'XL': 'String'}])],
+                     None, None,
+                     [self.system],
+                     {'X': 'table0.C0', 'X.login': 'table0.C1', 'XL': 'table0.C1'},
+                     [])],
+                       {'x': 999999})
+        finally:
+            self.cards.dont_cross_relations.remove('created_by')
+
+    def test_ambigous_cross_relation(self):
+        self.repo._type_source_cache[999999] = ('Note', 'cards', 999999)
+        self.cards.support_relations['see_also'] = True
+        self.cards.cross_relations.add('see_also')
+        try:
+            self._test('Any X,AA ORDERBY AA WHERE E eid %(x)s, E see_also X, X modification_date AA',
+                       [('AggrStep',
+                         'SELECT table0.C0, table0.C1 FROM table0 ORDER BY table0.C1',
+                         None,
+                         [('FetchStep',
+                           [('Any X,AA WHERE 999999 see_also X, X modification_date AA, X is Note',
+                             [{'AA': 'Datetime', 'X': 'Note'}])], [self.cards, self.system], {},
+                           {'AA': 'table0.C1', 'X': 'table0.C0',
+                            'X.modification_date': 'table0.C1'},
+                           []),
+                          ('FetchStep',
+                           [('Any X,AA WHERE 999999 see_also X, X modification_date AA, X is Bookmark',
+                             [{'AA': 'Datetime', 'X': 'Bookmark'}])],
+                           [self.system], {},
+                           {'AA': 'table0.C1', 'X': 'table0.C0',
+                            'X.modification_date': 'table0.C1'},
+                           [])])],
+                         {'x': 999999})
+        finally:
+            del self.cards.support_relations['see_also']
+            self.cards.cross_relations.remove('see_also')
+
     # non regression tests ####################################################
 
     def test_nonregr1(self):
@@ -1718,8 +1822,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 +1832,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],
@@ -1816,11 +1921,16 @@
     def test_nonregr8(self):
         repo._type_source_cache[999999] = ('Note', 'cards', 999999)
         self._test('Any X,Z WHERE X eid %(x)s, X multisource_rel Y, Z concerne X',
-                   [('FetchStep', [('Any  WHERE 999999 multisource_rel Y, Y is Note', [{'Y': 'Note'}])],
-                     [self.cards], None, {}, []),
+                   [('FetchStep', [('Any 999999 WHERE 999999 multisource_rel Y, Y is Note',
+                                    [{'Y': 'Note'}])],
+                     [self.cards],
+                     None, {u'%(x)s': 'table0.C0'},
+                     []),
                     ('OneFetchStep', [('Any 999999,Z WHERE Z concerne 999999, Z is Affaire',
                                        [{'Z': 'Affaire'}])],
-                     None, None, [self.system], {}, [])],
+                     None, None, [self.system],
+                     {u'%(x)s': 'table0.C0'}, []),
+                    ],
                    {'x': 999999})
 
     def test_nonregr9(self):
@@ -1870,8 +1980,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',
@@ -1923,7 +2032,7 @@
         # identity relation. BUT I think it's better to leave it as is and to
         # explain constraint propagation rules, and so why this should be
         # wrapped in exists() if used in multi-source
-        self.skip('take a look at me if you wish')
+        self.skipTest('take a look at me if you wish')
         self._test('Any B,U,UL GROUPBY B,U,UL WHERE B created_by U?, B is File '
                    'WITH U,UL BEING (Any U,UL WHERE ME eid %(x)s, (U identity ME '
                    'OR (EXISTS(U in_group G, G name IN("managers", "staff")))) '
@@ -2140,14 +2249,67 @@
                    {'x': 999999})
 
 
+    def test_nonregr_not_is(self):
+        self._test("Any X WHERE X owned_by U, U login 'anon', NOT X is Comment",
+                   [('FetchStep', [('Any X WHERE X is IN(Card, Note, State)',
+                                    [{'X': 'Note'}, {'X': 'State'}, {'X': 'Card'}])],
+                     [self.cards, self.cards2, self.system],
+                     None, {'X': 'table0.C0'}, []),
+                    ('UnionStep', None, None,
+                     [('OneFetchStep',
+                       [(u'Any X WHERE X owned_by U, U login "anon", U is CWUser, X is IN(Affaire, BaseTransition, Basket, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, CWUser, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+                         [{'U': 'CWUser', 'X': 'Affaire'},
+                          {'U': 'CWUser', 'X': 'BaseTransition'},
+                          {'U': 'CWUser', 'X': 'Basket'},
+                          {'U': 'CWUser', 'X': 'Bookmark'},
+                          {'U': 'CWUser', 'X': 'CWAttribute'},
+                          {'U': 'CWUser', 'X': 'CWCache'},
+                          {'U': 'CWUser', 'X': 'CWConstraint'},
+                          {'U': 'CWUser', 'X': 'CWConstraintType'},
+                          {'U': 'CWUser', 'X': 'CWEType'},
+                          {'U': 'CWUser', 'X': 'CWGroup'},
+                          {'U': 'CWUser', 'X': 'CWPermission'},
+                          {'U': 'CWUser', 'X': 'CWProperty'},
+                          {'U': 'CWUser', 'X': 'CWRType'},
+                          {'U': 'CWUser', 'X': 'CWRelation'},
+                          {'U': 'CWUser', 'X': 'CWUniqueTogetherConstraint'},
+                          {'U': 'CWUser', 'X': 'CWUser'},
+                          {'U': 'CWUser', 'X': 'Division'},
+                          {'U': 'CWUser', 'X': 'Email'},
+                          {'U': 'CWUser', 'X': 'EmailAddress'},
+                          {'U': 'CWUser', 'X': 'EmailPart'},
+                          {'U': 'CWUser', 'X': 'EmailThread'},
+                          {'U': 'CWUser', 'X': 'ExternalUri'},
+                          {'U': 'CWUser', 'X': 'File'},
+                          {'U': 'CWUser', 'X': 'Folder'},
+                          {'U': 'CWUser', 'X': 'Personne'},
+                          {'U': 'CWUser', 'X': 'RQLExpression'},
+                          {'U': 'CWUser', 'X': 'Societe'},
+                          {'U': 'CWUser', 'X': 'SubDivision'},
+                          {'U': 'CWUser', 'X': 'SubWorkflowExitPoint'},
+                          {'U': 'CWUser', 'X': 'Tag'},
+                          {'U': 'CWUser', 'X': 'TrInfo'},
+                          {'U': 'CWUser', 'X': 'Transition'},
+                          {'U': 'CWUser', 'X': 'Workflow'},
+                          {'U': 'CWUser', 'X': 'WorkflowTransition'}])],
+                       None, None,
+                       [self.system], {}, []),
+                      ('OneFetchStep',
+                       [(u'Any X WHERE X owned_by U, U login "anon", U is CWUser, X is IN(Card, Note, State)',
+                         [{'U': 'CWUser', 'X': 'Note'},
+                          {'U': 'CWUser', 'X': 'State'},
+                          {'U': 'CWUser', 'X': 'Card'}])],
+                       None, None,
+                       [self.system], {'X': 'table0.C0'}, [])
+                      ])
+                    ])
+
 
 class FakeVCSSource(AbstractSource):
     uri = 'ccc'
     support_entities = {'Card': True, 'Note': True}
     support_relations = {'multisource_inlined_rel': True,
                          'multisource_rel': True}
-    #dont_cross_relations = set(('fiche', 'in_state'))
-    #cross_relations = set(('multisource_crossed_rel',))
 
     def syntax_tree_search(self, *args, **kwargs):
         return []
--- a/server/test/unittest_multisources.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_multisources.py	Wed Nov 03 16:38:28 2010 +0100
@@ -101,39 +101,41 @@
 
     def test_eid_comp(self):
         rset = self.sexecute('Card X WHERE X eid > 1')
-        self.assertEquals(len(rset), 4)
+        self.assertEqual(len(rset), 4)
         rset = self.sexecute('Any X,T WHERE X title T, X eid > 1')
-        self.assertEquals(len(rset), 4)
+        self.assertEqual(len(rset), 4)
 
     def test_metainformation(self):
         rset = self.sexecute('Card X ORDERBY T WHERE X title T')
         # 2 added to the system source, 2 added to the external source
-        self.assertEquals(len(rset), 4)
+        self.assertEqual(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.assertEqual(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()
-        self.assertEquals(metainf['source'], {'adapter': 'pyrorql', 'base-url': 'http://extern.org/', 'uri': 'extern'})
-        self.assertEquals(metainf['type'], 'Card')
+        metainf = externent.cw_metainformation()
+        self.assertEqual(metainf['source'], {'adapter': 'pyrorql', 'base-url': 'http://extern.org/', 'uri': 'extern'})
+        self.assertEqual(metainf['type'], 'Card')
         self.assert_(metainf['extid'])
         etype = self.sexecute('Any ETN WHERE X is ET, ET name ETN, X eid %(x)s',
                              {'x': externent.eid})[0][0]
-        self.assertEquals(etype, 'Card')
+        self.assertEqual(etype, 'Card')
 
     def test_order_limit_offset(self):
         rsetbase = self.sexecute('Any W,X ORDERBY W,X WHERE X wikiid W')
-        self.assertEquals(len(rsetbase), 4)
-        self.assertEquals(sorted(rsetbase.rows), rsetbase.rows)
+        self.assertEqual(len(rsetbase), 4)
+        self.assertEqual(sorted(rsetbase.rows), rsetbase.rows)
         rset = self.sexecute('Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WHERE X wikiid W')
-        self.assertEquals(rset.rows, rsetbase.rows[2:4])
+        self.assertEqual(rset.rows, rsetbase.rows[2:4])
 
     def test_has_text(self):
         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,8 +147,11 @@
         cnx = self.login('anon')
         cu = cnx.cursor()
         rset = cu.execute('Any X WHERE X has_text "card"')
-        self.assertEquals(len(rset), 5, zip(rset.rows, rset.description))
-        Connection_close(cnx)
+        # 5: 4 card + 1 readable affaire
+        self.assertEqual(len(rset), 5, zip(rset.rows, rset.description))
+        rset = cu.execute('Any X ORDERBY FTIRANK(X) WHERE X has_text "card"')
+        self.assertEqual(len(rset), 5, zip(rset.rows, rset.description))
+        Connection_close(cnx.cnx) # cnx is a TestCaseConnectionProxy
 
     def test_synchronization(self):
         cu = cnx2.cursor()
@@ -173,14 +178,14 @@
         affeid = self.sexecute('Affaire X WHERE X ref "AFFREF"')[0][0]
         rset = self.sexecute('Any X,AA,AB WHERE E eid %(x)s, E in_state X, X name AA, X modification_date AB',
                             {'x': affeid})
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset[0][1], "pitetre")
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset[0][1], "pitetre")
 
     def test_simplifiable_var_2(self):
         affeid = self.sexecute('Affaire X WHERE X ref "AFFREF"')[0][0]
         rset = self.sexecute('Any E WHERE E eid %(x)s, E in_state S, NOT S name "moved"',
                             {'x': affeid, 'u': self.session.user.eid})
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
 
     def test_sort_func(self):
         self.sexecute('Affaire X ORDERBY DUMB_SORT(RF) WHERE X ref RF')
@@ -192,31 +197,31 @@
         iec1 = self.repo.extid2eid(self.repo.sources_by_uri['extern'], str(self.ec1),
                                    'Card', self.session)
         rset = self.sexecute('Any X WHERE X eid IN (%s, %s)' % (iec1, self.ic1))
-        self.assertEquals(sorted(r[0] for r in rset.rows), sorted([iec1, self.ic1]))
+        self.assertEqual(sorted(r[0] for r in rset.rows), sorted([iec1, self.ic1]))
 
     def test_greater_eid(self):
         rset = self.sexecute('Any X WHERE X eid > %s' % (self.ic1 - 1))
-        self.assertEquals(len(rset.rows), 2) # self.ic1 and self.ic2
+        self.assertEqual(len(rset.rows), 2) # self.ic1 and self.ic2
         cu = cnx2.cursor()
         ec2 = cu.execute('INSERT Card X: X title "glup"')[0][0]
         cnx2.commit()
         # 'X eid > something' should not trigger discovery
         rset = self.sexecute('Any X WHERE X eid > %s' % (self.ic1 - 1))
-        self.assertEquals(len(rset.rows), 2)
+        self.assertEqual(len(rset.rows), 2)
         # trigger discovery using another query
         crset = self.sexecute('Card X WHERE X title "glup"')
-        self.assertEquals(len(crset.rows), 1)
+        self.assertEqual(len(crset.rows), 1)
         rset = self.sexecute('Any X WHERE X eid > %s' % (self.ic1 - 1))
-        self.assertEquals(len(rset.rows), 3)
+        self.assertEqual(len(rset.rows), 3)
         rset = self.sexecute('Any MAX(X)')
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(rset.rows[0][0], crset[0][0])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(rset.rows[0][0], crset[0][0])
 
     def test_attr_unification_1(self):
         n1 = self.sexecute('INSERT Note X: X type "AFFREF"')[0][0]
         n2 = self.sexecute('INSERT Note X: X type "AFFREU"')[0][0]
         rset = self.sexecute('Any X,Y WHERE X is Note, Y is Affaire, X type T, Y ref T')
-        self.assertEquals(len(rset), 1, rset.rows)
+        self.assertEqual(len(rset), 1, rset.rows)
 
     def test_attr_unification_2(self):
         cu = cnx2.cursor()
@@ -225,7 +230,7 @@
         try:
             c1 = self.sexecute('INSERT Card C: C title "AFFREF"')[0][0]
             rset = self.sexecute('Any X,Y WHERE X is Card, Y is Affaire, X title T, Y ref T')
-            self.assertEquals(len(rset), 2, rset.rows)
+            self.assertEqual(len(rset), 2, rset.rows)
         finally:
             cu.execute('DELETE Card X WHERE X eid %(x)s', {'x': ec2})
             cnx2.commit()
@@ -242,26 +247,26 @@
         afeids = self.sexecute('Affaire X')
         ueids = self.sexecute('CWUser X')
         rset = self.sexecute('(Any X WHERE X is Affaire) UNION (Any X WHERE X is CWUser)')
-        self.assertEquals(sorted(r[0] for r in rset.rows),
+        self.assertEqual(sorted(r[0] for r in rset.rows),
                           sorted(r[0] for r in afeids + ueids))
 
     def test_subquery1(self):
         rsetbase = self.sexecute('Any W,X WITH W,X BEING (Any W,X ORDERBY W,X WHERE X wikiid W)')
-        self.assertEquals(len(rsetbase), 4)
-        self.assertEquals(sorted(rsetbase.rows), rsetbase.rows)
+        self.assertEqual(len(rsetbase), 4)
+        self.assertEqual(sorted(rsetbase.rows), rsetbase.rows)
         rset = self.sexecute('Any W,X LIMIT 2 OFFSET 2 WITH W,X BEING (Any W,X ORDERBY W,X WHERE X wikiid W)')
-        self.assertEquals(rset.rows, rsetbase.rows[2:4])
+        self.assertEqual(rset.rows, rsetbase.rows[2:4])
         rset = self.sexecute('Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WITH W,X BEING (Any W,X WHERE X wikiid W)')
-        self.assertEquals(rset.rows, rsetbase.rows[2:4])
+        self.assertEqual(rset.rows, rsetbase.rows[2:4])
         rset = self.sexecute('Any W,X WITH W,X BEING (Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WHERE X wikiid W)')
-        self.assertEquals(rset.rows, rsetbase.rows[2:4])
+        self.assertEqual(rset.rows, rsetbase.rows[2:4])
 
     def test_subquery2(self):
         affeid = self.sexecute('Affaire X WHERE X ref "AFFREF"')[0][0]
         rset = self.sexecute('Any X,AA,AB WITH X,AA,AB BEING (Any X,AA,AB WHERE E eid %(x)s, E in_state X, X name AA, X modification_date AB)',
                             {'x': affeid})
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset[0][1], "pitetre")
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset[0][1], "pitetre")
 
     def test_not_relation(self):
         states = set(tuple(x) for x in self.sexecute('Any S,SN WHERE S is State, S name SN'))
@@ -270,22 +275,22 @@
         states.remove((userstate.eid, userstate.name))
         notstates = set(tuple(x) for x in self.sexecute('Any S,SN WHERE S is State, S name SN, NOT X in_state S, X eid %(x)s',
                                                        {'x': self.session.user.eid}))
-        self.assertSetEquals(notstates, states)
+        self.assertSetEqual(notstates, states)
         aff1 = self.sexecute('Any X WHERE X is Affaire, X ref "AFFREF"')[0][0]
         aff1stateeid, aff1statename = self.sexecute('Any S,SN WHERE X eid %(x)s, X in_state S, S name SN', {'x': aff1})[0]
-        self.assertEquals(aff1statename, 'pitetre')
+        self.assertEqual(aff1statename, 'pitetre')
         states.add((userstate.eid, userstate.name))
         states.remove((aff1stateeid, aff1statename))
         notstates = set(tuple(x) for x in self.sexecute('Any S,SN WHERE S is State, S name SN, NOT X in_state S, X eid %(x)s',
                                                        {'x': aff1}))
-        self.assertSetEquals(notstates, states)
+        self.assertSetEqual(notstates, states)
 
     def test_absolute_url_base_url(self):
         cu = cnx2.cursor()
         ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
         cnx2.commit()
         lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
-        self.assertEquals(lc.absolute_url(), 'http://extern.org/card/eid/%s' % ceid)
+        self.assertEqual(lc.absolute_url(), 'http://extern.org/card/eid/%s' % ceid)
         cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
         cnx2.commit()
 
@@ -294,7 +299,7 @@
         ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
         cnx3.commit()
         lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
-        self.assertEquals(lc.absolute_url(), 'http://testing.fr/cubicweb/card/eid/%s' % lc.eid)
+        self.assertEqual(lc.absolute_url(), 'http://testing.fr/cubicweb/card/eid/%s' % lc.eid)
         cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
         cnx3.commit()
 
@@ -305,12 +310,13 @@
                      {'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)
-        self.assertEquals(rset.rows[0], [self.session.user.eid])
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.rows[0], [self.session.user.eid])
 
     def test_nonregr3(self):
         self.sexecute('DELETE Card X WHERE X eid %(x)s, NOT X multisource_inlined_rel Y', {'x': self.ic1})
--- a/server/test/unittest_querier.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_querier.py	Wed Nov 03 16:38:28 2010 +0100
@@ -16,7 +16,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unit tests for modules cubicweb.server.querier and cubicweb.server.querier_steps
+"""unit tests for modules cubicweb.server.querier and cubicweb.server.ssplanner
 """
 from datetime import date, datetime
 
@@ -57,7 +57,7 @@
 class MakeSchemaTC(TestCase):
     def test_known_values(self):
         solution = {'A': 'String', 'B': 'CWUser'}
-        self.assertEquals(make_schema((Variable('A'), Variable('B')), solution,
+        self.assertEqual(make_schema((Variable('A'), Variable('B')), solution,
                                       'table0', TYPEMAP),
                           ('C0 text,C1 integer', {'A': 'table0.C0', 'B': 'table0.C1'}))
 
@@ -84,7 +84,7 @@
     def test_preprocess_1(self):
         reid = self.execute('Any X WHERE X is CWRType, X name "owned_by"')[0][0]
         rqlst = self._prepare('Any COUNT(RDEF) WHERE RDEF relation_type X, X eid %(x)s', {'x': reid})
-        self.assertEquals(rqlst.solutions, [{'RDEF': 'CWAttribute'}, {'RDEF': 'CWRelation'}])
+        self.assertEqual(rqlst.solutions, [{'RDEF': 'CWAttribute'}, {'RDEF': 'CWRelation'}])
 
     def test_preprocess_2(self):
         teid = self.execute("INSERT Tag X: X name 'tag'")[0][0]
@@ -94,7 +94,7 @@
         rqlst = self._prepare('Any X WHERE E eid %(x)s, E tags X', {'x': teid})
         # the query may be optimized, should keep only one solution
         # (any one, etype will be discarded)
-        self.assertEquals(len(rqlst.solutions), 1)
+        self.assertEqual(len(rqlst.solutions), 1)
 
     def test_preprocess_security(self):
         plan = self._prepare_plan('Any ETN,COUNT(X) GROUPBY ETN '
@@ -102,24 +102,24 @@
         plan.session = self.user_groups_session('users')
         union = plan.rqlst
         plan.preprocess(union)
-        self.assertEquals(len(union.children), 1)
-        self.assertEquals(len(union.children[0].with_), 1)
+        self.assertEqual(len(union.children), 1)
+        self.assertEqual(len(union.children[0].with_), 1)
         subq = union.children[0].with_[0].query
-        self.assertEquals(len(subq.children), 3)
-        self.assertEquals([t.as_string() for t in union.children[0].selection],
+        self.assertEqual(len(subq.children), 3)
+        self.assertEqual([t.as_string() for t in union.children[0].selection],
                           ['ETN','COUNT(X)'])
-        self.assertEquals([t.as_string() for t in union.children[0].groupby],
+        self.assertEqual([t.as_string() for t in union.children[0].groupby],
                           ['ETN'])
         partrqls = sorted(((rqlst.as_string(), rqlst.solutions) for rqlst in subq.children))
         rql, solutions = partrqls[0]
-        self.assertEquals(rql,
+        self.assertEqual(rql,
                           'Any ETN,X WHERE X is ET, ET name ETN, (EXISTS(X owned_by %(B)s))'
                           ' OR ((((EXISTS(D concerne C?, C owned_by %(B)s, X identity D, C is Division, D is Affaire))'
                           ' OR (EXISTS(H concerne G?, G owned_by %(B)s, G is SubDivision, X identity H, H is Affaire)))'
                           ' OR (EXISTS(I concerne F?, F owned_by %(B)s, F is Societe, X identity I, I is Affaire)))'
                           ' OR (EXISTS(J concerne E?, E owned_by %(B)s, E is Note, X identity J, J is Affaire)))'
                           ', ET is CWEType, X is Affaire')
-        self.assertEquals(solutions, [{'C': 'Division',
+        self.assertEqual(solutions, [{'C': 'Division',
                                        'D': 'Affaire',
                                        'E': 'Note',
                                        'F': 'Societe',
@@ -130,8 +130,8 @@
                                        '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.assertListEquals(sorted(solutions),
+        self.assertEqual(rql,  'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
+        self.assertListEqual(sorted(solutions),
                               sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Card', 'ETN': 'String', 'ET': 'CWEType'},
@@ -143,19 +143,19 @@
                                       {'X': 'CWEType', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'CWAttribute', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'CWGroup', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWRelation', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWPermission', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWProperty', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWRType', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWUniqueTogetherConstraint', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Email', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailAddress', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailPart', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailThread', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWRelation', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWPermission', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWProperty', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWRType', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'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'},
@@ -169,10 +169,10 @@
                                       {'X': 'Workflow', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'WorkflowTransition', 'ETN': 'String', 'ET': 'CWEType'}]))
         rql, solutions = partrqls[2]
-        self.assertEquals(rql,
+        self.assertEqual(rql,
                           'Any ETN,X WHERE X is ET, ET name ETN, EXISTS(X owned_by %(C)s), '
                           'ET is CWEType, X is Basket')
-        self.assertEquals(solutions, [{'ET': 'CWEType',
+        self.assertEqual(solutions, [{'ET': 'CWEType',
                                        'X': 'Basket',
                                        'ETN': 'String',
                                        }])
@@ -182,45 +182,45 @@
         plan.session = self.user_groups_session('users')
         union = plan.rqlst
         plan.preprocess(union)
-        self.assertEquals(len(union.children), 1)
-        self.assertEquals(len(union.children[0].with_), 1)
+        self.assertEqual(len(union.children), 1)
+        self.assertEqual(len(union.children[0].with_), 1)
         subq = union.children[0].with_[0].query
-        self.assertEquals(len(subq.children), 3)
-        self.assertEquals([t.as_string() for t in union.children[0].selection],
+        self.assertEqual(len(subq.children), 3)
+        self.assertEqual([t.as_string() for t in union.children[0].selection],
                           ['MAX(X)'])
 
     def test_preprocess_nonregr(self):
         rqlst = self._prepare('Any S ORDERBY SI WHERE NOT S ecrit_par O, S para SI')
-        self.assertEquals(len(rqlst.solutions), 1)
+        self.assertEqual(len(rqlst.solutions), 1)
 
     def test_build_description(self):
         # should return an empty result set
         rset = self.execute('Any X WHERE X eid %(x)s', {'x': self.session.user.eid})
-        self.assertEquals(rset.description[0][0], 'CWUser')
+        self.assertEqual(rset.description[0][0], 'CWUser')
         rset = self.execute('Any 1')
-        self.assertEquals(rset.description[0][0], 'Int')
+        self.assertEqual(rset.description[0][0], 'Int')
         rset = self.execute('Any TRUE')
-        self.assertEquals(rset.description[0][0], 'Boolean')
+        self.assertEqual(rset.description[0][0], 'Boolean')
         rset = self.execute('Any "hop"')
-        self.assertEquals(rset.description[0][0], 'String')
+        self.assertEqual(rset.description[0][0], 'String')
         rset = self.execute('Any TODAY')
-        self.assertEquals(rset.description[0][0], 'Date')
+        self.assertEqual(rset.description[0][0], 'Date')
         rset = self.execute('Any NOW')
-        self.assertEquals(rset.description[0][0], 'Datetime')
+        self.assertEqual(rset.description[0][0], 'Datetime')
         rset = self.execute('Any %(x)s', {'x': 1})
-        self.assertEquals(rset.description[0][0], 'Int')
+        self.assertEqual(rset.description[0][0], 'Int')
         rset = self.execute('Any %(x)s', {'x': 1L})
-        self.assertEquals(rset.description[0][0], 'Int')
+        self.assertEqual(rset.description[0][0], 'Int')
         rset = self.execute('Any %(x)s', {'x': True})
-        self.assertEquals(rset.description[0][0], 'Boolean')
+        self.assertEqual(rset.description[0][0], 'Boolean')
         rset = self.execute('Any %(x)s', {'x': 1.0})
-        self.assertEquals(rset.description[0][0], 'Float')
+        self.assertEqual(rset.description[0][0], 'Float')
         rset = self.execute('Any %(x)s', {'x': datetime.now()})
-        self.assertEquals(rset.description[0][0], 'Datetime')
+        self.assertEqual(rset.description[0][0], 'Datetime')
         rset = self.execute('Any %(x)s', {'x': 'str'})
-        self.assertEquals(rset.description[0][0], 'String')
+        self.assertEqual(rset.description[0][0], 'String')
         rset = self.execute('Any %(x)s', {'x': u'str'})
-        self.assertEquals(rset.description[0][0], 'String')
+        self.assertEqual(rset.description[0][0], 'String')
 
 
 class QuerierTC(BaseQuerierTC):
@@ -244,46 +244,46 @@
                             {'data': Binary("xxx")})[0][0]
         fdata = self.execute('Any D WHERE X data D, X eid %(x)s', {'x': feid})[0][0]
         self.assertIsInstance(fdata, Binary)
-        self.assertEquals(fdata.getvalue(), 'xxx')
+        self.assertEqual(fdata.getvalue(), 'xxx')
 
     # selection queries tests #################################################
 
     def test_select_1(self):
         rset = self.execute('Any X ORDERBY X WHERE X is CWGroup')
         result, descr = rset.rows, rset.description
-        self.assertEquals(tuplify(result), [(1,), (2,), (3,), (4,)])
-        self.assertEquals(descr, [('CWGroup',), ('CWGroup',), ('CWGroup',), ('CWGroup',)])
+        self.assertEqual(tuplify(result), [(1,), (2,), (3,), (4,)])
+        self.assertEqual(descr, [('CWGroup',), ('CWGroup',), ('CWGroup',), ('CWGroup',)])
 
     def test_select_2(self):
         rset = self.execute('Any X ORDERBY N WHERE X is CWGroup, X name N')
-        self.assertEquals(tuplify(rset.rows), [(1,), (2,), (3,), (4,)])
-        self.assertEquals(rset.description, [('CWGroup',), ('CWGroup',), ('CWGroup',), ('CWGroup',)])
+        self.assertEqual(tuplify(rset.rows), [(1,), (2,), (3,), (4,)])
+        self.assertEqual(rset.description, [('CWGroup',), ('CWGroup',), ('CWGroup',), ('CWGroup',)])
         rset = self.execute('Any X ORDERBY N DESC WHERE X is CWGroup, X name N')
-        self.assertEquals(tuplify(rset.rows), [(4,), (3,), (2,), (1,)])
+        self.assertEqual(tuplify(rset.rows), [(4,), (3,), (2,), (1,)])
 
     def test_select_3(self):
         rset = self.execute('Any N GROUPBY N WHERE X is CWGroup, X name N')
         result, descr = rset.rows, rset.description
         result.sort()
-        self.assertEquals(tuplify(result), [('guests',), ('managers',), ('owners',), ('users',)])
-        self.assertEquals(descr, [('String',), ('String',), ('String',), ('String',)])
+        self.assertEqual(tuplify(result), [('guests',), ('managers',), ('owners',), ('users',)])
+        self.assertEqual(descr, [('String',), ('String',), ('String',), ('String',)])
 
     def test_select_is(self):
         rset = self.execute('Any X, TN ORDERBY TN LIMIT 10 WHERE X is T, T name TN')
         result, descr = rset.rows, rset.description
-        self.assertEquals(result[0][1], descr[0][0])
+        self.assertEqual(result[0][1], descr[0][0])
 
     def test_select_is_aggr(self):
         rset = self.execute('Any TN, COUNT(X) GROUPBY TN ORDERBY 2 DESC WHERE X is T, T name TN')
         result, descr = rset.rows, rset.description
-        self.assertEquals(descr[0][0], 'String')
-        self.assertEquals(descr[0][1], 'Int')
-        self.assertEquals(result[0][0], 'CWRelation') # XXX may change as schema evolve
+        self.assertEqual(descr[0][0], 'String')
+        self.assertEqual(descr[0][1], 'Int')
+        self.assertEqual(result[0][0], 'CWRelation') # XXX may change as schema evolve
 
     def test_select_groupby_orderby(self):
         rset = self.execute('Any N GROUPBY N ORDERBY N WHERE X is CWGroup, X name N')
-        self.assertEquals(tuplify(rset.rows), [('guests',), ('managers',), ('owners',), ('users',)])
-        self.assertEquals(rset.description, [('String',), ('String',), ('String',), ('String',)])
+        self.assertEqual(tuplify(rset.rows), [('guests',), ('managers',), ('owners',), ('users',)])
+        self.assertEqual(rset.description, [('String',), ('String',), ('String',), ('String',)])
 
     def test_select_complex_groupby(self):
         rset = self.execute('Any N GROUPBY N WHERE X name N')
@@ -295,20 +295,20 @@
 
     def test_select_complex_orderby(self):
         rset1 = self.execute('Any N ORDERBY N WHERE X name N')
-        self.assertEquals(sorted(rset1.rows), rset1.rows)
+        self.assertEqual(sorted(rset1.rows), rset1.rows)
         rset = self.execute('Any N ORDERBY N LIMIT 5 OFFSET 1 WHERE X name N')
-        self.assertEquals(rset.rows[0][0], rset1.rows[1][0])
-        self.assertEquals(len(rset), 5)
+        self.assertEqual(rset.rows[0][0], rset1.rows[1][0])
+        self.assertEqual(len(rset), 5)
 
     def test_select_5(self):
         rset = self.execute('Any X, TMP ORDERBY TMP WHERE X name TMP, X is CWGroup')
-        self.assertEquals(tuplify(rset.rows), [(1, 'guests',), (2, 'managers',), (3, 'owners',), (4, 'users',)])
-        self.assertEquals(rset.description, [('CWGroup', 'String',), ('CWGroup', 'String',), ('CWGroup', 'String',), ('CWGroup', 'String',)])
+        self.assertEqual(tuplify(rset.rows), [(1, 'guests',), (2, 'managers',), (3, 'owners',), (4, 'users',)])
+        self.assertEqual(rset.description, [('CWGroup', 'String',), ('CWGroup', 'String',), ('CWGroup', 'String',), ('CWGroup', 'String',)])
 
     def test_select_6(self):
         self.execute("INSERT Personne X: X nom 'bidule'")[0]
         rset = self.execute('Any Y where X name TMP, Y nom in (TMP, "bidule")')
-        #self.assertEquals(rset.description, [('Personne',), ('Personne',)])
+        #self.assertEqual(rset.description, [('Personne',), ('Personne',)])
         self.assert_(('Personne',) in rset.description)
         rset = self.execute('DISTINCT Any Y where X name TMP, Y nom in (TMP, "bidule")')
         self.assert_(('Personne',) in rset.description)
@@ -317,17 +317,17 @@
         peid = self.execute("INSERT Personne X: X nom 'bidule'")[0][0]
         seid = self.execute("INSERT Societe X: X nom 'chouette'")[0][0]
         rset = self.execute('Personne X WHERE NOT X nom "bidule"')
-        self.assertEquals(len(rset.rows), 0, rset.rows)
+        self.assertEqual(len(rset.rows), 0, rset.rows)
         rset = self.execute('Personne X WHERE NOT X nom "bid"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
         self.execute("SET P travaille S WHERE P nom 'bidule', S nom 'chouette'")
         rset = self.execute('Personne X WHERE NOT X travaille S')
-        self.assertEquals(len(rset.rows), 0, rset.rows)
+        self.assertEqual(len(rset.rows), 0, rset.rows)
 
     def test_select_is_in(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.execute("INSERT Societe X: X nom 'chouette'")
-        self.assertEquals(len(self.execute("Any X WHERE X is IN (Personne, Societe)")),
+        self.assertEqual(len(self.execute("Any X WHERE X is IN (Personne, Societe)")),
                           2)
 
     def test_select_not_rel(self):
@@ -336,9 +336,9 @@
         self.execute("INSERT Personne X: X nom 'autre'")
         self.execute("SET P travaille S WHERE P nom 'bidule', S nom 'chouette'")
         rset = self.execute('Personne X WHERE NOT X travaille S')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
         rset = self.execute('Personne X WHERE NOT X travaille S, S nom "chouette"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_nonregr_inlined(self):
         self.execute("INSERT Note X: X para 'bidule'")
@@ -347,15 +347,15 @@
         self.execute("SET X ecrit_par P WHERE X para 'bidule', P nom 'chouette'")
         rset = self.execute('Any U,T ORDERBY T DESC WHERE U is CWUser, '
                             'N ecrit_par U, N type T')#, {'x': self.ueid})
-        self.assertEquals(len(rset.rows), 0)
+        self.assertEqual(len(rset.rows), 0)
 
     def test_select_nonregr_edition_not(self):
         groupeids = set((1, 2, 3))
         groupreadperms = set(r[0] for r in self.execute('Any Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), X read_permission Y'))
         rset = self.execute('DISTINCT Any Y WHERE X is CWEType, X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y')
-        self.assertEquals(sorted(r[0] for r in rset.rows), sorted(groupeids - groupreadperms))
+        self.assertEqual(sorted(r[0] for r in rset.rows), sorted(groupeids - groupreadperms))
         rset = self.execute('DISTINCT Any Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y')
-        self.assertEquals(sorted(r[0] for r in rset.rows), sorted(groupeids - groupreadperms))
+        self.assertEqual(sorted(r[0] for r in rset.rows), sorted(groupeids - groupreadperms))
 
     def test_select_outer_join(self):
         peid1 = self.execute("INSERT Personne X: X nom 'bidule'")[0][0]
@@ -363,27 +363,27 @@
         seid1 = self.execute("INSERT Societe X: X nom 'chouette'")[0][0]
         seid2 = self.execute("INSERT Societe X: X nom 'chouetos'")[0][0]
         rset = self.execute('Any X,S ORDERBY X WHERE X travaille S?')
-        self.assertEquals(rset.rows, [[peid1, None], [peid2, None]])
+        self.assertEqual(rset.rows, [[peid1, None], [peid2, None]])
         self.execute("SET P travaille S WHERE P nom 'bidule', S nom 'chouette'")
         rset = self.execute('Any X,S ORDERBY X WHERE X travaille S?')
-        self.assertEquals(rset.rows, [[peid1, seid1], [peid2, None]])
+        self.assertEqual(rset.rows, [[peid1, seid1], [peid2, None]])
         rset = self.execute('Any S,X ORDERBY S WHERE X? travaille S')
-        self.assertEquals(rset.rows, [[seid1, peid1], [seid2, None]])
+        self.assertEqual(rset.rows, [[seid1, peid1], [seid2, None]])
 
     def test_select_outer_join_optimized(self):
         peid1 = self.execute("INSERT Personne X: X nom 'bidule'")[0][0]
         rset = self.execute('Any X WHERE X eid %(x)s, P? connait X', {'x':peid1})
-        self.assertEquals(rset.rows, [[peid1]])
+        self.assertEqual(rset.rows, [[peid1]])
         rset = self.execute('Any X WHERE X eid %(x)s, X require_permission P?',
                             {'x':peid1})
-        self.assertEquals(rset.rows, [[peid1]])
+        self.assertEqual(rset.rows, [[peid1]])
 
     def test_select_left_outer_join(self):
         rset = self.execute('DISTINCT Any G WHERE U? in_group G')
-        self.assertEquals(len(rset), 4)
+        self.assertEqual(len(rset), 4)
         rset = self.execute('DISTINCT Any G WHERE U? in_group G, U eid %(x)s',
                             {'x': self.session.user.eid})
-        self.assertEquals(len(rset), 4)
+        self.assertEqual(len(rset), 4)
 
     def test_select_ambigous_outer_join(self):
         teid = self.execute("INSERT Tag X: X name 'tag'")[0][0]
@@ -395,7 +395,7 @@
         self.failUnless(['users', 'tag'] in rset.rows)
         self.failUnless(['activated', None] in rset.rows)
         rset = self.execute("Any GN,TN ORDERBY GN WHERE T tags G?, T name TN, G name GN")
-        self.assertEquals(rset.rows, [[None, 'tagbis'], ['users', 'tag']])
+        self.assertEqual(rset.rows, [[None, 'tagbis'], ['users', 'tag']])
 
     def test_select_not_inline_rel(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
@@ -403,7 +403,7 @@
         self.execute("INSERT Note X: X type 'b'")
         self.execute("SET X ecrit_par Y WHERE X type 'a', Y nom 'bidule'")
         rset = self.execute('Note X WHERE NOT X ecrit_par P')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_not_unlinked_multiple_solutions(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
@@ -411,7 +411,7 @@
         self.execute("INSERT Note X: X type 'b'")
         self.execute("SET Y evaluee X WHERE X type 'a', Y nom 'bidule'")
         rset = self.execute('Note X WHERE NOT Y evaluee X')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_date_extraction(self):
         self.execute("INSERT Personne X: X nom 'foo', X datenaiss %(d)s",
@@ -421,41 +421,41 @@
         for funcname, result in test_data:
             rset = self.execute('Any %s(D) WHERE X is Personne, X datenaiss D'
                                 % funcname)
-            self.assertEquals(len(rset.rows), 1)
-            self.assertEquals(rset.rows[0][0], result)
-            self.assertEquals(rset.description, [('Int',)])
+            self.assertEqual(len(rset.rows), 1)
+            self.assertEqual(rset.rows[0][0], result)
+            self.assertEqual(rset.description, [('Int',)])
 
     def test_select_aggregat_count(self):
         rset = self.execute('Any COUNT(X)')
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(len(rset.rows[0]), 1)
-        self.assertEquals(rset.description, [('Int',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows[0]), 1)
+        self.assertEqual(rset.description, [('Int',)])
 
     def test_select_aggregat_sum(self):
         rset = self.execute('Any SUM(O) WHERE X ordernum O')
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(len(rset.rows[0]), 1)
-        self.assertEquals(rset.description, [('Int',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows[0]), 1)
+        self.assertEqual(rset.description, [('Int',)])
 
     def test_select_aggregat_min(self):
         rset = self.execute('Any MIN(X) WHERE X is Personne')
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(len(rset.rows[0]), 1)
-        self.assertEquals(rset.description, [('Personne',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows[0]), 1)
+        self.assertEqual(rset.description, [('Personne',)])
         rset = self.execute('Any MIN(O) WHERE X ordernum O')
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(len(rset.rows[0]), 1)
-        self.assertEquals(rset.description, [('Int',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows[0]), 1)
+        self.assertEqual(rset.description, [('Int',)])
 
     def test_select_aggregat_max(self):
         rset = self.execute('Any MAX(X) WHERE X is Personne')
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(len(rset.rows[0]), 1)
-        self.assertEquals(rset.description, [('Personne',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows[0]), 1)
+        self.assertEqual(rset.description, [('Personne',)])
         rset = self.execute('Any MAX(O) WHERE X ordernum O')
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(len(rset.rows[0]), 1)
-        self.assertEquals(rset.description, [('Int',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(len(rset.rows[0]), 1)
+        self.assertEqual(rset.description, [('Int',)])
 
     def test_select_custom_aggregat_concat_string(self):
         rset = self.execute('Any GROUP_CONCAT(N) WHERE X is CWGroup, X name N')
@@ -482,17 +482,17 @@
 
     def test_select_aggregat_sort(self):
         rset = self.execute('Any G, COUNT(U) GROUPBY G ORDERBY 2 WHERE U in_group G')
-        self.assertEquals(len(rset.rows), 2)
-        self.assertEquals(len(rset.rows[0]), 2)
-        self.assertEquals(rset.description[0], ('CWGroup', 'Int',))
+        self.assertEqual(len(rset.rows), 2)
+        self.assertEqual(len(rset.rows[0]), 2)
+        self.assertEqual(rset.description[0], ('CWGroup', 'Int',))
 
     def test_select_aggregat_having(self):
         rset = self.execute('Any N,COUNT(RDEF) GROUPBY N ORDERBY 2,N '
                             'WHERE RT name N, RDEF relation_type RT '
                             'HAVING COUNT(RDEF) > 10')
-        self.assertListEquals(rset.rows,
-                              [[u'description_format', 13],
-                               [u'description', 14],
+        self.assertListEqual(rset.rows,
+                              [[u'description_format', 12],
+                               [u'description', 13],
                                [u'name', 14],
                                [u'created_by', 38],
                                [u'creation_date', 38],
@@ -508,13 +508,13 @@
         rset = self.execute('Any U,COUNT(X) GROUPBY U '
                             'WHERE U eid %(x)s, X owned_by U '
                             'HAVING COUNT(X) > 10', {'x': self.ueid})
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(rset.rows[0][0], self.ueid)
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(rset.rows[0][0], self.ueid)
 
     def test_select_having_non_aggregat_1(self):
         rset = self.execute('Any L WHERE X login L, X creation_date CD '
                             'HAVING YEAR(CD) = %s' % date.today().year)
-        self.assertListEquals(rset.rows,
+        self.assertListEqual(rset.rows,
                               [[u'admin'],
                                [u'anon']])
 
@@ -522,7 +522,7 @@
         rset = self.execute('Any L GROUPBY L WHERE X login L, X in_group G, '
                             'X creation_date CD HAVING YEAR(CD) = %s OR COUNT(G) > 1'
                             % date.today().year)
-        self.assertListEquals(rset.rows,
+        self.assertListEqual(rset.rows,
                               [[u'admin'],
                                [u'anon']])
 
@@ -531,26 +531,26 @@
         rset = self.execute('Any X ORDERBY X,D LIMIT 5 WHERE X creation_date D')
         result = rset.rows
         result.sort()
-        self.assertEquals(tuplify(result), [(1,), (2,), (3,), (4,), (5,)])
+        self.assertEqual(tuplify(result), [(1,), (2,), (3,), (4,), (5,)])
 
     def test_select_upper(self):
         rset = self.execute('Any X, UPPER(L) ORDERBY L WHERE X is CWUser, X login L')
-        self.assertEquals(len(rset.rows), 2)
-        self.assertEquals(rset.rows[0][1], 'ADMIN')
-        self.assertEquals(rset.description[0], ('CWUser', 'String',))
-        self.assertEquals(rset.rows[1][1], 'ANON')
-        self.assertEquals(rset.description[1], ('CWUser', 'String',))
+        self.assertEqual(len(rset.rows), 2)
+        self.assertEqual(rset.rows[0][1], 'ADMIN')
+        self.assertEqual(rset.description[0], ('CWUser', 'String',))
+        self.assertEqual(rset.rows[1][1], 'ANON')
+        self.assertEqual(rset.description[1], ('CWUser', 'String',))
         eid = rset.rows[0][0]
         rset = self.execute('Any UPPER(L) WHERE X eid %s, X login L'%eid)
-        self.assertEquals(rset.rows[0][0], 'ADMIN')
-        self.assertEquals(rset.description, [('String',)])
+        self.assertEqual(rset.rows[0][0], 'ADMIN')
+        self.assertEqual(rset.description, [('String',)])
 
 ##     def test_select_simplified(self):
 ##         ueid = self.session.user.eid
 ##         rset = self.execute('Any L WHERE %s login L'%ueid)
-##         self.assertEquals(rset.rows[0][0], 'admin')
+##         self.assertEqual(rset.rows[0][0], 'admin')
 ##         rset = self.execute('Any L WHERE %(x)s login L', {'x':ueid})
-##         self.assertEquals(rset.rows[0][0], 'admin')
+##         self.assertEqual(rset.rows[0][0], 'admin')
 
     def test_select_searchable_text_1(self):
         rset = self.execute(u"INSERT Personne X: X nom 'bidüle'")
@@ -558,9 +558,9 @@
         rset = self.execute("INSERT Societe X: X nom 'chouette'")
         self.commit()
         rset = self.execute('Any X where X has_text %(text)s', {'text': u'bidüle'})
-        self.assertEquals(len(rset.rows), 2, rset.rows)
+        self.assertEqual(len(rset.rows), 2, rset.rows)
         rset = self.execute(u'Any N where N has_text "bidüle"')
-        self.assertEquals(len(rset.rows), 2, rset.rows)
+        self.assertEqual(len(rset.rows), 2, rset.rows)
         biduleeids = [r[0] for r in rset.rows]
         rset = self.execute(u'Any N where NOT N has_text "bidüle"')
         self.failIf([r[0] for r in rset.rows if r[0] in biduleeids])
@@ -573,7 +573,7 @@
         rset = self.execute("INSERT Societe X: X nom 'bidule'")
         self.commit()
         rset = self.execute('Personne N where N has_text "bidule"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_searchable_text_3(self):
         rset = self.execute("INSERT Personne X: X nom 'bidule', X sexe 'M'")
@@ -581,7 +581,7 @@
         rset = self.execute("INSERT Societe X: X nom 'bidule'")
         self.commit()
         rset = self.execute('Any X where X has_text "bidule" and X sexe "M"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_multiple_searchable_text(self):
         self.execute(u"INSERT Personne X: X nom 'bidüle'")
@@ -592,20 +592,20 @@
                             {'text': u'bidüle',
                              'text2': u'chouette',}
                             )
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_no_descr(self):
         rset = self.execute('Any X WHERE X is CWGroup', build_descr=0)
         rset.rows.sort()
-        self.assertEquals(tuplify(rset.rows), [(1,), (2,), (3,), (4,)])
-        self.assertEquals(rset.description, ())
+        self.assertEqual(tuplify(rset.rows), [(1,), (2,), (3,), (4,)])
+        self.assertEqual(rset.description, ())
 
     def test_select_limit_offset(self):
         rset = self.execute('CWGroup X ORDERBY N LIMIT 2 WHERE X name N')
-        self.assertEquals(tuplify(rset.rows), [(1,), (2,)])
-        self.assertEquals(rset.description, [('CWGroup',), ('CWGroup',)])
+        self.assertEqual(tuplify(rset.rows), [(1,), (2,)])
+        self.assertEqual(rset.description, [('CWGroup',), ('CWGroup',)])
         rset = self.execute('CWGroup X ORDERBY N LIMIT 2 OFFSET 2 WHERE X name N')
-        self.assertEquals(tuplify(rset.rows), [(3,), (4,)])
+        self.assertEqual(tuplify(rset.rows), [(3,), (4,)])
 
     def test_select_symmetric(self):
         self.execute("INSERT Personne X: X nom 'machin'")
@@ -615,24 +615,24 @@
         self.execute("SET X connait Y WHERE X nom 'chouette', Y nom 'bidule'")
         self.execute("SET X connait Y WHERE X nom 'machin', Y nom 'chouette'")
         rset = self.execute('Any P where P connait P2')
-        self.assertEquals(len(rset.rows), 3, rset.rows)
+        self.assertEqual(len(rset.rows), 3, rset.rows)
         rset = self.execute('Any P where NOT P connait P2')
-        self.assertEquals(len(rset.rows), 1, rset.rows) # trucmuche
+        self.assertEqual(len(rset.rows), 1, rset.rows) # trucmuche
         rset = self.execute('Any P where P connait P2, P2 nom "bidule"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
         rset = self.execute('Any P where P2 connait P, P2 nom "bidule"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
         rset = self.execute('Any P where P connait P2, P2 nom "chouette"')
-        self.assertEquals(len(rset.rows), 2, rset.rows)
+        self.assertEqual(len(rset.rows), 2, rset.rows)
         rset = self.execute('Any P where P2 connait P, P2 nom "chouette"')
-        self.assertEquals(len(rset.rows), 2, rset.rows)
+        self.assertEqual(len(rset.rows), 2, rset.rows)
 
     def test_select_inline(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.execute("INSERT Note X: X type 'a'")
         self.execute("SET X ecrit_par Y WHERE X type 'a', Y nom 'bidule'")
         rset = self.execute('Any N where N ecrit_par X, X nom "bidule"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_creation_date(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
@@ -679,19 +679,15 @@
         self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_ordered_distinct_1(self):
-        self.execute("INSERT Affaire X: X sujet 'cool', X ref '1'")
-        self.execute("INSERT Affaire X: X sujet 'cool', X ref '2'")
-        rset = self.execute('DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R')
-        self.assertEqual(rset.rows, [['cool']])
+        self.assertRaises(BadRQLQuery,
+                          self.execute, 'DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R')
 
     def test_select_ordered_distinct_2(self):
         self.execute("INSERT Affaire X: X sujet 'minor'")
-        self.execute("INSERT Affaire X: X sujet 'important'")
-        self.execute("INSERT Affaire X: X sujet 'normal'")
         self.execute("INSERT Affaire X: X sujet 'zou'")
         self.execute("INSERT Affaire X: X sujet 'abcd'")
         rset = self.execute('DISTINCT Any S ORDERBY S WHERE A is Affaire, A sujet S')
-        self.assertEqual(rset.rows, [['abcd'], ['important'], ['minor'], ['normal'], ['zou']])
+        self.assertEqual(rset.rows, [['abcd'], ['minor'], ['zou']])
 
     def test_select_ordered_distinct_3(self):
         rset = self.execute('DISTINCT Any N ORDERBY GROUP_SORT_VALUE(N) WHERE X is CWGroup, X name N')
@@ -706,13 +702,13 @@
     def test_select_explicit_eid(self):
         rset = self.execute('Any X,E WHERE X owned_by U, X eid E, U eid %(u)s', {'u': self.session.user.eid})
         self.failUnless(rset)
-        self.assertEquals(rset.description[0][1], 'Int')
+        self.assertEqual(rset.description[0][1], 'Int')
 
 #     def test_select_rewritten_optional(self):
 #         eid = self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
 #         rset = self.execute('Any X WHERE X eid %(x)s, EXISTS(X owned_by U) OR EXISTS(X concerne S?, S owned_by U)',
 #                             {'x': eid}, 'x')
-#         self.assertEquals(rset.rows, [[eid]])
+#         self.assertEqual(rset.rows, [[eid]])
 
     def test_today_bug(self):
         self.execute("INSERT Tag X: X name 'bidule', X creation_date NOW")
@@ -733,14 +729,14 @@
     def test_select_boolean(self):
         rset = self.execute('Any N WHERE X is CWEType, X name N, X final %(val)s',
                             {'val': True})
-        self.assertEquals(sorted(r[0] for r in rset.rows), ['Boolean', 'Bytes',
+        self.assertEqual(sorted(r[0] for r in rset.rows), ['Boolean', 'Bytes',
                                                             'Date', 'Datetime',
                                                             'Decimal', 'Float',
                                                             'Int', 'Interval',
                                                             'Password', 'String',
                                                             'Time'])
         rset = self.execute('Any N WHERE X is CWEType, X name N, X final TRUE')
-        self.assertEquals(sorted(r[0] for r in rset.rows), ['Boolean', 'Bytes',
+        self.assertEqual(sorted(r[0] for r in rset.rows), ['Boolean', 'Bytes',
                                                             'Date', 'Datetime',
                                                             'Decimal', 'Float',
                                                             'Int', 'Interval',
@@ -749,17 +745,17 @@
 
     def test_select_constant(self):
         rset = self.execute('Any X, "toto" ORDERBY X WHERE X is CWGroup')
-        self.assertEquals(rset.rows,
+        self.assertEqual(rset.rows,
                           map(list, zip((1,2,3,4), ('toto','toto','toto','toto',))))
         self.assertIsInstance(rset[0][1], unicode)
-        self.assertEquals(rset.description,
+        self.assertEqual(rset.description,
                           zip(('CWGroup', 'CWGroup', 'CWGroup', 'CWGroup'),
                               ('String', 'String', 'String', 'String',)))
         rset = self.execute('Any X, %(value)s ORDERBY X WHERE X is CWGroup', {'value': 'toto'})
-        self.assertEquals(rset.rows,
+        self.assertEqual(rset.rows,
                           map(list, zip((1,2,3,4), ('toto','toto','toto','toto',))))
         self.assertIsInstance(rset[0][1], unicode)
-        self.assertEquals(rset.description,
+        self.assertEqual(rset.description,
                           zip(('CWGroup', 'CWGroup', 'CWGroup', 'CWGroup'),
                               ('String', 'String', 'String', 'String',)))
         rset = self.execute('Any X,GN WHERE X is CWUser, G is CWGroup, X login "syt", X in_group G, G name GN')
@@ -770,9 +766,9 @@
                             ' UNION '
                             '(Any X,N WHERE X name N, X state_of WF, WF workflow_of E, E name %(name)s))',
                             {'name': 'CWUser'})
-        self.assertEquals([x[1] for x in rset.rows],
+        self.assertEqual([x[1] for x in rset.rows],
                           ['activate', 'activated', 'deactivate', 'deactivated'])
-        self.assertEquals(rset.description,
+        self.assertEqual(rset.description,
                           [('Transition', 'String'), ('State', 'String'),
                            ('Transition', 'String'), ('State', 'String')])
 
@@ -792,13 +788,13 @@
                             '((Any N,COUNT(X) GROUPBY N WHERE X name N, X is State HAVING COUNT(X)>1)'
                             ' UNION '
                             '(Any N,COUNT(X) GROUPBY N WHERE X name N, X is Transition HAVING COUNT(X)>1))')
-        self.assertEquals(rset.rows, [[u'hop', 2], [u'hop', 2]])
+        self.assertEqual(rset.rows, [[u'hop', 2], [u'hop', 2]])
 
     def test_select_union_selection_with_diff_variables(self):
         rset = self.execute('(Any N WHERE X name N, X is State)'
                             ' UNION '
                             '(Any NN WHERE XX name NN, XX is Transition)')
-        self.assertEquals(sorted(r[0] for r in rset.rows),
+        self.assertEqual(sorted(r[0] for r in rset.rows),
                           ['abort', 'activate', 'activated', 'ben non',
                            'deactivate', 'deactivated', 'done', 'en cours',
                            'end', 'finie', 'markasdone', 'pitetre', 'redoit',
@@ -811,7 +807,7 @@
                             ' UNION '
                             '(Any Y WHERE Y eid %(y)s)',
                             {'x': eid1, 'y': eid2})
-        self.assertEquals(rset.description[:], [('CWGroup',), ('CWUser',)])
+        self.assertEqual(rset.description[:], [('CWGroup',), ('CWUser',)])
 
     def test_exists(self):
         geid = self.execute("INSERT CWGroup X: X name 'lulufanclub'")[0][0]
@@ -819,15 +815,15 @@
         peid = self.execute("INSERT Personne X: X prenom 'lulu', X nom 'petit'")[0][0]
         rset = self.execute("Any X WHERE X prenom 'lulu',"
                             "EXISTS (U in_group G, G name 'lulufanclub' OR G name 'managers');")
-        self.assertEquals(rset.rows, [[peid]])
+        self.assertEqual(rset.rows, [[peid]])
 
     def test_identity(self):
         eid = self.execute('Any X WHERE X identity Y, Y eid 1')[0][0]
-        self.assertEquals(eid, 1)
+        self.assertEqual(eid, 1)
         eid = self.execute('Any X WHERE Y identity X, Y eid 1')[0][0]
-        self.assertEquals(eid, 1)
+        self.assertEqual(eid, 1)
         login = self.execute('Any L WHERE X login "admin", X identity Y, Y login L')[0][0]
-        self.assertEquals(login, 'admin')
+        self.assertEqual(login, 'admin')
 
     def test_select_date_mathexp(self):
         rset = self.execute('Any X, TODAY - CD WHERE X is CWUser, X creation_date CD')
@@ -837,62 +833,73 @@
         rset = self.execute('Any X, NOW - CD WHERE X is Personne, X creation_date CD')
         self.failUnlessEqual(rset.description[0][1], 'Interval')
 
-    def test_select_subquery_aggregat(self):
+    def test_select_subquery_aggregat_1(self):
         # percent users by groups
         self.execute('SET X in_group G WHERE G name "users"')
         rset = self.execute('Any GN, COUNT(X)*100/T GROUPBY GN ORDERBY 2,1'
                             ' WHERE G name GN, X in_group G'
                             ' WITH T BEING (Any COUNT(U) WHERE U is CWUser)')
-        self.assertEquals(rset.rows, [[u'guests', 50], [u'managers', 50], [u'users', 100]])
-        self.assertEquals(rset.description, [('String', 'Int'), ('String', 'Int'), ('String', 'Int')])
+        self.assertEqual(rset.rows, [[u'guests', 50], [u'managers', 50], [u'users', 100]])
+        self.assertEqual(rset.description, [('String', 'Int'), ('String', 'Int'), ('String', 'Int')])
+
+    def test_select_subquery_aggregat_2(self):
+        expected = self.execute('Any X, 0, COUNT(T) GROUPBY X '
+                                'WHERE X is Workflow, T transition_of X').rows
+        rset = self.execute('''
+Any P1,B,E WHERE P1 identity P2 WITH
+  P1,B BEING (Any P,COUNT(T) GROUPBY P WHERE P is Workflow, T is Transition,
+              T? transition_of P, T type "auto"),
+  P2,E BEING (Any P,COUNT(T) GROUPBY P WHERE P is Workflow, T is Transition,
+              T? transition_of P, T type "normal")''')
+        self.assertEqual(sorted(rset.rows), sorted(expected))
 
     def test_select_subquery_const(self):
         rset = self.execute('Any X WITH X BEING ((Any NULL) UNION (Any "toto"))')
-        self.assertEquals(rset.rows, [[None], ['toto']])
-        self.assertEquals(rset.description, [(None,), ('String',)])
+        self.assertEqual(rset.rows, [[None], ['toto']])
+        self.assertEqual(rset.description, [(None,), ('String',)])
 
     # insertion queries tests #################################################
 
     def test_insert_is(self):
         eid, = self.execute("INSERT Personne X: X nom 'bidule'")[0]
         etype, = self.execute("Any TN WHERE X is T, X eid %s, T name TN" % eid)[0]
-        self.assertEquals(etype, 'Personne')
+        self.assertEqual(etype, 'Personne')
         self.execute("INSERT Personne X: X nom 'managers'")
 
     def test_insert_1(self):
         rset = self.execute("INSERT Personne X: X nom 'bidule'")
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(rset.description, [('Personne',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(rset.description, [('Personne',)])
         rset = self.execute('Personne X WHERE X nom "bidule"')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne',)])
+        self.assertEqual(rset.description, [('Personne',)])
 
     def test_insert_1_multiple(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.execute("INSERT Personne X: X nom 'chouette'")
         rset = self.execute("INSERT Societe Y: Y nom N, P travaille Y WHERE P nom N")
-        self.assertEquals(len(rset.rows), 2)
-        self.assertEquals(rset.description, [('Societe',), ('Societe',)])
+        self.assertEqual(len(rset.rows), 2)
+        self.assertEqual(rset.description, [('Societe',), ('Societe',)])
 
     def test_insert_2(self):
         rset = self.execute("INSERT Personne X, Personne Y: X nom 'bidule', Y nom 'tutu'")
-        self.assertEquals(rset.description, [('Personne', 'Personne')])
+        self.assertEqual(rset.description, [('Personne', 'Personne')])
         rset = self.execute('Personne X WHERE X nom "bidule" or X nom "tutu"')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne',), ('Personne',)])
+        self.assertEqual(rset.description, [('Personne',), ('Personne',)])
 
     def test_insert_3(self):
         self.execute("INSERT Personne X: X nom Y WHERE U login 'admin', U login Y")
         rset = self.execute('Personne X WHERE X nom "admin"')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne',)])
+        self.assertEqual(rset.description, [('Personne',)])
 
     def test_insert_4(self):
         self.execute("INSERT Societe Y: Y nom 'toto'")
         self.execute("INSERT Personne X: X nom 'bidule', X travaille Y WHERE Y nom 'toto'")
         rset = self.execute('Any X, Y WHERE X nom "bidule", Y nom "toto", X travaille Y')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne', 'Societe',)])
+        self.assertEqual(rset.description, [('Personne', 'Societe',)])
 
     def test_insert_4bis(self):
         peid = self.execute("INSERT Personne X: X nom 'bidule'")[0][0]
@@ -917,7 +924,7 @@
         self.execute("INSERT Societe Y: Y nom 'toto', X travaille Y WHERE X nom 'bidule'")
         rset = self.execute('Any X, Y WHERE X nom "bidule", Y nom "toto", X travaille Y')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne', 'Societe',)])
+        self.assertEqual(rset.description, [('Personne', 'Societe',)])
 
     def test_insert_5bis(self):
         peid = self.execute("INSERT Personne X: X nom 'bidule'")[0][0]
@@ -925,45 +932,45 @@
                      {'x': peid})
         rset = self.execute('Any X, Y WHERE X nom "bidule", Y nom "toto", X travaille Y')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne', 'Societe',)])
+        self.assertEqual(rset.description, [('Personne', 'Societe',)])
 
     def test_insert_6(self):
         self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto', X travaille Y")
         rset = self.execute('Any X, Y WHERE X nom "bidule", Y nom "toto", X travaille Y')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne', 'Societe',)])
+        self.assertEqual(rset.description, [('Personne', 'Societe',)])
 
     def test_insert_7(self):
         self.execute("INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y WHERE U login 'admin', U login N")
         rset = self.execute('Any X, Y WHERE X nom "admin", Y nom "toto", X travaille Y')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne', 'Societe',)])
+        self.assertEqual(rset.description, [('Personne', 'Societe',)])
 
     def test_insert_7_2(self):
         self.execute("INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y WHERE U login N")
         rset = self.execute('Any X, Y WHERE Y nom "toto", X travaille Y')
-        self.assertEquals(len(rset), 2)
-        self.assertEquals(rset.description, [('Personne', 'Societe',),
+        self.assertEqual(len(rset), 2)
+        self.assertEqual(rset.description, [('Personne', 'Societe',),
                                              ('Personne', 'Societe',)])
 
     def test_insert_8(self):
         self.execute("INSERT Societe Y, Personne X: Y nom N, X nom 'toto', X travaille Y WHERE U login 'admin', U login N")
         rset = self.execute('Any X, Y WHERE X nom "toto", Y nom "admin", X travaille Y')
         self.assert_(rset.rows)
-        self.assertEquals(rset.description, [('Personne', 'Societe',)])
+        self.assertEqual(rset.description, [('Personne', 'Societe',)])
 
     def test_insert_9(self):
         self.execute("INSERT Societe X: X nom  'Lo'")
         self.execute("INSERT Societe X: X nom  'Gi'")
         self.execute("INSERT SubDivision X: X nom  'Lab'")
         rset = self.execute("INSERT Personne X: X nom N, X travaille Y, X travaille_subdivision Z WHERE Y is Societe, Z is SubDivision, Y nom N")
-        self.assertEquals(len(rset), 2)
-        self.assertEquals(rset.description, [('Personne',), ('Personne',)])
-        # self.assertSetEquals(set(x.nom for x in rset.entities()),
+        self.assertEqual(len(rset), 2)
+        self.assertEqual(rset.description, [('Personne',), ('Personne',)])
+        # self.assertSetEqual(set(x.nom for x in rset.entities()),
         #                      ['Lo', 'Gi'])
-        # self.assertSetEquals(set(y.nom for x in rset.entities() for y in x.travaille),
+        # self.assertSetEqual(set(y.nom for x in rset.entities() for y in x.travaille),
         #                      ['Lo', 'Gi'])
-        # self.assertEquals([y.nom for x in rset.entities() for y in x.travaille_subdivision],
+        # self.assertEqual([y.nom for x in rset.entities() for y in x.travaille_subdivision],
         #                      ['Lab', 'Lab'])
 
     def test_insert_query_error(self):
@@ -985,7 +992,7 @@
         rset = self.execute('INSERT CWUser E, EmailAddress EM: E login "X", E upassword "X", '
                             'E primary_email EM, EM address "X", E in_group G '
                             'WHERE G name "managers"')
-        self.assertEquals(list(rset.description[0]), ['CWUser', 'EmailAddress'])
+        self.assertEqual(list(rset.description[0]), ['CWUser', 'EmailAddress'])
 
     # deletion queries tests ##################################################
 
@@ -1000,10 +1007,10 @@
 
     def test_delete_2(self):
         rset = self.execute("INSERT Personne X, Personne Y, Societe Z : X nom 'syt', Y nom 'adim', Z nom 'Logilab', X travaille Z, Y travaille Z")
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(len(rset[0]), 3)
-        self.assertEquals(rset.description[0], ('Personne', 'Personne', 'Societe'))
-        self.assertEquals(self.execute('Any N WHERE X nom N, X eid %s'% rset[0][0])[0][0], 'syt')
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(len(rset[0]), 3)
+        self.assertEqual(rset.description[0], ('Personne', 'Personne', 'Societe'))
+        self.assertEqual(self.execute('Any N WHERE X nom N, X eid %s'% rset[0][0])[0][0], 'syt')
         rset = self.execute('Personne X WHERE X travaille Y, Y nom "Logilab"')
         self.assertEqual(len(rset.rows), 2, rset.rows)
         self.execute("DELETE X travaille Y WHERE X is Personne, Y nom 'Logilabo'")
@@ -1029,16 +1036,16 @@
         teid2 = self.execute("INSERT Folder T: T name 'tutu'")[0][0]
         self.execute('SET X see_also Y WHERE X eid %s, Y eid %s' % (teid1, teid2))
         rset = self.execute('Any X,Y WHERE X see_also Y')
-        self.assertEquals(len(rset) , 2, rset.rows)
+        self.assertEqual(len(rset) , 2, rset.rows)
         self.execute('DELETE X see_also Y WHERE X eid %s, Y eid %s' % (teid1, teid2))
         rset = self.execute('Any X,Y WHERE X see_also Y')
-        self.assertEquals(len(rset) , 0)
+        self.assertEqual(len(rset) , 0)
         self.execute('SET X see_also Y WHERE X eid %s, Y eid %s' % (teid1, teid2))
         rset = self.execute('Any X,Y WHERE X see_also Y')
-        self.assertEquals(len(rset) , 2)
+        self.assertEqual(len(rset) , 2)
         self.execute('DELETE X see_also Y WHERE X eid %s, Y eid %s' % (teid2, teid1))
         rset = self.execute('Any X,Y WHERE X see_also Y')
-        self.assertEquals(len(rset) , 0)
+        self.assertEqual(len(rset) , 0)
 
     def test_nonregr_delete_cache(self):
         """test that relations are properly cleaned when an entity is deleted
@@ -1053,28 +1060,28 @@
         self.o.execute(s, "DELETE Email X")
         sqlc = s.pool['system']
         sqlc.execute('SELECT * FROM recipients_relation')
-        self.assertEquals(len(sqlc.fetchall()), 0)
+        self.assertEqual(len(sqlc.fetchall()), 0)
         sqlc.execute('SELECT * FROM owned_by_relation WHERE eid_from=%s'%eeid)
-        self.assertEquals(len(sqlc.fetchall()), 0)
+        self.assertEqual(len(sqlc.fetchall()), 0)
 
     def test_nonregr_delete_cache2(self):
         eid = self.execute("INSERT Folder T: T name 'toto'")[0][0]
         self.commit()
         # fill the cache
         self.execute("Any X WHERE X eid %(x)s", {'x': eid})
-        self.execute("Any X WHERE X eid %s" %eid)
+        self.execute("Any X WHERE X eid %s" % eid)
         self.execute("Folder X WHERE X eid %(x)s", {'x': eid})
-        self.execute("Folder X WHERE X eid %s" %eid)
-        self.execute("DELETE Folder T WHERE T eid %s"%eid)
+        self.execute("Folder X WHERE X eid %s" % eid)
+        self.execute("DELETE Folder T WHERE T eid %s" % eid)
         self.commit()
         rset = self.execute("Any X WHERE X eid %(x)s", {'x': eid})
-        self.assertEquals(rset.rows, [])
-        rset = self.execute("Any X WHERE X eid %s" %eid)
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
+        rset = self.execute("Any X WHERE X eid %s" % eid)
+        self.assertEqual(rset.rows, [])
         rset = self.execute("Folder X WHERE X eid %(x)s", {'x': eid})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         rset = self.execute("Folder X WHERE X eid %s" %eid)
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
 
     # update queries tests ####################################################
 
@@ -1090,7 +1097,7 @@
     def test_update_2(self):
         peid, seid = self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'")[0]
         rset = self.execute("SET X travaille Y WHERE X nom 'bidule', Y nom 'toto'")
-        self.assertEquals(tuplify(rset.rows), [(peid, seid)])
+        self.assertEqual(tuplify(rset.rows), [(peid, seid)])
         rset = self.execute('Any X, Y WHERE X travaille Y')
         self.assertEqual(len(rset.rows), 1)
 
@@ -1118,8 +1125,8 @@
         peid1 = self.execute("INSERT Personne Y: Y nom 'tutu'")[0][0]
         peid2 = self.execute("INSERT Personne Y: Y nom 'toto'")[0][0]
         self.execute("SET X nom 'tutu', Y nom 'toto' WHERE X nom 'toto', Y nom 'tutu'")
-        self.assertEquals(self.execute('Any X WHERE X nom "toto"').rows, [[peid1]])
-        self.assertEquals(self.execute('Any X WHERE X nom "tutu"').rows, [[peid2]])
+        self.assertEqual(self.execute('Any X WHERE X nom "toto"').rows, [[peid1]])
+        self.assertEqual(self.execute('Any X WHERE X nom "tutu"').rows, [[peid2]])
 
     def test_update_multiple2(self):
         ueid = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto'")[0][0]
@@ -1142,13 +1149,13 @@
                      {'order': orders[splitidx]})
         orders2 = [r[0] for r in self.execute('Any O ORDERBY O WHERE ST name "Personne", X from_entity ST, X ordernum O')]
         orders = orders[:splitidx] + [o+1 for o in orders[splitidx:]]
-        self.assertEquals(orders2, orders)
+        self.assertEqual(orders2, orders)
 
     def test_update_string_concat(self):
         beid = self.execute("INSERT Bookmark Y: Y title 'toto', Y path '/view'")[0][0]
         self.execute('SET X title XN + %(suffix)s WHERE X is Bookmark, X title XN', {'suffix': u'-moved'})
         newname = self.execute('Any XN WHERE X eid %(x)s, X title XN', {'x': beid})[0][0]
-        self.assertEquals(newname, 'toto-moved')
+        self.assertEqual(newname, 'toto-moved')
 
     def test_update_query_error(self):
         self.execute("INSERT Personne Y: Y nom 'toto'")
@@ -1156,39 +1163,38 @@
         self.assertRaises(QueryError, self.execute, "SET X nom 'toto', X has_text 'tutu' WHERE X is Personne")
         self.assertRaises(QueryError, self.execute, "SET X login 'tutu', X eid %s" % cnx.user(self.session).eid)
 
-
     # upassword encryption tests #################################################
 
     def test_insert_upassword(self):
         rset = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto'")
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(rset.description, [('CWUser',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(rset.description, [('CWUser',)])
         self.assertRaises(Unauthorized,
                           self.execute, "Any P WHERE X is CWUser, X login 'bob', X upassword P")
         cursor = self.pool['system']
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
         passwd = str(cursor.fetchone()[0])
-        self.assertEquals(passwd, crypt_password('toto', passwd[:2]))
+        self.assertEqual(passwd, crypt_password('toto', passwd[:2]))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(rset.description, [('CWUser',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(rset.description, [('CWUser',)])
 
     def test_update_upassword(self):
         cursor = self.pool['system']
         rset = self.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s", {'pwd': 'toto'})
-        self.assertEquals(rset.description[0][0], 'CWUser')
+        self.assertEqual(rset.description[0][0], 'CWUser')
         rset = self.execute("SET X upassword %(pwd)s WHERE X is CWUser, X login 'bob'",
                             {'pwd': 'tutu'})
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
         passwd = str(cursor.fetchone()[0])
-        self.assertEquals(passwd, crypt_password('tutu', passwd[:2]))
+        self.assertEqual(passwd, crypt_password('tutu', passwd[:2]))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
-        self.assertEquals(len(rset.rows), 1)
-        self.assertEquals(rset.description, [('CWUser',)])
+        self.assertEqual(len(rset.rows), 1)
+        self.assertEqual(rset.description, [('CWUser',)])
 
     # non regression tests ####################################################
 
@@ -1196,11 +1202,11 @@
         teid = self.execute("INSERT Tag X: X name 'tag'")[0][0]
         self.execute("SET X tags Y WHERE X name 'tag', Y is State, Y name 'activated'")
         rset = self.execute('Any X WHERE T tags X')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
         rset = self.execute('Any T WHERE T tags X, X is State')
-        self.assertEquals(rset.rows, [[teid]])
+        self.assertEqual(rset.rows, [[teid]])
         rset = self.execute('Any T WHERE T tags X')
-        self.assertEquals(rset.rows, [[teid]])
+        self.assertEqual(rset.rows, [[teid]])
 
     def test_nonregr_2(self):
         teid = self.execute("INSERT Tag X: X name 'tag'")[0][0]
@@ -1209,7 +1215,7 @@
                        {'g': geid, 't': teid})
         rset = self.execute('Any X WHERE E eid %(x)s, E tags X',
                               {'x': teid})
-        self.assertEquals(rset.rows, [[geid]])
+        self.assertEqual(rset.rows, [[geid]])
 
     def test_nonregr_3(self):
         """bad sql generated on the second query (destination_state is not
@@ -1217,7 +1223,7 @@
         """
         rset = self.execute('Any S,ES,T WHERE S state_of WF, WF workflow_of ET, ET name "CWUser",'
                              'ES allowed_transition T, T destination_state S')
-        self.assertEquals(len(rset.rows), 2)
+        self.assertEqual(len(rset.rows), 2)
 
     def test_nonregr_4(self):
         # fix variables'type, else we get (nb of entity types with a 'name' attribute)**3
@@ -1225,7 +1231,7 @@
         # by the server (or client lib)
         rset = self.execute('Any ER,SE,OE WHERE SE name "Comment", ER name "comments", OE name "Comment",'
                             'ER is CWRType, SE is CWEType, OE is CWEType')
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
 
     def test_nonregr_5(self):
         # jpl #15505: equivalent queries returning different result sets
@@ -1245,9 +1251,9 @@
         rset4 = self.execute('Any N,U WHERE N todo_by U, T eid %s,'
                              'N filed_under T, W concerne N,'
                              'W filed_under A, A eid %s' % (teid1, teid2))
-        self.assertEquals(rset1.rows, rset2.rows)
-        self.assertEquals(rset1.rows, rset3.rows)
-        self.assertEquals(rset1.rows, rset4.rows)
+        self.assertEqual(rset1.rows, rset2.rows)
+        self.assertEqual(rset1.rows, rset3.rows)
+        self.assertEqual(rset1.rows, rset4.rows)
 
     def test_nonregr_6(self):
         self.execute('Any N,COUNT(S) GROUPBY N ORDERBY COUNT(N) WHERE S name N, S is State')
@@ -1265,7 +1271,7 @@
         rset = self.execute('Any lower(N) ORDERBY LOWER(N) WHERE X is Tag, X name N,'
                             'X owned_by U, U eid %(x)s',
                             {'x':self.session.user.eid})
-        self.assertEquals(rset.rows, [[u'\xe9name0']])
+        self.assertEqual(rset.rows, [[u'\xe9name0']])
 
 
     def test_nonregr_description(self):
@@ -1279,8 +1285,8 @@
         self.execute("SET X in_basket B WHERE X is Personne")
         self.execute("SET X in_basket B WHERE X is Societe")
         rset = self.execute('Any X WHERE X in_basket B, B eid %s' % beid)
-        self.assertEquals(len(rset), 2)
-        self.assertEquals(rset.description, [('Personne',), ('Societe',)])
+        self.assertEqual(len(rset), 2)
+        self.assertEqual(rset.description, [('Personne',), ('Societe',)])
 
 
     def test_nonregr_cache_1(self):
@@ -1290,19 +1296,19 @@
                        {'y': beid})
         rset = self.execute("Any X WHERE X in_basket B, B eid %(x)s",
                        {'x': beid})
-        self.assertEquals(rset.rows, [[peid]])
+        self.assertEqual(rset.rows, [[peid]])
         rset = self.execute("Any X WHERE X in_basket B, B eid %(x)s",
                        {'x': beid})
-        self.assertEquals(rset.rows, [[peid]])
+        self.assertEqual(rset.rows, [[peid]])
 
     def test_nonregr_has_text_cache(self):
         eid1 = self.execute("INSERT Personne X: X nom 'bidule'")[0][0]
         eid2 = self.execute("INSERT Personne X: X nom 'tag'")[0][0]
         self.commit()
         rset = self.execute("Any X WHERE X has_text %(text)s", {'text': 'bidule'})
-        self.assertEquals(rset.rows, [[eid1]])
+        self.assertEqual(rset.rows, [[eid1]])
         rset = self.execute("Any X WHERE X has_text %(text)s", {'text': 'tag'})
-        self.assertEquals(rset.rows, [[eid2]])
+        self.assertEqual(rset.rows, [[eid2]])
 
     def test_nonregr_sortterm_management(self):
         """Error: Variable has no attribute 'sql' in rql2sql.py (visit_variable)
@@ -1327,16 +1333,16 @@
         self.execute("SET X todo_by Y WHERE X is Note, Y eid %s" % ueid)
         rset = self.execute('Any N WHERE N todo_by U, N is Note, U eid %s, N filed_under T, T eid %s'
                              % (ueid, teid1))
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
 
     def test_nonregr_XXX(self):
         teid = self.execute('Transition S WHERE S name "deactivate"')[0][0]
         rset = self.execute('Any O WHERE O is State, '
                              'S eid %(x)s, S transition_of ET, O state_of ET', {'x': teid})
-        self.assertEquals(len(rset), 2)
+        self.assertEqual(len(rset), 2)
         rset = self.execute('Any O WHERE O is State, NOT S destination_state O, '
                              'S eid %(x)s, S transition_of ET, O state_of ET', {'x': teid})
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
 
 
     def test_nonregr_set_datetime(self):
@@ -1353,9 +1359,9 @@
         ueid = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto', X in_group G "
                              "WHERE G name 'users'")[0][0]
         rset = self.execute("CWUser U")
-        self.assertEquals(len(rset), 3) # bob + admin + anon
+        self.assertEqual(len(rset), 3) # bob + admin + anon
         rset = self.execute("Any U WHERE NOT U owned_by U")
-        self.assertEquals(len(rset), 0) # even admin created at repo initialization time should belong to itself
+        self.assertEqual(len(rset), 0) # even admin created at repo initialization time should belong to itself
 
     def test_nonreg_update_index(self):
         # this is the kind of queries generated by "cubicweb-ctl db-check -ry"
@@ -1370,11 +1376,11 @@
         self.execute('Any X,S, MAX(T) GROUPBY X,S ORDERBY S WHERE X is CWUser, T tags X, S eid IN(%s), X in_state S' % seid)
 
     def test_nonregr_solution_cache(self):
-        self.skip('XXX should be fixed or documented') # (doesn't occur if cache key is provided.)
+        self.skipTest('XXX should be fixed or documented') # (doesn't occur if cache key is provided.)
         rset = self.execute('Any X WHERE X is CWUser, X eid %(x)s', {'x':self.ueid})
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
         rset = self.execute('Any X WHERE X is CWUser, X eid %(x)s', {'x':12345})
-        self.assertEquals(len(rset), 0)
+        self.assertEqual(len(rset), 0)
 
     def test_nonregr_final_norestr(self):
         self.assertRaises(BadRQLQuery, self.execute, 'Date X')
--- a/server/test/unittest_repository.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_repository.py	Wed Nov 03 16:38:28 2010 +0100
@@ -32,8 +32,8 @@
 from yams.constraints import UniqueConstraint
 
 from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
-                      UnknownEid, AuthenticationError)
-from cubicweb.selectors import implements
+                      UnknownEid, AuthenticationError, Unauthorized, QueryError)
+from cubicweb.selectors import is_instance
 from cubicweb.schema import CubicWebSchema, RQLConstraint
 from cubicweb.dbapi import connect, multiple_connections_unfix
 from cubicweb.devtools.testlib import CubicWebTC
@@ -65,18 +65,36 @@
             self.session.set_pool()
             cu = self.session.system_sql('SELECT %s FROM %s WHERE %s is NULL' % (
                 namecol, table, finalcol))
-            self.assertEquals(cu.fetchall(), [])
+            self.assertEqual(cu.fetchall(), [])
             cu = self.session.system_sql('SELECT %s FROM %s WHERE %s=%%(final)s ORDER BY %s'
                                          % (namecol, table, finalcol, namecol), {'final': 'TRUE'})
-            self.assertEquals(cu.fetchall(), [(u'Boolean',), (u'Bytes',),
+            self.assertEqual(cu.fetchall(), [(u'Boolean',), (u'Bytes',),
                                               (u'Date',), (u'Datetime',),
                                               (u'Decimal',),(u'Float',),
                                               (u'Int',),
                                               (u'Interval',), (u'Password',),
                                               (u'String',), (u'Time',)])
+            sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to "
+                   "FROM cw_CWUniqueTogetherConstraint as cstr, "
+                   "     relations_relation as rel, "
+                   "     cw_CWEType as etype "
+                   "WHERE cstr.cw_eid = rel.eid_from "
+                   "  AND cstr.cw_constraint_of = etype.cw_eid "
+                   "  AND etype.cw_name = 'Personne' "
+                   ";")
+            cu = self.session.system_sql(sql)
+            rows = cu.fetchall()
+            self.assertEqual(len(rows), 3)
+            self.test_unique_together()
         finally:
             self.repo.set_schema(origshema)
 
+    def test_unique_together(self):
+        person = self.repo.schema.eschema('Personne')
+        self.assertEqual(len(person._unique_together), 1)
+        self.assertItemsEqual(person._unique_together[0],
+                                           ('nom', 'prenom', 'inline2'))
+
     def test_schema_has_owner(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
@@ -118,15 +136,47 @@
         repo.close(cnxid)
         self.assert_(repo.connect(u"barnabé", password=u"héhéhé".encode('UTF8')))
 
-    def test_invalid_entity_rollback(self):
+    def test_rollback_on_commit_error(self):
         cnxid = self.repo.connect(self.admlogin, password=self.admpassword)
-        # no group
         self.repo.execute(cnxid,
                           'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s',
                           {'login': u"tutetute", 'passwd': 'tutetute'})
         self.assertRaises(ValidationError, self.repo.commit, cnxid)
         self.failIf(self.repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"'))
 
+    def test_rollback_on_execute_validation_error(self):
+        class ValidationErrorAfterHook(Hook):
+            __regid__ = 'valerror-after-hook'
+            __select__ = Hook.__select__ & is_instance('CWGroup')
+            events = ('after_update_entity',)
+            def __call__(self):
+                raise ValidationError(self.entity.eid, {})
+        with self.temporary_appobjects(ValidationErrorAfterHook):
+            self.assertRaises(ValidationError,
+                              self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"')
+            self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
+            ex = self.assertRaises(QueryError, self.commit)
+            self.assertEqual(str(ex), 'transaction must be rollbacked')
+            self.rollback()
+            self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
+
+    def test_rollback_on_execute_unauthorized(self):
+        class UnauthorizedAfterHook(Hook):
+            __regid__ = 'unauthorized-after-hook'
+            __select__ = Hook.__select__ & is_instance('CWGroup')
+            events = ('after_update_entity',)
+            def __call__(self):
+                raise Unauthorized()
+        with self.temporary_appobjects(UnauthorizedAfterHook):
+            self.assertRaises(Unauthorized,
+                              self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"')
+            self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
+            ex = self.assertRaises(QueryError, self.commit)
+            self.assertEqual(str(ex), 'transaction must be rollbacked')
+            self.rollback()
+            self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
+
+
     def test_close(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
@@ -143,14 +193,14 @@
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
         repo.set_shared_data(cnxid, 'data', 4)
         cnxid2 = repo.connect(self.admlogin, password=self.admpassword)
-        self.assertEquals(repo.get_shared_data(cnxid, 'data'), 4)
-        self.assertEquals(repo.get_shared_data(cnxid2, 'data'), None)
+        self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
+        self.assertEqual(repo.get_shared_data(cnxid2, 'data'), None)
         repo.set_shared_data(cnxid2, 'data', 5)
-        self.assertEquals(repo.get_shared_data(cnxid, 'data'), 4)
-        self.assertEquals(repo.get_shared_data(cnxid2, 'data'), 5)
+        self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
+        self.assertEqual(repo.get_shared_data(cnxid2, 'data'), 5)
         repo.get_shared_data(cnxid2, 'data', pop=True)
-        self.assertEquals(repo.get_shared_data(cnxid, 'data'), 4)
-        self.assertEquals(repo.get_shared_data(cnxid2, 'data'), None)
+        self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
+        self.assertEqual(repo.get_shared_data(cnxid2, 'data'), None)
         repo.close(cnxid)
         repo.close(cnxid2)
         self.assertRaises(BadConnectionId, repo.get_shared_data, cnxid, 'data')
@@ -161,7 +211,7 @@
     def test_check_session(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        self.assertEquals(repo.check_session(cnxid), None)
+        self.assertEqual(repo.check_session(cnxid), None)
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.check_session, cnxid)
 
@@ -170,19 +220,19 @@
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
         # check db state
         result = repo.execute(cnxid, 'Personne X')
-        self.assertEquals(result.rowcount, 0)
+        self.assertEqual(result.rowcount, 0)
         # rollback entity insertion
         repo.execute(cnxid, "INSERT Personne X: X nom 'bidule'")
         result = repo.execute(cnxid, 'Personne X')
-        self.assertEquals(result.rowcount, 1)
+        self.assertEqual(result.rowcount, 1)
         repo.rollback(cnxid)
         result = repo.execute(cnxid, 'Personne X')
-        self.assertEquals(result.rowcount, 0, result.rows)
+        self.assertEqual(result.rowcount, 0, result.rows)
         # commit
         repo.execute(cnxid, "INSERT Personne X: X nom 'bidule'")
         repo.commit(cnxid)
         result = repo.execute(cnxid, 'Personne X')
-        self.assertEquals(result.rowcount, 1)
+        self.assertEqual(result.rowcount, 1)
 
     def test_transaction_base2(self):
         repo = self.repo
@@ -190,10 +240,10 @@
         # rollback relation insertion
         repo.execute(cnxid, "SET U in_group G WHERE U login 'admin', G name 'guests'")
         result = repo.execute(cnxid, "Any U WHERE U in_group G, U login 'admin', G name 'guests'")
-        self.assertEquals(result.rowcount, 1)
+        self.assertEqual(result.rowcount, 1)
         repo.rollback(cnxid)
         result = repo.execute(cnxid, "Any U WHERE U in_group G, U login 'admin', G name 'guests'")
-        self.assertEquals(result.rowcount, 0, result.rows)
+        self.assertEqual(result.rowcount, 0, result.rows)
 
     def test_transaction_base3(self):
         repo = self.repo
@@ -202,15 +252,15 @@
         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)
+        self.assertEqual(len(rset), 1)
         repo.rollback(cnxid)
         rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
-        self.assertEquals(len(rset), 0)
+        self.assertEqual(len(rset), 0)
 
     def test_transaction_interleaved(self):
-        self.skip('implement me')
+        self.skipTest('implement me')
 
     def test_close_kill_processing_request(self):
         repo = self.repo
@@ -228,14 +278,14 @@
             repo.commit(cnxid)
         try:
             ex = self.assertRaises(Exception, run_transaction)
-            self.assertEquals(str(ex), 'try to access pool on a closed session')
+            self.assertEqual(str(ex), 'try to access pool on a closed session')
         finally:
             t.join()
 
     def test_initial_schema(self):
         schema = self.repo.schema
         # check order of attributes is respected
-        self.assertListEquals([r.type for r in schema.eschema('CWAttribute').ordered_relations()
+        self.assertListEqual([r.type for r in schema.eschema('CWAttribute').ordered_relations()
                                if not r.type in ('eid', 'is', 'is_instance_of', 'identity',
                                                  'creation_date', 'modification_date', 'cwuri',
                                                  'owned_by', 'created_by',
@@ -248,11 +298,11 @@
                                'indexed', 'fulltextindexed', 'internationalizable',
                                'defaultval', 'description', 'description_format'])
 
-        self.assertEquals(schema.eschema('CWEType').main_attribute(), 'name')
-        self.assertEquals(schema.eschema('State').main_attribute(), 'name')
+        self.assertEqual(schema.eschema('CWEType').main_attribute(), 'name')
+        self.assertEqual(schema.eschema('State').main_attribute(), 'name')
 
         constraints = schema.rschema('name').rdef('CWEType', 'String').constraints
-        self.assertEquals(len(constraints), 2)
+        self.assertEqual(len(constraints), 2)
         for cstr in constraints[:]:
             if isinstance(cstr, UniqueConstraint):
                 constraints.remove(cstr)
@@ -260,17 +310,17 @@
         else:
             self.fail('unique constraint not found')
         sizeconstraint = constraints[0]
-        self.assertEquals(sizeconstraint.min, None)
-        self.assertEquals(sizeconstraint.max, 64)
+        self.assertEqual(sizeconstraint.min, None)
+        self.assertEqual(sizeconstraint.max, 64)
 
         constraints = schema.rschema('relation_type').rdef('CWAttribute', 'CWRType').constraints
-        self.assertEquals(len(constraints), 1)
+        self.assertEqual(len(constraints), 1)
         cstr = constraints[0]
         self.assert_(isinstance(cstr, RQLConstraint))
-        self.assertEquals(cstr.restriction, 'O final TRUE')
+        self.assertEqual(cstr.restriction, 'O final TRUE')
 
         ownedby = schema.rschema('owned_by')
-        self.assertEquals(ownedby.objects('CWEType'), ('CWUser',))
+        self.assertEqual(ownedby.objects('CWEType'), ('CWUser',))
 
     def test_pyro(self):
         import Pyro
@@ -301,7 +351,7 @@
             schema = cnx.get_schema()
             self.failUnless(cnx.vreg)
             self.failUnless('etypes'in cnx.vreg)
-            self.assertEquals(schema.__hashmode__, None)
+            self.assertEqual(schema.__hashmode__, None)
             cu = cnx.cursor()
             rset = cu.execute('Any U,G WHERE U in_group G')
             user = iter(rset.entities()).next()
@@ -319,25 +369,25 @@
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
         session = repo._get_session(cnxid, setpool=True)
-        self.assertEquals(repo.type_and_source_from_eid(1, session),
+        self.assertEqual(repo.type_and_source_from_eid(1, session),
                           ('CWGroup', 'system', None))
-        self.assertEquals(repo.type_from_eid(1, session), 'CWGroup')
-        self.assertEquals(repo.source_from_eid(1, session).uri, 'system')
-        self.assertEquals(repo.eid2extid(repo.system_source, 1, session), None)
+        self.assertEqual(repo.type_from_eid(1, session), 'CWGroup')
+        self.assertEqual(repo.source_from_eid(1, session).uri, 'system')
+        self.assertEqual(repo.eid2extid(repo.system_source, 1, session), None)
         class dummysource: uri = 'toto'
         self.assertRaises(UnknownEid, repo.eid2extid, dummysource, 1, session)
 
     def test_public_api(self):
-        self.assertEquals(self.repo.get_schema(), self.repo.schema)
-        self.assertEquals(self.repo.source_defs(), {'system': {'adapter': 'native', 'uri': 'system'}})
+        self.assertEqual(self.repo.get_schema(), self.repo.schema)
+        self.assertEqual(self.repo.source_defs(), {'system': {'adapter': 'native', 'uri': 'system'}})
         # .properties() return a result set
-        self.assertEquals(self.repo.properties().rql, 'Any K,V WHERE P is CWProperty,P pkey K, P value V, NOT P for_user U')
+        self.assertEqual(self.repo.properties().rql, 'Any K,V WHERE P is CWProperty,P pkey K, P value V, NOT P for_user U')
 
     def test_session_api(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        self.assertEquals(repo.user_info(cnxid), (5, 'admin', set([u'managers']), {}))
-        self.assertEquals(repo.describe(cnxid, 1), (u'CWGroup', u'system', None))
+        self.assertEqual(repo.user_info(cnxid), (5, 'admin', set([u'managers']), {}))
+        self.assertEqual(repo.describe(cnxid, 1), (u'CWGroup', u'system', None))
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.user_info, cnxid)
         self.assertRaises(BadConnectionId, repo.describe, cnxid, 1)
@@ -345,12 +395,12 @@
     def test_shared_data_api(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        self.assertEquals(repo.get_shared_data(cnxid, 'data'), None)
+        self.assertEqual(repo.get_shared_data(cnxid, 'data'), None)
         repo.set_shared_data(cnxid, 'data', 4)
-        self.assertEquals(repo.get_shared_data(cnxid, 'data'), 4)
+        self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
         repo.get_shared_data(cnxid, 'data', pop=True)
         repo.get_shared_data(cnxid, 'whatever', pop=True)
-        self.assertEquals(repo.get_shared_data(cnxid, 'data'), None)
+        self.assertEqual(repo.get_shared_data(cnxid, 'data'), None)
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.set_shared_data, cnxid, 'data', 0)
         self.assertRaises(BadConnectionId, repo.get_shared_data, cnxid, 'data')
@@ -376,21 +426,28 @@
                      {'x': note.eid, 'p': p1.eid})
         rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s',
                             {'x': note.eid})
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(len(rset), 1)
         p2 = self.request().create_entity('Personne', nom=u'tutu')
         self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s',
                      {'x': note.eid, 'p': p2.eid})
         rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s',
                             {'x': note.eid})
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset.rows[0][0], p2.eid)
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.rows[0][0], p2.eid)
 
+    def test_delete_if_object_inlined_singlecard(self):
+        req = self.request()
+        c = req.create_entity('Card', title=u'Carte')
+        req.create_entity('Personne', nom=u'Vincent', fiche=c)
+        req.create_entity('Personne', nom=u'Florent', fiche=c)
+        self.commit()
+        self.assertEqual(len(c.reverse_fiche), 1)
 
     def test_set_attributes_in_before_update(self):
         # local hook
         class DummyBeforeHook(Hook):
             __regid__ = 'dummy-before-hook'
-            __select__ = Hook.__select__ & implements('EmailAddress')
+            __select__ = Hook.__select__ & is_instance('EmailAddress')
             events = ('before_update_entity',)
             def __call__(self):
                 # safety belt: avoid potential infinite recursion if the test
@@ -405,13 +462,13 @@
             addr.set_attributes(address=u'a@b.com')
             rset = self.execute('Any A,AA WHERE X eid %(x)s, X address A, X alias AA',
                                 {'x': addr.eid})
-            self.assertEquals(rset.rows, [[u'a@b.com', u'foo']])
+            self.assertEqual(rset.rows, [[u'a@b.com', u'foo']])
 
     def test_set_attributes_in_before_add(self):
         # local hook
         class DummyBeforeHook(Hook):
             __regid__ = 'dummy-before-hook'
-            __select__ = Hook.__select__ & implements('EmailAddress')
+            __select__ = Hook.__select__ & is_instance('EmailAddress')
             events = ('before_add_entity',)
             def __call__(self):
                 # set_attributes is forbidden within before_add_entity()
@@ -430,7 +487,7 @@
         class DummyBeforeHook(Hook):
             _test = self # keep reference to test instance
             __regid__ = 'dummy-before-hook'
-            __select__ = Hook.__select__ & implements('Affaire')
+            __select__ = Hook.__select__ & is_instance('Affaire')
             events = ('before_update_entity',)
             def __call__(self):
                 # invoiced attribute shouldn't be considered "edited" before the hook
@@ -452,7 +509,7 @@
 
     def test_source_from_eid(self):
         self.session.set_pool()
-        self.assertEquals(self.repo.source_from_eid(1, self.session),
+        self.assertEqual(self.repo.source_from_eid(1, self.session),
                           self.repo.sources_by_uri['system'])
 
     def test_source_from_eid_raise(self):
@@ -461,7 +518,7 @@
 
     def test_type_from_eid(self):
         self.session.set_pool()
-        self.assertEquals(self.repo.type_from_eid(1, self.session), 'CWGroup')
+        self.assertEqual(self.repo.type_from_eid(1, self.session), 'CWGroup')
 
     def test_type_from_eid_raise(self):
         self.session.set_pool()
@@ -478,12 +535,12 @@
         self.assertIsInstance(data[0][3], datetime)
         data[0] = list(data[0])
         data[0][3] = None
-        self.assertEquals(tuplify(data), [(-1, 'Personne', 'system', None, None)])
+        self.assertEqual(tuplify(data), [(-1, 'Personne', 'system', None, None)])
         self.repo.delete_info(self.session, entity, 'system', None)
         #self.repo.commit()
         cu = self.session.system_sql('SELECT * FROM entities WHERE eid = -1')
         data = cu.fetchall()
-        self.assertEquals(data, [])
+        self.assertEqual(data, [])
 
 
 class FTITC(CubicWebTC):
@@ -493,7 +550,7 @@
         eidp = self.execute('INSERT Personne X: X nom "toto", X prenom "tutu"')[0][0]
         self.commit()
         ts = datetime.now()
-        self.assertEquals(len(self.execute('Personne X WHERE X has_text "tutu"')), 1)
+        self.assertEqual(len(self.execute('Personne X WHERE X has_text "tutu"')), 1)
         self.session.set_pool()
         cu = self.session.system_sql('SELECT mtime, eid FROM entities WHERE eid = %s' % eidp)
         omtime = cu.fetchone()[0]
@@ -502,23 +559,23 @@
         time.sleep(1 - (ts.second - int(ts.second)))
         self.execute('SET X nom "tata" WHERE X eid %(x)s', {'x': eidp})
         self.commit()
-        self.assertEquals(len(self.execute('Personne X WHERE X has_text "tutu"')), 1)
+        self.assertEqual(len(self.execute('Personne X WHERE X has_text "tutu"')), 1)
         self.session.set_pool()
         cu = self.session.system_sql('SELECT mtime FROM entities WHERE eid = %s' % eidp)
         mtime = cu.fetchone()[0]
         self.failUnless(omtime < mtime)
         self.commit()
         date, modified, deleted = self.repo.entities_modified_since(('Personne',), omtime)
-        self.assertEquals(modified, [('Personne', eidp)])
-        self.assertEquals(deleted, [])
+        self.assertEqual(modified, [('Personne', eidp)])
+        self.assertEqual(deleted, [])
         date, modified, deleted = self.repo.entities_modified_since(('Personne',), mtime)
-        self.assertEquals(modified, [])
-        self.assertEquals(deleted, [])
+        self.assertEqual(modified, [])
+        self.assertEqual(deleted, [])
         self.execute('DELETE Personne X WHERE X eid %(x)s', {'x': eidp})
         self.commit()
         date, modified, deleted = self.repo.entities_modified_since(('Personne',), omtime)
-        self.assertEquals(modified, [])
-        self.assertEquals(deleted, [('Personne', eidp)])
+        self.assertEqual(modified, [])
+        self.assertEqual(deleted, [('Personne', eidp)])
 
     def test_fulltext_container_entity(self):
         assert self.schema.rschema('use_email').fulltext_container == 'subject'
@@ -526,27 +583,27 @@
         toto = req.create_entity('EmailAddress', address=u'toto@logilab.fr')
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         req.user.set_relations(use_email=toto)
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
-        self.assertEquals(rset.rows, [[req.user.eid]])
+        self.assertEqual(rset.rows, [[req.user.eid]])
         req.execute('DELETE X use_email Y WHERE X login "admin", Y eid %(y)s',
                     {'y': toto.eid})
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         tutu = req.create_entity('EmailAddress', address=u'tutu@logilab.fr')
         req.user.set_relations(use_email=tutu)
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
-        self.assertEquals(rset.rows, [[req.user.eid]])
+        self.assertEqual(rset.rows, [[req.user.eid]])
         tutu.set_attributes(address=u'hip@logilab.fr')
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'hip'})
-        self.assertEquals(rset.rows, [[req.user.eid]])
+        self.assertEqual(rset.rows, [[req.user.eid]])
 
     def test_no_uncessary_ftiindex_op(self):
         req = self.request()
@@ -559,7 +616,7 @@
 
     def test_versions_inserted(self):
         inserted = [r[0] for r in self.execute('Any K ORDERBY K WHERE P pkey K, P pkey ~= "system.version.%"')]
-        self.assertEquals(inserted,
+        self.assertEqual(inserted,
                           [u'system.version.basket', u'system.version.card', u'system.version.comment',
                            u'system.version.cubicweb', u'system.version.email',
                            u'system.version.file', u'system.version.folder',
@@ -591,17 +648,29 @@
             eidp = self.execute('INSERT Personne X: X nom "toto"')[0][0]
             eidn = self.execute('INSERT Note X: X type "T"')[0][0]
             self.execute('SET N ecrit_par Y WHERE N type "T", Y nom "toto"')
-            self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
+            self.assertEqual(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
                                        ('after_add_relation', eidn, 'ecrit_par', eidp)])
             CALLED[:] = ()
             self.execute('DELETE N ecrit_par Y WHERE N type "T", Y nom "toto"')
-            self.assertEquals(CALLED, [('before_delete_relation', eidn, 'ecrit_par', eidp),
+            self.assertEqual(CALLED, [('before_delete_relation', eidn, 'ecrit_par', eidp),
                                        ('after_delete_relation', eidn, 'ecrit_par', eidp)])
             CALLED[:] = ()
             eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0]
-            self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
+            self.assertEqual(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
                                        ('after_add_relation', eidn, 'ecrit_par', eidp)])
 
+    def test_unique_contraint(self):
+        req = self.request()
+        toto = req.create_entity('Personne', nom=u'toto')
+        a01 = req.create_entity('Affaire', ref=u'A01', todo_by=toto)
+        req.cnx.commit()
+        req = self.request()
+        req.create_entity('Note', type=u'todo', inline1=a01)
+        req.cnx.commit()
+        req = self.request()
+        req.create_entity('Note', type=u'todo', inline1=a01)
+        ex = self.assertRaises(ValidationError, req.cnx.commit)
+        self.assertEqual(ex.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'})
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_rql2sql.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_rql2sql.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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()
@@ -271,7 +271,7 @@
     ('Any O WHERE NOT S ecrit_par O, S eid 1, S inline1 P, O inline2 P',
      '''SELECT _O.cw_eid
 FROM cw_Note AS _S, cw_Personne AS _O
-WHERE NOT (_S.cw_ecrit_par=_O.cw_eid) AND _S.cw_eid=1 AND _S.cw_inline1 IS NOT NULL AND _O.cw_inline2=_S.cw_inline1'''),
+WHERE (_S.cw_ecrit_par IS NULL OR _S.cw_ecrit_par!=_O.cw_eid) AND _S.cw_eid=1 AND _S.cw_inline1 IS NOT NULL AND _O.cw_inline2=_S.cw_inline1'''),
 
     ('DISTINCT Any S ORDERBY stockproc(SI) WHERE NOT S ecrit_par O, S para SI',
      '''SELECT T1.C0 FROM (SELECT DISTINCT _S.cw_eid AS C0, STOCKPROC(_S.cw_para) AS C1
@@ -424,26 +424,15 @@
 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'''),
-
-    ('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
-FROM cw_Affaire AS _A
-ORDER BY 2) AS T1'''),
+GROUP BY _X.cw_data_name,_X.cw_data_format
+ORDER BY 1,2,_X.cw_data_format'''),
 
     ('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
 '''),
 
@@ -578,6 +567,11 @@
 GROUP BY T1.C0,T1.C2
 ORDER BY T1.C2'''),
 
+    ('Any 1 WHERE X in_group G, X is CWUser',
+     '''SELECT 1
+FROM in_group_relation AS rel_in_group0'''),
+
+
     ]
 
 
@@ -838,7 +832,7 @@
     ('Any O,AD  WHERE NOT S inline1 O, S eid 123, O todo_by AD?',
      '''SELECT _O.cw_eid, rel_todo_by0.eid_to
 FROM cw_Affaire AS _O LEFT OUTER JOIN todo_by_relation AS rel_todo_by0 ON (rel_todo_by0.eid_from=_O.cw_eid), cw_Note AS _S
-WHERE NOT (_S.cw_inline1=_O.cw_eid) AND _S.cw_eid=123''')
+WHERE (_S.cw_inline1 IS NULL OR _S.cw_inline1!=_O.cw_eid) AND _S.cw_eid=123''')
     ]
 
 VIRTUAL_VARS = [
@@ -988,7 +982,7 @@
     ('Any N WHERE NOT N ecrit_par P, P nom "toto"',
      '''SELECT _N.cw_eid
 FROM cw_Note AS _N, cw_Personne AS _P
-WHERE NOT (_N.cw_ecrit_par=_P.cw_eid) AND _P.cw_nom=toto'''),
+WHERE (_N.cw_ecrit_par IS NULL OR _N.cw_ecrit_par!=_P.cw_eid) AND _P.cw_nom=toto'''),
 
     ('Any P WHERE NOT N ecrit_par P, P nom "toto"',
      '''SELECT _P.cw_eid
@@ -1008,7 +1002,7 @@
     ('Any P WHERE NOT N ecrit_par P, P is Personne, N eid 512',
      '''SELECT _P.cw_eid
 FROM cw_Note AS _N, cw_Personne AS _P
-WHERE NOT (_N.cw_ecrit_par=_P.cw_eid) AND _N.cw_eid=512'''),
+WHERE (_N.cw_ecrit_par IS NULL OR _N.cw_ecrit_par!=_P.cw_eid) AND _N.cw_eid=512'''),
 
     ('Any S,ES,T WHERE S state_of ET, ET name "CWUser", ES allowed_transition T, T destination_state S',
      # XXX "_T.cw_destination_state IS NOT NULL" could be avoided here but it's not worth it
@@ -1036,11 +1030,12 @@
     ('DISTINCT Any X WHERE X from_entity OET, NOT X from_entity NET, OET name "Image", NET eid 1',
      '''SELECT DISTINCT _X.cw_eid
 FROM cw_CWAttribute AS _X, cw_CWEType AS _OET
-WHERE _X.cw_from_entity=_OET.cw_eid AND NOT (_X.cw_from_entity=1) AND _OET.cw_name=Image
+WHERE _X.cw_from_entity=_OET.cw_eid AND (_X.cw_from_entity IS NULL OR _X.cw_from_entity!=1) AND _OET.cw_name=Image
 UNION
 SELECT DISTINCT _X.cw_eid
 FROM cw_CWEType AS _OET, cw_CWRelation AS _X
-WHERE _X.cw_from_entity=_OET.cw_eid AND NOT (_X.cw_from_entity=1) AND _OET.cw_name=Image'''),
+WHERE _X.cw_from_entity=_OET.cw_eid AND (_X.cw_from_entity IS NULL OR _X.cw_from_entity!=1) AND _OET.cw_name=Image'''),
+
     ]
 
 INTERSECT = [
@@ -1082,11 +1077,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,'
@@ -1103,21 +1096,19 @@
             for sol in delete.solutions:
                 s.add(sol.get(var))
             return s
-        self.assertEquals(var_sols('FROM_ENTITYOBJECT'), set(('CWAttribute', 'CWRelation')))
-        self.assertEquals(var_sols('FROM_ENTITYOBJECT'), delete.defined_vars['FROM_ENTITYOBJECT'].stinfo['possibletypes'])
-        self.assertEquals(var_sols('ISOBJECT'),
+        self.assertEqual(var_sols('FROM_ENTITYOBJECT'), set(('CWAttribute', 'CWRelation')))
+        self.assertEqual(var_sols('FROM_ENTITYOBJECT'), delete.defined_vars['FROM_ENTITYOBJECT'].stinfo['possibletypes'])
+        self.assertEqual(var_sols('ISOBJECT'),
                           set(x.type for x in self.schema.entities() if not x.final))
-        self.assertEquals(var_sols('ISOBJECT'), delete.defined_vars['ISOBJECT'].stinfo['possibletypes'])
+        self.assertEqual(var_sols('ISOBJECT'), delete.defined_vars['ISOBJECT'].stinfo['possibletypes'])
+
 
+def strip(text):
+    return '\n'.join(l.strip() for l in text.strip().splitlines())
 
 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()
@@ -1130,7 +1121,7 @@
             r, nargs, cbs = self.o.generate(union, args,
                                             varmap=varmap)
             args.update(nargs)
-            self.assertLinesEquals((r % args).strip(), self._norm_sql(sql), striplines=True)
+            self.assertMultiLineEqual(strip(r % args), self._norm_sql(sql))
         except Exception, ex:
             if 'r' in locals():
                 try:
@@ -1212,7 +1203,7 @@
     def test_is_null_transform(self):
         union = self._prepare('Any X WHERE X login %(login)s')
         r, args, cbs = self.o.generate(union, {'login': None})
-        self.assertLinesEquals((r % args).strip(),
+        self.assertMultiLineEqual((r % args).strip(),
                                '''SELECT _X.cw_eid
 FROM cw_CWUser AS _X
 WHERE _X.cw_login IS NULL''')
@@ -1377,13 +1368,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
 
@@ -1426,8 +1457,8 @@
         try:
             union = self._prepare('Any R WHERE X ref R')
             r, nargs, cbs = self.o.generate(union, args={})
-            self.assertLinesEquals(r.strip(), 'SELECT _X.cw_ref\nFROM cw_Affaire AS _X')
-            self.assertEquals(cbs, {0: [cb]})
+            self.assertMultiLineEqual(r.strip(), 'SELECT _X.cw_ref\nFROM cw_Affaire AS _X')
+            self.assertEqual(cbs, {0: [cb]})
         finally:
             self.o.attr_map.clear()
 
@@ -1443,13 +1474,18 @@
 FROM cw_CWUser AS _X
 WHERE ((CAST(EXTRACT(YEAR from _X.cw_creation_date) AS INTEGER)=2010) OR (_X.cw_creation_date IS NULL))''')
 
+    def test_not_no_where(self):
+        # XXX will check if some in_group relation exists, that's it.
+        # We  can't actually know if we want to check if there are some
+        # X without in_group relation, or some G without it.
+        self._check('Any 1 WHERE NOT X in_group G, X is CWUser',
+                    '''SELECT 1
+WHERE NOT (EXISTS(SELECT 1 FROM in_group_relation AS rel_in_group0))''')
+
+
 
 class SqliteSQLGeneratorTC(PostgresSQLGeneratorTC):
-
-    def setUp(self):
-        RQLGeneratorTC.setUp(self)
-        dbhelper = get_db_helper('sqlite')
-        self.o = SQLGenerator(schema, dbhelper)
+    backend = 'sqlite'
 
     def _norm_sql(self, sql):
         return sql.strip().replace(' ILIKE ', ' LIKE ')
@@ -1547,6 +1583,26 @@
 FROM appears AS appears0, cw_Folder AS _X
 WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
 """),
+
+            ('Any X ORDERBY FTIRANK(X) WHERE X has_text "toto tata"',
+             """SELECT DISTINCT appears0.uid
+FROM appears AS appears0
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata'))"""),
+
+            ('Any X ORDERBY FTIRANK(X) WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,Folder)',
+             """SELECT DISTINCT _X.cw_eid
+FROM appears AS appears0, cw_Basket AS _X
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
+UNION
+SELECT DISTINCT _X.cw_eid
+FROM appears AS appears0, cw_Folder AS _X
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=_X.cw_eid AND _X.cw_name=tutu
+"""),
+
+            ('Any X, FTIRANK(X) WHERE X has_text "toto tata"',
+             """SELECT DISTINCT appears0.uid, 1.0
+FROM appears AS appears0
+WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata'))"""),
             )):
             yield t
 
@@ -1560,11 +1616,7 @@
 
 
 class MySQLGenerator(PostgresSQLGeneratorTC):
-
-    def setUp(self):
-        RQLGeneratorTC.setUp(self)
-        dbhelper = get_db_helper('mysql')
-        self.o = SQLGenerator(schema, dbhelper)
+    backend = 'mysql'
 
     def _norm_sql(self, sql):
         sql = sql.strip().replace(' ILIKE ', ' LIKE ').replace('TRUE', '1').replace('FALSE', '0')
@@ -1651,12 +1703,19 @@
 WHERE ((EXTRACT(YEAR from _X.cw_creation_date)=2010) OR (_X.cw_creation_date IS NULL))''')
 
 
+    def test_not_no_where(self):
+        self._check('Any 1 WHERE NOT X in_group G, X is CWUser',
+                    '''SELECT 1
+FROM (SELECT 1) AS _T
+WHERE NOT (EXISTS(SELECT 1 FROM in_group_relation AS rel_in_group0))''')
+
+
 class removeUnsusedSolutionsTC(TestCase):
     def test_invariant_not_varying(self):
         rqlst = mock_object(defined_vars={})
         rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=True)
         rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=False)
-        self.assertEquals(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
+        self.assertEqual(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
                                                           {'A': 'FootGroup', 'B': 'FootTeam'}], {}, None),
                           ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
                             {'A': 'FootGroup', 'B': 'FootTeam'}],
@@ -1667,10 +1726,11 @@
         rqlst = mock_object(defined_vars={})
         rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=True)
         rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=False)
-        self.assertEquals(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
+        self.assertEqual(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
                                                           {'A': 'FootGroup', 'B': 'RugbyTeam'}], {}, None),
                           ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'}], {}, set())
                           )
 
+
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_rqlannotation.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_rqlannotation.py	Wed Nov 03 16:38:28 2010 +0100
@@ -41,256 +41,256 @@
 
     def test_0_1(self):
         rqlst = self._prepare('Any SEN,RN,OEN WHERE X from_entity SE, SE eid 44, X relation_type R, R eid 139, X to_entity OE, OE eid 42, R name RN, SE name SEN, OE name OEN')
-        self.assertEquals(rqlst.defined_vars['SE']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['OE']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['R']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['SE'].stinfo['attrvar'], None)
-        self.assertEquals(rqlst.defined_vars['OE'].stinfo['attrvar'], None)
-        self.assertEquals(rqlst.defined_vars['R'].stinfo['attrvar'], None)
+        self.assertEqual(rqlst.defined_vars['SE']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['OE']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['R']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['SE'].stinfo['attrvar'], None)
+        self.assertEqual(rqlst.defined_vars['OE'].stinfo['attrvar'], None)
+        self.assertEqual(rqlst.defined_vars['R'].stinfo['attrvar'], None)
 
     def test_0_2(self):
         rqlst = self._prepare('Any O WHERE NOT S ecrit_par O, S eid 1, S inline1 P, O inline2 P')
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['O'].stinfo['attrvar'], None)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['O'].stinfo['attrvar'], None)
 
     def test_0_4(self):
         rqlst = self._prepare('Any A,B,C WHERE A eid 12,A comment B, A ?wf_info_for C')
-        self.assertEquals(rqlst.defined_vars['A']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['A']._q_invariant, False)
         self.assert_(rqlst.defined_vars['B'].stinfo['attrvar'])
-        self.assertEquals(rqlst.defined_vars['C']._q_invariant, False)
-        self.assertEquals(rqlst.solutions, [{'A': 'TrInfo', 'B': 'String', 'C': 'Affaire'},
+        self.assertEqual(rqlst.defined_vars['C']._q_invariant, False)
+        self.assertEqual(rqlst.solutions, [{'A': 'TrInfo', 'B': 'String', 'C': 'Affaire'},
                                       {'A': 'TrInfo', 'B': 'String', 'C': 'CWUser'},
                                       {'A': 'TrInfo', 'B': 'String', 'C': 'Note'}])
 
     def test_0_5(self):
         rqlst = self._prepare('Any P WHERE N ecrit_par P, N eid 0')
-        self.assertEquals(rqlst.defined_vars['N']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
 
     def test_0_6(self):
         rqlst = self._prepare('Any P WHERE NOT N ecrit_par P, N eid 512')
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
 
     def test_0_7(self):
         rqlst = self._prepare('Personne X,Y where X nom NX, Y nom NX, X eid XE, not Y eid XE')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
         self.assert_(rqlst.defined_vars['XE'].stinfo['attrvar'])
 
     def test_0_8(self):
         rqlst = self._prepare('Any P WHERE X eid 0, NOT X connait P')
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, False)
-        #self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
-        self.assertEquals(len(rqlst.solutions), 1, rqlst.solutions)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
+        #self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(len(rqlst.solutions), 1, rqlst.solutions)
 
     def test_0_10(self):
         rqlst = self._prepare('Any X WHERE X concerne Y, Y is Note')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_0_11(self):
         rqlst = self._prepare('Any X WHERE X todo_by Y, X is Affaire')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_0_12(self):
         rqlst = self._prepare('Personne P WHERE P concerne A, A concerne S, S nom "Logilab"')
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['A']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['S']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['A']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
 
     def test_1_0(self):
         rqlst = self._prepare('Any X,Y WHERE X created_by Y, X eid 5, NOT Y eid 6')
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_1_1(self):
         rqlst = self._prepare('Any X,Y WHERE X created_by Y, X eid 5, NOT Y eid IN (6,7)')
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_2(self):
         rqlst = self._prepare('Any X WHERE X identity Y, Y eid 1')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_7(self):
         rqlst = self._prepare('Personne X,Y where X nom NX, Y nom NX, X eid XE, not Y eid XE')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_8(self):
         # DISTINCT Any P WHERE P require_group %(g)s, NOT %(u)s has_group_permission P, P is CWPermission
         rqlst = self._prepare('DISTINCT Any X WHERE A concerne X, NOT N migrated_from X, '
                               'X is Note, N eid 1')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_diff_scope_identity_deamb(self):
         rqlst = self._prepare('Any X WHERE X concerne Y, Y is Note, EXISTS(Y identity Z, Z migrated_from N)')
-        self.assertEquals(rqlst.defined_vars['Z']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['Z']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_optional_inlined(self):
         rqlst = self._prepare('Any X,S where X from_state S?')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['S']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
 
     def test_optional_inlined_2(self):
         rqlst = self._prepare('Any N,A WHERE N? inline1 A')
-        self.assertEquals(rqlst.defined_vars['N']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['A']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['A']._q_invariant, False)
 
     def test_optional_1(self):
         rqlst = self._prepare('Any X,S WHERE X travaille S?')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['S']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
 
     def test_greater_eid(self):
         rqlst = self._prepare('Any X WHERE X eid > 5')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_greater_eid_typed(self):
         rqlst = self._prepare('Any X WHERE X eid > 5, X is Note')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_max_eid(self):
         rqlst = self._prepare('Any MAX(X)')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_max_eid_typed(self):
         rqlst = self._prepare('Any MAX(X) WHERE X is Note')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_all_entities(self):
         rqlst = self._prepare('Any X')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_all_typed_entity(self):
         rqlst = self._prepare('Any X WHERE X is Note')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_has_text_1(self):
         rqlst = self._prepare('Any X WHERE X has_text "toto tata"')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'has_text')
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'has_text')
 
     def test_has_text_2(self):
         rqlst = self._prepare('Any X WHERE X is Personne, X has_text "coucou"')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'has_text')
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'has_text')
 
     def test_not_relation_1(self):
         # P can't be invariant since deambiguification caused by "NOT X require_permission P"
         # is not considered by generated sql (NOT EXISTS(...))
         rqlst = self._prepare('Any P,G WHERE P require_group G, NOT X require_permission P')
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['G']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['G']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_not_relation_2(self):
         rqlst = self._prepare('TrInfo X WHERE X eid 2, NOT X from_state Y, Y is State')
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_not_relation_3(self):
         rqlst = self._prepare('Any X, Y WHERE X eid 1, Y eid in (2, 3)')
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_4_1(self):
         rqlst = self._prepare('Note X WHERE NOT Y evaluee X')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_not_relation_4_2(self):
         rqlst = self._prepare('Any X WHERE NOT Y evaluee X')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_not_relation_4_3(self):
         rqlst = self._prepare('Any Y WHERE NOT Y evaluee X')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_4_4(self):
         rqlst = self._prepare('Any X WHERE NOT Y evaluee X, Y is CWUser')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_4_5(self):
         rqlst = self._prepare('Any X WHERE NOT Y evaluee X, Y eid %s, X is Note' % self.ueid)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.solutions, [{'X': 'Note'}])
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.solutions, [{'X': 'Note'}])
 
     def test_not_relation_5_1(self):
         rqlst = self._prepare('Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_5_2(self):
         rqlst = self._prepare('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_6(self):
         rqlst = self._prepare('Personne P where NOT P concerne A')
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['A']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['A']._q_invariant, True)
 
     def test_not_relation_7(self):
         rqlst = self._prepare('Any K,V WHERE P is CWProperty, P pkey K, P value V, NOT P for_user U')
-        self.assertEquals(rqlst.defined_vars['P']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, True)
 
     def test_exists_1(self):
         rqlst = self._prepare('Any U WHERE U eid IN (1,2), EXISTS(X owned_by U)')
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_exists_2(self):
         rqlst = self._prepare('Any U WHERE EXISTS(U eid IN (1,2), X owned_by U)')
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_exists_3(self):
         rqlst = self._prepare('Any U WHERE EXISTS(X owned_by U, X bookmarked_by U)')
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_exists_4(self):
         rqlst = self._prepare('Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_exists_5(self):
         rqlst = self._prepare('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_not_exists_1(self):
         rqlst = self._prepare('Any U WHERE NOT EXISTS(X owned_by U, X bookmarked_by U)')
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_not_exists_2(self):
         rqlst = self._prepare('Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)')
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_exists_distinct_1(self):
         rqlst = self._prepare('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)')
-        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_or_1(self):
         rqlst = self._prepare('Any X WHERE X concerne B OR C concerne X, B eid 12, C eid 13')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_or_2(self):
         rqlst = self._prepare('Any X WHERE X created_by U, X concerne B OR C concerne X, B eid 12, C eid 13')
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'created_by')
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'created_by')
 
     def test_or_3(self):
         rqlst = self._prepare('Any N WHERE A evaluee N or EXISTS(N todo_by U)')
-        self.assertEquals(rqlst.defined_vars['N']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['A']._q_invariant, True)
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['A']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, True)
 
     def test_or_exists_1(self):
         # query generated by security rewriting
@@ -300,30 +300,30 @@
                               'OR (EXISTS(I concerne H?, H owned_by D, H is Societe, A identity I, I is Affaire))) '
                               'OR (EXISTS(J concerne G?, G owned_by D, G is SubDivision, A identity J, J is Affaire))) '
                               'OR (EXISTS(K concerne F?, F owned_by D, F is Division, A identity K, K is Affaire)))')
-        self.assertEquals(rqlst.defined_vars['A']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['S']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['A']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
 
     def test_or_exists_2(self):
         rqlst = self._prepare('Any U WHERE EXISTS(U in_group G, G name "managers") OR EXISTS(X owned_by U, X bookmarked_by U)')
-        self.assertEquals(rqlst.defined_vars['U']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['G']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['X']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['G']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_or_exists_3(self):
         rqlst = self._prepare('Any COUNT(S),CS GROUPBY CS ORDERBY 1 DESC LIMIT 10 '
                               'WHERE C is Societe, S concerne C, C nom CS, '
                               '(EXISTS(S owned_by D)) OR (EXISTS(S documented_by N, N title "published"))')
-        self.assertEquals(rqlst.defined_vars['S']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
         rqlst = self._prepare('Any COUNT(S),CS GROUPBY CS ORDERBY 1 DESC LIMIT 10 '
                               'WHERE S is Affaire, C is Societe, S concerne C, C nom CS, '
                               '(EXISTS(S owned_by D)) OR (EXISTS(S documented_by N, N title "published"))')
-        self.assertEquals(rqlst.defined_vars['S']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
 
     def test_nonregr_ambiguity(self):
         rqlst = self._prepare('Note N WHERE N attachment F')
         # N may be an image as well, not invariant
-        self.assertEquals(rqlst.defined_vars['N']._q_invariant, False)
-        self.assertEquals(rqlst.defined_vars['F']._q_invariant, True)
+        self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
+        self.assertEqual(rqlst.defined_vars['F']._q_invariant, True)
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/server/test/unittest_schemaserial.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_schemaserial.py	Wed Nov 03 16:38:28 2010 +0100
@@ -47,7 +47,7 @@
 class Schema2RQLTC(TestCase):
 
     def test_eschema2rql1(self):
-        self.assertListEquals(list(eschema2rql(schema.eschema('CWAttribute'))),
+        self.assertListEqual(list(eschema2rql(schema.eschema('CWAttribute'))),
                               [
             ('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s',
              {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema',
@@ -55,26 +55,24 @@
             ])
 
     def test_eschema2rql2(self):
-        self.assertListEquals(list(eschema2rql(schema.eschema('String'))), [
+        self.assertListEqual(list(eschema2rql(schema.eschema('String'))), [
                 ('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s',
                  {'description': u'', 'final': True, 'name': u'String'})])
 
     def test_eschema2rql_specialization(self):
         # x: None since eschema.eid are None
-        self.assertListEquals(sorted(specialize2rql(schema)),
+        self.assertListEqual(sorted(specialize2rql(schema)),
                               [('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': 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})])
 
     def test_rschema2rql1(self):
-        self.assertListEquals(list(rschema2rql(schema.rschema('relation_type'), cstrtypemap)),
+        self.assertListEqual(list(rschema2rql(schema.rschema('relation_type'), cstrtypemap)),
                              [
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s',
              {'description': u'link a relation definition to its relation type', 'symmetric': False, 'name': u'relation_type', 'final' : False, 'fulltext_container': None, 'inlined': True}),
@@ -95,7 +93,7 @@
             ])
 
     def test_rschema2rql2(self):
-        self.assertListEquals(list(rschema2rql(schema.rschema('add_permission'), cstrtypemap)),
+        self.assertListEqual(list(rschema2rql(schema.rschema('add_permission'), cstrtypemap)),
                               [
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s', {'description': u'', 'symmetric': False, 'name': u'add_permission', 'final': False, 'fulltext_container': None, 'inlined': False}),
 
@@ -115,7 +113,7 @@
             ])
 
     def test_rschema2rql3(self):
-        self.assertListEquals(list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)),
+        self.assertListEqual(list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)),
                              [
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s',
              {'description': u'', 'symmetric': False, 'name': u'cardinality', 'final': True, 'fulltext_container': None, 'inlined': False}),
@@ -138,7 +136,7 @@
             ])
 
     def test_rdef2rql(self):
-        self.assertListEquals(list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap)),
+        self.assertListEqual(list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap)),
                               [
             ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
              {'se': None, 'rt': None, 'oe': None,
@@ -150,19 +148,19 @@
 
 
     def test_updateeschema2rql1(self):
-        self.assertListEquals(list(updateeschema2rql(schema.eschema('CWAttribute'), 1)),
+        self.assertListEqual(list(updateeschema2rql(schema.eschema('CWAttribute'), 1)),
                               [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
                                 {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema', 'x': 1, 'final': False, 'name': u'CWAttribute'}),
                                ])
 
     def test_updateeschema2rql2(self):
-        self.assertListEquals(list(updateeschema2rql(schema.eschema('String'), 1)),
+        self.assertListEqual(list(updateeschema2rql(schema.eschema('String'), 1)),
                               [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
                                 {'description': u'', 'x': 1, 'final': True, 'name': u'String'})
                                ])
 
     def test_updaterschema2rql1(self):
-        self.assertListEquals(list(updaterschema2rql(schema.rschema('relation_type'), 1)),
+        self.assertListEqual(list(updaterschema2rql(schema.rschema('relation_type'), 1)),
                              [
             ('SET X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s WHERE X eid %(x)s',
              {'x': 1, 'symmetric': False,
@@ -178,7 +176,7 @@
               'inlined': False, 'name': u'add_permission'})
             ]
         for i, (rql, args) in enumerate(updaterschema2rql(schema.rschema('add_permission'), 1)):
-            yield self.assertEquals, (rql, args), expected[i]
+            yield self.assertEqual, (rql, args), expected[i]
 
 class Perms2RQLTC(TestCase):
     GROUP_MAPPING = {
@@ -189,7 +187,7 @@
         }
 
     def test_eperms2rql1(self):
-        self.assertListEquals([(rql, kwargs) for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)],
+        self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)],
                               [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
                                ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
                                ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
@@ -199,7 +197,7 @@
                                ])
 
     def test_rperms2rql2(self):
-        self.assertListEquals([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('read_permission').rdef('CWEType', 'CWGroup'), self.GROUP_MAPPING)],
+        self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('read_permission').rdef('CWEType', 'CWGroup'), self.GROUP_MAPPING)],
                               [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
                                ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
                                ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
@@ -208,7 +206,7 @@
                                ])
 
     def test_rperms2rql3(self):
-        self.assertListEquals([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'), self.GROUP_MAPPING)],
+        self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'), self.GROUP_MAPPING)],
                               [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
                                ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
                                ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
@@ -216,7 +214,7 @@
                                ])
 
     #def test_perms2rql(self):
-    #    self.assertListEquals(perms2rql(schema, self.GROUP_MAPPING),
+    #    self.assertListEqual(perms2rql(schema, self.GROUP_MAPPING),
     #                         ['INSERT CWEType X: X name 'Societe', X final FALSE'])
 
 
--- a/server/test/unittest_security.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_security.py	Wed Nov 03 16:38:28 2010 +0100
@@ -22,7 +22,7 @@
 from logilab.common.testlib import unittest_main, TestCase
 from cubicweb.devtools.testlib import CubicWebTC
 
-from cubicweb import Unauthorized, ValidationError
+from cubicweb import Unauthorized, ValidationError, QueryError
 from cubicweb.server.querier import check_read_access
 
 class BaseSecurityTC(CubicWebTC):
@@ -81,10 +81,10 @@
         cnx = self.login('iaminusersgrouponly')
         self.hijack_source_execute()
         self.execute('Any U WHERE NOT A todo_by U, A is Affaire')
-        self.assertEquals(self.query[0][1].as_string(),
+        self.assertEqual(self.query[0][1].as_string(),
                           'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
         self.execute('Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
-        self.assertEquals(self.query[0][1].as_string(),
+        self.assertEqual(self.query[0][1].as_string(),
                           'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
 
 class SecurityTC(BaseSecurityTC):
@@ -103,7 +103,7 @@
         cu = cnx.cursor()
         cu.execute("INSERT Personne X: X nom 'bidule'")
         self.assertRaises(Unauthorized, cnx.commit)
-        self.assertEquals(cu.execute('Personne X').rowcount, 1)
+        self.assertEqual(cu.execute('Personne X').rowcount, 1)
 
     def test_insert_rql_permission(self):
         # test user can only add une affaire related to a societe he owns
@@ -113,7 +113,7 @@
         self.assertRaises(Unauthorized, cnx.commit)
         # test nothing has actually been inserted
         self.restore_connection()
-        self.assertEquals(self.execute('Affaire X').rowcount, 1)
+        self.assertEqual(self.execute('Affaire X').rowcount, 1)
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         cu.execute("INSERT Affaire X: X sujet 'cool'")
@@ -128,7 +128,7 @@
         cu.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
         self.assertRaises(Unauthorized, cnx.commit)
         self.restore_connection()
-        self.assertEquals(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
+        self.assertEqual(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
 
     def test_update_security_2(self):
         cnx = self.login('anon')
@@ -139,7 +139,7 @@
         #self.assertRaises(Unauthorized, cnx.commit)
         # test nothing has actually been inserted
         self.restore_connection()
-        self.assertEquals(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
+        self.assertEqual(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
 
     def test_update_security_3(self):
         cnx = self.login('iaminusersgrouponly')
@@ -189,6 +189,8 @@
         cnx.commit()
         # to actually get Unauthorized exception, try to delete an entity we can read
         self.assertRaises(Unauthorized, cu.execute, "DELETE Societe S")
+        self.assertRaises(QueryError, cnx.commit) # can't commit anymore
+        cnx.rollback() # required after Unauthorized
         cu.execute("INSERT Affaire X: X sujet 'pascool'")
         cu.execute("INSERT Societe X: X nom 'chouette'")
         cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
@@ -210,15 +212,16 @@
         cnx.commit()
         # to actually get Unauthorized exception, try to insert a relation were we can read both entities
         rset = cu.execute('Personne P')
-        self.assertEquals(len(rset), 1)
+        self.assertEqual(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")
+        self.assertRaises(QueryError, cnx.commit) # can't commit anymore
+        cnx.rollback()
         # test nothing has actually been inserted:
-        self.assertEquals(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe').rowcount, 0)
+        self.assertEqual(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe').rowcount, 0)
         cu.execute("INSERT Societe X: X nom 'chouette'")
         cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
         cnx.commit()
@@ -240,6 +243,8 @@
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         self.assertRaises(Unauthorized, cu.execute, "DELETE A concerne S")
+        self.assertRaises(QueryError, cnx.commit) # can't commit anymore
+        cnx.rollback() # required after Unauthorized
         cu.execute("INSERT Societe X: X nom 'chouette'")
         cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
         cnx.commit()
@@ -279,7 +284,7 @@
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         rset = cu.execute('Affaire X')
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
         # cache test
         self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
@@ -288,12 +293,12 @@
         cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
         cnx.commit()
         rset = cu.execute('Any X WHERE X eid %(x)s', {'x': aff2})
-        self.assertEquals(rset.rows, [[aff2]])
+        self.assertEqual(rset.rows, [[aff2]])
         # more cache test w/ NOT eid
         rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid})
-        self.assertEquals(rset.rows, [[aff2]])
+        self.assertEqual(rset.rows, [[aff2]])
         rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2})
-        self.assertEquals(rset.rows, [])
+        self.assertEqual(rset.rows, [])
         # test can't update an attribute of an entity that can't be readen
         self.assertRaises(Unauthorized, cu.execute, 'SET X sujet "hacked" WHERE X eid %(x)s', {'x': eid})
 
@@ -310,7 +315,7 @@
             self.failUnless(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
             # XXX would be nice if it worked
             rset = cu.execute("Affaire X WHERE X sujet 'cool'")
-            self.assertEquals(len(rset), 0)
+            self.assertEqual(len(rset), 0)
         finally:
             affschema.set_action_permissions('read', origperms)
             cnx.close()
@@ -330,7 +335,7 @@
         self.failUnless(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
         self.failUnless(cu.execute('Any X WHERE X eid %(x)s', {'x':card1}))
         rset = cu.execute("Any X WHERE X has_text 'cool'")
-        self.assertEquals(sorted(eid for eid, in rset.rows),
+        self.assertEqual(sorted(eid for eid, in rset.rows),
                           [card1, aff2])
 
     def test_read_erqlexpr_has_text2(self):
@@ -341,9 +346,9 @@
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         rset = cu.execute('Any N WHERE N has_text "bidule"')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
         rset = cu.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_read_erqlexpr_optional_rel(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
@@ -353,7 +358,7 @@
         cnx = self.login('anon')
         cu = cnx.cursor()
         rset = cu.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
-        self.assertEquals(len(rset.rows), 1, rset.rows)
+        self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_read_erqlexpr_aggregat(self):
         self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
@@ -361,22 +366,22 @@
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
-        self.assertEquals(rset.rows, [[0]])
+        self.assertEqual(rset.rows, [[0]])
         aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
         soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
         cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
         cnx.commit()
         rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
-        self.assertEquals(rset.rows, [[1]])
+        self.assertEqual(rset.rows, [[1]])
         rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN')
         values = dict(rset)
-        self.assertEquals(values['Affaire'], 1)
-        self.assertEquals(values['Societe'], 2)
+        self.assertEqual(values['Affaire'], 1)
+        self.assertEqual(values['Societe'], 2)
         rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN WITH X BEING ((Affaire X) UNION (Societe X))')
-        self.assertEquals(len(rset), 2)
+        self.assertEqual(len(rset), 2)
         values = dict(rset)
-        self.assertEquals(values['Affaire'], 1)
-        self.assertEquals(values['Societe'], 2)
+        self.assertEqual(values['Affaire'], 1)
+        self.assertEqual(values['Societe'], 2)
 
 
     def test_attribute_security(self):
@@ -405,7 +410,7 @@
         # Note.para attribute editable by managers or if the note is in "todo" state
         note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
         self.commit()
-        note.fire_transition('markasdone')
+        note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
         self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
         self.commit()
         cnx = self.login('iaminusersgrouponly')
@@ -414,13 +419,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})),
+        self.assertEqual(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()
@@ -434,11 +439,11 @@
         rset = cu.execute('CWUser X')
         self.failUnless(rset)
         x = rset.get_entity(0, 0)
-        self.assertEquals(x.login, None)
+        self.assertEqual(x.login, None)
         self.failUnless(x.creation_date)
         x = rset.get_entity(1, 0)
         x.complete()
-        self.assertEquals(x.login, None)
+        self.assertEqual(x.login, None)
         self.failUnless(x.creation_date)
         cnx.rollback()
 
@@ -455,11 +460,11 @@
         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"')),
+        self.assertEqual(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')),
                           1)
-        self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
+        self.assertEqual(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
                                            'X owned_by U, U login "admin"')),
                           1) # TrInfo at the above state change
         cnx = self.login('iaminusersgrouponly')
@@ -474,9 +479,9 @@
         cu = cnx.cursor()
         # anonymous user can only read itself
         rset = cu.execute('Any L WHERE X owned_by U, U login L')
-        self.assertEquals(rset.rows, [['anon']])
+        self.assertEqual(rset.rows, [['anon']])
         rset = cu.execute('CWUser X')
-        self.assertEquals(rset.rows, [[anon.eid]])
+        self.assertEqual(rset.rows, [[anon.eid]])
         # anonymous user can read groups (necessary to check allowed transitions for instance)
         self.assert_(cu.execute('CWGroup X'))
         # should only be able to read the anonymous user, not another one
@@ -489,7 +494,7 @@
         #                  {'x': self.user.eid})
 
         rset = cu.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid})
-        self.assertEquals(rset.rows, [[anon.eid]])
+        self.assertEqual(rset.rows, [[anon.eid]])
         # but can't modify it
         cu.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid})
         self.assertRaises(Unauthorized, cnx.commit)
@@ -517,14 +522,14 @@
         cnx = self.login('anon')
         cu = cnx.cursor()
         anoneid = self.session.user.eid
-        self.assertEquals(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+        self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
                                      'B bookmarked_by U, U eid %s' % anoneid).rows,
                           [['index', '?vid=index']])
-        self.assertEquals(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+        self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
                                      'B bookmarked_by U, U eid %(x)s', {'x': anoneid}).rows,
                           [['index', '?vid=index']])
         # can read others bookmarks as well
-        self.assertEquals(cu.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
+        self.assertEqual(cu.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
                           [[beid1]])
         self.assertRaises(Unauthorized, cu.execute,'DELETE B bookmarked_by U')
         self.assertRaises(Unauthorized,
@@ -536,7 +541,7 @@
         cnx = self.login('anon')
         cu = cnx.cursor()
         names = [t for t, in cu.execute('Any N ORDERBY lower(N) WHERE X name N')]
-        self.assertEquals(names, sorted(names, key=lambda x: x.lower()))
+        self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
 
     def test_in_state_without_update_perm(self):
         """check a user change in_state without having update permission on the
@@ -557,14 +562,15 @@
             cu = cnx.cursor()
             self.schema['Affaire'].set_action_permissions('read', ('users',))
             aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-            aff.fire_transition('abort')
+            aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
             cnx.commit()
             # though changing a user state (even logged user) is reserved to managers
             user = cnx.user(self.session)
             # XXX wether it should raise Unauthorized or ValidationError is not clear
             # the best would probably ValidationError if the transition doesn't exist
             # from the current state but Unauthorized if it exists but user can't pass it
-            self.assertRaises(ValidationError, user.fire_transition, 'deactivate')
+            self.assertRaises(ValidationError,
+                              user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
         finally:
             # restore orig perms
             for action, perms in affaire_perms.iteritems():
@@ -572,18 +578,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()
-        self.assertEquals(trinfo.comment, 'bouh!')
+        aff.cw_clear_relation_cache('wf_info_for', 'object')
+        trinfo = iworkflowable.latest_trinfo()
+        self.assertEqual(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_session.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_session.py	Wed Nov 03 16:38:28 2010 +0100
@@ -43,7 +43,7 @@
 class MakeDescriptionTC(TestCase):
     def test_known_values(self):
         solution = {'A': 'Int', 'B': 'CWUser'}
-        self.assertEquals(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
+        self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
                           ['Int','CWUser'])
 
 class InternalSessionTC(CubicWebTC):
--- a/server/test/unittest_sqlutils.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_sqlutils.py	Wed Nov 03 16:38:28 2010 +0100
@@ -36,13 +36,13 @@
 
     def test_init(self):
         o = SQLAdapterMixIn(BASE_CONFIG)
-        self.assertEquals(o.dbhelper.dbencoding, 'UTF-8')
+        self.assertEqual(o.dbhelper.dbencoding, 'UTF-8')
 
     def test_init_encoding(self):
         config = BASE_CONFIG.copy()
         config['db-encoding'] = 'ISO-8859-1'
         o = SQLAdapterMixIn(config)
-        self.assertEquals(o.dbhelper.dbencoding, 'ISO-8859-1')
+        self.assertEqual(o.dbhelper.dbencoding, 'ISO-8859-1')
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_storage.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_storage.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,13 +15,11 @@
 #
 # 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
 
-from logilab.common.testlib import unittest_main, tag
+from logilab.common.testlib import unittest_main, tag, Tags
 from cubicweb.devtools.testlib import CubicWebTC
 
 import os.path as osp
@@ -29,13 +27,13 @@
 import tempfile
 
 from cubicweb import Binary, QueryError
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.server.sources import storages
 from cubicweb.server.hook import Hook, Operation
 
 class DummyBeforeHook(Hook):
     __regid__ = 'dummy-before-hook'
-    __select__ = Hook.__select__ & implements('File')
+    __select__ = Hook.__select__ & is_instance('File')
     events = ('before_add_entity',)
 
     def __call__(self):
@@ -44,7 +42,7 @@
 
 class DummyAfterHook(Hook):
     __regid__ = 'dummy-after-hook'
-    __select__ = Hook.__select__ & implements('File')
+    __select__ = Hook.__select__ & is_instance('File')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -54,6 +52,8 @@
 
 class StorageTC(CubicWebTC):
 
+    tags = CubicWebTC.tags | Tags('Storage', 'BFSS')
+
     def setup_database(self):
         self.tempdir = tempfile.mkdtemp()
         bfs_storage = storages.BytesFileSystemStorage(self.tempdir)
@@ -80,34 +80,34 @@
         expected_filepath = osp.join(self.tempdir, '%s_data_%s' %
                                      (f1.eid, f1.data_name))
         self.failUnless(osp.isfile(expected_filepath))
-        self.assertEquals(file(expected_filepath).read(), 'the-data')
+        self.assertEqual(file(expected_filepath).read(), 'the-data')
         self.rollback()
         self.failIf(osp.isfile(expected_filepath))
         f1 = self.create_file()
         self.commit()
-        self.assertEquals(file(expected_filepath).read(), 'the-data')
+        self.assertEqual(file(expected_filepath).read(), 'the-data')
         f1.set_attributes(data=Binary('the new data'))
         self.rollback()
-        self.assertEquals(file(expected_filepath).read(), 'the-data')
-        f1.delete()
+        self.assertEqual(file(expected_filepath).read(), 'the-data')
+        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))
 
     def test_bfss_sqlite_fspath(self):
         f1 = self.create_file()
         expected_filepath = osp.join(self.tempdir, '%s_data_%s' % (f1.eid, f1.data_name))
-        self.assertEquals(self.fspath(f1), expected_filepath)
+        self.assertEqual(self.fspath(f1), expected_filepath)
 
     def test_bfss_fs_importing_doesnt_touch_path(self):
         self.session.transaction_data['fs_importing'] = True
         filepath = osp.abspath(__file__)
         f1 = self.session.create_entity('File', data=Binary(filepath),
                                         data_format=u'text/plain', data_name=u'foo')
-        self.assertEquals(self.fspath(f1), filepath)
+        self.assertEqual(self.fspath(f1), filepath)
 
     def test_source_storage_transparency(self):
         with self.temporary_appobjects(DummyBeforeHook, DummyAfterHook):
@@ -116,11 +116,11 @@
     def test_source_mapped_attribute_error_cases(self):
         ex = self.assertRaises(QueryError, self.execute,
                                'Any X WHERE X data ~= "hop", X is File')
-        self.assertEquals(str(ex), 'can\'t use File.data (X data ILIKE "hop") in restriction')
+        self.assertEqual(str(ex), 'can\'t use File.data (X data ILIKE "hop") in restriction')
         ex = self.assertRaises(QueryError, self.execute,
                                'Any X, Y WHERE X data D, Y data D, '
                                'NOT X identity Y, X is File, Y is File')
-        self.assertEquals(str(ex), "can't use D as a restriction variable")
+        self.assertEqual(str(ex), "can't use D as a restriction variable")
         # query returning mix of mapped / regular attributes (only file.data
         # mapped, not image.data for instance)
         ex = self.assertRaises(QueryError, self.execute,
@@ -129,15 +129,21 @@
                                '  UNION '
                                ' (Any D WHERE X data D, X is File)'
                                ')')
-        self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
+        self.assertEqual(str(ex), 'query fetch some source mapped attribute, some not')
         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)')
-        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')
+                               '(Any D WHERE X title D, X is Bookmark)')
+        self.assertEqual(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.assertEqual(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()
@@ -146,30 +152,30 @@
                             '  UNION '
                             ' (Any D, X WHERE X eid %(x)s, X data D)'
                             ')', {'x': f1.eid})
-        self.assertEquals(len(rset), 2)
-        self.assertEquals(rset[0][0], f1.eid)
-        self.assertEquals(rset[1][0], f1.eid)
-        self.assertEquals(rset[0][1].getvalue(), 'the-data')
-        self.assertEquals(rset[1][1].getvalue(), 'the-data')
+        self.assertEqual(len(rset), 2)
+        self.assertEqual(rset[0][0], f1.eid)
+        self.assertEqual(rset[1][0], f1.eid)
+        self.assertEqual(rset[0][1].getvalue(), 'the-data')
+        self.assertEqual(rset[1][1].getvalue(), 'the-data')
         rset = self.execute('Any X,LENGTH(D) WHERE X eid %(x)s, X data D',
                             {'x': f1.eid})
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset[0][0], f1.eid)
-        self.assertEquals(rset[0][1], len('the-data'))
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset[0][0], f1.eid)
+        self.assertEqual(rset[0][1], len('the-data'))
         rset = self.execute('Any X,LENGTH(D) WITH D,X BEING ('
                             ' (Any D, X WHERE X eid %(x)s, X data D)'
                             '  UNION '
                             ' (Any D, X WHERE X eid %(x)s, X data D)'
                             ')', {'x': f1.eid})
-        self.assertEquals(len(rset), 2)
-        self.assertEquals(rset[0][0], f1.eid)
-        self.assertEquals(rset[1][0], f1.eid)
-        self.assertEquals(rset[0][1], len('the-data'))
-        self.assertEquals(rset[1][1], len('the-data'))
+        self.assertEqual(len(rset), 2)
+        self.assertEqual(rset[0][0], f1.eid)
+        self.assertEqual(rset[1][0], f1.eid)
+        self.assertEqual(rset[0][1], len('the-data'))
+        self.assertEqual(rset[1][1], len('the-data'))
         ex = self.assertRaises(QueryError, self.execute,
                                'Any X,UPPER(D) WHERE X eid %(x)s, X data D',
                                {'x': f1.eid})
-        self.assertEquals(str(ex), 'UPPER can not be called on mapped attribute')
+        self.assertEqual(str(ex), 'UPPER can not be called on mapped attribute')
 
 
     def test_bfss_fs_importing_transparency(self):
@@ -177,10 +183,10 @@
         filepath = osp.abspath(__file__)
         f1 = self.session.create_entity('File', data=Binary(filepath),
                                         data_format=u'text/plain', data_name=u'foo')
-        self.assertEquals(f1.data.getvalue(), file(filepath).read(),
+        self.assertEqual(f1.data.getvalue(), file(filepath).read(),
                           'files content differ')
 
-    @tag('Storage', 'BFSS', 'update')
+    @tag('update')
     def test_bfss_update_with_existing_data(self):
         # use self.session to use server-side cache
         f1 = self.session.create_entity('File', data=Binary('some data'),
@@ -189,12 +195,12 @@
         #       update f1's local dict. We want the pure rql version to work
         self.execute('SET F data %(d)s WHERE F eid %(f)s',
                      {'d': Binary('some other data'), 'f': f1.eid})
-        self.assertEquals(f1.data.getvalue(), 'some other data')
+        self.assertEqual(f1.data.getvalue(), 'some other data')
         self.commit()
         f2 = self.execute('Any F WHERE F eid %(f)s, F is File', {'f': f1.eid}).get_entity(0, 0)
-        self.assertEquals(f2.data.getvalue(), 'some other data')
+        self.assertEqual(f2.data.getvalue(), 'some other data')
 
-    @tag('Storage', 'BFSS', 'update', 'extension', 'commit')
+    @tag('update', 'extension', 'commit')
     def test_bfss_update_with_different_extension_commited(self):
         # use self.session to use server-side cache
         f1 = self.session.create_entity('File', data=Binary('some data'),
@@ -204,7 +210,7 @@
         self.commit()
         old_path = self.fspath(f1)
         self.failUnless(osp.isfile(old_path))
-        self.assertEquals(osp.splitext(old_path)[1], '.txt')
+        self.assertEqual(osp.splitext(old_path)[1], '.txt')
         self.execute('SET F data %(d)s, F data_name %(dn)s, F data_format %(df)s WHERE F eid %(f)s',
                      {'d': Binary('some other data'), 'f': f1.eid, 'dn': u'bar.jpg', 'df': u'image/jpeg'})
         self.commit()
@@ -214,9 +220,9 @@
         new_path = self.fspath(f2)
         self.failIf(osp.isfile(old_path))
         self.failUnless(osp.isfile(new_path))
-        self.assertEquals(osp.splitext(new_path)[1], '.jpg')
+        self.assertEqual(osp.splitext(new_path)[1], '.jpg')
 
-    @tag('Storage', 'BFSS', 'update', 'extension', 'rollback')
+    @tag('update', 'extension', 'rollback')
     def test_bfss_update_with_different_extension_rollbacked(self):
         # use self.session to use server-side cache
         f1 = self.session.create_entity('File', data=Binary('some data'),
@@ -227,7 +233,7 @@
         old_path = self.fspath(f1)
         old_data = f1.data.getvalue()
         self.failUnless(osp.isfile(old_path))
-        self.assertEquals(osp.splitext(old_path)[1], '.txt')
+        self.assertEqual(osp.splitext(old_path)[1], '.txt')
         self.execute('SET F data %(d)s, F data_name %(dn)s, F data_format %(df)s WHERE F eid %(f)s',
                      {'d': Binary('some other data'), 'f': f1.eid, 'dn': u'bar.jpg', 'df': u'image/jpeg'})
         self.rollback()
@@ -237,10 +243,11 @@
         new_path = self.fspath(f2)
         new_data = f2.data.getvalue()
         self.failUnless(osp.isfile(new_path))
-        self.assertEquals(osp.splitext(new_path)[1], '.txt')
-        self.assertEquals(old_path, new_path)
-        self.assertEquals(old_data, new_data)
+        self.assertEqual(osp.splitext(new_path)[1], '.txt')
+        self.assertEqual(old_path, new_path)
+        self.assertEqual(old_data, new_data)
 
+    @tag('fs_importing', 'update')
     def test_bfss_update_with_fs_importing(self):
         # use self.session to use server-side cache
         f1 = self.session.create_entity('File', data=Binary('some data'),
@@ -252,10 +259,39 @@
         self.execute('SET F data %(d)s WHERE F eid %(f)s',
                      {'d': Binary(new_fspath), 'f': f1.eid})
         self.commit()
-        self.assertEquals(f1.data.getvalue(), 'the new data')
-        self.assertEquals(self.fspath(f1), new_fspath)
+        self.assertEqual(f1.data.getvalue(), 'the new data')
+        self.assertEqual(self.fspath(f1), new_fspath)
         self.failIf(osp.isfile(old_fspath))
 
+    @tag('fsimport')
+    def test_clean(self):
+        fsimport = storages.fsimport
+        td = self.session.transaction_data
+        self.assertNotIn('fs_importing', td)
+        with fsimport(self.session):
+            self.assertIn('fs_importing', td)
+            self.assertTrue(td['fs_importing'])
+        self.assertNotIn('fs_importing', td)
+
+    @tag('fsimport')
+    def test_true(self):
+        fsimport = storages.fsimport
+        td = self.session.transaction_data
+        td['fs_importing'] = True
+        with fsimport(self.session):
+            self.assertIn('fs_importing', td)
+            self.assertTrue(td['fs_importing'])
+        self.assertTrue(td['fs_importing'])
+
+    @tag('fsimport')
+    def test_False(self):
+        fsimport = storages.fsimport
+        td = self.session.transaction_data
+        td['fs_importing'] = False
+        with fsimport(self.session):
+            self.assertIn('fs_importing', td)
+            self.assertTrue(td['fs_importing'])
+        self.assertFalse(td['fs_importing'])
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_undo.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/test/unittest_undo.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -57,36 +54,36 @@
                           self.cnx.undo_transaction, 'hop')
         txinfo = self.cnx.transaction_info(self.txuuid)
         self.failUnless(txinfo.datetime)
-        self.assertEquals(txinfo.user_eid, self.session.user.eid)
-        self.assertEquals(txinfo.user().login, 'admin')
+        self.assertEqual(txinfo.user_eid, self.session.user.eid)
+        self.assertEqual(txinfo.user().login, 'admin')
         actions = txinfo.actions_list()
-        self.assertEquals(len(actions), 2)
+        self.assertEqual(len(actions), 2)
         actions = txinfo.actions_list(public=False)
-        self.assertEquals(len(actions), 6)
+        self.assertEqual(len(actions), 6)
         a1 = actions[0]
-        self.assertEquals(a1.action, 'C')
-        self.assertEquals(a1.eid, self.toto.eid)
-        self.assertEquals(a1.etype,'CWUser')
-        self.assertEquals(a1.changes, None)
-        self.assertEquals(a1.public, True)
-        self.assertEquals(a1.order, 1)
+        self.assertEqual(a1.action, 'C')
+        self.assertEqual(a1.eid, self.toto.eid)
+        self.assertEqual(a1.etype,'CWUser')
+        self.assertEqual(a1.changes, None)
+        self.assertEqual(a1.public, True)
+        self.assertEqual(a1.order, 1)
         a4 = actions[3]
-        self.assertEquals(a4.action, 'A')
-        self.assertEquals(a4.rtype, 'in_group')
-        self.assertEquals(a4.eid_from, self.toto.eid)
-        self.assertEquals(a4.eid_to, self.toto.in_group[0].eid)
-        self.assertEquals(a4.order, 4)
+        self.assertEqual(a4.action, 'A')
+        self.assertEqual(a4.rtype, 'in_group')
+        self.assertEqual(a4.eid_from, self.toto.eid)
+        self.assertEqual(a4.eid_to, self.toto.in_group[0].eid)
+        self.assertEqual(a4.order, 4)
         for i, rtype in ((1, 'owned_by'), (2, 'owned_by'),
                          (4, 'in_state'), (5, 'created_by')):
             a = actions[i]
-            self.assertEquals(a.action, 'A')
-            self.assertEquals(a.eid_from, self.toto.eid)
-            self.assertEquals(a.rtype, rtype)
-            self.assertEquals(a.order, i+1)
+            self.assertEqual(a.action, 'A')
+            self.assertEqual(a.eid_from, self.toto.eid)
+            self.assertEqual(a.rtype, rtype)
+            self.assertEqual(a.order, i+1)
         # test undoable_transactions
         txs = self.cnx.undoable_transactions()
-        self.assertEquals(len(txs), 1)
-        self.assertEquals(txs[0].uuid, self.txuuid)
+        self.assertEqual(len(txs), 1)
+        self.assertEqual(txs[0].uuid, self.txuuid)
         # test transaction_info / undoable_transactions security
         cnx = self.login('anon')
         self.assertRaises(NoSuchTransaction,
@@ -96,7 +93,7 @@
         self.assertRaises(NoSuchTransaction,
                           cnx.undo_transaction, self.txuuid)
         txs = cnx.undoable_transactions()
-        self.assertEquals(len(txs), 0)
+        self.assertEqual(len(txs), 0)
 
     def test_undoable_transactions(self):
         toto = self.toto
@@ -104,35 +101,35 @@
                                        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')
-        self.assertEquals(len(txs), 1, txs)
-        self.assertEquals(txs[0].uuid, txuuid2)
+        self.assertEqual(len(txs), 1, txs)
+        self.assertEqual(txs[0].uuid, txuuid2)
         txs = undoable_transactions(action='C')
-        self.assertEquals(len(txs), 2, txs)
-        self.assertEquals(txs[0].uuid, txuuid1)
-        self.assertEquals(txs[1].uuid, self.txuuid)
+        self.assertEqual(len(txs), 2, txs)
+        self.assertEqual(txs[0].uuid, txuuid1)
+        self.assertEqual(txs[1].uuid, self.txuuid)
         txs = undoable_transactions(eid=toto.eid)
-        self.assertEquals(len(txs), 3)
-        self.assertEquals(txs[0].uuid, txuuid2)
-        self.assertEquals(txs[1].uuid, txuuid1)
-        self.assertEquals(txs[2].uuid, self.txuuid)
+        self.assertEqual(len(txs), 3)
+        self.assertEqual(txs[0].uuid, txuuid2)
+        self.assertEqual(txs[1].uuid, txuuid1)
+        self.assertEqual(txs[2].uuid, self.txuuid)
         txs = undoable_transactions(etype='CWUser')
-        self.assertEquals(len(txs), 2)
+        self.assertEqual(len(txs), 2)
         txs = undoable_transactions(etype='CWUser', action='C')
-        self.assertEquals(len(txs), 1)
-        self.assertEquals(txs[0].uuid, self.txuuid)
+        self.assertEqual(len(txs), 1)
+        self.assertEqual(txs[0].uuid, self.txuuid)
         txs = undoable_transactions(etype='EmailAddress', action='D')
-        self.assertEquals(len(txs), 0)
+        self.assertEqual(len(txs), 0)
         txs = undoable_transactions(etype='EmailAddress', action='D',
                                     public=False)
-        self.assertEquals(len(txs), 1)
-        self.assertEquals(txs[0].uuid, txuuid2)
+        self.assertEqual(len(txs), 1)
+        self.assertEqual(txs[0].uuid, txuuid2)
         txs = undoable_transactions(eid=toto.eid, action='R', public=False)
-        self.assertEquals(len(txs), 1)
-        self.assertEquals(txs[0].uuid, txuuid2)
+        self.assertEqual(len(txs), 1)
+        self.assertEqual(txs[0].uuid, txuuid2)
 
     def test_undo_deletion_base(self):
         toto = self.toto
@@ -146,34 +143,34 @@
                                        for_user=toto)
         self.commit()
         txs = self.cnx.undoable_transactions()
-        self.assertEquals(len(txs), 2)
-        toto.delete()
+        self.assertEqual(len(txs), 2)
+        toto.cw_delete()
         txuuid = self.commit()
         actions = self.cnx.transaction_info(txuuid).actions_list()
-        self.assertEquals(len(actions), 1)
+        self.assertEqual(len(actions), 1)
         toto.clear_all_caches()
         e.clear_all_caches()
         errors = self.cnx.undo_transaction(txuuid)
         undotxuuid = self.commit()
-        self.assertEquals(undotxuuid, None) # undo not undoable
-        self.assertEquals(errors, [])
+        self.assertEqual(undotxuuid, None) # undo not undoable
+        self.assertEqual(errors, [])
         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([(p.pkey, p.value) for p in toto.reverse_for_user],
+        self.assertEqual(toto.cw_adapt_to('IWorkflowable').state, 'activated')
+        self.assertEqual(toto.cw_adapt_to('IEmailable').get_email(), 'toto@logilab.org')
+        self.assertEqual([(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],
+        self.assertEqual([g.name for g in toto.in_group],
                           ['users'])
-        self.assertEquals([et.name for et in toto.related('is', entities=True)],
+        self.assertEqual([et.name for et in toto.related('is', entities=True)],
                           ['CWUser'])
-        self.assertEquals([et.name for et in toto.is_instance_of],
+        self.assertEqual([et.name for et in toto.is_instance_of],
                           ['CWUser'])
         # undoing shouldn't be visble in undoable transaction, and the undoed
         # transaction should be removed
         txs = self.cnx.undoable_transactions()
-        self.assertEquals(len(txs), 2)
+        self.assertEqual(len(txs), 2)
         self.assertRaises(NoSuchTransaction,
                           self.cnx.transaction_info, txuuid)
         self.check_transaction_deleted(txuuid)
@@ -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)
@@ -194,9 +191,9 @@
         errors = self.cnx.undo_transaction(txuuid)
         self.commit()
         p.clear_all_caches()
-        self.assertEquals(p.fiche[0].eid, c2.eid)
-        self.assertEquals(len(errors), 1)
-        self.assertEquals(errors[0],
+        self.assertEqual(p.fiche[0].eid, c2.eid)
+        self.assertEqual(len(errors), 1)
+        self.assertEqual(errors[0],
                           "Can't restore object relation fiche to entity "
                           "%s which is already linked using this relation." % p.eid)
 
@@ -207,17 +204,17 @@
         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,
+        self.assertEqual(errors,
                           [u"Can't restore relation in_group, object entity "
                           "%s doesn't exist anymore." % g.eid])
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.entity, self.toto.eid)
-        self.assertEquals(ex.errors,
+        self.assertEqual(ex.entity, self.toto.eid)
+        self.assertEqual(ex.errors,
                           {'in_group-subject': u'at least one relation in_group is '
                            'required on CWUser (%s)' % self.toto.eid})
 
@@ -257,8 +254,8 @@
         self.commit()
         ex = self.assertRaises(ValidationError,
                                self.cnx.undo_transaction, txuuid)
-        self.assertEquals(ex.entity, tutu.eid)
-        self.assertEquals(ex.errors,
+        self.assertEqual(ex.entity, tutu.eid)
+        self.assertEqual(ex.errors,
                           {None: 'some later transaction(s) touch entity, undo them first'})
 
     def test_undo_creation_integrity_2(self):
@@ -270,15 +267,15 @@
         self.commit()
         ex = self.assertRaises(ValidationError,
                                self.cnx.undo_transaction, txuuid)
-        self.assertEquals(ex.entity, g.eid)
-        self.assertEquals(ex.errors,
+        self.assertEqual(ex.entity, g.eid)
+        self.assertEqual(ex.errors,
                           {None: 'some later transaction(s) touch entity, undo them first'})
-        # self.assertEquals(errors,
+        # self.assertEqual(errors,
         #                   [u"Can't restore relation in_group, object entity "
         #                   "%s doesn't exist anymore." % g.eid])
         # ex = self.assertRaises(ValidationError, self.commit)
-        # self.assertEquals(ex.entity, self.toto.eid)
-        # self.assertEquals(ex.errors,
+        # self.assertEqual(ex.entity, self.toto.eid)
+        # self.assertEqual(ex.errors,
         #                   {'in_group-subject': u'at least one relation in_group is '
         #                    'required on CWUser (%s)' % self.toto.eid})
 
--- a/server/utils.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/server/utils.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""Some utilities for the CubicWeb server.
-
-"""
+"""Some utilities for the CubicWeb server."""
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -62,7 +60,7 @@
         """recursive looping function"""
         if seqin:                   # any more sequences to process?
             for item in seqin[0]:
-                newcomb = comb + [item]     # add next item to current combination
+                newcomb = comb + [item] # add next item to current combination
                 # call rloop w/ remaining seqs, newcomb
                 for item in rloop(seqin[1:], newcomb):
                     yield item          # seqs and newcomb
@@ -119,6 +117,14 @@
     sconfig.input_config(inputlevel=inputlevel)
     return sconfig
 
+_MARKER=object()
+def func_name(func):
+    name = getattr(func, '__name__', _MARKER)
+    if name is _MARKER:
+        name = getattr(func, 'func_name', _MARKER)
+    if name is _MARKER:
+        name = repr(func)
+    return name
 
 class LoopTask(object):
     """threaded task restarting itself once executed"""
@@ -126,7 +132,7 @@
         if interval <= 0:
             raise ValueError('Loop task interval must be > 0 '
                              '(current value: %f for %s)' % \
-                             (interval, func.__name__))
+                             (interval, func_name(func)))
         self.interval = interval
         def auto_restart_func(self=self, func=func, args=args):
             try:
@@ -134,7 +140,7 @@
             finally:
                 self.start()
         self.func = auto_restart_func
-        self.name = func.__name__
+        self.name = func_name(func)
 
     def __str__(self):
         return '%s (%s seconds)' % (self.name, self.interval)
@@ -164,7 +170,7 @@
                 self.running_threads.remove(self)
         Thread.__init__(self, target=auto_remove_func)
         self.running_threads = running_threads
-        self._name = target.__name__
+        self._name = func_name(target)
 
     def start(self):
         self.running_threads.append(self)
--- a/setup.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/setup.py	Wed Nov 03 16:38:28 2010 +0100
@@ -36,6 +36,7 @@
     from distutils.core import setup
     from distutils.command import install_lib
     USE_SETUPTOOLS = False
+from distutils.command import install_data
 
 # import required features
 from __pkginfo__ import modname, version, license, description, web, \
@@ -47,7 +48,7 @@
 import __pkginfo__
 if USE_SETUPTOOLS:
     requires = {}
-    for entry in ("__depends__", "__recommends__"):
+    for entry in ("__depends__",): # "__recommends__"):
         requires.update(getattr(__pkginfo__, entry, {}))
     install_requires = [("%s %s" % (d, v and v or "")).strip()
                        for d, v in requires.iteritems()]
@@ -60,7 +61,7 @@
 data_files = getattr(__pkginfo__, 'data_files', None)
 subpackage_of = getattr(__pkginfo__, 'subpackage_of', None)
 ext_modules = getattr(__pkginfo__, 'ext_modules', None)
-
+package_data = getattr(__pkginfo__, 'package_data', {})
 
 BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
 IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
@@ -163,6 +164,45 @@
                 dest = join(self.install_dir, base, directory)
                 export(directory, dest, verbose=False)
 
+# write required share/cubicweb/cubes/__init__.py
+class MyInstallData(install_data.install_data):
+    """A class That manages data files installation"""
+    def run(self):
+        """overridden from install_data class"""
+        install_data.install_data.run(self)
+        path = join(self.install_dir, 'share', 'cubicweb', 'cubes', '__init__.py')
+        ini = open(path, 'w')
+        ini.write('# Cubicweb cubes directory\n')
+        ini.close()
+
+# re-enable copying data files in sys.prefix
+if USE_SETUPTOOLS:
+    # overwrite MyInstallData to use sys.prefix instead of the egg directory
+    MyInstallMoreData = MyInstallData
+    class MyInstallData(MyInstallMoreData):
+        """A class that manages data files installation"""
+        def run(self):
+            _old_install_dir = self.install_dir
+            if self.install_dir.endswith('egg'):
+                self.install_dir = sys.prefix
+            MyInstallMoreData.run(self)
+            self.install_dir = _old_install_dir
+    try:
+        import setuptools.command.easy_install # only if easy_install avaible
+        # monkey patch: Crack SandboxViolation verification
+        from setuptools.sandbox import DirectorySandbox as DS
+        old_ok = DS._ok
+        def _ok(self, path):
+            """Return True if ``path`` can be written during installation."""
+            out = old_ok(self, path)
+            realpath = os.path.normcase(os.path.realpath(path))
+            if realpath.startswith(sys.prefix):
+                out = True
+            return out
+        DS._ok = _ok
+    except ImportError:
+        pass
+
 def install(**kwargs):
     """setup entry point"""
     if USE_SETUPTOOLS:
@@ -182,13 +222,16 @@
         packages = [modname] + get_packages(os.getcwd(), modname)
     if USE_SETUPTOOLS:
         kwargs['install_requires'] = install_requires
+        kwargs['zip_safe'] = False
     kwargs['packages'] = packages
+    kwargs['package_data'] = package_data
     return setup(name=distname, version=version, license=license, url=web,
                  description=description, long_description=long_description,
                  author=author, author_email=author_email,
                  scripts=ensure_scripts(scripts), data_files=data_files,
                  ext_modules=ext_modules,
-                 cmdclass={'install_lib': MyInstallLib},
+                 cmdclass={'install_lib': MyInstallLib,
+                           'install_data': MyInstallData},
                  **kwargs
                  )
 
--- a/skeleton/MANIFEST.in	Tue Jul 27 12:36:03 2010 +0200
+++ b/skeleton/MANIFEST.in	Wed Nov 03 16:38:28 2010 +0100
@@ -1,5 +1,5 @@
 include *.py
 include */*.py
-recursive-include data external_resources *.gif *.png *.css *.ico *.js
-recursive-include i18n *.pot *.po
+recursive-include data *.gif *.png *.ico *.css *.js
+recursive-include i18n *.po
 recursive-include wdoc *
--- a/skeleton/data/external_resources.tmpl	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-# -*- shell-script -*-
-###############################################################################
-#
-# put here information about external resources used by your components,
-# or to overides existing external resources configuration
-#
-###############################################################################
-
-# CSS stylesheets to include in HTML headers
-# uncomment the line below to use template specific stylesheet
-# STYLESHEETS = DATADIR/cubes.%(cubename)s.css
--- a/skeleton/setup.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/skeleton/setup.py	Wed Nov 03 16:38:28 2010 +0100
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# pylint: disable-msg=W0404,W0622,W0704,W0613,W0152
+# pylint: disable=W0404,W0622,W0704,W0613
 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 
--- a/skeleton/test/test_CUBENAME.py	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""template automatic tests
-
-"""
-
-from logilab.common.testlib import TestCase, unittest_main
-
-class DefaultTC(TestCase):
-    def test_something(self):
-        self.skip('this cube has no test')
-
-## uncomment the import if you want to activate automatic test for your
-## template
-
-# from cubicweb.devtools.testlib import AutomaticWebTest
-
-
-if __name__ == '__main__':
-    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/skeleton/test/test_CUBENAME.py.tmpl	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,37 @@
+# copyright %(year)s %(author)s, all rights reserved.
+# contact %(author-web-site)s -- mailto:%(author-email)s
+#
+%(long-license)s
+"""%(distname)s automatic tests
+
+
+uncomment code below if you want to activate automatic test for your cube:
+
+.. sourcecode:: python
+
+    from cubicweb.devtools.testlib import AutomaticWebTest
+
+    class AutomaticWebTest(AutomaticWebTest):
+        '''provides `to_test_etypes` and/or `list_startup_views` implementation
+        to limit test scope
+        '''
+
+        def to_test_etypes(self):
+            '''only test views for entities of the returned types'''
+            return set(('My', 'Cube', 'Entity', 'Types'))
+
+        def list_startup_views(self):
+            '''only test startup views of the returned identifiers'''
+            return ('some', 'startup', 'views')
+"""
+
+from cubicweb.devtools import testlib
+
+class DefaultTC(testlib.CubicWebTC):
+    def test_something(self):
+        self.skip('this cube has no test')
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/skeleton/uiprops.py.tmpl	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/sobjects/notification.py	Wed Nov 03 16:38:28 2010 +0100
@@ -46,7 +46,8 @@
         mode = self._cw.vreg.config['default-recipients-mode']
         if mode == 'users':
             execute = self._cw.execute
-            dests = [(u.get_email(), u.property_value('ui.language'))
+            dests = [(u.cw_adapt_to('IEmailable').get_email(),
+                      u.property_value('ui.language'))
                      for u in execute(self.user_rql, build_descr=True).entities()]
         elif mode == 'default-dest-addrs':
             lang = self._cw.vreg.property_value('ui.language')
--- a/sobjects/test/data/sobjects/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/sobjects/test/data/sobjects/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,11 +15,9 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
 
-"""
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.sobjects.notification import StatusChangeMixIn, NotificationView
 
 class UserStatusChangeView(StatusChangeMixIn, NotificationView):
-    __select__ = NotificationView.__select__ & implements('CWUser')
+    __select__ = NotificationView.__select__ & is_instance('CWUser')
--- a/sobjects/test/unittest_email.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/sobjects/test/unittest_email.py	Wed Nov 03 16:38:28 2010 +0100
@@ -26,23 +26,23 @@
 
     def test_use_email_set_primary_email(self):
         self.execute('INSERT EmailAddress X: X address "admin@logilab.fr", U use_email X WHERE U login "admin"')
-        self.assertEquals(self.execute('Any A WHERE U primary_email X, U login "admin", X address A').rows,
+        self.assertEqual(self.execute('Any A WHERE U primary_email X, U login "admin", X address A').rows,
                           [])
         self.commit()
-        self.assertEquals(self.execute('Any A WHERE U primary_email X, U login "admin", X address A')[0][0],
+        self.assertEqual(self.execute('Any A WHERE U primary_email X, U login "admin", X address A')[0][0],
                           'admin@logilab.fr')
         # having another email should'nt change anything
         self.execute('INSERT EmailAddress X: X address "a@logilab.fr", U use_email X WHERE U login "admin"')
         self.commit()
-        self.assertEquals(self.execute('Any A WHERE U primary_email X, U login "admin", X address A')[0][0],
+        self.assertEqual(self.execute('Any A WHERE U primary_email X, U login "admin", X address A')[0][0],
                           'admin@logilab.fr')
 
     def test_primary_email_set_use_email(self):
         self.execute('INSERT EmailAddress X: X address "admin@logilab.fr", U primary_email X WHERE U login "admin"')
-        self.assertEquals(self.execute('Any A WHERE U use_email X, U login "admin", X address A').rows,
+        self.assertEqual(self.execute('Any A WHERE U use_email X, U login "admin", X address A').rows,
                           [])
         self.commit()
-        self.assertEquals(self.execute('Any A WHERE U use_email X, U login "admin", X address A')[0][0],
+        self.assertEqual(self.execute('Any A WHERE U use_email X, U login "admin", X address A')[0][0],
                           'admin@logilab.fr')
 
     def test_cardinality_check(self):
--- a/sobjects/test/unittest_notification.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/sobjects/test/unittest_notification.py	Wed Nov 03 16:38:28 2010 +0100
@@ -72,12 +72,12 @@
         finder = self.vreg['components'].select('recipients_finder',
                                                 self.request(), rset=urset)
         self.set_option('default-recipients-mode', 'none')
-        self.assertEquals(finder.recipients(), [])
+        self.assertEqual(finder.recipients(), [])
         self.set_option('default-recipients-mode', 'users')
-        self.assertEquals(finder.recipients(), [(u'admin@logilab.fr', 'fr')])
+        self.assertEqual(finder.recipients(), [(u'admin@logilab.fr', 'fr')])
         self.set_option('default-recipients-mode', 'default-dest-addrs')
         self.set_option('default-dest-addrs', 'abcd@logilab.fr, efgh@logilab.fr')
-        self.assertEquals(finder.recipients(), [('abcd@logilab.fr', 'en'), ('efgh@logilab.fr', 'en')])
+        self.assertEqual(finder.recipients(), [('abcd@logilab.fr', 'en'), ('efgh@logilab.fr', 'en')])
 
 
 class StatusChangeViewsTC(CubicWebTC):
@@ -85,12 +85,12 @@
     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)
+        self.assertEqual(len(MAILBOX), 1)
         email = MAILBOX[0]
-        self.assertEquals(email.content,
+        self.assertEqual(email.content,
                           '''
 admin changed status from <activated> to <deactivated> for entity
 'toto'
@@ -99,7 +99,7 @@
 
 url: http://testing.fr/cubicweb/cwuser/toto
 ''')
-        self.assertEquals(email.subject, 'status changed cwuser #%s (admin)' % u.eid)
+        self.assertEqual(email.subject, 'status changed cwuser #%s (admin)' % u.eid)
 
 if __name__ == '__main__':
     unittest_main()
--- a/sobjects/test/unittest_supervising.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/sobjects/test/unittest_supervising.py	Wed Nov 03 16:38:28 2010 +0100
@@ -52,16 +52,16 @@
         session = self.session
         sentops = [op for op in session.pending_operations
                    if isinstance(op, SupervisionMailOp)]
-        self.assertEquals(len(sentops), 1)
+        self.assertEqual(len(sentops), 1)
         # check view content
         op = sentops[0]
         view = sentops[0]._get_view()
-        self.assertEquals(view.recipients(), ['test@logilab.fr'])
-        self.assertEquals(view.subject(), '[data supervision] changes summary')
+        self.assertEqual(view.recipients(), ['test@logilab.fr'])
+        self.assertEqual(view.subject(), '[data supervision] changes summary')
         data = view.render(changes=session.transaction_data.get('pendingchanges')).strip()
         data = re.sub('#\d+', '#EID', data)
         data = re.sub('/\d+', '/EID', data)
-        self.assertTextEquals('''user admin has made the following change(s):
+        self.assertMultiLineEqual('''user admin has made the following change(s):
 
 * added cwuser #EID (toto)
   http://testing.fr/cubicweb/cwuser/toto
@@ -79,22 +79,22 @@
                               data)
         # check prepared email
         op._prepare_email()
-        self.assertEquals(len(op.to_send), 1)
+        self.assertEqual(len(op.to_send), 1)
         self.assert_(op.to_send[0][0])
-        self.assertEquals(op.to_send[0][1], ['test@logilab.fr'])
+        self.assertEqual(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)
+        self.assertEqual(len(sentops), 1)
         # check view content
         op = sentops[0]
         view = sentops[0]._get_view()
         data = view.render(changes=session.transaction_data.get('pendingchanges')).strip()
         data = re.sub('#\d+', '#EID', data)
         data = re.sub('/\d+', '/EID', data)
-        self.assertTextEquals('''user admin has made the following change(s):
+        self.assertMultiLineEqual('''user admin has made the following change(s):
 
 * changed state of cwuser #EID (toto)
   from state activated to state deactivated
--- a/sobjects/textparsers.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/sobjects/textparsers.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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:
--- a/tags.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/tags.py	Wed Nov 03 16:38:28 2010 +0100
@@ -48,6 +48,7 @@
 tr = tag('tr')
 th = tag('th')
 td = tag('td')
+iframe = tag('iframe')
 
 def select(name, id=None, multiple=False, options=[], **attrs):
     if multiple:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/comment/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,17 @@
+# 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/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/comment/__pkginfo__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,25 @@
+# pylint: disable-msg=W0622
+# 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-comment packaging information"""
+
+distname = "cubicweb-comment"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/email/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,17 @@
+# 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/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/email/__pkginfo__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,30 @@
+# pylint: disable-msg=W0622
+# 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-email packaging information"""
+
+distname = "cubicweb-email"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
+
+
+__depends__ = {'cubicweb': None,
+               'cubicweb-file': None}
+__recommends__ = {'cubicweb-comment': None}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/email/entities.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/email/hooks.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/email/views/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1 @@
+"test"
--- a/test/data/cubes/file/__pkginfo__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/data/cubes/file/__pkginfo__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""cubicweb-file packaging information
-
-"""
+"""cubicweb-file packaging information"""
 
 distname = "cubicweb-file"
 modname = distname.split('-', 1)[1]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/file/entities/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/file/hooks/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/file/views.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/forge/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,17 @@
+# 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/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/cubes/forge/__pkginfo__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,32 @@
+# pylint: disable-msg=W0622
+# 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-forge packaging information"""
+
+distname = "cubicweb-forge"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
+
+
+__depends__ = {'cubicweb': None,
+               'cubicweb-file': None,
+               'cubicweb-email': None,
+               'cubicweb-comment': None,
+               }
--- a/test/data/rewrite/bootstrap_cubes	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/data/rewrite/bootstrap_cubes	Wed Nov 03 16:38:28 2010 +0100
@@ -1,1 +1,1 @@
-card, person
+card
--- a/test/data/rewrite/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/data/rewrite/schema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -49,7 +49,7 @@
 
 
 class require_permission(RelationDefinition):
-    subject = ('Card', 'Note', 'Person')
+    subject = ('Card', 'Note')
     object = 'CWPermission'
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/scripts/script1.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,3 @@
+assert 'data/scripts/script3.py' == __file__
+assert '__main__' == __name__
+assert ['-vd', '-f', 'FILE.TXT'] == __args__, __args__
--- a/test/unittest_cwconfig.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_cwconfig.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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.cwconfig unit tests"""
 
-"""
 import sys
 import os
 import tempfile
@@ -51,34 +50,38 @@
         ApptestConfiguration.CUBES_PATH = []
 
     def test_reorder_cubes(self):
-        # jpl depends on email and file and comment
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
+        self.config.adjust_sys_path()
+        # forge depends on email and file and comment
         # email depends on file
-        self.assertEquals(self.config.reorder_cubes(['file', 'email', 'forge']),
+        self.assertEqual(self.config.reorder_cubes(['file', 'email', 'forge']),
                           ('forge', 'email', 'file'))
-        self.assertEquals(self.config.reorder_cubes(['email', 'file', 'forge']),
+        self.assertEqual(self.config.reorder_cubes(['email', 'file', 'forge']),
                           ('forge', 'email', 'file'))
-        self.assertEquals(self.config.reorder_cubes(['email', 'forge', 'file']),
+        self.assertEqual(self.config.reorder_cubes(['email', 'forge', 'file']),
                           ('forge', 'email', 'file'))
-        self.assertEquals(self.config.reorder_cubes(['file', 'forge', 'email']),
+        self.assertEqual(self.config.reorder_cubes(['file', 'forge', 'email']),
                           ('forge', 'email', 'file'))
-        self.assertEquals(self.config.reorder_cubes(['forge', 'file', 'email']),
+        self.assertEqual(self.config.reorder_cubes(['forge', 'file', 'email']),
                           ('forge', 'email', 'file'))
-        self.assertEquals(self.config.reorder_cubes(('forge', 'email', 'file')),
+        self.assertEqual(self.config.reorder_cubes(('forge', 'email', 'file')),
                           ('forge', 'email', 'file'))
 
     def test_reorder_cubes_recommends(self):
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
+        self.config.adjust_sys_path()
         from cubes.comment import __pkginfo__ as comment_pkginfo
         comment_pkginfo.__recommends_cubes__ = {'file': None}
         try:
             # email recommends comment
             # comment recommends file
-            self.assertEquals(self.config.reorder_cubes(('forge', 'email', 'file', 'comment')),
+            self.assertEqual(self.config.reorder_cubes(('forge', 'email', 'file', 'comment')),
                               ('forge', 'email', 'comment', 'file'))
-            self.assertEquals(self.config.reorder_cubes(('forge', 'email', 'comment', 'file')),
+            self.assertEqual(self.config.reorder_cubes(('forge', 'email', 'comment', 'file')),
                               ('forge', 'email', 'comment', 'file'))
-            self.assertEquals(self.config.reorder_cubes(('forge', 'comment', 'email', 'file')),
+            self.assertEqual(self.config.reorder_cubes(('forge', 'comment', 'email', 'file')),
                               ('forge', 'email', 'comment', 'file'))
-            self.assertEquals(self.config.reorder_cubes(('comment', 'forge', 'email', 'file')),
+            self.assertEqual(self.config.reorder_cubes(('comment', 'forge', 'email', 'file')),
                               ('forge', 'email', 'comment', 'file'))
         finally:
             comment_pkginfo.__recommends_cubes__ = {}
@@ -87,48 +90,53 @@
 #     def test_vc_config(self):
 #         vcconf = self.config.vc_config()
 #         self.assertIsInstance(vcconf['EEMAIL'], Version)
-#         self.assertEquals(vcconf['EEMAIL'], (0, 3, 1))
-#         self.assertEquals(vcconf['CW'], (2, 31, 2))
+#         self.assertEqual(vcconf['EEMAIL'], (0, 3, 1))
+#         self.assertEqual(vcconf['CW'], (2, 31, 2))
 #         self.assertRaises(KeyError, vcconf.__getitem__, 'CW_VERSION')
 #         self.assertRaises(KeyError, vcconf.__getitem__, 'CRM')
 
     def test_expand_cubes(self):
-        self.assertEquals(self.config.expand_cubes(('email', 'blog')),
-                          ['email', 'blog', 'file'])
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
+        self.config.adjust_sys_path()
+        self.assertEqual(self.config.expand_cubes(('email', 'comment')),
+                          ['email', 'comment', 'file'])
 
     def test_vregistry_path(self):
-        self.assertEquals([unabsolutize(p) for p in self.config.vregistry_path()],
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
+        self.config.adjust_sys_path()
+        self.assertEqual([unabsolutize(p) for p in self.config.vregistry_path()],
                           ['entities', 'web/views', 'sobjects', 'hooks',
-                           'file/entities.py', 'file/views', 'file/hooks.py',
+                           'file/entities', 'file/views.py', 'file/hooks',
                            'email/entities.py', 'email/views', 'email/hooks.py',
                            'test/data/entities.py', 'test/data/views.py'])
 
     def test_cubes_path(self):
         # make sure we don't import the email cube, but the stdlib email package
         import email
-        self.assertNotEquals(dirname(email.__file__), self.config.CUBES_DIR)
+        self.assertNotEqual(dirname(email.__file__), self.config.CUBES_DIR)
         self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
-        self.assertEquals(self.config.cubes_search_path(),
+        self.assertEqual(self.config.cubes_search_path(),
                           [CUSTOM_CUBES_DIR, self.config.CUBES_DIR])
         self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR,
                                             self.config.CUBES_DIR, 'unexistant']
         # filter out unexistant and duplicates
-        self.assertEquals(self.config.cubes_search_path(),
+        self.assertEqual(self.config.cubes_search_path(),
                           [CUSTOM_CUBES_DIR,
                            self.config.CUBES_DIR])
         self.failUnless('mycube' in self.config.available_cubes())
         # test cubes python path
         self.config.adjust_sys_path()
         import cubes
-        self.assertEquals(cubes.__path__, self.config.cubes_search_path())
+        self.assertEqual(cubes.__path__, self.config.cubes_search_path())
         # this import should succeed once path is adjusted
         from cubes import mycube
-        self.assertEquals(mycube.__path__, [join(CUSTOM_CUBES_DIR, 'mycube')])
+        self.assertEqual(mycube.__path__, [join(CUSTOM_CUBES_DIR, 'mycube')])
         # file cube should be overriden by the one found in data/cubes
         sys.modules.pop('cubes.file', None)
         del cubes.file
         from cubes import file
-        self.assertEquals(file.__path__, [join(CUSTOM_CUBES_DIR, 'file')])
+        self.assertEqual(file.__path__, [join(CUSTOM_CUBES_DIR, 'file')])
+
 
 class FindPrefixTC(TestCase):
     def make_dirs(self, *args):
@@ -149,35 +157,35 @@
     def test_samedir(self):
         prefix = tempfile.tempdir
         self.make_dirs('share', 'cubicweb')
-        self.assertEquals(_find_prefix(prefix), prefix)
+        self.assertEqual(_find_prefix(prefix), prefix)
 
     @with_tempdir
     def test_samedir_filepath(self):
         prefix = tempfile.tempdir
         self.make_dirs('share', 'cubicweb')
         file_path = self.make_file('bob.py')
-        self.assertEquals(_find_prefix(file_path), prefix)
+        self.assertEqual(_find_prefix(file_path), prefix)
 
     @with_tempdir
     def test_dir_inside_prefix(self):
         prefix = tempfile.tempdir
         self.make_dirs('share', 'cubicweb')
         dir_path = self.make_dirs('bob')
-        self.assertEquals(_find_prefix(dir_path), prefix)
+        self.assertEqual(_find_prefix(dir_path), prefix)
 
     @with_tempdir
     def test_file_in_dir_inside_prefix(self):
         prefix = tempfile.tempdir
         self.make_dirs('share', 'cubicweb')
         file_path = self.make_file('bob', 'toto.py')
-        self.assertEquals(_find_prefix(file_path), prefix)
+        self.assertEqual(_find_prefix(file_path), prefix)
 
     @with_tempdir
     def test_file_in_deeper_dir_inside_prefix(self):
         prefix = tempfile.tempdir
         self.make_dirs('share', 'cubicweb')
         file_path = self.make_file('bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
-        self.assertEquals(_find_prefix(file_path), prefix)
+        self.assertEqual(_find_prefix(file_path), prefix)
 
     @with_tempdir
     def test_multiple_candidate_prefix(self):
@@ -185,7 +193,7 @@
         prefix = self.make_dirs('bob')
         self.make_dirs('bob', 'share', 'cubicweb')
         file_path = self.make_file('bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
-        self.assertEquals(_find_prefix(file_path), prefix)
+        self.assertEqual(_find_prefix(file_path), prefix)
 
     @with_tempdir
     def test_sister_candidate_prefix(self):
@@ -193,7 +201,7 @@
         self.make_dirs('share', 'cubicweb')
         self.make_dirs('bob', 'share', 'cubicweb')
         file_path = self.make_file('bell', 'toto.py')
-        self.assertEquals(_find_prefix(file_path), prefix)
+        self.assertEqual(_find_prefix(file_path), prefix)
 
     @with_tempdir
     def test_multiple_parent_candidate_prefix(self):
@@ -201,7 +209,7 @@
         prefix = self.make_dirs('share', 'cubicweb', 'bob')
         self.make_dirs('share', 'cubicweb', 'bob', 'share', 'cubicweb')
         file_path = self.make_file('share', 'cubicweb', 'bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
-        self.assertEquals(_find_prefix(file_path), prefix)
+        self.assertEqual(_find_prefix(file_path), prefix)
 
     @with_tempdir
     def test_upper_candidate_prefix(self):
@@ -209,12 +217,12 @@
         self.make_dirs('share', 'cubicweb')
         self.make_dirs('bell','bob',  'share', 'cubicweb')
         file_path = self.make_file('bell', 'toto.py')
-        self.assertEquals(_find_prefix(file_path), prefix)
+        self.assertEqual(_find_prefix(file_path), prefix)
 
     @with_tempdir
     def test_no_prefix(self):
         prefix = tempfile.tempdir
-        self.assertEquals(_find_prefix(prefix), sys.prefix)
+        self.assertEqual(_find_prefix(prefix), sys.prefix)
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_cwctl.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_cwctl.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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()
@@ -35,7 +39,27 @@
 
     def test_list(self):
         from cubicweb.cwctl import ListCommand
-        ListCommand().run([])
+        ListCommand(None).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_dbapi.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_dbapi.py	Wed Nov 03 16:38:28 2010 +0100
@@ -29,40 +29,40 @@
 
     def test_public_repo_api(self):
         cnx = self.login('anon')
-        self.assertEquals(cnx.get_schema(), self.repo.schema)
-        self.assertEquals(cnx.source_defs(), {'system': {'adapter': 'native', 'uri': 'system'}})
+        self.assertEqual(cnx.get_schema(), self.repo.schema)
+        self.assertEqual(cnx.source_defs(), {'system': {'adapter': 'native', 'uri': 'system'}})
         self.restore_connection() # proper way to close cnx
         self.assertRaises(ProgrammingError, cnx.get_schema)
         self.assertRaises(ProgrammingError, cnx.source_defs)
 
     def test_db_api(self):
         cnx = self.login('anon')
-        self.assertEquals(cnx.rollback(), None)
-        self.assertEquals(cnx.commit(), None)
+        self.assertEqual(cnx.rollback(), None)
+        self.assertEqual(cnx.commit(), None)
         self.restore_connection() # proper way to close cnx
-        #self.assertEquals(cnx.close(), None)
+        #self.assertEqual(cnx.close(), None)
         self.assertRaises(ProgrammingError, cnx.rollback)
         self.assertRaises(ProgrammingError, cnx.commit)
         self.assertRaises(ProgrammingError, cnx.close)
 
     def test_api(self):
         cnx = self.login('anon')
-        self.assertEquals(cnx.user(None).login, 'anon')
-        self.assertEquals(cnx.describe(1), (u'CWGroup', u'system', None))
+        self.assertEqual(cnx.user(None).login, 'anon')
+        self.assertEqual(cnx.describe(1), (u'CWGroup', u'system', None))
         self.restore_connection() # proper way to close cnx
         self.assertRaises(ProgrammingError, cnx.user, None)
         self.assertRaises(ProgrammingError, cnx.describe, 1)
 
     def test_shared_data_api(self):
         cnx = self.login('anon')
-        self.assertEquals(cnx.get_shared_data('data'), None)
+        self.assertEqual(cnx.get_shared_data('data'), None)
         cnx.set_shared_data('data', 4)
-        self.assertEquals(cnx.get_shared_data('data'), 4)
+        self.assertEqual(cnx.get_shared_data('data'), 4)
         cnx.get_shared_data('data', pop=True)
         cnx.get_shared_data('whatever', pop=True)
-        self.assertEquals(cnx.get_shared_data('data'), None)
+        self.assertEqual(cnx.get_shared_data('data'), None)
         cnx.set_shared_data('data', 4)
-        self.assertEquals(cnx.get_shared_data('data'), 4)
+        self.assertEqual(cnx.get_shared_data('data'), 4)
         self.restore_connection() # proper way to close cnx
         self.assertRaises(ProgrammingError, cnx.check)
         self.assertRaises(ProgrammingError, cnx.set_shared_data, 'data', 0)
--- a/test/unittest_entity.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_entity.py	Wed Nov 03 16:38:28 2010 +0100
@@ -42,14 +42,14 @@
 
     def test_has_eid(self):
         e = self.vreg['etypes'].etype_class('CWUser')(self.request())
-        self.assertEquals(e.eid, None)
-        self.assertEquals(e.has_eid(), False)
+        self.assertEqual(e.eid, None)
+        self.assertEqual(e.has_eid(), False)
         e.eid = 'X'
-        self.assertEquals(e.has_eid(), False)
+        self.assertEqual(e.has_eid(), False)
         e.eid = 0
-        self.assertEquals(e.has_eid(), True)
+        self.assertEqual(e.has_eid(), True)
         e.eid = 2
-        self.assertEquals(e.has_eid(), True)
+        self.assertEqual(e.has_eid(), True)
 
     def test_copy(self):
         req = self.request()
@@ -61,11 +61,11 @@
         self.execute('SET TAG tags X WHERE X eid %(x)s', {'x': oe.eid})
         e = req.create_entity('Note', type=u'z')
         e.copy_relations(oe.eid)
-        self.assertEquals(len(e.ecrit_par), 1)
-        self.assertEquals(e.ecrit_par[0].eid, p.eid)
-        self.assertEquals(len(e.reverse_tags), 1)
+        self.assertEqual(len(e.ecrit_par), 1)
+        self.assertEqual(e.ecrit_par[0].eid, p.eid)
+        self.assertEqual(len(e.reverse_tags), 1)
         # check meta-relations are not copied, set on commit
-        self.assertEquals(len(e.created_by), 0)
+        self.assertEqual(len(e.created_by), 0)
 
     def test_copy_with_nonmeta_composite_inlined(self):
         req = self.request()
@@ -83,8 +83,8 @@
         user = self.user()
         adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
-        self.assertEquals(e.use_email[0].address, "toto@logilab.org")
-        self.assertEquals(e.use_email[0].eid, adeleid)
+        self.assertEqual(e.use_email[0].address, "toto@logilab.org")
+        self.assertEqual(e.use_email[0].eid, adeleid)
         usereid = self.execute('INSERT CWUser X: X login "toto", X upassword "toto", X in_group G '
                                'WHERE G name "users"')[0][0]
         e = self.execute('Any X WHERE X eid %(x)s', {'x': usereid}).get_entity(0, 0)
@@ -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.assertEqual(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.assertEqual(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.assertEqual(sorted(user._cw_related_cache), ['primary_email_subject'])
+        self.assertEqual(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.assertEqual(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()
@@ -125,8 +125,8 @@
         for tag in u'abcd':
             req.create_entity('Tag', name=tag)
         self.execute('SET X tags Y WHERE X is Tag, Y is Personne')
-        self.assertEquals(len(p.related('tags', 'object', limit=2)), 2)
-        self.assertEquals(len(p.related('tags', 'object')), 4)
+        self.assertEqual(len(p.related('tags', 'object', limit=2)), 2)
+        self.assertEqual(len(p.related('tags', 'object')), 4)
 
 
     def test_fetch_rql(self):
@@ -141,7 +141,7 @@
         peschema.subjrels['evaluee'].rdef(peschema, Note.e_schema).cardinality = '1*'
         seschema.subjrels['evaluee'].rdef(seschema, Note.e_schema).cardinality = '1*'
         # testing basic fetch_attrs attribute
-        self.assertEquals(Personne.fetch_rql(user),
+        self.assertEqual(Personne.fetch_rql(user),
                           'Any X,AA,AB,AC ORDERBY AA ASC '
                           'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC')
         pfetch_attrs = Personne.fetch_attrs
@@ -149,39 +149,39 @@
         try:
             # testing unknown attributes
             Personne.fetch_attrs = ('bloug', 'beep')
-            self.assertEquals(Personne.fetch_rql(user), 'Any X WHERE X is Personne')
+            self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is Personne')
             # testing one non final relation
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
-            self.assertEquals(Personne.fetch_rql(user),
+            self.assertEqual(Personne.fetch_rql(user),
                               'Any X,AA,AB,AC,AD ORDERBY AA ASC '
                               'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD')
             # testing two non final relations
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee')
-            self.assertEquals(Personne.fetch_rql(user),
+            self.assertEqual(Personne.fetch_rql(user),
                               'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC '
                               'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
                               'X evaluee AE?, AE modification_date AF')
             # testing one non final relation with recursion
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
             Societe.fetch_attrs = ('nom', 'evaluee')
-            self.assertEquals(Personne.fetch_rql(user),
+            self.assertEqual(Personne.fetch_rql(user),
                               'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC '
                               'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
                               'AC evaluee AE?, AE modification_date AF'
                               )
             # testing symmetric relation
             Personne.fetch_attrs = ('nom', 'connait')
-            self.assertEquals(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC '
+            self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC '
                               'WHERE X is Personne, X nom AA, X connait AB?')
             # testing optional relation
             peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '?*'
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
             Societe.fetch_attrs = ('nom',)
-            self.assertEquals(Personne.fetch_rql(user),
+            self.assertEqual(Personne.fetch_rql(user),
                               'Any X,AA,AB,AC,AD ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD')
             # testing relation with cardinality > 1
             peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '**'
-            self.assertEquals(Personne.fetch_rql(user),
+            self.assertEqual(Personne.fetch_rql(user),
                               'Any X,AA,AB ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB')
             # XXX test unauthorized attribute
         finally:
@@ -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.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(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,48 +219,48 @@
         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.assertEqual(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]
-        self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
+        rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+        self.assertEqual(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]
-        self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
+        rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+        self.assertEqual(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]
-        self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ASC '
+        rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
+        self.assertEqual(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]
-        #self.assertEquals(rql, '')
+        #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
+        #self.assertEqual(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]
-        self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+        rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
+        self.assertEqual(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]
-        #self.assertEquals(rql, '')
+        #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
+        #self.assertEqual(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]
-        self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+        rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
+        self.assertEqual(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)')
 
@@ -282,71 +282,71 @@
         e = req.create_entity('Tag', name=u'x')
         req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien')
         req.create_entity('Personne', nom=u'thenault', prenom=u'sylvain')
-        self.assertEquals(len(e.unrelated('tags', 'Personne', 'subject', limit=1)),
+        self.assertEqual(len(e.unrelated('tags', 'Personne', 'subject', limit=1)),
                           1)
 
     def test_unrelated_security(self):
         email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
         rset = email.unrelated('use_email', 'CWUser', 'object')
-        self.assertEquals([x.login for x in rset.entities()], [u'admin', u'anon'])
+        self.assertEqual([x.login for x in rset.entities()], [u'admin', u'anon'])
         user = self.request().user
         rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEquals([x.address for x in rset.entities()], [u'hop'])
+        self.assertEqual([x.address for x in rset.entities()], [u'hop'])
         self.create_user('toto')
         self.login('toto')
         email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
         rset = email.unrelated('use_email', 'CWUser', 'object')
-        self.assertEquals([x.login for x in rset.entities()], ['toto'])
+        self.assertEqual([x.login for x in rset.entities()], ['toto'])
         user = self.request().user
         rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEquals([x.address for x in rset.entities()], ['hop'])
+        self.assertEqual([x.address for x in rset.entities()], ['hop'])
         user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
         rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEquals([x.address for x in rset.entities()], [])
+        self.assertEqual([x.address for x in rset.entities()], [])
         self.login('anon')
         email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
         rset = email.unrelated('use_email', 'CWUser', 'object')
-        self.assertEquals([x.login for x in rset.entities()], [])
+        self.assertEqual([x.login for x in rset.entities()], [])
         user = self.request().user
         rset = user.unrelated('use_email', 'EmailAddress', 'subject')
-        self.assertEquals([x.address for x in rset.entities()], [])
+        self.assertEqual([x.address for x in rset.entities()], [])
 
     def test_unrelated_new_entity(self):
         e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         unrelated = [r[0] for r in e.unrelated('in_group', 'CWGroup', 'subject')]
         # should be default groups but owners, i.e. managers, users, guests
-        self.assertEquals(len(unrelated), 3)
+        self.assertEqual(len(unrelated), 3)
 
     def test_printable_value_string(self):
         e = self.request().create_entity('Card', title=u'rest test', content=u'du :eid:`1:*ReST*`',
                             content_format=u'text/rest')
-        self.assertEquals(e.printable_value('content'),
+        self.assertEqual(e.printable_value('content'),
                           '<p>du <a class="reference" href="http://testing.fr/cubicweb/cwgroup/guests">*ReST*</a></p>\n')
         e['content'] = 'du <em>html</em> <ref rql="CWUser X">users</ref>'
         e['content_format'] = 'text/html'
-        self.assertEquals(e.printable_value('content'),
+        self.assertEqual(e.printable_value('content'),
                           'du <em>html</em> <a href="http://testing.fr/cubicweb/view?rql=CWUser%20X">users</a>')
         e['content'] = 'du *texte*'
         e['content_format'] = 'text/plain'
-        self.assertEquals(e.printable_value('content'),
-                          '<p>\ndu *texte*\n</p>')
+        self.assertEqual(e.printable_value('content'),
+                          '<p>\ndu *texte*<br/>\n</p>')
         e['title'] = 'zou'
         e['content'] = '''\
 a title
 =======
 du :eid:`1:*ReST*`'''
         e['content_format'] = 'text/rest'
-        self.assertEquals(e.printable_value('content', format='text/plain'),
+        self.assertEqual(e.printable_value('content', format='text/plain'),
                           e['content'])
 
         e['content'] = u'<b>yo (zou éà ;)</b>'
         e['content_format'] = 'text/html'
-        self.assertEquals(e.printable_value('content', format='text/plain').strip(),
+        self.assertEqual(e.printable_value('content', format='text/plain').strip(),
                           u'**yo (zou éà ;)**')
         if HAS_TAL:
             e['content'] = '<h1 tal:content="self/title">titre</h1>'
             e['content_format'] = 'text/cubicweb-page-template'
-            self.assertEquals(e.printable_value('content'),
+            self.assertEqual(e.printable_value('content'),
                               '<h1>zou</h1>')
 
 
@@ -358,17 +358,17 @@
         if mttransforms.HAS_PYGMENTS_TRANSFORMS:
             import pygments
             if tuple(int(i) for i in pygments.__version__.split('.')[:2]) >= (1, 3):
-                self.assertEquals(e.printable_value('data'),
+                self.assertEqual(e.printable_value('data'),
                                   '''<div class="highlight"><pre><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="mi">1</span>
 </pre></div>
 ''')
             else:
-                self.assertEquals(e.printable_value('data'),
+                self.assertEqual(e.printable_value('data'),
                                   '''<div class="highlight"><pre><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="mf">1</span>
 </pre></div>
 ''')
         else:
-            self.assertEquals(e.printable_value('data'),
+            self.assertEqual(e.printable_value('data'),
                               '''<pre class="python">
 <span style="color: #C00000;">lambda</span> <span style="color: #000000;">x</span><span style="color: #0000C0;">:</span> <span style="color: #0080C0;">1</span>
 </pre>
@@ -376,7 +376,7 @@
 
         e = req.create_entity('File', data=Binary('*héhéhé*'), data_format=u'text/rest',
                             data_encoding=u'utf-8', data_name=u'toto.txt')
-        self.assertEquals(e.printable_value('data'),
+        self.assertEqual(e.printable_value('data'),
                           u'<p><em>héhéhé</em></p>\n')
 
     def test_printable_value_bad_html(self):
@@ -385,42 +385,42 @@
         e = req.create_entity('Card', title=u'bad html', content=u'<div>R&D<br>',
                             content_format=u'text/html')
         tidy = lambda x: x.replace('\n', '')
-        self.assertEquals(tidy(e.printable_value('content')),
+        self.assertEqual(tidy(e.printable_value('content')),
                           '<div>R&amp;D<br/></div>')
         e['content'] = u'yo !! R&D <div> pas fermé'
-        self.assertEquals(tidy(e.printable_value('content')),
+        self.assertEqual(tidy(e.printable_value('content')),
                           u'yo !! R&amp;D <div> pas fermé</div>')
         e['content'] = u'R&D'
-        self.assertEquals(tidy(e.printable_value('content')), u'R&amp;D')
+        self.assertEqual(tidy(e.printable_value('content')), u'R&amp;D')
         e['content'] = u'R&D;'
-        self.assertEquals(tidy(e.printable_value('content')), u'R&amp;D;')
+        self.assertEqual(tidy(e.printable_value('content')), u'R&amp;D;')
         e['content'] = u'yo !! R&amp;D <div> pas fermé'
-        self.assertEquals(tidy(e.printable_value('content')),
+        self.assertEqual(tidy(e.printable_value('content')),
                           u'yo !! R&amp;D <div> pas fermé</div>')
         e['content'] = u'été <div> été'
-        self.assertEquals(tidy(e.printable_value('content')),
+        self.assertEqual(tidy(e.printable_value('content')),
                           u'été <div> été</div>')
         e['content'] = u'C&apos;est un exemple s&eacute;rieux'
-        self.assertEquals(tidy(e.printable_value('content')),
+        self.assertEqual(tidy(e.printable_value('content')),
                           u"C'est un exemple sérieux")
         # make sure valid xhtml is left untouched
         e['content'] = u'<div>R&amp;D<br/></div>'
-        self.assertEquals(e.printable_value('content'), e['content'])
+        self.assertEqual(e.printable_value('content'), e['content'])
         e['content'] = u'<div>été</div>'
-        self.assertEquals(e.printable_value('content'), e['content'])
+        self.assertEqual(e.printable_value('content'), e['content'])
         e['content'] = u'été'
-        self.assertEquals(e.printable_value('content'), e['content'])
+        self.assertEqual(e.printable_value('content'), e['content'])
         e['content'] = u'hop\r\nhop\nhip\rmomo'
-        self.assertEquals(e.printable_value('content'), u'hop\nhop\nhip\nmomo')
+        self.assertEqual(e.printable_value('content'), u'hop\nhop\nhip\nmomo')
 
     def test_printable_value_bad_html_ms(self):
-        self.skip('fix soup2xhtml to handle this test')
+        self.skipTest('fix soup2xhtml to handle this test')
         req = self.request()
         e = req.create_entity('Card', title=u'bad html', content=u'<div>R&D<br>',
                             content_format=u'text/html')
         tidy = lambda x: x.replace('\n', '')
         e['content'] = u'<div x:foo="bar">ms orifice produces weird html</div>'
-        self.assertEquals(tidy(e.printable_value('content')),
+        self.assertEqual(tidy(e.printable_value('content')),
                           u'<div>ms orifice produces weird html</div>')
         import tidy as tidymod # apt-get install python-tidy
         tidy = lambda x: str(tidymod.parseString(x.encode('utf-8'),
@@ -429,7 +429,7 @@
                                                     'show_body_only' : True,
                                                     'quote-nbsp' : False,
                                                     'char_encoding' : 'utf8'})).decode('utf-8').strip()
-        self.assertEquals(tidy(e.printable_value('content')),
+        self.assertEqual(tidy(e.printable_value('content')),
                           u'<div>ms orifice produces weird html</div>')
 
 
@@ -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.assertEqual(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):
@@ -451,7 +451,7 @@
         p1 = req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien')
         p2 = req.create_entity('Personne', nom=u'toto')
         self.execute('SET X evaluee Y WHERE X nom "di mascio", Y nom "toto"')
-        self.assertEquals(p1.evaluee[0].nom, "toto")
+        self.assertEqual(p1.evaluee[0].nom, "toto")
         self.failUnless(not p1.reverse_evaluee)
 
     def test_complete_relation(self):
@@ -462,10 +462,10 @@
         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.assertEquals(trinfo.by_transition, ())
+        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.assertEqual(trinfo.by_transition, ())
 
     def test_request_cache(self):
         req = self.request()
@@ -477,49 +477,55 @@
     def test_rest_path(self):
         req = self.request()
         note = req.create_entity('Note', type=u'z')
-        self.assertEquals(note.rest_path(), 'note/%s' % note.eid)
+        self.assertEqual(note.rest_path(), 'note/%s' % note.eid)
         # unique attr
         tag = req.create_entity('Tag', name=u'x')
-        self.assertEquals(tag.rest_path(), 'tag/x')
+        self.assertEqual(tag.rest_path(), 'tag/x')
         # test explicit rest_attr
         person = req.create_entity('Personne', prenom=u'john', nom=u'doe')
-        self.assertEquals(person.rest_path(), 'personne/doe')
+        self.assertEqual(person.rest_path(), 'personne/doe')
         # ambiguity test
         person2 = req.create_entity('Personne', prenom=u'remi', nom=u'doe')
         person.clear_all_caches()
-        self.assertEquals(person.rest_path(), 'personne/eid/%s' % person.eid)
-        self.assertEquals(person2.rest_path(), 'personne/eid/%s' % person2.eid)
+        self.assertEqual(person.rest_path(), 'personne/eid/%s' % person.eid)
+        self.assertEqual(person2.rest_path(), 'personne/eid/%s' % person2.eid)
         # unique attr with None value (wikiid in this case)
         card1 = req.create_entity('Card', title=u'hop')
-        self.assertEquals(card1.rest_path(), 'card/eid/%s' % card1.eid)
-        card2 = req.create_entity('Card', title=u'pod', wikiid=u'zob/i')
-        self.assertEquals(card2.rest_path(), 'card/zob%2Fi')
+        self.assertEqual(card1.rest_path(), 'card/eid/%s' % card1.eid)
+        # don't use rest if we have /, ? or & in the path (breaks mod_proxy)
+        card2 = req.create_entity('Card', title=u'pod', wikiid=u'zo/bi')
+        self.assertEqual(card2.rest_path(), 'card/eid/%d' % card2.eid)
+        card3 = req.create_entity('Card', title=u'pod', wikiid=u'zo&bi')
+        self.assertEqual(card3.rest_path(), 'card/eid/%d' % card3.eid)
+        card4 = req.create_entity('Card', title=u'pod', wikiid=u'zo?bi')
+        self.assertEqual(card4.rest_path(), 'card/eid/%d' % card4.eid)
+        
 
     def test_set_attributes(self):
         req = self.request()
         person = req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien')
-        self.assertEquals(person.prenom, u'adrien')
-        self.assertEquals(person.nom, u'di mascio')
+        self.assertEqual(person.prenom, u'adrien')
+        self.assertEqual(person.nom, u'di mascio')
         person.set_attributes(prenom=u'sylvain', nom=u'thénault')
         person = self.execute('Personne P').get_entity(0, 0) # XXX retreival needed ?
-        self.assertEquals(person.prenom, u'sylvain')
-        self.assertEquals(person.nom, u'thénault')
+        self.assertEqual(person.prenom, u'sylvain')
+        self.assertEqual(person.nom, u'thénault')
 
     def test_metainformation_and_external_absolute_url(self):
         req = self.request()
         note = req.create_entity('Note', type=u'z')
-        metainf = note.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 = note.cw_metainformation()
+        self.assertEqual(metainf, {'source': {'adapter': 'native', 'uri': 'system'}, 'type': u'Note', 'extid': None})
+        self.assertEqual(note.absolute_url(), 'http://testing.fr/cubicweb/note/%s' % note.eid)
         metainf['source'] = metainf['source'].copy()
         metainf['source']['base-url']  = 'http://cubicweb2.com/'
         metainf['extid']  = 1234
-        self.assertEquals(note.absolute_url(), 'http://cubicweb2.com/note/1234')
+        self.assertEqual(note.absolute_url(), 'http://cubicweb2.com/note/1234')
 
     def test_absolute_url_empty_field(self):
         req = self.request()
         card = req.create_entity('Card', wikiid=u'', title=u'test')
-        self.assertEquals(card.absolute_url(),
+        self.assertEqual(card.absolute_url(),
                           'http://testing.fr/cubicweb/card/eid/%s' % card.eid)
 
     def test_create_entity(self):
@@ -531,10 +537,10 @@
         p = req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien',
                               connait=p1, evaluee=[p1, p2],
                               reverse_ecrit_par=note)
-        self.assertEquals(p.nom, 'di mascio')
-        self.assertEquals([c.nom for c in p.connait], ['fayolle'])
-        self.assertEquals(sorted([c.nom for c in p.evaluee]), ['campeas', 'fayolle'])
-        self.assertEquals([c.type for c in p.reverse_ecrit_par], ['z'])
+        self.assertEqual(p.nom, 'di mascio')
+        self.assertEqual([c.nom for c in p.connait], ['fayolle'])
+        self.assertEqual(sorted([c.nom for c in p.evaluee]), ['campeas', 'fayolle'])
+        self.assertEqual([c.type for c in p.reverse_ecrit_par], ['z'])
 
 
 
--- a/test/unittest_mail.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_mail.py	Wed Nov 03 16:38:28 2010 +0100
@@ -51,7 +51,7 @@
         mail = format_mail({'name': 'oim', 'email': 'oim@logilab.fr'},
                            ['test@logilab.fr'], u'un petit cöucou', u'bïjour',
                            config=self.config)
-        self.assertLinesEquals(mail.as_string(), """\
+        self.assertMultiLineEqual(mail.as_string(), """\
 MIME-Version: 1.0
 Content-Type: text/plain; charset="utf-8"
 Content-Transfer-Encoding: base64
@@ -64,17 +64,17 @@
 dW4gcGV0aXQgY8O2dWNvdQ==
 """)
         msg = message_from_string(mail.as_string())
-        self.assertEquals(msg.get('subject'), u'bïjour')
-        self.assertEquals(msg.get('from'), u'oim <oim@logilab.fr>')
-        self.assertEquals(msg.get('to'), u'test@logilab.fr')
-        self.assertEquals(msg.get('reply-to'), u'oim <oim@logilab.fr>, BimBam <bim@boum.fr>')
-        self.assertEquals(msg.get_payload(decode=True), u'un petit cöucou')
+        self.assertEqual(msg.get('subject'), u'bïjour')
+        self.assertEqual(msg.get('from'), u'oim <oim@logilab.fr>')
+        self.assertEqual(msg.get('to'), u'test@logilab.fr')
+        self.assertEqual(msg.get('reply-to'), u'oim <oim@logilab.fr>, BimBam <bim@boum.fr>')
+        self.assertEqual(msg.get_payload(decode=True), u'un petit cöucou')
 
 
     def test_format_mail_euro(self):
         mail = format_mail({'name': u'oîm', 'email': u'oim@logilab.fr'},
                            ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €')
-        self.assertLinesEquals(mail.as_string(), """\
+        self.assertMultiLineEqual(mail.as_string(), """\
 MIME-Version: 1.0
 Content-Type: text/plain; charset="utf-8"
 Content-Transfer-Encoding: base64
@@ -86,11 +86,11 @@
 dW4gcGV0aXQgY8O2dWNvdSDigqw=
 """)
         msg = message_from_string(mail.as_string())
-        self.assertEquals(msg.get('subject'), u'bïjour €')
-        self.assertEquals(msg.get('from'), u'oîm <oim@logilab.fr>')
-        self.assertEquals(msg.get('to'), u'test@logilab.fr')
-        self.assertEquals(msg.get('reply-to'), u'oîm <oim@logilab.fr>')
-        self.assertEquals(msg.get_payload(decode=True), u'un petit cöucou €')
+        self.assertEqual(msg.get('subject'), u'bïjour €')
+        self.assertEqual(msg.get('from'), u'oîm <oim@logilab.fr>')
+        self.assertEqual(msg.get('to'), u'test@logilab.fr')
+        self.assertEqual(msg.get('reply-to'), u'oîm <oim@logilab.fr>')
+        self.assertEqual(msg.get_payload(decode=True), u'un petit cöucou €')
 
 
     def test_format_mail_from_reply_to(self):
@@ -100,19 +100,19 @@
         msg = format_mail({'name': u'', 'email': u''},
                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
                           config=self.config)
-        self.assertEquals(msg.get('from'), u'')
-        self.assertEquals(msg.get('reply-to'), None)
+        self.assertEqual(msg.get('from'), u'')
+        self.assertEqual(msg.get('reply-to'), None)
         msg = format_mail({'name': u'tutu', 'email': u'tutu@logilab.fr'},
                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
                           config=self.config)
         msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'tutu <tutu@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
+        self.assertEqual(msg.get('from'), u'tutu <tutu@logilab.fr>')
+        self.assertEqual(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
         msg = format_mail({'name': u'tutu', 'email': u'tutu@logilab.fr'},
                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €')
         msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'tutu <tutu@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
+        self.assertEqual(msg.get('from'), u'tutu <tutu@logilab.fr>')
+        self.assertEqual(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
         # set sender name and address as expected
         self.set_option('sender-name', 'cubicweb-test')
         self.set_option('sender-addr', 'cubicweb-test@logilab.fr')
@@ -121,22 +121,22 @@
                            ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
                            config=self.config)
         msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'cubicweb-test <cubicweb-test@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'cubicweb-test <cubicweb-test@logilab.fr>')
+        self.assertEqual(msg.get('from'), u'cubicweb-test <cubicweb-test@logilab.fr>')
+        self.assertEqual(msg.get('reply-to'), u'cubicweb-test <cubicweb-test@logilab.fr>')
         # anonymous notification: only email specified
         msg = format_mail({'email': u'tutu@logilab.fr'},
                            ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
                            config=self.config)
         msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'cubicweb-test <tutu@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'cubicweb-test <tutu@logilab.fr>, cubicweb-test <cubicweb-test@logilab.fr>')
+        self.assertEqual(msg.get('from'), u'cubicweb-test <tutu@logilab.fr>')
+        self.assertEqual(msg.get('reply-to'), u'cubicweb-test <tutu@logilab.fr>, cubicweb-test <cubicweb-test@logilab.fr>')
         # anonymous notification: only name specified
         msg = format_mail({'name': u'tutu'},
                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
                           config=self.config)
         msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'tutu <cubicweb-test@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'tutu <cubicweb-test@logilab.fr>')
+        self.assertEqual(msg.get('from'), u'tutu <cubicweb-test@logilab.fr>')
+        self.assertEqual(msg.get('reply-to'), u'tutu <cubicweb-test@logilab.fr>')
 
 
 
--- a/test/unittest_migration.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_migration.py	Wed Nov 03 16:38:28 2010 +0100
@@ -52,26 +52,26 @@
         self.config.__class__.cube_appobject_path = frozenset()
 
     def test_filter_scripts_base(self):
-        self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,3,0), (2,4,0)),
+        self.assertListEqual(filter_scripts(self.config, SMIGRDIR, (2,3,0), (2,4,0)),
                               [])
-        self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,5,0)),
+        self.assertListEqual(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,5,0)),
                               [((2, 5, 0), SMIGRDIR+'2.5.0_Any.sql')])
-        self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,6,0)),
+        self.assertListEqual(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,6,0)),
                               [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')])
-        self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,6,0)),
+        self.assertListEqual(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,6,0)),
                               [((2, 5, 0), SMIGRDIR+'2.5.0_Any.sql'),
                                ((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')])
-        self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,5,1)),
+        self.assertListEqual(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,5,1)),
                               [])
-        self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,10,2)),
+        self.assertListEqual(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,10,2)),
                               [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql'),
                                ((2, 10, 2), SMIGRDIR+'2.10.2_Any.sql')])
-        self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,1), (2,6,0)),
+        self.assertListEqual(filter_scripts(self.config, SMIGRDIR, (2,5,1), (2,6,0)),
                               [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')])
 
-        self.assertListEquals(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,3)),
+        self.assertListEqual(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,3)),
                               [((0, 0, 3), TMIGRDIR+'0.0.3_Any.py')])
-        self.assertListEquals(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,4)),
+        self.assertListEqual(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,4)),
                               [((0, 0, 3), TMIGRDIR+'0.0.3_Any.py'),
                                ((0, 0, 4), TMIGRDIR+'0.0.4_Any.py')])
 
@@ -82,16 +82,16 @@
         self.assertIsInstance(config.migration_handler(), MigrationHelper)
         config = self.config
         config.__class__.name = 'twisted'
-        self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
+        self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
                               [((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'),
                                ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')])
         config.__class__.name = 'repository'
-        self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
+        self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
                               [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'),
                                ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'),
                                ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py')])
         config.__class__.name = 'all-in-one'
-        self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
+        self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
                               [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'),
                                ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'),
                                ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py'),
@@ -107,7 +107,7 @@
         """make sure database can be created"""
         config = ApptestConfiguration('data')
         source = config.sources()['system']
-        self.assertEquals(source['db-driver'], 'sqlite')
+        self.assertEqual(source['db-driver'], 'sqlite')
         cleanup_sqlite(source['db-name'], removetemplate=True)
         init_test_database(config=config)
 
--- a/test/unittest_req.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_req.py	Wed Nov 03 16:38:28 2010 +0100
@@ -17,17 +17,31 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 from logilab.common.testlib import TestCase, unittest_main
 from cubicweb.req import RequestSessionBase
+from cubicweb.devtools.testlib import CubicWebTC
+
 
 class RebuildURLTC(TestCase):
-    def test(self):
+    def test_rebuild_url(self):
         rebuild_url = RequestSessionBase(None).rebuild_url
-        self.assertEquals(rebuild_url('http://logilab.fr?__message=pouet', __message='hop'),
+        self.assertEqual(rebuild_url('http://logilab.fr?__message=pouet', __message='hop'),
+                          'http://logilab.fr?__message=hop')
+        self.assertEqual(rebuild_url('http://logilab.fr', __message='hop'),
                           'http://logilab.fr?__message=hop')
-        self.assertEquals(rebuild_url('http://logilab.fr', __message='hop'),
-                          'http://logilab.fr?__message=hop')
-        self.assertEquals(rebuild_url('http://logilab.fr?vid=index', __message='hop'),
+        self.assertEqual(rebuild_url('http://logilab.fr?vid=index', __message='hop'),
                           'http://logilab.fr?__message=hop&vid=index')
 
+    def test_build_url(self):
+        req = RequestSessionBase(None)
+        req.from_controller = lambda : 'view'
+        req.relative_path = lambda includeparams=True: None
+        req.base_url = lambda : 'http://testing.fr/cubicweb/'
+        self.assertEqual(req.build_url(), u'http://testing.fr/cubicweb/view')
+        self.assertEqual(req.build_url(None), u'http://testing.fr/cubicweb/view')
+        self.assertEqual(req.build_url('one'), u'http://testing.fr/cubicweb/one')
+        self.assertEqual(req.build_url(param='ok'), u'http://testing.fr/cubicweb/view?param=ok')
+        self.assertRaises(AssertionError, req.build_url, 'one', 'two not allowed')
+        self.assertRaises(ValueError, req.build_url, 'view', test=None)
+
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_rqlrewrite.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_rqlrewrite.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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 logilab.common.testlib import mock_object
 from yams import BadSchemaDefinition
@@ -109,7 +107,7 @@
         rqlst = parse('Any S WHERE S documented_by C, C eid %(u)s')
         rewrite(rqlst, {('C', 'X'): (card_constraint,), ('S', 'X'): affaire_constraints},
                 kwargs)
-        self.assertTextEquals(rqlst.as_string(),
+        self.assertMultiLineEqual(rqlst.as_string(),
                              "Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, "
                              "EXISTS(C in_state A, B in_group E, F require_state A, "
                              "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission), "
@@ -272,7 +270,7 @@
                              "EXISTS(U in_group B, B name 'managers', B is CWGroup), T is TrInfo")
 
     def test_unsupported_constraint_3(self):
-        self.skip('raise unauthorized for now')
+        self.skipTest('raise unauthorized for now')
         trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
         rqlst = parse('Any T WHERE T wf_info_for X')
         rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X in_group G, G name "managers"')}, {})
--- a/test/unittest_rset.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_rset.py	Wed Nov 03 16:38:28 2010 +0100
@@ -52,7 +52,7 @@
             }
         for rql, relations in queries.items():
             result = list(attr_desc_iterator(parse(rql).children[0]))
-            self.assertEquals((rql, result), (rql, relations))
+            self.assertEqual((rql, result), (rql, relations))
 
     def test_relations_description_indexed(self):
         """tests relations_description() function"""
@@ -63,7 +63,7 @@
         for rql, results in queries.items():
             for var_index, relations in results.items():
                 result = list(attr_desc_iterator(parse(rql).children[0], var_index))
-                self.assertEquals(result, relations)
+                self.assertEqual(result, relations)
 
 
 
@@ -79,15 +79,15 @@
     def compare_urls(self, url1, url2):
         info1 = urlsplit(url1)
         info2 = urlsplit(url2)
-        self.assertEquals(info1[:3], info2[:3])
+        self.assertEqual(info1[:3], info2[:3])
         if info1[3] != info2[3]:
             params1 = dict(pair.split('=') for pair in info1[3].split('&'))
             params2 = dict(pair.split('=') for pair in info1[3].split('&'))
-            self.assertDictEquals(params1, params2)
+            self.assertDictEqual(params1, params2)
 
     def test_pickle(self):
         del self.rset.req
-        self.assertEquals(len(pickle.dumps(self.rset)), 392)
+        self.assertEqual(len(pickle.dumps(self.rset)), 392)
 
     def test_build_url(self):
         req = self.request()
@@ -105,9 +105,9 @@
     def test_resultset_build(self):
         """test basic build of a ResultSet"""
         rs = ResultSet([1,2,3], 'CWGroup X', description=['CWGroup', 'CWGroup', 'CWGroup'])
-        self.assertEquals(rs.rowcount, 3)
-        self.assertEquals(rs.rows, [1,2,3])
-        self.assertEquals(rs.description, ['CWGroup', 'CWGroup', 'CWGroup'])
+        self.assertEqual(rs.rowcount, 3)
+        self.assertEqual(rs.rows, [1,2,3])
+        self.assertEqual(rs.description, ['CWGroup', 'CWGroup', 'CWGroup'])
 
 
     def test_resultset_limit(self):
@@ -117,12 +117,12 @@
         rs.req = self.request()
         rs.vreg = self.vreg
 
-        self.assertEquals(rs.limit(2).rows, [[12000, 'adim'], [13000, 'syt']])
+        self.assertEqual(rs.limit(2).rows, [[12000, 'adim'], [13000, 'syt']])
         rs2 = rs.limit(2, offset=1)
-        self.assertEquals(rs2.rows, [[13000, 'syt'], [14000, 'nico']])
-        self.assertEquals(rs2.get_entity(0, 0).cw_row, 0)
-        self.assertEquals(rs.limit(2, offset=2).rows, [[14000, 'nico']])
-        self.assertEquals(rs.limit(2, offset=3).rows, [])
+        self.assertEqual(rs2.rows, [[13000, 'syt'], [14000, 'nico']])
+        self.assertEqual(rs2.get_entity(0, 0).cw_row, 0)
+        self.assertEqual(rs.limit(2, offset=2).rows, [[14000, 'nico']])
+        self.assertEqual(rs.limit(2, offset=3).rows, [])
 
 
     def test_resultset_filter(self):
@@ -135,8 +135,8 @@
             return entity.login != 'nico'
 
         rs2 = rs.filtered_rset(test_filter)
-        self.assertEquals(len(rs2), 2)
-        self.assertEquals([login for _, login in rs2], ['adim', 'syt'])
+        self.assertEqual(len(rs2), 2)
+        self.assertEqual([login for _, login in rs2], ['adim', 'syt'])
 
     def test_resultset_transform(self):
         rs = ResultSet([[12, 'adim'], [13, 'syt'], [14, 'nico']],
@@ -147,8 +147,8 @@
             return row[1:], desc[1:]
         rs2 = rs.transformed_rset(test_transform)
 
-        self.assertEquals(len(rs2), 3)
-        self.assertEquals(list(rs2), [['adim'],['syt'],['nico']])
+        self.assertEqual(len(rs2), 3)
+        self.assertEqual(list(rs2), [['adim'],['syt'],['nico']])
 
     def test_resultset_sort(self):
         rs = ResultSet([[12000, 'adim'], [13000, 'syt'], [14000, 'nico']],
@@ -158,22 +158,22 @@
         rs.vreg = self.vreg
 
         rs2 = rs.sorted_rset(lambda e:e['login'])
-        self.assertEquals(len(rs2), 3)
-        self.assertEquals([login for _, login in rs2], ['adim', 'nico', 'syt'])
+        self.assertEqual(len(rs2), 3)
+        self.assertEqual([login for _, login in rs2], ['adim', 'nico', 'syt'])
         # make sure rs is unchanged
-        self.assertEquals([login for _, login in rs], ['adim', 'syt', 'nico'])
+        self.assertEqual([login for _, login in rs], ['adim', 'syt', 'nico'])
 
         rs2 = rs.sorted_rset(lambda e:e['login'], reverse=True)
-        self.assertEquals(len(rs2), 3)
-        self.assertEquals([login for _, login in rs2], ['syt', 'nico', 'adim'])
+        self.assertEqual(len(rs2), 3)
+        self.assertEqual([login for _, login in rs2], ['syt', 'nico', 'adim'])
         # make sure rs is unchanged
-        self.assertEquals([login for _, login in rs], ['adim', 'syt', 'nico'])
+        self.assertEqual([login for _, login in rs], ['adim', 'syt', 'nico'])
 
         rs3 = rs.sorted_rset(lambda row: row[1], col=-1)
-        self.assertEquals(len(rs3), 3)
-        self.assertEquals([login for _, login in rs3], ['adim', 'nico', 'syt'])
+        self.assertEqual(len(rs3), 3)
+        self.assertEqual([login for _, login in rs3], ['adim', 'nico', 'syt'])
         # make sure rs is unchanged
-        self.assertEquals([login for _, login in rs], ['adim', 'syt', 'nico'])
+        self.assertEqual([login for _, login in rs], ['adim', 'syt', 'nico'])
 
     def test_resultset_split(self):
         rs = ResultSet([[12000, 'adim', u'Adim chez les pinguins'],
@@ -188,32 +188,32 @@
         rs.vreg = self.vreg
 
         rsets = rs.split_rset(lambda e:e['login'])
-        self.assertEquals(len(rsets), 3)
-        self.assertEquals([login for _, login,_ in rsets[0]], ['adim', 'adim'])
-        self.assertEquals([login for _, login,_ in rsets[1]], ['syt'])
-        self.assertEquals([login for _, login,_ in rsets[2]], ['nico', 'nico'])
+        self.assertEqual(len(rsets), 3)
+        self.assertEqual([login for _, login,_ in rsets[0]], ['adim', 'adim'])
+        self.assertEqual([login for _, login,_ in rsets[1]], ['syt'])
+        self.assertEqual([login for _, login,_ in rsets[2]], ['nico', 'nico'])
         # make sure rs is unchanged
-        self.assertEquals([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico'])
+        self.assertEqual([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico'])
 
         rsets = rs.split_rset(lambda e:e['login'], return_dict=True)
-        self.assertEquals(len(rsets), 3)
-        self.assertEquals([login for _, login,_ in rsets['nico']], ['nico', 'nico'])
-        self.assertEquals([login for _, login,_ in rsets['adim']], ['adim', 'adim'])
-        self.assertEquals([login for _, login,_ in rsets['syt']], ['syt'])
+        self.assertEqual(len(rsets), 3)
+        self.assertEqual([login for _, login,_ in rsets['nico']], ['nico', 'nico'])
+        self.assertEqual([login for _, login,_ in rsets['adim']], ['adim', 'adim'])
+        self.assertEqual([login for _, login,_ in rsets['syt']], ['syt'])
         # make sure rs is unchanged
-        self.assertEquals([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico'])
+        self.assertEqual([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico'])
 
         rsets = rs.split_rset(lambda s: s.count('d'), col=2)
-        self.assertEquals(len(rsets), 2)
-        self.assertEquals([title for _, _, title in rsets[0]],
+        self.assertEqual(len(rsets), 2)
+        self.assertEqual([title for _, _, title in rsets[0]],
                           [u"Adim chez les pinguins",
                            u"Jardiner facile",
                            u"L'épluchage du castor commun",])
-        self.assertEquals([title for _, _, title in rsets[1]],
+        self.assertEqual([title for _, _, title in rsets[1]],
                           [u"Le carrelage en 42 leçons",
                            u"La tarte tatin en 15 minutes",])
         # make sure rs is unchanged
-        self.assertEquals([title for _, _, title in rs],
+        self.assertEqual([title for _, _, title in rs],
                           [u'Adim chez les pinguins',
                            u'Jardiner facile',
                            u'Le carrelage en 42 leçons',
@@ -228,15 +228,15 @@
 
     def test_get_entity_simple(self):
         self.request().create_entity('CWUser', login=u'adim', upassword='adim',
-                        surname=u'di mascio', firstname=u'adrien')
+                                     surname=u'di mascio', firstname=u'adrien')
         e = self.execute('Any X,T WHERE X login "adim", X surname T').get_entity(0, 0)
-        self.assertEquals(e['surname'], 'di mascio')
+        self.assertEqual(e['surname'], 'di mascio')
         self.assertRaises(KeyError, e.__getitem__, 'firstname')
         self.assertRaises(KeyError, e.__getitem__, 'creation_date')
-        self.assertEquals(pprelcachedict(e._related_cache), [])
+        self.assertEqual(pprelcachedict(e._cw_related_cache), [])
         e.complete()
-        self.assertEquals(e['firstname'], 'adrien')
-        self.assertEquals(pprelcachedict(e._related_cache), [])
+        self.assertEqual(e['firstname'], 'adrien')
+        self.assertEqual(pprelcachedict(e._cw_related_cache), [])
 
     def test_get_entity_advanced(self):
         self.request().create_entity('Bookmark', title=u'zou', path=u'/view')
@@ -244,24 +244,24 @@
         rset = self.execute('Any X,Y,XT,YN WHERE X bookmarked_by Y, X title XT, Y login YN')
 
         e = rset.get_entity(0, 0)
-        self.assertEquals(e.cw_row, 0)
-        self.assertEquals(e.cw_col, 0)
-        self.assertEquals(e['title'], 'zou')
+        self.assertEqual(e.cw_row, 0)
+        self.assertEqual(e.cw_col, 0)
+        self.assertEqual(e['title'], 'zou')
         self.assertRaises(KeyError, e.__getitem__, 'path')
-        self.assertEquals(e.view('text'), 'zou')
-        self.assertEquals(pprelcachedict(e._related_cache), [])
+        self.assertEqual(e.view('text'), 'zou')
+        self.assertEqual(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.assertEqual(e.cw_row, 0)
+        self.assertEqual(e.cw_col, 1)
+        self.assertEqual(e['login'], 'anon')
         self.assertRaises(KeyError, e.__getitem__, 'firstname')
-        self.assertEquals(pprelcachedict(e._related_cache),
+        self.assertEqual(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.assertEqual(e['firstname'], None)
+        self.assertEqual(e.view('text'), 'anon')
+        self.assertEqual(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.assertEqual(pprelcachedict(e._cw_related_cache),
                           [('in_state_subject', [seid])])
 
     def test_get_entity_advanced_prefilled_cache(self):
@@ -282,16 +282,16 @@
         rset = self.execute('Any X,U,S,XT,UL,SN WHERE X created_by U, U in_state S, '
                             '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.assertEqual(e['title'], 'zou')
+        self.assertEqual(pprelcachedict(e._cw_related_cache),
                           [('created_by_subject', [5])])
         # first level of recursion
         u = e.created_by[0]
-        self.assertEquals(u['login'], 'admin')
+        self.assertEqual(u['login'], 'admin')
         self.assertRaises(KeyError, u.__getitem__, 'firstname')
         # second level of recursion
         s = u.in_state[0]
-        self.assertEquals(s['name'], 'activated')
+        self.assertEqual(s['name'], 'activated')
         self.assertRaises(KeyError, s.__getitem__, 'description')
 
 
@@ -302,11 +302,11 @@
         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.assertEqual(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)
+        self.assertEqual(cached.rowcount, 0)
 
 
     def test_get_entity_union(self):
@@ -320,16 +320,16 @@
                     ('CWGroup', 'users'))
         for entity in rset.entities(): # test get_entity for each row actually
             etype, n = expected[entity.cw_row]
-            self.assertEquals(entity.__regid__, etype)
+            self.assertEqual(entity.__regid__, etype)
             attr = etype == 'Bookmark' and 'title' or 'name'
-            self.assertEquals(entity[attr], n)
+            self.assertEqual(entity[attr], n)
 
     def test_related_entity_optional(self):
         e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
         rset = self.execute('Any B,U,L WHERE B bookmarked_by U?, U login L')
         entity, rtype = rset.related_entity(0, 2)
-        self.assertEquals(entity, None)
-        self.assertEquals(rtype, None)
+        self.assertEqual(entity, None)
+        self.assertEqual(rtype, None)
 
     def test_related_entity_union_subquery(self):
         e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
@@ -338,27 +338,27 @@
                             ' UNION '
                             ' (Any X,N WHERE X is Bookmark, X title N))')
         entity, rtype = rset.related_entity(0, 1)
-        self.assertEquals(entity.eid, e.eid)
-        self.assertEquals(rtype, 'title')
+        self.assertEqual(entity.eid, e.eid)
+        self.assertEqual(rtype, 'title')
         entity, rtype = rset.related_entity(1, 1)
-        self.assertEquals(entity.__regid__, 'CWGroup')
-        self.assertEquals(rtype, 'name')
+        self.assertEqual(entity.__regid__, 'CWGroup')
+        self.assertEqual(rtype, 'name')
         #
         rset = self.execute('Any X,N ORDERBY N WHERE X is Bookmark WITH X,N BEING '
                             '((Any X,N WHERE X is CWGroup, X name N)'
                             ' UNION '
                             ' (Any X,N WHERE X is Bookmark, X title N))')
         entity, rtype = rset.related_entity(0, 1)
-        self.assertEquals(entity.eid, e.eid)
-        self.assertEquals(rtype, 'title')
+        self.assertEqual(entity.eid, e.eid)
+        self.assertEqual(rtype, 'title')
         #
         rset = self.execute('Any X,N ORDERBY N WITH N,X BEING '
                             '((Any N,X WHERE X is CWGroup, X name N)'
                             ' UNION '
                             ' (Any N,X WHERE X is Bookmark, X title N))')
         entity, rtype = rset.related_entity(0, 1)
-        self.assertEquals(entity.eid, e.eid)
-        self.assertEquals(rtype, 'title')
+        self.assertEqual(entity.eid, e.eid)
+        self.assertEqual(rtype, 'title')
 
     def test_related_entity_trap_subquery(self):
         req = self.request()
@@ -368,32 +368,40 @@
                             'WITH B,T BEING (Any B,T WHERE B is Bookmark, B title T)')
         rset.related_entity(0, 2)
 
+    def test_related_entity_subquery_outerjoin(self):
+        rset = self.execute('Any X,S,L WHERE X in_state S '
+                            'WITH X, L BEING (Any X,MAX(L) GROUPBY X '
+                            'WHERE X is CWUser, T? wf_info_for X, T creation_date L)')
+        self.assertEqual(len(rset), 2)
+        rset.related_entity(0, 1)
+        rset.related_entity(0, 2)
+
     def test_entities(self):
         rset = self.execute('Any U,G WHERE U in_group G')
         # make sure we have at least one element
         self.failUnless(rset)
-        self.assertEquals(set(e.e_schema.type for e in rset.entities(0)),
+        self.assertEqual(set(e.e_schema.type for e in rset.entities(0)),
                           set(['CWUser',]))
-        self.assertEquals(set(e.e_schema.type for e in rset.entities(1)),
+        self.assertEqual(set(e.e_schema.type for e in rset.entities(1)),
                           set(['CWGroup',]))
 
     def test_printable_rql(self):
         rset = self.execute(u'CWEType X WHERE X final FALSE')
-        self.assertEquals(rset.printable_rql(),
+        self.assertEqual(rset.printable_rql(),
                           'Any X WHERE X final FALSE, X is CWEType')
 
     def test_searched_text(self):
         rset = self.execute(u'Any X WHERE X has_text "foobar"')
-        self.assertEquals(rset.searched_text(), 'foobar')
+        self.assertEqual(rset.searched_text(), 'foobar')
         rset = self.execute(u'Any X WHERE X has_text %(text)s', {'text' : 'foo'})
-        self.assertEquals(rset.searched_text(), 'foo')
+        self.assertEqual(rset.searched_text(), 'foo')
 
     def test_union_limited_rql(self):
         rset = self.execute('(Any X,N WHERE X is Bookmark, X title N)'
                             ' UNION '
                             '(Any X,N WHERE X is CWGroup, X name N)')
         rset.limit(2, 10, inplace=True)
-        self.assertEquals(rset.limited_rql(),
+        self.assertEqual(rset.limited_rql(),
                           'Any A,B LIMIT 2 OFFSET 10 '
                           'WITH A,B BEING ('
                           '(Any X,N WHERE X is Bookmark, X title N) '
@@ -403,7 +411,21 @@
 
     def test_count_users_by_date(self):
         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))
+        self.assertEqual(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.assertEqual(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.assertEqual(len(str(rset).splitlines()), 1)
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_rtags.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_rtags.py	Wed Nov 03 16:38:28 2010 +0100
@@ -28,25 +28,25 @@
         rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary')
         rtags.tag_subject_of(('*', 'evaluee', '*'), 'secondary')
         rtags.tag_object_of(('*', 'tags', '*'), 'generated')
-        self.assertEquals(rtags.get('Note', 'evaluee', '*', 'subject'),
+        self.assertEqual(rtags.get('Note', 'evaluee', '*', 'subject'),
                           'secondary')
-        self.assertEquals(rtags.get('Societe', 'travaille', '*', 'subject'),
+        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
                           'primary')
-        self.assertEquals(rtags.get('Note', 'travaille', '*', 'subject'),
+        self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'),
                           None)
-        self.assertEquals(rtags.get('Note', 'tags', '*', 'subject'),
+        self.assertEqual(rtags.get('Note', 'tags', '*', 'subject'),
                           None)
-        self.assertEquals(rtags.get('*', 'tags', 'Note', 'object'),
+        self.assertEqual(rtags.get('*', 'tags', 'Note', 'object'),
                           'generated')
-        self.assertEquals(rtags.get('Tag', 'tags', '*', 'object'),
+        self.assertEqual(rtags.get('Tag', 'tags', '*', 'object'),
                           'generated')
 
-#         self.assertEquals(rtags.rtag('evaluee', 'Note', 'subject'), set(('secondary', 'link')))
-#         self.assertEquals(rtags.is_inlined('evaluee', 'Note', 'subject'), False)
-#         self.assertEquals(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
-#         self.assertEquals(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
-#         self.assertEquals(rtags.rtag('ecrit_par', 'Note', 'object'), set(('inlineview', 'link')))
-#         self.assertEquals(rtags.is_inlined('ecrit_par', 'Note', 'object'), True)
+#         self.assertEqual(rtags.rtag('evaluee', 'Note', 'subject'), set(('secondary', 'link')))
+#         self.assertEqual(rtags.is_inlined('evaluee', 'Note', 'subject'), False)
+#         self.assertEqual(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
+#         self.assertEqual(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
+#         self.assertEqual(rtags.rtag('ecrit_par', 'Note', 'object'), set(('inlineview', 'link')))
+#         self.assertEqual(rtags.is_inlined('ecrit_par', 'Note', 'object'), True)
 #         class Personne2(Personne):
 #             id = 'Personne'
 #             __rtags__ = {
@@ -54,21 +54,21 @@
 #                 }
 #         self.vreg.register(Personne2)
 #         rtags = Personne2.rtags
-#         self.assertEquals(rtags.rtag('evaluee', 'Note', 'subject'), set(('inlineview', 'link')))
-#         self.assertEquals(rtags.is_inlined('evaluee', 'Note', 'subject'), True)
-#         self.assertEquals(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
-#         self.assertEquals(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
+#         self.assertEqual(rtags.rtag('evaluee', 'Note', 'subject'), set(('inlineview', 'link')))
+#         self.assertEqual(rtags.is_inlined('evaluee', 'Note', 'subject'), True)
+#         self.assertEqual(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
+#         self.assertEqual(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
 
 
     def test_rtagset_expansion(self):
         rtags = RelationTagsSet()
         rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary')
         rtags.tag_subject_of(('*', 'travaille', '*'), 'secondary')
-        self.assertEquals(rtags.get('Societe', 'travaille', '*', 'subject'),
+        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
                           set(('primary', 'secondary')))
-        self.assertEquals(rtags.get('Note', 'travaille', '*', 'subject'),
+        self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'),
                           set(('secondary',)))
-        self.assertEquals(rtags.get('Note', 'tags', "*", 'subject'),
+        self.assertEqual(rtags.get('Note', 'tags', "*", 'subject'),
                           set())
 
     def test_rtagdict_expansion(self):
@@ -79,16 +79,16 @@
                              {'key1': 'val0', 'key3': 'val0'})
         rtags.tag_subject_of(('Societe', 'travaille', '*'),
                              {'key2': 'val2'})
-        self.assertEquals(rtags.get('Societe', 'travaille', '*', 'subject'),
+        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
                           {'key1': 'val1', 'key2': 'val2', 'key3': 'val0'})
-        self.assertEquals(rtags.get('Note', 'travaille', '*', 'subject'),
+        self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'),
                           {'key1': 'val0', 'key3': 'val0'})
-        self.assertEquals(rtags.get('Note', 'tags', "*", 'subject'),
+        self.assertEqual(rtags.get('Note', 'tags', "*", 'subject'),
                           {})
 
         rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key1', 'val4')
         rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key4', 'val4')
-        self.assertEquals(rtags.get('Societe', 'travaille', '*', 'subject'),
+        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
                           {'key1': 'val1', 'key2': 'val2', 'key3': 'val0', 'key4': 'val4'})
 
 if __name__ == '__main__':
--- a/test/unittest_schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_schema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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.schema
-
-"""
+"""unit tests for module cubicweb.schema"""
 
 import sys
 from os.path import join, isabs, basename, dirname
@@ -144,13 +142,13 @@
     def test_erqlexpression(self):
         self.assertRaises(RQLSyntaxError, ERQLExpression, '1')
         expr = ERQLExpression('X travaille S, S owned_by U')
-        self.assertEquals(str(expr), 'Any X WHERE X travaille S, S owned_by U, X eid %(x)s, U eid %(u)s')
+        self.assertEqual(str(expr), 'Any X WHERE X travaille S, S owned_by U, X eid %(x)s, U eid %(u)s')
 
     def test_rrqlexpression(self):
         self.assertRaises(Exception, RRQLExpression, '1')
         self.assertRaises(RQLSyntaxError, RRQLExpression, 'O X Y')
         expr = RRQLExpression('U has_update_permission O')
-        self.assertEquals(str(expr), 'Any O,U WHERE U has_update_permission O, O eid %(o)s, U eid %(u)s')
+        self.assertEqual(str(expr), 'Any O,U WHERE U has_update_permission O, O eid %(o)s, U eid %(u)s')
 
 loader = CubicWebSchemaLoader()
 config = TestConfiguration('data')
@@ -160,37 +158,40 @@
 
     def test_order_eschemas(self):
         schema = loader.load(config)
-        self.assertEquals(order_eschemas([schema['Note'], schema['SubNote']]),
+        self.assertEqual(order_eschemas([schema['Note'], schema['SubNote']]),
                                          [schema['Note'], schema['SubNote']])
-        self.assertEquals(order_eschemas([schema['SubNote'], schema['Note']]),
+        self.assertEqual(order_eschemas([schema['SubNote'], schema['Note']]),
                                          [schema['Note'], schema['SubNote']])
 
     def test_knownValues_load_schema(self):
         schema = loader.load(config)
         self.assert_(isinstance(schema, CubicWebSchema))
-        self.assertEquals(schema.name, 'data')
+        self.assertEqual(schema.name, 'data')
         entities = [str(e) for e in schema.entities()]
         entities.sort()
         expected_entities = ['BaseTransition', 'Bookmark', 'Boolean', 'Bytes', 'Card',
                              'Date', 'Datetime', 'Decimal',
                              'CWCache', 'CWConstraint', 'CWConstraintType', 'CWEType',
                              'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation',
-                             'CWPermission', 'CWProperty', 'CWRType', 'CWUser',
-                             'ExternalUri', 'File', 'Float', 'Image', 'Int', 'Interval', 'Note',
+                             'CWPermission', 'CWProperty', 'CWRType',
+                             'CWUniqueTogetherConstraint', 'CWUser',
+                             'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
                              'Password', 'Personne',
                              'RQLExpression',
                              'Societe', 'State', 'String', 'SubNote', 'SubWorkflowExitPoint',
                              'Tag', 'Time', 'Transition', 'TrInfo',
                              'Workflow', 'WorkflowTransition']
-        self.assertListEquals(entities, sorted(expected_entities))
+        self.assertListEqual(entities, sorted(expected_entities))
         relations = [str(r) for r in schema.relations()]
         relations.sort()
         expected_relations = ['add_permission', 'address', 'alias', 'allowed_transition',
                               'bookmarked_by', 'by_transition',
 
                               'cardinality', 'comment', 'comment_format',
-                              'composite', 'condition', 'connait', 'constrained_by', 'content',
-                              'content_format', 'created_by', 'creation_date', 'cstrtype', 'custom_workflow', 'cwuri',
+                              'composite', 'condition', 'connait',
+                              'constrained_by', 'constraint_of',
+                              'content', 'content_format',
+                              'created_by', 'creation_date', 'cstrtype', 'custom_workflow', 'cwuri',
 
                               'data', 'data_encoding', 'data_format', 'data_name', 'default_workflow', 'defaultval', 'delete_permission',
                               'description', 'description_format', 'destination_state',
@@ -214,7 +215,7 @@
 
                               'path', 'pkey', 'prefered_form', 'prenom', 'primary_email',
 
-                              'read_permission', 'relation_type', 'require_group',
+                              'read_permission', 'relation_type', 'relations', 'require_group',
 
                               'specializes', 'state_of', 'subworkflow', 'subworkflow_exit', 'subworkflow_state', 'surname', 'symmetric', 'synopsis',
 
@@ -226,11 +227,11 @@
 
                               'wf_info_for', 'wikiid', 'workflow_of']
 
-        self.assertListEquals(relations, expected_relations)
+        self.assertListEqual(relations, expected_relations)
 
         eschema = schema.eschema('CWUser')
         rels = sorted(str(r) for r in eschema.subject_relations())
-        self.assertListEquals(rels, ['created_by', 'creation_date', 'custom_workflow', 'cwuri', 'eid',
+        self.assertListEqual(rels, ['created_by', 'creation_date', 'custom_workflow', 'cwuri', 'eid',
                                      'evaluee', 'firstname', 'has_text', 'identity',
                                      'in_group', 'in_state', 'is',
                                      'is_instance_of', 'last_login_time',
@@ -238,11 +239,11 @@
                                      'primary_email', 'surname', 'upassword',
                                      'use_email'])
         rels = sorted(r.type for r in eschema.object_relations())
-        self.assertListEquals(rels, ['bookmarked_by', 'created_by', 'for_user',
+        self.assertListEqual(rels, ['bookmarked_by', 'created_by', 'for_user',
                                      'identity', 'owned_by', 'wf_info_for'])
         rschema = schema.rschema('relation_type')
         properties = rschema.rdef('CWAttribute', 'CWRType')
-        self.assertEquals(properties.cardinality, '1*')
+        self.assertEqual(properties.cardinality, '1*')
         constraints = properties.constraints
         self.failUnlessEqual(len(constraints), 1, constraints)
         constraint = constraints[0]
@@ -257,13 +258,13 @@
     def test_permission_settings(self):
         schema = loader.load(config)
         aschema = schema['TrInfo'].rdef('comment')
-        self.assertEquals(aschema.get_groups('read'),
+        self.assertEqual(aschema.get_groups('read'),
                           set(('managers', 'users', 'guests')))
-        self.assertEquals(aschema.get_rqlexprs('read'),
+        self.assertEqual(aschema.get_rqlexprs('read'),
                           ())
-        self.assertEquals(aschema.get_groups('update'),
+        self.assertEqual(aschema.get_groups('update'),
                           set(('managers',)))
-        self.assertEquals([x.expression for x in aschema.get_rqlexprs('update')],
+        self.assertEqual([x.expression for x in aschema.get_rqlexprs('update')],
                           ['U has_update_permission X'])
 
 class BadSchemaRQLExprTC(TestCase):
@@ -278,7 +279,7 @@
         self.loader.handle_file(join(DATADIR, schemafile))
         ex = self.assertRaises(BadSchemaDefinition,
                                self.loader._build_schema, 'toto', False)
-        self.assertEquals(str(ex), msg)
+        self.assertEqual(str(ex), msg)
 
     def test_rrqlexpr_on_etype(self):
         self._test('rrqlexpr_on_eetype.py',
@@ -300,12 +301,12 @@
 class NormalizeExpressionTC(TestCase):
 
     def test(self):
-        self.assertEquals(normalize_expression('X  bla Y,Y blur Z  ,  Z zigoulou   X '),
+        self.assertEqual(normalize_expression('X  bla Y,Y blur Z  ,  Z zigoulou   X '),
                                                'X bla Y, Y blur Z, Z zigoulou X')
 
 class RQLExpressionTC(TestCase):
     def test_comparison(self):
-        self.assertEquals(ERQLExpression('X is CWUser', 'X', 0),
+        self.assertEqual(ERQLExpression('X is CWUser', 'X', 0),
                           ERQLExpression('X is CWUser', 'X', 0))
         self.assertNotEquals(ERQLExpression('X is CWUser', 'X', 0),
                              ERQLExpression('X is CWGroup', 'X', 0))
@@ -313,7 +314,7 @@
 class GuessRrqlExprMainVarsTC(TestCase):
     def test_exists(self):
         mainvars = guess_rrqlexpr_mainvars(normalize_expression('NOT EXISTS(O team_competition C, C level < 3)'))
-        self.assertEquals(mainvars, 'O')
+        self.assertEqual(mainvars, 'O')
 
 
 if __name__ == '__main__':
--- a/test/unittest_selectors.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_selectors.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,15 +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 selectors mechanism
+"""unit tests for selectors mechanism"""
 
-"""
-
+from operator import eq, lt, le, gt
 from logilab.common.testlib import TestCase, unittest_main
 
+from cubicweb import Binary
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.appobject import Selector, AndSelector, OrSelector
-from cubicweb.selectors import implements, match_user_groups
+from cubicweb.selectors import (is_instance, adaptable, match_user_groups,
+                                multi_lines_rset)
 from cubicweb.interfaces import IDownloadable
 from cubicweb.web import action
 
@@ -42,114 +43,120 @@
 class SelectorsTC(TestCase):
     def test_basic_and(self):
         selector = _1_() & _1_()
-        self.assertEquals(selector(None), 2)
+        self.assertEqual(selector(None), 2)
         selector = _1_() & _0_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
         selector = _0_() & _1_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
 
     def test_basic_or(self):
         selector = _1_() | _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _1_() | _0_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _0_() | _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _0_() | _0_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
 
     def test_selector_and_function(self):
         selector = _1_() & _2_
-        self.assertEquals(selector(None), 3)
+        self.assertEqual(selector(None), 3)
         selector = _2_ & _1_()
-        self.assertEquals(selector(None), 3)
+        self.assertEqual(selector(None), 3)
 
     def test_three_and(self):
         selector = _1_() & _1_() & _1_()
-        self.assertEquals(selector(None), 3)
+        self.assertEqual(selector(None), 3)
         selector = _1_() & _0_() & _1_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
         selector = _0_() & _1_() & _1_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
 
     def test_three_or(self):
         selector = _1_() | _1_() | _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _1_() | _0_() | _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _0_() | _1_() | _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _0_() | _0_() | _0_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
 
     def test_composition(self):
         selector = (_1_() & _1_()) & (_1_() & _1_())
         self.failUnless(isinstance(selector, AndSelector))
-        self.assertEquals(len(selector.selectors), 4)
-        self.assertEquals(selector(None), 4)
+        self.assertEqual(len(selector.selectors), 4)
+        self.assertEqual(selector(None), 4)
         selector = (_1_() & _0_()) | (_1_() & _1_())
         self.failUnless(isinstance(selector, OrSelector))
-        self.assertEquals(len(selector.selectors), 2)
-        self.assertEquals(selector(None), 2)
+        self.assertEqual(len(selector.selectors), 2)
+        self.assertEqual(selector(None), 2)
 
     def test_search_selectors(self):
-        sel = implements('something')
-        self.assertIs(sel.search_selector(implements), sel)
+        sel = is_instance('something')
+        self.assertIs(sel.search_selector(is_instance), sel)
         csel = AndSelector(sel, Selector())
-        self.assertIs(csel.search_selector(implements), sel)
+        self.assertIs(csel.search_selector(is_instance), sel)
         csel = AndSelector(Selector(), sel)
-        self.assertIs(csel.search_selector(implements), sel)
+        self.assertIs(csel.search_selector(is_instance), sel)
 
     def test_inplace_and(self):
         selector = _1_()
         selector &= _1_()
         selector &= _1_()
-        self.assertEquals(selector(None), 3)
+        self.assertEqual(selector(None), 3)
         selector = _1_()
         selector &= _0_()
         selector &= _1_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
         selector = _0_()
         selector &= _1_()
         selector &= _1_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
         selector = _0_()
         selector &= _0_()
         selector &= _0_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
 
     def test_inplace_or(self):
         selector = _1_()
         selector |= _1_()
         selector |= _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _1_()
         selector |= _0_()
         selector |= _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _0_()
         selector |= _1_()
         selector |= _1_()
-        self.assertEquals(selector(None), 1)
+        self.assertEqual(selector(None), 1)
         selector = _0_()
         selector |= _0_()
         selector |= _0_()
-        self.assertEquals(selector(None), 0)
+        self.assertEqual(selector(None), 0)
 
 
 class ImplementsSelectorTC(CubicWebTC):
     def test_etype_priority(self):
         req = self.request()
-        cls = self.vreg['etypes'].etype_class('File')
-        anyscore = implements('Any').score_class(cls, req)
-        idownscore = implements(IDownloadable).score_class(cls, req)
+        f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
+        rset = f.as_rset()
+        anyscore = is_instance('Any')(f.__class__, req, rset=rset)
+        idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
         self.failUnless(idownscore > anyscore, (idownscore, anyscore))
-        filescore = implements('File').score_class(cls, req)
+        filescore = is_instance('File')(f.__class__, req, rset=rset)
         self.failUnless(filescore > idownscore, (filescore, idownscore))
 
     def test_etype_inheritance_no_yams_inheritance(self):
         cls = self.vreg['etypes'].etype_class('Personne')
-        self.failIf(implements('Societe').score_class(cls, self.request()))
+        self.failIf(is_instance('Societe').score_class(cls, self.request()))
+
+    def test_yams_inheritance(self):
+        cls = self.vreg['etypes'].etype_class('Transition')
+        self.assertEqual(is_instance('BaseTransition').score_class(cls, self.request()),
+                          3)
 
 
 class MatchUserGroupsTC(CubicWebTC):
@@ -185,6 +192,59 @@
         finally:
             del self.vreg[SomeAction.__registry__][SomeAction.__regid__]
 
+
+class MultiLinesRsetSelectorTC(CubicWebTC):
+    def setUp(self):
+        super(MultiLinesRsetSelectorTC, self).setUp()
+        self.req = self.request()
+        self.req.execute('INSERT CWGroup G: G name "group1"')
+        self.req.execute('INSERT CWGroup G: G name "group2"')
+        self.commit()
+        self.rset = self.req.execute('Any G WHERE G is CWGroup')
+
+    def test_default_op_in_selector(self):
+        expected = len(self.rset)
+        selector = multi_lines_rset(expected)
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected + 1)
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected - 1)
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        self.assertEqual(selector(None, self.req, None), 0)
+
+    def test_without_rset(self):
+        expected = len(self.rset)
+        selector = multi_lines_rset(expected)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected + 1)
+        self.assertEqual(selector(None, self.req, None), 0)
+        selector = multi_lines_rset(expected - 1)
+        self.assertEqual(selector(None, self.req, None), 0)
+
+    def test_with_operators(self):
+        expected = len(self.rset)
+
+        # Format     'expected', 'operator', 'assert'
+        testdata = (( expected,         eq,        1),
+                    ( expected+1,       eq,        0),
+                    ( expected-1,       eq,        0),
+                    ( expected,         le,        1),
+                    ( expected+1,       le,        1),
+                    ( expected-1,       le,        0),
+                    ( expected-1,       gt,        1),
+                    ( expected,         gt,        0),
+                    ( expected+1,       gt,        0),
+                    ( expected+1,       lt,        1),
+                    ( expected,         lt,        0),
+                    ( expected-1,       lt,        0))
+
+        for (expected, operator, assertion) in testdata:
+            selector = multi_lines_rset(expected, operator)
+            yield self.assertEqual, selector(None, self.req, self.rset), assertion
+
+
 if __name__ == '__main__':
     unittest_main()
 
--- a/test/unittest_spa2rql.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_spa2rql.py	Wed Nov 03 16:38:28 2010 +0100
@@ -37,7 +37,7 @@
 
     def _test(self, sparql, rql, args={}):
         qi = self.tr.translate(sparql)
-        self.assertEquals(qi.finalize(), (rql, args))
+        self.assertEqual(qi.finalize(), (rql, args))
 
     def XXX_test_base_01(self):
         self._test('SELECT * WHERE { }', 'Any X')
--- a/test/unittest_uilib.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_uilib.py	Wed Nov 03 16:38:28 2010 +0100
@@ -39,22 +39,22 @@
             ]
         for text, expected in data:
             got = uilib.remove_html_tags(text)
-            self.assertEquals(got, expected)
+            self.assertEqual(got, expected)
 
     def test_fallback_safe_cut(self):
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 4), u'ab c...')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 5), u'ab <a href="hello">cd</a>')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'ab &amp;...')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a> ef', 5), u'ab &amp;d...')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&igrave;d</a>', 4), u'ab ì...')
-        self.assertEquals(uilib.fallback_safe_cut(u'&amp; <a href="hello">&amp;d</a> ef', 4), u'&amp; &amp;d...')
+        self.assertEqual(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 4), u'ab c...')
+        self.assertEqual(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 5), u'ab <a href="hello">cd</a>')
+        self.assertEqual(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'ab &amp;...')
+        self.assertEqual(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a> ef', 5), u'ab &amp;d...')
+        self.assertEqual(uilib.fallback_safe_cut(u'ab <a href="hello">&igrave;d</a>', 4), u'ab ì...')
+        self.assertEqual(uilib.fallback_safe_cut(u'&amp; <a href="hello">&amp;d</a> ef', 4), u'&amp; &amp;d...')
 
     def test_lxml_safe_cut(self):
-        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div> ef', 4), u'<p>aaa</p><div>a...</div>')
-        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div> ef', 7), u'<p>aaa</p><div>aaad</div>...')
-        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div>', 7), u'<p>aaa</p><div>aaad</div>')
+        self.assertEqual(uilib.safe_cut(u'aaa<div>aaad</div> ef', 4), u'<p>aaa</p><div>a...</div>')
+        self.assertEqual(uilib.safe_cut(u'aaa<div>aaad</div> ef', 7), u'<p>aaa</p><div>aaad</div>...')
+        self.assertEqual(uilib.safe_cut(u'aaa<div>aaad</div>', 7), u'<p>aaa</p><div>aaad</div>')
         # Missing ellipsis due to space management but we don't care
-        self.assertEquals(uilib.safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'<p>ab <a href="hello">&amp;...</a></p>')
+        self.assertEqual(uilib.safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'<p>ab <a href="hello">&amp;...</a></p>')
 
     def test_cut(self):
         """tests uilib.cut() behaviour"""
@@ -65,7 +65,7 @@
             ]
         for text, expected in data:
             got = uilib.cut(text, 8)
-            self.assertEquals(got, expected)
+            self.assertEqual(got, expected)
 
     def test_text_cut(self):
         """tests uilib.text_cut() behaviour with no text"""
@@ -92,56 +92,64 @@
                 ]
         for text, expected in data:
             got = uilib.text_cut(text, 30)
-            self.assertEquals(got, expected)
+            self.assertEqual(got, expected)
 
     def test_soup2xhtml_1_1(self):
-        self.assertEquals(uilib.soup2xhtml('hop <div>', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop <div>', 'ascii'),
                           'hop <div/>')
-        self.assertEquals(uilib.soup2xhtml('<div> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('<div> hop', 'ascii'),
                           '<div> hop</div>')
-        self.assertEquals(uilib.soup2xhtml('hop <div> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop <div> hop', 'ascii'),
                           'hop <div> hop</div>')
 
     def test_soup2xhtml_1_2(self):
-        self.assertEquals(uilib.soup2xhtml('hop </div>', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop </div>', 'ascii'),
                           'hop ')
-        self.assertEquals(uilib.soup2xhtml('</div> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('</div> hop', 'ascii'),
                           '<div/> hop')
-        self.assertEquals(uilib.soup2xhtml('hop </div> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop </div> hop', 'ascii'),
                           '<div>hop </div> hop')
 
     def test_soup2xhtml_2_1(self):
-        self.assertEquals(uilib.soup2xhtml('hop <body>', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop <body>', 'ascii'),
                           'hop ')
-        self.assertEquals(uilib.soup2xhtml('<body> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('<body> hop', 'ascii'),
                           ' hop')
-        self.assertEquals(uilib.soup2xhtml('hop <body> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop <body> hop', 'ascii'),
                           'hop  hop')
 
     def test_soup2xhtml_2_2(self):
-        self.assertEquals(uilib.soup2xhtml('hop </body>', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop </body>', 'ascii'),
                           'hop ')
-        self.assertEquals(uilib.soup2xhtml('</body> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('</body> hop', 'ascii'),
                           ' hop')
-        self.assertEquals(uilib.soup2xhtml('hop </body> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop </body> hop', 'ascii'),
                           'hop  hop')
 
     def test_soup2xhtml_3_1(self):
-        self.assertEquals(uilib.soup2xhtml('hop <html>', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop <html>', 'ascii'),
                           'hop ')
-        self.assertEquals(uilib.soup2xhtml('<html> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('<html> hop', 'ascii'),
                           ' hop')
-        self.assertEquals(uilib.soup2xhtml('hop <html> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop <html> hop', 'ascii'),
                           'hop  hop')
 
     def test_soup2xhtml_3_2(self):
-        self.assertEquals(uilib.soup2xhtml('hop </html>', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop </html>', 'ascii'),
                           'hop ')
-        self.assertEquals(uilib.soup2xhtml('</html> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('</html> hop', 'ascii'),
                           ' hop')
-        self.assertEquals(uilib.soup2xhtml('hop </html> hop', 'ascii'),
+        self.assertEqual(uilib.soup2xhtml('hop </html> hop', 'ascii'),
                           'hop  hop')
 
+    def test_js(self):
+        self.assertEqual(str(uilib.js.pouet(1, "2")),
+                          'pouet(1,"2")')
+        self.assertEqual(str(uilib.js.cw.pouet(1, "2")),
+                          'cw.pouet(1,"2")')
+        self.assertEqual(str(uilib.js.cw.pouet(1, "2").pouet(None)),
+                          'cw.pouet(1,"2").pouet(null)')
+
 if __name__ == '__main__':
     unittest_main()
 
--- a/test/unittest_utils.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_utils.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -57,47 +57,48 @@
 
     def test_base(self):
         l = RepeatList(3, (1, 3))
-        self.assertEquals(l[0], (1, 3))
-        self.assertEquals(l[2], (1, 3))
-        self.assertEquals(l[-1], (1, 3))
-        self.assertEquals(len(l), 3)
+        self.assertEqual(l[0], (1, 3))
+        self.assertEqual(l[2], (1, 3))
+        self.assertEqual(l[-1], (1, 3))
+        self.assertEqual(len(l), 3)
         # XXX
-        self.assertEquals(l[4], (1, 3))
+        self.assertEqual(l[4], (1, 3))
 
         self.failIf(RepeatList(0, None))
 
     def test_slice(self):
         l = RepeatList(3, (1, 3))
-        self.assertEquals(l[0:1], [(1, 3)])
-        self.assertEquals(l[0:4], [(1, 3)]*3)
-        self.assertEquals(l[:], [(1, 3)]*3)
+        self.assertEqual(l[0:1], [(1, 3)])
+        self.assertEqual(l[0:4], [(1, 3)]*3)
+        self.assertEqual(l[:], [(1, 3)]*3)
 
     def test_iter(self):
-        self.assertEquals(list(RepeatList(3, (1, 3))),
+        self.assertEqual(list(RepeatList(3, (1, 3))),
                           [(1, 3)]*3)
 
     def test_add(self):
         l = RepeatList(3, (1, 3))
-        self.assertEquals(l + [(1, 4)], [(1, 3)]*3  + [(1, 4)])
-        self.assertEquals([(1, 4)] + l, [(1, 4)] + [(1, 3)]*3)
-        self.assertEquals(l + RepeatList(2, (2, 3)), [(1, 3)]*3 + [(2, 3)]*2)
+        self.assertEqual(l + [(1, 4)], [(1, 3)]*3  + [(1, 4)])
+        self.assertEqual([(1, 4)] + l, [(1, 4)] + [(1, 3)]*3)
+        self.assertEqual(l + RepeatList(2, (2, 3)), [(1, 3)]*3 + [(2, 3)]*2)
 
         x = l + RepeatList(2, (1, 3))
         self.assertIsInstance(x, RepeatList)
-        self.assertEquals(len(x), 5)
-        self.assertEquals(x[0], (1, 3))
+        self.assertEqual(len(x), 5)
+        self.assertEqual(x[0], (1, 3))
 
         x = l + [(1, 3)] * 2
-        self.assertEquals(x, [(1, 3)] * 5)
+        self.assertEqual(x, [(1, 3)] * 5)
 
     def test_eq(self):
-        self.assertEquals(RepeatList(3, (1, 3)),
+        self.assertEqual(RepeatList(3, (1, 3)),
                           [(1, 3)]*3)
 
     def test_pop(self):
         l = RepeatList(3, (1, 3))
         l.pop(2)
-        self.assertEquals(l, [(1, 3)]*2)
+        self.assertEqual(l, [(1, 3)]*2)
+
 
 class SizeConstrainedListTC(TestCase):
 
@@ -105,7 +106,7 @@
         l = SizeConstrainedList(10)
         for i in xrange(12):
             l.append(i)
-        self.assertEquals(l, range(2, 12))
+        self.assertEqual(l, range(2, 12))
 
     def test_extend(self):
         testdata = [(range(5), range(5)),
@@ -115,29 +116,44 @@
         for extension, expected in testdata:
             l = SizeConstrainedList(10)
             l.extend(extension)
-            yield self.assertEquals, l, expected
+            yield self.assertEqual, l, expected
+
 
 class JSONEncoderTC(TestCase):
     def setUp(self):
         if json is None:
-            self.skip('json not available')
+            self.skipTest('json not available')
 
     def encode(self, value):
         return json.dumps(value, cls=CubicWebJsonEncoder)
 
     def test_encoding_dates(self):
-        self.assertEquals(self.encode(datetime.datetime(2009, 9, 9, 20, 30)),
+        self.assertEqual(self.encode(datetime.datetime(2009, 9, 9, 20, 30)),
                           '"2009/09/09 20:30:00"')
-        self.assertEquals(self.encode(datetime.date(2009, 9, 9)),
+        self.assertEqual(self.encode(datetime.date(2009, 9, 9)),
                           '"2009/09/09"')
-        self.assertEquals(self.encode(datetime.time(20, 30)),
+        self.assertEqual(self.encode(datetime.time(20, 30)),
                           '"20:30:00"')
 
     def test_encoding_decimal(self):
-        self.assertEquals(self.encode(decimal.Decimal('1.2')), '1.2')
+        self.assertEqual(self.encode(decimal.Decimal('1.2')), '1.2')
+
+    def test_encoding_bare_entity(self):
+        e = Entity(None)
+        e['pouet'] = 'hop'
+        e.eid = 2
+        self.assertEqual(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.assertEqual(json.loads(self.encode([e])),
+                          [{'pouet': 'hop', 'eid': 2}])
 
     def test_encoding_unknown_stuff(self):
-        self.assertEquals(self.encode(TestCase), 'null')
+        self.assertEqual(self.encode(TestCase), 'null')
 
 
 if __name__ == '__main__':
--- a/test/unittest_vregistry.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/test/unittest_vregistry.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
 
@@ -50,27 +48,32 @@
         self.vreg.load_file(join(BASE, 'entities', '__init__.py'), 'cubicweb.entities.__init__')
         self.vreg.load_file(join(WEBVIEWSDIR, 'idownloadable.py'), 'cubicweb.web.views.idownloadable')
         self.vreg.load_file(join(WEBVIEWSDIR, 'primary.py'), 'cubicweb.web.views.primary')
-        self.assertEquals(len(self.vreg['views']['primary']), 2)
+        self.assertEqual(len(self.vreg['views']['primary']), 2)
         self.vreg.initialization_completed()
-        self.assertEquals(len(self.vreg['views']['primary']), 1)
+        self.assertEqual(len(self.vreg['views']['primary']), 1)
 
 
     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)
+        self.assertEqual(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')
@@ -81,7 +84,7 @@
     def test_property_default_overriding(self):
         # see data/views.py
         from cubicweb.web.views.xmlrss import RSSIconBox
-        self.assertEquals(self.vreg.property_info(RSSIconBox._cwpropkey('visible'))['default'], True)
+        self.assertEqual(self.vreg.property_info(RSSIconBox._cwpropkey('visible'))['default'], True)
 
 if __name__ == '__main__':
     unittest_main()
--- a/toolsutils.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/toolsutils.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""some utilities for cubicweb tools
+"""some utilities for cubicweb command line tools"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 # XXX move most of this in logilab.common (shellutils ?)
@@ -33,8 +32,7 @@
     def symlink(*args):
         raise NotImplementedError
 
-from logilab.common.clcommands import Command as BaseCommand, \
-     main_run as base_main_run
+from logilab.common.clcommands import Command as BaseCommand
 from logilab.common.compat import any
 from logilab.common.shellutils import ASK
 
@@ -196,30 +194,6 @@
                 config_file, ex)
     return config
 
-def env_path(env_var, default, name, checkexists=True):
-    """get a path specified in a variable or using the default value and return
-    it.
-
-    :type env_var: str
-    :param env_var: name of an environment variable
-
-    :type default: str
-    :param default: default value if the environment variable is not defined
-
-    :type name: str
-    :param name: the informal name of the path, used for error message
-
-    :rtype: str
-    :return: the value of the environment variable or the default value
-
-    :raise `ConfigurationError`: if the returned path does not exist
-    """
-    path = environ.get(env_var, default)
-    if checkexists and not exists(path):
-        raise ConfigurationError('%s directory %s doesn\'t exist' % (name, path))
-    return abspath(path)
-
-
 
 _HDLRS = {}
 
@@ -260,17 +234,6 @@
         sys.exit(1)
 
 
-def main_run(args, doc):
-    """command line tool"""
-    try:
-        base_main_run(args, doc, copyright=None)
-    except ConfigurationError, err:
-        print 'ERROR: ', err
-        sys.exit(1)
-    except ExecutionError, err:
-        print err
-        sys.exit(2)
-
 CONNECT_OPTIONS = (
     ("user",
      {'short': 'u', 'type' : 'string', 'metavar': '<user>',
--- a/uilib.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/uilib.py	Wed Nov 03 16:38:28 2010 +0100
@@ -31,6 +31,8 @@
 from logilab.mtconverter import xml_escape, html_unescape
 from logilab.common.date import ustrftime
 
+from cubicweb.utils import json_dumps
+
 
 def rql_for_eid(eid):
     """return the rql query necessary to fetch entity with the given eid.  This
@@ -44,6 +46,11 @@
     """
     return 'Any X WHERE X eid %s' % eid
 
+def eid_param(name, eid):
+    assert eid is not None
+    if eid is None:
+        eid = ''
+    return '%s:%s' % (name, eid)
 
 def printable_value(req, attrtype, value, props=None, displaytime=True):
     """return a displayable value (i.e. unicode string)"""
@@ -228,6 +235,54 @@
 
 # HTML generation helper functions ############################################
 
+class _JSId(object):
+    def __init__(self, id, parent=None):
+        self.id = id
+        self.parent = parent
+    def __unicode__(self):
+        if self.parent:
+            return u'%s.%s' % (self.parent, self.id)
+        return unicode(self.id)
+    def __str__(self):
+        return unicode(self).encode('utf8')
+    def __getattr__(self, attr):
+        return _JSId(attr, self)
+    def __call__(self, *args):
+        return _JSCallArgs(args, self)
+
+class _JSCallArgs(_JSId):
+    def __init__(self, args, parent=None):
+        assert isinstance(args, tuple)
+        self.args = args
+        self.parent = parent
+    def __unicode__(self):
+        args = u','.join(json_dumps(arg) for arg in self.args)
+        if self.parent:
+            return u'%s(%s)' % (self.parent, args)
+        return args
+
+class _JS(object):
+    def __getattr__(self, attr):
+        return _JSId(attr)
+
+"""magic object to return strings suitable to call some javascript function with
+the given arguments (which should be correctly typed).
+
+>>> str(js.pouet(1, "2"))
+'pouet(1,"2")'
+>>> str(js.cw.pouet(1, "2"))
+'cw.pouet(1,"2")'
+>>> str(js.cw.pouet(1, "2").pouet(None))
+'cw.pouet(1,"2").pouet(null)')
+"""
+js = _JS()
+
+def domid(string):
+    """return a valid DOM id from a string (should also be usable in jQuery
+    search expression...)
+    """
+    return string.replace('.', '_').replace('-', '_')
+
 HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
                               'img', 'area', 'input', 'col'))
 
--- a/utils.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/utils.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""Some utilities for CubicWeb server/clients.
+"""Some utilities for CubicWeb server/clients."""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import os
@@ -121,6 +120,9 @@
     def __init__(self, size, item):
         self._size = size
         self._item = item
+    def __repr__(self):
+        return '<cubicweb.utils.RepeatList at %s item=%s size=%s>' % (
+            id(self), self._item, self._size)
     def __len__(self):
         return self._size
     def __nonzero__(self):
@@ -129,6 +131,9 @@
         return repeat(self._item, self._size)
     def __getitem__(self, index):
         return self._item
+    def __delitem__(self, idc):
+        assert self._size > 0
+        self._size -= 1
     def __getslice__(self, i, j):
         # XXX could be more efficient, but do we bother?
         return ([self._item] * self._size)[i:j]
@@ -178,6 +183,11 @@
     javascripts and stylesheets
     """
     js_unload_code = u'jQuery(window).unload(unloadPageData);'
+    # Making <script> tag content work properly with all possible
+    # content-types (xml/html) and all possible browsers is very
+    # tricky, see http://www.hixie.ch/advocacy/xhtml for an in-depth discussion
+    xhtml_safe_script_opening = u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n'
+    xhtml_safe_script_closing = u'\n//--><!]]></script>'
 
     def __init__(self):
         super(HTMLHead, self).__init__()
@@ -251,14 +261,14 @@
         w = self.write
         # 1/ variable declaration if any
         if self.jsvars:
-            w(u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n')
+            w(self.xhtml_safe_script_opening)
             for var, value, override in self.jsvars:
                 vardecl = u'%s = %s;' % (var, json.dumps(value))
                 if not override:
                     vardecl = (u'if (typeof %s == "undefined") {%s}' %
                                (var, vardecl))
                 w(vardecl + u'\n')
-            w(u'//--><!]]></script>\n')
+            w(self.xhtml_safe_script_closing)
         # 2/ css files
         for cssfile, media in self.cssfiles:
             w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
@@ -276,9 +286,9 @@
               xml_escape(jsfile))
         # 5/ post inlined scripts (i.e. scripts depending on other JS files)
         if self.post_inlined_scripts:
-            w(u'<script type="text/javascript">\n')
+            w(self.xhtml_safe_script_opening)
             w(u'\n\n'.join(self.post_inlined_scripts))
-            w(u'\n</script>\n')
+            w(self.xhtml_safe_script_closing)
         header = super(HTMLHead, self).getvalue()
         if skiphead:
             return header
@@ -324,36 +334,28 @@
 
 try:
     # may not be there if cubicweb-web not installed
-    if sys.version_info < (2,6):
+    if sys.version_info < (2, 6):
         import simplejson as json
     else:
         import json
 except ImportError:
-    pass
+    json_dumps = None
+
 else:
+    from logilab.common.date import ustrftime
 
     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')
+                return ustrftime(obj, '%Y/%m/%d %H:%M:%S')
             elif isinstance(obj, datetime.date):
-                return obj.strftime('%Y/%m/%d')
+                return ustrftime(obj, '%Y/%m/%d')
             elif isinstance(obj, datetime.time):
                 return obj.strftime('%H:%M:%S')
             elif isinstance(obj, datetime.timedelta):
@@ -367,6 +369,9 @@
                 # just return None in those cases.
                 return None
 
+    def json_dumps(value):
+        return json.dumps(value, cls=CubicWebJsonEncoder)
+
 
 @deprecated('[3.7] merge_dicts is deprecated')
 def merge_dicts(dict1, dict2):
@@ -379,7 +384,7 @@
 _THIS_MOD_NS = globals()
 for funcname in ('date_range', 'todate', 'todatetime', 'datetime2ticks',
                  'days_in_month', 'days_in_year', 'previous_month',
-                 'next_month', 'first_day', 'last_day', 'ustrftime',
+                 'next_month', 'first_day', 'last_day',
                  'strptime'):
     msg = '[3.6] %s has been moved to logilab.common.date' % funcname
     _THIS_MOD_NS[funcname] = deprecated(msg)(getattr(date, funcname))
--- a/view.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/view.py	Wed Nov 03 16:38:28 2010 +0100
@@ -23,7 +23,6 @@
 from cStringIO import StringIO
 from warnings import warn
 
-from cubicweb.utils import json
 from logilab.common.deprecation import deprecated
 from logilab.mtconverter import xml_escape
 
@@ -33,7 +32,9 @@
 from cubicweb.selectors import yes, non_final_entity, nonempty_rset, none_rset
 from cubicweb.appobject import AppObject
 from cubicweb.utils import UStringIO, HTMLStream
+from cubicweb.uilib import domid, js
 from cubicweb.schema import display_name
+from cubicweb.vregistry import classid
 
 # robots control
 NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
@@ -334,7 +335,8 @@
         """ return the url of the entity creation form for a given entity type"""
         return self._cw.build_url('add/%s' % etype, **kwargs)
 
-    def field(self, label, value, row=True, show_label=True, w=None, tr=True, table=False):
+    def field(self, label, value, row=True, show_label=True, w=None, tr=True,
+              table=False):
         """read-only field"""
         if w is None:
             w = self.w
@@ -366,6 +368,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
@@ -493,12 +506,11 @@
 
     def build_update_js_call(self, cbname, msg):
         rql = self.cw_rset.printable_rql()
-        return "javascript:userCallbackThenUpdateUI('%s', '%s', %s, %s, '%s', '%s')" % (
-            cbname, self.id, json.dumps(rql), json.dumps(msg),
-            self.__registry__, self.div_id())
+        return "javascript: %s" % js.userCallbackThenUpdateUI(
+            cbname, self.__regid__, rql, msg, self.__registry__, self.domid)
 
     def build_reload_js_call(self, cbname, msg):
-        return "javascript:userCallbackThenReloadPage('%s', %s)" % (cbname, json.dumps(msg))
+        return "javascript: %s" % js.userCallbackThenReloadPage(cbname, msg)
 
     build_js = build_update_js_call # expect updatable component by default
 
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/vregistry.py	Wed Nov 03 16:38:28 2010 +0100
@@ -44,7 +44,8 @@
 
 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 +84,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,)
@@ -231,17 +222,14 @@
             elif appobjectscore > 0 and appobjectscore == score:
                 winners.append(appobject)
         if winners is None:
-            raise NoSelectableObject('args: %s\nkwargs: %s %s'
-                                     % (args, kwargs.keys(),
-                                        [repr(v) for v in appobjects]))
+            raise NoSelectableObject(args, kwargs, appobjects)
         if len(winners) > 1:
-            # log in production environement, error while debugging
-            if self.config.debugmode:
-                raise Exception('select ambiguity, args: %s\nkwargs: %s %s'
-                                % (args, kwargs.keys(),
-                                   [repr(v) for v in winners]))
-            self.error('select ambiguity, args: %s\nkwargs: %s %s',
-                       args, kwargs.keys(), [repr(v) for v in winners])
+            # log in production environement / test, error while debugging
+            msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
+            if self.config.debugmode or self.config.mode == 'test':
+                # raise bare exception in debug mode
+                raise Exception(msg % (winners, args, kwargs.keys()))
+            self.error(msg, winners, args, kwargs.keys())
         # return the result of calling the appobject
         return winners[0](*args, **kwargs)
 
@@ -382,7 +370,7 @@
         for registryname in class_registries(obj, registryname):
             registry = self.setdefault(registryname)
             registry.register(obj, oid=oid, clear=clear)
-            self.debug('registered appobject %s in registry %s with id %s',
+            self.debug('register %s in %s[\'%s\']',
                        vname, registryname, oid or class_regid(obj))
         self._loadedmods.setdefault(obj.__module__, {})[classid(obj)] = obj
 
@@ -405,6 +393,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
@@ -491,7 +480,7 @@
         - first ensure parent classes are already registered
 
         - class with __abstract__ == True in their local dictionnary or
-          with a name starting starting by an underscore are not registered
+          with a name starting with an underscore are not registered
 
         - appobject class needs to have __registry__ and __regid__ attributes
           set to a non empty string to be registered.
--- a/web/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -17,26 +17,20 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """CubicWeb web client core. You'll need a apache-modpython or twisted
 publisher to get a full CubicWeb web application
-
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-import sys
-if sys.version_info < (2,6):
-    import simplejson as json
-else:
-    import json
-
-dumps = json.dumps
-
 from urllib import quote as urlquote
 
 from logilab.common.deprecation import deprecated
 
 from cubicweb.web._exceptions import *
-from cubicweb.utils import CubicWebJsonEncoder
+from cubicweb.utils import json_dumps
+from cubicweb.uilib import eid_param
+
+dumps = deprecated('[3.9] use cubicweb.utils.json_dumps instead of dumps')(json_dumps)
 
 INTERNAL_FIELD_VALUE = '__cubicweb_internal_field__'
 
@@ -51,13 +45,6 @@
     NO  = (_('no'), None)
 
 
-def eid_param(name, eid):
-    assert eid is not None
-    if eid is None:
-        eid = ''
-    return '%s:%s' % (name, eid)
-
-
 from logging import getLogger
 LOGGER = getLogger('cubicweb.web')
 
@@ -65,9 +52,6 @@
 FACETTES = set()
 
 
-def json_dumps(value):
-    return dumps(value, cls=CubicWebJsonEncoder)
-
 def jsonize(function):
     def newfunc(*args, **kwargs):
         value = function(*args, **kwargs)
@@ -77,7 +61,7 @@
             return json_dumps(repr(value))
     return newfunc
 
-@deprecated('[3.4] use req.build_ajax_replace_url() instead')
+@deprecated('[3.4] use req.ajax_replace_url() instead')
 def ajax_replace_url(nodeid, rql, vid=None, swap=False, **extraparams):
     """builds a replacePageChunk-like url
     >>> ajax_replace_url('foo', 'Person P')
--- a/web/_exceptions.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/_exceptions.py	Wed Nov 03 16:38:28 2010 +0100
@@ -16,12 +16,12 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""exceptions used in the core of the CubicWeb web application
+"""exceptions used in the core of the CubicWeb web application"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb._exceptions import *
+from cubicweb.utils import json_dumps
 
 class PublishException(CubicWebException):
     """base class for publishing related exception"""
@@ -66,8 +66,7 @@
         self.reason = reason
 
     def dumps(self):
-        from cubicweb.web import json
-        return json.dumps({'reason': self.reason})
+        return json_dumps({'reason': self.reason})
 
 class LogOut(PublishException):
     """raised to ask for deauthentication of a logged in user"""
--- a/web/action.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/action.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/application.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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"
@@ -196,7 +195,7 @@
         if no session id is found, open a new session for the connected user
         or request authentification as needed
 
-        :raise Redirect: if authentication has occured and succeed
+        :raise Redirect: if authentication has occurred and succeed
         """
         cookie = req.get_cookie()
         try:
@@ -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)
@@ -370,7 +369,8 @@
         """
         path = path or 'view'
         # don't log form values they may contains sensitive information
-        self.info('publish "%s" (form params: %s)', path, req.form.keys())
+        self.info('publish "%s" (%s, form params: %s)',
+                  path, req.session.sessionid, req.form.keys())
         # remove user callbacks on a new request (except for json controllers
         # to avoid callbacks being unregistered before they could be called)
         tstart = clock()
@@ -430,11 +430,12 @@
                 self.validation_error_handler(req, ex)
             except (Unauthorized, BadRQLQuery, RequestError), ex:
                 self.error_handler(req, ex, tb=False)
-            except Exception, ex:
+            except BaseException, ex:
                 self.error_handler(req, ex, tb=True)
             except:
                 self.critical('Catch all triggered!!!')
                 self.exception('this is what happened')
+                result = 'oops'
         finally:
             if req.cnx and not commited:
                 try:
--- a/web/box.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/box.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""abstract box classes for CubicWeb web client
+"""abstract box classes for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -26,10 +25,11 @@
 from cubicweb import Unauthorized, role as get_role, target as get_target
 from cubicweb.schema import display_name
 from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
-                                match_context_prop, partial_has_related_entities)
+                                match_context_prop, partial_relation_possible,
+                                partial_has_related_entities)
 from cubicweb.view import View, ReloadableMixIn
-
-from cubicweb.web import INTERNAL_FIELD_VALUE
+from cubicweb.uilib import domid, js
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
                                       RawBoxItem, BoxSeparator)
 from cubicweb.web.action import UnregisteredAction
@@ -143,7 +143,7 @@
 
     def to_display_rql(self):
         assert self.rql is not None, self.__regid__
-        return (self.rql, {'x': self._cw.user.eid}, 'x')
+        return (self.rql, {'x': self._cw.user.eid})
 
 
 class EntityBoxTemplate(BoxTemplate):
@@ -165,7 +165,8 @@
         role = get_role(self)
         self.w(u'<div class="sideBox">')
         self.wview('sidebox', entity.related(self.rtype, role, limit=limit),
-                   title=display_name(self._cw, self.rtype, role))
+                   title=display_name(self._cw, self.rtype, role,
+                                      context=entity.__regid__))
         self.w(u'</div>')
 
 
@@ -180,7 +181,8 @@
     def cell_call(self, row, col, view=None, **kwargs):
         self._cw.add_js('cubicweb.ajax.js')
         entity = self.cw_rset.get_entity(row, col)
-        box = SideBoxWidget(display_name(self._cw, self.rtype), self.__regid__)
+        title = display_name(self._cw, self.rtype, get_role(self), context=entity.__regid__)
+        box = SideBoxWidget(title, self.__regid__)
         related = self.related_boxitems(entity)
         unrelated = self.unrelated_boxitems(entity)
         box.extend(related)
@@ -224,8 +226,8 @@
         """returns the list of unrelated entities, using the entity's
         appropriate vocabulary function
         """
-        skip = set(e.eid for e in entity.related(self.rtype, get_role(self),
-                                                 entities=True))
+        skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self),
+                                                          entities=True))
         skip.add(None)
         skip.add(INTERNAL_FIELD_VALUE)
         filteretype = getattr(self, 'etype', None)
@@ -241,3 +243,92 @@
                     entities.append(entity)
         return entities
 
+
+class AjaxEditRelationBoxTemplate(EntityBoxTemplate):
+    __select__ = EntityBoxTemplate.__select__ & partial_relation_possible()
+
+    # view used to display related entties
+    item_vid = 'incontext'
+    # values separator when multiple values are allowed
+    separator = ','
+    # msgid of the message to display when some new relation has been added/removed
+    added_msg = None
+    removed_msg = None
+
+    # class attributes below *must* be set in concret classes (additionaly to
+    # rtype / role [/ target_etype]. They should correspond to js_* methods on
+    # the json controller
+
+    # function(eid)
+    # -> expected to return a list of values to display as input selector
+    #    vocabulary
+    fname_vocabulary = None
+
+    # function(eid, value)
+    # -> handle the selector's input (eg create necessary entities and/or
+    # relations). If the relation is multiple, you'll get a list of value, else
+    # a single string value.
+    fname_validate = None
+
+    # function(eid, linked entity eid)
+    # -> remove the relation
+    fname_remove = None
+
+    def cell_call(self, row, col, **kwargs):
+        req = self._cw
+        entity = self.cw_rset.get_entity(row, col)
+        related = entity.related(self.rtype, self.role)
+        rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
+        if self.role == 'subject':
+            mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid)
+            maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid)
+        else:
+            mayadd = rdef.has_perm(req, 'add', toeid=entity.eid)
+            maydel = rdef.has_perm(req, 'delete', toeid=entity.eid)
+        if not (related or mayadd):
+            return
+        if mayadd or maydel:
+            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
+        _ = req._
+        w = self.w
+        divid = domid(self.__regid__) + unicode(entity.eid)
+        w(u'<div class="sideBox" id="%s%s">' % (domid(self.__regid__), entity.eid))
+        w(u'<div class="sideBoxTitle"><span>%s</span></div>' %
+               rdef.rtype.display_name(req, self.role, context=entity.__regid__))
+        w(u'<div class="sideBox"><div class="sideBoxBody">')
+        if related:
+            w(u'<table>')
+            for rentity in related.entities():
+                # for each related entity, provide a link to remove the relation
+                subview = rentity.view(self.item_vid)
+                if maydel:
+                    jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
+                        self.__regid__, entity.eid, rentity.eid,
+                        self.fname_remove,
+                        self.removed_msg and _(self.removed_msg)))
+                    w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
+                      '<td class="tagged">%s</td></tr>' % (xml_escape(jscall),
+                                                           subview))
+                else:
+                    w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
+            w(u'</table>')
+        else:
+            w(_('no related entity'))
+        if mayadd:
+            req.add_js('jquery.autocomplete.js')
+            req.add_css('jquery.autocomplete.css')
+            multiple = rdef.role_cardinality(self.role) in '*+'
+            w(u'<table><tr><td>')
+            jscall = unicode(js.ajaxBoxShowSelector(
+                self.__regid__, entity.eid, self.fname_vocabulary,
+                self.fname_validate, self.added_msg and _(self.added_msg),
+                _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
+                multiple and self.separator))
+            w('<a class="button sglink" href="javascript: %s">%s</a>' % (
+                xml_escape(jscall),
+                multiple and _('add_relation') or _('update_relation')))
+            w(u'</td><td>')
+            w(u'<div id="%sHolder"></div>' % divid)
+            w(u'</td></tr></table>')
+        w(u'</div>\n')
+        w(u'</div></div>\n')
--- a/web/component.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/component.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,10 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""abstract component class and base components definition for CubicWeb web client
+"""abstract component class and base components definition for CubicWeb web
+client
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -25,7 +26,8 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import role
-from cubicweb.web import json
+from cubicweb.utils import json_dumps
+from cubicweb.uilib import js
 from cubicweb.view import Component
 from cubicweb.selectors import (
     paginated_rset, one_line_rset, primary_view, match_context_prop,
@@ -61,9 +63,15 @@
     context = 'navcontentbottom'
 
     def call(self, view=None):
-        return self.cell_call(0, 0, view=view)
+        if self.cw_rset is None:
+            self.entity_call(self.cw_extra_kwargs.pop('entity'))
+        else:
+            self.cell_call(0, 0, view=view)
 
     def cell_call(self, row, col, view=None):
+        self.entity_call(self.cw_rset.get_entity(row, col), view=view)
+
+    def entity_call(self, entity, view=None):
         raise NotImplementedError()
 
 
@@ -126,23 +134,31 @@
         if self.stop_param in params:
             del params[self.stop_param]
 
-    def page_url(self, path, params, start, stop):
+    def page_url(self, path, params, start=None, stop=None):
         params = dict(params)
-        params.update({self.start_param : start,
-                       self.stop_param : stop,})
+        if start is not None:
+            params[self.start_param] = start
+        if stop is not None:
+            params[self.stop_param] = stop
         view = self.cw_extra_kwargs.get('view')
         if view is not None and hasattr(view, 'page_navigation_url'):
             url = view.page_navigation_url(self, path, params)
         elif path == 'json':
-            rql = params.pop('rql', self.cw_rset.printable_rql())
-            # latest 'true' used for 'swap' mode
-            url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
-                json.dumps(params.get('divid', 'pageContent')),
-                json.dumps(rql), json.dumps(params.pop('vid', None)), json.dumps(params))
+            url = self.ajax_page_url(**params)
         else:
             url = self._cw.build_url(path, **params)
+        # XXX hack to avoid opening a new page containing the evaluation of the
+        # js expression on ajax call
+        if url.startswith('javascript:'):
+            url += '; noop();'
         return url
 
+    def ajax_page_url(self, **params):
+        divid = params.setdefault('divid', 'pageContent')
+        params['rql'] = self.cw_rset.printable_rql()
+        return "javascript: $(%s).loadxhtml('json', %s, 'get', 'swap')" % (
+            json_dumps('#'+divid), js.ajaxFuncArgs('view', params))
+
     def page_link(self, path, params, start, stop, content):
         url = xml_escape(self.page_url(path, params, start, stop))
         if start == self.starting_from:
--- a/web/controller.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/controller.py	Wed Nov 03 16:38:28 2010 +0100
@@ -23,6 +23,7 @@
 
 from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
+from cubicweb.mail import format_mail
 from cubicweb.web import LOGGER, Redirect, RequestError
 
 
@@ -79,18 +80,20 @@
 
     # generic methods useful for concrete implementations ######################
 
-    def process_rql(self, rql):
+    def process_rql(self):
         """execute rql if specified"""
-        # XXX assigning to self really necessary?
-        self.cw_rset = None
+        req = self._cw
+        rql = req.form.get('rql')
         if rql:
-            self._cw.ensure_ro_rql(rql)
+            req.ensure_ro_rql(rql)
             if not isinstance(rql, unicode):
-                rql = unicode(rql, self._cw.encoding)
-            pp = self._cw.vreg['components'].select_or_none('magicsearch', self._cw)
+                rql = unicode(rql, req.encoding)
+            pp = req.vreg['components'].select_or_none('magicsearch', req)
             if pp is not None:
-                self.cw_rset = pp.process_query(rql)
-        return self.cw_rset
+                return pp.process_query(rql)
+        if 'eid' in req.form:
+            return req.eid_rset(req.form['eid'])
+        return None
 
     def notify_edited(self, entity):
         """called by edit_entity() to notify which entity is edited"""
@@ -104,6 +107,16 @@
         view.set_http_cache_headers()
         self._cw.validate_cache()
 
+    def sendmail(self, recipient, subject, body):
+        senderemail = self._cw.user.cw_adapt_to('IEmailable').get_email()
+        msg = format_mail({'email' : senderemail,
+                           'name' : self._cw.user.dc_title(),},
+                          [recipient], body, subject)
+        if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
+            msg = self._cw._('could not connect to the SMTP server')
+            url = self._cw.build_url(__message=msg)
+            raise Redirect(url)
+
     def reset(self):
         """reset form parameters and redirect to a view determinated by given
         parameters
Binary file web/data/actionBoxHeader.png has changed
Binary file web/data/boxHeader.png has changed
Binary file web/data/button.png has changed
--- a/web/data/cubicweb.acl.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.acl.css	Wed Nov 03 16:38:28 2010 +0100
@@ -6,78 +6,35 @@
  */
 
 /******************************************************************************/
-/* security edition form (views/management.py)                                */
+/* security edition form (views/management.py)   web/views/schema.py          */
 /******************************************************************************/
 
 h2.schema{
- background : #ff7700;
- color: #fff;
- font-weight: bold;
- padding : 0.1em 0.3em;
+ color: %(aColor)s;
 }
 
-
-h3.schema{
+table.schemaInfo td a.users{
+ color : #00CC33;
  font-weight: bold;
 }
 
-h4 a,
-h4 a:link,
-h4 a:visited{
- color:#000;
- }
-
-table.schemaInfo {
-  margin: 1em 0em;
-  text-align: left;
-  border: 1px solid black;
-  border-collapse: collapse;
-  width:100%;
-}
-
-table.schemaInfo th,
-table.schemaInfo td {
-  padding: .3em .5em;
-  border: 1px solid grey;
-  width:33%;
-}
-
-
-table.schemaInfo tr th {
- padding: 0.2em 0px 0.2em 5px;
- background-image:none;
- background-color:#dfdfdf;
-}
-
-table.schemaInfo thead tr {
-  border: 1px solid #dfdfdf;
-}
-
-table.schemaInfo td {
-  padding: 3px 10px 3px 5px;
-
-}
-
-a.users{
- color : #00CC33;
- font-weight: bold }
-
-a.guests{
- color :  #ff7700;
+table.schemaInfo td a.guests{
+ color:  #ff7700;
  font-weight: bold;
 }
 
-a.owners{
- color : #8b0000;
+table.schemaInfo td a.owners{
+ color: #8b0000;
  font-weight: bold;
 }
 
-a.managers{
+table.schemaInfo td a.managers{
  color: #000000;
+ font-weight: bold;
 }
 
 .discret,
-a.grey{
+table.schemaInfo td a.grey{
  color:#666;
 }
 
@@ -86,39 +43,9 @@
 }
 
 .red{
- color :  #ff7700;
+ color:  #ff7700;
  }
 
 div#schema_security{
  width:100%;
- }
-/******************************************************************************/
-/* user groups edition form (views/euser.py)                                  */
-/******************************************************************************/
-
-table#groupedit {
-  margin: 1ex 1em;
-  text-align: left;
-  border: 1px solid black;
-  border-collapse: collapse;
-}
-
-table#groupedit th,
-table#groupedit td {
-  padding: 0.5em 1em;
-}
-
-table#groupedit tr {
-  border-bottom: 1px solid black;
-}
-
-table#groupedit tr.nogroup {
-  border: 1px solid red;
-  margin: 1px;
-}
-
-table#groupedit td {
-  text-align: center;
-  padding: 0.5em;
-}
-
+ }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.ajax.box.js	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,81 @@
+/**
+ * Functions for ajax boxes.
+ *
+ *  :organization: Logilab
+ *  :copyright: 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
+ */
+
+function ajaxBoxValidateSelectorInput(boxid, eid, separator, fname, msg) {
+    var holderid = cw.utils.domid(boxid) + eid + 'Holder';
+    var value = $('#' + holderid + 'Input').val();
+    if (separator) {
+        value = $.map(value.split(separator), jQuery.trim);
+    }
+    var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value));
+    d.addCallback(function() {
+            $('#' + holderid).empty();
+            var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            if (msg) {
+                document.location.hash = '#header';
+                updateMessage(msg);
+            }
+        });
+}
+
+function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) {
+    var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid));
+    d.addCallback(function() {
+            var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            if (msg) {
+                document.location.hash = '#header';
+                updateMessage(msg);
+            }
+    });
+}
+
+function ajaxBoxShowSelector(boxid, eid,
+                             unrelfname,
+                             addfname, msg,
+                             oklabel, cancellabel,
+                             separator) {
+    var holderid = cw.utils.domid(boxid) + eid + 'Holder';
+    var holder = $('#' + holderid);
+    if (holder.children().length) {
+        holder.empty();
+    }
+    else {
+        var inputid = holderid + 'Input';
+        var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
+        deferred.addCallback(function (unrelated) {
+            var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
+            holder.append(input).show();
+            $input = $(input);
+            $input.keypress(function (event) {
+                if (event.keyCode == KEYS.KEY_ENTER) {
+                    // XXX not very user friendly: we should test that the suggestions
+                    //     aren't visible anymore
+                    ajaxBoxValidateSelectorInput(boxid, eid, separator, addfname, msg);
+                }
+            });
+            var buttons = DIV({'class' : "sgformbuttons"},
+                              A({'href' : "javascript: noop();",
+                                 'onclick' : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput',
+                                                                  boxid, eid, separator, addfname, msg)},
+                                  oklabel),
+                              ' / ',
+                              A({'href' : "javascript: noop();",
+                                 'onclick' : '$("#' + holderid + '").empty()'},
+                                  cancellabel));
+            holder.append(buttons);
+            $input.autocomplete(unrelated, {
+                multiple: separator,
+                max: 15
+            });
+            $input.focus();
+        });
+    }
+}
--- a/web/data/cubicweb.ajax.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.ajax.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,33 +1,128 @@
-/*
- *  :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) && 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 && this._error) {
+            callback.apply(null, [this._error, this._req]);
+        }
+        else {
+            this._onFailure.push([callback, cw.utils.sliceList(arguments, 1)]);
+        }
+        return this;
+    },
+
+    success: function(result) {
+        this._result = result;
+        try {
+            for (var i = 0; i < this._onSuccess.length; i++) {
+                var callback = this._onSuccess[i][0];
+                var args = [result, this._req];
+                jQuery.merge(args, this._onSuccess[i][1]);
+                callback.apply(null, args);
+            }
+        } catch(error) {
+            this.error(this.xhr, null, error);
+        }
+    },
+
+    error: function(xhr, status, error) {
+        this._error = error;
+        for (var i = 0; i < this._onFailure.length; i++) {
+            var callback = this._onFailure[i][0];
+            var args = [error, this._req];
+            jQuery.merge(args, this._onFailure[i][1]);
+            callback.apply(null, args);
+        }
+    }
+
+});
+
 
 var JSON_BASE_URL = baseuri() + 'json?';
 
-function _loadAjaxHtmlHead(node, head, tag, srcattr) {
-    var loaded = [];
+//============= utility function handling remote calls responses. ==============//
+function _loadAjaxHtmlHead($node, $head, tag, srcattr) {
     var jqtagfilter = tag + '[' + srcattr + ']';
-    jQuery('head ' + jqtagfilter).each(function(i) {
-        loaded.push(this.getAttribute(srcattr));
-    });
-    node.find(tag).each(function(i) {
-        if (this.getAttribute(srcattr)) {
-            if (!loaded.contains(this.getAttribute(srcattr))) {
-                jQuery(this).appendTo(head);
+    if (cw['loaded_'+srcattr] === undefined) {
+        cw['loaded_'+srcattr] = [];
+        var loaded = cw['loaded_'+srcattr];
+        jQuery('head ' + jqtagfilter).each(function(i) {
+                   loaded.push(this.getAttribute(srcattr));
+               });
+    } else {
+        var loaded = cw['loaded_'+srcattr];
+    }
+    $node.find(tag).each(function(i) {
+        var url = this.getAttribute(srcattr);
+        if (url) {
+            if (jQuery.inArray(url, loaded) == -1) {
+                // take care to <script> tags: jQuery append method script nodes
+                // don't appears in the DOM (See comments on
+                // http://api.jquery.com/append/), which cause undesired
+                // duplicated load in our case. After trying to use bare DOM api
+                // to avoid this, we switched to handle a list of already loaded
+                // stuff ourselves, since bare DOM api gives bug with the
+                // server-response event, since we loose control on when the
+                // script is loaded (jQuery load it immediatly).
+                loaded.push(url);
+                jQuery(this).appendTo($head);
             }
         } else {
-            jQuery(this).appendTo(head);
+            jQuery(this).appendTo($head);
         }
     });
-    node.find(jqtagfilter).remove();
+    $node.find(jqtagfilter).remove();
 }
 
-/*
+/**
+ * .. function:: function loadAjaxHtmlHead(response)
+ *
  * inspect dom response (as returned by getDomFromResponse), search for
  * a <div class="ajaxHtmlHead"> node and put its content into the real
  * document's head.
@@ -59,18 +154,13 @@
     //    we can safely return this node. Otherwise, the view itself
     //    returned several 'root' nodes and we need to keep the wrapper
     //    created by getDomFromResponse()
-    if (response.childNodes.length == 1 &&
-        response.getAttribute('cubicweb:type') == 'cwResponseWrapper') {
+    if (response.childNodes.length == 1 && response.getAttribute('cubicweb:type') == 'cwResponseWrapper') {
         return response.firstChild;
     }
     return response;
 }
 
-function preprocessAjaxLoad(node, newdomnode) {
-    return loadAjaxHtmlHead(newdomnode);
-}
-
-function postAjaxLoad(node) {
+function _postAjaxLoad(node) {
     // find sortable tables if there are some
     if (typeof(Sortable) != 'undefined') {
         Sortable.sortTables(node);
@@ -89,47 +179,80 @@
         roundedCorners(node);
     }
     if (typeof setFormsTarget != 'undefined') {
-       setFormsTarget(node);
+        setFormsTarget(node);
     }
-    loadDynamicFragments(node);
+    _loadDynamicFragments(node);
     // XXX [3.7] jQuery.one is now used instead jQuery.bind,
     // jquery.treeview.js can be unpatched accordingly.
     jQuery(CubicWeb).trigger('server-response', [true, node]);
+    jQuery(node).trigger('server-response', [true, node]);
+}
+
+function remoteCallFailed(err, req) {
+    cw.log(err);
+    if (req.status == 500) {
+        updateMessage(err);
+    } else {
+        updateMessage(_("an error occurred while processing your request"));
+    }
 }
 
-/* cubicweb loadxhtml plugin to make jquery handle xhtml response
+//============= base AJAX functions to make remote calls =====================//
+/**
+ * .. function:: ajaxFuncArgs(fname, form, *args)
  *
- * fetches `url` and replaces this's content with the result
+ * extend `form` parameters to call the js_`fname` function of the json
+ * controller with `args` arguments.
+ */
+function ajaxFuncArgs(fname, form /* ... */) {
+    form = form || {};
+    $.extend(form, {
+        'fname': fname,
+        'pageid': pageid,
+        'arg': $.map(cw.utils.sliceList(arguments, 2), jQuery.toJSON)
+    });
+    return form;
+}
+
+/**
+ * .. function:: loadxhtml(url, form, reqtype='get', mode='replace', cursor=true)
  *
- * @param mode how the replacement should be done (default is 'replace')
- *  Possible values are :
+ * build url given by absolute or relative `url` and `form` parameters
+ * (dictionary), fetch it using `reqtype` method, then evaluate the
+ * returned XHTML and insert it according to `mode` in the
+ * document. Possible modes are :
+ *
  *    - 'replace' to replace the node's content with the generated HTML
  *    - 'swap' to replace the node itself with the generated HTML
  *    - 'append' to append the generated HTML to the node's content
+ *
+ * If `cursor`, turn mouse cursor into 'progress' cursor until the remote call
+ * is back.
  */
-jQuery.fn.loadxhtml = function(url, data, reqtype, mode) {
-    var ajax = null;
-    if (reqtype == 'post') {
-        ajax = jQuery.post;
-    } else {
-        ajax = jQuery.get;
+jQuery.fn.loadxhtml = function(url, form, reqtype, mode, cursor) {
+    if (this.size() > 1) {
+        cw.log('loadxhtml was called with more than one element');
     }
-    if (this.size() > 1) {
-        log('loadxhtml was called with more than one element');
+    var callback = null;
+    if (form && form.callback) {
+        cw.log('[3.9] callback given through form.callback is deprecated, add ' + 'callback on the defered');
+        callback = form.callback;
+        delete form.callback;
     }
     var node = this.get(0); // only consider the first element
-    mode = mode || 'replace';
-    var callback = null;
-    if (data && data.callback) {
-        callback = data.callback;
-        delete data.callback;
+    if (cursor) {
+        setProgressCursor();
     }
-    ajax(url, data, function(response) {
+    var d = loadRemote(url, form, reqtype);
+    d.addCallback(function(response) {
         var domnode = getDomFromResponse(response);
-        domnode = preprocessAjaxLoad(node, domnode);
+        domnode = loadAjaxHtmlHead(domnode);
+        mode = mode || 'replace';
+        // make sure the component is visible
+        $(node).removeClass("hidden");
         if (mode == 'swap') {
             var origId = node.id;
-            node = swapDOM(node, domnode);
+            node = cw.swapDOM(node, domnode);
             if (!node.id) {
                 node.id = origId;
             }
@@ -138,19 +261,98 @@
         } else if (mode == 'append') {
             jQuery(node).append(domnode);
         }
-        postAjaxLoad(node);
+        _postAjaxLoad(node);
         while (jQuery.isFunction(callback)) {
             callback = callback.apply(this, [domnode]);
         }
     });
-};
+    d.addErrback(remoteCallFailed);
+    if (cursor) {
+        d.addCallback(resetCursor);
+        d.addErrback(resetCursor);
+    }
+    return d;
+}
 
+/**
+ * .. function:: loadRemote(url, form, reqtype='GET', sync=false)
+ *
+ * Asynchronously (unless `async` argument is set to false) load an url or path
+ * and return a deferred whose callbacks args are decoded according to the
+ * Content-Type response header. `form` should be additional form params
+ * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
+ */
+function loadRemote(url, form, reqtype, sync) {
+    if (!url.startswith(baseuri())) {
+        url = baseuri() + url;
+    }
+    if (!sync) {
+        var deferred = new Deferred();
+        jQuery.ajax({
+            url: url,
+            type: (reqtype || 'POST').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
+        });
+        // check result.responseText instead of result to avoid error encountered with IE
+        if (result.responseText) {
+            // XXX no good reason to force json here,
+            // it should depends on request content-type
+            result = cw.evalJSON(result.responseText);
+        }
+        return result;
+    }
+}
 
-/* finds each dynamic fragment in the page and executes the
+//============= higher level AJAX functions using remote calls ===============//
+/**
+ * .. function:: _(message)
+ *
+ * emulation of gettext's _ shortcut
+ */
+function _(message) {
+    return loadRemote('json', ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
+}
+
+/**
+ * .. function:: _loadDynamicFragments(node)
+ *
+ * finds each dynamic fragment in the page and executes the
  * the associated RQL to build them (Async call)
  */
-function loadDynamicFragments(node) {
+function _loadDynamicFragments(node) {
     if (node) {
         var fragments = jQuery(node).find('div.dynamicFragment');
     } else {
@@ -162,247 +364,136 @@
     if (typeof LOADING_MSG == 'undefined') {
         LOADING_MSG = 'loading'; // this is only a safety belt, it should not happen
     }
-    for(var i=0; i<fragments.length; i++) {
+    for (var i = 0; i < fragments.length; i++) {
         var fragment = fragments[i];
         fragment.innerHTML = '<h3>' + LOADING_MSG + ' ... <img src="data/loading.gif" /></h3>';
+        var $fragment = jQuery(fragment);
         // if cubicweb:loadurl is set, just pick the url et send it to loadxhtml
-        var url = getNodeAttribute(fragment, 'cubicweb:loadurl');
+        var url = $fragment.attr('cubicweb:loadurl');
         if (url) {
-            jQuery(fragment).loadxhtml(url);
+            $fragment.loadxhtml(url);
             continue;
         }
         // else: rebuild full url by fetching cubicweb:rql, cubicweb:vid, etc.
-        var rql = getNodeAttribute(fragment, 'cubicweb:rql');
-        var items = getNodeAttribute(fragment, 'cubicweb:vid').split('&');
+        var rql = $fragment.attr('cubicweb:rql');
+        var items = $fragment.attr('cubicweb:vid').split('&');
         var vid = items[0];
         var extraparams = {};
         // case where vid='myvid&param1=val1&param2=val2': this is a deprecated abuse-case
         if (items.length > 1) {
-            console.log("[3.5] you're using extraargs in cubicweb:vid attribute, this is deprecated, consider using loadurl instead");
-            for (var j=1; j<items.length; j++) {
+            cw.log("[3.5] you're using extraargs in cubicweb:vid " +
+                   "attribute, this is deprecated, consider using " +
+                   "loadurl instead");
+            for (var j = 1; j < items.length; j++) {
                 var keyvalue = items[j].split('=');
                 extraparams[keyvalue[0]] = keyvalue[1];
             }
         }
-        var actrql = getNodeAttribute(fragment, 'cubicweb:actualrql');
-        if (actrql) { extraparams['actualrql'] = actrql; }
-        var fbvid = getNodeAttribute(fragment, 'cubicweb:fallbackvid');
-        if (fbvid) { extraparams['fallbackvid'] = fbvid; }
-        replacePageChunk(fragment.id, rql, vid, extraparams);
-    }
-}
-
-jQuery(document).ready(function() {loadDynamicFragments();});
-
-//============= base AJAX functions to make remote calls =====================//
-
-function remoteCallFailed(err, req) {
-    if (req.status == 500) {
-        updateMessage(err);
-    } else {
-        log(err);
-        updateMessage(_("an error occured while processing your request"));
+        var actrql = $fragment.attr('cubicweb:actualrql');
+        if (actrql) {
+            extraparams['actualrql'] = actrql;
+        }
+        var fbvid = $fragment.attr('cubicweb:fallbackvid');
+        if (fbvid) {
+            extraparams['fallbackvid'] = fbvid;
+        }
+        extraparams['rql'] = rql;
+        extraparams['vid'] = vid;
+        $fragment.loadxhtml('json', ajaxFuncArgs('view', extraparams));
     }
 }
 
-
-/*
- * This function will call **synchronously** a remote method on the cubicweb server
- * @param fname: the function name to call (as exposed by the JSONController)
- *
- * additional arguments will be directly passed to the specified function
- *
- * It looks at http headers to guess the response type.
- */
-function remoteExec(fname /* ... */) {
-    setProgressCursor();
-    var props = {'fname' : fname, 'pageid' : pageid,
-                      'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-    var result  = jQuery.ajax({url: JSON_BASE_URL, data: props, async: false}).responseText;
-    if (result) {
-        result = evalJSON(result);
-    }
-    resetCursor();
-    return result;
-}
-
-/*
- * This function will call **asynchronously** a remote method on the json
- * controller of the cubicweb http server
- *
- * @param fname: the function name to call (as exposed by the JSONController)
- *
- * additional arguments will be directly passed to the specified function
- *
- * It looks at http headers to guess the response type.
- */
-
-function asyncRemoteExec(fname /* ... */) {
-    setProgressCursor();
-    var props = {'fname' : fname, 'pageid' : pageid,
-                 'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-    // XXX we should inline the content of loadRemote here
-    var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
-    deferred = deferred.addErrback(remoteCallFailed);
-    deferred = deferred.addErrback(resetCursor);
-    deferred = deferred.addCallback(resetCursor);
-    return deferred;
-}
-
-
-/* emulation of gettext's _ shortcut
- */
-function _(message) {
-    return remoteExec('i18n', [message])[0];
-}
-
-function userCallback(cbname) {
-    asyncRemoteExec('user_callback', cbname);
-}
+jQuery(document).ready(function() {
+    _loadDynamicFragments();
+});
 
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    remoteExec('unload_page_data');
+    loadRemote('json', ajaxFuncArgs('unload_page_data'), 'GET', true);
+}
+
+function removeBookmark(beid) {
+    var d = loadRemote('json', ajaxFuncArgs('delete_bookmark', null, beid));
+    d.addCallback(function(boxcontent) {
+        $('#bookmarks_box').loadxhtml('json',
+                                      ajaxFuncArgs('render', null, 'boxes',
+                                                   'bookmarks_box'));
+        document.location.hash = '#header';
+        updateMessage(_("bookmark has been removed"));
+    });
+}
+
+function userCallback(cbname) {
+    setProgressCursor();
+    var d = loadRemote('json', ajaxFuncArgs('user_callback', null, cbname));
+    d.addCallback(resetCursor);
+    d.addErrback(resetCursor);
+    d.addErrback(remoteCallFailed);
+    return d;
 }
 
+function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
+    var d = userCallback(cbname);
+    d.addCallback(function() {
+        $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {'rql': rql},
+                                                       registry, compid));
+        if (msg) {
+            updateMessage(msg);
+        }
+    });
+}
+
+function userCallbackThenReloadPage(cbname, msg) {
+    var d = userCallback(cbname);
+    d.addCallback(function() {
+        window.location.reload();
+        if (msg) {
+            updateMessage(msg);
+        }
+    });
+}
+
+/**
+ * .. function:: unregisterUserCallback(cbname)
+ *
+ * unregisters the python function registered on the server's side
+ * while the page was generated.
+ */
+function unregisterUserCallback(cbname) {
+    setProgressCursor();
+    var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+                                            null, cbname));
+    d.addCallback(resetCursor);
+    d.addErrback(resetCursor);
+    d.addErrback(remoteCallFailed);
+}
+
+//============= XXX move those functions? ====================================//
 function openHash() {
     if (document.location.hash) {
         var nid = document.location.hash.replace('#', '');
         var node = jQuery('#' + nid);
-        if (node) { removeElementClass(node, "hidden"); }
+        if (node) {
+            $(node).removeClass("hidden");
+        }
     };
 }
 jQuery(document).ready(openHash);
 
-function reloadComponent(compid, rql, registry, nodeid, extraargs) {
-    registry = registry || 'components';
-    rql = rql || '';
-    nodeid = nodeid || (compid + 'Component');
-    extraargs = extraargs || {};
-    var node = getNode(nodeid);
-    var d = asyncRemoteExec('component', compid, rql, registry, extraargs);
-    d.addCallback(function(result, req) {
-        var domnode = getDomFromResponse(result);
-        if (node) {
-            // make sure the component is visible
-            removeElementClass(node, "hidden");
-            domnode = preprocessAjaxLoad(node, domnode);
-            swapDOM(node, domnode);
-            postAjaxLoad(domnode);
-        }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-    });
-  return d;
-}
-
-/* XXX: HTML architecture of cubicweb boxes is a bit strange */
-function reloadBox(boxid, rql) {
-    return reloadComponent(boxid, rql, 'boxes', boxid);
-}
-
-function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
-    var d = asyncRemoteExec('user_callback', cbname);
-    d.addCallback(function() {
-        reloadComponent(compid, rql, registry, nodeid);
-        if (msg) { updateMessage(msg); }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-function userCallbackThenReloadPage(cbname, msg) {
-    var d = asyncRemoteExec('user_callback', cbname);
-    d.addCallback(function() {
-        window.location.reload();
-        if (msg) { updateMessage(msg); }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-/*
- * unregisters the python function registered on the server's side
- * while the page was generated.
- */
-function unregisterUserCallback(cbname) {
-    var d = asyncRemoteExec('unregister_user_callback', cbname);
-    d.addCallback(function() {resetCursor();});
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-
-/* executes an async query to the server and replaces a node's
- * content with the query result
+/**
+ * .. function:: buildWysiwygEditors(parent)
  *
- * @param nodeId the placeholder node's id
- * @param rql the RQL query
- * @param vid the vid to apply to the RQL selection (default if not specified)
- * @param extraparmas table of additional query parameters
- */
-function replacePageChunk(nodeId, rql, vid, extraparams, /* ... */ swap, callback) {
-    var params = null;
-    if (callback) {
-        params = {callback: callback};
-    }
-
-    var node = jQuery('#' + nodeId)[0];
-    var props = {};
-    if (node) {
-        props['rql'] = rql;
-        props['fname'] = 'view';
-        props['pageid'] = pageid;
-        if (vid) { props['vid'] = vid; }
-        if (extraparams) { jQuery.extend(props, extraparams); }
-        // FIXME we need to do asURL(props) manually instead of
-        // passing `props` directly to loadxml because replacePageChunk
-        // is sometimes called (abusively) with some extra parameters in `vid`
-        var mode = swap?'swap':'replace';
-        var url = JSON_BASE_URL + asURL(props);
-        jQuery(node).loadxhtml(url, params, 'get', mode);
-    } else {
-        log('Node', nodeId, 'not found');
-    }
-}
-
-/* XXX deprecates?
- * fetches `url` and replaces `nodeid`'s content with the result
- * @param replacemode how the replacement should be done (default is 'replace')
- *  Possible values are :
- *    - 'replace' to replace the node's content with the generated HTML
- *    - 'swap' to replace the node itself with the generated HTML
- *    - 'append' to append the generated HTML to the node's content
- */
-function loadxhtml(nodeid, url, /* ... */ replacemode) {
-    jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode);
-}
-
-/* XXX: this function should go in edition.js but as for now, htmlReplace
+ *XXX: this function should go in edition.js but as for now, htmlReplace
  * references it.
  *
  * replace all textareas with fckeditors.
  */
 function buildWysiwygEditors(parent) {
-    jQuery('textarea').each(function () {
+    jQuery('textarea').each(function() {
         if (this.getAttribute('cubicweb:type') == 'wysiwyg') {
             // mark editor as instanciated, we may be called a number of times
-            // (see postAjaxLoad)
+            // (see _postAjaxLoad)
             this.setAttribute('cubicweb:type', 'fckeditor');
             if (typeof FCKeditor != "undefined") {
                 var fck = new FCKeditor(this.id);
@@ -411,29 +502,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 +532,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 +556,116 @@
     }
     // several children => wrap them in a single node and return the wrap
     return DIV({'cubicweb:type': "cwResponseWrapper"},
-               map(function(node) {
-                    return jQuery(node).clone().context;
-            }, children));
+               $.map(children, function(node) {
+                       return jQuery(node).clone().context;})
+               );
 }
 
-function postJSON(url, data, callback) {
-    return jQuery.post(url, data, callback, 'json');
-}
+/* DEPRECATED *****************************************************************/
+
+preprocessAjaxLoad = cw.utils.deprecatedFunction(
+    '[3.9] preprocessAjaxLoad() is deprecated, use loadAjaxHtmlHead instead',
+    function(node, newdomnode) {
+        return loadAjaxHtmlHead(newdomnode);
+    }
+);
+
+reloadComponent = cw.utils.deprecatedFunction(
+    '[3.9] reloadComponent() is deprecated, use loadxhtml instead',
+    function(compid, rql, registry, nodeid, extraargs) {
+        registry = registry || 'components';
+        rql = rql || '';
+        nodeid = nodeid || (compid + 'Component');
+        extraargs = extraargs || {};
+        var node = cw.jqNode(nodeid);
+        return node.loadxhtml('json', ajaxFuncArgs('component', null, compid,
+                                                   rql, registry, extraargs));
+    }
+);
+
+reloadBox = cw.utils.deprecatedFunction(
+    '[3.9] reloadBox() is deprecated, use loadxhtml instead',
+    function(boxid, rql) {
+        return reloadComponent(boxid, rql, 'boxes', boxid);
+    }
+);
 
-function getJSON(url, data, callback){
-    return jQuery.get(url, data, callback, 'json');
-}
+replacePageChunk = cw.utils.deprecatedFunction(
+    '[3.9] replacePageChunk() is deprecated, use loadxhtml instead',
+    function(nodeId, rql, vid, extraparams, /* ... */ swap, callback) {
+        var params = null;
+        if (callback) {
+            params = {
+                callback: callback
+            };
+        }
+        var node = jQuery('#' + nodeId)[0];
+        var props = {};
+        if (node) {
+            props['rql'] = rql;
+            props['fname'] = 'view';
+            props['pageid'] = pageid;
+            if (vid) {
+                props['vid'] = vid;
+            }
+            if (extraparams) {
+                jQuery.extend(props, extraparams);
+            }
+            // FIXME we need to do asURL(props) manually instead of
+            // passing `props` directly to loadxml because replacePageChunk
+            // is sometimes called (abusively) with some extra parameters in `vid`
+            var mode = swap ? 'swap': 'replace';
+            var url = JSON_BASE_URL + asURL(props);
+            jQuery(node).loadxhtml(url, params, 'get', mode);
+        } else {
+            cw.log('Node', nodeId, 'not found');
+        }
+    }
+);
+
+loadxhtml = cw.utils.deprecatedFunction(
+    '[3.9] loadxhtml() function is deprecated, use loadxhtml method instead',
+    function(nodeid, url, /* ... */ replacemode) {
+        jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode);
+    }
+);
 
-CubicWeb.provide('ajax.js');
+remoteExec = cw.utils.deprecatedFunction(
+    '[3.9] remoteExec() is deprecated, use loadRemote instead',
+    function(fname /* ... */) {
+        setProgressCursor();
+        var props = {
+            'fname': fname,
+            'pageid': pageid,
+            'arg': $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
+        };
+        var result = jQuery.ajax({
+            url: JSON_BASE_URL,
+            data: props,
+            async: false
+        }).responseText;
+        if (result) {
+            result = cw.evalJSON(result);
+        }
+        resetCursor();
+        return result;
+    }
+);
+
+asyncRemoteExec = cw.utils.deprecatedFunction(
+    '[3.9] asyncRemoteExec() is deprecated, use loadRemote instead',
+    function(fname /* ... */) {
+        setProgressCursor();
+        var props = {
+            'fname': fname,
+            'pageid': pageid,
+            'arg': $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
+        };
+        // XXX we should inline the content of loadRemote here
+        var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
+        deferred = deferred.addErrback(remoteCallFailed);
+        deferred = deferred.addErrback(resetCursor);
+        deferred = deferred.addCallback(resetCursor);
+        return deferred;
+    }
+);
--- a/web/data/cubicweb.bookmarks.js	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-CubicWeb.require('ajax.js');
-
-function removeBookmark(beid) {
-    d = asyncRemoteExec('delete_bookmark', beid);
-    d.addCallback(function(boxcontent) {
-	    reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
-  	document.location.hash = '#header';
- 	updateMessage(_("bookmark has been removed"));
-    });
-}
--- a/web/data/cubicweb.calendar.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.calendar.css	Wed Nov 03 16:38:28 2010 +0100
@@ -230,7 +230,7 @@
 .calendar th.month {
  font-weight:bold;
  padding-bottom:0.2em;
- background: #cfceb7;
+ background: %(actionBoxTitleBgColor)s;
 }
 
 .calendar th.month a{
--- a/web/data/cubicweb.calendar.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.calendar.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.compat.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.css	Wed Nov 03 16:38:28 2010 +0100
@@ -3,82 +3,67 @@
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
+
 /***************************************/
-/* xhtml tags styles                   */
+/* xhtml tags                          */
 /***************************************/
 
-*{
-  margin:0px;
-  padding :0px;
-}
-
-html, body {
-  background: #e2e2e2;
-}
-
+/* scale and rhythm cf http://lamb.cc/typograph/ */
 body {
-  font-size: 69%;
-  font-weight: normal;
-  font-family: Verdana, sans-serif;
+  font-family:  %(defaultFontFamily)s;
+  font-size: %(defaultSize)s;
+  line-height: %(defaultLineHeight)s;
+  color: %(defaultColor)s;
 }
+h1, h2, h3 { margin-top:0; margin-bottom:0; }
+
+/* got rhythm ? beat of 12*1.25 = 15 px */
+.rhythm_bg { background: url("%(baseRhythmBg)s") repeat ! important; }
+
+/* scale 3:5 stranded */
+/* h1 { font-size:2em; } */
+/* h2 { font-size:1.61538em; } */
+/* h3 { font-size:1.23077em; } */
 
+/* scale le corbusier */
+/* h1 { font-size:2.11538em; } */
+/* h2 { font-size:1.61538em; } */
+/* h3 { font-size:1.30769em; } */
+
+/* scale traditional */
+h1 { font-size: %(h1FontSize)s; }
+h2 { font-size: %(h2FontSize)s; }
+h3 { font-size: %(h3FontSize)s; }
+
+/* paddings */
 h1 {
-  font-size: 188%;
-  margin: 0.2em 0px 0.3em;
-  border-bottom: 1px solid #000;
+  border-bottom: %(h1BorderBottomStyle)s;
+  padding: %(h1Padding)s;
+  margin: %(h1Margin)s;
+  color: %(h1Color)s;
 }
 
-h2, h3 {
-  margin-top: 0.2em;
-  margin-bottom: 0.3em;
-}
-
-h2 {
-  font-size: 135%;
+div.tabbedprimary + h1, h1.plain {
+ border-bottom: none;
 }
 
-h3 {
-  font-size: 130%;
-}
+h2 { padding: %(h2Padding)s; }
+h3 { padding: %(h3Padding)s; }
 
-h4 {
-  font-size: 120%;
-  margin: 0.2em 0px;
-}
-
-h5 {
-  font-size:110%;
-}
-
-h6{
-  font-size:105%;
+html, body {
+  background: %(pageBgColor)s;
 }
 
 a, a:active, a:visited, a:link {
-  color: #ff4500;
+  color: %(aColor)s;
   text-decoration: none;
 }
 
-a:hover{
+a:hover {
   text-decoration: underline;
 }
 
-a img, img {
-  border: none;
-  text-align: center;
-}
-
-p {
-  margin: 0em 0px 0.2em;
-  padding-top: 2px;
-}
-
-table, td, input, select{
-  font-size: 100%;
-}
-
 table {
-  border-collapse: collapse;
   border: none;
 }
 
@@ -86,90 +71,88 @@
   vertical-align: top;
 }
 
-table td img {
-  vertical-align: middle;
-  margin-right: 10px;
+label, .label {
+  font-weight: bold;
+}
+
+pre {
+  clear: both;
+  font-family: 'Courier New', monospace;
+  letter-spacing: 0.015em;
+  padding: 0.6em;
+  margin: 0 2em 1.7em;
+  background-color: %(listingHihligthedBgColor)s;
+  border: 1px solid %(listingBorderColor)s;
+}
+
+p {
+  text-align: justify;
+  margin-bottom: %(defaultLineHeightEm)s;
+}
+
+ul {
+  margin-bottom: %(defaultLineHeightEm)s;
 }
 
 ol {
-  margin: 1px 0px 1px 16px;
+  list-style-type: decimal;
+ /* margin-bottom: %(defaultLineHeightEm)s; */
 }
 
-ul{
-  margin: 1px 0px 1px 4px;
-  list-style-type: none;
+ol ol,
+ul ul{
+  margin-left: 8px;
+  margin-bottom : 0px;
 }
 
-ul li {
-  margin-top: 2px;
-  padding: 0px 0px 2px 8px;
-  background: url("bullet_orange.png") 0% 6px no-repeat;
+/* p + ul { */
+/*   margin-top: -%(defaultLineHeightEm)s; */
+/* } */
+
+li {
+  margin-left: 1.5em;
 }
 
-dt {
-  font-size:1.17em;
-  font-weight:600;
-}
-
-dd {
-  margin: 0.6em 0 1.5em 2em;
+img{
+  border: none;
 }
 
 fieldset {
   border: none;
 }
 
-legend {
-  padding: 0px 2px;
-  font: bold 1em Verdana, sans-serif;
+h1 a, h1 a:active, h1 a:visited, h1 a:link,
+h2 a, h2 a:active, h2 a:visited, h2 a:link,
+h3 a, h3 a:active, h3 a:visited, h3 a:link {
+  color: inherit;
+  text-decoration: none;
 }
 
 input, textarea {
-  padding: 0.2em;
-  vertical-align: middle;
-  border: 1px solid #ccc;
+  padding: 0.1em 0.2em;
+  vertical-align: bottom;
+  border: 1px solid %(pageContentBorderColor)s;
+
 }
 
 input:focus {
-  border: 1px inset #ff7700;
-}
-
-label, .label {
-  font-weight: bold;
-}
-
-iframe {
-  border: 0px;
+  border: 1px inset %(headerBgColor)s;
 }
 
-pre {
-  font-family: Courier, "Courier New", Monaco, monospace;
-  font-size: 100%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
-}
-
-code {
-  font-size: 120%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
-}
-
-blockquote {
-  font-family: Courier, "Courier New", serif;
-  font-size: 120%;
-  margin: 5px 0px;
-  padding: 0.8em;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
+hr{
+  border: none;
+  border-bottom: 1px solid %(defaultColor)s;
+  height: 1px;
 }
 
 /***************************************/
 /* generic classes                     */
 /***************************************/
 
+h1 a:hover {
+ text-decoration: none;
+}
+
 .odd {
   background-color: #f7f6f1;
 }
@@ -179,8 +162,14 @@
 }
 
 .hr {
-  border-bottom: 1px dotted #ccc;
-  margin: 1em 0px;
+  border-bottom: 1px dotted %(pageContentBorderColor)s;
+  height: 17px;
+}
+
+hr.boxSeparator{
+  border: none;
+  border-bottom: 1px solid %(listingBorderColor)s;
+  height: 1px;
 }
 
 .left {
@@ -200,13 +189,26 @@
   visibility: hidden;
 }
 
-li.invisible { list-style: none; background: none; padding: 0px 0px
-1px 1px; }
+li.invisible {
+  background: none;
+  padding: 0px 0px 1px 1px;
+}
 
 li.invisible div{
   display: inline;
 }
 
+.caption {
+    font-weight: bold;
+}
+
+.legend{
+    font-style: italic;
+}
+
+.align-center{
+    text-align: center;
+}
 
 /***************************************/
 /*   LAYOUT                            */
@@ -215,7 +217,7 @@
 /* header */
 
 table#header {
-  background: #ff7700 url("banner.png") left top repeat-x;
+  background: %(headerBgColor)s url("banner.png") repeat-x top left;
   text-align: left;
 }
 
@@ -224,129 +226,136 @@
 }
 
 table#header a {
-color: #000;
+  color: %(defaultColor)s;
+}
+
+table#header img#logo{
+  vertical-align: middle;
 }
 
 span#appliName {
- font-weight: bold;
- color: #000;
- white-space: nowrap;
+  font-weight: bold;
+  color: %(defaultColor)s;
+  white-space: nowrap;
 }
 
 table#header td#headtext {
   width: 100%;
 }
 
-/* FIXME appear with 4px width in IE6 */
-div#stateheader{
-  min-width: 66%;
-}
-
 /* Popup on login box and userActionBox */
-div.popupWrapper{
- position:relative;
- z-index:100;
+div.popupWrapper {
+  position: relative;
+  z-index: 100;
 }
 
 div.popup {
   position: absolute;
   background: #fff;
-  border: 1px solid black;
+  /* background-color: #f0eff0; */
+  /* background-image: url(popup.png); */
+  /* background-repeat: repeat-x; */
+  /* background-positon: top left; */
+  border: 1px solid %(listingBorderColor)s;
+  border-top: none;
   text-align: left;
-  z-index:400;
+  z-index: 400;
 }
 
 div.popup ul li a {
   text-decoration: none;
-  color: black;
+  color: #000;
 }
 
 /* main zone */
 
 div#page {
-  background: #e2e2e2;
-  position: relative;
-  min-height: 800px;
+  margin: %(defaultLayoutMargin)s;
 }
 
-table#mainLayout{
- margin:0px 3px;
+table#mainLayout #navColumnLeft {
+  width: 16em;
+  padding-right: %(defaultLayoutMargin)s;
+}
+
+table#mainLayout #navColumnRight {
+  width: 16em;
+  padding-left: %(defaultLayoutMargin)s;
 }
 
-table#mainLayout td#contentcol {
-  padding: 8px 10px 5px;
+div#pageContent {
+  clear: both;
+  /* margin-top:-1px; *//* enable when testing rhythm */
+  background: %(pageContentBgColor)s;
+  border: 1px solid %(pageContentBorderColor)s;
+  padding: 0 %(pageContentPadding)s %(pageContentPadding)s;
 }
 
-table#mainLayout td.navcol {
-  width: 16em;
+div#pageContent #contentmain .pagination {
+  margin-top: 0;
 }
 
+div#pageContent .pagination{
+  margin-top: 1.5em;
+}
+
+div#contentmain{
+  margin-top: %(pageContentPadding)s
+}
+
+/*FIXME */
 #contentheader {
   margin: 0px;
   padding: 0.2em 0.5em 0.5em 0.5em;
 }
 
 #contentheader a {
-  color: #000;
-}
-
-div#pageContent {
-  clear: both;
-  padding: 10px 1em 2em;
-  background: #ffffff;
-  border: 1px solid #ccc;
+  color: %(defaultColor)s;
 }
 
 /* rql bar */
 
 div#rqlinput {
-  border: 1px solid #cfceb7;
-  margin-bottom: 8px;
-  padding: 3px;
-  background: #cfceb7;
+  margin-bottom: %(defaultLayoutMargin)s;
 }
 
 input#rql{
-  width: 95%;
+  padding: 0.25em 0.3em;
+  width: 99%;
 }
 
 /* boxes */
-div.navboxes {
- margin-top: 8px;
-}
 
 div.boxFrame {
   width: 100%;
 }
 
 div.boxTitle {
-  padding-top: 0px;
-  padding-bottom: 0.2em;
-  font: bold 100% Georgia;
   overflow: hidden;
+  font-weight: bold;
   color: #fff;
-  background: #ff9900 url("search.png") left bottom repeat-x;
+  background: %(boxTitleBg)s;
+}
+
+div.boxTitle span,
+div.sideBoxTitle span {
+  padding: 0px 0.5em;
+  white-space: nowrap;
 }
 
 div.searchBoxFrame div.boxTitle,
 div.greyBoxFrame div.boxTitle {
-  background: #cfceb7;
-}
-
-div.boxTitle span,
-div.sideBoxTitle span {
-  padding: 0px 5px;
-  white-space: nowrap;
+  background: %(actionBoxTitleBg)s;
 }
 
 div.sideBoxTitle span,
 div.searchBoxFrame div.boxTitle span,
 div.greyBoxFrame div.boxTitle span {
-  color: #222211;
+  color: %(defaultColor)s;
 }
 
 .boxFrame a {
-  color: #000;
+  color: %(defaultColor)s;
 }
 
 div.boxContent {
@@ -355,80 +364,22 @@
   border-top: none;
 }
 
-ul.boxListing {
-  margin: 0px;
-  padding: 0px 3px;
-}
-
-ul.boxListing li,
-ul.boxListing ul li {
-  display: inline;
-  margin: 0px;
-  padding: 0px;
-  background-image: none;
-}
-
-ul.boxListing ul {
-  margin: 0px 0px 0px 7px;
-  padding: 1px 3px;
-}
-
-ul.boxListing a {
-  color: #000;
+a.boxMenu {
   display: block;
   padding: 1px 9px 1px 3px;
+  background: transparent %(bulletDownImg)s;
 }
 
-ul.boxListing .selected {
-  color: #FF4500;
-  font-weight: bold;
-}
-
-ul.boxListing a.boxBookmark:hover,
-ul.boxListing a:hover,
-ul.boxListing ul li a:hover {
-  text-decoration: none;
-  background: #eeedd9;
-  color: #111100;
+a.boxMenu:hover {
+  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
+  cursor: pointer;
 }
 
-ul.boxListing a.boxMenu:hover {
-                                background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px;
-                                cursor:pointer;
-                                border-top:medium none;
-                                }
-a.boxMenu {
-  background: transparent url("puce_down.png") 98% 6px no-repeat;
-  display: block;
-  padding: 1px 9px 1px 3px;
-}
-
-
 a.popupMenu {
   background: transparent url("puce_down_black.png") 2% 6px no-repeat;
   padding-left: 2em;
 }
 
-ul.boxListing ul li a:hover {
-  background: #eeedd9  url("bullet_orange.png") 0% 6px no-repeat;
-}
-
-a.boxMenu:hover {
-  background: #eeedd9 url("puce_down.png") 98% 6px no-repeat;
-  cursor: pointer;
-}
-
-ul.boxListing a.boxBookmark {
-  padding-left: 3px;
-  background-image:none;
-  background:#fff;
-}
-
-ul.boxListing ul li a {
-  background: #fff url("bullet_orange.png") 0% 6px no-repeat;
-  padding: 1px 3px 0px 10px;
-}
-
 div.searchBoxFrame div.boxContent {
   padding: 4px 4px 3px;
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
@@ -440,29 +391,32 @@
 }
 
 div.sideBoxTitle {
-  background: #cfceb7;
+  background: %(actionBoxTitleBg)s;
   display: block;
-  font: bold 100% Georgia;
+  font-weight: bold;
 }
 
 div.sideBox {
-  padding: 0 0 0.2em;
-  margin-bottom: 0.5em;
+  margin-bottom: 1em;
+}
+
+ul.sideBox,
+ul.sideBox ul{
+  margin-bottom: 0px;
 }
 
 ul.sideBox li{
- list-style: none;
- background: none;
  padding: 0px 0px 1px 1px;
- }
+ margin: 1px 0 1px 4px;
+}
 
 div.sideBoxBody {
   padding: 0.2em 5px;
-  background: #eeedd9;
+  background: %(sideBoxBodyBg)s;
 }
 
 div.sideBoxBody a {
-  color:#555544;
+  color: %(sideBoxBodyColor)s;
 }
 
 div.sideBoxBody a:hover {
@@ -474,10 +428,11 @@
 }
 
 input.rqlsubmit{
-  background: #fffff8 url("go.png") 50% 50% no-repeat;
+  display: block;
   width: 20px;
   height: 20px;
-  margin: 0px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
+  vertical-align: bottom;
 }
 
 input#norql{
@@ -497,14 +452,14 @@
 }
 
 div#userActionsBox a.popupMenu {
-  color: black;
+  color: #000;
   text-decoration: underline;
   padding-right: 2em;
 }
 
 /* download box XXX move to its own file? */
 div.downloadBoxTitle{
- background : #8FBC8F;
+ background : #8fbc8f;
  font-weight: bold;
 }
 
@@ -513,7 +468,7 @@
 }
 
 div.downloadBox div.sideBoxBody{
- background : #EEFED9;
+ background : #eefed9;
 }
 
 /**************/
@@ -521,17 +476,18 @@
 /**************/
 div#etyperestriction {
   margin-bottom: 1ex;
-  border-bottom: 1px solid #ccc;
+  border-bottom: 1px solid %(pageContentBorderColor)s;
 }
 
+/* pagination */
 span.slice a:visited,
 span.slice a:hover{
-  color: #555544;
+  color: %(helperColor)s;
 }
 
 span.selectedSlice a:visited,
 span.selectedSlice a {
-  color: #000;
+  color: %(defaultColor)s;
 }
 
 /* FIXME should be moved to cubes/folder */
@@ -546,19 +502,13 @@
 }
 
 div.prevnext a {
-  color: #000;
+  color: %(defaultColor)s;
 }
 
 /***************************************/
 /* entity views                        */
 /***************************************/
 
-.mainInfo  {
-  margin-right: 1em;
-  padding: 0.2em;
-}
-
-
 div.mainRelated {
   border: none;
   margin-right: 1em;
@@ -566,18 +516,17 @@
 }
 
 div.primaryRight{
- }
+  margin-left: %(defaultLayoutMargin)s;
+}
 
 div.metadata {
   font-size: 90%;
   margin: 5px 0px 3px;
-  color: #666;
-  font-style: italic;
+  color: %(helperColor)s;
   text-align: right;
 }
 
 div.section {
-  margin-top: 0.5em;
   width:100%;
 }
 
@@ -600,6 +549,7 @@
   float: right;
   padding-left: 24px;
   position: relative;
+  z-index: 10;
 }
 div.toolbarButton {
   display: inline;
@@ -611,56 +561,50 @@
 
 .warning,
 .message,
-.errorMessage ,
-.searchMessage{
-  padding: 0.3em 0.3em 0.3em 1em;
+.errorMessage{
+  padding: 0.2em;
   font-weight: bold;
 }
 
-.simpleMessage {
-  margin: 4px 0px;
-  font-weight: bold;
-  color: #ff7700;
+.searchMessage{
+ margin-top: %(defaultLayoutMargin)s;
 }
 
-div#appMsg, div.appMsg {
-  border: 1px solid #cfceb7;
-  margin-bottom: 8px;
-  padding: 3px;
-  background: #f8f8ee;
+.loginMessage {
+  margin: 4px 0px;
+  font-weight: bold;
+  color: %(aColor)s;
+}
+
+div#appMsg {
+  margin-bottom: %(defaultLayoutMargin)s;
+  border: 1px solid %(actionBoxTitleBgColor)s;
 }
 
 .message {
-  margin: 0px;
-  background: #f8f8ee url("information.png") 5px center no-repeat;
+  background: %(msgBgColor)s %(infoMsgBgImg)s;
   padding-left: 15px;
 }
 
 .errorMessage {
   margin: 10px 0px;
   padding-left: 25px;
-  background: #f7f6f1 url("critical.png") 2px center no-repeat;
-  color: #ed0d0d;
-  border: 1px solid #cfceb7;
+  background: %(msgBgColor)s url("critical.png") 2px center no-repeat;
+  color: %(errorMsgColor)s;
+  border: 1px solid %(actionBoxTitleBgColor)s;
 }
 
-.searchMessage {
-  margin-top: 0.5em;
-  border-top: 1px solid #cfceb7;
-  background: #eeedd9 url("information.png") 0% 50% no-repeat; /*dcdbc7*/
-}
-
+/* search-associate message */
 .stateMessage {
-  border: 1px solid #ccc;
-  background: #f8f8ee url("information.png") 10px 50% no-repeat;
-  padding:4px 0px 4px 20px;
-  border-width: 1px 0px 1px 0px;
+  border: 1px solid %(pageContentBorderColor)s;
+  background: %(msgBgColor)s %(infoMsgBgImg)s;
+  padding: 0.1em 0 0.1em 20px;
 }
 
 /* warning messages like "There are too many results ..." */
 .warning {
   padding-left: 25px;
-  background: #f2f2f2 url("critical.png") 3px 50% no-repeat;
+  background: %(msgBgColor)s url("critical.png") 3px 50% no-repeat;
 }
 
 /* label shown in the top-right hand corner during form validation */
@@ -668,8 +612,8 @@
   position: fixed;
   right: 5px;
   top: 0px;
-  background: #222211;
-  color: white;
+  background: %(defaultColor)s;
+  color: #fff;
   font-weight: bold;
   display: none;
 }
@@ -679,72 +623,71 @@
 /***************************************/
 
 table.listing {
- padding: 10px 0em;
- color: #000;
- width: 100%;
- border-right: 1px solid #dfdfdf;
+  width: 100%;
+  font-size: 0.9167em;
+  padding: 10px 0em;
+  color: %(defaultColor)s;
+  border: 1px solid %(listingBorderColor)s;
+  margin-bottom: 1em;
 }
 
+table.listing th {
+  font-weight: bold;
+  font-size: 8pt;
+  background: %(listingHeaderBgColor)s; 
+  padding: 2px 4px;
+  border: 1px solid %(listingBorderColor)s;
+  border-right:none;
+ /* white-space: nowrap; */
+}
 
 table.listing thead th.over {
-  background-color: #746B6B;
+  background-color: %(listingHeaderBgColor)s;
   cursor: pointer;
 }
 
-table.listing tr th {
-  border: 1px solid #dfdfdf;
-  border-right:none;
-  font-size: 8pt;
-  padding: 4px;
-}
-
 table.listing tr .header {
-  border-right: 1px solid #dfdfdf;
+  border-right: 1px solid %(listingBorderColor)s;
   cursor: pointer;
 }
 
 table.listing td {
-  color: #3D3D3D;
-  padding: 4px;
-  background-color: #FFF;
+  padding: 3px;
   vertical-align: top;
-}
-
-table.listing th,
-table.listing td {
-  padding: 3px 0px 3px 5px;
-  border: 1px solid #dfdfdf;
+  border: 1px solid %(listingBorderColor)s;
   border-right: none;
-}
-
-table.listing th {
-  font-weight: bold;
-  background: #ebe8d9 url("button.png") repeat-x;
+  background-color: #fff;
 }
 
 table.listing td a,
 table.listing td a:visited {
-  color: #666;
+  color: %(defaultColor)s;
 }
 
 table.listing a:hover,
 table.listing tr.highlighted td a {
-  color:#000;
+  color:%(defaultColor)s;
 }
 
 table.listing td.top {
-  border: 1px solid white;
+  border: 1px solid #fff;
   border-bottom: none;
   text-align: right ! important;
-  /* insane IE row bug workaround */
+  /* insane IE row bug workraound */
   position: relative;
   left: -1px;
   top: -1px;
 }
 
+table.listing input,
+table.listing textarea {
+ background: %(listingHihligthedBgColor)s;
+}
+
 table.htableForm {
   vertical-align: middle;
 }
+
 table.htableForm td{
   padding-left: 1em;
   padding-top: 0.5em;
@@ -774,27 +717,26 @@
   color: #ff0000;
 }
 
-
 /***************************************/
 /* addcombobox                         */
 /***************************************/
 
-input#newopt{
- width:120px ;
- display:block;
- float:left;
- }
+input#newopt {
+  display: block;
+  float: left;
+  width: 120px;
+}
 
 div#newvalue{
- margin-top:2px;
- }
+  margin-top: 2px;
+}
 
-#add_newopt{
- background: #fffff8 url("go.png") 50% 50% no-repeat;
- width: 20px;
- line-height: 20px;
- display:block;
- float:left;
+#add_newopt {
+  display: block;
+  float: left;
+  width: 20px;
+  line-height: 20px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
 }
 
 /***************************************/
@@ -803,9 +745,8 @@
 
 input.button{
   margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
+  border: 1px solid %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
 }
 
 /* FileItemInnerView  jquery.treeview.css */
@@ -815,18 +756,105 @@
 }
 
 /***************************************/
+/* lists                               */
+/***************************************/
+
+ul.section,
+ul.startup {
+  margin-bottom: 0px;
+}
+
+ul.startup li,
+ul.section li {
+  margin-left:0px
+}
+
+ul.boxListing {
+  margin: 0px;
+  padding: 0px 3px;
+}
+
+ul.boxListing li,
+ul.boxListing ul li {
+  margin: 0px;
+  padding: 0px;
+  background-image: none;
+}
+
+ul.boxListing ul {
+  padding: 1px 3px;
+}
+
+ul.boxListing a {
+  color: %(defaultColor)s;
+  display:block;
+  padding: 1px 9px 1px 3px;
+}
+
+ul.boxListing .selected {
+  color: %(aColor)s;
+  font-weight: bold;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
+}
+
+ul.boxListing a.boxBookmark {
+  padding-left: 3px;
+  background-image: none;
+  background:#fff;
+}
+
+ul.simple li,
+ul.boxListing ul li ,
+.popupWrapper ul li {
+  background: transparent url("bullet_orange.png") no-repeat 0% 6px;
+}
+
+ul.boxListing a.boxBookmark:hover,
+ul.boxListing a:hover,
+ul.boxListing ul li a:hover {
+  text-decoration: none;
+  background: %(sideBoxBodyBg)s;
+}
+
+ul.boxListing ul li a:hover{
+  background-color: transparent;
+}
+
+ul.boxListing ul li a {
+  padding: 1px 3px 0px 10px;
+}
+
+ul.simple li {
+  padding-left: 8px;
+}
+
+.popupWrapper ul {
+  padding:0.2em 0.3em;
+  margin-bottom: 0px;
+}
+
+.popupWrapper ul li {
+  padding-left: 8px;
+  margin-left: 0px;
+  white-space: nowrap;
+}
+
+/***************************************/
 /* footer                              */
 /***************************************/
 
-div.footer {
+div#footer {
   text-align: center;
 }
-div.footer a {
-  color: #000;
+div#footer a {
+  color: %(defaultColor)s;
   text-decoration: none;
 }
 
-
 /****************************************/
 /* FIXME must by managed by cubes       */
 /****************************************/
@@ -835,21 +863,11 @@
   color: gray;
 }
 
-
-/***************************************/
-/* FIXME : Deprecated ? entity view ?  */
-/***************************************/
-.title {
-  text-align: left;
-  font-size:  large;
-  font-weight: bold;
-}
-
 .validateButton {
   margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
+  border: 1px solid %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
 }
 
 /********************************/
@@ -859,3 +877,55 @@
 .otherView {
   float: right;
 }
+
+/********************************/
+/* rest related classes         */
+/********************************/
+
+img.align-right {
+  margin-left: 1.5em;
+}
+
+img.align-left {
+  margin-right: 1.5em;
+}
+
+/******************************/
+/* reledit                    */
+/******************************/
+
+.releditField {
+    display: inline;
+}
+
+.releditForm {
+ display:none;
+}
+
+/********************************/
+/* overwite other css here      */
+/********************************/
+
+/* ui.tabs.css */
+ul.ui-tabs-nav,
+div.ui-tabs-panel {
+  font-family: %(defaultFontFamily)s;
+  font-size: %(defaultSize)s;
+}
+
+div.ui-tabs-panel {
+  border-top:1px solid #b6b6b6;
+}
+
+ul.ui-tabs-nav a {
+  color: #3d3d3d;
+}
+
+ul.ui-tabs-nav a:hover {
+  color: #000;
+}
+
+img.ui-datepicker-trigger {
+  margin-left: 0.5em;
+  vertical-align: bottom;
+}
--- a/web/data/cubicweb.edition.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.edition.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,334 +1,401 @@
-/*
+/**
+ * Functions dedicated to edition.
+ *
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
  */
 
-CubicWeb.require('python.js');
-CubicWeb.require('htmlhelpers.js');
-CubicWeb.require('ajax.js');
-
-
 //============= Eproperty form functions =====================================//
-
-/* called on Eproperty key selection:
+/**
+ * .. function:: setPropValueWidget(varname, tabindex)
+ *
+ * called on Eproperty key selection:
  * - get the selected value
  * - get a widget according to the key by a sync query to the server
  * - fill associated div with the returned html
  *
- * @param varname the name of the variable as used in the original creation form
- * @param tabindex the tabindex that should be set on the widget
+ * * `varname`, the name of the variable as used in the original creation form
+ * * `tabindex`, the tabindex that should be set on the widget
  */
+
 function setPropValueWidget(varname, tabindex) {
-    var key = firstSelected(document.getElementById('pkey:'+varname));
+    var key = firstSelected(document.getElementById('pkey:' + varname));
     if (key) {
-	var args = {fname: 'prop_widget', pageid: pageid,
-     		    arg: map(jQuery.toJSON, [key, varname, tabindex])};
-	jqNode('div:value:'+varname).loadxhtml(JSON_BASE_URL, args, 'post');
+        var args = {
+            fname: 'prop_widget',
+            pageid: pageid,
+            arg: $.map([key, varname, tabindex], jQuery.toJSON)
+        };
+        cw.jqNode('div:value:' + varname).loadxhtml(JSON_BASE_URL, args, 'post');
     }
 }
 
-
 // *** EDITION FUNCTIONS ****************************************** //
-
-/*
+/**
+ * .. function:: reorderTabindex(start, formid)
+ *
  * this function is called when an AJAX form was generated to
  * make sure tabindex remains consistent
  */
 function reorderTabindex(start, formid) {
-    var form = getNode(formid || 'entityForm');
+    var form = cw.getNode(formid || 'entityForm');
     var inputTypes = ['INPUT', 'SELECT', 'TEXTAREA'];
-    var tabindex = (start==null)?15:start;
-    nodeWalkDepthFirst(form, function(elem) {
+    var tabindex = (start == null) ? 15: start;
+    cw.utils.nodeWalkDepthFirst(form, function(elem) {
         var tagName = elem.tagName.toUpperCase();
-	if (inputTypes.contains(tagName)) {
-	    if (getNodeAttribute(elem, 'tabindex') != null) {
-		tabindex += 1;
-		elem.setAttribute('tabindex', tabindex);
-	    }
-	    return null;
-	}
-	return filter(isElementNode, elem.childNodes);
+        if ($.inArray(tagName, inputTypes)) {
+            if (jQuery(elem).attr('tabindex') != null) {
+                tabindex += 1;
+                jQuery(elem).attr('tabindex', tabindex);
+            }
+            return null;
+        }
+        return jQuery.grep(elem.childNodes, isElementNode);
     });
 }
 
-
 function showMatchingSelect(selectedValue, eid) {
     if (selectedValue) {
-	divId = 'div' + selectedValue + '_' + eid;
-	var divNode = jQuery('#' + divId);
-	if (!divNode.length) {
-	    var args = {vid: 'unrelateddivs', relation: selectedValue,
-			rql: rql_for_eid(eid), '__notemplate': 1,
-			callback: function() {_showMatchingSelect(eid, jQuery('#' + divId));}};
-	    jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
-	} else {
-	    _showMatchingSelect(eid, divNode);
-	}
+        divId = 'div' + selectedValue + '_' + eid;
+        var divNode = jQuery('#' + divId);
+        if (!divNode.length) {
+            var args = {
+                vid: 'unrelateddivs',
+                relation: selectedValue,
+                rql: rql_for_eid(eid),
+                '__notemplate': 1,
+                callback: function() {
+                    _showMatchingSelect(eid, jQuery('#' + divId));
+                }
+            };
+            jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
+        } else {
+            _showMatchingSelect(eid, divNode);
+        }
     } else {
-	_showMatchingSelect(eid, null);
+        _showMatchingSelect(eid, null);
     }
 }
 
-
-// @param divNode is a jQuery selection
+/**
+ * .. function:: _showMatchingSelect(eid, divNode)
+ *
+ * * `divNode`, a jQuery selection
+ */
 function _showMatchingSelect(eid, divNode) {
     // hide all divs, and then show the matching one
     // (would actually be better to directly hide the displayed one)
     jQuery('#unrelatedDivs_' + eid).children().hide();
     // divNode not found means 'no relation selected' (i.e. first blank item)
     if (divNode && divNode.length) {
-	divNode.show();
+        divNode.show();
     }
 }
 
-// this function builds a Handle to cancel pending insertion
+/**
+ * .. function:: buildPendingInsertHandle(elementId, element_name, selectNodeId, eid)
+ *
+ * this function builds a Handle to cancel pending insertion
+ */
 function buildPendingInsertHandle(elementId, element_name, selectNodeId, eid) {
-   jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')";
-   return A({'class' : 'handle', 'href' : jscall,
-	     'title' : _("cancel this insert")}, '[x]');
+    jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')";
+    return A({
+        'class': 'handle',
+        'href': jscall,
+        'title': _("cancel this insert")
+    },
+    '[x]');
 }
 
 function buildEntityLine(relationName, selectedOptionNode, comboId, eid) {
-   // textContent doesn't seem to work on selectedOptionNode
-   var content = selectedOptionNode.firstChild.nodeValue;
-   var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid);
-   var link = A({'href' : 'view?rql=' + selectedOptionNode.value,
-	  	 'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id},
-		content);
-   var tr = TR({'id' : 'tr' + selectedOptionNode.id}, [ TH(null, relationName),
-							TD(null, [handle, link])
-						      ]);
-   try {
-      var separator = getNode('relationSelectorRow_' + eid);
-      //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least)
-      // XXX Warning: separator.parentNode is not (always ?) the
-      // table itself, but an intermediate node (TableSectionElement)
-      var tableBody = separator.parentNode;
-      tableBody.insertBefore(tr, separator);
-   } catch(ex) {
-      log("got exception(2)!" + ex);
-   }
+    // textContent doesn't seem to work on selectedOptionNode
+    var content = selectedOptionNode.firstChild.nodeValue;
+    var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid);
+    var link = A({
+        'href': 'view?rql=' + selectedOptionNode.value,
+        'class': 'editionPending',
+        'id': 'a' + selectedOptionNode.id
+    },
+    content);
+    var tr = TR({
+        'id': 'tr' + selectedOptionNode.id
+    },
+    [TH(null, relationName), TD(null, [handle, link])]);
+    try {
+        var separator = cw.getNode('relationSelectorRow_' + eid);
+        //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least)
+        // XXX Warning: separator.parentNode is not (always ?) the
+        // table itself, but an intermediate node (TableSectionElement)
+        var tableBody = separator.parentNode;
+        tableBody.insertBefore(tr, separator);
+    } catch(ex) {
+        log("got exception(2)!" + ex);
+    }
 }
 
 function buildEntityCell(relationName, selectedOptionNode, comboId, eid) {
     var handle = buildPendingInsertHandle(selectedOptionNode.id, 'div_insert_', comboId, eid);
-    var link = A({'href' : 'view?rql=' + selectedOptionNode.value,
-		  'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id},
-		 content);
-    var div = DIV({'id' : 'div_insert_' + selectedOptionNode.id}, [handle, link]);
+    var link = A({
+        'href': 'view?rql=' + selectedOptionNode.value,
+        'class': 'editionPending',
+        'id': 'a' + selectedOptionNode.id
+    },
+    content);
+    var div = DIV({
+        'id': 'div_insert_' + selectedOptionNode.id
+    },
+    [handle, link]);
     try {
-	var td = jQuery('#cell'+ relationName +'_'+eid);
-	td.appendChild(div);
+        var td = jQuery('#cell' + relationName + '_' + eid);
+        td.appendChild(div);
     } catch(ex) {
-	alert("got exception(3)!" + ex);
+        alert("got exception(3)!" + ex);
     }
 }
 
 function addPendingInsert(optionNode, eid, cell, relname) {
-    var value = getNodeAttribute(optionNode, 'value');
+    var value = jQuery(optionNode).attr('value');
     if (!value) {
-	// occurs when the first element in the box is selected (which is not
-	// an entity but the combobox title)
+        // occurs when the first element in the box is selected (which is not
+        // an entity but the combobox title)
         return;
     }
     // 2nd special case
     if (value.indexOf('http') == 0) {
-	document.location = value;
-	return;
+        document.location = value;
+        return;
     }
     // add hidden parameter
     var entityForm = jQuery('#entityForm');
     var oid = optionNode.id.substring(2); // option id is prefixed by "id"
-    remoteExec('add_pending_inserts', [oid.split(':')]);
+    loadRemote('json', ajaxFuncArgs('add_pending_inserts', null,
+                                    [oid.split(':')]), 'GET', true);
     var selectNode = optionNode.parentNode;
     // remove option node
     selectNode.removeChild(optionNode);
     // add line in table
     if (cell) {
-      // new relation as a cell in multiple edit
-      // var relation_name = relationSelected.getAttribute('value');
-      // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_'));
-      buildEntityCell(relname, optionNode, selectNode.id, eid);
+        // new relation as a cell in multiple edit
+        // var relation_name = relationSelected.getAttribute('value');
+        // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_'));
+        buildEntityCell(relname, optionNode, selectNode.id, eid);
     }
     else {
-	var relationSelector = getNode('relationSelector_'+eid);
-	var relationSelected = relationSelector.options[relationSelector.selectedIndex];
-	// new relation as a line in simple edit
-	buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid);
+        var relationSelector = cw.getNode('relationSelector_' + eid);
+        var relationSelected = relationSelector.options[relationSelector.selectedIndex];
+        // new relation as a line in simple edit
+        buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid);
     }
 }
 
 function cancelPendingInsert(elementId, element_name, comboId, eid) {
     // remove matching insert element
-    var entityView = jqNode('a' + elementId).text();
-    jqNode(element_name + elementId).remove();
+    var entityView = cw.jqNode('a' + elementId).text();
+    cw.jqNode(element_name + elementId).remove();
     if (comboId) {
-	// re-insert option in combobox if it was taken from there
-	var selectNode = getNode(comboId);
+        // re-insert option in combobox if it was taken from there
+        var selectNode = cw.getNode(comboId);
         // XXX what on object relation
-	if (selectNode){
-	   var options = selectNode.options;
-	   var node_id = elementId.substring(0, elementId.indexOf(':'));
-	   options[options.length] = OPTION({'id' : elementId, 'value' : node_id}, entityView);
-	}
+        if (selectNode) {
+            var options = selectNode.options;
+            var node_id = elementId.substring(0, elementId.indexOf(':'));
+            options[options.length] = OPTION({
+                'id': elementId,
+                'value': node_id
+            },
+            entityView);
+        }
     }
     elementId = elementId.substring(2, elementId.length);
-    remoteExec('remove_pending_insert', elementId.split(':'));
+    loadRemote('json', ajaxFuncArgs('remove_pending_inserts', null,
+                                    elementId.split(':')), 'GET', true);
 }
 
-// this function builds a Handle to cancel pending insertion
+/**
+ * .. function:: buildPendingDeleteHandle(elementId, eid)
+ *
+ * this function builds a Handle to cancel pending insertion
+ */
 function buildPendingDeleteHandle(elementId, eid) {
-  var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');";
-  return A({'href' : jscall, 'class' : 'pendingDeleteHandle',
-    'title' : _("delete this relation")}, '[x]');
+    var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');";
+    return A({
+        'href': jscall,
+        'class': 'pendingDeleteHandle',
+        'title': _("delete this relation")
+    },
+    '[x]');
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: addPendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function addPendingDelete(nodeId, eid) {
-    var d = asyncRemoteExec('add_pending_delete', nodeId.split(':'));
-    d.addCallback(function () {
-	// and strike entity view
-	jqNode('span' + nodeId).addClass('pendingDelete');
-	// replace handle text
-	jqNode('handle' + nodeId).text('+');
+    var d = loadRemote('json', ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
+    d.addCallback(function() {
+        // and strike entity view
+        cw.jqNode('span' + nodeId).addClass('pendingDelete');
+        // replace handle text
+        cw.jqNode('handle' + nodeId).text('+');
     });
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: cancelPendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function cancelPendingDelete(nodeId, eid) {
-    var d = asyncRemoteExec('remove_pending_delete', nodeId.split(':'));
-    d.addCallback(function () {
-	// reset link's CSS class
-	jqNode('span' + nodeId).removeClass('pendingDelete');
-	// replace handle text
-	jqNode('handle' + nodeId).text('x');
+    var d = loadRemote('json', ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
+    d.addCallback(function() {
+        // reset link's CSS class
+        cw.jqNode('span' + nodeId).removeClass('pendingDelete');
+        // replace handle text
+        cw.jqNode('handle' + nodeId).text('x');
     });
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: togglePendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function togglePendingDelete(nodeId, eid) {
     // node found means we should cancel deletion
-    if ( hasElementClass(getNode('span' + nodeId), 'pendingDelete') ) {
-	cancelPendingDelete(nodeId, eid);
+    if (jQuery.className.has(cw.getNode('span' + nodeId), 'pendingDelete')) {
+        cancelPendingDelete(nodeId, eid);
     } else {
-	addPendingDelete(nodeId, eid);
+        addPendingDelete(nodeId, eid);
     }
 }
 
-
 function selectForAssociation(tripletIdsString, originalEid) {
-    var tripletlist = map(function (x) { return x.split(':'); },
-			  tripletIdsString.split('-'));
-    var d = asyncRemoteExec('add_pending_inserts', tripletlist);
-    d.addCallback(function () {
-	var args = {vid: 'edition', __mode: 'normal',
-		    rql: rql_for_eid(originalEid)};
-	document.location = 'view?' + asURL(args);
+    var tripletlist = $.map(tripletIdsString.split('-'),
+                            function(x) { return [x.split(':')] ;});
+    var d = loadRemote('json', ajaxFuncArgs('add_pending_inserts', null, tripletlist));
+    d.addCallback(function() {
+        var args = {
+            vid: 'edition',
+            __mode: 'normal',
+            rql: rql_for_eid(originalEid)
+        };
+        document.location = 'view?' + asURL(args);
     });
 
 }
 
-
 function updateInlinedEntitiesCounters(rtype, role) {
-    jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function (i) {
-	this.innerHTML = i+1;
+    jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function(i) {
+        this.innerHTML = i + 1;
     });
 }
 
-
-/*
+/**
+ * .. function:: addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore)
+ *
  * makes an AJAX request to get an inline-creation view's content
- * @param peid : the parent entity eid
- * @param petype : the parent entity type
- * @param ttype : the target (inlined) entity type
- * @param rtype : the relation type between both entities
+ * * `peid`, the parent entity eid
+ *
+ * * `petype`, the parent entity type
+ *
+ * * `ttype`, the target (inlined) entity type
+ *
+ * * `rtype`, the relation type between both entities
  */
 function addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore) {
-    insertBefore = insertBefore || getNode('add' + rtype + ':' + peid + 'link').parentNode;
-    var d = asyncRemoteExec('inline_creation_form', peid, petype, ttype, rtype, role, i18nctx);
-    d.addCallback(function (response) {
+    insertBefore = insertBefore || cw.getNode('add' + rtype + ':' + peid + 'link').parentNode;
+    var args = ajaxFuncArgs('inline_creation_form', null, peid, petype, ttype, rtype, role, i18nctx);
+    var d = loadRemote('json', args);
+    d.addCallback(function(response) {
         var dom = getDomFromResponse(response);
-        preprocessAjaxLoad(null, dom);
+        loadAjaxHtmlHead(dom);
         var form = jQuery(dom);
         form.css('display', 'none');
         form.insertBefore(insertBefore).slideDown('fast');
         updateInlinedEntitiesCounters(rtype, role);
         reorderTabindex(null, $(insertBefore).closest('form')[0]);
-        jQuery(CubicWeb).trigger('inlinedform-added', form);
+        jQuery(cw).trigger('inlinedform-added', form);
         // if the inlined form contains a file input, we must force
         // the form enctype to multipart/form-data
         if (form.find('input:file').length) {
-	    // NOTE: IE doesn't support dynamic enctype modification, we have
-	    //       to set encoding too.
-            form.closest('form').attr('enctype', 'multipart/form-data')
-		.attr('encoding', 'multipart/form-data');
+            // NOTE: IE doesn't support dynamic enctype modification, we have
+            //       to set encoding too.
+            form.closest('form').attr('enctype', 'multipart/form-data').attr('encoding', 'multipart/form-data');
         }
-        postAjaxLoad(dom);
+        _postAjaxLoad(dom);
     });
-    d.addErrback(function (xxx) {
+    d.addErrback(function(xxx) {
         log('xxx =', xxx);
     });
 }
 
-/*
+/**
+ * .. function:: removeInlineForm(peid, rtype, role, eid, showaddnewlink)
+ *
  * removes the part of the form used to edit an inlined entity
  */
 function removeInlineForm(peid, rtype, role, eid, showaddnewlink) {
-    jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() {
-	$(this).remove();
-	updateInlinedEntitiesCounters(rtype, role);
+    cw.jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() {
+            $(this).remove();
+            updateInlinedEntitiesCounters(rtype, role);
     });
     if (showaddnewlink) {
-	toggleVisibility(showaddnewlink);
+        toggleVisibility(showaddnewlink);
     }
 }
 
-/*
+/**
+ * .. function:: removeInlinedEntity(peid, rtype, eid)
+ *
  * alternatively adds or removes the hidden input that make the
  * edition of the relation `rtype` possible between `peid` and `eid`
- * @param peid : the parent entity eid
- * @param rtype : the relation type between both entities
- * @param eid : the inlined entity eid
+ * * `peid`, the parent entity eid
+ *
+ * * `rtype`, the relation type between both entities
+ *
+ * * `eid`, the inlined entity eid
  */
 function removeInlinedEntity(peid, rtype, eid) {
     // XXX work around the eid_param thing (eid + ':' + eid) for #471746
     var nodeid = ['rel', peid, rtype, eid + ':' + eid].join('-');
-    var node = jqNode(nodeid);
-    if (! node.attr('cubicweb:type')) {
+    var node = cw.jqNode(nodeid);
+    if (!node.attr('cubicweb:type')) {
         node.attr('cubicweb:type', node.val());
         node.val('');
-	var divid = ['div', peid, rtype, eid].join('-');
-	jqNode(divid).fadeTo('fast', 0.5);
-	var noticeid = ['notice', peid, rtype, eid].join('-');
-	jqNode(noticeid).fadeIn('fast');
+        var divid = ['div', peid, rtype, eid].join('-');
+        cw.jqNode(divid).fadeTo('fast', 0.5);
+        var noticeid = ['notice', peid, rtype, eid].join('-');
+        cw.jqNode(noticeid).fadeIn('fast');
     }
 }
 
 function restoreInlinedEntity(peid, rtype, eid) {
     // XXX work around the eid_param thing (eid + ':' + eid) for #471746
     var nodeid = ['rel', peid, rtype, eid + ':' + eid].join('-');
-    var node = jqNode(nodeid);
+    var node = cw.jqNode(nodeid);
     if (node.attr('cubicweb:type')) {
         node.val(node.attr('cubicweb:type'));
         node.attr('cubicweb:type', '');
-	jqNode(['fs', peid, rtype, eid].join('-')).append(node);
+        cw.jqNode(['fs', peid, rtype, eid].join('-')).append(node);
         var divid = ['div', peid, rtype, eid].join('-');
-	jqNode(divid).fadeTo('fast', 1);
+        cw.jqNode(divid).fadeTo('fast', 1);
         var noticeid = ['notice', peid, rtype, eid].join('-');
-	jqNode(noticeid).hide();
+        cw.jqNode(noticeid).hide();
     }
 }
 
 function _clearPreviousErrors(formid) {
     // on some case (eg max request size exceeded, we don't know the formid
     if (formid) {
-	jQuery('#' + formid + 'ErrorMessage').remove();
-	jQuery('#' + formid + ' span.errorMsg').remove();
-	jQuery('#' + formid + ' .error').removeClass('error');
+        jQuery('#' + formid + 'ErrorMessage').remove();
+        jQuery('#' + formid + ' span.errorMsg').remove();
+        jQuery('#' + formid + ' .error').removeClass('error');
     } else {
-	jQuery('span.errorMsg').remove();
-	jQuery('.error').removeClass('error');
+        jQuery('span.errorMsg').remove();
+        jQuery('.error').removeClass('error');
     }
 }
 
@@ -336,60 +403,66 @@
     var globalerrors = [];
     var firsterrfield = null;
     for (fieldname in errors) {
-	var errmsg = errors[fieldname];
-	if (!fieldname) {
-	    globalerrors.push(errmsg);
-	} else {
-	    var fieldid = fieldname + ':' + eid;
-	    var suffixes = ['', '-subject', '-object'];
-	    var found = false;
-	    // XXX remove suffixes at some point
-	    for (var i=0, length=suffixes.length; i<length;i++) {
-		var field = jqNode(fieldname + suffixes[i] + ':' + eid);
-		if (field && getNodeAttribute(field, 'type') != 'hidden') {
-		    if ( !firsterrfield ) {
-			firsterrfield = 'err-' + fieldid;
-		    }
-		    addElementClass(field, 'error');
-		    var span = SPAN({'id': 'err-' + fieldid, 'class': "errorMsg"}, errmsg);
-		    field.before(span);
-		    found = true;
-		    break;
-		}
-	    }
-	    if (!found) {
-		firsterrfield = formid;
-		globalerrors.push(_(fieldname) + ' : ' + errmsg);
-	    }
-	}
+        var errmsg = errors[fieldname];
+        if (!fieldname) {
+            globalerrors.push(errmsg);
+        } else {
+            var fieldid = fieldname + ':' + eid;
+            var suffixes = ['', '-subject', '-object'];
+            var found = false;
+            // XXX remove suffixes at some point
+            for (var i = 0, length = suffixes.length; i < length; i++) {
+                var field = cw.jqNode(fieldname + suffixes[i] + ':' + eid);
+                if (field && jQuery(field).attr('type') != 'hidden') {
+                    if (!firsterrfield) {
+                        firsterrfield = 'err-' + fieldid;
+                    }
+                    jQuery(field).addClass('error');
+                    var span = SPAN({
+                        'id': 'err-' + fieldid,
+                        'class': "errorMsg"
+                    },
+                    errmsg);
+                    field.before(span);
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                firsterrfield = formid;
+                globalerrors.push(_(fieldname) + ' : ' + errmsg);
+            }
+        }
     }
     if (globalerrors.length) {
-	if (globalerrors.length == 1) {
-	    var innernode = SPAN(null, globalerrors[0]);
-	} else {
-	    var innernode = UL(null, map(partial(LI, null), globalerrors));
-	}
-	// insert DIV and innernode before the form
-	var div = DIV({'class' : "errorMessage", 'id': formid + 'ErrorMessage'});
-	div.appendChild(innernode);
-	jQuery('#' + formid).before(div);
+        if (globalerrors.length == 1) {
+            var innernode = SPAN(null, globalerrors[0]);
+        } else {
+            var innernode = UL(null, $.map(globalerrors, partial(LI, null)));
+        }
+        // insert DIV and innernode before the form
+        var div = DIV({
+            'class': "errorMessage",
+            'id': formid + 'ErrorMessage'
+        });
+        div.appendChild(innernode);
+        jQuery('#' + formid).before(div);
     }
     return firsterrfield || formid;
 }
 
-
 function handleFormValidationResponse(formid, onsuccess, onfailure, result, cbargs) {
     // Success
     if (result[0]) {
-	if (onsuccess) {
-             onsuccess(result, formid, cbargs);
-	} else {
-	    document.location.href = result[1];
-	}
-      return true;
+        if (onsuccess) {
+            onsuccess(result, formid, cbargs);
+        } else {
+            document.location.href = result[1];
+        }
+        return true;
     }
-    if (onfailure && !onfailure(result, formid, cbargs)) {
-	return false;
+    if (onfailure && ! onfailure(result, formid, cbargs)) {
+        return false;
     }
     unfreezeFormButtons(formid);
     // Failures
@@ -397,11 +470,11 @@
     var descr = result[1];
     var errmsg;
     // Unknown structure
-    if ( !isArrayLike(descr) || descr.length != 2 ) {
-	errmsg = descr;
+    if ( !cw.utils.isArrayLike(descr) || descr.length != 2 ) {
+        errmsg = descr;
     } else {
-	_displayValidationerrors(formid, descr[0], descr[1]);
-	errmsg = _('please correct errors below');
+        _displayValidationerrors(formid, descr[0], descr[1]);
+        errmsg = _('please correct errors below');
     }
     updateMessage(errmsg);
     // ensure the browser does not scroll down
@@ -409,68 +482,101 @@
     return false;
 }
 
-
-/* unfreeze form buttons when the validation process is over*/
+/**
+ * .. function:: unfreezeFormButtons(formid)
+ *
+ * unfreeze form buttons when the validation process is over
+ */
 function unfreezeFormButtons(formid) {
     jQuery('#progress').hide();
     // on some case (eg max request size exceeded, we don't know the formid
     if (formid) {
-	jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
+        jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
     } else {
-	jQuery('.validateButton').removeAttr('disabled');
+        jQuery('.validateButton').removeAttr('disabled');
     }
     return true;
 }
 
-/* disable form buttons while the validation is being done */
+/**
+ * .. function:: freezeFormButtons(formid)
+ *
+ * disable form buttons while the validation is being done
+ */
 function freezeFormButtons(formid) {
     jQuery('#progress').show();
     jQuery('#' + formid + ' .validateButton').attr('disabled', 'disabled');
     return true;
 }
 
-/* used by additional submit buttons to remember which button was clicked */
+/**
+ * .. function:: postForm(bname, bvalue, formid)
+ *
+ * used by additional submit buttons to remember which button was clicked
+ */
 function postForm(bname, bvalue, formid) {
-    var form = getNode(formid);
+    var form = cw.getNode(formid);
     if (bname) {
-	var child = form.appendChild(INPUT({type: 'hidden', name: bname, value: bvalue}));
+        var child = form.appendChild(INPUT({
+            type: 'hidden',
+            name: bname,
+            value: bvalue
+        }));
     }
     var onsubmit = form.onsubmit;
     if (!onsubmit || (onsubmit && onsubmit())) {
-	form.submit();
+        form.submit();
     }
     if (bname) {
-	jQuery(child).remove(); /* cleanup */
+        jQuery(child).remove();
     }
 }
 
-
-/* called on load to set target and iframeso object.
- * NOTE: this is a hack to make the XHTML compliant.
- * NOTE2: `object` nodes might be a potential replacement for iframes
- * NOTE3: there is a XHTML module allowing iframe elements but there
- *        is still the problem of the form's `target` attribute
+/**
+ * .. function:: setFormsTarget(node)
+ *
+ * called on load to set target and iframeso object.
+ *
+ * .. note::
+ *
+ *    this is a hack to make the XHTML compliant.
+ *
+ * .. note::
+ *
+ *   `object` nodes might be a potential replacement for iframes
+ *
+ * .. note::
+ *
+ *    there is a XHTML module allowing iframe elements but there
+ *    is still the problem of the form's `target` attribute
  */
 function setFormsTarget(node) {
     var $node = jQuery(node || document.body);
-    $node.find('form').each(function () {
-	var form = jQuery(this);
-	var target = form.attr('cubicweb:target');
-	if (target) {
-	    form.attr('target', target);
-	    /* do not use display: none because some browsers ignore iframe
+    $node.find('form').each(function() {
+        var form = jQuery(this);
+        var target = form.attr('cubicweb:target');
+        if (target) {
+            form.attr('target', target);
+            /* do not use display: none because some browsers ignore iframe
              * with no display */
-	    form.append(IFRAME({name: target, id: target,
-				src: 'javascript: void(0)',
-				width: '0px', height: '0px'}));
-	}
+            form.append(IFRAME({
+                name: target,
+                id: target,
+                src: 'javascript: void(0)',
+                width: '0px',
+                height: '0px'
+            }));
+        }
     });
 }
 
-jQuery(document).ready(function() {setFormsTarget();});
+jQuery(document).ready(function() {
+    setFormsTarget();
+});
 
-
-/*
+/**
+ * .. function:: validateForm(formid, action, onsuccess, onfailure)
+ *
  * called on traditionnal form submission : the idea is to try
  * to post the form. If the post is successful, `validateForm` redirects
  * to the appropriate URL. Otherwise, the validation errors are displayed
@@ -478,77 +584,183 @@
  */
 function validateForm(formid, action, onsuccess, onfailure) {
     try {
-	var zipped = formContents(formid);
-	var d = asyncRemoteExec('validate_form', action, zipped[0], zipped[1]);
-    } catch (ex) {
-	log('got exception', ex);
-	return false;
+        var zipped = cw.utils.formContents(formid);
+        var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
+        var d = loadRemote('json', args, 'POST');
+    } catch(ex) {
+        log('got exception', ex);
+        return false;
     }
     function _callback(result, req) {
-	handleFormValidationResponse(formid, onsuccess, onfailure, result);
+        handleFormValidationResponse(formid, onsuccess, onfailure, result);
     }
     d.addCallback(_callback);
     return false;
 }
 
 
-/*
+
+// ======================= DEPRECATED FUNCTIONS ========================= //
+// (mostly reledit related)
+/**
+ * .. function:: inlineValidateRelationFormOptions(rtype, eid, divid, options)
+ *
  * called by reledit forms to submit changes
- * @param formid : the dom id of the form used
- * @param rtype : the attribute being edited
- * @param eid : the eid of the entity being edited
- * @param reload: boolean to reload page if true (when changing URL dependant data)
- * @param default_value : value if the field is empty
- * @param lzone : html fragment (string) for a clic-zone triggering actual edition
+ * * `rtype`, the attribute being edited
+ *
+ * * `eid`, the eid of the entity being edited
+ *
+ * * `options`, a dictionnary of options used by the form validation handler such
+ *    as ``role``, ``onsuccess``, ``onfailure``, ``reload``, ``vid``, ``lzone``
+ *    and ``default_value``:
+ *
+ *     * `onsucess`, javascript function to execute on success, default is noop
+ *
+ *     * `onfailure`, javascript function to execute on failure, default is noop
+ *
+ *     * `default_value`, value if the field is empty
+ *
+ *     * `lzone`, html fragment (string) for a clic-zone triggering actual edition
  */
-function inlineValidateRelationForm(rtype, role, eid, divid, reload, vid,
-                                    default_value, lzone) {
-    try {
-	var form = getNode(divid+'-form');
-        var relname = rtype + ':' + eid;
-        var newtarget = jQuery('[name=' + relname + ']').val();
-	var zipped = formContents(form);
-	var d = asyncRemoteExec('validate_form', 'apply', zipped[0], zipped[1]);
-    } catch (ex) {
-	return false;
+
+
+showInlineEditionForm = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function showInlineEditionForm(eid, rtype, divid) {
+        jQuery('#' + divid).hide();
+        jQuery('#' + divid + '-value').hide();
+        jQuery('#' + divid + '-form').show();
+    }
+);
+
+hideInlineEdit = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function hideInlineEdit(eid, rtype, divid) {
+        jQuery('#appMsg').hide();
+        jQuery('div.errorMessage').remove();
+        jQuery('#' + divid).show();
+        jQuery('#' + divid + '-value').show();
+        jQuery('#' + divid + '-form').hide();
     }
-    d.addCallback(function (result, req) {
-	if (handleFormValidationResponse(divid+'-form', noop, noop, result)) {
-          if (reload) {
-            document.location.reload();
-          } else {
-              var args = {fname: 'reledit_form', rtype: rtype, role: role, eid: eid, divid: divid,
-                          reload: reload, vid: vid, default_value: default_value, landing_zone: lzone};
-              jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
-          }
-	}
+);
+
+
+inlineValidateRelationFormOptions = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function inlineValidateRelationFormOptions(rtype, eid, divid, options) {
+        try {
+            var form = cw.getNode(divid + '-form');
+            var relname = rtype + ':' + eid;
+            var newtarget = jQuery('[name=' + relname + ']').val();
+            var zipped = cw.utils.formContents(form);
+            var args = ajaxFuncArgs('validate_form', null, 'apply', zipped[0], zipped[1]);
+            var d = loadRemote(JSON_BASE_URL, args, 'POST');
+        } catch(ex) {
+            return false;
+        }
+        d.addCallback(function(result, req) {
+            execFormValidationResponse(rtype, eid, divid, options, result);
+        });
         return false;
     });
-  return false;
-}
+
+execFormValidationResponse = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function execFormValidationResponse(rtype, eid, divid, options, result) {
+        options = $.extend({onsuccess: noop,
+                            onfailure: noop
+                           }, options);
+        if (handleFormValidationResponse(divid + '-form', options.onsucess , options.onfailure, result)) {
+            if (options.reload) {
+                document.location.reload();
+            } else {
+                var args = {
+                    fname: 'reledit_form',
+                    rtype: rtype,
+                    role: options.role,
+                    eid: eid,
+                    divid: divid,
+                    reload: options.reload,
+                    vid: options.vid,
+                    default_value: options.default_value,
+                    landing_zone: options.lzone
+                };
+                jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+            }
+        }
+});
 
 
-/**** inline edition ****/
-function loadInlineEditionForm(eid, rtype, role, divid, reload, vid,
-                               default_value, lzone) {
-  var args = {fname: 'reledit_form', rtype: rtype, role: role, eid: eid, divid: divid,
-              reload: reload, vid: vid, default_value: default_value, landing_zone: lzone,
-              callback: function () {showInlineEditionForm(eid, rtype, divid);}};
-  jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
-}
+/**
+ * .. function:: loadInlineEditionFormOptions(eid, rtype, divid, options)
+ *
+ * inline edition
+ */
+loadInlineEditionFormOptions = cw.utils.deprecatedFunction(
+  '[3.9] this is now unused by reledit (see cw.reledit.js) ',
+  function loadInlineEditionFormOptions(eid, rtype, divid, options) {
+    var args = {
+        fname: 'reledit_form',
+        rtype: rtype,
+        role: options.role,
+        eid: eid,
+        divid: divid,
+        reload: options.reload,
+        vid: options.vid,
+        default_value: options.default_value,
+        landing_zone: options.lzone,
+        callback: function() {
+            showInlineEditionForm(eid, rtype, divid);
+        }
+    };
+    jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+});
+
 
-function showInlineEditionForm(eid, rtype, divid) {
-    jQuery('#' + divid).hide();
-    jQuery('#' + divid + '-value' ).hide();
-    jQuery('#' + divid+ '-form').show();
-}
+inlineValidateRelationForm = cw.utils.deprecatedFunction(
+    '[3.9] inlineValidateRelationForm() function is deprecated, use inlineValidateRelationFormOptions instead',
+    function(rtype, role, eid, divid, reload, vid, default_value, lzone, onsucess, onfailure) {
+        try {
+            var form = cw.getNode(divid + '-form');
+            var relname = rtype + ':' + eid;
+            var newtarget = jQuery('[name=' + relname + ']').val();
+            var zipped = cw.utils.formContents(form);
+            var d = asyncRemoteExec('validate_form', 'apply', zipped[0], zipped[1]);
+        } catch(ex) {
+            return false;
+        }
+        d.addCallback(function(result, req) {
+        var options = {role : role,
+                       reload: reload,
+                       vid: vid,
+                       default_value: default_value,
+                       lzone: lzone,
+                       onsucess: onsucess || $.noop,
+                       onfailure: onfailure || $.noop
+                      };
+            execFormValidationResponse(rtype, eid, divid, options);
+        });
+        return false;
+    }
+);
 
-function hideInlineEdit(eid, rtype, divid) {
-    jQuery('#appMsg').hide();
-    jQuery('div.errorMessage').remove();
-    jQuery('#' + divid).show();
-    jQuery('#' + divid + '-value').show();
-    jQuery('#' + divid +'-form').hide();
-}
-
-CubicWeb.provide('edition.js');
+loadInlineEditionForm = cw.utils.deprecatedFunction(
+    '[3.9] loadInlineEditionForm() function is deprecated, use loadInlineEditionFormOptions instead',
+    function(eid, rtype, role, divid, reload, vid, default_value, lzone) {
+        var args = {
+            fname: 'reledit_form',
+            rtype: rtype,
+            role: role,
+            eid: eid,
+            divid: divid,
+            reload: reload,
+            vid: vid,
+            default_value: default_value,
+            landing_zone: lzone,
+            callback: function() {
+                showInlineEditionForm(eid, rtype, divid);
+            }
+        };
+        jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+    }
+);
--- a/web/data/cubicweb.facets.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.facets.css	Wed Nov 03 16:38:28 2010 +0100
@@ -91,6 +91,7 @@
 
 .facetValueDisabled {
   font-style: italic;
+  text-decoration: line-through;
 }
 
 
--- a/web/data/cubicweb.facets.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.facets.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,228 +1,248 @@
-/*
+/** filter form, aka facets, javascript functions
+ *
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
-CubicWeb.require('htmlhelpers.js');
-CubicWeb.require('ajax.js');
+var SELECTED_IMG = baseuri() + "data/black-check.png";
+var UNSELECTED_IMG = baseuri() + "data/no-check-no-border.png";
+var UNSELECTED_BORDER_IMG = baseuri() + "data/black-uncheck.png";
+
 
-//============= filter form functions ========================================//
 function copyParam(origparams, newparams, param) {
-    var index = findValue(origparams[0], param);
-    if (index > -1) {
-	newparams[param] = origparams[1][index];
+    var index = jQuery.inArray(param, origparams[0]);
+    if (index > - 1) {
+        newparams[param] = origparams[1][index];
     }
 }
 
-function facetFormContent(form) {
+
+function facetFormContent($form) {
     var names = [];
     var values = [];
-    jQuery(form).find('.facet').each(function () {
+    $form.find('.facet').each(function() {
         var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
         var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) {
-  	    names.push(facetName);
-  	    values.push(this.getAttribute('cubicweb:value'));
+            names.push(facetName);
+            values.push(this.getAttribute('cubicweb:value'));
         });
     });
-    jQuery(form).find('input').each(function () {
+    $form.find('input').each(function() {
         names.push(this.name);
         values.push(this.value);
     });
-    jQuery(form).find('select option[selected]').each(function () {
-	names.push(this.parentNode.name);
-	values.push(this.value);
+    $form.find('select option[selected]').each(function() {
+        names.push(this.parentNode.name);
+        values.push(this.value);
     });
     return [names, values];
 }
 
+
 function buildRQL(divid, vid, paginate, vidargs) {
     jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
-    var form = getNode(divid+'Form');
-    var zipped = facetFormContent(form);
+    var $form = $('#' + divid + 'Form');
+    var zipped = facetFormContent($form);
     zipped[0].push('facetargs');
     zipped[1].push(vidargs);
-    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
+    var d = loadRemote('json', ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
     d.addCallback(function(result) {
-	var rql = result[0];
-	var $bkLink = jQuery('#facetBkLink');
-	if ($bkLink.length) {
-	    var bkPath = 'view?rql=' + escape(rql);
-	    if (vid) {
-		bkPath += '&vid=' + escape(vid);
-	    }
-	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + escape(bkPath);
-	    $bkLink.attr('href', bkUrl);
-	}
-	var toupdate = result[1];
-	var extraparams = vidargs;
-	if (paginate) { extraparams['paginate'] = '1'; } // XXX in vidargs
-	// copy some parameters
-	// XXX cleanup vid/divid mess
-	// if vid argument is specified , the one specified in form params will
-	// be overriden by replacePageChunk
-	copyParam(zipped, extraparams, 'vid');
-	extraparams['divid'] = divid;
-	copyParam(zipped, extraparams, 'divid');
-	copyParam(zipped, extraparams, 'subvid');
-	copyParam(zipped, extraparams, 'fromformfilter');
-	// paginate used to know if the filter box is acting, in which case we
-	// want to reload action box to match current selection (we don't want
-	// this from a table filter)
-	replacePageChunk(divid, rql, vid, extraparams, true, function() {
-	  jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
-	});
-	if (paginate) {
-	    // FIXME the edit box might not be displayed in which case we don't
-	    // know where to put the potential new one, just skip this case
-	    // for now
-	    if (jQuery('#edit_box').length) {
-		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
-	    }
-	    if (jQuery('#breadcrumbs').length) {
-		reloadComponent('breadcrumbs', rql, 'components', 'breadcrumbs');
-	    }
-	}
-	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
-	d.addCallback(function(updateMap) {
-	    for (facetId in updateMap) {
-		var values = updateMap[facetId];
-		jqNode(facetId).find('.facetCheckBox').each(function () {
-		    var value = this.getAttribute('cubicweb:value');
-		    if (!values.contains(value)) {
-			if (!jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).addClass('facetValueDisabled');
-			}
-		    } else {
-			if (jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).removeClass('facetValueDisabled');
-			}
-		    }
-		});
-	    }
-	});
+        var rql = result[0];
+        var $bkLink = jQuery('#facetBkLink');
+        if ($bkLink.length) {
+            var bkPath = 'view?rql=' + escape(rql);
+            if (vid) {
+                bkPath += '&vid=' + escape(vid);
+            }
+            var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + escape(bkPath);
+            $bkLink.attr('href', bkUrl);
+        }
+        var toupdate = result[1];
+        var extraparams = vidargs;
+        if (paginate) { extraparams['paginate'] = '1'; } // XXX in vidargs
+        // copy some parameters
+        // XXX cleanup vid/divid mess
+        // if vid argument is specified , the one specified in form params will
+        // be overriden by replacePageChunk
+        copyParam(zipped, extraparams, 'vid');
+        extraparams['divid'] = divid;
+        copyParam(zipped, extraparams, 'divid');
+        copyParam(zipped, extraparams, 'subvid');
+        copyParam(zipped, extraparams, 'fromformfilter');
+        // paginate used to know if the filter box is acting, in which case we
+        // want to reload action box to match current selection (we don't want
+        // this from a table filter)
+        extraparams['rql'] = rql;
+        if (vid) { // XXX see copyParam above. Need cleanup
+            extraparams['vid'] = vid;
+        }
+        d = $('#' + divid).loadxhtml('json', ajaxFuncArgs('view', extraparams),
+                                     null, 'swap');
+        d.addCallback(function() {
+            // XXX rql/vid in extraparams
+            jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
+        });
+        if (paginate) {
+            // FIXME the edit box might not be displayed in which case we don't
+            // know where to put the potential new one, just skip this case for
+            // now
+            var $node = jQuery('#edit_box');
+            if ($node.length) {
+                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                    'rql': rql
+                },
+                'boxes', 'edit_box'));
+            }
+            $node = jQuery('#breadcrumbs')
+            if ($node.length) {
+                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                    'rql': rql
+                },
+                'components', 'breadcrumbs'));
+            }
+        }
+        var d = loadRemote('json', ajaxFuncArgs('filter_select_content', null, toupdate, rql));
+        d.addCallback(function(updateMap) {
+            for (facetId in updateMap) {
+                var values = updateMap[facetId];
+                cw.jqNode(facetId).find('.facetCheckBox').each(function() {
+                    var value = this.getAttribute('cubicweb:value');
+                    if (jQuery.inArray(value, values) == -1) {
+                        if (!jQuery(this).hasClass('facetValueDisabled')) {
+                            jQuery(this).addClass('facetValueDisabled');
+                        }
+                    } else {
+                        if (jQuery(this).hasClass('facetValueDisabled')) {
+                            jQuery(this).removeClass('facetValueDisabled');
+                        }
+                    }
+                });
+            }
+        });
     });
 }
 
 
-var SELECTED_IMG = baseuri()+"data/black-check.png";
-var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png";
-var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png";
-
 function initFacetBoxEvents(root) {
     // facetargs : (divid, vid, paginate, extraargs)
     root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	// NOTE: don't evaluate facetargs here but in callbacks since its value
-	//       may changes and we must send its value when the callback is
-	//       called, not when the page is initialized
-	var facetargs = form.attr('cubicweb:facetargs');
-	if (facetargs !== undefined) {
-	    form.submit(function() {
-	        buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-	        return false;
-	    });
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		facet.find('div.facetCheckBox').each(function (i) {
-		    this.setAttribute('cubicweb:idx', i);
-		});
-		facet.find('div.facetCheckBox').click(function () {
-		    var $this = jQuery(this);
-		    // NOTE : add test on the facet operator (i.e. OR, AND)
-		    // if ($this.hasClass('facetValueDisabled')){
-		    //  	    return
-		    // }
-		    if ($this.hasClass('facetValueSelected')) {
-			$this.removeClass('facetValueSelected');
-			$this.find('img').each(function (i){
-			if (this.getAttribute('cubicweb:unselimg')){
-			       this.setAttribute('src', UNSELECTED_BORDER_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			    else{
-			       this.setAttribute('src', UNSELECTED_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			});
-			var index = parseInt($this.attr('cubicweb:idx'));
-			// we dont need to move the element when cubicweb:idx == 0
-			if (index > 0){
-			    var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) {
-				    var nindex = parseInt(n.getAttribute('cubicweb:idx'));
-				    return nindex > index;
-				}).length;
-			    index += shift;
-			    var parent = this.parentNode;
-			    var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')');
-			    if ( ! ($insertAfter.length == 1 && shift == 0) ) {
-				// only rearrange element if necessary
-				$insertAfter.after(this);
-			    }
-			}
-		    } else {
-			var lastSelected = facet.find('.facetValueSelected:last');
-			if (lastSelected.length) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			jQuery(this).addClass('facetValueSelected');
-			var $img = jQuery(this).find('img');
-			$img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
-		    }
-		    buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    facet.find('.facetBody').animate({scrollTop: 0}, '');
-		});
-		facet.find('select.facetOperator').change(function() {
-		    var nbselected = facet.find('div.facetValueSelected').length;
-		    if (nbselected >= 2) {
-			buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    }
-		});
-		facet.find('div.facetTitle').click(function() {
-		  facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
-		  jQuery(this).toggleClass('opened');
-		   });
+    jQuery(root).find('form').each(function() {
+        var form = jQuery(this);
+        // NOTE: don't evaluate facetargs here but in callbacks since its value
+        //       may changes and we must send its value when the callback is
+        //       called, not when the page is initialized
+        var facetargs = form.attr('cubicweb:facetargs');
+        if (facetargs !== undefined) {
+            form.submit(function() {
+                buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                return false;
+            });
+            form.find('div.facet').each(function() {
+                var facet = jQuery(this);
+                facet.find('div.facetCheckBox').each(function(i) {
+                    this.setAttribute('cubicweb:idx', i);
+                });
+                facet.find('div.facetCheckBox').click(function() {
+                    var $this = jQuery(this);
+                    // NOTE : add test on the facet operator (i.e. OR, AND)
+                    // if ($this.hasClass('facetValueDisabled')){
+                    //          return
+                    // }
+                    if ($this.hasClass('facetValueSelected')) {
+                        $this.removeClass('facetValueSelected');
+                        $this.find('img').each(function(i) {
+                            if (this.getAttribute('cubicweb:unselimg')) {
+                                this.setAttribute('src', UNSELECTED_BORDER_IMG);
+                                this.setAttribute('alt', (_('not selected')));
+                            }
+                            else {
+                                this.setAttribute('src', UNSELECTED_IMG);
+                                this.setAttribute('alt', (_('not selected')));
+                            }
+                        });
+                        var index = parseInt($this.attr('cubicweb:idx'));
+                        // we dont need to move the element when cubicweb:idx == 0
+                        if (index > 0) {
+                            var shift = jQuery.grep(facet.find('.facetValueSelected'), function(n) {
+                                var nindex = parseInt(n.getAttribute('cubicweb:idx'));
+                                return nindex > index;
+                            }).length;
+                            index += shift;
+                            var parent = this.parentNode;
+                            var $insertAfter = jQuery(parent).find('.facetCheckBox:nth(' + index + ')');
+                            if (! ($insertAfter.length == 1 && shift == 0)) {
+                                // only rearrange element if necessary
+                                $insertAfter.after(this);
+                            }
+                        }
+                    } else {
+                        var lastSelected = facet.find('.facetValueSelected:last');
+                        if (lastSelected.length) {
+                            lastSelected.after(this);
+                        } else {
+                            var parent = this.parentNode;
+                            jQuery(parent).prepend(this);
+                        }
+                        jQuery(this).addClass('facetValueSelected');
+                        var $img = jQuery(this).find('img');
+                        $img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
+                    }
+                    buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                    facet.find('.facetBody').animate({
+                        scrollTop: 0
+                    },
+                    '');
+                });
+                facet.find('select.facetOperator').change(function() {
+                    var nbselected = facet.find('div.facetValueSelected').length;
+                    if (nbselected >= 2) {
+                        buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                    }
+                });
+                facet.find('div.facetTitle').click(function() {
+                    facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
+                    jQuery(this).toggleClass('opened');
+                });
 
-	    });
-	}
+            });
+        }
     });
 }
 
+
 // trigger this function on document ready event if you provide some kind of
 // persistent search (eg crih)
-function reorderFacetsItems(root){
+function reorderFacetsItems(root) {
     root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	if (form.attr('cubicweb:facetargs')) {
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		var lastSelected = null;
-		facet.find('div.facetCheckBox').each(function (i) {
-		    var $this = jQuery(this);
-		    if ($this.hasClass('facetValueSelected')) {
-			if (lastSelected) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			lastSelected = $this;
-		    }
-		});
-	    });
-	}
+    jQuery(root).find('form').each(function() {
+        var form = jQuery(this);
+        if (form.attr('cubicweb:facetargs')) {
+            form.find('div.facet').each(function() {
+                var facet = jQuery(this);
+                var lastSelected = null;
+                facet.find('div.facetCheckBox').each(function(i) {
+                    var $this = jQuery(this);
+                    if ($this.hasClass('facetValueSelected')) {
+                        if (lastSelected) {
+                            lastSelected.after(this);
+                        } else {
+                            var parent = this.parentNode;
+                            jQuery(parent).prepend(this);
+                        }
+                        lastSelected = $this;
+                    }
+                });
+            });
+        }
     });
 }
 
-// we need to differenciate cases where initFacetBoxEvents is called
-// with one argument or without any argument. If we use `initFacetBoxEvents`
-// as the direct callback on the jQuery.ready event, jQuery will pass some argument
-// of his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {initFacetBoxEvents();});
 
-CubicWeb.provide('facets.js');
+// we need to differenciate cases where initFacetBoxEvents is called with one
+// argument or without any argument. If we use `initFacetBoxEvents` as the
+// direct callback on the jQuery.ready event, jQuery will pass some argument of
+// his, so we use this small anonymous function instead.
+jQuery(document).ready(function() {
+    initFacetBoxEvents();
+});
--- a/web/data/cubicweb.fckcwconfig-full.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.fckcwconfig-full.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,28 +1,28 @@
 // cf /usr/share/fckeditor/fckconfig.js
 
-FCKConfig.AutoDetectLanguage	= false ;
+FCKConfig.AutoDetectLanguage = false ;
 
 FCKConfig.ToolbarSets["Default"] = [
     // removed : 'Save','NewPage','DocProps','-','Templates','-','Preview'
-	['Source'],
+        ['Source'],
     // removed: 'Print','-','SpellCheck'
-	['Cut','Copy','Paste','PasteText','PasteWord'],
-	['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
+        ['Cut','Copy','Paste','PasteText','PasteWord'],
+        ['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
     //['Form','Checkbox','Radio','TextField','Textarea','Select','Button','ImageButton','HiddenField'],
-	'/',
+        '/',
     // ,'StrikeThrough','-','Subscript','Superscript'
-	['Bold','Italic','Underline'],
+        ['Bold','Italic','Underline'],
     // ,'-','Outdent','Indent','Blockquote'
-	['OrderedList','UnorderedList'],
+        ['OrderedList','UnorderedList'],
     // ['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],
-	['Link','Unlink','Anchor'],
+        ['Link','Unlink','Anchor'],
     // removed : 'Image','Flash','Smiley','PageBreak'
-	['Table','Rule','SpecialChar']
+        ['Table','Rule','SpecialChar']
     // , '/',
     // ['Style','FontFormat','FontName','FontSize'],
     // ['TextColor','BGColor'],
     //,'ShowBlocks'
-    // ['FitWindow','-','About']		// No comma for the last row.
+    // ['FitWindow','-','About']                // No comma for the last row.
 ] ;
 
 // 'Flash','Select','Textarea','Checkbox','Radio','TextField','HiddenField','ImageButton','Button','Form',
--- a/web/data/cubicweb.flot.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.flot.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,14 +1,14 @@
 function showTooltip(x, y, contents) {
-    $('<div id="tooltip">' + contents + '</div>').css( {
-            position: 'absolute',
+    $('<div id="tooltip">' + contents + '</div>').css({
+        position: 'absolute',
         display: 'none',
         top: y + 5,
-            left: x + 5,
-            border: '1px solid #fdd',
-            padding: '2px',
-            'background-color': '#fee',
-            opacity: 0.80
-        }).appendTo("body").fadeIn(200);
+        left: x + 5,
+        border: '1px solid #fdd',
+        padding: '2px',
+        'background-color': '#fee',
+        opacity: 0.80
+    }).appendTo("body").fadeIn(200);
 }
 
 var previousPoint = null;
@@ -18,19 +18,19 @@
             previousPoint = item.datapoint;
             $("#tooltip").remove();
             var x = item.datapoint[0].toFixed(2),
-                y = item.datapoint[1].toFixed(2);
+            y = item.datapoint[1].toFixed(2);
             if (item.datapoint.length == 3) {
                 x = new Date(item.datapoint[2]);
                 x = x.toLocaleDateString() + ' ' + x.toLocaleTimeString();
             } else if (item.datapoint.length == 4) {
-               x = new Date(item.datapoint[2]);
-               x = x.strftime(item.datapoint[3]);
+                x = new Date(item.datapoint[2]);
+                x = x.strftime(item.datapoint[3]);
             }
-            showTooltip(item.pageX, item.pageY,
-            item.series.label + ': (' + x + ' ; ' + y + ')');
+            showTooltip(item.pageX, item.pageY, item.series.label + ': (' + x + ' ; ' + y + ')');
         }
     } else {
         $("#tooltip").remove();
         previousPoint = null;
     }
 }
+
--- a/web/data/cubicweb.form.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.form.css	Wed Nov 03 16:38:28 2010 +0100
@@ -9,7 +9,6 @@
   width: 100%;
   font-size : 160%;
   font-weight: bold;
-  color: #ff4500;
   padding-bottom : 0.4em;
   text-transform: capitalize;
   margin-bottom: 0.6em
@@ -23,9 +22,8 @@
 div.iformTitle {
   font-weight: bold;
   font-size: 110%;
-  color: #222211;
-  background: #e4ead8;
-  border: 1px solid #E4EAD8;  /*#b7b6a3 */
+  background: %(formHeaderBgColor)s;
+  border: 1px solid %(formHeaderBgColor)s;  /*#b7b6a3 */
   border-bottom: none;
 }
 
@@ -46,14 +44,14 @@
 }
 
 fieldset.subentity {
-  border: 1px solid #E4EAD8;
+  border: 1px solid %(formHeaderBgColor)s;
   display: block;
   margin-bottom: 1em;
   padding: 0.4em;
 }
 
 table.attributeForm {
-  border: 1px solid #E4EAD8;
+  border: 1px solid %(formHeaderBgColor)s;
   margin-bottom: 1em;
   padding: 0.8em 1.2em;
   width: 100%;
@@ -91,7 +89,7 @@
 table.attributeForm input,
 table.attributeForm textarea,
 table.attributeForm select {
-  border: 1px solid #E4EAD8;  /*#b7b6a3*/
+  border: 1px solid %(formHeaderBgColor)s;  /*#b7b6a3*/
 }
 
 table.attributeForm textarea {
@@ -163,10 +161,10 @@
 }
 
 a.editionPending {
-  color: #557755;
+  color: #9c9b24; /*557755*/
   font-weight: bold;
 }
-
+ 
 div.pendingDelete {
   text-decoration: line-through;
 }
@@ -187,22 +185,20 @@
   display: inline;
 }
 
-div.editableField:hover,
-div.editableField p:hover {
-  background-color: #eeedd9;
-}
+/* div.editableField:hover, */
+/* div.editableField p:hover { */
+/*   background-color: #eeedd9; */
+/* } */
 
-.error input { /* error added by the form renderer */
-  background: transparent url("error.png") 100% 50% no-repeat;
-}
+.error input, /* error added by the form renderer */
 input.error { /* error added by javascript */
-  background: transparent url("error.png") 100% 50% no-repeat;
+  background: transparent %(errorMsgBgImg)s;
 }
 
 span.errorMsg {
   display: block;
   font-weight: bold;
-  color: #ed0d0d;
+  color: %(errorMsgColor)s;
 }
 
 option.separator {
@@ -216,12 +212,12 @@
   font-style: italic;
   font-size: 110%;
   padding-left: 2em;
-  background : #f8f8ee url("information.png") 5px center no-repeat ;
+  background : %(msgBgColor)s %(infoMsgBgImg)s;
 }
 
 .helper{
   font-size: 96%;
-  color: #555544;
+  color: %(helperColor)s;
 }
 
 .helper:hover {
@@ -231,8 +227,8 @@
 
 .validateButton {
   margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
+  border-width: 1px;
+  border-style: solid;
+  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  background: %(buttonBgColor)s %(buttonBgImg)s;
 }
-
--- a/web/data/cubicweb.gmap.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.gmap.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.goa.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,10 +1,16 @@
-/*
+/**
  *  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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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');
+    cw.jqNode(elemId).toggleClass('hidden');
 }
 
-
-/* toggles visibility of login popup div */
+/**
+ * .. function:: popupLoginBox()
+ *
+ * toggles visibility of login popup div
+ */
 // XXX used exactly ONCE in basecomponents
 function popupLoginBox() {
-    toggleVisibility('popupLoginBox');
+    $('#popupLoginBox').toggleClass('hidden');
     jQuery('#__login:visible').focus();
 }
 
-
-/* returns the list of elements in the document matching the tag name
+/**
+ * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
+ *
+ * returns the list of elements in the document matching the tag name
  * and the properties provided
  *
- * @param tagName the tag's name
- * @param properties a js Object used as a dict
- * @return an iterator (if a *real* array is needed, you can use the
+ * * `tagName`, the tag's name
+ *
+ * * `properties`, a js Object used as a dict
+ *
+ * Return an iterator (if a *real* array is needed, you can use the
  *                      list() function)
  */
 function getElementsMatching(tagName, properties, /* optional */ parent) {
     parent = parent || document;
-    return filter(function elementMatches(element) {
-                     for (prop in properties) {
-                       if (getNodeAttribute(element, prop) != properties[prop]) {
-	                 return false;}}
-                    return true;},
-                  parent.getElementsByTagName(tagName));
+    return jQuery.grep(parent.getElementsByTagName(tagName), function elementMatches(element) {
+        for (prop in properties) {
+            if (jQuery(element).attr(prop) != properties[prop]) {
+                return false;
+            }
+        }
+        return true;
+    });
 }
 
-/*
+/**
+ * .. function:: setCheckboxesState(nameprefix, value, checked)
+ *
  * sets checked/unchecked status of checkboxes
  */
-function setCheckboxesState(nameprefix, checked){
+
+function setCheckboxesState(nameprefix, value, checked) {
     // XXX: this looks in *all* the document for inputs
-    var elements = getElementsMatching('input', {'type': "checkbox"});
-    filterfunc = function(cb) { return nameprefix && cb.name.startsWith(nameprefix); };
-    forEach(filter(filterfunc, elements), function(cb) {cb.checked=checked;});
+    jQuery('input:checkbox[name^=' + nameprefix + ']').each(function() {
+        if (value == null || this.value == value) {
+            this.checked = checked;
+        }
+    });
 }
 
-function setCheckboxesState2(nameprefix, value, checked){
-    // XXX: this looks in *all* the document for inputs
-    var elements = getElementsMatching('input', {'type': "checkbox"});
-    filterfunc = function(cb) { return nameprefix && cb.name.startsWith(nameprefix) && cb.value == value; };
-    forEach(filter(filterfunc, elements), function(cb) {cb.checked=checked;});
-}
-
-
-/* this function is a hack to build a dom node from html source */
+/**
+ * .. function:: html2dom(source)
+ *
+ * this function is a hack to build a dom node from html source
+ */
 function html2dom(source) {
     var tmpNode = SPAN();
     tmpNode.innerHTML = source;
     if (tmpNode.childNodes.length == 1) {
-	return tmpNode.firstChild;
+        return tmpNode.firstChild;
     }
     else {
-	// we leave the span node when `source` has no root node
-	// XXX This is cleary not the best solution, but css/html-wise,
-	///    a span not should not be too  much disturbing
-	return tmpNode;
+        // we leave the span node when `source` has no root node
+        // XXX This is cleary not the best solution, but css/html-wise,
+        ///    a span not should not be too  much disturbing
+        return tmpNode;
     }
 }
 
-
 // *** HELPERS **************************************************** //
-function rql_for_eid(eid) { return 'Any X WHERE X eid ' + eid; }
-function isTextNode(domNode) { return domNode.nodeType == 3; }
-function isElementNode(domNode) { return domNode.nodeType == 1; }
+function rql_for_eid(eid) {
+    return 'Any X WHERE X eid ' + eid;
+}
+function isTextNode(domNode) {
+    return domNode.nodeType == 3;
+}
+function isElementNode(domNode) {
+    return domNode.nodeType == 1;
+}
 
 function autogrow(area) {
-    if (area.scrollHeight > area.clientHeight && !window.opera) {
-	if (area.rows < 20) {
-	    area.rows += 2;
-	}
+    if (area.scrollHeight > area.clientHeight && ! window.opera) {
+        if (area.rows < 20) {
+            area.rows += 2;
+        }
     }
 }
 //============= page loading events ==========================================//
-
-CubicWeb.rounded = [
-		    ['div.sideBoxBody', 'bottom 6px'],
-		    ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']
-		    ];
+cw.rounded = [['div.sideBoxBody', 'bottom 6px'],
+              ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']];
 
 function roundedCorners(node) {
-    node = jQuery(node);
-    for(var r=0; r < CubicWeb.rounded.length; r++) {
-       node.find(CubicWeb.rounded[r][0]).corner(CubicWeb.rounded[r][1]);
+    if (jQuery.fn.corner !== undefined) {
+        node = jQuery(node);
+        for (var r = 0; r < cw.rounded.length; r++) {
+            node.find(cw.rounded[r][0]).corner(cw.rounded[r][1]);
+        }
     }
 }
 
-jQuery(document).ready(function () {roundedCorners(this.body);});
-
-CubicWeb.provide('corners.js');
-
-CubicWeb.provide('htmlhelpers.js');
+jQuery(document).ready(function() {
+    roundedCorners(this.body);
+});
--- a/web/data/cubicweb.ie.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.ie.css	Wed Nov 03 16:38:28 2010 +0100
@@ -4,8 +4,20 @@
   margin-top: 0px;
 }
 
-/* quick and dirty solution for pop to be 
+/* quick and dirty solution for pop to be
    correctly displayed on right edge of window */
-div.popupWrapper{ 
+div.popupWrapper{
   direction:rtl;
 }
+
+div#rqlinput input.rqlsubmit{
+  height: 24px;
+  width: 24px;
+}
+
+
+table#mainLayout #navColumnLeft,
+table#mainLayout #navColumnRight {
+  width: auto;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.image.js	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,31 @@
+jQuery.fn.autoResize = function() {
+    // remove enforced with / height (by CSS and/or HTML attributes)
+    this.css("width", "auto").css("height", "auto");
+    this.removeAttr("width").removeAttr("height"); // Remove
+    // compute image size / max allowed size to fit screen
+    var imgHSize = this.width();
+    var maxHSize = $(window).width() - ($(document).width() - imgHSize);
+    var imgVSize = this.height();
+    // we don't mind if content in [content]footer moved out of the screen
+    var maxVSize = $(window).height() - ($(document).height() - imgVSize) + $('#footer').height() + $('#contentfooter').height();
+    if (maxHSize > 0 && maxVSize > 0) {
+        // if image don't fit screen, set width or height so that
+        // browser keep img ratio, ensuring the other dimension will
+        // also fit the screen
+        if (imgHSize > maxHSize && ((imgVSize / imgHSize) * maxHSize) <= maxVSize) {
+            this.css("width", maxHSize);
+        } else if (imgVSize > maxVSize && ((imgHSize / imgVSize) * maxVSize) <= maxHSize) {
+            this.css("height", maxVSize);
+        }
+        else {
+            // image already fit in screen, don't scale it up
+        }
+    } else {
+        // can't fit image in, don't do anything
+    }
+};
+
+
+$(document).ready(function() {
+        $("img.contentimage").load(function() {$(this).autoResize()});
+});
--- a/web/data/cubicweb.iprogress.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.iprogress.css	Wed Nov 03 16:38:28 2010 +0100
@@ -8,11 +8,11 @@
 /* progressbar                                                                */
 /******************************************************************************/
 
-.done{ background:red }
+.done { background:red }
 
-.inprogress{ background:green }
+.inprogress { background:green }
 
-.overpassed{ background: yellow}
+.overpassed { background: yellow}
 
 
 canvas.progressbar {
@@ -20,64 +20,59 @@
 }
 
 .progressbarback {
- border: 1px solid #000000;
- background: transparent;
- height: 10px;
- width: 100px;
+  border: 1px solid #000000;
+  background: transparent;
+  height: 10px;
+  width: 100px;
 }
 
 /******************************************************************************/
 /* progress table                                                             */
 /******************************************************************************/
 
-table.progress{
+table.progress {
  /* The default table view */
- margin: 10px 0px;
- color : #000;
- width:100%;
- font-size:98%;
- border:2px solid #ebe8d9;
+  margin: 10px 0px 1em;
+  width: 100%;
+  font-size: 0.9167em;
 }
 
-table.progress th{
- text-align:left;
- white-space:nowrap;
- font-weight : bold;
- background:#ebe8d9 url("button.png") repeat-x;
- padding:2px 3px;
+table.progress th {
+  white-space: nowrap;
+  font-weight: bold;
+  background: %(listingHeaderBgColor)s;
+  padding: 2px 4px;
+  font-size:8pt;
 }
 
 table.progress th,
-table.progress td{
- border: 1px solid #dedede;
- margin:0px;
+table.progress td {
+  border: 1px solid %(listingBorderColor)s;
 }
 
-table.progress td{
- text-align:right;
- padding:2px 5px 2px 2px;
+table.progress td {
+  text-align: right;
+  padding: 2px 3px;
 }
 
 table.progress th.tdleft,
-table.progress td.tdleft{
- text-align:left;
- padding:2px 3px 2px 5px;
+table.progress td.tdleft {
+  text-align: left;
+  padding: 2px 3px 2px 5px;
 }
 
-
-table.progress tr.highlighted{
- background-color: #f4f5ed;
+table.progress tr.highlighted {
+  background-color: %(listingHihligthedBgColor)s;
 }
 
 table.progress tr.highlighted .progressbarback {
- border: 1px solid #555;
+  border: 1px solid %(listingHihligthedBgColor)s;
 }
 
 table.progress .progressbarback {
- border: 1px solid #777;
+  border: 1px solid #777;
 }
 
-.progress_data{
- padding-right:3px;
-}
-
+.progress_data {
+  padding-right: 3px;
+}
\ No newline at end of file
--- a/web/data/cubicweb.iprogress.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.iprogress.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,436 @@
+
+function Namespace(name) {
+   this.__name__ = name;
+}
+
+cw = new Namespace('cw');
+
+jQuery.extend(cw, {
+    log: function () {
+        var args = [];
+        for (var i = 0; i < arguments.length; i++) {
+            args.push(arguments[i]);
+        }
+        if (typeof(window) != "undefined" && window.console && window.console.log) {
+            window.console.log(args.join(' '));
+        }
+    },
+
+    //removed: getElementsByTagAndClassName, replaceChildNodes, toggleElementClass
+    //         partial, merge, isNotEmpty, update,
+    //         String.in_, String.join, list, getattr, attrgetter, methodcaller,
+    //         min, max, dict, concat
+    jqNode: function (node) {
+    /**
+     * .. function:: jqNode(node)
+     *
+     * safe version of jQuery('#nodeid') because we use ':' in nodeids
+     * which messes with jQuery selection mechanism
+     */
+        if (typeof(node) == 'string') {
+            node = document.getElementById(node);
+        }
+        if (node) {
+            return $(node);
+        }
+        return null;
+    },
+
+    getNode: function (node) {
+        if (typeof(node) == 'string') {
+            return document.getElementById(node);
+        }
+        return node;
+    },
+
+    evalJSON: function (json) { // trust source
+        return eval("(" + json + ")");
+    },
+
+    urlEncode: function (str) {
+        if (typeof(encodeURIComponent) != "undefined") {
+            return encodeURIComponent(str).replace(/\'/g, '%27');
+        } else {
+            return escape(str).replace(/\+/g, '%2B').replace(/\"/g, '%22').
+                    rval.replace(/\'/g, '%27');
+        }
+    },
+
+    swapDOM: function (dest, src) {
+        dest = cw.getNode(dest);
+        var parent = dest.parentNode;
+        if (src) {
+            src = cw.getNode(src);
+            parent.replaceChild(src, dest);
+        } else {
+            parent.removeChild(dest);
+        }
+        return src;
+    }
+});
+
+
+cw.utils = new Namespace('cw.utils');
+jQuery.extend(cw.utils, {
+
+    deprecatedFunction: function (msg, newfunc) {
+        return function () {
+            cw.log(msg);
+            return newfunc.apply(this, arguments);
+        };
+    },
+
+    movedToNamespace: function (funcnames, namespace) {
+        for (var i = 0; i < funcnames.length; i++) {
+            var funcname = funcnames[i];
+            var msg = ('[3.9] ' + funcname + ' is deprecated, use ' +
+		       namespace.__name__ + '.' + funcname + ' instead');
+            window[funcname] = cw.utils.deprecatedFunction(msg, namespace[funcname]);
+        }
+    },
+
+    createDomFunction: function (tag) {
+        function builddom(params, children) {
+            var node = document.createElement(tag);
+            for (key in params) {
+                var value = params[key];
+                if (key.substring(0, 2) == 'on') {
+                    // this is an event handler definition
+                    if (typeof value == 'string') {
+                        // litteral definition
+                        value = new Function(value);
+                    }
+                    node[key] = value;
+                } else { // normal node attribute
+                    jQuery(node).attr(key, params[key]);
+                }
+            }
+            if (children) {
+                if (!cw.utils.isArrayLike(children)) {
+                    children = [children];
+                    for (var i = 2; i < arguments.length; i++) {
+                        var arg = arguments[i];
+                        if (cw.utils.isArray(arg)) {
+                            jQuery.merge(children, arg);
+                        } else {
+                            children.push(arg);
+                        }
+                    }
+                }
+                for (var i = 0; i < children.length; i++) {
+                    var child = children[i];
+                    if (typeof child == "string" || typeof child == "number") {
+                        child = document.createTextNode(child);
+                    }
+                    node.appendChild(child);
+                }
+            }
+            return node;
+        }
+        return builddom;
+    },
+
+    /**
+     * .. function:: toISOTimestamp(date)
+     *
+     */
+    toISOTimestamp: function (date) {
+        if (typeof(date) == "undefined" || date === null) {
+            return null;
+        }
+
+        function _padTwo(n) {
+            return (n > 9) ? n : "0" + n;
+        }
+        var isoTime = [_padTwo(date.getHours()), _padTwo(date.getMinutes()),
+                       _padTwo(date.getSeconds())].join(':');
+        var isoDate = [date.getFullYear(), _padTwo(date.getMonth() + 1),
+                       _padTwo(date.getDate())].join("-");
+        return isoDate + " " + isoTime;
+    },
+
+    /**
+     * .. function:: nodeWalkDepthFirst(node, visitor)
+     *
+     * depth-first implementation of the nodeWalk function found
+     * in `MochiKit.Base <http://mochikit.com/doc/html/MochiKit/Base.html#fn-nodewalk>`_
+     */
+    nodeWalkDepthFirst: function (node, visitor) {
+        var children = visitor(node);
+        if (children) {
+            for (var i = 0; i < children.length; i++) {
+                cw.utils.nodeWalkDepthFirst(children[i], visitor);
+            }
+        }
+    },
+
+    isArray: function (it) { // taken from dojo
+        return it && (it instanceof Array || typeof it == "array");
+    },
+
+    isString: function (it) { // taken from dojo
+        return !!arguments.length && it != null && (typeof it == "string" || it instanceof String);
+    },
+
+    isArrayLike: function (it) { // taken from dojo
+        return (it && it !== undefined &&
+                // keep out built-in constructors (Number, String, ...)
+                // which have length properties
+                !cw.utils.isString(it) && !jQuery.isFunction(it) &&
+                !(it.tagName && it.tagName.toLowerCase() == 'form') &&
+                (cw.utils.isArray(it) || isFinite(it.length)));
+    },
+
+    /**
+     * .. function:: formContents(elem \/* = document.body *\/)
+     *
+     * this implementation comes from MochiKit
+     */
+    formContents: function (elem /* = document.body */ ) {
+        var names = [];
+        var values = [];
+        if (typeof(elem) == "undefined" || elem === null) {
+            elem = document.body;
+        } else {
+            elem = cw.getNode(elem);
+        }
+        cw.utils.nodeWalkDepthFirst(elem, function (elem) {
+            var name = elem.name;
+            if (name && name.length) {
+                var tagName = elem.tagName.toUpperCase();
+                if (tagName === "INPUT" && (elem.type == "radio" || elem.type == "checkbox") && !elem.checked) {
+                    return null;
+                }
+                if (tagName === "SELECT") {
+                    if (elem.type == "select-one") {
+                        if (elem.selectedIndex >= 0) {
+                            var opt = elem.options[elem.selectedIndex];
+                            var v = opt.value;
+                            if (!v) {
+                                var h = opt.outerHTML;
+                                // internet explorer sure does suck.
+                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
+                                    v = opt.text;
+                                }
+                            }
+                            names.push(name);
+                            values.push(v);
+                            return null;
+                        }
+                        // no form elements?
+                        names.push(name);
+                        values.push("");
+                        return null;
+                    } else {
+                        var opts = elem.options;
+                        if (!opts.length) {
+                            names.push(name);
+                            values.push("");
+                            return null;
+                        }
+                        for (var i = 0; i < opts.length; i++) {
+                            var opt = opts[i];
+                            if (!opt.selected) {
+                                continue;
+                            }
+                            var v = opt.value;
+                            if (!v) {
+                                var h = opt.outerHTML;
+                                // internet explorer sure does suck.
+                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
+                                    v = opt.text;
+                                }
+                            }
+                            names.push(name);
+                            values.push(v);
+                        }
+                        return null;
+                    }
+                }
+                if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" || tagName === "DIV") {
+                    return elem.childNodes;
+                }
+		var value = elem.value;
+		if (tagName === "TEXTAREA") {
+		    if (typeof(FCKeditor) != 'undefined') {
+			var fck = FCKeditorAPI.GetInstance(elem.id);
+			if (fck) {
+			    value = fck.GetHTML();
+			}
+		    }
+		}
+                names.push(name);
+                values.push(value || '');
+                return null;
+            }
+            return elem.childNodes;
+        });
+        return [names, values];
+    },
+
+    /**
+     * .. function:: sliceList(lst, start, stop, step)
+     *
+     * returns a subslice of `lst` using `start`/`stop`/`step`
+     * start, stop might be negative
+     *
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2)
+     * ['c', 'd', 'e', 'f']
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2, -2)
+     * ['c', 'd']
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], -3)
+     * ['d', 'e', 'f']
+     */
+    sliceList: function (lst, start, stop, step) {
+        start = start || 0;
+        stop = stop || lst.length;
+        step = step || 1;
+        if (stop < 0) {
+            stop = Math.max(lst.length + stop, 0);
+        }
+        if (start < 0) {
+            start = Math.min(lst.length + start, lst.length);
+        }
+        var result = [];
+        for (var i = start; i < stop; i += step) {
+            result.push(lst[i]);
+        }
+        return result;
+    },
+
+    /**
+     * .. function:: domid(string)
+     *
+     * return a valid DOM id from a string (should also be usable in jQuery
+     * search expression...). This is the javascript implementation of
+     * :func:`cubicweb.uilib.domid`.
+     */
+    domid: function (string) {
+	var newstring = string.replace(".", "_").replace("-", "_");
+	while (newstring != string) {
+	    string = newstring;
+	    newstring = newstring.replace(".", "_").replace("-", "_");
+	}
+	return newstring; // XXX
+    },
+
+    /**
+     * .. function:: strFuncCall(fname, *args)
+     *
+     * return a string suitable to call the `fname` javascript function with the
+     * given arguments (which should be correctly typed).. This is providing
+     * javascript implementation equivalent to :func:`cubicweb.uilib.js`.
+     */
+    strFuncCall: function(fname /* ...*/) {
+	    return (fname + '(' +
+		    $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON).join(',')
+		    + ')'
+		    );
+    }
+
+});
+
+String.prototype.startsWith = cw.utils.deprecatedFunction('[3.9] str.startsWith() is deprecated, use str.startswith() instead', function (prefix) {
+    return this.startswith(prefix);
+});
+
+String.prototype.endsWith = cw.utils.deprecatedFunction('[3.9] str.endsWith() is deprecated, use str.endswith() instead', function (suffix) {
+    return this.endswith(prefix);
+});
+
+/** DOM factories ************************************************************/
+A = cw.utils.createDomFunction('a');
+BUTTON = cw.utils.createDomFunction('button');
+BR = cw.utils.createDomFunction('br');
+CANVAS = cw.utils.createDomFunction('canvas');
+DD = cw.utils.createDomFunction('dd');
+DIV = cw.utils.createDomFunction('div');
+DL = cw.utils.createDomFunction('dl');
+DT = cw.utils.createDomFunction('dt');
+FIELDSET = cw.utils.createDomFunction('fieldset');
+FORM = cw.utils.createDomFunction('form');
+H1 = cw.utils.createDomFunction('H1');
+H2 = cw.utils.createDomFunction('H2');
+H3 = cw.utils.createDomFunction('H3');
+H4 = cw.utils.createDomFunction('H4');
+H5 = cw.utils.createDomFunction('H5');
+H6 = cw.utils.createDomFunction('H6');
+HR = cw.utils.createDomFunction('hr');
+IMG = cw.utils.createDomFunction('img');
+INPUT = cw.utils.createDomFunction('input');
+LABEL = cw.utils.createDomFunction('label');
+LEGEND = cw.utils.createDomFunction('legend');
+LI = cw.utils.createDomFunction('li');
+OL = cw.utils.createDomFunction('ol');
+OPTGROUP = cw.utils.createDomFunction('optgroup');
+OPTION = cw.utils.createDomFunction('option');
+P = cw.utils.createDomFunction('p');
+PRE = cw.utils.createDomFunction('pre');
+SELECT = cw.utils.createDomFunction('select');
+SPAN = cw.utils.createDomFunction('span');
+STRONG = cw.utils.createDomFunction('strong');
+TABLE = cw.utils.createDomFunction('table');
+TBODY = cw.utils.createDomFunction('tbody');
+TD = cw.utils.createDomFunction('td');
+TEXTAREA = cw.utils.createDomFunction('textarea');
+TFOOT = cw.utils.createDomFunction('tfoot');
+TH = cw.utils.createDomFunction('th');
+THEAD = cw.utils.createDomFunction('thead');
+TR = cw.utils.createDomFunction('tr');
+TT = cw.utils.createDomFunction('tt');
+UL = cw.utils.createDomFunction('ul');
+
+// cubicweb specific
+//IFRAME = cw.utils.createDomFunction('iframe');
+
+
+function IFRAME(params) {
+    if ('name' in params) {
+        try {
+            var node = document.createElement('<iframe name="' + params['name'] + '">');
+        } catch (ex) {
+            var node = document.createElement('iframe');
+            node.id = node.name = params.name;
+        }
+    }
+    else {
+        var node = document.createElement('iframe');
+    }
+    for (key in params) {
+        if (key != 'name') {
+            var value = params[key];
+            if (key.substring(0, 2) == 'on') {
+                // this is an event handler definition
+                if (typeof value == 'string') {
+                    // litteral definition
+                    value = new Function(value);
+                }
+                node[key] = value;
+            } else { // normal node attribute
+                node.setAttribute(key, params[key]);
+            }
+        }
+    }
+    return node;
+}
+
+// XXX avoid crashes / backward compat
+CubicWeb = {
+    require: cw.utils.deprecatedFunction(
+        '[3.9] CubicWeb.require() is not used anymore',
+        function(module) {}),
+    provide: cw.utils.deprecatedFunction(
+        '[3.9] CubicWeb.provide() is not used anymore',
+        function(module) {})
+};
+
+jQuery(document).ready(function() {
+    jQuery(CubicWeb).trigger('server-response', [false, document]);
+    jQuery(cw).trigger('server-response', [false, document]);
+});
+
+// XXX as of 2010-04-07, no known cube uses this
+jQuery(CubicWeb).bind('ajax-loaded', function() {
+    log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
+    jQuery(cw).trigger('server-response', [false, document]);
+});
--- a/web/data/cubicweb.lazy.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.lazy.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,10 +1,9 @@
-
 function load_now(eltsel, holesel, reloadable) {
     var lazydiv = jQuery(eltsel);
     var hole = lazydiv.children(holesel);
-    if ((hole.length == 0) && !reloadable) {
-	/* the hole is already filled */
-	return;
+    if ((hole.length == 0) && ! reloadable) {
+        /* the hole is already filled */
+        return;
     }
     lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
 }
@@ -12,3 +11,4 @@
 function trigger_load(divid) {
     jQuery('#lazy-' + divid).trigger('load_' + divid);
 }
+
--- a/web/data/cubicweb.login.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.login.css	Wed Nov 03 16:38:28 2010 +0100
@@ -11,7 +11,7 @@
   right: 0px;
   width: 26em;
   padding: 0px 1px 1px;
-  background: #E4EAD8;
+  background: %(listingBorderColor)s; 
 }
 
 div#popupLoginBox label{
@@ -30,13 +30,13 @@
   margin-left: -14em;
   width: 28em;
   background: #fff;
-  border: 2px solid #cfceb7;
+  border: 2px solid %(actionBoxTitleBgColor)s;
   padding-bottom: 0.5em;
   text-align: center;
 }
 
 div#loginBox h1 {
-  color: #FF7700;
+  color: %(aColor)s;
   font-size: 140%;
 }
 
@@ -46,7 +46,7 @@
   font-size: 140%;
   text-align: center;
   padding: 3px 0px;
-  background: #ff7700 url("banner.png") left top repeat-x;
+  background: %(headerBgColor)s url("banner.png") repeat-x top left;
 }
 
 div#loginBox div#loginContent form {
@@ -80,7 +80,7 @@
 
 .loginButton {
   border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
+  border-color: #edecd2 %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s  #edecd2;
   margin: 2px 0px 0px;
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
 }
--- a/web/data/cubicweb.manageview.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.manageview.css	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,886 @@
+/*
+ *  :organization: Logilab
+ *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ */
+
+/***************************************/
+/* xhtml tags                          */
+/***************************************/
+* {
+  margin: 0px;
+  padding: 0px;
+}
+
+html, body {
+  background: #e2e2e2;
+}
+
+body {
+  font-size: 69%;
+  font-weight: normal;
+  font-family: Verdana, sans-serif;
+}
+
+h1 {
+  font-size: 188%;
+  margin: 0.2em 0px 0.3em;
+  border-bottom: 1px solid #000;
+}
+
+h2, h3 {
+  margin-top: 0.2em;
+  margin-bottom: 0.3em;
+}
+
+h2 {
+  font-size: 135%;
+}
+
+h3 {
+  font-size: 130%;
+}
+
+h4 {
+  font-size: 120%;
+  margin: 0.2em 0px;
+}
+
+h5 {
+  font-size:110%;
+}
+
+h6{
+  font-size:105%;
+}
+
+a, a:active, a:visited, a:link {
+  color: #ff4500;
+  text-decoration: none;
+}
+
+a:hover{
+  text-decoration: underline;
+}
+
+a img, img {
+  border: none;
+  text-align: center;
+}
+
+p {
+  margin: 0em 0px 0.2em;
+  padding-top: 2px;
+}
+
+table, td, input, select{
+  font-size: 100%;
+}
+
+table {
+  border-collapse: collapse;
+  border: none;
+}
+
+table th, table td {
+  vertical-align: top;
+}
+
+table td img {
+  vertical-align: middle;
+  margin-right: 10px;
+}
+
+ol {
+  margin: 1px 0px 1px 16px;
+}
+
+ul{
+  margin: 1px 0px 1px 4px;
+  list-style-type: none;
+}
+
+ul li {
+  margin-top: 2px;
+  padding: 0px 0px 2px 8px;
+  background: url("bullet_orange.png") 0% 6px no-repeat;
+}
+
+dt {
+  font-size:1.17em;
+  font-weight:600;
+}
+
+dd {
+  margin: 0.6em 0 1.5em 2em;
+}
+
+fieldset {
+  border: none;
+}
+
+legend {
+  padding: 0px 2px;
+  font: bold 1em Verdana, sans-serif;
+}
+
+input, textarea {
+  padding: 0.2em;
+  vertical-align: middle;
+  border: 1px solid #ccc;
+}
+
+input:focus {
+  border: 1px inset #ff7700;
+}
+
+label, .label {
+  font-weight: bold;
+}
+
+iframe {
+  border: 0px;
+}
+
+pre {
+  font-family: Courier, "Courier New", Monaco, monospace;
+  font-size: 100%;
+  color: #000;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+}
+
+code {
+  font-size: 120%;
+  color: #000;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+}
+
+blockquote {
+  font-family: Courier, "Courier New", serif;
+  font-size: 120%;
+  margin: 5px 0px;
+  padding: 0.8em;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+}
+
+/***************************************/
+/* generic classes                     */
+/***************************************/
+
+.odd {
+  background-color: #f7f6f1;
+}
+
+.even {
+  background-color: transparent;
+}
+
+.hr {
+  border-bottom: 1px dotted #ccc;
+  margin: 1em 0px;
+}
+
+.left {
+  float: left;
+}
+
+.right {
+  float: right;
+}
+
+.clear {
+  clear: both;
+}
+
+.hidden {
+  display: none;
+  visibility: hidden;
+}
+
+li.invisible { list-style: none; background: none; padding: 0px 0px
+1px 1px; }
+
+li.invisible div{
+  display: inline;
+}
+
+.caption {
+    font-weight: bold;
+}
+
+.legend{
+    font-style: italic;
+}
+
+.align-center{
+    text-align: center;
+}
+
+/***************************************/
+/*   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;
+}
+
+
+/******************************/
+/* reledit                    */
+/******************************/
+
+.releditField {
+    display: inline;
+}
+
+.releditForm {
+ display:none;
+}
--- a/web/data/cubicweb.preferences.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.preferences.css	Wed Nov 03 16:38:28 2010 +0100
@@ -5,106 +5,90 @@
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
+div.propertiesform {
+  margin-bottom: 1.2857em;
+  line-height: 1.2857em;
+  font-size: %(h3FontSize)s;
+}
 
-.preferences .validateButton{
- margin-top:0px;
+div.propertiesform a {
+  display: block;
+  margin: 10px 0px 6px 0px;
+  padding-left: 16px;
+  font-weight: bold;
+  color: #000;
+  background: transparent url("puce_down.png") 3px center no-repeat;
+  text-decoration:none;
+}
+
+div.propertiesform a:hover {
+  background-color: %(listingHeaderBgColor)s;
+}
+
+.preferences .validateButton {
+  margin-top: 0px;
  }
 
-fieldset.preferences{
- border : 1px solid #CFCEB7;
- margin:7px 1em 0;
- padding:2px 6px 6px;
+fieldset.preferences {
+  margin: 7px 1em 0;
+  padding: 2px 6px 6px;
+  border : 1px solid %(pageContentBorderColor)s;
 }
 
 div.component {
- margin-left: 1em;
-}
-
-div.componentLink{
- margin-top:0.3em;
- }
-
-a.componentTitle{
- font-weight:bold;
- color: #000/*#0083AB;*/
- }
-
-a.componentTitle:visited{
- color: #000;
+  margin: 0 0 1em 16px;
 }
 
-h2.propertiesform a{
- display:block;
- margin: 10px 0px 6px 0px;
+a.componentTitle {
  font-weight: bold;
- color: #000;
- padding: 0.2em 0.2em 0.2em 16px;
- background:#eeedd9 url("puce_down.png") 3px center no-repeat;
- font-size:89%;
+ color: #000
 }
 
-h2.propertiesform a:hover{
- background-color:#cfceb7;
-}
-
-h2.propertiesform a:hover,
-h2.propertiesform a:visited{
- text-decoration:none;
- color: #000;
+a.componentTitle:visited {
+  color: #000;
 }
 
 div.preffield {
- margin-bottom: 5px;
- padding:2px 5px;
- background:#eeedd9;
+  margin-bottom: 5px;
+  padding: 2px 5px;
+  background: %(listingHeaderBgColor)s;
 }
 
-div.prefinput{
- margin:.3em;
+div.prefinput {
+  margin: .3em;
 }
 
-
 div.prefinput select.changed,
-div.prefinput input.changed{
- border: 1px solid #000;
- font-weight:bold;
-
-}
-
-div.prefinput select,
-div.prefinput input{
- background:#fff;
- border: 1px solid #CFCEB7;
+div.prefinput input.changed {
+  border: 1px solid #000;
+  font-weight: bold;
 }
 
 .prefinput input.error {
- /* background:#fff url(error.png) no-repeat scroll 100% 50% !important; */
- border:1px solid red !important;
- color:red;
- padding-right:1em;
+  border:1px solid red !important;
+  color:red;
+  padding-right:1em;
 }
 
-
-div.formsg{
- font-weight:bold;
- margin:0.5em 0px;
+div.formsg {
+  font-weight: bold;
+  margin: 0.5em 0px;
 }
 
-
-div.critical{
- color:red;
- padding-left:20px;
- background:#fff url(critical.png) no-repeat;
+div.critical {
+  color: red;
+  padding-left: 20px;
+  background: #fff url(critical.png) no-repeat;
  }
 
-div.formsg .msg{
- color : green;
+div.formsg .msg {
+  color: green;
 }
 
-.helper{
+.helper {
   font-size: 96%;
-  color: #555544;
-  padding:0;
+  color: %(helperColor)s;
+  padding: 0;
 }
 
 div.prefinput .helper:hover {
@@ -112,6 +96,6 @@
   cursor: default;
 }
 
-div.openlink{
- display:inline;
+div.openlink {
+  display: inline;
  }
--- a/web/data/cubicweb.preferences.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.preferences.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.print.css	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.python.js	Wed Nov 03 16:38:28 2010 +0100
@@ -1,18 +1,14 @@
-/*
+/**
  * This file contains extensions for standard javascript types
  *
  */
 
 ONE_DAY = 86400000; // (in milliseconds)
-
 // ========== DATE EXTENSIONS ========== ///
-
 Date.prototype.equals = function(other) {
     /* compare with other date ignoring time differences */
-    if (this.getYear() == other.getYear() &&
-	this.getMonth() == other.getMonth() &&
-	this.getDate() == other.getDate()) {
-	return true;
+    if (this.getYear() == other.getYear() && this.getMonth() == other.getMonth() && this.getDate() == other.getDate()) {
+        return true;
     }
     return false;
 };
@@ -24,7 +20,7 @@
 };
 
 Date.prototype.sub = function(days) {
-    return this.add(-days);
+    return this.add( - days);
 };
 
 Date.prototype.iadd = function(days) {
@@ -39,33 +35,37 @@
     this.setTime(this.getTime() - (days * ONE_DAY));
 };
 
-/*
+/**
+ * .. function:: Date.prototype.nextMonth()
+ *
  * returns the first day of the next month
  */
 Date.prototype.nextMonth = function() {
     if (this.getMonth() == 11) {
-	var d =new Date(this.getFullYear()+1, 0, 1);
-	return d;
+        var d = new Date(this.getFullYear() + 1, 0, 1);
+        return d;
     } else {
-	var d2 = new Date(this.getFullYear(), this.getMonth()+1, 1);
-	return d2;
+        var d2 = new Date(this.getFullYear(), this.getMonth() + 1, 1);
+        return d2;
     }
 };
 
-/*
+/**
+ * .. function:: Date.prototype.getRealDay()
+ *
  * returns the day of week, 0 being monday, 6 being sunday
  */
 Date.prototype.getRealDay = function() {
     // getDay() returns 0 for Sunday ==> 6 for Saturday
-    return (this.getDay()+6) % 7;
+    return (this.getDay() + 6) % 7;
 };
 
 Date.prototype.strftime = function(fmt) {
     if (this.toLocaleFormat !== undefined) { // browser dependent
-	return this.toLocaleFormat(fmt);
+        return this.toLocaleFormat(fmt);
     }
     // XXX implement at least a decent fallback implementation
-    return this.getFullYear() + '/' + (this.getMonth()+1) + '/' + this.getDate();
+    return this.getFullYear() + '/' + (this.getMonth() + 1) + '/' + this.getDate();
 };
 
 var _DATE_FORMAT_REGXES = {
@@ -74,231 +74,131 @@
     'm': new RegExp('^[0-9]{1,2}'),
     'H': new RegExp('^[0-9]{1,2}'),
     'M': new RegExp('^[0-9]{1,2}')
-}
+};
 
-/*
+/**
+ * .. function:: _parseDate(datestring, format)
+ *
  * _parseData does the actual parsing job needed by `strptime`
  */
 function _parseDate(datestring, format) {
     var skip0 = new RegExp('^0*[0-9]+');
     var parsed = {};
-    for (var i1=0,i2=0;i1<format.length;i1++,i2++) {
-	var c1 = format.charAt(i1);
-	var c2 = datestring.charAt(i2);
-	if (c1 == '%') {
-	    c1 = format.charAt(++i1);
-	    var data = _DATE_FORMAT_REGXES[c1].exec(datestring.substring(i2));
-	    if (!data.length) {
-		return null;
-	    }
-	    data = data[0];
-	    i2 += data.length-1;
-	    var value = parseInt(data, 10);
-	    if (isNaN(value)) {
-		return null;
-	    }
-	    parsed[c1] = value;
-	    continue;
-	}
-	if (c1 != c2) {
-	    return null;
-	}
+    for (var i1 = 0, i2 = 0; i1 < format.length; i1++, i2++) {
+        var c1 = format.charAt(i1);
+        var c2 = datestring.charAt(i2);
+        if (c1 == '%') {
+            c1 = format.charAt(++i1);
+            var data = _DATE_FORMAT_REGXES[c1].exec(datestring.substring(i2));
+            if (!data.length) {
+                return null;
+            }
+            data = data[0];
+            i2 += data.length - 1;
+            var value = parseInt(data, 10);
+            if (isNaN(value)) {
+                return null;
+            }
+            parsed[c1] = value;
+            continue;
+        }
+        if (c1 != c2) {
+            return null;
+        }
     }
     return parsed;
 }
 
-/*
+/**
+ * .. function:: strptime(datestring, format)
+ *
  * basic implementation of strptime. The only recognized formats
  * defined in _DATE_FORMAT_REGEXES (i.e. %Y, %d, %m, %H, %M)
  */
 function strptime(datestring, format) {
     var parsed = _parseDate(datestring, format);
     if (!parsed) {
-	return null;
+        return null;
     }
     // create initial date (!!! year=0 means 1900 !!!)
     var date = new Date(0, 0, 1, 0, 0);
     date.setFullYear(0); // reset to year 0
     if (parsed.Y) {
-	date.setFullYear(parsed.Y);
+        date.setFullYear(parsed.Y);
     }
     if (parsed.m) {
-	if (parsed.m < 1 || parsed.m > 12) {
-	    return null;
-	}
-	// !!! month indexes start at 0 in javascript !!!
-	date.setMonth(parsed.m - 1);
+        if (parsed.m < 1 || parsed.m > 12) {
+            return null;
+        }
+        // !!! month indexes start at 0 in javascript !!!
+        date.setMonth(parsed.m - 1);
     }
     if (parsed.d) {
-	if (parsed.m < 1 || parsed.m > 31) {
-	    return null;
-	}
-	date.setDate(parsed.d);
+        if (parsed.m < 1 || parsed.m > 31) {
+            return null;
+        }
+        date.setDate(parsed.d);
     }
     if (parsed.H) {
-	if (parsed.H < 0 || parsed.H > 23) {
-	    return null;
-	}
-	date.setHours(parsed.H);
+        if (parsed.H < 0 || parsed.H > 23) {
+            return null;
+        }
+        date.setHours(parsed.H);
     }
     if (parsed.M) {
-	if (parsed.M < 0 || parsed.M > 59) {
-	    return null;
-	}
-	date.setMinutes(parsed.M);
+        if (parsed.M < 0 || parsed.M > 59) {
+            return null;
+        }
+        date.setMinutes(parsed.M);
     }
     return date;
 }
 
 // ========== END OF DATE EXTENSIONS ========== ///
-
-
-
-// ========== ARRAY EXTENSIONS ========== ///
-Array.prototype.contains = function(element) {
-    return findValue(this, element) != -1;
-};
-
-// ========== END OF ARRAY EXTENSIONS ========== ///
-
-
-
 // ========== STRING EXTENSIONS ========== //
-
-/* python-like startsWith method for js strings
+/**
+ * .. function:: String.prototype.startswith(prefix)
+ *
+ * python-like startsWith method for js strings
  * >>>
  */
-String.prototype.startsWith = function(prefix) {
+String.prototype.startswith = function(prefix) {
     return this.indexOf(prefix) == 0;
 };
 
-/* python-like endsWith method for js strings */
-String.prototype.endsWith = function(suffix) {
+/**
+ * .. function:: String.prototype.endswith(suffix)
+ *
+ * python-like endsWith method for js strings
+ */
+String.prototype.endswith = function(suffix) {
     var startPos = this.length - suffix.length;
-    if (startPos < 0) { return false; }
+    if (startPos < 0) {
+        return false;
+    }
     return this.lastIndexOf(suffix, startPos) == startPos;
 };
 
-/* python-like strip method for js strings */
+/**
+ * .. function:: String.prototype.strip()
+ *
+ * python-like strip method for js strings
+ */
 String.prototype.strip = function() {
     return this.replace(/^\s*(.*?)\s*$/, "$1");
 };
 
-/* py-equiv: string in list */
-String.prototype.in_ = function(values) {
-    return findValue(values, this) != -1;
-};
-
-/* py-equiv: str.join(list) */
-String.prototype.join = function(args) {
-    return args.join(this);
-};
+// ========= class factories ========= //
 
-/* python-like list builtin
- * transforms an iterable in a js sequence
- * >>> gen = ifilter(function(x) {return x%2==0}, range(10))
- * >>> s = list(gen)
- * [0,2,4,6,8]
- */
-function list(iterable) {
-    var iterator = iter(iterable);
-    var result = [];
-    while (true) {
-	/* iterates until StopIteration occurs */
-	try {
-	    result.push(iterator.next());
-	} catch (exc) {
-	    if (exc != StopIteration) { throw exc; }
-	    return result;
-	}
-    }
-}
-
-/* py-equiv: getattr(obj, attrname, default=None) */
-function getattr(obj, attrname, defaultValue) {
-    // when not passed, defaultValue === undefined
-    return obj[attrname] || defaultValue;
-}
-
-/* py-equiv: operator.attrgetter */
-function attrgetter(attrname) {
-    return function(obj) { return getattr(obj, attrname); };
-}
-
-
-/* returns a subslice of `lst` using `start`/`stop`/`step`
- * start, stop might be negative
+/**
+ * .. function:: makeUnboundMethod(meth)
  *
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2)
- * ['c', 'd', 'e', 'f']
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2, -2)
- * ['c', 'd']
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], -3)
- * ['d', 'e', 'f']
+ * transforms a function into an unbound method
  */
-function sliceList(lst, start, stop, step) {
-    start = start || 0;
-    stop = stop || lst.length;
-    step = step || 1;
-    if (stop < 0) {
-	stop = max(lst.length+stop, 0);
-    }
-    if (start < 0) {
-	start = min(lst.length+start, lst.length);
-    }
-    var result = [];
-    for (var i=start; i < stop; i+=step) {
-	result.push(lst[i]);
-    }
-    return result;
-}
-
-/* returns a partial func that calls a mehod on its argument
- * py-equiv: return lambda obj: getattr(obj, methname)(*args)
- */
-// XXX looks completely unused (candidate for removal)
-function methodcaller(methname) {
-    var args = sliceList(arguments, 1);
-    return function(obj) {
-	return obj[methname].apply(obj, args);
-    };
-}
-
-/* use MochiKit's listMin / listMax */
-function min() { return listMin(arguments); }
-function max() { return listMax(arguments); }
-
-/*
- * >>> d = dict(["x", "y", "z"], [0, 1, 2])
- * >>> d['y']
- * 1
- * >>> d.y
- * 1
- */
-function dict(keys, values) {
-    if (keys.length != values.length) {
-	throw "got different number of keys and values !";
-    }
-    var newobj = {};
-    for(var i=0; i<keys.length; i++) {
-	newobj[keys[i]] = values[i];
-    }
-    return newobj;
-}
-
-
-function concat() {
-    return ''.join(list(arguments));
-}
-
-
-/**** class factories ****/
-
-// transforms a function into an unbound method
 function makeUnboundMethod(meth) {
     function unboundMeth(self) {
-	var newargs = sliceList(arguments, 1);
-	return meth.apply(self, newargs);
+        var newargs = cw.utils.sliceList(arguments, 1);
+        return meth.apply(self, newargs);
     }
     unboundMeth.__name__ = meth.__name__;
     return unboundMeth;
@@ -312,29 +212,40 @@
     cls.prototype[methname] = meth; // for the instance
 }
 
-// simple internal function that tells if the attribute should
-// be copied from baseclasses or not
+/**
+ * .. function:: _isAttrSkipped(attrname)
+ *
+ * simple internal function that tells if the attribute should
+ * be copied from baseclasses or not
+ */
 function _isAttrSkipped(attrname) {
     var skipped = ['__class__', '__dict__', '__bases__', 'prototype'];
-    for (var i=0; i < skipped.length; i++) {
-	if (skipped[i] == attrname) {
-	    return true;
-	}
+    for (var i = 0; i < skipped.length; i++) {
+        if (skipped[i] == attrname) {
+            return true;
+        }
     }
     return false;
 }
 
-// internal function used to build the class constructor
+/**
+ * .. function:: makeConstructor(userctor)
+ *
+ * internal function used to build the class constructor
+ */
 function makeConstructor(userctor) {
     return function() {
-	// this is a proxy to user's __init__
-	if (userctor) {
-	    userctor.apply(this, arguments);
-	}
+        // this is a proxy to user's __init__
+        if (userctor) {
+            userctor.apply(this, arguments);
+        }
     };
 }
 
-/* this is a js class factory. objects returned by this function behave
+/**
+ * .. function:: defclass(name, bases, classdict)
+ *
+ * this is a js class factory. objects returned by this function behave
  * more or less like a python class. The `class` function prototype is
  * inspired by the python `type` builtin
  * Important notes :
@@ -347,19 +258,21 @@
     // this is the static inheritance approach (<=> differs from python)
     var basemeths = {};
     var reverseLookup = [];
-    for(var i=baseclasses.length-1; i >= 0; i--) {
-	reverseLookup.push(baseclasses[i]);
+    for (var i = baseclasses.length - 1; i >= 0; i--) {
+        reverseLookup.push(baseclasses[i]);
     }
-    reverseLookup.push({'__dict__' : classdict});
+    reverseLookup.push({
+        '__dict__': classdict
+    });
 
-    for(var i=0; i < reverseLookup.length; i++) {
-	var cls = reverseLookup[i];
-	for (prop in cls.__dict__) {
-	    // XXX hack to avoid __init__, __bases__...
-	    if ( !_isAttrSkipped(prop) ) {
-		basemeths[prop] = cls.__dict__[prop];
-	    }
-	}
+    for (var i = 0; i < reverseLookup.length; i++) {
+        var cls = reverseLookup[i];
+        for (prop in cls.__dict__) {
+            // XXX hack to avoid __init__, __bases__...
+            if (!_isAttrSkipped(prop)) {
+                basemeths[prop] = cls.__dict__[prop];
+            }
+        }
     }
     var userctor = basemeths['__init__'];
     var constructor = makeConstructor(userctor);
@@ -371,38 +284,8 @@
     constructor.prototype.__class__ = constructor;
     // make bound / unbound methods
     for (methname in basemeths) {
-	attachMethodToClass(constructor, methname, basemeths[methname]);
+        attachMethodToClass(constructor, methname, basemeths[methname]);
     }
 
     return constructor;
 }
-
-// Not really python-like
-CubicWeb = {};
-// XXX backward compatibility
-Erudi = CubicWeb;
-CubicWeb.loaded = [];
-CubicWeb.require = function(module) {
-    if (!CubicWeb.loaded.contains(module)) {
-	// a CubicWeb.load_javascript(module) function would require a dependency on ajax.js
-	log(module, ' is required but not loaded');
-    }
-};
-
-CubicWeb.provide = function(module) {
-    if (!CubicWeb.loaded.contains(module)) {
-	CubicWeb.loaded.push(module);
-    }
-};
-
-jQuery(document).ready(function() {
-    jQuery(CubicWeb).trigger('server-response', [false, document]);
-});
-
-// XXX as of 2010-04-07, no known cube uses this
-jQuery(CubicWeb).bind('ajax-loaded', function() {
-    log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
-    jQuery(CubicWeb).trigger('server-response', [false, document]);
-});
-
-CubicWeb.provide('python.js');
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.reledit.js	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,74 @@
+cw.reledit = new Namespace('cw.reledit');
+
+
+jQuery.extend(cw.reledit, {
+
+    /* Unhides the part of reledit div containing the form
+     * hides other parts
+     */
+    showInlineEditionForm: function (divid) {
+        jQuery('#' + divid).hide();
+        jQuery('#' + divid + '-value').hide();
+        jQuery('#' + divid + '-form').show();
+      },
+
+    /* Hides and removes edition parts, incl. messages
+     * show initial widget state
+     */
+    cleanupAfterCancel: function (divid) {
+        jQuery('#appMsg').hide();
+        jQuery('div.errorMessage').remove();
+        jQuery('#' + divid).show();
+        jQuery('#' + divid + '-value').show();
+        jQuery('#' + divid + '-form').hide();
+    },
+
+    /* callback used on form validation success
+     * refreshes the whole page or just the edited reledit zone
+     * @param results: [status, ...]
+     * @param formid: the dom id of the reledit form
+     * @param cbargs: ...
+     */
+     onSuccess: function (results, formid, cbargs) {
+        var params = {fname: 'reledit_form'};
+        jQuery('#' + formid + ' input:hidden').each(function (elt) {
+            var name = jQuery(this).attr('name');
+            if (name && name.startswith('__reledit|')) {
+                params[name.split('|')[1]] = this.value;
+            }
+        });
+        var reload = cw.evalJSON(params.reload);
+        if (reload || (params.formid == 'deleteconf')) {
+            if (typeof reload == 'string') {
+                /* Sometimes we want to reload but the reledit thing
+                 * updated a key attribute which was a component of the
+                 * url
+                 */
+                document.location.href = reload;
+                return;
+            }
+            else {
+                document.location.reload();
+                return;
+            }
+        }
+        jQuery('#'+params.divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, params, 'post');
+        jQuery(cw).trigger('reledit-reloaded', params);
+    },
+
+    /* called by reledit forms to submit changes
+     * @param formid : the dom id of the form used
+     * @param rtype : the attribute being edited
+     * @param eid : the eid of the entity being edited
+     * @param reload: boolean to reload page if true (when changing URL dependant data)
+     * @param default_value : value if the field is empty
+     */
+    loadInlineEditionForm: function(formid, eid, rtype, role, divid, reload, vid) {
+        var args = {fname: 'reledit_form', rtype: rtype, role: role,
+                    pageid: pageid,
+                    eid: eid, divid: divid, formid: formid,
+                    reload: reload, vid: vid};
+        var d = jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+        d.addCallback(function () {cw.reledit.showInlineEditionForm(divid);});
+    }
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.reset.css	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,53 @@
+/* http://meyerweb.com/eric/tools/css/reset/ */
+/* v1.0 | 20080212 */
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  outline: 0;
+  font-size: 100%;
+  vertical-align: baseline;
+  background: transparent;
+}
+body {
+  line-height: 1;
+}
+ol, ul {
+  list-style: none;
+}
+blockquote, q {
+  quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+  content: '';
+  content: none;
+}
+
+/* remember to define focus styles! */
+:focus {
+  outline: 0;
+}
+
+/* remember to highlight inserts somehow! */
+ins {
+  text-decoration: none;
+}
+del {
+  text-decoration: line-through;
+}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.rhythm.js	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,7 @@
+$(document).ready(function() {
+    $('a.rhythm').click(function (event){
+        $('div#pageContent').toggleClass('rhythm_bg');
+        $('div#page').toggleClass('rhythm_bg');
+        event.preventDefault();
+    });
+});
--- a/web/data/cubicweb.tablesorter.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.tablesorter.css	Wed Nov 03 16:38:28 2010 +0100
@@ -1,12 +1,4 @@
-/* tables */
-/*table.tablesorter {
-	font-family:arial;
-	background-color: #CDCDCD;
-	margin:10px 0pt 15px;
-	font-size: 8pt;
-	width: 100%;
-	text-align: left;
-} */
+/* sortable tables */
 
 table.listing tr .headerSortUp {
   background-image: url(asc.gif);
@@ -15,7 +7,7 @@
   background-image: url(desc.gif);
 }
 table.listing tr .headerSortDown, table.listing tr .headerSortUp {
-   background-color: #DDD;
+   background-color: %(listingBorderColor)s;
    background-repeat: no-repeat;
    background-position: center right;
 }
--- a/web/data/cubicweb.tableview.css	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.tableview.css	Wed Nov 03 16:38:28 2010 +0100
@@ -6,7 +6,7 @@
   font-weight: bold;
   background: #ebe8d9 url("button.png") repeat-x;
   padding: 0.3em;
-  border-bottom: 1px solid #cfceb7;
+  border-bottom: 1px solid %(actionBoxTitleBgColor)s;
   text-align: left;
 }
 
--- a/web/data/cubicweb.tabs.js	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.tabs.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.timeline-bundle.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.timeline-ext.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/cubicweb.widgets.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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,176 @@
 // 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);
+        return new wdgclass(wdgnode);
     }
+    return null;
 }
 
-/* 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 +190,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 +273,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)];
+        this.tl = Timeline.create(tldiv, bandInfos);
+        var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
+        Timeline.loadJSON(loadurl, function(json, url) {
+            eventSource.loadJSON(json, url);
+        });
 
     }
 });
 
 Widgets.TemplateTextField = defclass("TemplateTextField", null, {
 
-    __init__ : function(wdgnode) {
-	this.variables = getNodeAttribute(wdgnode, 'cubicweb:variables').split(',');
-	this.options = {'name'   : wdgnode.getAttribute('cubicweb:inputid'),
-			'rows' : wdgnode.getAttribute('cubicweb:rows') || 40,
-			'cols' : wdgnode.getAttribute('cubicweb:cols') || 80
-		       };
-	// this.variableRegexp = /%\((\w+)\)s/;
-	this.errorField = DIV({'class' : "errorMessage"});
-	this.textField = TEXTAREA(this.options);
-	jQuery(this.textField).bind('keyup', {'self': this}, this.highlightInvalidVariables);
-	jQuery('#substitutions').prepend(this.errorField);
-	jQuery('#substitutions .errorMessage').hide();
-	wdgnode.appendChild(this.textField);
+    __init__: function(wdgnode) {
+        this.variables = jQuery(wdgnode).attr('cubicweb:variables').split(',');
+        this.options = {
+            name: wdgnode.getAttribute('cubicweb:inputid'),
+            rows: wdgnode.getAttribute('cubicweb:rows') || 40,
+            cols: wdgnode.getAttribute('cubicweb:cols') || 80
+        };
+        // this.variableRegexp = /%\((\w+)\)s/;
+        this.errorField = DIV({
+            'class': "errorMessage"
+        });
+        this.textField = TEXTAREA(this.options);
+        jQuery(this.textField).bind('keyup', {
+            'self': this
+        },
+        this.highlightInvalidVariables);
+        jQuery('#substitutions').prepend(this.errorField);
+        jQuery('#substitutions .errorMessage').hide();
+        wdgnode.appendChild(this.textField);
     },
 
     /* signal callbacks */
 
-    highlightInvalidVariables : function(event) {
-	var self = event.data.self;
-	var text = self.textField.value;
-	var unknownVariables = [];
-	var it = 0;
-	var group = null;
-	var variableRegexp = /%\((\w+)\)s/g;
-	// emulates rgx.findAll()
-	while ( group=variableRegexp.exec(text) ) {
-	    if ( !self.variables.contains(group[1]) ) {
-		unknownVariables.push(group[1]);
-	    }
-	    it++;
-	    if (it > 5) {
-		break;
-	    }
-	}
-	var errText = '';
-	if (unknownVariables.length) {
-	    errText = "Detected invalid variables : " + ", ".join(unknownVariables);
-	    jQuery('#substitutions .errorMessage').show();
-	} else {
-	    jQuery('#substitutions .errorMessage').hide();
-	}
-	self.errorField.innerHTML = errText;
+    highlightInvalidVariables: function(event) {
+        var self = event.data.self;
+        var text = self.textField.value;
+        var unknownVariables = [];
+        var it = 0;
+        var group = null;
+        var variableRegexp = /%\((\w+)\)s/g;
+        // emulates rgx.findAll()
+        while (group = variableRegexp.exec(text)) {
+            if (!$.inArray(group[1], self.variables)) {
+                unknownVariables.push(group[1]);
+            }
+            it++;
+            if (it > 5) {
+                break;
+            }
+        }
+        var errText = '';
+        if (unknownVariables.length) {
+            errText = "Detected invalid variables : " + unknownVariables.join(', ');
+            jQuery('#substitutions .errorMessage').show();
+        } else {
+            jQuery('#substitutions .errorMessage').hide();
+        }
+        self.errorField.innerHTML = errText;
     }
 
 });
 
-/*
- * ComboBox with a textinput : allows to add a new value
- */
-
-Widgets.AddComboBox = defclass('AddComboBox', null, {
-   __init__ : function(wdgnode) {
-       jQuery("#add_newopt").click(function() {
-	  var new_val = jQuery("#newopt").val();
-	      if (!new_val){
-		  return false;
-	      }
-          name = wdgnode.getAttribute('name').split(':');
-	  this.rel = name[0];
-	  this.eid_to = name[1];
-          this.etype_to = wdgnode.getAttribute('cubicweb:etype_to');
-          this.etype_from = wdgnode.getAttribute('cubicweb:etype_from');
-     	  var d = asyncRemoteExec('add_and_link_new_entity', this.etype_to, this.rel, this.eid_to, this.etype_from, 'new_val');
-          d.addCallback(function (eid) {
-	      jQuery(wdgnode).find("option[selected]").removeAttr("selected");
-              var new_option = OPTION({'value':eid, 'selected':'selected'}, value=new_val);
-              wdgnode.appendChild(new_option);
-          });
-          d.addErrback(function (xxx) {
-              log('xxx =', xxx);
-          });
-     });
-   }
-});
-
-
-CubicWeb.provide('widgets.js');
+cw.widgets = {
+    /**
+     * .. function:: insertText(text, areaId)
+     *
+     * inspects textarea with id `areaId` and replaces the current selected text
+     * with `text`. Cursor is then set at the end of the inserted text.
+     */
+    insertText: function (text, areaId) {
+        var textarea = jQuery('#' + areaId);
+        if (document.selection) { // IE
+            var selLength;
+            textarea.focus();
+            var sel = document.selection.createRange();
+            selLength = sel.text.length;
+            sel.text = text;
+            sel.moveStart('character', selLength - text.length);
+            sel.select();
+        } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla
+            var startPos = textarea.selectionStart;
+            var endPos = textarea.selectionEnd;
+            // insert text so that it replaces the [startPos, endPos] part
+            textarea.value = textarea.value.substring(0, startPos) + text + textarea.value.substring(endPos, textarea.value.length);
+            // set cursor pos at the end of the inserted text
+            textarea.selectionStart = textarea.selectionEnd = startPos + text.length;
+            textarea.focus();
+        } else { // safety belt for other browsers
+            textarea.value += text;
+        }
+    }
+};
--- a/web/data/external_resources	Tue Jul 27 12:36:03 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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/data/jquery.tablesorter.js	Wed Nov 03 16:38:28 2010 +0100
@@ -705,10 +705,10 @@
 	ts.addParser({
 	    id: "json",
 	    is: function(s) {
-	        return s.startsWith('json:');
+	        return s.startswith('json:');
 	    },
 	    format: function(s,table,cell) {
-		return evalJSON(s.slice(5));
+		return cw.evalJSON(s.slice(5));
 	    },
 	  type: "text"
 	});
Binary file web/data/logo.png has changed
Binary file web/data/mail.gif has changed
Binary file web/data/nomail.gif has changed
Binary file web/data/rhythm15.png has changed
Binary file web/data/rhythm18.png has changed
Binary file web/data/rhythm20.png has changed
Binary file web/data/rhythm22.png has changed
Binary file web/data/rhythm24.png has changed
Binary file web/data/rhythm26.png has changed
Binary file web/data/tab.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/uiprops.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,138 @@
+"""define default ui properties"""
+
+# CSS stylesheets to include systematically in HTML headers
+# use the following line if you *need* to keep the old stylesheet
+STYLESHEETS =       [data('cubicweb.reset.css'),
+                     data('cubicweb.css'), ]
+STYLESHEETS_IE =    [data('cubicweb.ie.css')]
+STYLESHEETS_PRINT = [data('cubicweb.print.css')]
+
+# Javascripts files to include systematically in HTML headers
+JAVASCRIPTS = [data('jquery.js'),
+               data('jquery.corner.js'),
+               data('jquery.json.js'),
+               data('cubicweb.js'),
+               data('cubicweb.compat.js'),
+               data('cubicweb.python.js'),
+               data('cubicweb.htmlhelpers.js')]
+
+# where is installed fckeditor
+FCKEDITOR_PATH = '/usr/share/fckeditor/'
+
+# favicon and logo for the instance
+FAVICON = data('favicon.ico')
+LOGO = data('logo.png')
+
+# rss logo (link to get the rss view of a selection)
+RSS_LOGO = data('rss.png')
+RSS_LOGO_16 = data('feed-icon16x16.png')
+RSS_LOGO_32 = data('feed-icon32x32.png')
+
+# XXX cleanup resources below, some of them are probably not used
+# (at least entity types icons...)
+
+# images
+HELP = data('help.png')
+SEARCH_GO = data('go.png')
+PUCE_UP = data('puce_up.png')
+PUCE_DOWN = data('puce_down.png')
+
+# button icons
+OK_ICON = data('ok.png')
+CANCEL_ICON = data('cancel.png')
+APPLY_ICON = data('plus.png')
+TRASH_ICON = data('trash_can_small.png')
+
+# icons for entity types
+BOOKMARK_ICON = data('icon_bookmark.gif')
+EMAILADDRESS_ICON = data('icon_emailaddress.gif')
+EUSER_ICON = data('icon_euser.gif')
+STATE_ICON = data('icon_state.gif')
+
+# other icons
+CALENDAR_ICON = data('calendar.gif')
+CANCEL_EMAIL_ICON = data('sendcancel.png')
+SEND_EMAIL_ICON = data('sendok.png')
+DOWNLOAD_ICON = data('download.gif')
+UPLOAD_ICON = data('upload.gif')
+GMARKER_ICON = data('gmap_blue_marker.png')
+UP_ICON = data('up.gif')
+
+# colors, fonts, etc
+
+# default (body, html)
+defaultColor = '#000'
+defaultFontFamily = "'Bitstream Vera Sans','Lucida Grande','Lucida Sans Unicode','Geneva','Verdana',sans-serif"
+defaultSize = '12px'
+defaultLineHeight = '1.5'
+defaultLineHeightEm = lazystr('%(defaultLineHeight)sem')
+baseRhythmBg = 'rhythm18.png'
+
+inputHeight = '1.3em'
+inputPadding = 'O.2em'
+# XXX
+defaultLayoutMargin = '8px'
+
+# header
+headerBgColor = '#ff7700'
+
+# h
+h1FontSize = '1.5em' # 18px
+h1Padding = '0 0 0.14em 0 '
+h1Margin = '0.8em 0 0.5em'
+h1Color = '#000'
+h1BorderBottomStyle = lazystr('0.06em solid %(h1Color)s')
+
+h2FontSize = '1.33333em'
+h2Padding = '0.4em 0 0.35em 0'
+h2Margin = '0'
+
+h3FontSize = '1.16667em'
+h3Padding = '0.5em 0 0.57em 0'
+h3Margin = '0'
+
+# links
+aColor = '#e6820e'
+aActiveColor = aVisitedColor = aLinkColor = lazystr('%(aColor)s')
+
+
+# page frame
+pageBgColor = '#e2e2e2'
+pageContentBorderColor = '#ccc'
+pageContentBgColor = '#fff'
+pageContentPadding = '1em'
+pageMinHeight = '800px'
+
+# boxes
+boxTitleBg = lazystr('%(headerBgColor)s url("boxHeader.png") repeat-x 50%% 50%%')
+boxBodyBgColor = '#efefde'
+
+# action, search, sideBoxes
+actionBoxTitleBgColor = '#cfceb7'
+actionBoxTitleBg = lazystr('%(actionBoxTitleBgColor)s url("actionBoxHeader.png") repeat-x 50%% 50%%')
+sideBoxBodyBgColor = '#f8f8ee'
+sideBoxBodyBg = lazystr('%(sideBoxBodyBgColor)s')
+sideBoxBodyColor = '#555544'
+
+# table listing & co
+listingBorderColor = '#ccc'
+listingHeaderBgColor = '#efefef'
+listingHihligthedBgColor = '#fbfbfb'
+
+# puce
+bulletDownImg = 'url("puce_down.png") 98% 6px no-repeat'
+
+#forms
+formHeaderBgColor = lazystr('%(listingHeaderBgColor)s')
+helperColor = '#555'
+
+# button
+buttonBorderColor = '#edecd2'
+buttonBgColor = '#fffff8'
+buttonBgImg = 'url("button.png") repeat-x 50% 50%'
+
+# messages
+msgBgColor = '#f8f8ee'
+infoMsgBgImg = 'url("information.png") 5px center no-repeat'
+errorMsgBgImg = 'url("error.png") 100% 50% no-repeat'
+errorMsgColor = '#ed0d0d'
--- a/web/facet.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/facet.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,10 +15,35 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""contains utility functions and some visual component to restrict results of
-a search
+"""
+The :mod:`cubicweb.web.facet` module contains a set of abstract classes to use
+as bases to build your own facets
+
+All facet classes inherits from the :class:`AbstractFacet` class, though you'll
+usually find some more handy class that do what you want.
+
+Let's see available classes.
 
+Classes you'll want to use
+--------------------------
+.. autoclass:: cubicweb.web.facet.RelationFacet
+.. autoclass:: cubicweb.web.facet.RelationAttributeFacet
+.. autoclass:: cubicweb.web.facet.HasRelationFacet
+.. autoclass:: cubicweb.web.facet.AttributeFacet
+.. autoclass:: cubicweb.web.facet.RangeFacet
+.. autoclass:: cubicweb.web.facet.DateRangeFacet
+
+Classes for facets implementor
+------------------------------
+Unless you didn't find the class that does the job you want above, you may want
+to skip those classes...
+
+.. autoclass:: cubicweb.web.facet.AbstractFacet
+.. autoclass:: cubicweb.web.facet.VocabularyFacet
+
+.. comment: XXX widgets
 """
+
 __docformat__ = "restructuredtext en"
 
 from copy import deepcopy
@@ -39,6 +64,14 @@
 from cubicweb.appobject import AppObject
 from cubicweb.web.htmlwidgets import HTMLWidget
 
+
+def rtype_facet_title(facet):
+    ptypes = facet.cw_rset.column_types(0)
+    if len(ptypes) == 1:
+        return display_name(facet._cw, facet.rtype, form=facet.role,
+                            context=iter(ptypes).next())
+    return display_name(facet._cw, facet.rtype, form=facet.role)
+
 ## rqlst manipulation functions used by facets ################################
 
 def prepare_facets_rqlst(rqlst, args=None):
@@ -145,7 +178,8 @@
         rqlst.add_relation(mainvar, rtype, newvar)
     return newvar
 
-def _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role):
+def _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
+                              select_target_entity=True):
     """prepare a syntax tree to generate a filter vocabulary rql using the given
     relation:
     * create a variable to filter on this relation
@@ -154,9 +188,10 @@
     * add the new variable to the selection
     """
     newvar = _add_rtype_relation(rqlst, mainvar, rtype, role)
-    if rqlst.groupby:
-        rqlst.add_group_var(newvar)
-    rqlst.add_selected(newvar)
+    if select_target_entity:
+        if rqlst.groupby:
+            rqlst.add_group_var(newvar)
+        rqlst.add_selected(newvar)
     # add is restriction if necessary
     if mainvar.stinfo['typerel'] is None:
         etypes = frozenset(sol[mainvar.name] for sol in rqlst.solutions)
@@ -191,7 +226,8 @@
         rqlst.add_sort_term(term)
 
 def insert_attr_select_relation(rqlst, mainvar, rtype, role, attrname,
-                                sortfuncname=None, sortasc=True):
+                                sortfuncname=None, sortasc=True,
+                                select_target_entity=True):
     """modify a syntax tree to :
     * link a new variable to `mainvar` through `rtype` (where mainvar has `role`)
     * retrieve only the newly inserted variable and its `attrname`
@@ -202,7 +238,8 @@
     * no sort if `sortasc` is None
     """
     _cleanup_rqlst(rqlst, mainvar)
-    var = _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role)
+    var = _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
+                                    select_target_entity)
     # not found, create one
     attrvar = rqlst.make_variable()
     rqlst.add_relation(var, attrname, attrvar)
@@ -264,20 +301,52 @@
                 toremove.add(rqlst.defined_vars[ovarname])
 
 
+## base facet classes ##########################################################
 
-## base facet classes #########################################################
 class AbstractFacet(AppObject):
+    """Abstract base class for all facets. Facets are stored in their own
+    'facets' registry. They are similar to contextual components since the use
+    the following configurable properties:
+
+    * `visible`, boolean flag telling if a facet should be displayed or not
+
+    * `order`, integer to control facets display order
+
+    * `context`, telling if a facet should be displayed in the table form filter
+      (context = 'tablefilter') or in the facet box (context = 'facetbox') or in
+      both (context = '')
+
+    The following methods define the facet API:
+
+    .. automethod:: cubicweb.web.facet.AbstractFacet.get_widget
+    .. automethod:: cubicweb.web.facet.AbstractFacet.add_rql_restrictions
+
+    Facets will have the following attributes set (beside the standard
+    :class:`~cubicweb.appobject.AppObject` ones):
+
+    * `rqlst`, the rql syntax tree being facetted
+
+    * `filtered_variable`, the variable node in this rql syntax tree that we're
+      interested in filtering
+
+    Facets implementors may also be interested in the following properties /
+    methods:
+
+    .. automethod:: cubicweb.web.facet.AbstractFacet.operator
+    .. automethod:: cubicweb.web.facet.AbstractFacet.rqlexec
+    """
     __abstract__ = True
     __registry__ = 'facets'
     cw_property_defs = {
         _('visible'): dict(type='Boolean', default=True,
-                           help=_('display the box or not')),
+                           help=_('display the facet or not')),
         _('order'):   dict(type='Int', default=99,
-                           help=_('display order of the box')),
+                           help=_('display order of the facet')),
         _('context'): dict(type='String', default='',
                            # None <-> both
                            vocabulary=(_('tablefilter'), _('facetbox'), ''),
-                           help=_('context where this box should be displayed')),
+                           help=_('context where this facet should be displayed, '
+                                  'leave empty for both')),
         }
     visible = True
     context = ''
@@ -297,27 +366,54 @@
 
     @property
     def operator(self):
+        """Return the operator (AND or OR) to use for this facet when multiple
+        values are selected.
+        """
         # OR between selected values by default
         return self._cw.form.get(self.__regid__ + '_andor', 'OR')
 
+    def rqlexec(self, rql, args=None):
+        """Utility method to execute some rql queries, and simply returning an
+        empty list if :exc:`Unauthorized` is raised.
+        """
+        try:
+            return self._cw.execute(rql, args)
+        except Unauthorized:
+            return []
+
     def get_widget(self):
-        """return the widget instance to use to display this facet
+        """Return the widget instance to use to display this facet, or None if
+        the facet can't do anything valuable (only one value in the vocabulary
+        for instance).
         """
         raise NotImplementedError
 
     def add_rql_restrictions(self):
-        """add restriction for this facet into the rql syntax tree"""
+        """When some facet criteria has been updated, this method is called to
+        add restriction for this facet into the rql syntax tree. It should get
+        back its value in form parameters, and modify the syntax tree
+        (`self.rqlst`) accordingly.
+        """
         raise NotImplementedError
 
 
 class VocabularyFacet(AbstractFacet):
+    """This abstract class extend :class:`AbstractFacet` to use the
+    :class:`FacetVocabularyWidget` as widget, suitable for facets that may
+    restrict values according to a (usually computed) vocabulary.
+
+    A class which inherits from VocabularyFacet must define at least these methods:
+
+    .. automethod:: cubicweb.web.facet.VocabularyFacet.vocabulary
+    .. automethod:: cubicweb.web.facet.VocabularyFacet.possible_values
+    """
     needs_update = True
 
     def get_widget(self):
-        """return the widget instance to use to display this facet
+        """Return the widget instance to use to display this facet.
 
-        default implentation expects a .vocabulary method on the facet and
-        return a combobox displaying this vocabulary
+        This implementation expects a .vocabulary method on the facet and
+        return a combobox displaying this vocabulary.
         """
         vocab = self.vocabulary()
         if len(vocab) <= 1:
@@ -332,33 +428,86 @@
         return wdg
 
     def vocabulary(self):
-        """return vocabulary for this facet, eg a list of 2-uple (label, value)
+        """Return vocabulary for this facet, eg a list of 2-uple (label, value).
         """
         raise NotImplementedError
 
     def possible_values(self):
-        """return a list of possible values (as string since it's used to
-        compare to a form value in javascript) for this facet
+        """Return a list of possible values (as string since it's used to
+        compare to a form value in javascript) for this facet.
         """
         raise NotImplementedError
 
     def support_and(self):
         return False
 
-    def rqlexec(self, rql, args=None):
-        try:
-            return self._cw.execute(rql, args)
-        except Unauthorized:
-            return []
+
+class RelationFacet(VocabularyFacet):
+    """Base facet to filter some entities according to other entities to which
+    they are related. Create concret facet by inheriting from this class an then
+    configuring it by setting class attribute described below.
+
+    The relation is defined by the `rtype` and `role` attributes.
+
+    The values displayed for related entities will be:
+
+    * result of calling their `label_vid` view if specified
+    * else their `target_attr` attribute value if specified
+    * else their eid (you usually want something nicer...)
+
+    When no `label_vid` is set, you will get translated value if `i18nable` is
+    set.
+
+    You can filter out target entity types by specifying `target_type`
+
+    By default, vocabulary will be displayed sorted on `target_attr` value in an
+    ascending way. You can control sorting with:
+
+    * `sortfunc`: set this to a stored procedure name if you want to sort on the
+      result of this function's result instead of direct value
+
+    * `sortasc`: boolean flag to control ascendant/descendant sorting
+
+    To illustrate this facet, let's take for example an *excerpt* of the schema
+    of an office location search application:
+
+    .. sourcecode:: python
+
+      class Office(WorkflowableEntityType):
+          price = Int(description='euros / m2 / HC / HT')
+          surface = Int(description='m2')
+          has_address = SubjectRelation('PostalAddress',
+                                        cardinality='1?',
+                                        composite='subject')
+          proposed_by = SubjectRelation('Agency')
 
 
-class RelationFacet(VocabularyFacet):
+    We can simply define a facet to filter offices according to the agency
+    proposing it:
+
+    .. sourcecode:: python
+
+      class AgencyFacet(RelationFacet):
+          __regid__ = 'agency'
+          # this facet should only be selected when visualizing offices
+          __select__ = RelationFacet.__select__ & is_instance('Office')
+          # this facet is a filter on the 'Agency' entities linked to the office
+          # through the 'proposed_by' relation, where the office is the subject
+          # of the relation
+          rtype = 'has_address'
+          # 'subject' is the default but setting it explicitly doesn't hurt...
+          role = 'subject'
+          # we want to display the agency's name
+          target_attr = 'name'
+    """
     __select__ = partial_relation_possible() & match_context_prop()
     # class attributes to configure the relation facet
     rtype = None
     role = 'subject'
     target_attr = 'eid'
     target_type = None
+    # should value be internationalized (XXX could be guessed from the schema)
+    i18nable = True
     # set this to a stored procedure name if you want to sort on the result of
     # this function's result instead of direct value
     sortfunc = None
@@ -367,24 +516,33 @@
     # if you want to call a view on the entity instead of using `target_attr`
     label_vid = None
 
+    # internal purpose
+    _select_target_entity = True
+
+    title = property(rtype_facet_title)
+
     @property
-    def title(self):
-        return display_name(self._cw, self.rtype, form=self.role)
-
+    def rql_sort(self):
+        """return true if we can handle sorting in the rql query. E.g.  if
+        sortfunc is set or if we have not to transform the returned value (eg no
+        label_vid and not i18nable)
+        """
+        return self.sortfunc is not None or (self.label_vid is None
+                                             and not self.i18nable)
     def vocabulary(self):
         """return vocabulary for this facet, eg a list of 2-uple (label, value)
         """
         rqlst = self.rqlst
         rqlst.save_state()
-        if self.label_vid is not None and self.sortfunc is None:
+        if self.rql_sort:
+            sort = self.sortasc
+        else:
             sort = None # will be sorted on label
-        else:
-            sort = self.sortasc
         try:
             mainvar = self.filtered_variable
             var = insert_attr_select_relation(
                 rqlst, mainvar, self.rtype, self.role, self.target_attr,
-                self.sortfunc, sort)
+                self.sortfunc, sort, self._select_target_entity)
             if self.target_type is not None:
                 rqlst.add_type_restriction(var, self.target_type)
             try:
@@ -407,20 +565,37 @@
         rqlst.save_state()
         try:
             _cleanup_rqlst(rqlst, self.filtered_variable)
-            _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype, self.role)
+            if self._select_target_entity:
+                _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype,
+                                          self.role, select_target_entity=True)
+            else:
+                insert_attr_select_relation(
+                    rqlst, self.filtered_variable, self.rtype, self.role, self.target_attr,
+                    select_target_entity=False)
             return [str(x) for x, in self.rqlexec(rqlst.as_string())]
+        except:
+            import traceback
+            traceback.print_exc()
         finally:
             rqlst.recover()
 
     def rset_vocabulary(self, rset):
-        if self.label_vid is None:
+        if self.i18nable:
             _ = self._cw._
+        else:
+            _ = unicode
+        if self.rql_sort:
             return [(_(label), eid) for eid, label in rset]
-        if self.sortfunc is None:
-            return sorted((entity.view(self.label_vid), entity.eid)
-                          for entity in rset.entities())
-        return [(entity.view(self.label_vid), entity.eid)
-                for entity in rset.entities()]
+        if self.label_vid is None:
+            assert self.i18nable
+            values = [(_(label), eid) for eid, label in rset]
+        else:
+            values = [(entity.view(self.label_vid), entity.eid)
+                      for entity in rset.entities()]
+        values = sorted(values)
+        if self.sortasc:
+            return values
+        return reversed(values)
 
     @cached
     def support_and(self):
@@ -449,10 +624,10 @@
             # only one value selected
             self.rqlst.add_eid_restriction(restrvar, value)
         elif self.operator == 'OR':
-            #  multiple values with OR operator
             # set_distinct only if rtype cardinality is > 1
             if self.support_and():
                 self.rqlst.set_distinct(True)
+            # multiple ORed values: using IN is fine
             self.rqlst.add_eid_restriction(restrvar, value)
         else:
             # multiple values with AND operator
@@ -462,12 +637,117 @@
                 self.rqlst.add_eid_restriction(restrvar, value.pop())
 
 
-class AttributeFacet(RelationFacet):
+class RelationAttributeFacet(RelationFacet):
+    """Base facet to filter some entities according to an attribute of other
+    entities to which they are related. Most things work similarly as
+    :class:`RelationFacet`, except that:
+
+    * `label_vid` doesn't make sense here
+
+    * you should specify the attribute type using `attrtype` if it's not a
+      String
+
+    * you can specify a comparison operator using `comparator`
+
+
+    Back to our example... if you want to search office by postal code and that
+    you use a :class:`RelationFacet` for that, you won't get the expected
+    behaviour: if two offices have the same postal code, they've however two
+    different addresses.  So you'll see in the facet the same postal code twice,
+    though linked to a different address entity. There is a great chance your
+    users won't understand that...
+
+    That's where this class come in ! It's used to said that you want to filter
+    according to the *attribute value* of a relatied entity, not to the entity
+    itself. Now here is the source code for the facet:
+
+    .. sourcecode:: python
+
+      class PostalCodeFacet(RelationAttributeFacet):
+          __regid__ = 'postalcode'
+          # this facet should only be selected when visualizing offices
+          __select__ = RelationAttributeFacet.__select__ & is_instance('Office')
+          # this facet is a filter on the PostalAddress entities linked to the
+          # office through the 'has_address' relation, where the office is the
+          # subject of the relation
+          rtype = 'has_address'
+          role = 'subject'
+          # we want to search according to address 'postal_code' attribute
+          target_attr = 'postalcode'
+    """
+    _select_target_entity = False
     # attribute type
     attrtype = 'String'
     # type of comparison: default is an exact match on the attribute value
     comparator = '=' # could be '<', '<=', '>', '>='
-    i18nable = True
+
+    def rset_vocabulary(self, rset):
+        if self.i18nable:
+            _ = self._cw._
+        else:
+            _ = unicode
+        if self.rql_sort:
+            return [(_(value), value) for value, in rset]
+        values = [(_(value), value) for value, in rset]
+        if self.sortasc:
+            return sorted(values)
+        return reversed(sorted(values))
+
+    def add_rql_restrictions(self):
+        """add restriction for this facet into the rql syntax tree"""
+        value = self._cw.form.get(self.__regid__)
+        if not value:
+            return
+        mainvar = self.filtered_variable
+        restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, self.role)
+        self.rqlst.set_distinct(True)
+        if isinstance(value, basestring) or self.operator == 'OR':
+            # only one value selected or multiple ORed values: using IN is fine
+            self.rqlst.add_constant_restriction(restrvar, self.target_attr, value,
+                                                self.attrtype, self.comparator)
+        else:
+            # multiple values with AND operator
+            self.rqlst.add_constant_restriction(restrvar, self.target_attr, value.pop(),
+                                                self.attrtype, self.comparator)
+            while value:
+                restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, self.role)
+                self.rqlst.add_constant_restriction(restrvar, self.target_attr, value.pop(),
+                                                    self.attrtype, self.comparator)
+
+
+class AttributeFacet(RelationAttributeFacet):
+    """Base facet to filter some entities according one of their attribute.
+    Configuration is mostly similarly as :class:`RelationAttributeFacet`, except that:
+
+    * `target_attr` doesn't make sense here (you specify the attribute using `rtype`
+    * `role` neither, it's systematically 'subject'
+
+    So, suppose that in our office search example you want to refine search according
+    to the office's surface. Here is a code snippet achieving this:
+
+    .. sourcecode:: python
+
+      class SurfaceFacet(AttributeFacet):
+          __regid__ = 'surface'
+          __select__ = AttributeFacet.__select__ & is_instance('Office')
+          # this facet is a filter on the office'surface
+          rtype = 'surface'
+          # override the default value of operator since we want to filter
+          # according to a minimal value, not an exact one
+          comparator = '>='
+
+          def vocabulary(self):
+              '''override the default vocabulary method since we want to
+              hard-code our threshold values.
+
+              Not overriding would generate a filter containing all existing
+              surfaces defined in the database.
+              '''
+              return [('> 200', '200'), ('> 250', '250'),
+                      ('> 275', '275'), ('> 300', '300')]
+    """
+
+    _select_target_entity = True
 
     def vocabulary(self):
         """return vocabulary for this facet, eg a list of 2-uple (label, value)
@@ -491,13 +771,6 @@
         # *list* (see rqlexec implementation)
         return rset and self.rset_vocabulary(rset)
 
-    def rset_vocabulary(self, rset):
-        if self.i18nable:
-            _ = self._cw._
-        else:
-            _ = unicode
-        return [(_(value), value) for value, in rset]
-
     def support_and(self):
         return False
 
@@ -511,27 +784,35 @@
                                             self.attrtype, self.comparator)
 
 
-class FilterRQLBuilder(object):
-    """called by javascript to get a rql string from filter form"""
+class RangeFacet(AttributeFacet):
+    """This class allows to filter entities according to an attribute of
+    numerical type.
+
+    It displays a slider using `jquery`_ to choose a lower bound and an upper
+    bound.
 
-    def __init__(self, req):
-        self._cw = req
+    The example below provides an alternative to the surface facet seen earlier,
+    in a more powerful way since
+
+    * lower/upper boundaries are computed according to entities to filter
+    * user can specify lower/upper boundaries, not only the lower one
+
+    .. sourcecode:: python
 
-    def build_rql(self):#, tablefilter=False):
-        form = self._cw.form
-        facetids = form['facets'].split(',')
-        select = self._cw.vreg.parse(self._cw, form['baserql']).children[0] # XXX Union unsupported yet
-        mainvar = filtered_variable(select)
-        toupdate = []
-        for facetid in facetids:
-            facet = get_facet(self._cw, facetid, select, mainvar)
-            facet.add_rql_restrictions()
-            if facet.needs_update:
-                toupdate.append(facetid)
-        return select.as_string(), toupdate
+      class SurfaceFacet(RangeFacet):
+          __regid__ = 'surface'
+          __select__ = RangeFacet.__select__ & is_instance('Office')
+          # this facet is a filter on the office'surface
+          rtype = 'surface'
 
+    All this with even less code!
 
-class RangeFacet(AttributeFacet):
+    The image below display the rendering of the slider:
+
+    .. image:: ../images/facet_range.png
+
+    .. _jquery: http://www.jqueryui.com/
+    """
     attrtype = 'Float' # only numerical types are supported
 
     @property
@@ -572,6 +853,13 @@
 
 
 class DateRangeFacet(RangeFacet):
+    """This class works similarly as the :class:`RangeFacet` but for attribute
+    of date type.
+
+    The image below display the rendering of the slider for a date range:
+
+    .. image:: ../images/facet_date_range.png
+    """
     attrtype = 'Date' # only date types are supported
 
     @property
@@ -584,12 +872,29 @@
 
 
 class HasRelationFacet(AbstractFacet):
+    """This class simply filter according to the presence of a relation
+    (whatever the entity at the other end). It display a simple checkbox that
+    lets you refine your selection in order to get only entities that actually
+    have this relation. You simply have to define which relation using the
+    `rtype` and `role` attributes.
+
+    Here is an example of the rendering of thos facet to filter book with image
+    and the corresponding code:
+
+    .. image:: ../images/facet_has_image.png
+
+    .. sourcecode:: python
+
+      class HasImageFacet(HasRelationFacet):
+          __regid__ = 'hasimage'
+          __select__ = HasRelationFacet.__select__ & is_instance('Book')
+          rtype = 'has_image'
+          role = 'subject'
+    """
     rtype = None # override me in subclass
     role = 'subject' # role of filtered entity in the relation
 
-    @property
-    def title(self):
-        return display_name(self._cw, self.rtype, self.role)
+    title = property(rtype_facet_title)
 
     def support_and(self):
         return False
@@ -802,3 +1107,25 @@
 
     def _render(self):
         pass
+
+# other classes ################################################################
+
+class FilterRQLBuilder(object):
+    """called by javascript to get a rql string from filter form"""
+
+    def __init__(self, req):
+        self._cw = req
+
+    def build_rql(self):#, tablefilter=False):
+        form = self._cw.form
+        facetids = form['facets'].split(',')
+        # XXX Union unsupported yet
+        select = self._cw.vreg.parse(self._cw, form['baserql']).children[0]
+        mainvar = filtered_variable(select)
+        toupdate = []
+        for facetid in facetids:
+            facet = get_facet(self._cw, facetid, select, mainvar)
+            facet.add_rql_restrictions()
+            if facet.needs_update:
+                toupdate.append(facetid)
+        return select.as_string(), toupdate
--- a/web/form.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/form.py	Wed Nov 03 16:38:28 2010 +0100
@@ -224,7 +224,7 @@
         if forminfo:
             self._form_previous_values = forminfo['values']
             self._form_valerror = forminfo['error']
-            # if some validation error occured on entity creation, we have to
+            # if some validation error occurred on entity creation, we have to
             # get the original variable name from its attributed eid
             foreid = self.form_valerror.entity
             for var, eid in forminfo['eidmap'].items():
--- a/web/formfields.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/formfields.py	Wed Nov 03 16:38:28 2010 +0100
@@ -215,13 +215,24 @@
         self.creation_rank = Field.__creation_rank
         Field.__creation_rank += 1
 
+    def as_string(self, repr=True):
+        l = [u'<%s' % self.__class__.__name__]
+        for attr in ('name', 'eidparam', 'role', 'id', 'value'):
+            value = getattr(self, attr)
+            if value is not None and value is not _MARKER:
+                l.append('%s=%r' % (attr, value))
+        if repr:
+            l.append('@%#x' % id(self))
+        return u'%s>' % ' '.join(l)
+
     def __unicode__(self):
-        return u'<%s name=%r eidparam=%s role=%r id=%r value=%r visible=%r @%x>' % (
-            self.__class__.__name__, self.name, self.eidparam, self.role,
-            self.id, self.value, self.is_visible(), id(self))
+        return self.as_string(False)
+
+    def __str__(self):
+        return self.as_string(False).encode('UTF8')
 
     def __repr__(self):
-        return self.__unicode__().encode('utf-8')
+        return self.as_string(True).encode('UTF8')
 
     def init_widget(self, widget):
         if widget is not None:
@@ -325,7 +336,7 @@
                     value = getattr(entity, self.name)
                     if value is not None or not self.fallback_on_none_attribute:
                         return value
-            elif entity.has_eid() or entity.relation_cached(self.name, self.role):
+            elif entity.has_eid() or entity.cw_relation_cached(self.name, self.role):
                 value = [r[0] for r in entity.related(self.name, self.role)]
                 if value or not self.fallback_on_none_attribute:
                     return value
@@ -361,8 +372,11 @@
         return widget.render(form, self, renderer)
 
     def vocabulary(self, form, **kwargs):
-        """return vocabulary for this field. This method will be called by
-        widgets which requires a vocabulary.
+        """return vocabulary for this field. This method will be
+        called by widgets which requires a vocabulary.
+
+        It should return a list of tuple (label, value), where value
+        *must be an unicode string*, not a typed value.
         """
         assert self.choices is not None
         if callable(self.choices):
@@ -387,12 +401,25 @@
         if vocab and not isinstance(vocab[0], (list, tuple)):
             vocab = [(x, x) for x in vocab]
         if self.internationalizable:
-            # the short-cirtcuit 'and' boolean operator is used here to permit
-            # a valid empty string in vocabulary without attempting to translate
-            # it by gettext (which can lead to weird strings display)
-            vocab = [(label and form._cw._(label), value) for label, value in vocab]
+            # the short-cirtcuit 'and' boolean operator is used here
+            # to permit a valid empty string in vocabulary without
+            # attempting to translate it by gettext (which can lead to
+            # weird strings display)
+            vocab = [(label and form._cw._(label), value)
+                     for label, value in vocab]
         if self.sort:
             vocab = vocab_sort(vocab)
+        # XXX pre 3.9 bw compat
+        for i, option in enumerate(vocab):
+            # option may be a 2 or 3-uple (see Select widget _render method for
+            # explanation)
+            value = option[1]
+            if value is not None and not isinstance(value, basestring):
+                warn('[3.9] %s: vocabulary value should be an unicode string'
+                     % self, DeprecationWarning)
+                option = list(option)
+                option[1] = unicode(value)
+                vocab[i] = option
         return vocab
 
     def format(self, form):
@@ -401,7 +428,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 +437,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 +447,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 +478,7 @@
         except ProcessFormError:
             return True
         except UnmodifiedField:
-            return False
+            return False # not modified
         if previous_value == new_value:
             return False # not modified
         return True
@@ -826,12 +859,32 @@
     """
     widget = fw.Radio
 
+    def __init__(self, allow_none=False, **kwargs):
+        super(BooleanField, self).__init__(**kwargs)
+        self.allow_none = allow_none
+
     def vocabulary(self, form):
         if self.choices:
             return super(BooleanField, self).vocabulary(form)
+        if self.allow_none:
+            return [('', ''), (form._cw._('yes'), '1'), (form._cw._('no'), '0')]
+        # XXX empty string for 'no' in that case for bw compat
         return [(form._cw._('yes'), '1'), (form._cw._('no'), '')]
 
+    def format_single_value(self, req, value):
+        """return value suitable for display"""
+        if self.allow_none:
+            if value is None:
+                return u''
+            if value is False:
+                return '0'
+        return super(BooleanField, self).format_single_value(req, value)
+
     def _ensure_correctly_typed(self, form, value):
+        if self.allow_none:
+            if value:
+                return bool(int(value))
+            return None
         return bool(value)
 
 
@@ -943,7 +996,7 @@
     linkedto = entity.linked_to(rtype, role)
     if linkedto:
         buildent = entity._cw.entity_from_eid
-        return [(buildent(eid).view('combobox'), eid) for eid in linkedto]
+        return [(buildent(eid).view('combobox'), unicode(eid)) for eid in linkedto]
     return []
 
 def relvoc_init(entity, rtype, role, required=False):
@@ -954,7 +1007,7 @@
     # vocabulary doesn't include current values, add them
     if entity.has_eid():
         rset = entity.related(rtype, role)
-        vocab += [(e.view('combobox'), e.eid) for e in rset.entities()]
+        vocab += [(e.view('combobox'), unicode(e.eid)) for e in rset.entities()]
     return vocab
 
 def relvoc_unrelated(entity, rtype, role, limit=None):
@@ -985,7 +1038,7 @@
         if entity.eid in done:
             continue
         done.add(entity.eid)
-        res.append((entity.view('combobox'), entity.eid))
+        res.append((entity.view('combobox'), unicode(entity.eid)))
     return res
 
 
@@ -1044,7 +1097,7 @@
             form.formvalues[(self, form)] = value
 
     def format_single_value(self, req, value):
-        return value
+        return unicode(value)
 
     def process_form_value(self, form):
         """process posted form and return correctly typed value"""
@@ -1080,18 +1133,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 +1161,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 +1186,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/formwidgets.py	Wed Nov 03 16:38:28 2010 +0100
@@ -60,7 +60,6 @@
 .. autoclass:: cubicweb.web.formwidgets.AjaxWidget
 .. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget
 
-.. kill or document AddComboBoxWidget
 .. kill or document StaticFileAutoCompletionWidget
 .. kill or document LazyRestrictedAutoCompletionWidget
 .. kill or document RestrictedAutoCompletionWidget
@@ -550,7 +549,7 @@
         return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
 <img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
                 % (helperid, inputid, year, month,
-                   form._cw.external_resource('CALENDAR_ICON'),
+                   form._cw.uiprops['CALENDAR_ICON'],
                    form._cw._('calendar'), helperid) )
 
 
@@ -571,10 +570,10 @@
         # XXX find a way to understand every format
         fmt = req.property_value('ui.date-format')
         fmt = fmt.replace('%Y', 'yy').replace('%m', 'mm').replace('%d', 'dd')
-        req.add_onload(u'jqNode("%s").datepicker('
+        req.add_onload(u'cw.jqNode("%s").datepicker('
                        '{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
                        ' showOn: "button", buttonImageOnly: true})' % (
-                           domid, req.external_resource('CALENDAR_ICON'), fmt))
+                           domid, req.uiprops['CALENDAR_ICON'], fmt))
         if self.datestr is None:
             value = self.values(form, field)[0]
         else:
@@ -599,7 +598,7 @@
     def _render(self, form, field, renderer):
         req = form._cw
         domid = field.dom_id(form, self.suffix)
-        req.add_onload(u'jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
+        req.add_onload(u'cw.jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
             domid, self.timestr, self.timesteps, self.separator))
         if self.timestr is None:
             value = self.values(form, field)[0]
@@ -654,10 +653,16 @@
         timestr = req.form.get(field.input_name(form, 'time')).strip() or None
         if datestr is None:
             return None
-        date = todatetime(req.parse_datetime(datestr, 'Date'))
+        try:
+            date = todatetime(req.parse_datetime(datestr, 'Date'))
+        except ValueError, exc:
+            raise ProcessFormError(unicode(exc))
         if timestr is None:
             return date
-        time = req.parse_datetime(timestr, 'Time')
+        try:
+            time = req.parse_datetime(timestr, 'Time')
+        except ValueError, exc:
+            raise ProcessFormError(unicode(exc))
         return date.replace(hour=time.hour, minute=time.minute, second=time.second)
 
 
@@ -776,24 +781,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 +941,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 +972,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/htmlwidgets.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,120 @@
+# 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
+
+TYPE_CHECKS = [('STYLESHEETS', list), ('JAVASCRIPTS', list),
+               ('STYLESHEETS_IE', list), ('STYLESHEETS_PRINT', list),
+               ]
+
+class lazystr(object):
+    def __init__(self, string, context):
+        self.string = string
+        self.context = context
+    def __str__(self):
+        return self.string % self.context
+
+
+class PropertySheet(dict):
+    def __init__(self, cache_directory, **context):
+        self._cache_directory = cache_directory
+        self.context = context
+        self.reset()
+        context['sheet'] = self
+        context['lazystr'] = self.lazystr
+        self._percent_rgx = re.compile('%(?!\()')
+
+    def lazystr(self, str):
+        return lazystr(str, self)
+
+    def reset(self):
+        self.clear()
+        self._ordered_propfiles = []
+        self._propfile_mtime = {}
+        self._sourcefile_mtime = {}
+        self._cache = {}
+
+    def load(self, fpath):
+        scriptglobals = self.context.copy()
+        scriptglobals['__file__'] = fpath
+        execfile(fpath, scriptglobals, self)
+        for name, type in TYPE_CHECKS:
+            if name in self:
+                if not isinstance(self[name], type):
+                    msg = "Configuration error: %s.%s should be a %s" % (fpath, name, type)
+                    raise Exception(msg)
+        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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/request.py	Wed Nov 03 16:38:28 2010 +0100
@@ -27,6 +27,7 @@
 from datetime import date
 from urlparse import urlsplit
 from itertools import count
+from warnings import warn
 
 from rql.utils import rqlvar_maker
 
@@ -36,15 +37,13 @@
 
 from cubicweb.dbapi import DBAPIRequest
 from cubicweb.mail import header
-from cubicweb.uilib import remove_html_tags
+from cubicweb.uilib import remove_html_tags, js
 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
-                          RequestError, StatusResponse, json)
+                          RequestError, StatusResponse)
 from cubicweb.web.http_headers import Headers
 
-dumps = json.dumps
-
 _MARKER = object()
 
 
@@ -83,6 +82,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
@@ -94,12 +99,8 @@
         # search state: 'normal' or 'linksearch' (eg searching for an object
         # to create a relation with another)
         self.search_state = ('normal',)
-        # tabindex generator
-        self.tabindexgen = count(1)
-        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()
@@ -127,6 +128,13 @@
         """
         return self.set_varmaker()
 
+    def _get_tabindex_func(self):
+        nextfunc = self.get_page_data('nexttabfunc')
+        if nextfunc is None:
+            nextfunc = count(1).next
+            self.set_page_data('nexttabfunc', nextfunc)
+        return nextfunc
+
     def set_varmaker(self):
         varmaker = self.get_page_data('rql_varmaker')
         if varmaker is None:
@@ -139,6 +147,8 @@
         or an anonymous connection is open
         """
         super(CubicWebRequestBase, self).set_session(session, user)
+        # tabindex generator
+        self.next_tabindex = self._get_tabindex_func()
         # set request language
         vreg = self.vreg
         if self.user:
@@ -339,23 +349,37 @@
             return breadcrumbs.pop()
         return self.base_url()
 
-    def user_rql_callback(self, args, msg=None):
+    def user_rql_callback(self, rqlargs, *args, **kwargs):
         """register a user callback to execute some rql query and return an url
-        to call it ready to be inserted in html
+        to call it ready to be inserted in html.
+
+        rqlargs should be a tuple containing argument to give to the execute function.
+
+        For other allowed arguments, see :meth:`user_callback` method
         """
         def rqlexec(req, rql, args=None, key=None):
             req.execute(rql, args, key)
-        return self.user_callback(rqlexec, args, msg)
+        return self.user_callback(rqlexec, rqlargs, *args, **kwargs)
+
+    def user_callback(self, cb, cbargs, *args, **kwargs):
+        """register the given user callback and return an url to call it ready
+        to be inserted in html.
 
-    def user_callback(self, cb, args, msg=None, nonify=False):
-        """register the given user callback and return an url to call it ready to be
-        inserted in html
+        You can specify the underlying js function to call using a 'jsfunc'
+        named args, to one of :func:`userCallback`,
+        ':func:`userCallbackThenUpdateUI`, ':func:`userCallbackThenReloadPage`
+        (the default). Take care arguments may vary according to the used
+        function.
         """
         self.add_js('cubicweb.ajax.js')
-        cbname = self.register_onetime_callback(cb, *args)
-        msg = dumps(msg or '')
-        return "javascript:userCallbackThenReloadPage('%s', %s)" % (
-            cbname, msg)
+        jsfunc = kwargs.pop('jsfunc', 'userCallbackThenReloadPage')
+        if 'msg' in kwargs:
+            warn('[3.10] msg should be given as positional argument',
+                 DeprecationWarning, stacklevel=2)
+            args = (kwargs.pop('msg'),) + args
+        assert not kwargs, 'dunno what to do with remaining kwargs: %s' % kwargs
+        cbname = self.register_onetime_callback(cb, *cbargs)
+        return "javascript: %s" % getattr(js, jsfunc)(cbname, *args)
 
     def register_onetime_callback(self, func, *args):
         cbname = 'cb_%s' % (
@@ -366,7 +390,6 @@
             try:
                 ret = func(req, *args)
             except TypeError:
-                from warnings import warn
                 warn('[3.2] user callback should now take request as argument')
                 ret = func(*args)
             self.unregister_callback(self.pageid, cbname)
@@ -508,7 +531,7 @@
         """set output content type for this request. An optional filename
         may be given
         """
-        if content_type.startswith('text/'):
+        if content_type.startswith('text/') and ';charset=' not in content_type:
             content_type += ';charset=' + (encoding or self.encoding)
         self.set_header('content-type', content_type)
         if filename:
@@ -564,24 +587,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').%s; noop()" % (
+            nodeid, js.loadxhtml(url, None, 'get', replacemode))
 
     # urls/path management ####################################################
 
@@ -589,10 +618,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 +643,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 +718,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 +823,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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,3 @@
+body { background-color: %(bgcolor)s
+       font-size: 100%;
+     }
\ No newline at end of file
--- a/web/test/data/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/data/schema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -75,3 +75,19 @@
     cp   = String(maxsize=12)
     ville= String(maxsize=32)
 
+# enough relations to cover most reledit use cases
+class Project(EntityType):
+    title = String(maxsize=32, required=True, fulltextindexed=True)
+    long_desc = SubjectRelation('Blog', composite='subject', cardinality='?*')
+    manager = SubjectRelation('Personne', cardinality='?*')
+
+class composite_card11_2ttypes(RelationDefinition):
+    subject = 'Project'
+    object = ('File', 'Blog')
+    composite = 'subject'
+    cardinality = '?*'
+
+class Ticket(EntityType):
+    title = String(maxsize=32, required=True, fulltextindexed=True)
+    concerns = SubjectRelation('Project', composite='object')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet1.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,4 @@
+bgcolor = '#000000'
+stylesheets = ['%s/cubicweb.css' % datadir_url]
+logo = '%s/logo.png' % datadir_url
+lazy = lazystr('%(bgcolor)s')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet2.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,3 @@
+fontcolor = 'black'
+bgcolor = '#FFFFFF'
+stylesheets = sheet['stylesheets'] + ['%s/mycube.css' % datadir_url]
--- a/web/test/jstest_python.jst	Tue Jul 27 12:36:03 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-// run tests with the following command line :
-// $ crosscheck jstest_python.jst
-
-crosscheck.addTest({
-
-    setup: function() {
-        crosscheck.load("testutils.js");
-        crosscheck.load("../data/jquery.js");
-        crosscheck.load("../data/cubicweb.compat.js");
-        crosscheck.load("../data/cubicweb.python.js");
-    },
-
-    test_basic_number_parsing: function () {
-	var d = strptime('2008/08/08', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
-	d = strptime('2008/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
-	d = strptime('8/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [8, 8, 8, 0, 0])
-	d = strptime('0/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [0, 8, 8, 0, 0])
-	d = strptime('-10/8/8', '%Y/%m/%d');
-	assertArrayEquals(datetuple(d), [-10, 8, 8, 0, 0])
-	d = strptime('-35000', '%Y');
-	assertArrayEquals(datetuple(d), [-35000, 1, 1, 0, 0])
-    },
-
-    test_custom_format_parsing: function () {
-	var d = strptime('2008-08-08', '%Y-%m-%d');
-	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
- 	d = strptime('2008 - !  08: 08', '%Y - !  %m: %d');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 0, 0])
- 	d = strptime('2008-08-08 12:14', '%Y-%m-%d %H:%M');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 12, 14])
- 	d = strptime('2008-08-08 1:14', '%Y-%m-%d %H:%M');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 1, 14])
- 	d = strptime('2008-08-08 01:14', '%Y-%m-%d %H:%M');
- 	assertArrayEquals(datetuple(d), [2008, 8, 8, 1, 14])
-   }
-//,
-//
-//  test_gregorian_parsing: function() {
-//     var d = parseGregorianDateTime("May 28 0100 09:00:00 GMT");
-//     assertArrayEquals(datetuple(d), [100, 5, 28, 10, 0]);
-//     d = parseGregorianDateTime("May 28 0099 09:00:00 GMT");
-//     assertArrayEquals(datetuple(d), [99, 5, 28, 10, 0]);
-//   }
-
-})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url0.html	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,1 @@
+['foo', 'bar']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_ajax.html	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,24 @@
+<html>
+  <head>
+    <!-- dependencies -->
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.dom.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.ajax.js" type="text/javascript"></script>
+    <!-- qunit files -->
+    <script type="text/javascript" src="../../../devtools/data/qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="../../../devtools/data/qunit.css" />
+    <!-- test suite -->
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_ajax.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main"> </div>
+    <h1 id="qunit-header">cubicweb.ajax.js functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_ajax.js	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,244 @@
+$(document).ready(function() {
+
+    module("ajax", {
+        setup: function() {
+          this.scriptsLength = $('head script[src]').length-1;
+          this.cssLength = $('head link[rel=stylesheet]').length-1;
+          // re-initialize cw loaded cache so that each tests run in a
+          // clean environment, have a lookt at _loadAjaxHtmlHead implementation
+          // in cubicweb.ajax.js for more information.
+          cw.loaded_src = [];
+          cw.loaded_href = [];
+        },
+        teardown: function() {
+          $('head script[src]:gt(' + this.scriptsLength + ')').remove();
+          $('head link[rel=stylesheet]:gt(' + this.cssLength + ')').remove();
+        }
+      });
+
+    function jsSources() {
+        return $.map($('head script[src]'), function(script) {
+            return script.getAttribute('src');
+        });
+    }
+
+    test('test simple h1 inclusion (ajax_url0.html)', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(jQuery('#main').children().length, 1);
+                equals(jQuery('#main h1').html(), 'Hello');
+                start();
+            }
+        });
+    });
+
+    test('test simple html head inclusion (ajax_url1.html)', function() {
+        expect(6);
+        var scriptsIncluded = jsSources();
+        equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url1.html', {
+            callback: function() {
+                var origLength = scriptsIncluded.length;
+                scriptsIncluded = jsSources();
+                // check that foo.js has been inserted in <head>
+                equals(scriptsIncluded.length, origLength + 1);
+                equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+                // check that <div class="ajaxHtmlHead"> has been removed
+                equals(jQuery('#main').children().length, 1);
+                equals(jQuery('div.ajaxHtmlHead').length, 0);
+                equals(jQuery('#main h1').html(), 'Hello');
+                start();
+            }
+        });
+    });
+
+    test('test addCallback', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function() {
+            equals(jQuery('#main').children().length, 1);
+            equals(jQuery('#main h1').html(), 'Hello');
+            start();
+        });
+    });
+
+    test('test callback after synchronous request', function() {
+        expect(1);
+        var deferred = new Deferred();
+        var result = jQuery.ajax({
+            url: './ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+        stop();
+        deferred.addCallback(function() {
+            // add an assertion to ensure the callback is executed
+            ok(true, "callback is executed");
+            start();
+        });
+    });
+
+    test('test addCallback with parameters', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function(data, req, arg1, arg2) {
+            equals(arg1, 'Hello');
+            equals(arg2, 'world');
+            start();
+        },
+        'Hello', 'world');
+    });
+
+    test('test callback after synchronous request with parameters', function() {
+        var deferred = new Deferred();
+        var result = jQuery.ajax({
+            url: './ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+        deferred.addCallback(function(data, req, arg1, arg2) {
+            // add an assertion to ensure the callback is executed
+            ok(true, "callback is executed");
+            equals(arg1, 'Hello');
+            equals(arg2, 'world');
+        },
+        'Hello', 'world');
+    });
+
+  test('test addErrback', function() {
+        expect(1);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function() {
+            // throw an exception to start errback chain
+            throw new Error();
+        });
+        d.addErrback(function() {
+            ok(true, "errback is executed");
+            start();
+        });
+    });
+
+    test('test callback / errback execution order', function() {
+        expect(4);
+        var counter = 0;
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(++counter, 1); // should be executed first
+                start();
+            }
+        });
+        d.addCallback(function() {
+            equals(++counter, 2); // should be executed and break callback chain
+            throw new Error();
+        });
+        d.addCallback(function() {
+            // should not be executed since second callback raised an error
+            ok(false, "callback is executed");
+        });
+        d.addErrback(function() {
+            // should be executed after the second callback
+            equals(++counter, 3);
+        });
+        d.addErrback(function() {
+            // should be executed after the first errback
+            equals(++counter, 4);
+        });
+    });
+
+    test('test already included resources are ignored (ajax_url1.html)', function() {
+        expect(10);
+        var scriptsIncluded = jsSources();
+        // NOTE:
+        equals(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
+        equals(jQuery('head link').length, 1);
+        /* use endswith because in pytest context we have an absolute path */
+        ok(jQuery('head link').attr('href').endswith('/qunit.css'));
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url1.html', {
+            callback: function() {
+                var origLength = scriptsIncluded.length;
+                scriptsIncluded = jsSources();
+                try {
+                    // check that foo.js has been inserted in <head>
+                    equals(scriptsIncluded.length, origLength + 1);
+                    equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+                    // check that <div class="ajaxHtmlHead"> has been removed
+                    equals(jQuery('#main').children().length, 1);
+                    equals(jQuery('div.ajaxHtmlHead').length, 0);
+                    equals(jQuery('#main h1').html(), 'Hello');
+                    // qunit.css is not added twice
+                    equals(jQuery('head link').length, 1);
+                    /* use endswith because in pytest context we have an absolute path */
+                    ok(jQuery('head link').attr('href').endswith('/qunit.css'));
+                } finally {
+                    start();
+                }
+            }
+        });
+    });
+
+    test('test synchronous request loadRemote', function() {
+        var res = loadRemote('/../ajaxresult.json', {},
+        'GET', true);
+        same(res, ['foo', 'bar']);
+    });
+
+    test('test event on CubicWeb', function() {
+        expect(1);
+        stop();
+        var events = null;
+        jQuery(CubicWeb).bind('server-response', function() {
+            // check that server-response event on CubicWeb is triggered
+            events = 'CubicWeb';
+        });
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(events, 'CubicWeb');
+                start();
+            }
+        });
+    });
+
+    test('test event on node', function() {
+        expect(3);
+        stop();
+        var nodes = [];
+        jQuery('#main').bind('server-response', function() {
+            nodes.push('node');
+        });
+        jQuery(CubicWeb).bind('server-response', function() {
+            nodes.push('CubicWeb');
+        });
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(nodes.length, 2);
+                // check that server-response event on CubicWeb is triggered
+                // only once and event server-response on node is triggered
+                equals(nodes[0], 'CubicWeb');
+                equals(nodes[1], 'node');
+                start();
+            }
+        });
+    });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_htmlhelpers.html	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,21 @@
+<html>
+  <head>
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <script type="text/javascript" src="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_htmlhelpers.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">cubicweb.htmlhelpers.js functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_htmlhelpers.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,22 @@
+<html>
+  <head>
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/jquery.corner.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="utils.js" type="text/javascript"></script>
+    <script type="text/javascript" src="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_utils.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">cw.utils functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_utils.js	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/test_views.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
@@ -68,7 +66,7 @@
         self.vreg.register(SomeView)
         rset = self.execute('CWUser X')
         source = self.view('someview', rset).source
-        self.assertEquals(source.count('spam.js'), 1)
+        self.assertEqual(source.count('spam.js'), 1)
 
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/test_windmill.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,6 @@
+# Run all scenarii found in windmill directory
+from cubicweb.devtools.cwwindmill import (CubicWebWindmillUseCase,
+                                          unittest_main)
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/test/unittest_application.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_application.py	Wed Nov 03 16:38:28 2010 +0100
@@ -69,32 +69,32 @@
     def test_list_arg(self):
         """tests the list_arg() function"""
         list_arg = self._cw.list_form_param
-        self.assertEquals(list_arg('arg3', {}), [])
+        self.assertEqual(list_arg('arg3', {}), [])
         d = {'arg1' : "value1",
              'arg2' : ('foo', INTERNAL_FIELD_VALUE,),
              'arg3' : ['bar']}
-        self.assertEquals(list_arg('arg1', d, True), ['value1'])
-        self.assertEquals(d, {'arg2' : ('foo', INTERNAL_FIELD_VALUE), 'arg3' : ['bar'],})
-        self.assertEquals(list_arg('arg2', d, True), ['foo'])
-        self.assertEquals({'arg3' : ['bar'],}, d)
-        self.assertEquals(list_arg('arg3', d), ['bar',])
-        self.assertEquals({'arg3' : ['bar'],}, d)
+        self.assertEqual(list_arg('arg1', d, True), ['value1'])
+        self.assertEqual(d, {'arg2' : ('foo', INTERNAL_FIELD_VALUE), 'arg3' : ['bar'],})
+        self.assertEqual(list_arg('arg2', d, True), ['foo'])
+        self.assertEqual({'arg3' : ['bar'],}, d)
+        self.assertEqual(list_arg('arg3', d), ['bar',])
+        self.assertEqual({'arg3' : ['bar'],}, d)
 
 
     def test_from_controller(self):
         self._cw.vreg['controllers'] = {'view': 1, 'login': 1}
-        self.assertEquals(self._cw.from_controller(), 'view')
+        self.assertEqual(self._cw.from_controller(), 'view')
         req = FakeRequest(url='project?vid=list')
         req.vreg['controllers'] = {'view': 1, 'login': 1}
         # this assertion is just to make sure that relative_path can be
         # correctly computed as it is used in from_controller()
-        self.assertEquals(req.relative_path(False), 'project')
-        self.assertEquals(req.from_controller(), 'view')
+        self.assertEqual(req.relative_path(False), 'project')
+        self.assertEqual(req.from_controller(), 'view')
         # test on a valid non-view controller
         req = FakeRequest(url='login?x=1&y=2')
         req.vreg['controllers'] = {'view': 1, 'login': 1}
-        self.assertEquals(req.relative_path(False), 'login')
-        self.assertEquals(req.from_controller(), 'login')
+        self.assertEqual(req.relative_path(False), 'login')
+        self.assertEqual(req.from_controller(), 'login')
 
 
 class UtilsTC(TestCase):
@@ -107,22 +107,22 @@
     #    """tests which mapping is used (application or core)"""
     #    init_mapping()
     #    from cubicweb.common import mapping
-    #    self.assertEquals(mapping.MAPPING_USED, 'core')
+    #    self.assertEqual(mapping.MAPPING_USED, 'core')
     #    sys.modules['mapping'] = FakeMapping()
     #    init_mapping()
-    #    self.assertEquals(mapping.MAPPING_USED, 'application')
+    #    self.assertEqual(mapping.MAPPING_USED, 'application')
     #    del sys.modules['mapping']
 
     def test_execute_linkto(self):
         """tests the execute_linkto() function"""
-        self.assertEquals(self.ctrl.execute_linkto(), None)
-        self.assertEquals(self.ctrl._cursor.executed,
+        self.assertEqual(self.ctrl.execute_linkto(), None)
+        self.assertEqual(self.ctrl._cursor.executed,
                           [])
 
         self.ctrl.set_form({'__linkto' : 'works_for:12_13_14:object',
                               'eid': 8})
         self.ctrl.execute_linkto()
-        self.assertEquals(self.ctrl._cursor.executed,
+        self.assertEqual(self.ctrl._cursor.executed,
                           ['SET Y works_for X WHERE X eid 8, Y eid %s' % i
                            for i in (12, 13, 14)])
 
@@ -130,7 +130,7 @@
         self.ctrl.set_form({'__linkto' : 'works_for:12_13_14:subject',
                               'eid': 8})
         self.ctrl.execute_linkto()
-        self.assertEquals(self.ctrl._cursor.executed,
+        self.assertEqual(self.ctrl._cursor.executed,
                           ['SET X works_for Y WHERE X eid 8, Y eid %s' % i
                            for i in (12, 13, 14)])
 
@@ -138,14 +138,14 @@
         self.ctrl.new_cursor()
         self.ctrl._cw.form = {'__linkto' : 'works_for:12_13_14:object'}
         self.ctrl.execute_linkto(eid=8)
-        self.assertEquals(self.ctrl._cursor.executed,
+        self.assertEqual(self.ctrl._cursor.executed,
                           ['SET Y works_for X WHERE X eid 8, Y eid %s' % i
                            for i in (12, 13, 14)])
 
         self.ctrl.new_cursor()
         self.ctrl.set_form({'__linkto' : 'works_for:12_13_14:subject'})
         self.ctrl.execute_linkto(eid=8)
-        self.assertEquals(self.ctrl._cursor.executed,
+        self.assertEqual(self.ctrl._cursor.executed,
                           ['SET X works_for Y WHERE X eid 8, Y eid %s' % i
                            for i in (12, 13, 14)])
 
@@ -159,13 +159,13 @@
 
     def test_cnx_user_groups_sync(self):
         user = self.user()
-        self.assertEquals(user.groups, set(('managers',)))
+        self.assertEqual(user.groups, set(('managers',)))
         self.execute('SET X in_group G WHERE X eid %s, G name "guests"' % user.eid)
         user = self.user()
-        self.assertEquals(user.groups, set(('managers',)))
+        self.assertEqual(user.groups, set(('managers',)))
         self.commit()
         user = self.user()
-        self.assertEquals(user.groups, set(('managers', 'guests')))
+        self.assertEqual(user.groups, set(('managers', 'guests')))
         # cleanup
         self.execute('DELETE X in_group G WHERE X eid %s, G name "guests"' % user.eid)
         self.commit()
@@ -193,13 +193,13 @@
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
         forminfo = req.session.data['view?vid=edition...']
         eidmap = forminfo['eidmap']
-        self.assertEquals(eidmap, {})
+        self.assertEqual(eidmap, {})
         values = forminfo['values']
-        self.assertEquals(values['login-subject:'+eid], '')
-        self.assertEquals(values['eid'], eid)
+        self.assertEqual(values['login-subject:'+eid], '')
+        self.assertEqual(values['eid'], eid)
         error = forminfo['error']
-        self.assertEquals(error.entity, user.eid)
-        self.assertEquals(error.errors['login-subject'], 'required field')
+        self.assertEqual(error.entity, user.eid)
+        self.assertEqual(error.errors['login-subject'], 'required field')
 
 
     def test_validation_error_dont_loose_subentity_data_ctrl(self):
@@ -222,13 +222,13 @@
                     }
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
         forminfo = req.session.data['view?vid=edition...']
-        self.assertEquals(set(forminfo['eidmap']), set('XY'))
-        self.assertEquals(forminfo['eidmap']['X'], None)
+        self.assertEqual(set(forminfo['eidmap']), set('XY'))
+        self.assertEqual(forminfo['eidmap']['X'], None)
         self.assertIsInstance(forminfo['eidmap']['Y'], int)
-        self.assertEquals(forminfo['error'].entity, 'X')
-        self.assertEquals(forminfo['error'].errors,
+        self.assertEqual(forminfo['error'].entity, 'X')
+        self.assertEqual(forminfo['error'].errors,
                           {'login-subject': 'required field'})
-        self.assertEquals(forminfo['values'], req.form)
+        self.assertEqual(forminfo['values'], req.form)
 
 
     def test_validation_error_dont_loose_subentity_data_repo(self):
@@ -251,13 +251,13 @@
                     }
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
         forminfo = req.session.data['view?vid=edition...']
-        self.assertEquals(set(forminfo['eidmap']), set('XY'))
+        self.assertEqual(set(forminfo['eidmap']), set('XY'))
         self.assertIsInstance(forminfo['eidmap']['X'], int)
         self.assertIsInstance(forminfo['eidmap']['Y'], int)
-        self.assertEquals(forminfo['error'].entity, forminfo['eidmap']['X'])
-        self.assertEquals(forminfo['error'].errors,
+        self.assertEqual(forminfo['error'].entity, forminfo['eidmap']['X'])
+        self.assertEqual(forminfo['error'].errors,
                           {'login-subject': u'the value "admin" is already used, use another one'})
-        self.assertEquals(forminfo['values'], req.form)
+        self.assertEqual(forminfo['values'], req.form)
 
 
     def _test_cleaned(self, kwargs, injected, cleaned):
@@ -282,24 +282,24 @@
         # protocol
         vreg = self.app.vreg
         # default value
-        self.assertEquals(vreg.property_value('ui.language'), 'en')
+        self.assertEqual(vreg.property_value('ui.language'), 'en')
         self.execute('INSERT CWProperty X: X value "fr", X pkey "ui.language"')
-        self.assertEquals(vreg.property_value('ui.language'), 'en')
+        self.assertEqual(vreg.property_value('ui.language'), 'en')
         self.commit()
-        self.assertEquals(vreg.property_value('ui.language'), 'fr')
+        self.assertEqual(vreg.property_value('ui.language'), 'fr')
         self.execute('SET X value "de" WHERE X pkey "ui.language"')
-        self.assertEquals(vreg.property_value('ui.language'), 'fr')
+        self.assertEqual(vreg.property_value('ui.language'), 'fr')
         self.commit()
-        self.assertEquals(vreg.property_value('ui.language'), 'de')
+        self.assertEqual(vreg.property_value('ui.language'), 'de')
         self.execute('DELETE CWProperty X WHERE X pkey "ui.language"')
-        self.assertEquals(vreg.property_value('ui.language'), 'de')
+        self.assertEqual(vreg.property_value('ui.language'), 'de')
         self.commit()
-        self.assertEquals(vreg.property_value('ui.language'), 'en')
+        self.assertEqual(vreg.property_value('ui.language'), 'en')
 
     def test_login_not_available_to_authenticated(self):
         req = self.request()
         ex = self.assertRaises(Unauthorized, self.app_publish, req, 'login')
-        self.assertEquals(str(ex), 'log out first')
+        self.assertEqual(str(ex), 'log out first')
 
     def test_fb_login_concept(self):
         """see data/views.py"""
@@ -311,7 +311,7 @@
         req.form['__fblogin'] = u'turlututu'
         page = self.app_publish(req)
         self.failIf(req.cnx is origcnx)
-        self.assertEquals(req.user.login, 'turlututu')
+        self.assertEqual(req.user.login, 'turlututu')
         self.failUnless('turlututu' in page, page)
 
     # authentication tests ####################################################
@@ -320,13 +320,13 @@
         req, origsession = self.init_authentication('http')
         self.assertAuthFailure(req)
         self.assertRaises(AuthenticationError, self.app_publish, req, 'login')
-        self.assertEquals(req.cnx, None)
+        self.assertEqual(req.cnx, None)
         authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password']))
         req._headers['Authorization'] = 'basic %s' % authstr
         self.assertAuthSuccess(req, origsession)
-        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
         self.assertRaises(LogOut, self.app_publish, req, 'logout')
-        self.assertEquals(len(self.open_sessions), 0)
+        self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_no_anon(self):
         req, origsession = self.init_authentication('cookie')
@@ -334,13 +334,13 @@
         form = self.app_publish(req, 'login')
         self.failUnless('__login' in form)
         self.failUnless('__password' in form)
-        self.assertEquals(req.cnx, None)
+        self.assertEqual(req.cnx, None)
         req.form['__login'] = origsession.login
         req.form['__password'] = origsession.authinfo['password']
         self.assertAuthSuccess(req, origsession)
-        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
         self.assertRaises(LogOut, self.app_publish, req, 'logout')
-        self.assertEquals(len(self.open_sessions), 0)
+        self.assertEqual(len(self.open_sessions), 0)
 
     def test_login_by_email(self):
         login = self.request().user.login
@@ -359,9 +359,9 @@
         req.form['__login'] = address
         req.form['__password'] = origsession.authinfo['password']
         self.assertAuthSuccess(req, origsession)
-        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
         self.assertRaises(LogOut, self.app_publish, req, 'logout')
-        self.assertEquals(len(self.open_sessions), 0)
+        self.assertEqual(len(self.open_sessions), 0)
 
     def _reset_cookie(self, req):
         # preparing the suite of the test
@@ -376,18 +376,18 @@
     def _test_auth_anon(self, req):
         self.app.connect(req)
         asession = req.session
-        self.assertEquals(len(self.open_sessions), 1)
-        self.assertEquals(asession.login, 'anon')
-        self.assertEquals(asession.authinfo['password'], 'anon')
+        self.assertEqual(len(self.open_sessions), 1)
+        self.assertEqual(asession.login, 'anon')
+        self.assertEqual(asession.authinfo['password'], 'anon')
         self.failUnless(asession.anonymous_session)
         self._reset_cookie(req)
 
     def _test_anon_auth_fail(self, req):
-        self.assertEquals(len(self.open_sessions), 1)
+        self.assertEqual(len(self.open_sessions), 1)
         self.app.connect(req)
-        self.assertEquals(req.message, 'authentication failure')
-        self.assertEquals(req.session.anonymous_session, True)
-        self.assertEquals(len(self.open_sessions), 1)
+        self.assertEqual(req.message, 'authentication failure')
+        self.assertEqual(req.session.anonymous_session, True)
+        self.assertEqual(len(self.open_sessions), 1)
         self._reset_cookie(req)
 
     def test_http_auth_anon_allowed(self):
@@ -399,9 +399,9 @@
         authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password']))
         req._headers['Authorization'] = 'basic %s' % authstr
         self.assertAuthSuccess(req, origsession)
-        self.assertEquals(req.session.authinfo, {'password': origsession.authinfo['password']})
+        self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
         self.assertRaises(LogOut, self.app_publish, req, 'logout')
-        self.assertEquals(len(self.open_sessions), 0)
+        self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_anon_allowed(self):
         req, origsession = self.init_authentication('cookie', 'anon')
@@ -412,10 +412,10 @@
         req.form['__login'] = origsession.login
         req.form['__password'] = origsession.authinfo['password']
         self.assertAuthSuccess(req, origsession)
-        self.assertEquals(req.session.authinfo,
+        self.assertEqual(req.session.authinfo,
                           {'password': origsession.authinfo['password']})
         self.assertRaises(LogOut, self.app_publish, req, 'logout')
-        self.assertEquals(len(self.open_sessions), 0)
+        self.assertEqual(len(self.open_sessions), 0)
 
     def test_non_regr_optional_first_var(self):
         req = self.request()
--- a/web/test/unittest_breadcrumbs.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_breadcrumbs.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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):
@@ -26,11 +28,11 @@
         self.execute('SET F2 filed_under F1 WHERE F1 eid %(f1)s, F2 eid %(f2)s',
                      {'f1' : f1.eid, 'f2' : f2.eid})
         self.commit()
-        self.assertEquals(f2.view('breadcrumbs'),
+        self.assertEqual(f2.view('breadcrumbs'),
                           '<a href="http://testing.fr/cubicweb/folder/%s" title="">chi&amp;ld</a>' % f2.eid)
         childrset = f2.as_rset()
         ibc = self.vreg['components'].select('breadcrumbs', self.request(), rset=childrset)
-        self.assertEquals(ibc.render(),
+        self.assertEqual(ibc.render(),
                           """<span id="breadcrumbs" class="pathbar">&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/Folder">folder_plural</a>&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/folder/%s" title="">par&amp;ent</a>&#160;&gt;&#160;
 <a href="http://testing.fr/cubicweb/folder/%s" title="">chi&amp;ld</a></span>""" % (f1.eid, f2.eid))
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_facet.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,126 @@
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web import facet
+
+class BaseFacetTC(CubicWebTC):
+
+    def prepare_rqlst(self):
+        req = self.request()
+        rset = self.execute('CWUser X')
+        rqlst = rset.syntax_tree().copy()
+        req.vreg.rqlhelper.annotate(rqlst)
+        mainvar, baserql = facet.prepare_facets_rqlst(rqlst, rset.args)
+        self.assertEqual(mainvar.name, 'X')
+        self.assertEqual(baserql, 'Any X WHERE X is CWUser')
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
+        return req, rset, rqlst, mainvar
+
+    def test_relation_simple(self):
+        req, rset, rqlst, mainvar = self.prepare_rqlst()
+        f = facet.RelationFacet(req, rset=rset,
+                                rqlst=rqlst.children[0],
+                                filtered_variable=mainvar)
+        f.rtype = 'in_group'
+        f.role = 'subject'
+        f.target_attr = 'name'
+        guests, managers = [eid for eid, in self.execute('CWGroup G ORDERBY GN '
+                                                         'WHERE G name GN, G name IN ("guests", "managers")')]
+        self.assertEqual(f.vocabulary(),
+                          [(u'guests', guests), (u'managers', managers)])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
+        #rqlst = rset.syntax_tree()
+        self.assertEqual(f.possible_values(),
+                          [str(guests), str(managers)])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
+        req.form[f.__regid__] = str(guests)
+        f.add_rql_restrictions()
+        # selection is cluttered because rqlst has been prepared for facet (it
+        # is not in real life)
+        self.assertEqual(f.rqlst.as_string(),
+                          'DISTINCT Any  WHERE X is CWUser, X in_group D, D eid %s' % guests)
+
+    def test_relation_optional_rel(self):
+        req = self.request()
+        rset = self.execute('Any X,GROUP_CONCAT(GN) GROUPBY X '
+                            'WHERE X in_group G?, G name GN, NOT G name "users"')
+        rqlst = rset.syntax_tree().copy()
+        req.vreg.rqlhelper.annotate(rqlst)
+        mainvar, baserql = facet.prepare_facets_rqlst(rqlst, rset.args)
+
+        f = facet.RelationFacet(req, rset=rset,
+                                rqlst=rqlst.children[0],
+                                filtered_variable=mainvar)
+        f.rtype = 'in_group'
+        f.role = 'subject'
+        f.target_attr = 'name'
+        guests, managers = [eid for eid, in self.execute('CWGroup G ORDERBY GN '
+                                                         'WHERE G name GN, G name IN ("guests", "managers")')]
+        self.assertEqual(f.vocabulary(),
+                          [(u'guests', guests), (u'managers', managers)])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  GROUPBY X WHERE X in_group G?, G name GN, NOT G name "users"')
+        #rqlst = rset.syntax_tree()
+        self.assertEqual(sorted(f.possible_values()),
+                          [str(guests), str(managers)])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  GROUPBY X WHERE X in_group G?, G name GN, NOT G name "users"')
+        req.form[f.__regid__] = str(guests)
+        f.add_rql_restrictions()
+        # selection is cluttered because rqlst has been prepared for facet (it
+        # is not in real life)
+        self.assertEqual(f.rqlst.as_string(),
+                          'DISTINCT Any  GROUPBY X WHERE X in_group G?, G name GN, NOT G name "users", X in_group D, D eid %s' % guests)
+
+
+    def test_relationattribute(self):
+        req, rset, rqlst, mainvar = self.prepare_rqlst()
+        f = facet.RelationAttributeFacet(req, rset=rset,
+                                         rqlst=rqlst.children[0],
+                                         filtered_variable=mainvar)
+        f.rtype = 'in_group'
+        f.role = 'subject'
+        f.target_attr = 'name'
+        self.assertEqual(f.vocabulary(),
+                          [(u'guests', u'guests'), (u'managers', u'managers')])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
+        #rqlst = rset.syntax_tree()
+        self.assertEqual(f.possible_values(),
+                          ['guests', 'managers'])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
+        req.form[f.__regid__] = 'guests'
+        f.add_rql_restrictions()
+        # selection is cluttered because rqlst has been prepared for facet (it
+        # is not in real life)
+        self.assertEqual(f.rqlst.as_string(),
+                          "DISTINCT Any  WHERE X is CWUser, X in_group E, E name 'guests'")
+
+
+    def test_attribute(self):
+        req, rset, rqlst, mainvar = self.prepare_rqlst()
+        f = facet.AttributeFacet(req, rset=rset,
+                                 rqlst=rqlst.children[0],
+                                 filtered_variable=mainvar)
+        f.rtype = 'login'
+        self.assertEqual(f.vocabulary(),
+                          [(u'admin', u'admin'), (u'anon', u'anon')])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
+        #rqlst = rset.syntax_tree()
+        self.assertEqual(f.possible_values(),
+                          ['admin', 'anon'])
+        # ensure rqlst is left unmodified
+        self.assertEqual(rqlst.as_string(), 'DISTINCT Any  WHERE X is CWUser')
+        req.form[f.__regid__] = 'admin'
+        f.add_rql_restrictions()
+        # selection is cluttered because rqlst has been prepared for facet (it
+        # is not in real life)
+        self.assertEqual(f.rqlst.as_string(),
+                          "DISTINCT Any  WHERE X is CWUser, X login 'admin'")
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/web/test/unittest_form.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_form.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
 
@@ -39,10 +36,10 @@
 
     def test_form_field_format(self):
         form = FieldsForm(self.request(), None)
-        self.assertEquals(StringField().format(form), 'text/html')
+        self.assertEqual(StringField().format(form), 'text/html')
         self.execute('INSERT CWProperty X: X pkey "ui.default-text-format", X value "text/rest", X for_user U WHERE U login "admin"')
         self.commit()
-        self.assertEquals(StringField().format(form), 'text/rest')
+        self.assertEqual(StringField().format(form), 'text/rest')
 
 
 class EntityFieldsFormTC(CubicWebTC):
@@ -57,15 +54,15 @@
         t = self.req.create_entity('Tag', name=u'x')
         form1 = self.vreg['forms'].select('edition', self.req, entity=t)
         unrelated = [reid for rview, reid in form1.field_by_name('tags', 'subject', t.e_schema).choices(form1)]
-        self.failUnless(b.eid in unrelated, unrelated)
+        self.failUnless(unicode(b.eid) in unrelated, unrelated)
         form2 = self.vreg['forms'].select('edition', self.req, entity=b)
         unrelated = [reid for rview, reid in form2.field_by_name('tags', 'object', t.e_schema).choices(form2)]
-        self.failUnless(t.eid in unrelated, unrelated)
+        self.failUnless(unicode(t.eid) in unrelated, unrelated)
         self.execute('SET X tags Y WHERE X is Tag, Y is BlogEntry')
         unrelated = [reid for rview, reid in form1.field_by_name('tags', 'subject', t.e_schema).choices(form1)]
-        self.failIf(b.eid in unrelated, unrelated)
+        self.failIf(unicode(b.eid) in unrelated, unrelated)
         unrelated = [reid for rview, reid in form2.field_by_name('tags', 'object', t.e_schema).choices(form2)]
-        self.failIf(t.eid in unrelated, unrelated)
+        self.failIf(unicode(t.eid) in unrelated, unrelated)
 
 
     def test_form_field_vocabulary_new_entity(self):
@@ -73,7 +70,7 @@
         form = self.vreg['forms'].select('edition', self.req, entity=e)
         unrelated = [rview for rview, reid in form.field_by_name('in_group', 'subject').choices(form)]
         # should be default groups but owners, i.e. managers, users, guests
-        self.assertEquals(unrelated, [u'guests', u'managers', u'users'])
+        self.assertEqual(unrelated, [u'guests', u'managers', u'users'])
 
     def test_consider_req_form_params(self):
         e = self.vreg['etypes'].etype_class('CWUser')(self.request())
@@ -82,7 +79,7 @@
         field = StringField(name='login', role='subject', eidparam=True)
         form.append_field(field)
         form.build_context({})
-        self.assertEquals(field.widget.values(form, field), (u'toto',))
+        self.assertEqual(field.widget.values(form, field), (u'toto',))
 
 
     def test_linkto_field_duplication(self):
@@ -103,7 +100,7 @@
         rset = self.execute('INSERT BlogEntry X: X title "cubicweb.org", X content "hop"')
         form = self.vreg['views'].select('doreledit', self.request(),
                                          rset=rset, row=0, rtype='content')
-        data = form.render(row=0, rtype='content')
+        data = form.render(row=0, rtype='content', formid='base')
         self.failUnless('content_format' in data)
 
     # form view tests #########################################################
@@ -147,7 +144,7 @@
         form = RTFForm(self.req, redirect_path='perdu.com', entity=state)
         # make it think it can use fck editor anyway
         form.field_by_name('description', 'subject').format = lambda x: 'text/html'
-        self.assertTextEquals(self._render_entity_field('description', form),
+        self.assertMultiLineEqual(self._render_entity_field('description', form),
                               expected % {'eid': state.eid})
 
 
@@ -177,7 +174,7 @@
         file = self.req.create_entity('File', data_name=u"pouet.txt", data_encoding=u'UTF-8',
                                data=Binary('new widgets system'))
         form = FFForm(self.req, redirect_path='perdu.com', entity=file)
-        self.assertTextEquals(self._render_entity_field('data', form),
+        self.assertMultiLineEqual(self._render_entity_field('data', form),
                               '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
 <a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
 <div id="data-subject:%(eid)s-advanced" class="hidden">
@@ -186,8 +183,7 @@
 </div>
 <br/>
 <input name="data-subject__detach:%(eid)s" type="checkbox" />
-detach attached file
-''' % {'eid': file.eid})
+detach attached file''' % {'eid': file.eid})
 
 
     def test_editablefilefield(self):
@@ -201,7 +197,7 @@
         file = self.req.create_entity('File', data_name=u"pouet.txt", data_encoding=u'UTF-8',
                                data=Binary('new widgets system'))
         form = EFFForm(self.req, redirect_path='perdu.com', entity=file)
-        self.assertTextEquals(self._render_entity_field('data', form),
+        self.assertMultiLineEqual(self._render_entity_field('data', form),
                               '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
 <a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
 <div id="data-subject:%(eid)s-advanced" class="hidden">
@@ -219,7 +215,7 @@
         class PFForm(EntityFieldsForm):
             upassword = PasswordField(eidparam=True, role='subject')
         form = PFForm(self.req, redirect_path='perdu.com', entity=self.entity)
-        self.assertTextEquals(self._render_entity_field('upassword', form),
+        self.assertMultiLineEqual(self._render_entity_field('upassword', form),
                               '''<input id="upassword-subject:%(eid)s" name="upassword-subject:%(eid)s" tabindex="1" type="password" value="" />
 <br/>
 <input name="upassword-subject-confirm:%(eid)s" tabindex="1" type="password" value="" />
@@ -233,7 +229,7 @@
     #     form = DFForm(self.req, entity=self.entity)
     #     init, cur = (fromstring(self._render_entity_field(attr, form)).get('value')
     #                  for attr in ('edits-creation_date', 'creation_date'))
-    #     self.assertEquals(init, cur)
+    #     self.assertEqual(init, cur)
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_formfields.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_formfields.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
 
@@ -40,64 +38,64 @@
     def test_state_fields(self):
         title_field = guess_field(schema['State'], schema['name'])
         self.assertIsInstance(title_field, StringField)
-        self.assertEquals(title_field.required, True)
+        self.assertEqual(title_field.required, True)
 
 #         synopsis_field = guess_field(schema['State'], schema['synopsis'])
 #         self.assertIsInstance(synopsis_field, StringField)
 #         self.assertIsInstance(synopsis_field.widget, TextArea)
-#         self.assertEquals(synopsis_field.required, False)
-#         self.assertEquals(synopsis_field.help, 'an abstract for this state')
+#         self.assertEqual(synopsis_field.required, False)
+#         self.assertEqual(synopsis_field.help, 'an abstract for this state')
 
         description_field = guess_field(schema['State'], schema['description'])
         self.assertIsInstance(description_field, RichTextField)
-        self.assertEquals(description_field.required, False)
-        self.assertEquals(description_field.format_field, None)
+        self.assertEqual(description_field.required, False)
+        self.assertEqual(description_field.format_field, None)
+
+        # description_format_field = guess_field(schema['State'], schema['description_format'])
+        # self.assertEqual(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)
+        self.assertEqual(description_format_field.internationalizable, True)
+        self.assertEqual(description_format_field.sort, True)
 
 #         wikiid_field = guess_field(schema['State'], schema['wikiid'])
 #         self.assertIsInstance(wikiid_field, StringField)
-#         self.assertEquals(wikiid_field.required, False)
+#         self.assertEqual(wikiid_field.required, False)
 
 
     def test_cwuser_fields(self):
         upassword_field = guess_field(schema['CWUser'], schema['upassword'])
         self.assertIsInstance(upassword_field, StringField)
         self.assertIsInstance(upassword_field.widget, PasswordInput)
-        self.assertEquals(upassword_field.required, True)
+        self.assertEqual(upassword_field.required, True)
 
         last_login_time_field = guess_field(schema['CWUser'], schema['last_login_time'])
         self.assertIsInstance(last_login_time_field, DateTimeField)
-        self.assertEquals(last_login_time_field.required, False)
+        self.assertEqual(last_login_time_field.required, False)
 
         in_group_field = guess_field(schema['CWUser'], schema['in_group'])
         self.assertIsInstance(in_group_field, RelationField)
-        self.assertEquals(in_group_field.required, True)
-        self.assertEquals(in_group_field.role, 'subject')
-        self.assertEquals(in_group_field.help, 'groups grant permissions to the user')
+        self.assertEqual(in_group_field.required, True)
+        self.assertEqual(in_group_field.role, 'subject')
+        self.assertEqual(in_group_field.help, 'groups grant permissions to the user')
 
         owned_by_field = guess_field(schema['CWUser'], schema['owned_by'], 'object')
         self.assertIsInstance(owned_by_field, RelationField)
-        self.assertEquals(owned_by_field.required, False)
-        self.assertEquals(owned_by_field.role, 'object')
+        self.assertEqual(owned_by_field.required, False)
+        self.assertEqual(owned_by_field.role, 'object')
 
 
     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.assertEqual(data_format_field, None)
+        # data_encoding_field = guess_field(schema['File'], schema['data_encoding'])
+        # self.assertEqual(data_encoding_field, None)
+        # data_name_field = guess_field(schema['File'], schema['data_name'])
+        # self.assertEqual(data_name_field, None)
 
         data_field = guess_field(schema['File'], schema['data'])
         self.assertIsInstance(data_field, FileField)
-        self.assertEquals(data_field.required, True)
+        self.assertEqual(data_field.required, True)
         self.assertIsInstance(data_field.format_field, StringField)
         self.assertIsInstance(data_field.encoding_field, StringField)
         self.assertIsInstance(data_field.name_field, StringField)
@@ -105,7 +103,7 @@
     def test_constraints_priority(self):
         salesterm_field = guess_field(schema['Salesterm'], schema['reason'])
         constraints = schema['reason'].rdef('Salesterm', 'String').constraints
-        self.assertEquals([c.__class__ for c in constraints],
+        self.assertEqual([c.__class__ for c in constraints],
                           [SizeConstraint, StaticVocabularyConstraint])
         self.assertIsInstance(salesterm_field, StringField)
         self.assertIsInstance(salesterm_field.widget, Select)
@@ -114,16 +112,16 @@
     def test_bool_field_base(self):
         field = guess_field(schema['CWAttribute'], schema['indexed'])
         self.assertIsInstance(field, BooleanField)
-        self.assertEquals(field.required, False)
+        self.assertEqual(field.required, False)
         self.assertIsInstance(field.widget, Radio)
-        self.assertEquals(field.vocabulary(mock(_cw=mock(_=unicode))),
+        self.assertEqual(field.vocabulary(mock(_cw=mock(_=unicode))),
                           [(u'yes', '1'), (u'no', '')])
 
     def test_bool_field_explicit_choices(self):
         field = guess_field(schema['CWAttribute'], schema['indexed'],
                             choices=[(u'maybe', '1'), (u'no', '')])
         self.assertIsInstance(field.widget, Radio)
-        self.assertEquals(field.vocabulary(mock(req=mock(_=unicode))),
+        self.assertEqual(field.vocabulary(mock(req=mock(_=unicode))),
                           [(u'maybe', '1'), (u'no', '')])
 
 
@@ -135,18 +133,18 @@
         form = EntityFieldsForm(req, entity=e)
         description_field = guess_field(schema['State'], schema['description'])
         description_format_field = description_field.get_format_field(form)
-        self.assertEquals(description_format_field.internationalizable, True)
-        self.assertEquals(description_format_field.sort, True)
+        self.assertEqual(description_format_field.internationalizable, True)
+        self.assertEqual(description_format_field.sort, True)
         # unlike below, initial is bound to form.form_field_format
-        self.assertEquals(description_format_field.value(form), 'text/html')
+        self.assertEqual(description_format_field.value(form), 'text/html')
         self.execute('INSERT CWProperty X: X pkey "ui.default-text-format", X value "text/rest", X for_user U WHERE U login "admin"')
         self.commit()
-        self.assertEquals(description_format_field.value(form), 'text/rest')
+        self.assertEqual(description_format_field.value(form), 'text/rest')
 
 
 class UtilsTC(TestCase):
     def test_vocab_sort(self):
-        self.assertEquals(vocab_sort([('Z', 1), ('A', 2),
+        self.assertEqual(vocab_sort([('Z', 1), ('A', 2),
                                       ('Group 1', None), ('Y', 3), ('B', 4),
                                       ('Group 2', None), ('X', 5), ('C', 6)]),
                           [('A', 2), ('Z', 1),
--- a/web/test/unittest_magicsearch.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_magicsearch.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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
 
@@ -63,16 +61,16 @@
         """tests basic translations (no ambiguities)"""
         rql = "Any C WHERE C is Adresse, P adel C, C adresse 'Logilab'"
         rql, = self.proc.preprocess_query(rql)
-        self.assertEquals(rql, "Any C WHERE C is EmailAddress, P use_email C, C address 'Logilab'")
+        self.assertEqual(rql, "Any C WHERE C is EmailAddress, P use_email C, C address 'Logilab'")
 
     def test_ambiguous_translations(self):
         """tests possibly ambiguous translations"""
         rql = "Any P WHERE P adel C, C is EmailAddress, C nom 'Logilab'"
         rql, = self.proc.preprocess_query(rql)
-        self.assertEquals(rql, "Any P WHERE P use_email C, C is EmailAddress, C alias 'Logilab'")
+        self.assertEqual(rql, "Any P WHERE P use_email C, C is EmailAddress, C alias 'Logilab'")
         rql = "Any P WHERE P is Utilisateur, P adel C, P nom 'Smith'"
         rql, = self.proc.preprocess_query(rql)
-        self.assertEquals(rql, "Any P WHERE P is CWUser, P use_email C, P surname 'Smith'")
+        self.assertEqual(rql, "Any P WHERE P is CWUser, P use_email C, P surname 'Smith'")
 
 
 class QSPreProcessorTC(CubicWebTC):
@@ -88,21 +86,21 @@
     def test_entity_translation(self):
         """tests QSPreProcessor._get_entity_name()"""
         translate = self.proc._get_entity_type
-        self.assertEquals(translate(u'EmailAddress'), "EmailAddress")
-        self.assertEquals(translate(u'emailaddress'), "EmailAddress")
-        self.assertEquals(translate(u'Adresse'), "EmailAddress")
-        self.assertEquals(translate(u'adresse'), "EmailAddress")
+        self.assertEqual(translate(u'EmailAddress'), "EmailAddress")
+        self.assertEqual(translate(u'emailaddress'), "EmailAddress")
+        self.assertEqual(translate(u'Adresse'), "EmailAddress")
+        self.assertEqual(translate(u'adresse'), "EmailAddress")
         self.assertRaises(BadRQLQuery, translate, 'whatever')
 
     def test_attribute_translation(self):
         """tests QSPreProcessor._get_attribute_name"""
         translate = self.proc._get_attribute_name
         eschema = self.schema.eschema('CWUser')
-        self.assertEquals(translate(u'prénom', eschema), "firstname")
-        self.assertEquals(translate(u'nom', eschema), 'surname')
+        self.assertEqual(translate(u'prénom', eschema), "firstname")
+        self.assertEqual(translate(u'nom', eschema), 'surname')
         eschema = self.schema.eschema('EmailAddress')
-        self.assertEquals(translate(u'adresse', eschema), "address")
-        self.assertEquals(translate(u'nom', eschema), 'alias')
+        self.assertEqual(translate(u'adresse', eschema), "address")
+        self.assertEqual(translate(u'nom', eschema), 'alias')
         # should fail if the name is not an attribute for the given entity schema
         self.assertRaises(BadRQLQuery, translate, 'whatever', eschema)
         self.assertRaises(BadRQLQuery, translate, 'prénom', eschema)
@@ -110,64 +108,64 @@
     def test_one_word_query(self):
         """tests the 'one word shortcut queries'"""
         transform = self.proc._one_word_query
-        self.assertEquals(transform('123'),
+        self.assertEqual(transform('123'),
                           ('Any X WHERE X eid %(x)s', {'x': 123}, 'x'))
-        self.assertEquals(transform('CWUser'),
+        self.assertEqual(transform('CWUser'),
                           ('CWUser C',))
-        self.assertEquals(transform('Utilisateur'),
+        self.assertEqual(transform('Utilisateur'),
                           ('CWUser C',))
-        self.assertEquals(transform('Adresse'),
+        self.assertEqual(transform('Adresse'),
                           ('EmailAddress E',))
-        self.assertEquals(transform('adresse'),
+        self.assertEqual(transform('adresse'),
                           ('EmailAddress E',))
         self.assertRaises(BadRQLQuery, transform, 'Workcases')
 
     def test_two_words_query(self):
         """tests the 'two words shortcut queries'"""
         transform = self.proc._two_words_query
-        self.assertEquals(transform('CWUser', 'E'),
+        self.assertEqual(transform('CWUser', 'E'),
                           ("CWUser E",))
-        self.assertEquals(transform('CWUser', 'Smith'),
-                          ('CWUser C WHERE C has_text %(text)s', {'text': 'Smith'}))
-        self.assertEquals(transform('utilisateur', 'Smith'),
-                          ('CWUser C WHERE C has_text %(text)s', {'text': 'Smith'}))
-        self.assertEquals(transform(u'adresse', 'Logilab'),
-                          ('EmailAddress E WHERE E has_text %(text)s', {'text': 'Logilab'}))
-        self.assertEquals(transform(u'adresse', 'Logi%'),
+        self.assertEqual(transform('CWUser', 'Smith'),
+                          ('CWUser C ORDERBY FTIRANK(C) DESC WHERE C has_text %(text)s', {'text': 'Smith'}))
+        self.assertEqual(transform('utilisateur', 'Smith'),
+                          ('CWUser C ORDERBY FTIRANK(C) DESC WHERE C has_text %(text)s', {'text': 'Smith'}))
+        self.assertEqual(transform(u'adresse', 'Logilab'),
+                          ('EmailAddress E ORDERBY FTIRANK(E) DESC WHERE E has_text %(text)s', {'text': 'Logilab'}))
+        self.assertEqual(transform(u'adresse', 'Logi%'),
                           ('EmailAddress E WHERE E alias LIKE %(text)s', {'text': 'Logi%'}))
         self.assertRaises(BadRQLQuery, transform, "pers", "taratata")
 
     def test_three_words_query(self):
         """tests the 'three words shortcut queries'"""
         transform = self.proc._three_words_query
-        self.assertEquals(transform('utilisateur', u'prénom', 'cubicweb'),
+        self.assertEqual(transform('utilisateur', u'prénom', 'cubicweb'),
                           ('CWUser C WHERE C firstname %(text)s', {'text': 'cubicweb'}))
-        self.assertEquals(transform('utilisateur', 'nom', 'cubicweb'),
+        self.assertEqual(transform('utilisateur', 'nom', 'cubicweb'),
                           ('CWUser C WHERE C surname %(text)s', {'text': 'cubicweb'}))
-        self.assertEquals(transform(u'adresse', 'nom', 'cubicweb'),
+        self.assertEqual(transform(u'adresse', 'nom', 'cubicweb'),
                           ('EmailAddress E WHERE E alias %(text)s', {'text': 'cubicweb'}))
-        self.assertEquals(transform('EmailAddress', 'nom', 'cubicweb'),
+        self.assertEqual(transform('EmailAddress', 'nom', 'cubicweb'),
                           ('EmailAddress E WHERE E alias %(text)s', {'text': 'cubicweb'}))
-        self.assertEquals(transform('utilisateur', u'prénom', 'cubicweb%'),
+        self.assertEqual(transform('utilisateur', u'prénom', 'cubicweb%'),
                           ('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'}))
-        self.assertEquals(transform('CWUser', 'use_email', '%Logilab'),
+        self.assertEqual(transform('CWUser', 'use_email', 'Logilab'),
+                          ('CWUser C ORDERBY FTIRANK(C1) DESC WHERE C use_email C1, C1 has_text %(text)s', {'text': 'Logilab'}))
+        self.assertEqual(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')
 
     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'})),
             ]
         transform = self.proc._quoted_words_query
         for query, expected in queries:
-            self.assertEquals(transform(query), expected)
+            self.assertEqual(transform(query), expected)
         self.assertRaises(BadRQLQuery, transform, "unquoted rql")
         self.assertRaises(BadRQLQuery, transform, 'pers "Jean Paul"')
         self.assertRaises(BadRQLQuery, transform, 'CWUser firstname other "Jean Paul"')
@@ -177,11 +175,11 @@
         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:
-            self.assertEquals(self.proc.preprocess_query(query), expected)
+            self.assertEqual(self.proc.preprocess_query(query), expected)
         self.assertRaises(BadRQLQuery,
                           self.proc.preprocess_query, 'Any X WHERE X is Something')
 
@@ -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"',
@@ -215,13 +213,13 @@
             ]
         for query, expected in queries:
             rset = self.proc.process_query(query)
-            self.assertEquals((rset.rql, rset.args), expected)
+            self.assertEqual((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.assertEqual(rset.rql, "Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s")
+        self.assertEqual(rset.args, {'text': u'écrire'})
 
     def test_explicit_component(self):
         self.assertRaises(RQLSyntaxError,
@@ -229,8 +227,8 @@
         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.args, {'text': u'utilisateur Smith'})
+        self.assertEqual(rset.rql, 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s')
+        self.assertEqual(rset.args, {'text': u'utilisateur Smith'})
 
 if __name__ == '__main__':
     unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_propertysheet.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,53 @@
+import os
+from os.path import join, dirname
+from shutil import rmtree
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.web.propertysheet import *
+
+DATADIR = join(dirname(__file__), 'data')
+CACHEDIR = join(DATADIR, 'uicache')
+
+class PropertySheetTC(TestCase):
+
+    def tearDown(self):
+        rmtree(CACHEDIR)
+
+    def test(self):
+        ps = PropertySheet(CACHEDIR, datadir_url='http://cwtest.com')
+        ps.load(join(DATADIR, 'sheet1.py'))
+        ps.load(join(DATADIR, 'sheet2.py'))
+        # defined by sheet1
+        self.assertEqual(ps['logo'], 'http://cwtest.com/logo.png')
+        # defined by sheet1, overriden by sheet2
+        self.assertEqual(ps['bgcolor'], '#FFFFFF')
+        # defined by sheet2
+        self.assertEqual(ps['fontcolor'], 'black')
+        # defined by sheet1, extended by sheet2
+        self.assertEqual(ps['stylesheets'], ['http://cwtest.com/cubicweb.css',
+                                              'http://cwtest.com/mycube.css'])
+        # lazy string defined by sheet1
+        self.assertIsInstance(ps['lazy'], lazystr)
+        self.assertEqual(str(ps['lazy']), '#FFFFFF')
+        # test compilation
+        self.assertEqual(ps.compile('a {bgcolor: %(bgcolor)s; size: 1%;}'),
+                          'a {bgcolor: #FFFFFF; size: 1%;}')
+        self.assertEqual(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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_reledit.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,225 @@
+# 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/>.
+"""
+mainly regression-preventing tests for reledit/doreledit views
+"""
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web.uicfg import reledit_ctrl
+
+class ReleditMixinTC(object):
+
+    def setup_database(self):
+        self.req = self.request()
+        self.proj = self.req.create_entity('Project', title=u'cubicweb-world-domination')
+        self.tick = self.req.create_entity('Ticket', title=u'write the code')
+        self.toto = self.req.create_entity('Personne', nom=u'Toto')
+
+class ClickAndEditFormTC(ReleditMixinTC, CubicWebTC):
+
+    def test_default_config(self):
+        reledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, false, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+                   'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
+                   'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+                   'composite_card11_2ttypes': """&lt;not specified&gt;""",
+                   'concerns': """&lt;not specified&gt;"""}
+
+        for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+            if rschema not in reledit:
+                continue
+            rtype = rschema.type
+            self.assertMultiLineEqual(reledit[rtype] % {'eid': self.proj.eid}, self.proj.view('reledit', rtype=rtype, role=role), rtype)
+
+    def test_default_forms(self):
+        doreledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="title-subject-%(eid)s-form" onsubmit="return freezeFormButtons(&#39;title-subject-%(eid)s-form&#39;);" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="base" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#title-subject-%(eid)s-form" />
+<input name="__domid" type="hidden" value="title-subject-%(eid)s-form" />
+<input name="__type:%(eid)s" type="hidden" value="Project" />
+<input name="eid" type="hidden" value="%(eid)s" />
+<input name="__maineid" type="hidden" value="%(eid)s" />
+<input name="__reledit|vid" type="hidden" value="" />
+<input name="__reledit|rtype" type="hidden" value="title" />
+<input name="__reledit|divid" type="hidden" value="title-subject-%(eid)s" />
+<input name="__reledit|formid" type="hidden" value="base" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="%(eid)s" />
+<input name="_cw_edited_fields:%(eid)s" type="hidden" value="title-subject,__type" />
+<fieldset class="default">
+<table class="">
+<tr class="title_subject_row">
+<td
+>
+<input id="title-subject:%(eid)s" maxlength="32" name="title-subject:%(eid)s" size="32" tabindex="1" type="text" value="cubicweb-world-domination" />
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="2" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;title-subject-%(eid)s&#39;)" tabindex="3" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, false, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+
+                     'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="long_desc-subject-%(eid)s-form" onsubmit="return freezeFormButtons(&#39;long_desc-subject-%(eid)s-form&#39;);" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="edition" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#long_desc-subject-%(eid)s-form" />
+<input name="__domid" type="hidden" value="long_desc-subject-%(eid)s-form" />
+<input name="__type:A" type="hidden" value="Blog" />
+<input name="eid" type="hidden" value="A" />
+<input name="__maineid" type="hidden" value="A" />
+<input name="__linkto" type="hidden" value="long_desc:%(eid)s:object" />
+<input name="__message" type="hidden" value="entity linked" />
+<input name="__reledit|vid" type="hidden" value="autolimited" />
+<input name="__reledit|rtype" type="hidden" value="long_desc" />
+<input name="__reledit|divid" type="hidden" value="long_desc-subject-%(eid)s" />
+<input name="__reledit|formid" type="hidden" value="edition" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="%(eid)s" />
+<input name="_cw_edited_fields:A" type="hidden" value="title-subject,rss_url-subject,__type,description-subject" />
+<fieldset class="default">
+<table class="attributeForm">
+<tr class="title_subject_row">
+<th class="labelCol"><label class="required" for="title-subject:A">title</label></th>
+<td
+>
+<input id="title-subject:A" maxlength="50" name="title-subject:A" size="45" tabindex="4" type="text" value="" />
+</td></tr>
+<tr class="description_subject_row">
+<th class="labelCol"><label for="description-subject:A">description</label></th>
+<td
+>
+<input name="description_format-subject:A" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:A" name="description-subject:A" onkeyup="autogrow(this)" rows="2" tabindex="5"></textarea>
+</td></tr>
+<tr class="rss_url_subject_row">
+<th class="labelCol"><label for="rss_url-subject:A">rss_url</label></th>
+<td
+>
+<input id="rss_url-subject:A" maxlength="128" name="rss_url-subject:A" size="45" tabindex="6" type="text" value="" />
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="7" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;long_desc-subject-%(eid)s&#39;)" tabindex="8" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
+
+                     'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="manager-subject-%(eid)s-form" onsubmit="return freezeFormButtons(&#39;manager-subject-%(eid)s-form&#39;);" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="base" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#manager-subject-%(eid)s-form" />
+<input name="__domid" type="hidden" value="manager-subject-%(eid)s-form" />
+<input name="__type:%(eid)s" type="hidden" value="Project" />
+<input name="eid" type="hidden" value="%(eid)s" />
+<input name="__maineid" type="hidden" value="%(eid)s" />
+<input name="__linkto" type="hidden" value="long_desc:%(eid)s:object" />
+<input name="__message" type="hidden" value="entity linked" />
+<input name="__reledit|vid" type="hidden" value="autolimited" />
+<input name="__reledit|rtype" type="hidden" value="manager" />
+<input name="__reledit|divid" type="hidden" value="manager-subject-%(eid)s" />
+<input name="__reledit|formid" type="hidden" value="base" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="%(eid)s" />
+<input name="_cw_edited_fields:%(eid)s" type="hidden" value="manager-subject,__type" />
+<fieldset class="default">
+<table class="">
+<tr class="manager_subject_row">
+<td
+>
+<select id="manager-subject:%(eid)s" name="manager-subject:%(eid)s" size="1" tabindex="9">
+<option value="__cubicweb_internal_field__"></option>
+<option value="%(toto)s">Toto</option>
+</select>
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="10" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;manager-subject-%(eid)s&#39;)" tabindex="11" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+                     'composite_card11_2ttypes': """&lt;not specified&gt;""",
+                     'concerns': """&lt;not specified&gt;"""
+            }
+        for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+            if rschema not in doreledit:
+                continue
+            rtype = rschema.type
+            self.assertMultiLineEqual(doreledit[rtype] % {'eid': self.proj.eid, 'toto': self.toto.eid},
+                                  self.proj.view('doreledit', rtype=rtype, role=role,
+                                                 formid='edition' if rtype == 'long_desc' else 'base'),
+                                  rtype)
+
+class ClickAndEditFormUICFGTC(ReleditMixinTC, CubicWebTC):
+
+    def setup_database(self):
+        super(ClickAndEditFormUICFGTC, self).setup_database()
+        self.tick.set_relations(concerns=self.proj)
+        self.proj.set_relations(manager=self.toto)
+
+    def test_with_uicfg(self):
+        old_rctl = reledit_ctrl._tagdefs.copy()
+        reledit_ctrl.tag_attribute(('Project', 'title'),
+                                   {'novalue_label': '<title is required>', 'reload': True})
+        reledit_ctrl.tag_subject_of(('Project', 'long_desc', '*'),
+                                    {'reload': True, 'edit_target': 'rtype',
+                                     'novalue_label': u'<long_desc is required>'})
+        reledit_ctrl.tag_subject_of(('Project', 'manager', '*'),
+                                   {'edit_target': 'related'})
+        reledit_ctrl.tag_subject_of(('Project', 'composite_card11_2ttypes', '*'),
+                                   {'edit_target': 'related'})
+        reledit_ctrl.tag_object_of(('Ticket', 'concerns', 'Project'),
+                                   {'edit_target': 'rtype'})
+        reledit = {
+            'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, true, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+            'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;long_desc is required&gt;</div><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, true, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+            'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/personne/%(toto)s" title="">Toto</a></div><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div><div id="manager-subject-%(eid)s-delete" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;deleteconf&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to delete this value"><img title="click to delete this value" src="data/cancel.png" alt="click to delete this value"/></div></div></div>""",
+            'composite_card11_2ttypes': """&lt;not specified&gt;""",
+            'concerns': """<div id="concerns-object-%(eid)s-reledit" onmouseout="jQuery('#concerns-object-%(eid)s').addClass('hidden')" onmouseover="jQuery('#concerns-object-%(eid)s').removeClass('hidden')" class="releditField"><div id="concerns-object-%(eid)s-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/ticket/%(tick)s" title="">write the code</a></div><div id="concerns-object-%(eid)s" class="editableField hidden"><div id="concerns-object-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;concerns&#39;, &#39;object&#39;, &#39;concerns-object-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>"""
+            }
+        for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+            if rschema not in reledit:
+                continue
+            rtype = rschema.type
+            self.assertMultiLineEqual(reledit[rtype] % {'eid': self.proj.eid, 'toto': self.toto.eid, 'tick': self.tick.eid},
+                                  self.proj.view('reledit', rtype=rtype, role=role),
+                                  rtype)
+        reledit_ctrl.clear()
+        reledit_ctrl._tagdefs.update(old_rctl)
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/web/test/unittest_session.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_session.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,8 +15,8 @@
         # make is if the web session has been opened by the session manager
         sm._sessions[self.cnx.sessionid] = self.websession
         sessionid = self.websession.sessionid
-        self.assertEquals(len(sm._sessions), 1)
-        self.assertEquals(self.websession.sessionid, self.websession.cnx.sessionid)
+        self.assertEqual(len(sm._sessions), 1)
+        self.assertEqual(self.websession.sessionid, self.websession.cnx.sessionid)
         # fake the repo session is expiring
         self.repo.close(sessionid)
         try:
@@ -24,9 +24,9 @@
             # don't use self.request() which try to call req.set_session
             req = self.requestcls(self.vreg)
             websession = sm.get_session(req, sessionid)
-            self.assertEquals(len(sm._sessions), 1)
+            self.assertEqual(len(sm._sessions), 1)
             self.assertIs(websession, self.websession)
-            self.assertEquals(websession.sessionid, sessionid)
+            self.assertEqual(websession.sessionid, sessionid)
             self.assertNotEquals(websession.sessionid, websession.cnx.sessionid)
         finally:
             # avoid error in tearDown by telling this connection is closed...
--- a/web/test/unittest_uicfg.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_uicfg.py	Wed Nov 03 16:38:28 2010 +0100
@@ -62,7 +62,7 @@
     def test_definition_order_hidden(self):
         result = uicfg.autoform_section.get('CWUser', 'login', 'String', 'subject')
         expected = set(['main_inlined', 'muledit_attributes', 'inlined_attributes'])
-        self.assertSetEquals(result, expected)
+        self.assertSetEqual(result, expected)
 
     def tearDown(self):
         super(DefinitionOrderTC, self).tearDown()
--- a/web/test/unittest_urlpublisher.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_urlpublisher.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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 url publishing service
-
-"""
+"""Unit tests for url publishing service"""
 
 import re
 
@@ -47,51 +45,51 @@
 
     def test_raw_path(self):
         """tests raw path resolution'"""
-        self.assertEquals(self.process('view'), ('view', None))
-        self.assertEquals(self.process('edit'), ('edit', None))
+        self.assertEqual(self.process('view'), ('view', None))
+        self.assertEqual(self.process('edit'), ('edit', None))
         self.assertRaises(NotFound, self.process, 'whatever')
 
     def test_eid_path(self):
         """tests eid path resolution"""
         self.assertIsInstance(self.process('123')[1], ResultSet)
-        self.assertEquals(len(self.process('123')[1]), 1)
+        self.assertEqual(len(self.process('123')[1]), 1)
         self.assertRaises(NotFound, self.process, '123/345')
         self.assertRaises(NotFound, self.process, 'not_eid')
 
     def test_rest_path(self):
         """tests the rest path resolution"""
         ctrl, rset = self.process('CWUser')
-        self.assertEquals(ctrl, 'view')
-        self.assertEquals(rset.description[0][0], 'CWUser')
-        self.assertEquals(rset.printable_rql(),
+        self.assertEqual(ctrl, 'view')
+        self.assertEqual(rset.description[0][0], 'CWUser')
+        self.assertEqual(rset.printable_rql(),
                           "Any X,AA,AB,AC,AD ORDERBY AA WHERE X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD")
         ctrl, rset = self.process('CWUser/login/admin')
-        self.assertEquals(ctrl, 'view')
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset.description[0][0], 'CWUser')
-        self.assertEquals(rset.printable_rql(), 'Any X WHERE X is CWUser, X login "admin"')
+        self.assertEqual(ctrl, 'view')
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.description[0][0], 'CWUser')
+        self.assertEqual(rset.printable_rql(), 'Any X,AA,AB,AC,AD WHERE X login "admin", X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD')
         ctrl, rset = self.process('cwuser/admin')
-        self.assertEquals(ctrl, 'view')
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset.description[0][0], 'CWUser')
-        self.assertEquals(rset.printable_rql(), 'Any X WHERE X is CWUser, X login "admin"')
+        self.assertEqual(ctrl, 'view')
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.description[0][0], 'CWUser')
+        self.assertEqual(rset.printable_rql(), 'Any X,AA,AB,AC,AD WHERE X login "admin", X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD')
         ctrl, rset = self.process('cwuser/eid/%s'%rset[0][0])
-        self.assertEquals(ctrl, 'view')
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset.description[0][0], 'CWUser')
-        self.assertEquals(rset.printable_rql(), 'Any X WHERE X is CWUser, X eid 5')
+        self.assertEqual(ctrl, 'view')
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.description[0][0], 'CWUser')
+        self.assertEqual(rset.printable_rql(), 'Any X,AA,AB,AC,AD WHERE X eid 5, X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD')
         # test non-ascii paths
         ctrl, rset = self.process('CWUser/login/%C3%BFsa%C3%BFe')
-        self.assertEquals(ctrl, 'view')
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset.description[0][0], 'CWUser')
-        self.assertEquals(rset.printable_rql(), u'Any X WHERE X is CWUser, X login "ÿsaÿe"')
+        self.assertEqual(ctrl, 'view')
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.description[0][0], 'CWUser')
+        self.assertEqual(rset.printable_rql(), u'Any X,AA,AB,AC,AD WHERE X login "\xffsa\xffe", X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD')
         # test quoted paths
         ctrl, rset = self.process('BlogEntry/title/hell%27o')
-        self.assertEquals(ctrl, 'view')
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset.description[0][0], 'BlogEntry')
-        self.assertEquals(rset.printable_rql(), u'Any X WHERE X is BlogEntry, X title "hell\'o"')
+        self.assertEqual(ctrl, 'view')
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset.description[0][0], 'BlogEntry')
+        self.assertEqual(rset.printable_rql(), u'Any X,AA,AB,AC WHERE X title "hell\'o", X is BlogEntry, X creation_date AA, X title AB, X modification_date AC')
         # errors
         self.assertRaises(NotFound, self.process, 'CWUser/eid/30000')
         self.assertRaises(NotFound, self.process, 'Workcases')
@@ -110,9 +108,9 @@
     def test_regexp_path(self):
         """tests the regexp path resolution"""
         ctrl, rset = self.process('add/Task')
-        self.assertEquals(ctrl, 'view')
-        self.assertEquals(rset, None)
-        self.assertEquals(self.req.form, {'etype' : "Task", 'vid' : "creation"})
+        self.assertEqual(ctrl, 'view')
+        self.assertEqual(rset, None)
+        self.assertEqual(self.req.form, {'etype' : "Task", 'vid' : "creation"})
         self.assertRaises(NotFound, self.process, 'add/foo/bar')
 
 
@@ -122,8 +120,8 @@
         try:
             path = str(FakeRequest().url_quote(u'été'))
             ctrl, rset = self.process(path)
-            self.assertEquals(rset, None)
-            self.assertEquals(self.req.form, {'vid' : "foo"})
+            self.assertEqual(rset, None)
+            self.assertEqual(self.req.form, {'vid' : "foo"})
         finally:
             SimpleReqRewriter.rules = oldrules
 
--- a/web/test/unittest_urlrewrite.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_urlrewrite.py	Wed Nov 03 16:38:28 2010 +0100
@@ -39,7 +39,7 @@
             if hasattr(pattern, 'pattern'):
                 pattern = pattern.pattern
             rules.append((pattern, values))
-        self.assertListEquals(rules, [
+        self.assertListEqual(rules, [
             ('foo' , dict(rql='Foo F')),
             ('/index' , dict(vid='index2')),
             ('/_', dict(vid='manage')),
@@ -71,7 +71,7 @@
                 ('foo', dict(rql='Foo F')),
                 ('/index', dict(vid='index2')),
                 ]
-        self.assertListEquals(Rewriter.rules, [
+        self.assertListEqual(Rewriter.rules, [
             ('foo' , dict(rql='Foo F')),
             ('/index' , dict(vid='index2')),
             ])
@@ -81,19 +81,19 @@
         req = FakeRequest()
         rewriter = SimpleReqRewriter(req)
         self.assertRaises(KeyError, rewriter.rewrite, req, '/view?vid=whatever')
-        self.assertEquals(req.form, {})
+        self.assertEqual(req.form, {})
         rewriter.rewrite(req, '/index')
-        self.assertEquals(req.form, {'vid' : "index"})
+        self.assertEqual(req.form, {'vid' : "index"})
 
     def test_regexp_transformation(self):
         """test regexp-based rewrite"""
         req = FakeRequest()
         rewriter = SimpleReqRewriter(req)
         rewriter.rewrite(req, '/add/Task')
-        self.assertEquals(req.form, {'vid' : "creation", 'etype' : "Task"})
+        self.assertEqual(req.form, {'vid' : "creation", 'etype' : "Task"})
         req = FakeRequest()
         rewriter.rewrite(req, '/add/Task/')
-        self.assertEquals(req.form, {'vid' : "creation", 'etype' : "Task"})
+        self.assertEqual(req.form, {'vid' : "creation", 'etype' : "Task"})
 
 
 
@@ -117,8 +117,8 @@
         req = self.request()
         rewriter = TestSchemaBasedRewriter(req)
         pmid, rset = rewriter.rewrite(req, u'/DaLToN/JoE')
-        self.assertEquals(len(rset), 1)
-        self.assertEquals(rset[0][0], self.p1.eid)
+        self.assertEqual(len(rset), 1)
+        self.assertEqual(rset[0][0], self.p1.eid)
 
     def test_inheritance_precedence(self):
         RQL1 = 'Any C WHERE C is CWEType'
@@ -142,17 +142,17 @@
         req = self.request()
         rewriter = Rewriter(req)
         pmid, rset = rewriter.rewrite(req, '/collector')
-        self.assertEquals(rset.rql, RQL1)
-        self.assertEquals(req.form, {'vid' : "baseindex"})
+        self.assertEqual(rset.rql, RQL1)
+        self.assertEqual(req.form, {'vid' : "baseindex"})
         pmid, rset = rewriter.rewrite(req, '/collector/something')
-        self.assertEquals(rset.rql, RQL2)
-        self.assertEquals(req.form, {'vid' : "index"})
+        self.assertEqual(rset.rql, RQL2)
+        self.assertEqual(req.form, {'vid' : "index"})
         pmid, rset = rewriter.rewrite(req, '/collector/something/')
-        self.assertEquals(req.form, {'vid' : "index"})
-        self.assertEquals(rset.rql, RQL2)
+        self.assertEqual(req.form, {'vid' : "index"})
+        self.assertEqual(rset.rql, RQL2)
         pmid, rset = rewriter.rewrite(req, '/collector/somethingelse/')
-        self.assertEquals(rset.rql, RQL1)
-        self.assertEquals(req.form, {'vid' : "baseindex"})
+        self.assertEqual(rset.rql, RQL1)
+        self.assertEqual(req.form, {'vid' : "baseindex"})
 
     def test_inheritance_precedence_same_rgx(self):
         RQL1 = 'Any C WHERE C is CWEType'
@@ -176,17 +176,17 @@
         req = self.request()
         rewriter = Rewriter(req)
         pmid, rset = rewriter.rewrite(req, '/collector')
-        self.assertEquals(rset.rql, RQL2)
-        self.assertEquals(req.form, {'vid' : "index"})
+        self.assertEqual(rset.rql, RQL2)
+        self.assertEqual(req.form, {'vid' : "index"})
         pmid, rset = rewriter.rewrite(req, '/collector/something')
-        self.assertEquals(rset.rql, RQL2)
-        self.assertEquals(req.form, {'vid' : "index"})
+        self.assertEqual(rset.rql, RQL2)
+        self.assertEqual(req.form, {'vid' : "index"})
         pmid, rset = rewriter.rewrite(req, '/collector/something/')
-        self.assertEquals(req.form, {'vid' : "index"})
-        self.assertEquals(rset.rql, RQL2)
+        self.assertEqual(req.form, {'vid' : "index"})
+        self.assertEqual(rset.rql, RQL2)
         pmid, rset = rewriter.rewrite(req, '/collector/somethingelse/')
-        self.assertEquals(rset.rql, RQL2)
-        self.assertEquals(req.form, {'vid' : "index"})
+        self.assertEqual(rset.rql, RQL2)
+        self.assertEqual(req.form, {'vid' : "index"})
 
 
 if __name__ == '__main__':
--- a/web/test/unittest_views_actions.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_actions.py	Wed Nov 03 16:38:28 2010 +0100
@@ -28,7 +28,7 @@
         rset = self.execute('CWUser X')
         actions = self.vreg['actions'].poss_visible_objects(req, rset=rset)
         vaction = [action for action in actions if action.__regid__ == 'view'][0]
-        self.assertEquals(vaction.url(), 'http://testing.fr/cubicweb/view?rql=CWUser%20X')
+        self.assertEqual(vaction.url(), 'http://testing.fr/cubicweb/view?rql=CWUser%20X')
 
     def test_sendmail_action(self):
         req = self.request()
--- a/web/test/unittest_views_apacherewrite.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_apacherewrite.py	Wed Nov 03 16:38:28 2010 +0100
@@ -41,18 +41,18 @@
             urlrewriter.rewrite('logilab.fr', '/whatever', req)
             self.fail('redirect exception expected')
         except Redirect, ex:
-            self.assertEquals(ex.location, 'http://www.logilab.fr/whatever')
-        self.assertEquals(urlrewriter.rewrite('www.logilab.fr', '/whatever', req),
+            self.assertEqual(ex.location, 'http://www.logilab.fr/whatever')
+        self.assertEqual(urlrewriter.rewrite('www.logilab.fr', '/whatever', req),
                           '/whatever')
-        self.assertEquals(urlrewriter.rewrite('www.logilab.fr', '/json/bla', req),
+        self.assertEqual(urlrewriter.rewrite('www.logilab.fr', '/json/bla', req),
                           '/json/bla')
-        self.assertEquals(urlrewriter.rewrite('abcd.logilab.fr', '/json/bla', req),
+        self.assertEqual(urlrewriter.rewrite('abcd.logilab.fr', '/json/bla', req),
                           '/json/bla')
-        self.assertEquals(urlrewriter.rewrite('abcd.logilab.fr', '/data/bla', req),
+        self.assertEqual(urlrewriter.rewrite('abcd.logilab.fr', '/data/bla', req),
                           '/data/bla')
-        self.assertEquals(urlrewriter.rewrite('abcd.logilab.fr', '/whatever', req),
+        self.assertEqual(urlrewriter.rewrite('abcd.logilab.fr', '/whatever', req),
                           '/m_abcd/whatever')
-        self.assertEquals(urlrewriter.rewrite('abcd.fr', '/whatever', req),
+        self.assertEqual(urlrewriter.rewrite('abcd.fr', '/whatever', req),
                           '/whatever')
 
 
--- a/web/test/unittest_views_basecontrollers.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,17 +15,16 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb.web.views.basecontrollers unit tests
-
-"""
+"""cubicweb.web.views.basecontrollers unit tests"""
 
 from logilab.common.testlib import unittest_main, mock_object
 
 from cubicweb import Binary, NoSelectableObject, ValidationError
 from cubicweb.view import STRICT_DOCTYPE
 from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.utils import json_dumps
 from cubicweb.uilib import rql_for_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError, json
+from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError
 from cubicweb.entities.authobjs import CWUser
 from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes
 u = unicode
@@ -49,7 +48,7 @@
         """check behaviour of this controller without any form parameter
         """
         ex = self.assertRaises(ValidationError, self.ctrl_publish, self.request())
-        self.assertEquals(ex.errors, {None: u'no selected entities'})
+        self.assertEqual(ex.errors, {None: u'no selected entities'})
 
     def test_validation_unique(self):
         """test creation of two linked entities
@@ -63,7 +62,7 @@
                     'upassword-subject-confirm:X': u'toto',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
+        self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
     def test_user_editing_itself(self):
         """checking that a manager user can edit itself
@@ -84,10 +83,10 @@
             }
         path, params = self.expect_redirect_publish(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
-        self.assertEquals(e.firstname, u'Sylvain')
-        self.assertEquals(e.surname, u'Th\xe9nault')
-        self.assertEquals(e.login, user.login)
-        self.assertEquals([g.eid for g in e.in_group], groupeids)
+        self.assertEqual(e.firstname, u'Sylvain')
+        self.assertEqual(e.surname, u'Th\xe9nault')
+        self.assertEqual(e.login, user.login)
+        self.assertEqual([g.eid for g in e.in_group], groupeids)
 
     def test_user_can_change_its_password(self):
         user = self.create_user('user')
@@ -103,7 +102,7 @@
             }
         path, params = self.expect_redirect_publish(req, 'edit')
         cnx.commit() # commit to check we don't get late validation error for instance
-        self.assertEquals(path, 'cwuser/user')
+        self.assertEqual(path, 'cwuser/user')
         self.failIf('vid' in params)
 
     def test_user_editing_itself_no_relation(self):
@@ -124,11 +123,11 @@
             }
         path, params = self.expect_redirect_publish(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
-        self.assertEquals(e.login, user.login)
-        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.assertEqual(e.login, user.login)
+        self.assertEqual(e.firstname, u'Th\xe9nault')
+        self.assertEqual(e.surname, u'Sylvain')
+        self.assertEqual([g.eid for g in e.in_group], groupeids)
+        self.assertEqual(e.cw_adapt_to('IWorkflowable').state, 'activated')
 
 
     def test_create_multiple_linked(self):
@@ -150,11 +149,11 @@
                     }
         path, params = self.expect_redirect_publish(req, 'edit')
         # should be redirected on the created person
-        self.assertEquals(path, 'cwuser/adim')
+        self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Mascio"').get_entity(0, 0)
-        self.assertEquals(e.surname, 'Di Mascio')
+        self.assertEqual(e.surname, 'Di Mascio')
         email = e.use_email[0]
-        self.assertEquals(email.address, 'dima@logilab.fr')
+        self.assertEqual(email.address, 'dima@logilab.fr')
 
     def test_edit_multiple_linked(self):
         peid = u(self.create_user('adim').eid)
@@ -172,10 +171,10 @@
                     }
         path, params = self.expect_redirect_publish(req, 'edit')
         # should be redirected on the created person
-        self.assertEquals(path, 'cwuser/adim')
+        self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Masci"').get_entity(0, 0)
         email = e.use_email[0]
-        self.assertEquals(email.address, 'dima@logilab.fr')
+        self.assertEqual(email.address, 'dima@logilab.fr')
 
         emaileid = u(email.eid)
         req = self.request()
@@ -192,7 +191,7 @@
                     }
         path, params = self.expect_redirect_publish(req, 'edit')
         email.clear_all_caches()
-        self.assertEquals(email.address, 'adim@logilab.fr')
+        self.assertEqual(email.address, 'adim@logilab.fr')
 
 
     def test_password_confirm(self):
@@ -207,7 +206,7 @@
                     'upassword-subject:X': u'toto',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
+        self.assertEqual(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
         req = self.request()
         req.form = {'__cloned_eid:X': u(user.eid),
                     'eid': 'X', '__type:X': 'CWUser',
@@ -217,13 +216,14 @@
                     'upassword-subject-confirm:X': u'tutu',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
+        self.assertEqual(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
 
 
     def test_interval_bound_constraint_success(self):
         feid = self.execute('INSERT File X: X data_name "toto.txt", X data %(data)s',
                             {'data': Binary('yo')})[0][0]
-        req = self.request()
+        self.commit()
+        req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
                     '_cw_edited_fields:X': 'amount-subject,described_by_test-subject',
@@ -231,8 +231,8 @@
                     'described_by_test-subject:X': u(feid),
                 }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'amount-subject': 'value must be >= 0'})
-        req = self.request()
+        self.assertEqual(ex.errors, {'amount-subject': 'value must be >= 0'})
+        req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
                     '_cw_edited_fields:X': 'amount-subject,described_by_test-subject',
@@ -240,8 +240,8 @@
                     'described_by_test-subject:X': u(feid),
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'amount-subject': 'value must be <= 100'})
-        req = self.request()
+        self.assertEqual(ex.errors, {'amount-subject': 'value must be <= 100'})
+        req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
                     '_cw_edited_fields:X': 'amount-subject,described_by_test-subject',
@@ -252,7 +252,7 @@
         # should be redirected on the created
         #eid = params['rql'].split()[-1]
         e = self.execute('Salesterm X').get_entity(0, 0)
-        self.assertEquals(e.amount, 10)
+        self.assertEqual(e.amount, 10)
 
     def test_req_pending_insert(self):
         """make sure req's pending insertions are taken into account"""
@@ -263,8 +263,8 @@
         path, params = self.expect_redirect_publish(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
-        self.assertUnorderedIterableEquals(usergroups, ['managers', 'test'])
-        self.assertEquals(get_pending_inserts(req), [])
+        self.assertItemsEqual(usergroups, ['managers', 'test'])
+        self.assertEqual(get_pending_inserts(req), [])
 
 
     def test_req_pending_delete(self):
@@ -275,35 +275,15 @@
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         # just make sure everything was set correctly
-        self.assertUnorderedIterableEquals(usergroups, ['managers', 'test'])
+        self.assertItemsEqual(usergroups, ['managers', 'test'])
         # now try to delete the relation
         req = self.request(**req_form(user))
         req.session.data['pending_delete'] = set([(user.eid, 'in_group', groupeid)])
         path, params = self.expect_redirect_publish(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
-        self.assertUnorderedIterableEquals(usergroups, ['managers'])
-        self.assertEquals(get_pending_deletes(req), [])
-
-    # def test_custom_attribute_handler(self):
-    #     def custom_login_edit(self, formparams, value, relations):
-    #         formparams['login'] = value.upper()
-    #         relations.append('X login %(login)s')
-    #     CWUser.custom_login_edit = custom_login_edit
-    #     try:
-    #         user = self.user()
-    #         eid = repr(user.eid)
-    #         req = self.request()
-    #         req.form = {
-    #             'eid': eid,
-    #             '__type:'+eid:  'CWUser',
-    #             'login:'+eid: u'foo',
-    #             }
-    #         path, params = self.expect_redirect_publish(req, 'edit')
-    #         rset = self.execute('Any L WHERE X eid %(x)s, X login L', {'x': user.eid}, 'x')
-    #         self.assertEquals(rset[0][0], 'FOO')
-    #     finally:
-    #         del CWUser.custom_login_edit
+        self.assertItemsEqual(usergroups, ['managers'])
+        self.assertEqual(get_pending_deletes(req), [])
 
     def test_redirect_apply_button(self):
         redirectrql = rql_for_eid(4012) # whatever
@@ -322,11 +302,11 @@
         path, params = self.expect_redirect_publish(req, 'edit')
         self.failUnless(path.startswith('blogentry/'))
         eid = path.split('/')[1]
-        self.assertEquals(params['vid'], 'edition')
-        self.assertNotEquals(int(eid), 4012)
-        self.assertEquals(params['__redirectrql'], redirectrql)
-        self.assertEquals(params['__redirectvid'], 'primary')
-        self.assertEquals(params['__redirectparams'], 'toto=tutu&tata=titi')
+        self.assertEqual(params['vid'], 'edition')
+        self.assertNotEqual(int(eid), 4012)
+        self.assertEqual(params['__redirectrql'], redirectrql)
+        self.assertEqual(params['__redirectvid'], 'primary')
+        self.assertEqual(params['__redirectparams'], 'toto=tutu&tata=titi')
 
     def test_redirect_ok_button(self):
         redirectrql = rql_for_eid(4012) # whatever
@@ -342,11 +322,11 @@
             '__form_id': 'edition',
             }
         path, params = self.expect_redirect_publish(req, 'edit')
-        self.assertEquals(path, 'view')
-        self.assertEquals(params['rql'], redirectrql)
-        self.assertEquals(params['vid'], 'primary')
-        self.assertEquals(params['tata'], 'titi')
-        self.assertEquals(params['toto'], 'tutu')
+        self.assertEqual(path, 'view')
+        self.assertEqual(params['rql'], redirectrql)
+        self.assertEqual(params['vid'], 'primary')
+        self.assertEqual(params['tata'], 'titi')
+        self.assertEqual(params['toto'], 'tutu')
 
     def test_redirect_delete_button(self):
         req = self.request()
@@ -354,7 +334,7 @@
         req.form = {'eid': u(eid), '__type:%s'%eid: 'BlogEntry',
                     '__action_delete': ''}
         path, params = self.expect_redirect_publish(req, 'edit')
-        self.assertEquals(path, 'blogentry')
+        self.assertEqual(path, 'blogentry')
         self.assertIn('_cwmsgid', params)
         eid = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
         self.execute('SET X use_email E WHERE E eid %(e)s, X eid %(x)s',
@@ -364,7 +344,7 @@
         req.form = {'eid': u(eid), '__type:%s'%eid: 'EmailAddress',
                     '__action_delete': ''}
         path, params = self.expect_redirect_publish(req, 'edit')
-        self.assertEquals(path, 'cwuser/admin')
+        self.assertEqual(path, 'cwuser/admin')
         self.assertIn('_cwmsgid', params)
         eid1 = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
         eid2 = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
@@ -374,7 +354,7 @@
                     '__type:%s'%eid2: 'EmailAddress',
                     '__action_delete': ''}
         path, params = self.expect_redirect_publish(req, 'edit')
-        self.assertEquals(path, 'view')
+        self.assertEqual(path, 'view')
         self.assertIn('_cwmsgid', params)
 
     def test_nonregr_eetype_etype_editing(self):
@@ -398,8 +378,8 @@
         try:
             path, params = self.expect_redirect_publish(req, 'edit')
             e = self.execute('Any X WHERE X eid %(x)s', {'x': cwetypeeid}).get_entity(0, 0)
-            self.assertEquals(e.name, 'CWEType')
-            self.assertEquals(sorted(g.eid for g in e.read_permission), groupeids)
+            self.assertEqual(e.name, 'CWEType')
+            self.assertEqual(sorted(g.eid for g in e.read_permission), groupeids)
         finally:
             # restore
             self.execute('SET X read_permission Y WHERE X name "CWEType", Y eid IN (%s), NOT X read_permission Y' % (','.join(basegroups)))
@@ -420,8 +400,8 @@
         self.failUnless(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         e = self.execute('Any C, T WHERE C eid %(x)s, C content T', {'x': eid}).get_entity(0, 0)
-        self.assertEquals(e.title, '"13:03:40"')
-        self.assertEquals(e.content, '"13:03:43"')
+        self.assertEqual(e.title, '"13:03:40"')
+        self.assertEqual(e.content, '"13:03:43"')
 
 
     def test_nonregr_multiple_empty_email_addr(self):
@@ -442,7 +422,7 @@
                     'use_email-object:Y': 'X',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'address-subject': u'required field'})
+        self.assertEqual(ex.errors, {'address-subject': u'required field'})
 
     def test_nonregr_copy(self):
         user = self.user()
@@ -454,10 +434,10 @@
                     'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                     }
         path, params = self.expect_redirect_publish(req, 'edit')
-        self.assertEquals(path, 'cwuser/toto')
+        self.assertEqual(path, 'cwuser/toto')
         e = self.execute('Any X WHERE X is CWUser, X login "toto"').get_entity(0, 0)
-        self.assertEquals(e.login, 'toto')
-        self.assertEquals(e.in_group[0].name, 'managers')
+        self.assertEqual(e.login, 'toto')
+        self.assertEqual(e.in_group[0].name, 'managers')
 
 
     def test_nonregr_rollback_on_validation_error(self):
@@ -489,7 +469,7 @@
                 req.form['vid'] = 'copy'
                 self.app_publish(req, 'view')
             rset = self.execute('CWUser P WHERE P surname "Boom"')
-            self.assertEquals(len(rset), 0)
+            self.assertEqual(len(rset), 0)
         finally:
             p.__class__.skip_copy_for = old_skips
 
@@ -561,96 +541,96 @@
 #     def test_json_exec(self):
 #         rql = 'Any T,N WHERE T is Tag, T name N'
 #         ctrl = self.ctrl(self.request(mode='json', rql=rql, pageid='123'))
-#         self.assertEquals(ctrl.publish(),
-#                           json.dumps(self.execute(rql).rows))
+#         self.assertEqual(ctrl.publish(),
+#                           json_dumps(self.execute(rql).rows))
 
     def test_remote_add_existing_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['python'])
-        self.assertUnorderedIterableEquals(
+        self.assertItemsEqual(
             [tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
             ['python', 'cubicweb'])
-        self.assertEquals(
+        self.assertEqual(
             self.execute('Any N WHERE T tags P, P is CWUser, T name N').rows,
             [['python']])
 
     def test_remote_add_new_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['javascript'])
-        self.assertUnorderedIterableEquals(
+        self.assertItemsEqual(
             [tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
             ['python', 'cubicweb', 'javascript'])
-        self.assertEquals(
+        self.assertEqual(
             self.execute('Any N WHERE T tags P, P is CWUser, T name N').rows,
             [['javascript']])
 
     def test_pending_insertion(self):
         res, req = self.remote_call('add_pending_inserts', [['12', 'tags', '13']])
         deletes = get_pending_deletes(req)
-        self.assertEquals(deletes, [])
+        self.assertEqual(deletes, [])
         inserts = get_pending_inserts(req)
-        self.assertEquals(inserts, ['12:tags:13'])
+        self.assertEqual(inserts, ['12:tags:13'])
         res, req = self.remote_call('add_pending_inserts', [['12', 'tags', '14']])
         deletes = get_pending_deletes(req)
-        self.assertEquals(deletes, [])
+        self.assertEqual(deletes, [])
         inserts = get_pending_inserts(req)
-        self.assertEquals(inserts, ['12:tags:13', '12:tags:14'])
+        self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
         inserts = get_pending_inserts(req, 12)
-        self.assertEquals(inserts, ['12:tags:13', '12:tags:14'])
+        self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
         inserts = get_pending_inserts(req, 13)
-        self.assertEquals(inserts, ['12:tags:13'])
+        self.assertEqual(inserts, ['12:tags:13'])
         inserts = get_pending_inserts(req, 14)
-        self.assertEquals(inserts, ['12:tags:14'])
+        self.assertEqual(inserts, ['12:tags:14'])
         req.remove_pending_operations()
 
     def test_pending_deletion(self):
         res, req = self.remote_call('add_pending_delete', ['12', 'tags', '13'])
         inserts = get_pending_inserts(req)
-        self.assertEquals(inserts, [])
+        self.assertEqual(inserts, [])
         deletes = get_pending_deletes(req)
-        self.assertEquals(deletes, ['12:tags:13'])
+        self.assertEqual(deletes, ['12:tags:13'])
         res, req = self.remote_call('add_pending_delete', ['12', 'tags', '14'])
         inserts = get_pending_inserts(req)
-        self.assertEquals(inserts, [])
+        self.assertEqual(inserts, [])
         deletes = get_pending_deletes(req)
-        self.assertEquals(deletes, ['12:tags:13', '12:tags:14'])
+        self.assertEqual(deletes, ['12:tags:13', '12:tags:14'])
         deletes = get_pending_deletes(req, 12)
-        self.assertEquals(deletes, ['12:tags:13', '12:tags:14'])
+        self.assertEqual(deletes, ['12:tags:13', '12:tags:14'])
         deletes = get_pending_deletes(req, 13)
-        self.assertEquals(deletes, ['12:tags:13'])
+        self.assertEqual(deletes, ['12:tags:13'])
         deletes = get_pending_deletes(req, 14)
-        self.assertEquals(deletes, ['12:tags:14'])
+        self.assertEqual(deletes, ['12:tags:14'])
         req.remove_pending_operations()
 
     def test_remove_pending_operations(self):
         self.remote_call('add_pending_delete', ['12', 'tags', '13'])
         _, req = self.remote_call('add_pending_inserts', [['12', 'tags', '14']])
         inserts = get_pending_inserts(req)
-        self.assertEquals(inserts, ['12:tags:14'])
+        self.assertEqual(inserts, ['12:tags:14'])
         deletes = get_pending_deletes(req)
-        self.assertEquals(deletes, ['12:tags:13'])
+        self.assertEqual(deletes, ['12:tags:13'])
         req.remove_pending_operations()
-        self.assertEquals(get_pending_deletes(req), [])
-        self.assertEquals(get_pending_inserts(req), [])
+        self.assertEqual(get_pending_deletes(req), [])
+        self.assertEqual(get_pending_inserts(req), [])
 
 
     def test_add_inserts(self):
         res, req = self.remote_call('add_pending_inserts',
                                     [('12', 'tags', '13'), ('12', 'tags', '14')])
         inserts = get_pending_inserts(req)
-        self.assertEquals(inserts, ['12:tags:13', '12:tags:14'])
+        self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
         req.remove_pending_operations()
 
 
     # 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')))
+        self.assertEqual(self.remote_call('external_resource', 'RSS_LOGO')[0],
+                          json_dumps(self.config.uiprops['RSS_LOGO']))
     def test_i18n(self):
-        self.assertEquals(self.remote_call('i18n', ['bimboom'])[0],
-                          json.dumps(['bimboom']))
+        self.assertEqual(self.remote_call('i18n', ['bimboom'])[0],
+                          json_dumps(['bimboom']))
 
     def test_format_date(self):
-        self.assertEquals(self.remote_call('format_date', '2007-01-01 12:00:00')[0],
-                          json.dumps('2007/01/01'))
+        self.assertEqual(self.remote_call('format_date', '2007-01-01 12:00:00')[0],
+                          json_dumps('2007/01/01'))
 
 
 
--- a/web/test/unittest_views_basetemplates.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_basetemplates.py	Wed Nov 03 16:38:28 2010 +0100
@@ -31,9 +31,9 @@
 
     def test_label(self):
         self.set_option('allow-email-login', 'yes')
-        self.assertEquals(self._login_labels(), ['login or email', 'password'])
+        self.assertEqual(self._login_labels(), ['login or email', 'password'])
         self.set_option('allow-email-login', 'no')
-        self.assertEquals(self._login_labels(), ['login', 'password'])
+        self.assertEqual(self._login_labels(), ['login', 'password'])
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/web/test/unittest_views_baseviews.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_baseviews.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,76 +15,72 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
 
-"""
 from logilab.common.testlib import unittest_main
 from logilab.mtconverter import html_unescape
 
 from cubicweb.devtools.testlib import CubicWebTC
-
+from cubicweb.utils import json
 from cubicweb.web.htmlwidgets import TableWidget
 from cubicweb.web.views import vid_from_rset
-from cubicweb.web import json
-loads = json.loads
 
 def loadjson(value):
-    return loads(html_unescape(value))
+    return json.loads(html_unescape(value))
 
 class VidFromRsetTC(CubicWebTC):
 
     def test_no_rset(self):
         req = self.request()
-        self.assertEquals(vid_from_rset(req, None, self.schema), 'index')
+        self.assertEqual(vid_from_rset(req, None, self.schema), 'index')
 
     def test_no_entity(self):
         req = self.request()
         rset = self.execute('Any X WHERE X login "blabla"')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'noresult')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'noresult')
 
     def test_one_entity(self):
         req = self.request()
         rset = self.execute('Any X WHERE X login "admin"')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'primary')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'primary')
         rset = self.execute('Any X, L WHERE X login "admin", X login L')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'primary')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'primary')
         req.search_state = ('pasnormal',)
         rset = self.execute('Any X WHERE X login "admin"')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'outofcontext-search')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'outofcontext-search')
 
     def test_one_entity_eid(self):
         req = self.request()
         rset = self.execute('Any X WHERE X eid 1')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'primary')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'primary')
 
     def test_more_than_one_entity_same_type(self):
         req = self.request()
         rset = self.execute('Any X WHERE X is CWUser')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'sameetypelist')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'sameetypelist')
         rset = self.execute('Any X, L WHERE X login L')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'sameetypelist')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'sameetypelist')
 
     def test_more_than_one_entity_diff_type(self):
         req = self.request()
         rset = self.execute('Any X WHERE X is IN (CWUser, CWGroup)')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'list')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'list')
 
     def test_more_than_one_entity_by_row(self):
         req = self.request()
         rset = self.execute('Any X, G WHERE X in_group G')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'table')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'table')
 
     def test_more_than_one_entity_by_row_2(self):
         req = self.request()
         rset = self.execute('Any X, GN WHERE X in_group G, G name GN')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'table')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'table')
 
     def test_aggregat(self):
         req = self.request()
         rset = self.execute('Any X, COUNT(T) GROUPBY X WHERE X is T')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'table')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'table')
         rset = self.execute('Any MAX(X) WHERE X is CWUser')
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'table')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'table')
 
     def test_subquery(self):
         rset = self.execute(
@@ -94,7 +90,7 @@
 '       UNION'
 '     (DISTINCT Any W,N WHERE W is CWGroup, W name N))')
         req = self.request()
-        self.assertEquals(vid_from_rset(req, rset, self.schema), 'table')
+        self.assertEqual(vid_from_rset(req, rset, self.schema), 'table')
 
 
 class TableViewTC(CubicWebTC):
@@ -108,7 +104,7 @@
         return e, rset, view
 
     def test_headers(self):
-        self.skip('implement me')
+        self.skipTest('implement me')
 
     def test_sortvalue(self):
         e, _, view = self._prepare_entity()
--- a/web/test/unittest_views_editforms.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_editforms.py	Wed Nov 03 16:38:28 2010 +0100
@@ -50,19 +50,19 @@
     def test_cwuser_relations_by_category(self):
         e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         # see custom configuration in views.cwuser
-        self.assertEquals(rbc(e, 'main', 'attributes'),
+        self.assertEqual(rbc(e, 'main', 'attributes'),
                           [('login', 'subject'),
                            ('upassword', 'subject'),
                            ('firstname', 'subject'),
                            ('surname', 'subject'),
                            ('in_group', 'subject'),
                            ])
-        self.assertListEquals(rbc(e, 'muledit', 'attributes'),
+        self.assertListEqual(rbc(e, 'muledit', 'attributes'),
                               [('login', 'subject'),
                                ('upassword', 'subject'),
                                ('in_group', 'subject'),
                                ])
-        self.assertListEquals(rbc(e, 'main', 'metadata'),
+        self.assertListEqual(rbc(e, 'main', 'metadata'),
                               [('last_login_time', 'subject'),
                                ('modification_date', 'subject'),
                                ('created_by', 'subject'),
@@ -74,18 +74,18 @@
         # XXX skip 'tags' relation here and in the hidden category because
         # of some test interdependancy when pytest is launched on whole cw
         # (appears here while expected in hidden
-        self.assertListEquals([x for x in rbc(e, 'main', 'relations')
+        self.assertListEqual([x for x in rbc(e, 'main', 'relations')
                                if x != ('tags', 'object')],
                               [('primary_email', 'subject'),
                                ('custom_workflow', 'subject'),
                                ('connait', 'subject'),
                                ('checked_by', 'object'),
                                ])
-        self.assertListEquals(rbc(e, 'main', 'inlined'),
+        self.assertListEqual(rbc(e, 'main', 'inlined'),
                               [('use_email', 'subject'),
                                ])
         # owned_by is defined both as subject and object relations on CWUser
-        self.assertListEquals(sorted(x for x in rbc(e, 'main', 'hidden')
+        self.assertListEqual(sorted(x for x in rbc(e, 'main', 'hidden')
                                      if x != ('tags', 'object')),
                               sorted([('for_user', 'object'),
                                       ('created_by', 'object'),
@@ -100,7 +100,7 @@
 
     def test_personne_relations_by_category(self):
         e = self.vreg['etypes'].etype_class('Personne')(self.request())
-        self.assertListEquals(rbc(e, 'main', 'attributes'),
+        self.assertListEqual(rbc(e, 'main', 'attributes'),
                               [('nom', 'subject'),
                                ('prenom', 'subject'),
                                ('sexe', 'subject'),
@@ -115,21 +115,22 @@
                                ('description', 'subject'),
                                ('salary', 'subject'),
                                ])
-        self.assertListEquals(rbc(e, 'muledit', 'attributes'),
+        self.assertListEqual(rbc(e, 'muledit', 'attributes'),
                               [('nom', 'subject'),
                                ])
-        self.assertListEquals(rbc(e, 'main', 'metadata'),
+        self.assertListEqual(rbc(e, 'main', 'metadata'),
                               [('creation_date', 'subject'),
                                ('cwuri', 'subject'),
                                ('modification_date', 'subject'),
                                ('created_by', 'subject'),
                                ('owned_by', 'subject'),
                                ])
-        self.assertListEquals(rbc(e, 'main', 'relations'),
+        self.assertListEqual(rbc(e, 'main', 'relations'),
                               [('travaille', 'subject'),
-                               ('connait', 'object')
+                               ('manager', 'object'),
+                               ('connait', 'object'),
                                ])
-        self.assertListEquals(rbc(e, 'main', 'hidden'),
+        self.assertListEqual(rbc(e, 'main', 'hidden'),
                               [])
 
     def test_edition_form(self):
--- a/web/test/unittest_views_embeding.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_embeding.py	Wed Nov 03 16:38:28 2010 +0100
@@ -46,7 +46,7 @@
         ]
         for orig_a, expected_a in zip(orig, expected):
             got = prefix_links(orig_a, 'PREFIX', 'http://embedded.com/page1.html')
-            self.assertEquals(got, expected_a)
+            self.assertEqual(got, expected_a)
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_views_navigation.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_navigation.py	Wed Nov 03 16:38:28 2010 +0100
@@ -62,10 +62,10 @@
         req = self.request()
         rset = self.execute('Any X,N LIMIT 10 WHERE X name N')
         navcomp = self.vreg['components'].select_or_none('navigation', req, rset=rset)
-        self.assertEquals(navcomp, None)
+        self.assertEqual(navcomp, None)
         req.set_search_state('W:X:Y:Z')
         navcomp = self.vreg['components'].select_or_none('navigation', req, rset=rset)
-        self.assertEquals(navcomp, None)
+        self.assertEqual(navcomp, None)
         req.set_search_state('normal')
 
     def test_navigation_selection_not_enough(self):
--- a/web/test/unittest_views_pyviews.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_pyviews.py	Wed Nov 03 16:38:28 2010 +0100
@@ -25,7 +25,7 @@
                                          pyvalue=[[1, 'a'], [2, 'b']])
         content = view.render(pyvalue=[[1, 'a'], [2, 'b']],
                               headers=['num', 'char'])
-        self.assertEquals(content.strip(), '''<table class="listing">
+        self.assertEqual(content.strip(), '''<table class="listing">
 <thead><tr><th>num</th><th>char</th></tr>
 </thead><tbody><tr><td>1</td><td>a</td></tr>
 <tr><td>2</td><td>b</td></tr>
@@ -35,7 +35,7 @@
         view = self.vreg['views'].select('pyvallist', self.request(),
                                          pyvalue=[1, 'a'])
         content = view.render(pyvalue=[1, 'a'])
-        self.assertEquals(content.strip(), '''<ul>
+        self.assertEqual(content.strip(), '''<ul>
 <li>1</li>
 <li>a</li>
 </ul>''')
--- a/web/test/unittest_views_searchrestriction.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_views_searchrestriction.py	Wed Nov 03 16:38:28 2010 +0100
@@ -46,32 +46,32 @@
                           'B in_group P, P name "managers"')
 
     def test_1(self):
-        self.assertEquals(self._generate(self.select, 'in_state', 'subject', 'name'),
+        self.assertEqual(self._generate(self.select, 'in_state', 'subject', 'name'),
                           "DISTINCT Any A,C ORDERBY C WHERE B in_group P, P name 'managers', "
                           "B in_state A, B is CWUser, A name C")
 
     def test_2(self):
-        self.assertEquals(self._generate(self.select, 'tags', 'object', 'name'),
+        self.assertEqual(self._generate(self.select, 'tags', 'object', 'name'),
                           "DISTINCT Any A,C ORDERBY C WHERE B in_group P, P name 'managers', "
                           "A tags B, B is CWUser, A name C")
 
     def test_3(self):
-        self.assertEquals(self._generate(self.select, 'created_by', 'subject', 'login'),
+        self.assertEqual(self._generate(self.select, 'created_by', 'subject', 'login'),
                           "DISTINCT Any A,C ORDERBY C WHERE B in_group P, P name 'managers', "
                           "B created_by A, B is CWUser, A login C")
 
     def test_4(self):
-        self.assertEquals(self._generate(self.parse('Any X WHERE X is CWUser'), 'created_by', 'subject', 'login'),
+        self.assertEqual(self._generate(self.parse('Any X WHERE X is CWUser'), 'created_by', 'subject', 'login'),
                           "DISTINCT Any A,B ORDERBY B WHERE X is CWUser, X created_by A, A login B")
 
     def test_5(self):
-        self.assertEquals(self._generate(self.parse('Any X,L WHERE X is CWUser, X login L'), 'created_by', 'subject', 'login'),
+        self.assertEqual(self._generate(self.parse('Any X,L WHERE X is CWUser, X login L'), 'created_by', 'subject', 'login'),
                           "DISTINCT Any A,B ORDERBY B WHERE X is CWUser, X created_by A, A login B")
 
     def test_nonregr1(self):
         select = self.parse('Any T,V WHERE T bookmarked_by V?, '
                             'V in_state VS, VS name "published", T created_by U')
-        self.assertEquals(self._generate(select, 'created_by', 'subject', 'login'),
+        self.assertEqual(self._generate(select, 'created_by', 'subject', 'login'),
                           "DISTINCT Any A,B ORDERBY B WHERE T created_by U, "
                           "T created_by A, T is Bookmark, A login B")
 
@@ -83,7 +83,7 @@
         for rdefs in rschema.rdefs.values():
             rdefs.cardinality =  '++'
         try:
-            self.assertEquals(self._generate(select, 'in_state', 'subject', 'name'),
+            self.assertEqual(self._generate(select, 'in_state', 'subject', 'name'),
                               "DISTINCT Any A,B ORDERBY B WHERE V is CWUser, "
                               "NOT EXISTS(V in_state VS), VS name 'published', "
                               "V in_state A, A name B")
@@ -94,7 +94,7 @@
     def test_nonregr3(self):
         #'DISTINCT Any X,TMP,N WHERE P name TMP, X version_of P, P is Project, X is Version, not X in_state S,S name "published", X num N ORDERBY TMP,N'
         select = self.parse('DISTINCT Any X, MAX(Y) GROUPBY X WHERE X is CWUser, Y is Bookmark, X in_group A')
-        self.assertEquals(self._generate(select, 'in_group', 'subject', 'name'),
+        self.assertEqual(self._generate(select, 'in_group', 'subject', 'name'),
                           "DISTINCT Any B,C ORDERBY C WHERE X is CWUser, X in_group B, B name C")
 
 
--- a/web/test/unittest_viewselector.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_viewselector.py	Wed Nov 03 16:38:28 2010 +0100
@@ -22,7 +22,7 @@
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import CW_SOFTWARE_ROOT as BASE, Binary, UnknownProperty
-from cubicweb.selectors import (match_user_groups, implements,
+from cubicweb.selectors import (match_user_groups, is_instance,
                                 specified_etype_implements, rql_condition,
                                 traced_selection)
 from cubicweb.web import NoSelectableObject
@@ -41,7 +41,7 @@
 SITEACTIONS = [actions.SiteConfigurationAction,
                actions.ManageAction,
                schema.ViewSchemaAction,
-               actions.SiteInfoAction]
+               debug.SiteInfoAction]
 FOOTERACTIONS = [wdoc.HelpAction,
                  wdoc.ChangeLogAction,
                  wdoc.AboutAction,
@@ -65,7 +65,7 @@
         except AttributeError:
             return
         if registry == 'hooks':
-            self.assertEquals(len(content), expected, content)
+            self.assertEqual(len(content), expected, content)
             return
         try:
             self.assertSetEqual(content.keys(), expected)
@@ -408,27 +408,37 @@
                               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 primary view priority
+        image = self.request().create_entity('File', data_name=u'bim.png', data=Binary('bim'))
+        # image/ehtml 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'))
-        # image primary view priority
+        self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'ehtml', req, rset=rset)
+
+        fileobj = self.request().create_entity('File', data_name=u'bim.html', data=Binary('<html>bam</html'))
+        # image/ehtml primary view priority
+        req = self.request()
+        rset = req.execute('File X WHERE X data_name "bim.html"')
+        self.assertIsInstance(self.vreg['views'].select('ehtml', req, rset=rset),
+                              idownloadable.EHTMLView)
+        self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'image', req, rset=rset)
+
+        fileobj = self.request().create_entity('File', data_name=u'bim.txt', data=Binary('boum'))
+        # image/ehtml primary view priority
         req = self.request()
         rset = req.execute('File X WHERE X data_name "bim.txt"')
         self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'image', req, rset=rset)
-
+        self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'ehtml', req, rset=rset)
 
 
     def _test_view(self, vid, rql, args):
@@ -457,18 +467,18 @@
 
 
     def test_properties(self):
-        self.assertEquals(sorted(k for k in self.vreg['propertydefs'].keys()
+        self.assertEqual(sorted(k for k in self.vreg['propertydefs'].keys()
                                  if k.startswith('boxes.edit_box')),
                           ['boxes.edit_box.context',
                            'boxes.edit_box.order',
                            'boxes.edit_box.visible'])
-        self.assertEquals([k for k in self.vreg['propertyvalues'].keys()
+        self.assertEqual([k for k in self.vreg['propertyvalues'].keys()
                            if not k.startswith('system.version')],
                           [])
-        self.assertEquals(self.vreg.property_value('boxes.edit_box.visible'), True)
-        self.assertEquals(self.vreg.property_value('boxes.edit_box.order'), 2)
-        self.assertEquals(self.vreg.property_value('boxes.possible_views_box.visible'), False)
-        self.assertEquals(self.vreg.property_value('boxes.possible_views_box.order'), 10)
+        self.assertEqual(self.vreg.property_value('boxes.edit_box.visible'), True)
+        self.assertEqual(self.vreg.property_value('boxes.edit_box.order'), 2)
+        self.assertEqual(self.vreg.property_value('boxes.possible_views_box.visible'), False)
+        self.assertEqual(self.vreg.property_value('boxes.possible_views_box.order'), 10)
         self.assertRaises(UnknownProperty, self.vreg.property_value, 'boxes.actions_box')
 
 
@@ -476,7 +486,7 @@
 
 class CWETypeRQLAction(Action):
     __regid__ = 'testaction'
-    __select__ = implements('CWEType') & rql_condition('X name "CWEType"')
+    __select__ = is_instance('CWEType') & rql_condition('X name "CWEType"')
     title = 'bla'
 
 class RQLActionTC(ViewSelectorTC):
--- a/web/test/unittest_web.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_web.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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.assertEqual(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.assertEqual(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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/test/unittest_webconfig.py	Wed Nov 03 16:38:28 2010 +0100
@@ -33,17 +33,16 @@
     def test_nonregr_print_css_as_list(self):
         """make sure PRINT_CSS *must* is a list"""
         config = self.config
-        req = fake.FakeRequest()
-        print_css = req.external_resource('STYLESHEETS_PRINT')
+        print_css = config.uiprops['STYLESHEETS_PRINT']
         self.failUnless(isinstance(print_css, list))
-        ie_css = req.external_resource('IE_STYLESHEETS')
+        ie_css = config.uiprops['STYLESHEETS_IE']
         self.failUnless(isinstance(ie_css, list))
 
     def test_locate_resource(self):
-        self.failUnless('FILE_ICON' in self.config.ext_resources)
-        rname = self.config.ext_resources['FILE_ICON'].replace('DATADIR/', '')
-        self.failUnless('file' in self.config.locate_resource(rname).split(os.sep))
-        cubicwebcsspath = self.config.locate_resource('cubicweb.css').split(os.sep)
+        self.failUnless('FILE_ICON' in self.config.uiprops)
+        rname = self.config.uiprops['FILE_ICON'].replace(self.config.datadir_url, '')
+        self.failUnless('file' in self.config.locate_resource(rname)[0].split(os.sep))
+        cubicwebcsspath = self.config.locate_resource('cubicweb.css')[0].split(os.sep)
         self.failUnless('web' in cubicwebcsspath or 'shared' in cubicwebcsspath) # 'shared' if tests under apycot
 
 if __name__ == '__main__':
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/windmill/test_connexion.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,41 @@
+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.waits.forPageLoad(timeout=u'20000')
+    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.waits.forPageLoad(timeout=u'20000')
+    client.open(url=u'/')
+    client.waits.forPageLoad(timeout=u'20000')
+    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.waits.forPageLoad(timeout=u'20000')
+    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	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,56 @@
+from cubicweb.devtools import DEFAULT_SOURCES
+LOGIN, PASSWORD = DEFAULT_SOURCES['admin'].values()
+
+# Generated by the windmill services transformer
+from windmill.authoring import WindmillTestClient
+
+def test_creation():
+    client = WindmillTestClient(__name__)
+
+    client.open(url=u'/')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.type(text=LOGIN, id=u'__login')
+    client.type(text=PASSWORD, id=u'__password')
+    client.click(value=u'log in')
+    client.waits.forPageLoad(timeout=u'20000')
+
+    # pre-condition
+    client.open(url=u'/cwuser/myuser')
+    client.asserts.assertJS(js=u'$(\'#contentmain h1\').text() == "this resource does not exist"')
+    client.open(url=u'/?rql=Any U WHERE U is CWUser, U login "myuser"')
+    client.asserts.assertJS(js=u'$(\'.searchMessage strong\').text() == "No result matching query"')
+
+    client.open(url=u'/manage')
+    client.open(url=u'/add/CWUser')
+    client.type(text=u'myuser', id=u'login-subject:A')
+    client.type(text=u'myuser', id=u'upassword-subject:A')
+    client.type(text=u'myuser', name=u'upassword-subject-confirm:A')
+    client.type(text=u'myuser', id=u'firstname-subject:A')
+    client.select(val=u'4', id=u'in_group-subject:A')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.click(id=u'adduse_email:Alink')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.type(text=u'myuser@logilab.fr', id=u'address-subject:B')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertJS(js=u'$(\'.message\').text() == "entity created"')
+    client.open(url=u'/?rql=Any U WHERE U is CWUser, U login "myuser"')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertJS(js=u'$(\'#contentmain h1\').text() == "myuser"')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.open(url=u'/cwuser/myuser?vid=sameetypelist')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.asserts.assertJS(js=u'$(\'#contentmain a\').text() == "myuser"')
+    client.open(url=u'/cwuser/myuser?vid=text')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.asserts.assertJS(js=u'$(\'#contentmain\').text() == "\\nmyuser"')
+    client.open(url=u'/cwuser/myuser?vid=deleteconf')
+    client.waits.forElement(timeout=u'8000', value=u'button_delete')
+    client.click(value=u'button_delete')
+    client.waits.forPageLoad(timeout=u'8000')
+    client.open(url=u'/cwuser/myuser')
+    client.asserts.assertJS(js=u'$(\'#contentmain h1\').text() == "this resource does not exist"')
+    client.open(url=u'/?rql=Any U WHERE U is CWUser, U login "myuser"')
+    client.asserts.assertJS(js=u'$(\'.searchMessage strong\').text() == "No result matching query"')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/windmill/test_edit_relation.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,82 @@
+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_edit_relation():
+    client = WindmillTestClient(__name__)
+
+    client.open(url=u'/logout')
+    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.open(url=u'/add/Folder')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(timeout=u'8000', id=u'name-subject:A')
+    client.click(id=u'name-subject:A')
+    client.type(text=u'folder1', id=u'name-subject:A')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(link=u'add add Folder filed_under Folder object', timeout=u'8000')
+    client.click(link=u'add add Folder filed_under Folder object')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(timeout=u'8000', id=u'name-subject:A')
+    client.click(id=u'name-subject:A')
+    client.type(text=u'subfolder1', id=u'name-subject:A')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(link=u'more actions', timeout=u'8000')
+    client.click(link=u'more actions')
+    client.click(link=u'copy')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.type(text=u'folder2', id=u'name-subject:A')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(link=u'modify', timeout=u'8000')
+    client.click(link=u'modify')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(timeout=u'8000', id=u'footer')
+    client.click(link=u'x')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(link=u'add add Folder filed_under Folder object', timeout=u'8000')
+    client.click(link=u'add add Folder filed_under Folder object')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.type(text=u'subfolder2', id=u'name-subject:A')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(link=u'subfolder2', timeout=u'8000')
+    client.click(link=u'subfolder2')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(link=u'modify', timeout=u'8000')
+    client.click(link=u'modify')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(timeout=u'8000', id=u'footer')
+    client.click(link=u'x')
+    client.select(xpath=u'//select', index=u'1')
+    #client.execJQuery(jquery=u'("select").trigger(\'change\')') # BUGGY freeze UI..
+    client.execJS(js=u'$("select").trigger(\'change\')')
+    client.waits.sleep(milliseconds=u'2000')
+    client.select(jquery=u'(\'select:contains("Search")\')[0]', option=u'Search for folder')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.click(link=u'folder1')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.waits.forElement(timeout=u'8000', value=u'button_ok')
+    client.click(value=u'button_ok')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertText(xpath=u'//h1', validator=u'subfolder2')
+    client.waits.forElement(link=u'folder_plural', timeout=u'8000')
+    client.click(link=u'folder_plural')
+    client.waits.forPageLoad(timeout=u'20000')
+    client.asserts.assertText(jquery=u"('#contentmain div a')[0]", validator=u'folder1')
+    client.asserts.assertText(jquery=u"('#contentmain div a')[1]", validator=u'folder2')
+    client.asserts.assertText(jquery=u"('#contentmain div a')[2]", validator=u'subfolder1')
+    client.asserts.assertText(jquery=u"('#contentmain div a')[3]", validator=u'subfolder2')
+    client.click(link=u'subfolder2')
+    client.click(link=u'modify')
+    client.click(link=u'folder1')
--- a/web/uicfg.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/uicfg.py	Wed Nov 03 16:38:28 2010 +0100
@@ -42,8 +42,6 @@
    # Adds all subjects of the entry_of relation in the add menu of the ``Blog``
    # primary view
    uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'entry_of', 'Blog'), True)
-
-
 """
 __docformat__ = "restructuredtext en"
 
@@ -53,7 +51,8 @@
 
 from cubicweb import neg_role
 from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
-                            RelationTagsDict, register_rtag, _ensure_str_key)
+                            RelationTagsDict, NoTargetRelationTagsDict,
+                            register_rtag, _ensure_str_key)
 from cubicweb.schema import META_RTYPES
 
 
@@ -83,35 +82,16 @@
                                               'sideboxes', 'hidden')))
 
 
-class DisplayCtrlRelationTags(RelationTagsDict):
+class DisplayCtrlRelationTags(NoTargetRelationTagsDict):
     def __init__(self, *args, **kwargs):
         super(DisplayCtrlRelationTags, self).__init__(*args, **kwargs)
         self.counter = 0
 
-    def tag_subject_of(self, key, tag):
-        subj, rtype, obj = key
-        if obj != '*':
-            self.warning('using explict target type in display_ctrl.tag_subject_of() '
-                         'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
-                         subj, rtype, subj, rtype, obj)
-        super(DisplayCtrlRelationTags, self).tag_subject_of((subj, rtype, '*'), tag)
-
-    def tag_object_of(self, key, tag):
-        subj, rtype, obj = key
-        if subj != '*':
-            self.warning('using explict subject type in display_ctrl.tag_object_of() '
-                         'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
-                         rtype, obj, subj, rtype, obj)
-        super(DisplayCtrlRelationTags, self).tag_object_of(('*', rtype, obj), tag)
-
 def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role):
     if role == 'subject':
         oschema = '*'
-        label = rschema.type
     else:
         sschema = '*'
-        label = '%s_%s' % (rschema, role)
-    rtag.setdefault((sschema, rschema, oschema, role), 'label', label)
     rtag.counter += 1
     rtag.setdefault((sschema, rschema, oschema, role), 'order', rtag.counter)
 
@@ -218,6 +198,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 (
@@ -375,7 +361,7 @@
 autoform_field = RelationTags('autoform_field')
 
 # relations'field explicit kwargs (given to field's __init__)
-autoform_field_kwargs = RelationTagsDict()
+autoform_field_kwargs = RelationTagsDict('autoform_field_kwargs')
 
 
 # set of tags of the form <action>_on_new on relations. <action> is a
@@ -383,6 +369,66 @@
 # permissions checking is by-passed and supposed to be ok
 autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides')
 
+class ReleditTags(NoTargetRelationTagsDict):
+    """Associate to relation a dictionnary to control `reledit` (e.g. edition of
+    attributes / relations from within views).
+
+    Possible keys and associated values are:
+
+    * `novalue_label`, alternative default value (shown when there is no value).
+
+    * `novalue_include_rtype`, when `novalue_label` is not specified, this boolean
+      flag control wether the generated default value should contains the
+      relation label or not. Will be the opposite of the `showlabel` value found
+      in the `primaryview_display_ctrl` rtag by default.
+
+    * `reload`, boolean, eid (to reload to) or function taking subject and
+      returning bool/eid. This is useful when editing a relation (or attribute)
+      that impacts the url or another parts of the current displayed
+      page. Defaults to False.
+
+    * `rvid`, alternative view id (as str) for relation or composite edition.
+      Default is 'autolimited'.
+
+    * `edit_target`, may be either 'rtype' (to edit the relation) or 'related'
+      (to edit the related entity).  This controls whether to edit the relation
+      or the target entity of the relation.  Currently only one-to-one relations
+      support target entity edition. By default, the 'related' option is taken
+      whenever the relation is composite.
+    """
+    _keys = frozenset('novalue_label novalue_include_rtype reload rvid edit_target'.split())
+
+    def tag_relation(self, key, tag):
+        for tagkey in tag.iterkeys():
+            assert tagkey in self._keys, 'tag %r not in accepted tags: %r' % (tag, self._keys)
+        return super(ReleditTags, self).tag_relation(key, tag)
+
+def init_reledit_ctrl(rtag, sschema, rschema, oschema, role):
+    if rschema.final:
+        return
+    composite = rschema.rdef(sschema, oschema).composite == role
+    if role == 'subject':
+        oschema = '*'
+    else:
+        sschema = '*'
+    values = rtag.get(sschema, rschema, oschema, role)
+    edittarget = values.get('edit_target')
+    if edittarget not in (None, 'rtype', 'related'):
+        rtag.warning('reledit: wrong value for edit_target on relation %s: %s',
+                     rschema, edittarget)
+        edittarget = None
+    if not edittarget:
+        edittarget = 'related' if composite else 'rtype'
+        rtag.tag_relation((sschema, rschema, oschema, role),
+                          {'edit_target': edittarget})
+    if not 'novalue_include_rtype' in values:
+        showlabel = primaryview_display_ctrl.get(
+            sschema, rschema, oschema, role).get('showlabel', True)
+        rtag.tag_relation((sschema, rschema, oschema, role),
+                          {'novalue_include_rtype': not showlabel})
+
+reledit_ctrl = ReleditTags('reledit', init_reledit_ctrl)
+
 # boxes.EditBox configuration #################################################
 
 # 'link' / 'create' relation tags, used to control the "add entity" submenu
--- a/web/views/__init__.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/__init__.py	Wed Nov 03 16:38:28 2010 +0100
@@ -84,7 +84,7 @@
             return VID_BY_MIMETYPE[mimetype]
     nb_rows = len(rset)
     # empty resultset
-    if nb_rows == 0 :
+    if nb_rows == 0:
         return 'noresult'
     # entity result set
     if not schema.eschema(rset.description[0][0]).final:
--- a/web/views/actions.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/actions.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,21 +15,22 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Set of HTML base actions
+"""Set of HTML base actions"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from warnings import warn
 
+from logilab.mtconverter import xml_escape
+
 from cubicweb.schema import display_name
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import (EntitySelector, yes,
     one_line_rset, multi_lines_rset, one_etype_rset, relation_possible,
     nonempty_rset, non_final_entity,
     authenticated_user, match_user_groups, match_search_state,
-    has_permission, has_add_permission, implements,
+    has_permission, has_add_permission, is_instance, debug_mode,
     )
 from cubicweb.web import uicfg, controller, action
 from cubicweb.web.views import linksearch_select_url, vid_from_rset
@@ -56,8 +57,7 @@
                  'optional argument', DeprecationWarning)
             editableattrs = form.editable_attributes()
         for rschema, role in editableattrs:
-            if not rschema.final:
-                return 1
+            return 1
         return 0
 
 @objectify_selector
@@ -141,8 +141,8 @@
 
 class ModifyAction(action.Action):
     __regid__ = 'edit'
-    __select__ = (action.Action.__select__ & one_line_rset() &
-                  (has_permission('update') | has_editable_relation('add')))
+    __select__ = (action.Action.__select__
+                  & one_line_rset() & has_editable_relation('add'))
 
     title = _('modify')
     category = 'mainactions'
@@ -322,7 +322,7 @@
     """when displaying the schema of a CWEType, offer to list entities of that type
     """
     __regid__ = 'entitiesoftype'
-    __select__ = one_line_rset() & implements('CWEType')
+    __select__ = one_line_rset() & is_instance('CWEType')
     category = 'mainactions'
     order = 40
 
@@ -355,7 +355,7 @@
     __regid__ = 'myinfos'
     __select__ = authenticated_user()
 
-    title = _('personnal informations')
+    title = _('profile')
     category = 'useractions'
     order = 20
 
@@ -398,12 +398,6 @@
     title = _('manage')
     order = 20
 
-class SiteInfoAction(ManagersAction):
-    __regid__ = 'siteinfo'
-    __select__ = match_user_groups('users','managers')
-    title = _('info')
-    order = 30
-
 
 # footer actions ###############################################################
 
@@ -418,6 +412,20 @@
     def url(self):
         return 'http://www.cubicweb.org'
 
+class GotRhythmAction(action.Action):
+    __regid__ = 'rhythm'
+    __select__ = debug_mode()
+
+    category = 'footer'
+    order = 3
+    title = _('Got rhythm?')
+
+    def url(self):
+        return xml_escape(self._cw.url()+'#')
+
+    def html_class(self):
+        self._cw.add_js('cubicweb.rhythm.js')
+        return 'rhythm'
 
 ## default actions ui configuration ###########################################
 
--- a/web/views/authentication.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/authentication.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""user authentication component
+"""user authentication component"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
--- a/web/views/autoform.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/autoform.py	Wed Nov 03 16:38:28 2010 +0100
@@ -134,8 +134,9 @@
 from cubicweb.selectors import (
     match_kwargs, match_form_params, non_final_entity,
     specified_etype_implements)
-from cubicweb.web import stdmsgs, uicfg, eid_param, dumps, \
-     form as f, formwidgets as fw, formfields as ff
+from cubicweb.utils import json_dumps
+from cubicweb.web import (stdmsgs, uicfg, eid_param,
+                          form as f, formwidgets as fw, formfields as ff)
 from cubicweb.web.views import forms
 
 _AFS = uicfg.autoform_section
@@ -374,7 +375,7 @@
     entities
     """
     js = u"javascript: togglePendingDelete('%s', %s);" % (
-        nodeid, xml_escape(dumps(eid)))
+        nodeid, xml_escape(json_dumps(eid)))
     return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (
         js, nodeid, label)
 
@@ -442,7 +443,8 @@
         for rschema, role, related in field.relations_table(form):
             # already linked entities
             if related:
-                w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, role))
+                label = rschema.display_name(req, role, context=form.edited_entity.__regid__)
+                w(u'<tr><th class="labelCol">%s</th>' % label)
                 w(u'<td>')
                 w(u'<ul>')
                 for viewparams in related:
@@ -475,7 +477,7 @@
         w(u'<th class="labelCol">')
         w(u'<select id="relationSelector_%s" tabindex="%s" '
           'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
-          % (eid, req.next_tabindex(), xml_escape(dumps(eid))))
+          % (eid, req.next_tabindex(), xml_escape(json_dumps(eid))))
         w(u'<option value="">%s</option>' % _('select a relation'))
         for i18nrtype, rschema, role in field.relations:
             # more entities to link to
@@ -599,7 +601,7 @@
   </select>
 </div>
 """ % (hidden and 'hidden' or '', divid, selectid,
-       xml_escape(dumps(entity.eid)), is_cell and 'true' or 'null', relname,
+       xml_escape(json_dumps(entity.eid)), is_cell and 'true' or 'null', relname,
        '\n'.join(options))
 
     def _get_select_options(self, entity, rschema, role):
@@ -783,7 +785,7 @@
         """return a list of (relation schema, role) to edit for the entity"""
         if self.display_fields is not None:
             return self.display_fields
-        if self.edited_entity.has_eid() and not self.edited_entity.has_perm('update'):
+        if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
             return []
         # XXX we should simply put eid in the generated section, no?
         return [(rtype, role) for rtype, _, role in self._relations_by_section(
@@ -886,7 +888,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,
@@ -949,7 +951,12 @@
     global etype_relation_field
 
     def etype_relation_field(etype, rtype, role='subject'):
-        eschema = vreg.schema.eschema(etype)
-        return AutomaticEntityForm.field_by_name(rtype, role, eschema)
+        try:
+            eschema = vreg.schema.eschema(etype)
+            return AutomaticEntityForm.field_by_name(rtype, role, eschema)
+        except (KeyError, f.FieldNotFound):
+            # catch KeyError raised when etype/rtype not found in schema
+            AutomaticEntityForm.error('field for %s %s may not be found in schema' % (rtype, role))
+            return None
 
     vreg.register_all(globals().values(), __name__)
--- a/web/views/basecomponents.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/basecomponents.py	Wed Nov 03 16:38:28 2010 +0100
@@ -19,8 +19,8 @@
 
 * the rql input form
 * the logged user link
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -59,11 +59,9 @@
           <form action="%s">
 <fieldset>
 <input type="text" id="rql" name="rql" value="%s"  title="%s" tabindex="%s" accesskey="q" class="searchField" />
-<input type="submit" value="" class="rqlsubmit" tabindex="%s" />
 </fieldset>
 ''' % (not self.cw_propval('visible') and 'hidden' or '',
-       self._cw.build_url('view'), xml_escape(rql), req._('full text or RQL query'), req.next_tabindex(),
-        req.next_tabindex()))
+       self._cw.build_url('view'), xml_escape(rql), req._('full text or RQL query'), req.next_tabindex()))
         if self._cw.search_state[0] != 'normal':
             self.w(u'<input type="hidden" name="__mode" value="%s"/>'
                    % ':'.join(req.search_state[1]))
@@ -78,8 +76,8 @@
     site_wide = True
 
     def call(self):
-        self.w(u'<a href="%s"><img class="logo" src="%s" alt="logo"/></a>'
-               % (self._cw.base_url(), self._cw.external_resource('LOGO')))
+        self.w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
+               % (self._cw.base_url(), self._cw.uiprops['LOGO']))
 
 
 class ApplHelp(component.Component):
--- a/web/views/basecontrollers.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/basecontrollers.py	Wed Nov 03 16:38:28 2010 +0100
@@ -22,17 +22,14 @@
 
 __docformat__ = "restructuredtext en"
 
-from smtplib import SMTP
-
-from logilab.common.decorators import cached
 from logilab.common.date import strptime
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, typed_eid)
-from cubicweb.utils import CubicWebJsonEncoder
+from cubicweb.utils import json, json_dumps
 from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
 from cubicweb.mail import format_mail
-from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, json_dumps, json
+from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse
 from cubicweb.web.controller import Controller
 from cubicweb.web.views import vid_from_rset, formrenderers
 
@@ -44,7 +41,7 @@
     HAS_SEARCH_RESTRICTION = False
 
 def jsonize(func):
-    """decorator to sets correct content_type and calls `json.dumps` on
+    """decorator to sets correct content_type and calls `json_dumps` on
     results
     """
     def wrapper(self, *args, **kwargs):
@@ -130,7 +127,7 @@
         if rset is None and not hasattr(req, '_rql_processed'):
             req._rql_processed = True
             if req.cnx:
-                rset = self.process_rql(req.form.get('rql'))
+                rset = self.process_rql()
             else:
                 rset = None
         if rset and rset.rowcount == 1 and '__method' in req.form:
@@ -157,13 +154,12 @@
             else:
                 req.set_message(req._("You have no access to this view or it can not "
                                       "be used to display the current data."))
-            self.warning("the view %s can not be applied to this query", vid)
             vid = req.form.get('fallbackvid') or vid_from_rset(req, rset, req.vreg.schema)
             view = req.vreg['views'].select(vid, req, rset=rset)
         return view, rset
 
     def add_to_breadcrumbs(self, view):
-        # update breadcrumps **before** validating cache, unless the view
+        # update breadcrumbs **before** validating cache, unless the view
         # specifies explicitly it should not be added to breadcrumb or the
         # view is a binary view
         if view.add_to_breadcrumbs and not view.binary:
@@ -224,7 +220,7 @@
         except Exception, ex:
             req.cnx.rollback()
             req.exception('unexpected error while validating form')
-            return (False, req._(str(ex).decode('utf-8')), ctrl._edited_entity)
+            return (False, str(ex).decode('utf-8'), ctrl._edited_entity)
         else:
             # complete entity: it can be used in js callbacks where we might
             # want every possible information
@@ -234,7 +230,7 @@
     except Exception, ex:
         req.cnx.rollback()
         req.exception('unexpected error while validating form')
-        return (False, req._(str(ex).decode('utf-8')), ctrl._edited_entity)
+        return (False, str(ex).decode('utf-8'), ctrl._edited_entity)
     return (False, '???', None)
 
 
@@ -246,7 +242,7 @@
         errback = str(self._cw.form.get('__onfailure', 'null'))
         cbargs = str(self._cw.form.get('__cbargs', 'null'))
         self._cw.set_content_type('text/html')
-        jsargs = json.dumps((status, args, entity), cls=CubicWebJsonEncoder)
+        jsargs = json_dumps((status, args, entity))
         return """<script type="text/javascript">
  window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);
 </script>""" %  (domid, callback, errback, jsargs, cbargs)
@@ -260,6 +256,11 @@
             self._cw.encoding)
         return self.response(domid, status, args, entity)
 
+def optional_kwargs(extraargs):
+    if extraargs is None:
+        return {}
+    # we receive unicode keys which is not supported by the **syntax
+    return dict((str(key), value) for key, value in extraargs.iteritems())
 
 class JSonController(Controller):
     __regid__ = 'json'
@@ -288,14 +289,15 @@
         try:
             args = [json.loads(arg) for arg in args]
         except ValueError, exc:
-            self.exception('error while decoding json arguments for js_%s: %s', fname, args, exc)
+            self.exception('error while decoding json arguments for js_%s: %s',
+                           fname, args, exc)
             raise RemoteCallFailed(repr(exc))
         try:
             result = func(*args)
         except (RemoteCallFailed, DirectResponse):
             raise
         except Exception, ex:
-            self.exception('an exception occured while calling js_%s(%s): %s',
+            self.exception('an exception occurred while calling js_%s(%s): %s',
                            fname, args, ex)
             raise RemoteCallFailed(repr(ex))
         if result is None:
@@ -330,6 +332,9 @@
 
     def _exec(self, rql, args=None, rocheck=True):
         """json mode: execute RQL and return resultset as json"""
+        rql = rql.strip()
+        if rql.startswith('rql:'):
+            rql = rql[4:]
         if rocheck:
             self._cw.ensure_ro_rql(rql)
         try:
@@ -339,28 +344,30 @@
             return None
         return None
 
-    def _call_view(self, view, **kwargs):
-        req = self._cw
-        divid = req.form.get('divid', 'pageContent')
-        # we need to call pagination before with the stream set
+    def _call_view(self, view, paginate=False, **kwargs):
+        # set stream first, in case we need to call pagination
         stream = view.set_stream()
-        if req.form.get('paginate'):
-            if divid == 'pageContent':
-                # mimick main template behaviour
-                stream.write(u'<div id="pageContent">')
-                vtitle = self._cw.form.get('vtitle')
-                if vtitle:
-                    stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+        divid = self._cw.form.get('divid')
+        if divid == 'pageContent':
+            # ensure divid isn't reused by the view (e.g. table view)
+            del self._cw.form['divid']
+            # mimick main template behaviour
+            stream.write(u'<div id="pageContent">')
+            vtitle = self._cw.form.get('vtitle')
+            if vtitle:
+                stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+            paginate = True
+        if paginate:
             view.paginate()
-            if divid == 'pageContent':
-                stream.write(u'<div id="contentmain">')
+        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 divid == 'pageContent':
             stream.write(u'</div></div>')
         return stream.getvalue()
 
@@ -371,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)
@@ -380,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.pop('paginate', False))
 
     @xhtmlize
     def js_prop_widget(self, propkey, varname, tabindex=None):
@@ -401,33 +410,32 @@
             rset = self._exec(rql)
         else:
             rset = None
-        if extraargs is None:
-            extraargs = {}
-        else: # we receive unicode keys which is not supported by the **syntax
-            extraargs = dict((str(key), value)
-                             for key, value in extraargs.items())
         # XXX while it sounds good, addition of the try/except below cause pb:
         # when filtering using facets return an empty rset, the edition box
         # isn't anymore selectable, as expected. The pb is that with the
-        # try/except below, we see a "an error occured" message in the ui, while
+        # try/except below, we see a "an error occurred" message in the ui, while
         # we don't see it without it. Proper fix would probably be to deal with
         # this by allowing facet handling code to tell to js_component that such
         # error is expected and should'nt be reported.
         #try:
         comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
-                                              **extraargs)
+                                              **optional_kwargs(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 self._cw.form.get('rql'):
+            rset = self._cw.execute(self._cw.form['rql'])
+        else:
+            rset = None
+        view = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
+                                              **optional_kwargs(selectargs))
+        return self._call_view(view, **optional_kwargs(renderargs))
 
     @check_pageid
     @xhtmlize
@@ -448,23 +456,15 @@
     @xhtmlize
     def js_reledit_form(self):
         req = self._cw
-        args = dict((x, self._cw.form[x])
-                    for x in frozenset(('rtype', 'role', 'reload', 'landing_zone')))
-        entity = self._cw.entity_from_eid(int(self._cw.form['eid']))
-        # note: default is reserved in js land
-        args['default'] = self._cw.form['default_value']
-        args['reload'] = json.loads(args['reload'])
-        rset = req.eid_rset(int(self._cw.form['eid']))
+        args = dict((x, req.form[x])
+                    for x in ('formid', 'rtype', 'role', 'reload'))
+        rset = req.eid_rset(typed_eid(self._cw.form['eid']))
+        try:
+            args['reload'] = json.loads(args['reload'])
+        except ValueError: # not true/false, an absolute url
+            assert args['reload'].startswith('http')
         view = req.vreg['views'].select('doreledit', req, rset=rset, rtype=args['rtype'])
-        stream = view.set_stream()
-        view.render(**args)
-        # XXX why not _call_view ?
-        extresources = req.html_headers.getvalue(skiphead=True)
-        if extresources:
-            stream.write(u'<div class="ajaxHtmlHead">\n')
-            stream.write(extresources)
-            stream.write(u'</div>\n')
-        return stream.getvalue()
+        return self._call_view(view, **args)
 
     @jsonize
     def js_i18n(self, msgids):
@@ -480,7 +480,7 @@
     @jsonize
     def js_external_resource(self, resource):
         """returns the URL of the external resource named `resource`"""
-        return self._cw.external_resource(resource)
+        return self._cw.uiprops[resource]
 
     @check_pageid
     @jsonize
@@ -580,52 +580,10 @@
     def js_add_pending_delete(self, (eidfrom, rel, eidto)):
         self._add_pending(eidfrom, rel, eidto, 'delete')
 
-    # XXX specific code. Kill me and my AddComboBox friend
-    @jsonize
-    def js_add_and_link_new_entity(self, etype_to, rel, eid_to, etype_from, value_from):
-        # create a new entity
-        eid_from = self._cw.execute('INSERT %s T : T name "%s"' % ( etype_from, value_from ))[0][0]
-        # link the new entity to the main entity
-        rql = 'SET F %(rel)s T WHERE F eid %(eid_to)s, T eid %(eid_from)s' % {'rel' : rel, 'eid_to' : eid_to, 'eid_from' : eid_from}
-        return eid_from
 
 # XXX move to massmailing
-class SendMailController(Controller):
-    __regid__ = 'sendmail'
-    __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
 
-    def recipients(self):
-        """returns an iterator on email's recipients as entities"""
-        eids = self._cw.form['recipient']
-        # eids may be a string if only one recipient was specified
-        if isinstance(eids, basestring):
-            rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
-        else:
-            rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
-        return rset.entities()
-
-    def sendmail(self, recipient, subject, body):
-        msg = format_mail({'email' : self._cw.user.get_email(),
-                           'name' : self._cw.user.dc_title(),},
-                          [recipient], body, subject)
-        if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
-            msg = self._cw._('could not connect to the SMTP server')
-            url = self._cw.build_url(__message=msg)
-            raise Redirect(url)
-
-    def publish(self, rset=None):
-        # XXX this allows users with access to an cubicweb instance to use it as
-        # a mail relay
-        body = self._cw.form['mailbody']
-        subject = self._cw.form['subject']
-        for recipient in self.recipients():
-            text = body % recipient.as_email_context()
-            self.sendmail(recipient.get_email(), subject, text)
-        url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
-        raise Redirect(url)
-
-
-class MailBugReportController(SendMailController):
+class MailBugReportController(Controller):
     __regid__ = 'reportbug'
     __select__ = match_form_params('description')
 
@@ -636,7 +594,7 @@
         raise Redirect(url)
 
 
-class UndoController(SendMailController):
+class UndoController(Controller):
     __regid__ = 'undo'
     __select__ = authenticated_user() & match_form_params('txuuid')
 
@@ -644,7 +602,7 @@
         txuuid = self._cw.form['txuuid']
         errors = self._cw.cnx.undo_transaction(txuuid)
         if errors:
-            self.w(self._cw._('some errors occured:'))
+            self.w(self._cw._('some errors occurred:'))
             self.wview('pyvalist', pyvalue=errors)
         else:
             self.redirect()
--- a/web/views/basetemplates.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/basetemplates.py	Wed Nov 03 16:38:28 2010 +0100
@@ -91,6 +91,7 @@
         return 0
     return view.templatable
 
+
 class NonTemplatableViewTemplate(MainTemplate):
     """main template for any non templatable views (xml, binaries, etc.)"""
     __regid__ = 'main-template'
@@ -132,14 +133,14 @@
             'etypenavigation', self._cw, rset=self.cw_rset)
         if etypefilter and etypefilter.cw_propval('visible'):
             etypefilter.render(w=w)
-        self.nav_html = UStringIO()
+        nav_html = UStringIO()
         if view:
-            view.paginate(w=self.nav_html.write)
-        w(_(self.nav_html.getvalue()))
+            view.paginate(w=nav_html.write)
+        w(nav_html.getvalue())
         w(u'<div id="contentmain">\n')
         view.render(w=w)
         w(u'</div>\n') # close id=contentmain
-        w(_(self.nav_html.getvalue()))
+        w(nav_html.getvalue())
         w(u'</div>\n') # closes id=pageContent
         self.template_footer(view)
 
@@ -168,7 +169,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 +191,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')
@@ -204,7 +205,7 @@
 
 
 class ErrorTemplate(TheMainTemplate):
-    """fallback template if an internal error occured during displaying the main
+    """fallback template if an internal error occurred during displaying the main
     template. This template may be called for authentication error, which means
     that req.cnx and req.user may not be set.
     """
@@ -215,7 +216,7 @@
         self.set_request_content_type()
         self._cw.reset_headers()
         view = self._cw.vreg['views'].select('error', self._cw, rset=self.cw_rset)
-        self.template_header(self.content_type, view, self._cw._('an error occured'),
+        self.template_header(self.content_type, view, self._cw._('an error occurred'),
                              [NOINDEX, NOFOLLOW])
         view.render(w=self.w)
         self.template_footer(view)
@@ -254,7 +255,7 @@
         w(u'<body>\n')
         w(u'<div id="page">')
         w(u'<table width="100%" height="100%" border="0"><tr>\n')
-        w(u'<td class="navcol">\n')
+        w(u'<td id="navColumnLeft">\n')
         self.topleft_header()
         boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context='left'))
@@ -272,7 +273,7 @@
 
     def topleft_header(self):
         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
-                                                      rset=self.cw_rset)
+                                                          rset=self.cw_rset)
         if logo and logo.cw_propval('visible'):
             self.w(u'<table id="header"><tr>\n')
             self.w(u'<td>')
@@ -294,22 +295,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):
@@ -327,12 +328,9 @@
 
     def call(self, view, **kwargs):
         self.main_header(view)
-        self.w(u'''
-  <div id="stateheader">''')
+        self.w(u'<div id="stateheader">')
         self.state_header()
-        self.w(u'''
-  </div>
-  ''')
+        self.w(u'</div>')
 
     def main_header(self, view):
         """build the top menu with authentification info and the rql box"""
@@ -389,13 +387,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,11 +469,16 @@
             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)
-        if self._cw.vreg.config['auth-mode'] != 'http':
-            # Cookie authentication
-            self.login_form(id)
-        self.w(u'</div></div>\n')
+            self.w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
+        config = self._cw.vreg.config
+        if config['auth-mode'] != 'http':
+            self.login_form(id) # Cookie authentication
+        self.w(u'</div>')
+        if self._cw.https and config.anonymous_user()[0]:
+            path = xml_escape(config['base-url'] + self._cw.relative_path())
+            self.w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
+                   % (path, self._cw._('No account? Try public access at %s') % path))
+        self.w(u'</div>\n')
 
     def login_form(self, id):
         cw = self._cw
@@ -488,6 +493,7 @@
 
 LogFormTemplate = class_renamed('LogFormTemplate', LogFormView)
 
+
 def login_form_url(req):
     if req.https:
         return req.url()
--- a/web/views/bookmark.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/bookmark.py	Wed Nov 03 16:38:28 2010 +0100
@@ -23,7 +23,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import Unauthorized
-from cubicweb.selectors import implements, one_line_rset
+from cubicweb.selectors import is_instance, one_line_rset
 from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, RawBoxItem
 from cubicweb.web import action, box, uicfg, formwidgets as fw
 from cubicweb.web.views import primary
@@ -43,7 +43,7 @@
 
 class FollowAction(action.Action):
     __regid__ = 'follow'
-    __select__ = one_line_rset() & implements('Bookmark')
+    __select__ = one_line_rset() & is_instance('Bookmark')
 
     title = _('follow')
     category = 'mainactions'
@@ -53,7 +53,7 @@
 
 
 class BookmarkPrimaryView(primary.PrimaryView):
-    __select__ = implements('Bookmark')
+    __select__ = is_instance('Bookmark')
 
     def cell_call(self, row, col):
         """the primary view for bookmark entity"""
@@ -96,7 +96,7 @@
         eschema = self._cw.vreg.schema.eschema(self.etype)
         candelete = rschema.has_perm(req, 'delete', toeid=ueid)
         if candelete:
-            req.add_js( ('cubicweb.ajax.js', 'cubicweb.bookmarks.js') )
+            req.add_js('cubicweb.ajax.js')
         else:
             dlink = None
         for bookmark in rset.entities():
--- a/web/views/boxes.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/boxes.py	Wed Nov 03 16:38:28 2010 +0100
@@ -198,6 +198,9 @@
         """display a list of entities by calling their <item_vid> view"""
         if title:
             self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title)
+        if 'dispctrl' in self.cw_extra_kwargs:
+            # XXX do not modify dispctrl!
+            self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext')
         self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass)
         self.wview('autolimited', self.cw_rset, **self.cw_extra_kwargs)
         self.w(u'</div>\n</div>\n')
--- a/web/views/calendar.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/calendar.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,20 +15,36 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
+"""html calendar views"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from datetime import datetime, date, timedelta
 
 from logilab.mtconverter import xml_escape
-from logilab.common.date import strptime, date_range, todate, todatetime
+from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
 
 from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+
+class ICalendarableAdapter(EntityAdapter):
+    __regid__ = 'ICalendarable'
+    __select__ = implements(ICalendarable, warn=False) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('ICalendarable')
+    def start(self):
+        """return start date"""
+        raise NotImplementedError
+
+    @property
+    @implements_adapter_compat('ICalendarable')
+    def stop(self):
+        """return stop state"""
+        raise NotImplementedError
 
 
 # useful constants & functions ################################################
@@ -52,7 +68,7 @@
 
         Does apply to ICalendarable compatible entities
         """
-        __select__ = implements(ICalendarable)
+        __select__ = adaptable('ICalendarable')
         paginable = False
         content_type = 'text/calendar'
         title = _('iCalendar')
@@ -66,10 +82,11 @@
                 event = ical.add('vevent')
                 event.add('summary').value = task.dc_title()
                 event.add('description').value = task.dc_description()
-                if task.start:
-                    event.add('dtstart').value = task.start
-                if task.stop:
-                    event.add('dtend').value = task.stop
+                icalendarable = task.cw_adapt_to('ICalendarable')
+                if icalendarable.start:
+                    event.add('dtstart').value = icalendarable.start
+                if icalendarable.stop:
+                    event.add('dtend').value = icalendarable.stop
 
             buff = ical.serialize()
             if not isinstance(buff, unicode):
@@ -85,7 +102,7 @@
     Does apply to ICalendarable compatible entities
     """
     __regid__ = 'hcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
     paginable = False
     title = _('hCalendar')
     #templatable = False
@@ -98,10 +115,15 @@
             self.w(u'<h3 class="summary">%s</h3>' % xml_escape(task.dc_title()))
             self.w(u'<div class="description">%s</div>'
                    % task.dc_description(format='text/html'))
-            if task.start:
-                self.w(u'<abbr class="dtstart" title="%s">%s</abbr>' % (task.start.isoformat(), self._cw.format_date(task.start)))
-            if task.stop:
-                self.w(u'<abbr class="dtstop" title="%s">%s</abbr>' % (task.stop.isoformat(), self._cw.format_date(task.stop)))
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            if icalendarable.start:
+                self.w(u'<abbr class="dtstart" title="%s">%s</abbr>'
+                       % (icalendarable.start.isoformat(),
+                          self._cw.format_date(icalendarable.start)))
+            if icalendarable.stop:
+                self.w(u'<abbr class="dtstop" title="%s">%s</abbr>'
+                       % (icalendarable.stop.isoformat(),
+                          self._cw.format_date(icalendarable.stop)))
             self.w(u'</div>')
         self.w(u'</div>')
 
@@ -113,10 +135,15 @@
         task = self.cw_rset.complete_entity(row, 0)
         task.view('oneline', w=self.w)
         if dates:
-            if task.start and task.stop:
-                self.w('<br/>' % self._cw._('from %(date)s' % {'date': self._cw.format_date(task.start)}))
-                self.w('<br/>' % self._cw._('to %(date)s' % {'date': self._cw.format_date(task.stop)}))
-                self.w('<br/>to %s'%self._cw.format_date(task.stop))
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            if icalendarable.start and icalendarable.stop:
+                self.w('<br/> %s' % self._cw._('from %(date)s')
+                       % {'date': self._cw.format_date(icalendarable.start)})
+                self.w('<br/> %s' % self._cw._('to %(date)s')
+                       % {'date': self._cw.format_date(icalendarable.stop)})
+            else:
+                self.w('<br/>%s'%self._cw.format_date(icalendarable.start
+                                                      or icalendarable.stop))
 
 class CalendarLargeItemView(CalendarItemView):
     __regid__ = 'calendarlargeitem'
@@ -128,22 +155,25 @@
         self.color = color
         self.index = index
         self.length = 1
+        icalendarable = task.cw_adapt_to('ICalendarable')
+        self.start = icalendarable.start
+        self.stop = icalendarable.stop
 
     def in_working_hours(self):
         """predicate returning True is the task is in working hours"""
-        if todatetime(self.task.start).hour > 7 and todatetime(self.task.stop).hour < 20:
+        if todatetime(self.start).hour > 7 and todatetime(self.stop).hour < 20:
             return True
         return False
 
     def is_one_day_task(self):
-        task = self.task
-        return task.start and task.stop and task.start.isocalendar() ==  task.stop.isocalendar()
+        return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar()
 
 
 class OneMonthCal(EntityView):
     """At some point, this view will probably replace ampm calendars"""
     __regid__ = 'onemonthcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
+
     paginable = False
     title = _('one month')
 
@@ -181,13 +211,14 @@
             else:
                 user = None
             the_dates = []
-            tstart = task.start
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            tstart = icalendarable.start
             if tstart:
-                tstart = todate(task.start)
+                tstart = todate(icalendarable.start)
                 if tstart > lastday:
                     continue
                 the_dates = [tstart]
-            tstop = task.stop
+            tstop = icalendarable.stop
             if tstop:
                 tstop = todate(tstop)
                 if tstop < firstday:
@@ -199,7 +230,7 @@
                         the_dates = [tstart]
                 else:
                     the_dates = date_range(max(tstart, firstday),
-                                           min(tstop, lastday))
+                                           min(tstop + ONEDAY, lastday))
             if not the_dates:
                 continue
 
@@ -278,12 +309,14 @@
         prevdate = curdate - timedelta(31)
         nextdate = curdate + timedelta(31)
         rql = self.cw_rset.printable_rql()
-        prevlink = self._cw.build_ajax_replace_url('onemonthcalid', rql, 'onemonthcal',
-                                                   year=prevdate.year,
-                                                   month=prevdate.month)
-        nextlink = self._cw.build_ajax_replace_url('onemonthcalid', rql, 'onemonthcal',
-                                                   year=nextdate.year,
-                                                   month=nextdate.month)
+        prevlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
+                                             vid='onemonthcal',
+                                             year=prevdate.year,
+                                             month=prevdate.month)
+        nextlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
+                                             vid='onemonthcal',
+                                             year=nextdate.year,
+                                             month=nextdate.month)
         return prevlink, nextlink
 
     def _build_calendar_cell(self, celldate, rows, curdate):
@@ -335,7 +368,8 @@
 class OneWeekCal(EntityView):
     """At some point, this view will probably replace ampm calendars"""
     __regid__ = 'oneweekcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
+
     paginable = False
     title = _('one week')
 
@@ -361,15 +395,16 @@
         # colors here are class names defined in cubicweb.css
         colors = [ "col%x" % i for i in range(12) ]
         next_color_index = 0
-        done_tasks = []
+        done_tasks = set()
         for row in xrange(self.cw_rset.rowcount):
             task = self.cw_rset.get_entity(row, 0)
-            if task in done_tasks:
+            if task.eid in done_tasks:
                 continue
-            done_tasks.append(task)
+            done_tasks.add(task.eid)
             the_dates = []
-            tstart = task.start
-            tstop = task.stop
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            tstart = icalendarable.start
+            tstop = icalendarable.stop
             if tstart:
                 tstart = todate(tstart)
                 if tstart > lastday:
@@ -382,7 +417,7 @@
                 the_dates = [tstop]
             if tstart and tstop:
                 the_dates = date_range(max(tstart, firstday),
-                                       min(tstop, lastday))
+                                       min(tstop + ONEDAY, lastday))
             if not the_dates:
                 continue
 
@@ -462,7 +497,7 @@
     def _build_calendar_cell(self, date, task_descrs):
         inday_tasks = [t for t in task_descrs if t.is_one_day_task() and  t.in_working_hours()]
         wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
-        inday_tasks.sort(key=lambda t:t.task.start)
+        inday_tasks.sort(key=lambda t:t.start)
         sorted_tasks = []
         for i, t in enumerate(wholeday_tasks):
             t.index = i
@@ -470,7 +505,7 @@
         while inday_tasks:
             t = inday_tasks.pop(0)
             for i, c in enumerate(sorted_tasks):
-                if not c or c[-1].task.stop <= t.task.start:
+                if not c or c[-1].stop <= t.start:
                     c.append(t)
                     t.index = i+ncols
                     break
@@ -491,15 +526,15 @@
             start_min = 0
             stop_hour = 20
             stop_min = 0
-            if task.start:
-                if date < todate(task.start) < date + ONEDAY:
-                    start_hour = max(8, task.start.hour)
-                    start_min = task.start.minute
-            if task.stop:
-                if date < todate(task.stop) < date + ONEDAY:
-                    stop_hour = min(20, task.stop.hour)
+            if task_desc.start:
+                if date < todate(task_desc.start) < date + ONEDAY:
+                    start_hour = max(8, task_desc.start.hour)
+                    start_min = task_desc.start.minute
+            if task_desc.stop:
+                if date < todate(task_desc.stop) < date + ONEDAY:
+                    stop_hour = min(20, task_desc.stop.hour)
                     if stop_hour < 20:
-                        stop_min = task.stop.minute
+                        stop_min = task_desc.stop.minute
 
             height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
             top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
@@ -518,7 +553,7 @@
             self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
             task.view('tooltip', w=self.w)
             self.w(u'</div>')
-            if task.start is None:
+            if task_desc.start is None:
                 self.w(u'<div class="bottommarker">')
                 self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
                 self.w(u'</div>')
@@ -535,10 +570,12 @@
         prevdate = curdate - timedelta(7)
         nextdate = curdate + timedelta(7)
         rql = self.cw_rset.printable_rql()
-        prevlink = self._cw.build_ajax_replace_url('oneweekcalid', rql, 'oneweekcal',
-                                                   year=prevdate.year,
-                                                   week=prevdate.isocalendar()[1])
-        nextlink = self._cw.build_ajax_replace_url('oneweekcalid', rql, 'oneweekcal',
-                                                   year=nextdate.year,
-                                                   week=nextdate.isocalendar()[1])
+        prevlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
+                                             vid='oneweekcal',
+                                             year=prevdate.year,
+                                             week=prevdate.isocalendar()[1])
+        nextlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
+                                             vid='oneweekcal',
+                                             year=nextdate.year,
+                                             week=nextdate.isocalendar()[1])
         return prevlink, nextlink
--- a/web/views/cwproperties.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/cwproperties.py	Wed Nov 03 16:38:28 2010 +0100
@@ -26,7 +26,7 @@
 from logilab.common.decorators import cached
 
 from cubicweb import UnknownProperty
-from cubicweb.selectors import (one_line_rset, none_rset, implements,
+from cubicweb.selectors import (one_line_rset, none_rset, is_instance,
                                 match_user_groups, objectify_selector,
                                 logged_user_in_rset)
 from cubicweb.view import StartupView
@@ -35,7 +35,7 @@
 from cubicweb.web.formfields import FIELDS, StringField
 from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
                                       FieldWidget)
-from cubicweb.web.views import primary, formrenderers
+from cubicweb.web.views import primary, formrenderers, editcontroller
 
 uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
 
@@ -74,7 +74,7 @@
 
 
 class CWPropertyPrimaryView(primary.PrimaryView):
-    __select__ = implements('CWProperty')
+    __select__ = is_instance('CWProperty')
     skip_none = False
 
 
@@ -144,7 +144,7 @@
         for label, group, form in sorted((_(g), g, f)
                                          for g, f in mainopts.iteritems()):
             status = css_class(self._group_status(group))
-            w(u'<h2 class="propertiesform">%s</h2>\n' %
+            w(u'<div class="propertiesform">%s</div>\n' %
             (make_togglable_link('fieldset_' + group, label.capitalize())))
             w(u'<div id="fieldset_%s" %s>' % (group, status))
             w(u'<fieldset class="preferences">')
@@ -154,7 +154,7 @@
         for label, group, objects in sorted((_(g), g, o)
                                             for g, o in groupedopts.iteritems()):
             status = css_class(self._group_status(group))
-            w(u'<h2 class="propertiesform">%s</h2>\n' %
+            w(u'<div class="propertiesform">%s</div>\n' %
               (make_togglable_link('fieldset_' + group, label.capitalize())))
             w(u'<div id="fieldset_%s" %s>' % (group, status))
             # create selection
@@ -243,7 +243,7 @@
     __select__ = (
         (none_rset() & match_user_groups('users','managers'))
         | (one_line_rset() & match_user_groups('users') & logged_user_in_rset())
-        | (one_line_rset() & match_user_groups('managers') & implements('CWUser'))
+        | (one_line_rset() & match_user_groups('managers') & is_instance('CWUser'))
         )
 
     title = _('preferences')
@@ -396,6 +396,15 @@
         w(u'</div>')
 
 
+class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter):
+    __select__ = is_instance('CWProperty')
+
+    def after_deletion_path(self):
+        """return (path, parameters) which should be used as redirect
+        information when this entity is being deleted
+        """
+        return 'view', {}
+
 _afs = uicfg.autoform_section
 _afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
 _afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
--- a/web/views/cwuser.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/cwuser.py	Wed Nov 03 16:38:28 2010 +0100
@@ -19,9 +19,11 @@
 
 __docformat__ = "restructuredtext en"
 
+import hashlib
+
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import one_line_rset, implements, match_user_groups
+from cubicweb.selectors import one_line_rset, is_instance, match_user_groups
 from cubicweb.view import EntityView
 from cubicweb.web import action, uicfg
 from cubicweb.web.views import tabs
@@ -38,7 +40,7 @@
 
 class UserPreferencesEntityAction(action.Action):
     __regid__ = 'prefs'
-    __select__ = (one_line_rset() & implements('CWUser') &
+    __select__ = (one_line_rset() & is_instance('CWUser') &
                   match_user_groups('owners', 'managers'))
 
     title = _('preferences')
@@ -51,7 +53,7 @@
 
 class FoafView(EntityView):
     __regid__ = 'foaf'
-    __select__ = implements('CWUser')
+    __select__ = is_instance('CWUser')
 
     title = _('foaf')
     templatable = False
@@ -68,21 +70,23 @@
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
-        self.w(u'''<foaf:PersonalProfileDocument rdf:about="">
-                      <foaf:maker rdf:resource="%s"/>
-                      <foaf:primaryTopic rdf:resource="%s"/>
-                   </foaf:PersonalProfileDocument>''' % (entity.absolute_url(), entity.absolute_url()))
-        self.w(u'<foaf:Person rdf:ID="%s">\n' % entity.eid)
-        self.w(u'<foaf:name>%s</foaf:name>\n' % xml_escape(entity.dc_long_title()))
+        # account
+        self.w(u'<foaf:OnlineAccount rdf:about="%s">\n' % entity.absolute_url())
+        self.w(u'  <foaf:accountName>%s</foaf:accountName>\n' % entity.login)
+        self.w(u'</foaf:OnlineAccount>\n')
+        # person
+        self.w(u'<foaf:Person rdf:about="%s#user">\n' % entity.absolute_url())
+        self.w(u'  <foaf:account rdf:resource="%s" />\n' % entity.absolute_url())
         if entity.surname:
-            self.w(u'<foaf:family_name>%s</foaf:family_name>\n'
+            self.w(u'<foaf:familyName>%s</foaf:familyName>\n'
                    % xml_escape(entity.surname))
         if entity.firstname:
-            self.w(u'<foaf:givenname>%s</foaf:givenname>\n'
+            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:mbox_sha1sum>%s</foaf:mbox_sha1sum>\n'
+                   % hashlib.sha1(emailaddr.encode('utf-8')).hexdigest())
         self.w(u'</foaf:Person>\n')
 
 
@@ -93,14 +97,14 @@
 
 
 class CWGroupPrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWGroup')
+    __select__ = is_instance('CWGroup')
     tabs = [_('cwgroup-main'), _('cwgroup-permissions')]
     default_tab = 'cwgroup-main'
 
 
 class CWGroupMainTab(tabs.PrimaryTab):
     __regid__ = 'cwgroup-main'
-    __select__ = tabs.PrimaryTab.__select__ & implements('CWGroup')
+    __select__ = tabs.PrimaryTab.__select__ & is_instance('CWGroup')
 
     def render_entity_attributes(self, entity):
         rql = 'Any U, FN, LN, CD, LL ORDERBY L WHERE U in_group G, ' \
@@ -114,7 +118,7 @@
 
 class CWGroupPermTab(EntityView):
     __regid__ = 'cwgroup-permissions'
-    __select__ = implements('CWGroup')
+    __select__ = is_instance('CWGroup')
 
     def cell_call(self, row, col):
         self._cw.add_css(('cubicweb.schema.css','cubicweb.acl.css'))
@@ -140,7 +144,7 @@
 
 class CWGroupInContextView(EntityView):
     __regid__ = 'incontext'
-    __select__ = implements('CWGroup')
+    __select__ = is_instance('CWGroup')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
--- a/web/views/debug.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/debug.py	Wed Nov 03 16:38:28 2010 +0100
@@ -25,6 +25,7 @@
 
 from cubicweb.selectors import none_rset, match_user_groups
 from cubicweb.view import StartupView
+from cubicweb.web.views import actions
 
 def dict_to_html(w, dict):
     # XHTML doesn't allow emtpy <ul> nodes
@@ -37,10 +38,17 @@
 
 
 
+class SiteInfoAction(actions.ManagersAction):
+    __regid__ = 'siteinfo'
+    __select__ = match_user_groups('users','managers')
+    title = _('info')
+    order = 30
+
+
 class ProcessInformationView(StartupView):
     """display various web server /repository information"""
     __regid__ = 'info'
-    __select__ = none_rset() & match_user_groups('managers')
+    __select__ = none_rset() & match_user_groups('managers', 'users')
 
     title = _('server information')
     cache_max_age = 0
@@ -81,7 +89,7 @@
                    % (element, xml_escape(unicode(stats[element])),
                       element.endswith('percent') and '%' or '' ))
         w(u'</table>')
-        if req.cnx._cnxtype == 'inmemory':
+        if req.cnx._cnxtype == 'inmemory' and req.user.is_in_group('managers'):
             w(u'<h3>%s</h3>' % _('opened sessions'))
             sessions = repo._sessions.values()
             if sessions:
@@ -104,21 +112,22 @@
         w(u'<tr><th align="left">%s</th><td>%s</td></tr>' % (
             _('data directory url'), req.datadir_url))
         w(u'</table>')
-        from cubicweb.web.application import SESSION_MANAGER
-        sessions = SESSION_MANAGER.current_sessions()
-        w(u'<h3>%s</h3>' % _('opened web sessions'))
-        if sessions:
-            w(u'<ul>')
-            for session in sessions:
-                w(u'<li>%s (%s: %s)<br/>' % (
-                    session.sessionid,
-                    _('last usage'),
-                    strftime(dtformat, localtime(session.last_usage_time))))
-                dict_to_html(w, session.data)
-                w(u'</li>')
-            w(u'</ul>')
-        else:
-            w(u'<p>%s</p>' % _('no web sessions found'))
+        if req.user.is_in_group('managers'):
+            from cubicweb.web.application import SESSION_MANAGER
+            sessions = SESSION_MANAGER.current_sessions()
+            w(u'<h3>%s</h3>' % _('opened web sessions'))
+            if sessions:
+                w(u'<ul>')
+                for session in sessions:
+                    w(u'<li>%s (%s: %s)<br/>' % (
+                        session.sessionid,
+                        _('last usage'),
+                        strftime(dtformat, localtime(session.last_usage_time))))
+                    dict_to_html(w, session.data)
+                    w(u'</li>')
+                w(u'</ul>')
+            else:
+                w(u'<p>%s</p>' % _('no web sessions found'))
 
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/dotgraphview.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,81 @@
+# 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/>.
+"""some basic stuff to build dot generated graph images"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+import tempfile
+import os
+
+from logilab.mtconverter import xml_escape
+from logilab.common.graph import GraphGenerator, DotBackend
+
+from cubicweb.view import EntityView
+from cubicweb.utils import make_uid
+
+class DotGraphView(EntityView):
+    __abstract__ = True
+    backend_class = DotBackend
+    backend_kwargs = {'ratio': 'compress', 'size': '30,10'}
+    def cell_call(self, row, col):
+        entity = self.cw_rset.get_entity(row, col)
+        visitor = self.build_visitor(entity)
+        prophdlr = self.build_dotpropshandler()
+        graphname = 'dotgraph%s' % str(entity.eid)
+        generator = GraphGenerator(self.backend_class(graphname, None,
+                                                      **self.backend_kwargs))
+        # map file
+        pmap, mapfile = tempfile.mkstemp(".map", graphname)
+        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="#%s" />' % (
+            xml_escape(entity.absolute_url(vid='tmppng', tmpfile=filekeyid)),
+            xml_escape(self._cw._('Data connection graph for %s') % entity.dc_title()),
+            graphname))
+        stream = open(mapfile, 'r').read()
+        stream = stream.decode(self._cw.encoding)
+        self.w(stream)
+        os.unlink(mapfile)
+
+    def build_visitor(self, entity):
+        raise NotImplementedError
+
+    def build_dotpropshandler(self):
+        return DotPropsHandler(self._cw)
+
+
+class DotPropsHandler(object):
+    def __init__(self, req):
+        self._ = req._
+
+    def node_properties(self, entity):
+        """return default DOT drawing options for a state or transition"""
+        return {'label': entity.dc_title(),
+                'href': entity.absolute_url(),
+                'fontname': 'Courier', 'fontsize': 10, 'shape':'box',
+                 }
+
+    def edge_properties(self, transition, fromstate, tostate):
+        return {'label': '', 'dir': 'forward',
+                'color': 'black', 'style': 'filled'}
--- a/web/views/editcontroller.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/editcontroller.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""The edit controller, handling form submitting.
+"""The edit controller, automatically handling entity form submitting"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -27,9 +26,37 @@
 from logilab.common.textutils import splitstrip
 
 from cubicweb import Binary, ValidationError, typed_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import is_instance
+from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
+                          ProcessFormError)
 from cubicweb.web.views import basecontrollers, autoform
 
+
+class IEditControlAdapter(EntityAdapter):
+    __regid__ = 'IEditControl'
+    __select__ = is_instance('Any')
+
+    @implements_adapter_compat('IEditControl')
+    def after_deletion_path(self):
+        """return (path, parameters) which should be used as redirect
+        information when this entity is being deleted
+        """
+        parent = self.entity.cw_adapt_to('IBreadCrumbs').parent_entity()
+        if parent is not None:
+            return parent.rest_path(), {}
+        return str(self.entity.e_schema).lower(), {}
+
+    @implements_adapter_compat('IEditControl')
+    def pre_web_edit(self):
+        """callback called by the web editcontroller when an entity will be
+        created/modified, to let a chance to do some entity specific stuff.
+
+        Do nothing by default.
+        """
+        pass
+
+
 def valerror_eid(eid):
     try:
         return typed_eid(eid)
@@ -133,8 +160,6 @@
     def _insert_entity(self, etype, eid, rqlquery):
         rql = rqlquery.insert_query(etype)
         try:
-            # get the new entity (in some cases, the type might have
-            # changed as for the File --> Image mutation)
             entity = self._cw.execute(rql, rqlquery.kwargs).get_entity(0, 0)
             neweid = entity.eid
         except ValidationError, ex:
@@ -152,10 +177,10 @@
         """edit / create / copy an entity and return its eid"""
         etype = formparams['__type']
         entity = self._cw.vreg['etypes'].etype_class(etype)(self._cw)
-        entity.eid = formparams['eid']
+        entity.eid = valerror_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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/editforms.py	Wed Nov 03 16:38:28 2010 +0100
@@ -26,16 +26,17 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cached
+from logilab.common.deprecation import class_moved
 
 from cubicweb import tags
 from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
-                                specified_etype_implements, implements, yes)
+                                specified_etype_implements, is_instance, yes)
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
-from cubicweb.web import uicfg, stdmsgs, eid_param, dumps, \
+from cubicweb.web import uicfg, stdmsgs, eid_param, \
      formfields as ff, formwidgets as fw
 from cubicweb.web.form import FormViewMixIn, FieldNotFound
-from cubicweb.web.views import forms
+from cubicweb.web.views import forms, reledit
 
 _pvdc = uicfg.primaryview_display_ctrl
 
@@ -43,7 +44,7 @@
 class DeleteConfForm(forms.CompositeForm):
     __regid__ = 'deleteconf'
     # XXX non_final_entity does not implement eclass_selector
-    __select__ = implements('Any')
+    __select__ = is_instance('Any')
 
     domid = 'deleteconf'
     copy_nav_params = True
@@ -99,7 +100,7 @@
     # though not baseforms based customized view
     __select__ = one_line_rset() & non_final_entity() & yes()
 
-    title = _('edition')
+    title = _('modification')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.complete_entity(row, col)
@@ -207,7 +208,7 @@
             if not rschema.final:
                 # ensure relation cache is filed
                 rset = self.copying.related(rschema, role)
-                self.newentity.set_related_cache(rschema, role, rset)
+                self.newentity.cw_set_relation_cache(rschema, role, rset)
 
     def submited_message(self):
         """return the message that will be displayed on successful edition"""
@@ -260,213 +261,5 @@
 
 # click and edit handling ('reledit') ##########################################
 
-class DummyForm(object):
-    __slots__ = ('event_args',)
-    def form_render(self, **_args):
-        return u''
-    def render(self, **_args):
-        return u''
-    def append_field(self, *args):
-        pass
-    def field_by_name(self, rtype, role, eschema=None):
-        return None
-
-
-class ClickAndEditFormView(FormViewMixIn, EntityView):
-    """form used to permit ajax edition of a relation or attribute of an entity
-    in a view, if logged user have the permission to edit it.
-
-    (double-click on the field to see an appropriate edition widget).
-    """
-    __regid__ = 'doreledit'
-    __select__ = non_final_entity() & match_kwargs('rtype')
-    # FIXME editableField class could be toggleable from userprefs
-
-    _onclick = u"showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')"
-    _onsubmit = ("return inlineValidateRelationForm('%(rtype)s', '%(role)s', '%(eid)s', "
-                 "'%(divid)s', %(reload)s, '%(vid)s', '%(default)s', '%(lzone)s');")
-    _cancelclick = "hideInlineEdit(%s,\'%s\',\'%s\')"
-    _defaultlandingzone = (u'<img title="%(msg)s" src="data/pen_icon.png" '
-                           'alt="%(msg)s"/>')
-    _landingzonemsg = _('click to edit this field')
-    # default relation vids according to cardinality
-    _one_rvid = 'incontext'
-    _many_rvid = 'csv'
-
-
-    def cell_call(self, row, col, rtype=None, role='subject',
-                  reload=False,      # controls reloading the whole page after change
-                  rvid=None,         # vid to be applied to other side of rtype (non final relations only)
-                  default=None,      # default value
-                  landing_zone=None  # prepend value with a separate html element to click onto
-                                     # (esp. needed when values are links)
-                  ):
-        """display field to edit entity's `rtype` relation on click"""
-        assert rtype
-        assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
-        self._cw.add_js('cubicweb.edition.js')
-        self._cw.add_css('cubicweb.form.css')
-        if default is None:
-            default = xml_escape(self._cw._('<%s not specified>')
-                                 % display_name(self._cw, rtype, role))
-        schema = self._cw.vreg.schema
-        entity = self.cw_rset.get_entity(row, col)
-        rschema = schema.rschema(rtype)
-        lzone = self._build_landing_zone(landing_zone)
-        # compute value, checking perms, build form
-        if rschema.final:
-            form = self._build_form(entity, rtype, role, 'base', default, reload, lzone)
-            if not self.should_edit_attribute(entity, rschema, form):
-                self.w(entity.printable_value(rtype))
-                return
-            value = entity.printable_value(rtype) or default
-        else:
-            rvid = self._compute_best_vid(entity.e_schema, rschema, role)
-            rset = entity.related(rtype, role)
-            if rset:
-                value = self._cw.view(rvid, rset)
-            else:
-                value = default
-            if not self.should_edit_relation(entity, rschema, role, rvid):
-                if rset:
-                    self.w(value)
-                return
-            # XXX do we really have to give lzone twice?
-            form = self._build_form(entity, rtype, role, 'base', default, reload, lzone,
-                                    dict(vid=rvid, lzone=lzone))
-        field = form.field_by_name(rtype, role, entity.e_schema)
-        form.append_field(field)
-        self.relation_form(lzone, value, form,
-                           self._build_renderer(entity, rtype, role))
-
-    def should_edit_attribute(self, entity, rschema, form):
-        if not entity.has_perm('update'):
-            return False
-        rdef = entity.e_schema.rdef(rschema)
-        if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
-            return False
-        try:
-            form.field_by_name(str(rschema), 'subject', entity.e_schema)
-        except FieldNotFound:
-            return False
-        return True
-
-    def should_edit_relation(self, entity, rschema, role, rvid):
-        if ((role == 'subject' and not rschema.has_perm(self._cw, 'add',
-                                                        fromeid=entity.eid))
-            or
-            (role == 'object' and not rschema.has_perm(self._cw, 'add',
-                                                       toeid=entity.eid))):
-            return False
-        return True
-
-    def relation_form(self, lzone, value, form, renderer):
-        """xxx-reledit div (class=field)
-              +-xxx div (class="editableField")
-              |   +-landing zone
-              +-xxx-value div
-              +-xxx-form div
-        """
-        w = self.w
-        divid = form.event_args['divid']
-        w(u'<div id="%s-reledit" class="field" '
-          u'onmouseout="addElementClass(jQuery(\'#%s\'), \'hidden\')" '
-          u'onmouseover="removeElementClass(jQuery(\'#%s\'), \'hidden\')">'
-          % (divid, divid, divid))
-        w(u'<div id="%s-value" class="editableFieldValue">%s</div>' % (divid, value))
-        w(form.render(renderer=renderer))
-        w(u'<div id="%s" class="editableField hidden" onclick="%s" title="%s">' % (
-                divid, xml_escape(self._onclick % form.event_args),
-                self._cw._(self._landingzonemsg)))
-        w(lzone)
-        w(u'</div>')
-        w(u'</div>')
-
-    def _compute_best_vid(self, eschema, rschema, role):
-        dispctrl = _pvdc.etype_get(eschema, rschema, role)
-        if dispctrl.get('rvid'):
-            return dispctrl['rvid']
-        if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
-            return self._many_rvid
-        return self._one_rvid
-
-    def _build_landing_zone(self, lzone):
-        return lzone or self._defaultlandingzone % {
-            'msg': xml_escape(self._cw._(self._landingzonemsg))}
-
-    def _build_renderer(self, entity, rtype, role):
-        return self._cw.vreg['formrenderers'].select(
-            'base', self._cw, entity=entity, display_label=False,
-            display_help=False, table_class='',
-            button_bar_class='buttonbar', display_progress_div=False)
-
-    def _build_args(self, entity, rtype, role, formid, default, reload, lzone,
-                    extradata=None):
-        divid = '%s-%s-%s' % (rtype, role, entity.eid)
-        event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype,
-                      'reload' : dumps(reload), 'default' : default, 'role' : role, 'vid' : u'',
-                      'lzone' : lzone}
-        if extradata:
-            event_args.update(extradata)
-        return divid, event_args
-
-    def _build_form(self, entity, rtype, role, formid, default, reload, lzone,
-                  extradata=None, **formargs):
-        divid, event_args = self._build_args(entity, rtype, role, formid, default,
-                                      reload, lzone, extradata)
-        onsubmit = self._onsubmit % event_args
-        cancelclick = self._cancelclick % (entity.eid, rtype, divid)
-        form = self._cw.vreg['forms'].select(
-            formid, self._cw, entity=entity, domid='%s-form' % divid,
-            cssstyle='display: none', onsubmit=onsubmit, action='#',
-            form_buttons=[fw.SubmitButton(),
-                          fw.Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)],
-            **formargs)
-        form.event_args = event_args
-        return form
-
-
-class AutoClickAndEditFormView(ClickAndEditFormView):
-    """same as ClickAndEditFormView but checking if the view *should* be applied
-    by checking uicfg configuration and composite relation property.
-    """
-    __regid__ = 'reledit'
-    _onclick = (u"loadInlineEditionForm(%(eid)s, '%(rtype)s', '%(role)s', "
-                "'%(divid)s', %(reload)s, '%(vid)s', '%(default)s', '%(lzone)s');")
-
-    def should_edit_attribute(self, entity, rschema, form):
-        rdef = entity.e_schema.rdef(rschema)
-        afs = uicfg.autoform_section.etype_get(
-            entity.__regid__, rschema, 'subject', rdef.object)
-        if 'main_hidden' in afs:
-            return False
-        return super(AutoClickAndEditFormView, self).should_edit_attribute(
-            entity, rschema, form)
-
-    def should_edit_relation(self, entity, rschema, role, rvid):
-        eschema = entity.e_schema
-        dispctrl = _pvdc.etype_get(eschema, rschema, role)
-        vid = dispctrl.get('vid', 'reledit')
-        if vid != 'reledit': # reledit explicitly disabled
-            return False
-        rdef = eschema.rdef(rschema, role)
-        if rdef.composite == role:
-            return False
-        afs = uicfg.autoform_section.etype_get(
-            entity.__regid__, rschema, role, rdef.object)
-        if 'main_hidden' in afs:
-            return False
-        return super(AutoClickAndEditFormView, self).should_edit_relation(
-            entity, rschema, role, rvid)
-
-    def _build_form(self, entity, rtype, role, formid, default, reload, lzone,
-                  extradata=None, **formargs):
-        _divid, event_args = self._build_args(entity, rtype, role, formid, default,
-                                              reload, lzone, extradata)
-        form = DummyForm()
-        form.event_args = event_args
-        return form
-
-    def _build_renderer(self, entity, rtype, role):
-        pass
-
+ClickAndEditFormView = class_moved(reledit.ClickAndEditFormView)
+AutoClickAndEditFormView = class_moved(reledit.AutoClickAndEditFormView)
--- a/web/views/editviews.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/editviews.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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/>.
-"""Some views used to help to the edition process
+"""Some views used to help to the edition process"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -59,10 +58,10 @@
         # 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)
-        rset = self._cw.execute(rql, args, tuple(args))
+        rql, args = entity.cw_unrelated_rql(rtype, etype, role,
+                                            ordermethod='fetch_order',
+                                            vocabconstraints=False)
+        rset = self._cw.execute(rql, args)
         return rset, 'list', "search-associate-content", True
 
 
@@ -90,7 +89,7 @@
     __regid__ = 'combobox'
     title = None
 
-    def cell_call(self, row, col):
+    def cell_call(self, row, col, **kwargs):
         """the combo-box view for an entity: same as text out of context view
         by default
         """
--- a/web/views/emailaddress.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/emailaddress.py	Wed Nov 03 16:38:28 2010 +0100
@@ -23,17 +23,17 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.schema import display_name
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb import Unauthorized
 from cubicweb.web import uicfg
-from cubicweb.web.views import baseviews, primary
+from cubicweb.web.views import baseviews, primary, ibreadcrumbs
 
 _pvs = uicfg.primaryview_section
 _pvs.tag_subject_of(('*', 'use_email', '*'), 'attributes')
 _pvs.tag_subject_of(('*', 'primary_email', '*'), 'hidden')
 
 class EmailAddressPrimaryView(primary.PrimaryView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, skipeids=None):
         self.skipeids = skipeids
@@ -72,7 +72,7 @@
 
 
 class EmailAddressShortPrimaryView(EmailAddressPrimaryView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
     __regid__ = 'shortprimary'
     title = None # hidden view
 
@@ -83,7 +83,7 @@
 
 
 class EmailAddressOneLineView(baseviews.OneLineView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
@@ -104,7 +104,7 @@
     'mailto:'"""
 
     __regid__ = 'mailto'
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
@@ -127,14 +127,21 @@
 
 
 class EmailAddressInContextView(baseviews.InContextView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         self.wview('mailto', self.cw_rset, row=row, col=col, **kwargs)
 
 
 class EmailAddressTextView(baseviews.TextView):
-    __select__ = implements('EmailAddress')
+    __select__ = is_instance('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
         self.w(self.cw_rset.get_entity(row, col).display_address())
+
+
+class EmailAddressIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('EmailAddress')
+
+    def parent_entity(self):
+        return self.entity.email_of
--- a/web/views/embedding.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/embedding.py	Wed Nov 03 16:38:28 2010 +0100
@@ -28,16 +28,27 @@
 
 from logilab.mtconverter import guess_encoding
 
-from cubicweb.selectors import (one_line_rset, score_entity,
-                                match_search_state, implements)
+from cubicweb.selectors import (one_line_rset, score_entity, implements,
+                                adaptable, match_search_state)
 from cubicweb.interfaces import IEmbedable
-from cubicweb.view import NOINDEX, NOFOLLOW
+from cubicweb.view import NOINDEX, NOFOLLOW, EntityAdapter, implements_adapter_compat
 from cubicweb.uilib import soup2xhtml
 from cubicweb.web.controller import Controller
 from cubicweb.web.action import Action
 from cubicweb.web.views import basetemplates
 
 
+class IEmbedableAdapter(EntityAdapter):
+    """interface for embedable entities"""
+    __regid__ = 'IEmbedable'
+    __select__ = implements(IEmbedable, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('IEmbedable')
+    def embeded_url(self):
+        """embed action interface"""
+        raise NotImplementedError
+
+
 class ExternalTemplate(basetemplates.TheMainTemplate):
     """template embeding an external web pages into CubicWeb web interface
     """
@@ -84,14 +95,14 @@
             except HTTPError, err:
                 body = '<h2>%s</h2><h3>%s</h3>' % (
                     _('error while embedding page'), err)
-        self.process_rql(req.form.get('rql'))
+        rset = self.process_rql()
         return self._cw.vreg['views'].main_template(req, self.template,
-                                                rset=self.cw_rset, body=body)
+                                                    rset=rset, body=body)
 
 
 def entity_has_embedable_url(entity):
     """return 1 if the entity provides an allowed embedable url"""
-    url = entity.embeded_url()
+    url = entity.cw_adapt_to('IEmbedable').embeded_url()
     if not url or not url.strip():
         return 0
     allowed = entity._cw.vreg.config['embed-allowed']
@@ -106,14 +117,14 @@
     """
     __regid__ = 'embed'
     __select__ = (one_line_rset() & match_search_state('normal')
-                  & implements(IEmbedable)
+                  & adaptable('IEmbedable')
                   & score_entity(entity_has_embedable_url))
 
     title = _('embed')
 
     def url(self, row=0):
         entity = self.cw_rset.get_entity(row, 0)
-        url = urljoin(self._cw.base_url(), entity.embeded_url())
+        url = urljoin(self._cw.base_url(), entity.cw_adapt_to('IEmbedable').embeded_url())
         if self._cw.form.has_key('rql'):
             return self._cw.build_url('embed', url=url, rql=self._cw.form['rql'])
         return self._cw.build_url('embed', url=url)
--- a/web/views/error.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/error.py	Wed Nov 03 16:38:28 2010 +0100
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Set of HTML errors views. Error view are generally implemented
 as startup views and are used for standard error pages (404, 500, etc.)
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.view import StartupView
@@ -37,4 +37,4 @@
     def call(self):
         _ = self._cw._
         self.w(u"<h1>%s</h1>" %
-               _('an error occured, the request cannot be fulfilled'))
+               _('an error occurred, the request cannot be fulfilled'))
--- a/web/views/facets.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/facets.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""the facets box and some basic facets
+"""the facets box and some basic facets"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
@@ -25,7 +24,7 @@
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import (non_final_entity, multi_lines_rset,
                                 match_context_prop, yes, relation_possible)
-from cubicweb.web import dumps
+from cubicweb.utils import json_dumps
 from cubicweb.web.box import BoxTemplate
 from cubicweb.web.facet import (AbstractFacet, FacetStringWidget, RelationFacet,
                                 prepare_facets_rqlst, filter_hiddens, _cleanup_rqlst,
@@ -102,7 +101,7 @@
             self.display_bookmark_link(rset)
         w = self.w
         w(u'<form method="post" id="%sForm" cubicweb:facetargs="%s" action="">'  % (
-            divid, xml_escape(dumps([divid, vid, paginate, self.facetargs()]))))
+            divid, xml_escape(json_dumps([divid, vid, paginate, self.facetargs()]))))
         w(u'<fieldset>')
         hiddens = {'facets': ','.join(wdg.facet.__regid__ for wdg in widgets),
                    'baserql': baserql}
--- a/web/views/formrenderers.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/formrenderers.py	Wed Nov 03 16:38:28 2010 +0100
@@ -40,8 +40,9 @@
 
 from cubicweb import tags
 from cubicweb.appobject import AppObject
-from cubicweb.selectors import implements, yes
-from cubicweb.web import dumps, eid_param, formwidgets as fwdgs
+from cubicweb.selectors import is_instance, yes
+from cubicweb.utils import json_dumps
+from cubicweb.web import eid_param, formwidgets as fwdgs
 
 
 def checkbox(name, value, attrs='', checked=None):
@@ -334,15 +335,21 @@
     def render_fields(self, w, form, values):
         if form.parent_form is None:
             w(u'<table class="listing">')
-            subfields = [field for field in form.forms[0].fields
-                         if field.is_visible()]
+            # get fields from the first subform with something to display (we
+            # may have subforms with nothing editable that will simply be
+            # skipped later)
+            for subform in form.forms:
+                subfields = [field for field in subform.fields
+                             if field.is_visible()]
+                if subfields:
+                    break
             if subfields:
                 # main form, display table headers
                 w(u'<tr class="header">')
                 w(u'<th align="left">%s</th>' %
                   tags.input(type='checkbox',
                              title=self._cw._('toggle check boxes'),
-                             onclick="setCheckboxesState('eid', this.checked)"))
+                             onclick="setCheckboxesState('eid', null, this.checked)"))
                 for field in subfields:
                     w(u'<th>%s</th>' % field_label(form, field))
                 w(u'</tr>')
@@ -358,8 +365,8 @@
             entity = form.edited_entity
             values = form.form_previous_values
             qeid = eid_param('eid', entity.eid)
-            cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % \
-                         xml_escape(dumps(entity.eid))
+            cbsetstate = "setCheckboxesState('eid', %s, 'checked')" % \
+                         xml_escape(json_dumps(entity.eid))
             w(u'<tr class="%s">' % (entity.cw_row % 2 and u'even' or u'odd'))
             # XXX turn this into a widget used on the eid field
             w(u'<td>%s</td>' % checkbox('eid', entity.eid,
@@ -392,7 +399,7 @@
     """
     __regid__ = 'default'
     # needs some additional points in some case (XXX explain cases)
-    __select__ = implements('Any') & yes()
+    __select__ = is_instance('Any') & yes()
 
     _options = FormRenderer._options + ('main_form_title',)
     main_form_title = _('main informations')
--- a/web/views/forms.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/forms.py	Wed Nov 03 16:38:28 2010 +0100
@@ -188,7 +188,7 @@
         if self.formvalues is not None:
             return # already built
         self.formvalues = formvalues or {}
-        # use a copy in case fields are modified while context is build (eg
+        # use a copy in case fields are modified while context is built (eg
         # __linkto handling for instance)
         for field in self.fields[:]:
             for field in field.actual_fields(self):
--- a/web/views/ibreadcrumbs.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/ibreadcrumbs.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,26 +15,84 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""navigation components definition for CubicWeb web client
+"""breadcrumbs components definition for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
 
-from cubicweb.interfaces import IBreadCrumbs
-from cubicweb.selectors import (one_line_rset, implements, one_etype_rset,
-                                multi_lines_rset, any_rset)
-from cubicweb.view import EntityView, Component
+#from cubicweb.interfaces import IBreadCrumbs
+from cubicweb.selectors import (is_instance, one_line_rset, adaptable,
+                                one_etype_rset, multi_lines_rset, any_rset)
+from cubicweb.view import EntityView, Component, EntityAdapter
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
 from cubicweb.entity import Entity
 from cubicweb import tags, uilib
 
 
+# ease bw compat
+def ibreadcrumb_adapter(entity):
+    if hasattr(entity, 'breadcrumbs'):
+        warn('[3.9] breadcrumbs() method is deprecated, define a custom '
+             'IBreadCrumbsAdapter for %s instead' % entity.__class__,
+             DeprecationWarning)
+        return entity
+    return entity.cw_adapt_to('IBreadCrumbs')
+
+
+class IBreadCrumbsAdapter(EntityAdapter):
+    """adapters for entities which can be"located" on some path to display in
+    the web ui
+    """
+    __regid__ = 'IBreadCrumbs'
+    __select__ = is_instance('Any', accept_none=False)
+
+    def parent_entity(self):
+        if hasattr(self.entity, 'parent'):
+            warn('[3.9] parent() method is deprecated, define a '
+                 'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead'
+                 % self.entity.__class__, DeprecationWarning)
+            return self.entity.parent()
+        itree = self.entity.cw_adapt_to('ITree')
+        if itree is not None:
+            return itree.parent()
+        return None
+
+    def breadcrumbs(self, view=None, recurs=False):
+        """return a list containing some:
+
+        * tuple (url, label)
+        * entity
+        * simple label string
+
+        defining path from a root to the current view
+
+        the main view is given as argument so breadcrumbs may vary according
+        to displayed view (may be None). When recursing on a parent entity,
+        the `recurs` argument should be set to True.
+        """
+        parent = self.parent_entity()
+        if parent is not None:
+            adapter = ibreadcrumb_adapter(parent)
+            path = adapter.breadcrumbs(view, True) + [self.entity]
+        else:
+            path = [self.entity]
+        if not recurs:
+            if view is None:
+                if 'vtitle' in self._cw.form:
+                    # embeding for instance
+                    path.append( self._cw.form['vtitle'] )
+            elif view.__regid__ != 'primary' and hasattr(view, 'title'):
+                path.append( self._cw._(view.title) )
+        return path
+
+
 class BreadCrumbEntityVComponent(Component):
     __regid__ = 'breadcrumbs'
-    __select__ = one_line_rset() & implements(IBreadCrumbs, accept_none=False)
+    __select__ = one_line_rset() & adaptable('IBreadCrumbs')
 
     cw_property_defs = {
         _('visible'):  dict(type='Boolean', default=True,
@@ -47,7 +105,8 @@
 
     def call(self, view=None, first_separator=True):
         entity = self.cw_rset.get_entity(0, 0)
-        path = entity.breadcrumbs(view)
+        adapter = ibreadcrumb_adapter(entity)
+        path = adapter.breadcrumbs(view)
         if path:
             self.open_breadcrumbs()
             if first_separator:
@@ -73,7 +132,7 @@
             self.w(u"\n")
             self.wpath_part(parent, contextentity, i == len(path) - 1)
 
-    def wpath_part(self, part, contextentity, last=False):
+    def wpath_part(self, part, contextentity, last=False): # XXX deprecates last argument?
         if isinstance(part, Entity):
             self.w(part.view('breadcrumbs'))
         elif isinstance(part, tuple):
@@ -88,7 +147,7 @@
 
 class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
     __select__ = multi_lines_rset() & one_etype_rset() & \
-                 implements(IBreadCrumbs, accept_none=False)
+                 adaptable('IBreadCrumbs')
 
     def render_breadcrumbs(self, contextentity, path):
         # XXX hack: only display etype name or first non entity path part
--- a/web/views/idownloadable.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/idownloadable.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,29 +15,22 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IDownloadable
+"""Specific views for entities adapting to IDownloadable"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape
 
+from cubicweb import tags
 from cubicweb.view import EntityView
-from cubicweb.selectors import (one_line_rset, score_entity,
-                                implements, match_context_prop)
-from cubicweb.interfaces import IDownloadable
+from cubicweb.selectors import (one_line_rset, is_instance, match_context_prop,
+                                adaptable, has_mimetype)
 from cubicweb.mttransforms import ENGINE
 from cubicweb.web import box, httpcache
 from cubicweb.web.views import primary, baseviews
 
 
-def is_image(entity):
-    mt = entity.download_content_type()
-    if not (mt and mt.startswith('image/')):
-        return 0
-    return 1
-
 def download_box(w, entity, title=None, label=None, footer=u''):
     req = entity._cw
     w(u'<div class="sideBox">')
@@ -47,8 +40,8 @@
       % xml_escape(title))
     w(u'<div class="sideBox downloadBox"><div class="sideBoxBody">')
     w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
-      % (xml_escape(entity.download_url()),
-         req.external_resource('DOWNLOAD_ICON'),
+      % (xml_escape(entity.cw_adapt_to('IDownloadable').download_url()),
+         req.uiprops['DOWNLOAD_ICON'],
          _('download icon'), xml_escape(label or entity.dc_title())))
     w(u'%s</div>' % footer)
     w(u'</div></div>\n')
@@ -58,8 +51,8 @@
     __regid__ = 'download_box'
     # no download box for images
     # XXX primary_view selector ?
-    __select__ = (one_line_rset() & implements(IDownloadable) &
-                  match_context_prop() & ~score_entity(is_image))
+    __select__ = (one_line_rset() & match_context_prop()
+                  & adaptable('IDownloadable') & ~has_mimetype('image/'))
     order = 10
 
     def cell_call(self, row, col, title=None, label=None, **kwargs):
@@ -72,7 +65,7 @@
     downloading of entities providing the necessary interface
     """
     __regid__ = 'download'
-    __select__ = one_line_rset() & implements(IDownloadable)
+    __select__ = one_line_rset() & adaptable('IDownloadable')
 
     templatable = False
     content_type = 'application/octet-stream'
@@ -82,100 +75,129 @@
 
     def set_request_content_type(self):
         """overriden to set the correct filetype and filename"""
-        entity = self.cw_rset.complete_entity(0, 0)
-        encoding = entity.download_encoding()
+        entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+        adapter = entity.cw_adapt_to('IDownloadable')
+        encoding = adapter.download_encoding()
         if encoding in BINARY_ENCODINGS:
             contenttype = 'application/%s' % encoding
             encoding = None
         else:
-            contenttype = entity.download_content_type()
+            contenttype = adapter.download_content_type()
         self._cw.set_content_type(contenttype or self.content_type,
-                                  filename=entity.download_file_name(),
+                                  filename=adapter.download_file_name(),
                                   encoding=encoding)
 
     def call(self):
-        self.w(self.cw_rset.complete_entity(0, 0).download_data())
+        entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+        adapter = entity.cw_adapt_to('IDownloadable')
+        self.w(adapter.download_data())
 
     def last_modified(self):
         return self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0).modification_date
 
+
 class DownloadLinkView(EntityView):
     """view displaying a link to download the file"""
     __regid__ = 'downloadlink'
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
     title = None # should not be listed in possible views
 
 
     def cell_call(self, row, col, title=None, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
-        url = xml_escape(entity.download_url())
+        url = xml_escape(entity.cw_adapt_to('IDownloadable').download_url())
         self.w(u'<a href="%s">%s</a>' % (url, xml_escape(title or entity.dc_title())))
 
 
 class IDownloadablePrimaryView(primary.PrimaryView):
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
 
     def render_entity_attributes(self, entity):
-        super(IDownloadablePrimaryView, self).render_entity_attributes(entity)
         self.w(u'<div class="content">')
-        contenttype = entity.download_content_type()
+        adapter = entity.cw_adapt_to('IDownloadable')
+        contenttype = adapter.download_content_type()
         if contenttype.startswith('image/'):
-            self.wview('image', entity.cw_rset, row=entity.cw_row)
+            self._cw.add_js('cubicweb.image.js')
+            self.wview('image', entity.cw_rset, row=entity.cw_row, col=entity.cw_col,
+                       link=True, klass='contentimage')
+            super(IDownloadablePrimaryView, self).render_entity_attributes(entity)
+        elif contenttype.endswith('html'):
+            self.wview('downloadlink', entity.cw_rset, title=self._cw._('download'), row=entity.cw_row)
+            self.wview('ehtml', entity.cw_rset, row=entity.cw_row, col=entity.cw_col,
+                       height='600px', width='100%')
         else:
+            super(IDownloadablePrimaryView, self).render_entity_attributes(entity)
             self.wview('downloadlink', entity.cw_rset, title=self._cw._('download'), row=entity.cw_row)
+            self.render_data(entity, contenttype, 'text/html')
+        self.w(u'</div>')
+
+    def render_data(self, entity, sourcemt, targetmt):
+        adapter = entity.cw_adapt_to('IDownloadable')
+        if ENGINE.find_path(sourcemt, targetmt):
             try:
-                if ENGINE.has_input(contenttype):
-                    self.w(entity.printable_value('data'))
-            except TransformError:
-                pass
+                self.w(entity._cw_mtc_transform(adapter.download_data(), sourcemt,
+                                                targetmt, adapter.download_encoding()))
             except Exception, ex:
+                self.exception('while rendering data for %s', entity)
                 msg = self._cw._("can't display data, unexpected error: %s") \
-                      % xml_escape(str(ex))
+                      % xml_escape(unicode(ex))
                 self.w('<div class="error">%s</div>' % msg)
-        self.w(u'</div>')
+            return True
+        return False
 
 
 class IDownloadableLineView(baseviews.OneLineView):
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
 
     def cell_call(self, row, col, title=None, **kwargs):
         """the oneline view is a link to download the file"""
         entity = self.cw_rset.get_entity(row, col)
         url = xml_escape(entity.absolute_url())
-        name = xml_escape(title or entity.download_file_name())
-        durl = xml_escape(entity.download_url())
+        adapter = entity.cw_adapt_to('IDownloadable')
+        name = xml_escape(title or adapter.download_file_name())
+        durl = xml_escape(adapter.download_url())
         self.w(u'<a href="%s">%s</a> [<a href="%s">%s</a>]' %
                (url, name, durl, self._cw._('download')))
 
 
-class ImageView(EntityView):
-    __regid__ = 'image'
-    __select__ = implements(IDownloadable) & score_entity(is_image)
+class AbstractEmbeddedView(EntityView):
+    __abstract__ = True
 
-    title = _('image')
+    _embedding_tag = None
 
-    def call(self):
+    def call(self, **kwargs):
         rset = self.cw_rset
         for i in xrange(len(rset)):
             self.w(u'<div class="efile">')
-            self.wview(self.__regid__, rset, row=i, col=0)
+            self.wview(self.__regid__, rset, row=i, col=0, **kwargs)
             self.w(u'</div>')
 
-    def cell_call(self, row, col, width=None, height=None, link=False):
+    def cell_call(self, row, col, link=False, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
-        #if entity.data_format.startswith('image/'):
-        imgtag = u'<img src="%s" alt="%s" ' % (
-            xml_escape(entity.download_url()),
-            (self._cw._('download %s')  % xml_escape(entity.download_file_name())))
-        if width:
-            imgtag += u'width="%i" ' % width
-        if height:
-            imgtag += u'height="%i" ' % height
-        imgtag += u'/>'
+        adapter = entity.cw_adapt_to('IDownloadable')
+        tag = self._embedding_tag(src=adapter.download_url(),
+                                  alt=(self._cw._('download %s') % adapter.download_file_name()),
+                                  **kwargs)
         if link:
-            self.w(u'<a href="%s">%s</a>' % (entity.absolute_url(vid='download'),
-                                             imgtag))
+            self.w(u'<a href="%s">%s</a>' % (adapter.download_url(), tag))
         else:
-            self.w(imgtag)
+            self.w(tag)
 
 
+class ImageView(AbstractEmbeddedView):
+    __regid__ = 'image'
+    __select__ = has_mimetype('image/')
+
+    title = _('image')
+    _embedding_tag = tags.img
+
+
+class EHTMLView(AbstractEmbeddedView):
+    __regid__ = 'ehtml'
+    __select__ = has_mimetype('text/html')
+
+    title = _('embedded html')
+    _embedding_tag = tags.iframe
+
+
+
--- a/web/views/igeocodable.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/igeocodable.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,33 +15,64 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IGeocodable
+"""Specific views for entities implementing IGeocodable"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.interfaces import IGeocodable
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
-from cubicweb.web import json
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
+from cubicweb.utils import json_dumps
+
+class IGeocodableAdapter(EntityAdapter):
+    """interface required by geocoding views such as gmap-view"""
+    __regid__ = 'IGeocodable'
+    __select__ = implements(IGeocodable, warn=False) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('IGeocodable')
+    def latitude(self):
+        """returns the latitude of the entity"""
+        raise NotImplementedError
+
+    @property
+    @implements_adapter_compat('IGeocodable')
+    def longitude(self):
+        """returns the longitude of the entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IGeocodable')
+    def marker_icon(self):
+        """returns the icon that should be used as the marker.
+
+        an icon is defined by a 4-uple:
+
+          (icon._url, icon.size,  icon.iconAnchor, icon.shadow)
+        """
+        return (self._cw.uiprops['GMARKER_ICON'], (20, 34), (4, 34), None)
+
 
 class GeocodingJsonView(EntityView):
     __regid__ = 'geocoding-json'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     binary = True
     templatable = False
     content_type = 'application/json'
 
     def call(self):
-        # remove entities that don't define latitude and longitude
-        self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
         zoomlevel = self._cw.form.pop('zoomlevel', 8)
         extraparams = self._cw.form.copy()
         extraparams.pop('vid', None)
         extraparams.pop('rql', None)
-        markers = [self.build_marker_data(rowidx, extraparams)
-                   for rowidx in xrange(len(self.cw_rset))]
+        markers = []
+        for entity in self.cw_rset.entities():
+            igeocodable = entity.cw_adapt_to('IGeocodable')
+            # remove entities that don't define latitude and longitude
+            if not (igeocodable.latitude and igeocodable.longitude):
+                continue
+            markers.append(self.build_marker_data(entity, igeocodable,
+                                                  extraparams))
         center = {
             'latitude': sum(marker['latitude'] for marker in markers) / len(markers),
             'longitude': sum(marker['longitude'] for marker in markers) / len(markers),
@@ -51,26 +82,21 @@
             'center': center,
             'markers': markers,
             }
-        self.w(json.dumps(geodata))
+        self.w(json_dumps(geodata))
 
-    def build_marker_data(self, row, extraparams):
-        entity = self.cw_rset.get_entity(row, 0)
-        icon = None
-        if hasattr(entity, 'marker_icon'):
-            icon = entity.marker_icon()
-        else:
-            icon = (self._cw.external_resource('GMARKER_ICON'), (20, 34), (4, 34), None)
-        return {'latitude': entity.latitude, 'longitude': entity.longitude,
+    def build_marker_data(self, entity, igeocodable, extraparams):
+        return {'latitude': igeocodable.latitude,
+                'longitude': igeocodable.longitude,
+                'icon': igeocodable.marker_icon(),
                 'title': entity.dc_long_title(),
-                #icon defines : (icon._url, icon.size,  icon.iconAncho', icon.shadow)
-                'icon': icon,
-                'bubbleUrl': entity.absolute_url(vid='gmap-bubble', __notemplate=1, **extraparams),
+                'bubbleUrl': entity.absolute_url(
+                    vid='gmap-bubble', __notemplate=1, **extraparams),
                 }
 
 
 class GoogleMapBubbleView(EntityView):
     __regid__ = 'gmap-bubble'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -80,16 +106,14 @@
 
 class GoogleMapsView(EntityView):
     __regid__ = 'gmap-view'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     paginable = False
 
     def call(self, gmap_key, width=400, height=400, uselabel=True, urlparams=None):
         self._cw.demote_to_html()
-        # remove entities that don't define latitude and longitude
-        self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
-        self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s' % gmap_key,
-                        localfile=False)
+        self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s'
+                        % gmap_key, localfile=False)
         self._cw.add_js( ('cubicweb.widgets.js', 'cubicweb.gmap.js', 'gmap.utility.labeledmarker.js') )
         rql = self.cw_rset.printable_rql()
         if urlparams is None:
@@ -98,7 +122,8 @@
             loadurl = self._cw.build_url(rql=rql, vid='geocoding-json', **urlparams)
         self.w(u'<div style="width: %spx; height: %spx;" class="widget gmap" '
                u'cubicweb:wdgtype="GMapWidget" cubicweb:loadtype="auto" '
-               u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>' % (width, height, loadurl, uselabel))
+               u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>'
+               % (width, height, loadurl, uselabel))
 
 
 class GoogeMapsLegend(EntityView):
--- a/web/views/iprogress.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/iprogress.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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,13 @@
     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
+        done = iprogress.done or 0
+        todo = iprogress.todo or 0
+        revised_cost = iprogress.revised_cost or 0
+        if done + todo > revised_cost:
+            overrun = done + todo - revised_cost
         else:
             overrun = 0
         if overrun < cls.precision:
@@ -187,20 +190,21 @@
         return overrun
 
     @classmethod
-    def overrun_percentage(cls, entity):
+    def overrun_percentage(cls, iprogress):
         """pourcentage overrun = overrun / budget"""
-        if entity.revised_cost == 0:
+        revised_cost = iprogress.revised_cost or 0
+        if revised_cost == 0:
             return 0
-        else:
-            return cls.overrun(entity) * 100. / entity.revised_cost
+        return cls.overrun(iprogress) * 100. / 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 or 0
+        todo = iprogress.todo or 0
+        budget = iprogress.revised_cost or 0
         if budget == 0:
             pourcent = 100
         else:
@@ -229,25 +233,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/isioc.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,20 +15,70 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for SIOC interfaces
+"""Specific views for SIOC (Semantically-Interlinked Online Communities)
 
+http://sioc-project.org
 """
+
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
 from cubicweb.interfaces import ISiocItem, ISiocContainer
 
+
+class ISIOCItemAdapter(EntityAdapter):
+    """interface for entities which may be represented as an ISIOC items"""
+    __regid__ = 'ISIOCItem'
+    __select__ = implements(ISiocItem, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_content(self):
+        """return item's content"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_container(self):
+        """return container entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_type(self):
+        """return container type (post, BlogPost, MailMessage)"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_replies(self):
+        """return replies items"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_topics(self):
+        """return topics items"""
+        raise NotImplementedError
+
+
+class ISIOCContainerAdapter(EntityAdapter):
+    """interface for entities which may be represented as an ISIOC container"""
+    __regid__ = 'ISIOCContainer'
+    __select__ = implements(ISiocContainer, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ISIOCContainer')
+    def isioc_type(self):
+        """return container type (forum, Weblog, MailingList)"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCContainer')
+    def isioc_items(self):
+        """return contained items"""
+        raise NotImplementedError
+
+
 class SIOCView(EntityView):
     __regid__ = 'sioc'
-    __select__ = EntityView.__select__ & implements(ISiocItem, ISiocContainer)
+    __select__ = adaptable('ISIOCItem', 'ISIOCContainer')
     title = _('sioc')
     templatable = False
     content_type = 'text/xml'
@@ -52,48 +102,51 @@
 
 class SIOCContainerView(EntityView):
     __regid__ = 'sioc_element'
-    __select__ = EntityView.__select__ & implements(ISiocContainer)
+    __select__ = adaptable('ISIOCContainer')
     templatable = False
     content_type = 'text/xml'
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
-        sioct = xml_escape(entity.isioc_type())
+        isioc = entity.cw_adapt_to('ISIOCContainer')
+        isioct = isioc.isioc_type()
         self.w(u'<sioc:%s rdf:about="%s">\n'
-               % (sioct, xml_escape(entity.absolute_url())))
+               % (isioct, xml_escape(entity.absolute_url())))
         self.w(u'<dcterms:title>%s</dcterms:title>'
                % xml_escape(entity.dc_title()))
         self.w(u'<dcterms:created>%s</dcterms:created>'
-               % entity.creation_date)
+               % entity.creation_date) # XXX format
         self.w(u'<dcterms:modified>%s</dcterms:modified>'
-               % entity.modification_date)
+               % entity.modification_date) # XXX format
         self.w(u'<!-- FIXME : here be items -->')#entity.isioc_items()
-        self.w(u'</sioc:%s>\n' % sioct)
+        self.w(u'</sioc:%s>\n' % isioct)
 
 
 class SIOCItemView(EntityView):
     __regid__ = 'sioc_element'
-    __select__ = EntityView.__select__ & implements(ISiocItem)
+    __select__ = adaptable('ISIOCItem')
     templatable = False
     content_type = 'text/xml'
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
-        sioct = xml_escape(entity.isioc_type())
+        isioc = entity.cw_adapt_to('ISIOCItem')
+        isioct = isioc.isioc_type()
         self.w(u'<sioc:%s rdf:about="%s">\n'
-               %  (sioct, xml_escape(entity.absolute_url())))
+               % (isioct, xml_escape(entity.absolute_url())))
         self.w(u'<dcterms:title>%s</dcterms:title>'
                % xml_escape(entity.dc_title()))
         self.w(u'<dcterms:created>%s</dcterms:created>'
-               % entity.creation_date)
+               % entity.creation_date) # XXX format
         self.w(u'<dcterms:modified>%s</dcterms:modified>'
-               % entity.modification_date)
-        if entity.content:
-            self.w(u'<sioc:content>%s</sioc:content>'''
-                   % xml_escape(entity.isioc_content()))
-        if entity.related('entry_of'):
+               % entity.modification_date) # XXX format
+        content = isioc.isioc_content()
+        if content:
+            self.w(u'<sioc:content>%s</sioc:content>' % xml_escape(content))
+        container = isioc.isioc_container()
+        if container:
             self.w(u'<sioc:has_container rdf:resource="%s"/>\n'
-                   % xml_escape(entity.isioc_container().absolute_url()))
+                   % xml_escape(container.absolute_url()))
         if entity.creator:
             self.w(u'<sioc:has_creator>\n')
             self.w(u'<sioc:User rdf:about="%s">\n'
@@ -103,5 +156,5 @@
             self.w(u'</sioc:has_creator>\n')
         self.w(u'<!-- FIXME : here be topics -->')#entity.isioc_topics()
         self.w(u'<!-- FIXME : here be replies -->')#entity.isioc_replies()
-        self.w(u' </sioc:%s>\n' % sioct)
+        self.w(u' </sioc:%s>\n' % isioct)
 
--- a/web/views/magicsearch.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/magicsearch.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/management.py	Wed Nov 03 16:38:28 2010 +0100
@@ -122,7 +122,7 @@
                                           cwperm.view('oneline')))
                 else:
                     w(u'<td>%s</td>' % cwperm.view('oneline'))
-                w(u'<td>%s</td>' % self.view('csv', cwperm.related('require_group'), 'null'))
+                w(u'<td>%s</td>' % self._cw.view('csv', cwperm.related('require_group'), 'null'))
                 w(u'</tr>\n')
             w(u'</table>')
         else:
@@ -166,14 +166,14 @@
         """returns a title according to the result set - used for the
         title in the HTML header
         """
-        return self._cw._('an error occured')
+        return self._cw._('an error occurred')
 
     def call(self):
         req = self._cw.reset_headers()
         w = self.w
         ex = req.data.get('ex')#_("unable to find exception information"))
         excinfo = req.data.get('excinfo')
-        title = self._cw._('an error occured')
+        title = self._cw._('an error occurred')
         w(u'<h2>%s</h2>' % title)
         if 'errmsg' in req.data:
             ex = req.data['errmsg']
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/massmailing.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,18 +15,18 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Mass mailing form views
+"""Mass mailing handling: send mail to entities adaptable to IEmailable"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 import operator
 
-from cubicweb.interfaces import IEmailable
-from cubicweb.selectors import implements, authenticated_user
+from cubicweb.selectors import (is_instance, authenticated_user,
+                                adaptable, match_form_params)
 from cubicweb.view import EntityView
-from cubicweb.web import stdmsgs, action, form, formfields as ff
+from cubicweb.web import (Redirect, stdmsgs, controller, action,
+                          form, formfields as ff)
 from cubicweb.web.formwidgets import CheckBox, TextInput, AjaxWidget, ImgButton
 from cubicweb.web.views import forms, formrenderers
 
@@ -34,8 +34,9 @@
 class SendEmailAction(action.Action):
     __regid__ = 'sendemail'
     # XXX should check email is set as well
-    __select__ = (action.Action.__select__ & implements(IEmailable)
-                  & authenticated_user())
+    __select__ = (action.Action.__select__
+                  & authenticated_user()
+                  & adaptable('IEmailable'))
 
     title = _('send email')
     category = 'mainactions'
@@ -49,23 +50,28 @@
 
 
 def recipient_vocabulary(form, field):
-    vocab = [(entity.get_email(), entity.eid) for entity in form.cw_rset.entities()]
+    vocab = [(entity.cw_adapt_to('IEmailable').get_email(), unicode(entity.eid))
+             for entity in form.cw_rset.entities()]
     return [(label, value) for label, value in vocab if label]
 
+
 class MassMailingForm(forms.FieldsForm):
     __regid__ = 'massmailing'
 
-    needs_js = ('cubicweb.widgets.js', 'cubicweb.massmailing.js')
+    needs_js = ('cubicweb.edition.js', 'cubicweb.widgets.js',)
     needs_css = ('cubicweb.mailform.css')
     domid = 'sendmail'
     action = 'sendmail'
 
     sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
                             label=_('From:'),
-                            value=lambda f: '%s <%s>' % (f._cw.user.dc_title(), f._cw.user.get_email()))
+                            value=lambda f: '%s <%s>' % (
+                                f._cw.user.dc_title(),
+                                f._cw.user.cw_adapt_to('IEmailable').get_email()))
     recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'),
                                choices=recipient_vocabulary,
-                               value= lambda f: [entity.eid for entity in f.cw_rset.entities() if entity.get_email()])
+                               value= lambda f: [entity.eid for entity in f.cw_rset.entities()
+                                                 if entity.cw_adapt_to('IEmailable').get_email()])
     subject = ff.StringField(label=_('Subject:'), max_length=256)
     mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
                                                 inputid='mailbody'))
@@ -73,7 +79,7 @@
     form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
                               _('send email'), 'SEND_EMAIL_ICON'),
                     ImgButton('cancelbutton', "javascript: history.back()",
-                              stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
+                              _(stdmsgs.BUTTON_CANCEL[0]), stdmsgs.BUTTON_CANCEL[1])]
     form_renderer_id = __regid__
 
     def __init__(self, *args, **kwargs):
@@ -84,12 +90,12 @@
     def get_allowed_substitutions(self):
         attrs = []
         for coltype in self.cw_rset.column_types(0):
-            eclass = self._cw.vreg['etypes'].etype_class(coltype)
-            attrs.append(eclass.allowed_massmail_keys())
+            entity = self._cw.vreg['etypes'].etype_class(coltype)(self._cw)
+            attrs.append(entity.cw_adapt_to('IEmailable').allowed_massmail_keys())
         return sorted(reduce(operator.and_, attrs))
 
     def build_substitutions_help(self):
-        insertLink = u'<a href="javascript: insertText(\'%%(%s)s\', \'emailarea\');">%%(%s)s</a>'
+        insertLink = u'<a href="javascript: cw.widgets.insertText(\'%%(%s)s\', \'emailarea\');">%%(%s)s</a>'
         substs = (u'<div class="substitution">%s</div>' % (insertLink % (subst, subst))
                   for subst in self.get_allowed_substitutions())
         helpmsg = self._cw._('You can use any of the following substitutions in your text')
@@ -135,9 +141,36 @@
 
 class MassMailingFormView(form.FormViewMixIn, EntityView):
     __regid__ = 'massmailing'
-    __select__ = implements(IEmailable) & authenticated_user()
+    __select__ = authenticated_user() & adaptable('IEmailable')
 
     def call(self):
         form = self._cw.vreg['forms'].select('massmailing', self._cw,
                                              rset=self.cw_rset)
         self.w(form.render())
+
+
+class SendMailController(controller.Controller):
+    __regid__ = 'sendmail'
+    __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
+
+    def recipients(self):
+        """returns an iterator on email's recipients as entities"""
+        eids = self._cw.form['recipient']
+        # eids may be a string if only one recipient was specified
+        if isinstance(eids, basestring):
+            rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
+        else:
+            rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
+        return rset.entities()
+
+    def publish(self, rset=None):
+        # XXX this allows users with access to an cubicweb instance to use it as
+        # a mail relay
+        body = self._cw.form['mailbody']
+        subject = self._cw.form['subject']
+        for recipient in self.recipients():
+            iemailable = recipient.cw_adapt_to('IEmailable')
+            text = body % iemailable.as_email_context()
+            self.sendmail(iemailable.get_email(), subject, text)
+        url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
+        raise Redirect(url)
--- a/web/views/navigation.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/navigation.py	Wed Nov 03 16:38:28 2010 +0100
@@ -25,11 +25,10 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import deprecated
 
-from cubicweb.interfaces import IPrevNext
 from cubicweb.selectors import (paginated_rset, sorted_rset,
-                                primary_view, match_context_prop,
-                                one_line_rset, implements)
+                                adaptable, implements)
 from cubicweb.uilib import cut
+from cubicweb.view import EntityAdapter, implements_adapter_compat
 from cubicweb.web.component import EntityVComponent, NavigationComponent
 
 
@@ -133,7 +132,7 @@
                 if rel is None:
                     continue
                 attrname = rel.r_type
-                if attrname == 'is':
+                if attrname in ('is', 'has_text'):
                     continue
                 if not rschema(attrname).final:
                     col = var.selected_index()
@@ -182,44 +181,74 @@
         self.w(u'</div>')
 
 
+from cubicweb.interfaces import IPrevNext
+
+class IPrevNextAdapter(EntityAdapter):
+    """interface for entities which can be linked to a previous and/or next
+    entity
+    """
+    __regid__ = 'IPrevNext'
+    __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
+
+    @implements_adapter_compat('IPrevNext')
+    def next_entity(self):
+        """return the 'next' entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IPrevNext')
+    def previous_entity(self):
+        """return the 'previous' entity"""
+        raise NotImplementedError
+
+
 class NextPrevNavigationComponent(EntityVComponent):
     __regid__ = 'prevnext'
     # register msg not generated since no entity implements IPrevNext in cubicweb
     # itself
     title = _('contentnavigation_prevnext')
     help = _('contentnavigation_prevnext_description')
-    __select__ = (one_line_rset() & primary_view()
-                  & match_context_prop() & implements(IPrevNext))
+    __select__ = EntityVComponent.__select__ & adaptable('IPrevNext')
     context = 'navbottom'
     order = 10
+
     def call(self, view=None):
-        entity = self.cw_rset.get_entity(0, 0)
-        previous = entity.previous_entity()
-        next = entity.next_entity()
+        self.cell_call(0, 0, view=view)
+
+    def cell_call(self, row, col, view=None):
+        entity = self.cw_rset.get_entity(row, col)
+        adapter = entity.cw_adapt_to('IPrevNext')
+        previous = adapter.previous_entity()
+        next = adapter.next_entity()
         if previous or next:
             textsize = self._cw.property_value('navigation.short-line-size')
             self.w(u'<div class="prevnext">')
             if previous:
-                self.w(u'<div class="previousEntity left">')
-                self.w(self.previous_link(previous, textsize))
-                self.w(u'</div>')
-                self._cw.html_headers.add_raw('<link rel="prev" href="%s" />'
-                                              % xml_escape(previous.absolute_url()))
+                self.previous_div(previous, textsize)
             if next:
-                self.w(u'<div class="nextEntity right">')
-                self.w(self.next_link(next, textsize))
-                self.w(u'</div>')
-                self._cw.html_headers.add_raw('<link rel="next" href="%s" />'
-                                              % xml_escape(next.absolute_url()))
+                self.next_div(next, textsize)
             self.w(u'</div>')
             self.w(u'<div class="clear"></div>')
 
+    def previous_div(self, previous, textsize):
+        self.w(u'<div class="previousEntity left">')
+        self.w(self.previous_link(previous, textsize))
+        self.w(u'</div>')
+        self._cw.html_headers.add_raw('<link rel="prev" href="%s" />'
+                                      % xml_escape(previous.absolute_url()))
+
     def previous_link(self, previous, textsize):
         return u'<a href="%s" title="%s">&lt;&lt; %s</a>' % (
             xml_escape(previous.absolute_url()),
             self._cw._('i18nprevnext_previous'),
             xml_escape(cut(previous.dc_title(), textsize)))
 
+    def next_div(self, next, textsize):
+        self.w(u'<div class="nextEntity right">')
+        self.w(self.next_link(next, textsize))
+        self.w(u'</div>')
+        self._cw.html_headers.add_raw('<link rel="next" href="%s" />'
+                                      % xml_escape(next.absolute_url()))
+
     def next_link(self, next, textsize):
         return u'<a href="%s" title="%s">%s &gt;&gt;</a>' % (
             xml_escape(next.absolute_url()),
@@ -248,9 +277,11 @@
         nav.clean_params(params)
         # make a link to see them all
         if show_all_option:
-            url = xml_escape(req.build_url(__force_display=1, **params))
-            w(u'<span><a href="%s">%s</a></span>\n'
-              % (url, req._('show %s results') % len(rset)))
+            basepath = req.relative_path(includeparams=False)
+            params['__force_display'] = 1
+            url = nav.page_url(basepath, params)
+            w(u'<div><a href="%s">%s</a></div>\n'
+              % (xml_escape(url), req._('show %s results') % len(rset)))
         rset.limit(offset=start, limit=stop-start, inplace=True)
 
 
--- a/web/views/old_calendar.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/old_calendar.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
-
-"""
+"""html calendar views"""
 
 from datetime import date, time, timedelta
 
@@ -26,8 +24,26 @@
                                  next_month, first_day, last_day, date_range)
 
 from cubicweb.interfaces import ICalendarViews
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+class ICalendarViewsAdapter(EntityAdapter):
+    """calendar views interface"""
+    __regid__ = 'ICalendarViews'
+    __select__ = implements(ICalendarViews, warn=False) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ICalendarViews')
+    def matching_dates(self, begin, end):
+        """
+        :param begin: day considered as begin of the range (`DateTime`)
+        :param end: day considered as end of the range (`DateTime`)
+
+        :return:
+          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+          this entity apply
+        """
+        raise NotImplementedError
+
 
 # used by i18n tools
 WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
@@ -39,7 +55,7 @@
 
 class _CalendarView(EntityView):
     """base calendar view containing helpful methods to build calendar views"""
-    __select__ = implements(ICalendarViews,)
+    __select__ = adaptable('ICalendarViews')
     paginable = False
 
     # Navigation building methods / views ####################################
@@ -126,7 +142,7 @@
             infos = u'<div class="event">'
             infos += self._cw.view(itemvid, self.cw_rset, row=row)
             infos += u'</div>'
-            for date_ in entity.matching_dates(begin, end):
+            for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end):
                 day = date(date_.year, date_.month, date_.day)
                 try:
                     dt = time(date_.hour, date_.minute, date_.second)
@@ -288,7 +304,7 @@
             monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
             self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \
                   % (_('week'), monday.isocalendar()[1], monthlink))
-            for day in date_range(monday, sunday):
+            for day in date_range(monday, sunday+ONEDAY):
                 self.w(u'<tr>')
                 self.w(u'<td>%s</td>' % _(WEEKDAYS[day.weekday()]))
                 self.w(u'<td>%s</td>' % (day.strftime('%Y-%m-%d')))
@@ -478,7 +494,7 @@
             w(u'<tr>%s</tr>' % (
                 WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
             w(u'<tr><th>%s</th><th>&#160;</th></tr>'% _(u'Date'))
-            for day in date_range(monday, sunday):
+            for day in date_range(monday, sunday+ONEDAY):
                 events = schedule.get(day)
                 style = day.weekday() % 2 and "even" or "odd"
                 w(u'<tr class="%s">' % style)
--- a/web/views/plots.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/plots.py	Wed Nov 03 16:38:28 2010 +0100
@@ -23,10 +23,9 @@
 from logilab.common.date import datetime2ticks
 from logilab.mtconverter import xml_escape
 
-from cubicweb.utils import UStringIO
+from cubicweb.utils import UStringIO, json_dumps
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import multi_columns_rset
-from cubicweb.web import dumps
 from cubicweb.web.views import baseviews
 
 @objectify_selector
@@ -107,7 +106,7 @@
         #     cf. function onPlotHover in cubicweb.flot.js
         if self.timemode:
             plot = [(datetime2ticks(x), y, datetime2ticks(x)) for x, y in plot]
-        return dumps(plot)
+        return json_dumps(plot)
 
     def _render(self, req, width=500, height=400):
         if req.ie_browser():
--- a/web/views/primary.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/primary.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""The default primary view
+"""The default primary view"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -25,7 +24,7 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb import Unauthorized
+from cubicweb import Unauthorized, NoSelectableObject
 from cubicweb.selectors import match_kwargs
 from cubicweb.view import EntityView
 from cubicweb.schema import VIRTUAL_RTYPES, display_name
@@ -54,7 +53,6 @@
     def cell_call(self, row, col):
         self.cw_row = row
         self.cw_col = col
-        self.maxrelated = self._cw.property_value('navigation.related-limit')
         entity = self.cw_rset.complete_entity(row, col)
         self.render_entity(entity)
 
@@ -130,8 +128,9 @@
         display_attributes = []
         for rschema, _, role, dispctrl in self._section_def(entity, 'attributes'):
             vid = dispctrl.get('vid', 'reledit')
-            if rschema.final or vid == 'reledit':
-                value = entity.view(vid, rtype=rschema.type, role=role)
+            if rschema.final or vid == 'reledit' or dispctrl.get('rtypevid'):
+                value = entity.view(vid, rtype=rschema.type, role=role,
+                                    initargs={'dispctrl': dispctrl})
             else:
                 rset = self._relation_rset(entity, rschema, role, dispctrl)
                 if rset:
@@ -146,23 +145,68 @@
                 try:
                     self._render_attribute(dispctrl, rschema, value,
                                            role=role, table=True)
+                    warn('[3.9] _render_attribute prototype has changed and '
+                         'renamed to render_attribute, please update %s'
+                         % self.__class___, DeprecationWarning)
                 except TypeError:
-                    warn('[3.6] _render_attribute prototype has changed, please'
-                         ' update %s' % self.__class___, DeprecationWarning)
                     self._render_attribute(rschema, value, role=role, table=True)
+                    warn('[3.6] _render_attribute prototype has changed and '
+                         'renamed to render_attribute, please update %s'
+                         % self.__class___, DeprecationWarning)
+                except AttributeError:
+                    label = self._rel_label(entity, rschema, role, dispctrl)
+                    self.render_attribute(label, value, table=True)
             self.w(u'</table>')
 
+    def render_attribute(self, label, value, table=False):
+        self.field(label, value, tr=False, table=table)
+
     def render_entity_relations(self, entity):
         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):
-            rset = self._relation_rset(entity, rschema, role, dispctrl)
-            if rset:
+            if rschema.final or dispctrl.get('rtypevid'):
+                vid = dispctrl.get('vid', 'reledit')
                 try:
-                    self._render_relation(dispctrl, rset, 'autolimited')
-                except TypeError:
-                    warn('[3.6] _render_relation prototype has changed, '
-                         'please update %s' % self.__class__, DeprecationWarning)
-                    self._render_relation(rset, dispctrl, 'autolimited',
-                                          self.show_rel_label)
+                    rview = self._cw.vreg['views'].select(
+                        vid, self._cw, rset=entity.cw_rset, row=entity.cw_row,
+                        col=entity.cw_col, dispctrl=dispctrl,
+                        rtype=rschema, role=role)
+                except NoSelectableObject:
+                    continue
+                value = rview.render(row=entity.cw_row, col=entity.cw_col,
+                                     rtype=rschema.type, role=role)
+            else:
+                rset = self._relation_rset(entity, rschema, role, dispctrl)
+                if not rset:
+                    continue
+                if hasattr(self, '_render_relation'):
+                    try:
+                        self._render_relation(dispctrl, rset, 'autolimited')
+                        warn('[3.9] _render_relation prototype has changed and has '
+                             'been renamed to render_relation, please update %s'
+                             % self.__class__, DeprecationWarning)
+                    except TypeError:
+                        self._render_relation(rset, dispctrl, 'autolimited',
+                                              self.show_rel_label)
+                        warn('[3.6] _render_relation prototype has changed and has '
+                             'been renamed to render_relation, please update %s'
+                             % self.__class__, DeprecationWarning)
+                    continue
+                vid = dispctrl.get('vid', 'autolimited')
+                try:
+                    rview = self._cw.vreg['views'].select(
+                        vid, self._cw, rset=rset, dispctrl=dispctrl)
+                except NoSelectableObject:
+                    continue
+                value = rview.render()
+            label = self._rel_label(entity, rschema, role, dispctrl)
+            self.render_relation(label, value)
+
+    def render_relation(self, label, value):
+        self.w(u'<div class="section">')
+        if label:
+            self.w(u'<h4>%s</h4>' % label)
+        self.w(value)
+        self.w(u'</div>')
 
     def render_side_boxes(self, boxes):
         """display side related relations:
@@ -224,61 +268,58 @@
                 if section == where:
                     matchtschemas.append(tschema)
             if matchtschemas:
-                # XXX pick the latest dispctrl
-                dispctrl = self.display_ctrl.etype_get(eschema, rschema, role,
-                                                       matchtschemas[-1])
-
+                dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, '*')
                 rdefs.append( (rschema, matchtschemas, role, dispctrl) )
         return sorted(rdefs, key=lambda x: x[-1]['order'])
 
     def _relation_rset(self, entity, rschema, role, dispctrl):
         try:
-            dispctrl.setdefault('limit', self.maxrelated)
-            rset = entity.related(rschema.type, role, limit=dispctrl['limit']+1)
+            rset = entity.related(rschema.type, role)
         except Unauthorized:
             return
         if 'filter' in dispctrl:
             rset = dispctrl['filter'](rset)
         return rset
 
-    def _render_relation(self, dispctrl, rset, defaultvid):
-        self.w(u'<div class="section">')
-        if dispctrl.get('showlabel', self.show_rel_label):
-            self.w(u'<h4>%s</h4>' % self._cw._(dispctrl['label']))
-        self.wview(dispctrl.get('vid', defaultvid), rset,
-                   initargs={'dispctrl': dispctrl})
-        self.w(u'</div>')
-
-    def _render_attribute(self, dispctrl, rschema, value,
-                          role='subject', table=False):
+    def _rel_label(self, entity, rschema, role, dispctrl):
         if rschema.final:
             showlabel = dispctrl.get('showlabel', self.show_attr_label)
         else:
             showlabel = dispctrl.get('showlabel', self.show_rel_label)
-        if dispctrl.get('label'):
-            label = self._cw._(dispctrl.get('label'))
-        else:
-            label = display_name(self._cw, rschema.type, role)
-        self.field(label, value, show_label=showlabel, tr=False, table=table)
+        if showlabel:
+            if dispctrl.get('label'):
+                label = self._cw._(dispctrl['label'])
+            else:
+                label = display_name(self._cw, rschema.type, role,
+                                     context=entity.__regid__)
+            return label
+        return u''
 
 
 class RelatedView(EntityView):
+    """Display a rset, usually containing entities linked to another entity
+    being displayed.
+
+    It will try to display nicely according to the number of items in the result
+    set.
+    """
     __regid__ = 'autolimited'
 
     def call(self, **kwargs):
-        # nb: rset is retreived using entity.related with limit + 1 if any.
-        # Because of that, we know that rset.printable_rql() will return rql
-        # with no limit set anyway (since it's handled manually)
         if 'dispctrl' in self.cw_extra_kwargs:
-            limit = self.cw_extra_kwargs['dispctrl'].get('limit')
+            if 'limit' in self.cw_extra_kwargs['dispctrl']:
+                limit = self.cw_extra_kwargs['dispctrl']['limit']
+            else:
+                limit = self._cw.property_value('navigation.related-limit')
+            list_limit = self.cw_extra_kwargs['dispctrl'].get('use_list_limit', 5)
             subvid = self.cw_extra_kwargs['dispctrl'].get('subvid', 'incontext')
         else:
-            limit = None
+            limit = list_limit = None
             subvid = 'incontext'
         if limit is None or self.cw_rset.rowcount <= limit:
             if self.cw_rset.rowcount == 1:
                 self.wview(subvid, self.cw_rset, row=0)
-            elif 1 < self.cw_rset.rowcount <= 5:
+            elif list_limit is None or 1 < self.cw_rset.rowcount <= list_limit:
                 self.wview('csv', self.cw_rset, subvid=subvid)
             else:
                 self.w(u'<div>')
@@ -288,12 +329,18 @@
         else:
             rql = self.cw_rset.printable_rql()
             self.cw_rset.limit(limit) # remove extra entity
-            self.w(u'<div>')
-            self.wview('simplelist', self.cw_rset, subvid=subvid)
-            self.w(u'[<a href="%s">%s</a>]' % (
-                xml_escape(self._cw.build_url(rql=rql, vid=subvid)),
-                self._cw._('see them all')))
-            self.w(u'</div>')
+            if list_limit is None:
+                self.wview('csv', self.cw_rset, subvid=subvid)
+                self.w(u'[<a href="%s">%s</a>]' % (
+                    xml_escape(self._cw.build_url(rql=rql, vid=subvid)),
+                    self._cw._('see them all')))
+            else:
+                self.w(u'<div>')
+                self.wview('simplelist', self.cw_rset, subvid=subvid)
+                self.w(u'[<a href="%s">%s</a>]' % (
+                    xml_escape(self._cw.build_url(rql=rql, vid=subvid)),
+                    self._cw._('see them all')))
+                self.w(u'</div>')
 
 
 class URLAttributeView(EntityView):
@@ -309,6 +356,28 @@
         if url:
             self.w(u'<a href="%s">%s</a>' % (url, url))
 
+class AttributeView(EntityView):
+    """use this view on an entity as an alternative to more sophisticated
+    views such as reledit.
+
+    Ex. usage:
+
+    uicfg.primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'})
+    """
+    __regid__ = 'attribute'
+    __select__ = EntityView.__select__ & match_kwargs('rtype')
+
+    def cell_call(self, row, col, rtype, **kwargs):
+        entity = self.cw_rset.get_entity(row, col)
+        if self._cw.vreg.schema.rschema(rtype).final:
+            self.w(entity.printable_value(rtype))
+        else:
+            dispctrl = uicfg.primaryview_display_ctrl.etype_get(
+                entity.e_schema, rtype, kwargs['role'], '*')
+            rset = entity.related(rtype, role)
+            if rset:
+                self.wview('autolimited', rset, initargs={'dispctrl': dispctrl})
+
 
 ## default primary ui configuration ###########################################
 
--- a/web/views/pyviews.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/pyviews.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,14 +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/>.
-"""Views to display bare python values
-
+"""Basic views for python values (eg without any result set)
 """
 __docformat__ = "restructuredtext en"
 
 from cubicweb.view import View
 from cubicweb.selectors import match_kwargs
 
+
 class PyValTableView(View):
     __regid__ = 'pyvaltable'
     __select__ = match_kwargs('pyvalue')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/reledit.py	Wed Nov 03 16:38:28 2010 +0100
@@ -0,0 +1,379 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""the 'reedit' feature (eg edit attribute/relation from primary view)
+"""
+
+import copy
+from warnings import warn
+
+from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import deprecated
+
+from cubicweb import neg_role
+from cubicweb.schema import display_name
+from cubicweb.utils import json_dumps
+from cubicweb.selectors import non_final_entity, match_kwargs
+from cubicweb.view import EntityView
+from cubicweb.web import uicfg, stdmsgs
+from cubicweb.web.form import FieldNotFound
+from cubicweb.web.formwidgets import Button, SubmitButton
+
+class _DummyForm(object):
+    __slots__ = ('event_args',)
+    def form_render(self, **_args):
+        return u''
+    def render(self, *_args, **_kwargs):
+        return u''
+    def append_field(self, *args):
+        pass
+
+rctrl = uicfg.reledit_ctrl
+
+class ClickAndEditFormView(EntityView):
+    __regid__ = 'doreledit'
+    __select__ = non_final_entity() & match_kwargs('rtype')
+
+    # ui side continuations
+    _onclick = (u"cw.reledit.loadInlineEditionForm('%(formid)s', %(eid)s, '%(rtype)s', '%(role)s', "
+                "'%(divid)s', %(reload)s, '%(vid)s');")
+    _cancelclick = "cw.reledit.cleanupAfterCancel('%s')"
+
+    # ui side actions/buttons
+    _addzone = u'<img title="%(msg)s" src="data/plus.png" alt="%(msg)s"/>'
+    _addmsg = _('click to add a value')
+    _deletezone = u'<img title="%(msg)s" src="data/cancel.png" alt="%(msg)s"/>'
+    _deletemsg = _('click to delete this value')
+    _editzone = u'<img title="%(msg)s" src="data/pen_icon.png" alt="%(msg)s"/>'
+    _editzonemsg = _('click to edit this field')
+
+    # renderer
+    _form_renderer_id = 'base'
+
+    def cell_call(self, row, col, rtype=None, role='subject',
+                  reload=False, # controls reloading the whole page after change
+                                # boolean, eid (to redirect), or
+                                # function taking the subject entity & returning a boolean or an eid
+                  rvid=None,    # vid to be applied to other side of rtype (non final relations only)
+                  default_value=None,
+                  formid='base'
+                  ):
+        """display field to edit entity's `rtype` relation on click"""
+        assert rtype
+        assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
+        self._cw.add_css('cubicweb.form.css')
+        self._cw.add_js('cubicweb.reledit.js', 'cubicweb.edition.js')
+        entity = self.cw_rset.get_entity(row, col)
+        rschema = self._cw.vreg.schema[rtype]
+        self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
+        if rvid is not None or default_value is not None:
+            warn('[3.9] specifying rvid/default_value on select is deprecated, '
+                 'reledit_ctrl rtag to control this' % self, DeprecationWarning)
+        reload = self._compute_reload(entity, rschema, role, reload)
+        divid = self._build_divid(rtype, role, entity.eid)
+        if rschema.final:
+            self._handle_attribute(entity, rschema, role, divid, reload)
+        else:
+            if self._is_composite():
+                self._handle_composite(entity, rschema, role, divid, reload, formid)
+            else:
+                self._handle_relation(entity, rschema, role, divid, reload, formid)
+
+    def _handle_attribute(self, entity, rschema, role, divid, reload):
+        rtype = rschema.type
+        value = entity.printable_value(rtype)
+        if not self._should_edit_attribute(entity, rschema):
+            self.w(value)
+            return
+        display_label, related_entity = self._prepare_form(entity, rtype, role)
+        form, renderer = self._build_form(entity, rtype, role, divid, 'base',
+                                          reload, display_label, related_entity)
+        value = value or self._compute_default_value(rschema, role)
+        self.view_form(divid, value, form, renderer)
+
+    def _compute_formid_value(self, entity, rschema, role, rvid, formid):
+        related_rset = entity.related(rschema.type, role)
+        if related_rset:
+            value = self._cw.view(rvid, related_rset)
+        else:
+            value = self._compute_default_value(rschema, role)
+        if not self._should_edit_relation(entity, rschema, role):
+            return None, value
+        return formid, value
+
+    def _handle_relation(self, entity, rschema, role, divid, reload, formid):
+        rvid = self._rules.get('rvid', 'autolimited')
+        formid, value = self._compute_formid_value(entity, rschema, role, rvid, formid)
+        if formid is None:
+            return self.w(value)
+        rtype = rschema.type
+        display_label, related_entity = self._prepare_form(entity, rtype, role)
+        form, renderer = self._build_form(entity, rtype, role, divid, formid, reload,
+                                          display_label, related_entity, dict(vid=rvid))
+        self.view_form(divid, value, form, renderer)
+
+    def _handle_composite(self, entity, rschema, role, divid, reload, formid):
+        # this is for attribute-like composites (1 target type, 1 related entity at most, for now)
+        ttypes = self._compute_ttypes(rschema, role)
+        related_rset = entity.related(rschema.type, role)
+        add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes)
+        edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes)
+        delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role)
+        rvid = self._rules.get('rvid', 'autolimited')
+        formid, value = self._compute_formid_value(entity, rschema, role, rvid, formid)
+        if formid is None or not (edit_related or add_related):
+            # till we learn to handle cases where not (edit_related or add_related)
+            self.w(value)
+            return
+        rtype = rschema.type
+        ttype = ttypes[0]
+        _fdata = self._prepare_composite_form(entity, rtype, role, edit_related,
+                                              add_related and ttype)
+        display_label, related_entity = _fdata
+        form, renderer = self._build_form(entity, rtype, role, divid, formid, reload,
+                                          display_label, related_entity, dict(vid=rvid))
+        self.view_form(divid, value, form, renderer,
+                       edit_related, add_related, delete_related)
+
+    def _compute_ttypes(self, rschema, role):
+        dual_role = neg_role(role)
+        return getattr(rschema, '%ss' % dual_role)()
+
+    def _compute_reload(self, entity, rschema, role, reload):
+        ctrl_reload = self._rules.get('reload', reload)
+        if callable(ctrl_reload):
+            ctrl_reload = ctrl_reload(entity)
+        if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False
+            ctrl_reload = self._cw.build_url(ctrl_reload)
+        return ctrl_reload
+
+    def _compute_default_value(self, rschema, role):
+        default = self._rules.get('novalue_label')
+        if default is None:
+            if self._rules.get('novalue_include_rtype'):
+                default = self._cw._('<%s not specified>') % display_name(
+                    self._cw, rschema.type, role)
+            else:
+                default = self._cw._('<not specified>')
+        return xml_escape(default)
+
+    def _is_composite(self):
+        return self._rules.get('edit_target') == 'related'
+
+    def _may_add_related(self, related_rset, entity, rschema, role, ttypes):
+        """ ok for attribute-like composite entities """
+        if len(ttypes) > 1: # many etypes: learn how to do it
+            return False
+        rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role)
+        card = rdef.role_cardinality(role)
+        if related_rset or card not in '?1':
+            return False
+        if role == 'subject':
+            kwargs = {'fromeid': entity.eid}
+        else:
+            kwargs = {'toeid': entity.eid}
+        return rdef.has_perm(self._cw, 'add', **kwargs)
+
+    def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes):
+        """ controls the edition of the related entity """
+        if len(ttypes) > 1 or len(related_rset.rows) != 1:
+            return False
+        if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1':
+            return False
+        return related_rset.get_entity(0, 0).cw_has_perm('update')
+
+    def _may_delete_related(self, related_rset, entity, rschema, role):
+        # we assume may_edit_related, only 1 related entity
+        if not related_rset:
+            return False
+        rentity = related_rset.get_entity(0, 0)
+        if role == 'subject':
+            kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid}
+        else:
+            kwargs = {'fromeid': rentity.eid, 'toeid': entity.eid}
+        # NOTE: should be sufficient given a well built schema/security
+        return rschema.has_perm(self._cw, 'delete', **kwargs)
+
+    def _build_edit_zone(self):
+        return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))}
+
+    def _build_delete_zone(self):
+        return self._deletezone % {'msg': xml_escape(self._cw._(self._deletemsg))}
+
+    def _build_add_zone(self):
+        return self._addzone % {'msg': xml_escape(self._cw._(self._addmsg))}
+
+    def _build_divid(self, rtype, role, entity_eid):
+        """ builds an id for the root div of a reledit widget """
+        return '%s-%s-%s' % (rtype, role, entity_eid)
+
+    def _build_args(self, entity, rtype, role, formid, reload,
+                    extradata=None):
+        divid = self._build_divid(rtype, role, entity.eid)
+        event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'formid': formid,
+                      'reload' : json_dumps(reload),
+                      'role' : role, 'vid' : u''}
+        if extradata:
+            event_args.update(extradata)
+        return event_args
+
+    def _prepare_form(self, entity, _rtype, role):
+        display_label = False
+        related_entity = entity
+        return display_label, related_entity
+
+    def _prepare_composite_form(self, entity, rtype, role, edit_related, add_related):
+        display_label = True
+        if edit_related and not add_related:
+            related_entity = entity.related(rtype, role).get_entity(0, 0)
+        elif add_related:
+            _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw)
+            _new_entity.eid = self._cw.varmaker.next()
+            related_entity = _new_entity
+            # XXX see forms.py ~ 276 and entities.linked_to method
+            #     is there another way ?
+            self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role))
+        return display_label, related_entity
+
+    def _build_renderer(self, related_entity, display_label):
+        return self._cw.vreg['formrenderers'].select(
+            self._form_renderer_id, self._cw, entity=related_entity,
+            display_label=display_label,
+            table_class='attributeForm' if display_label else '',
+            display_help=False, button_bar_class='buttonbar',
+            display_progress_div=False)
+
+    def _build_form(self, entity, rtype, role, divid, formid, reload,
+                    display_label, related_entity, extradata=None, **formargs):
+        event_args = self._build_args(entity, rtype, role, formid,
+                                      reload, extradata)
+        cancelclick = self._cancelclick % divid
+        form = self._cw.vreg['forms'].select(
+            formid, self._cw, rset=related_entity.as_rset(), entity=related_entity,
+            domid='%s-form' % divid, formtype='inlined',
+            action=self._cw.build_url('validateform', __onsuccess='window.parent.cw.reledit.onSuccess'),
+            cwtarget='eformframe', cssclass='releditForm',
+            **formargs)
+        # pass reledit arguments
+        for pname, pvalue in event_args.iteritems():
+            form.add_hidden('__reledit|' + pname, pvalue)
+        # handle buttons
+        if form.form_buttons: # edition, delete
+            form_buttons = []
+            for button in form.form_buttons:
+                if not button.label.endswith('apply'):
+                    if button.label.endswith('cancel'):
+                        button = copy.deepcopy(button)
+                        button.cwaction = None
+                        button.onclick = cancelclick
+                    form_buttons.append(button)
+            form.form_buttons = form_buttons
+        else: # base
+            form.form_buttons = [SubmitButton(),
+                                 Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)]
+        form.event_args = event_args
+        if formid == 'base':
+            field = form.field_by_name(rtype, role, entity.e_schema)
+            form.append_field(field)
+        return form, self._build_renderer(related_entity, display_label)
+
+    def _should_edit_attribute(self, entity, rschema):
+        rdef = entity.e_schema.rdef(rschema)
+        # check permissions
+        if not entity.cw_has_perm('update'):
+            return False
+        rdef = entity.e_schema.rdef(rschema)
+        return rdef.has_perm(self._cw, 'update', eid=entity.eid)
+
+    should_edit_attributes = deprecated('[3.9] should_edit_attributes is deprecated,'
+                                        ' use _should_edit_attribute instead',
+                                        _should_edit_attribute)
+
+    def _should_edit_relation(self, entity, rschema, role):
+        eeid = entity.eid
+        perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid}
+        return rschema.has_perm(self._cw, 'add', **perm_args)
+
+    should_edit_relations = deprecated('[3.9] should_edit_relations is deprecated,'
+                                       ' use _should_edit_relation instead',
+                                       _should_edit_relation)
+
+    def _open_form_wrapper(self, divid, value, form, renderer,
+                           _edit_related, _add_related, _delete_related):
+        w = self.w
+        w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s" class="%(css)s">' %
+          {'id': divid, 'css': 'releditField',
+           'out': "jQuery('#%s').addClass('hidden')" % divid,
+           'over': "jQuery('#%s').removeClass('hidden')" % divid})
+        w(u'<div id="%s-value" class="editableFieldValue">' % divid)
+        w(value)
+        w(u'</div>')
+        w(form.render(renderer=renderer))
+        w(u'<div id="%s" class="editableField hidden">' % divid)
+
+    def _edit_action(self, divid, args, edit_related, add_related, _delete_related):
+        if not add_related: # currently, excludes edition
+            w = self.w
+            args['formid'] = 'edition' if edit_related else 'base'
+            w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg)))
+            w(self._build_edit_zone())
+            w(u'</div>')
+
+    def _add_action(self, divid, args, _edit_related, add_related, _delete_related):
+        if add_related:
+            w = self.w
+            args['formid'] = 'edition' if add_related else 'base'
+            w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg)))
+            w(self._build_add_zone())
+            w(u'</div>')
+
+    def _del_action(self, divid, args, _edit_related, _add_related, delete_related):
+        if delete_related:
+            w = self.w
+            args['formid'] = 'deleteconf'
+            w(u'<div id="%s-delete" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg)))
+            w(self._build_delete_zone())
+            w(u'</div>')
+
+    def _close_form_wrapper(self):
+        self.w(u'</div>')
+        self.w(u'</div>')
+
+    def view_form(self, divid, value, form=None, renderer=None,
+                  edit_related=False, add_related=False, delete_related=False):
+        self._open_form_wrapper(divid, value, form, renderer,
+                                edit_related, add_related, delete_related)
+        args = form.event_args.copy()
+        self._edit_action(divid, args, edit_related, add_related, delete_related)
+        self._add_action(divid, args, edit_related, add_related, delete_related)
+        self._del_action(divid, args, edit_related, add_related, delete_related)
+        self._close_form_wrapper()
+
+
+class AutoClickAndEditFormView(ClickAndEditFormView):
+    __regid__ = 'reledit'
+
+    def _build_form(self, entity, rtype, role, divid, formid, reload,
+                    display_label, related_entity, extradata=None, **formargs):
+        event_args = self._build_args(entity, rtype, role, 'base',
+                                      reload, extradata)
+        form = _DummyForm()
+        form.event_args = event_args
+        return form, None
--- a/web/views/schema.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/schema.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,27 +15,31 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for schema related entities
+"""Specific views for schema related entities"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from itertools import cycle
 
+import tempfile
+import os, os.path as osp
+
+from logilab.common.graph import GraphGenerator, DotBackend
 from logilab.common.ureports import Section, Table
 from logilab.mtconverter import xml_escape
 from yams import BASE_TYPES, schema2dot as s2d
 from yams.buildobjs import DEFAULT_ATTRPERMS
 
-from cubicweb.selectors import (implements, yes, match_user_groups,
-                                has_related_entities, authenticated_user)
+from cubicweb.selectors import (is_instance, match_user_groups, match_kwargs,
+                                has_related_entities, authenticated_user, yes)
 from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                              WORKFLOW_TYPES, INTERNAL_TYPES)
+from cubicweb.utils import make_uid
 from cubicweb.view import EntityView, StartupView
 from cubicweb import tags, uilib
 from cubicweb.web import action, facet, uicfg, schemaviewer
 from cubicweb.web.views import TmpFileViewMixin
-from cubicweb.web.views import primary, baseviews, tabs, tableview, iprogress
+from cubicweb.web.views import primary, baseviews, tabs, tableview, ibreadcrumbs
 
 ALWAYS_SKIP_TYPES = BASE_TYPES | SCHEMA_TYPES
 SKIP_TYPES  = (ALWAYS_SKIP_TYPES | META_RTYPES | SYSTEM_RTYPES | WORKFLOW_TYPES
@@ -83,7 +87,7 @@
         self._cw.add_css('cubicweb.acl.css')
         w = self.w
         _ = self._cw._
-        w(u'<table class="schemaInfo">')
+        w(u'<table class="listing schemaInfo">')
         w(u'<tr><th>%s</th><th>%s</th><th>%s</th></tr>' % (
             _("permission"), _('granted to groups'), _('rql expressions')))
         for action in erschema.ACTIONS:
@@ -109,7 +113,7 @@
         perms = {}
         for rdef in rschema.rdefs.itervalues():
             rdef_perms = []
-            for action in ('read', 'add', 'delete'):
+            for action in rdef.ACTIONS:
                 groups = sorted(rdef.get_groups(action))
                 exprs = sorted(e.expression for e in rdef.get_rqlexprs(action))
                 rdef_perms.append( (action, (tuple(groups), tuple(exprs))) )
@@ -158,10 +162,7 @@
         self.w(u'<div><a href="%s">%s</a></div>' %
                (self._cw.build_url('view', vid='owl'),
                 self._cw._(u'Download schema as OWL')))
-        self.w(u'<img src="%s" alt="%s"/>\n' % (
-            xml_escape(self._cw.build_url('view', vid='schemagraph', skipmeta=1)),
-            self._cw._("graphical representation of the instance'schema")))
-
+        self.wview('schemagraph')
 
 class SchemaETypeTab(StartupView):
     __regid__ = 'schema-entity-types'
@@ -202,13 +203,13 @@
         url = xml_escape(self._cw.build_url('schema'))
         self.w(u'<div id="schema_security">')
         self.w(u'<h2 class="schema">%s</h2>' % _('Index'))
-        self.w(u'<h4 id="entities">%s</h4>' % _('Entity types'))
+        self.w(u'<h3 id="entities">%s</h3>' % _('Entity types'))
         ents = []
         for eschema in sorted(entities):
             ents.append(u'<a class="grey" href="%s#%s">%s</a>' % (
                 url,  eschema.type, eschema.type))
         self.w(u', '.join(ents))
-        self.w(u'<h4 id="relations">%s</h4>' % _('Relation types'))
+        self.w(u'<h3 id="relations">%s</h3>' % _('Relation types'))
         rels = []
         for rschema in sorted(relations):
             rels.append(u'<a class="grey" href="%s#%s">%s</a>' %  (
@@ -248,7 +249,7 @@
                 eschema.type, self._cw.build_url('cwetype/%s' % eschema.type),
                 eschema.type, _(eschema.type)))
             self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
-                url,  self._cw.external_resource('UP_ICON'), _('up')))
+                url,  self._cw.uiprops['UP_ICON'], _('up')))
             self.w(u'</h3>')
             self.w(u'<div style="margin: 0px 1.5em">')
             self.permissions_table(eschema)
@@ -277,7 +278,7 @@
                 rschema.type, self._cw.build_url('cwrtype/%s' % rschema.type),
                 rschema.type, _(rschema.type)))
             self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
-                url,  self._cw.external_resource('UP_ICON'), _('up')))
+                url,  self._cw.uiprops['UP_ICON'], _('up')))
             self.w(u'</h3>')
             self.grouped_permissions_table(rschema)
 
@@ -288,7 +289,7 @@
 _('i18ncard_1'), _('i18ncard_?'), _('i18ncard_+'), _('i18ncard_*')
 
 class CWETypePrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWEType')
+    __select__ = is_instance('CWEType')
     tabs = [_('cwetype-description'), _('cwetype-box'), _('cwetype-workflow'),
             _('cwetype-views'), _('cwetype-permissions')]
     default_tab = 'cwetype-description'
@@ -296,14 +297,14 @@
 
 class CWETypeDescriptionTab(tabs.PrimaryTab):
     __regid__ = 'cwetype-description'
-    __select__ = tabs.PrimaryTab.__select__ & implements('CWEType')
+    __select__ = tabs.PrimaryTab.__select__ & is_instance('CWEType')
 
     def render_entity_attributes(self, entity):
         super(CWETypeDescriptionTab, self).render_entity_attributes(entity)
         _ = self._cw._
         # inheritance
         if entity.specializes:
-            self.w(u'<div>%s' % _('Parent classes:'))
+            self.w(u'<div>%s' % _('Parent class:'))
             self.wview('csv', entity.related('specializes', 'subject'))
             self.w(u'</div>')
         if entity.reverse_specializes:
@@ -311,9 +312,7 @@
             self.wview('csv', entity.related('specializes', 'object'))
             self.w(u'</div>')
         # entity schema image
-        self.w(u'<img src="%s" alt="%s"/>' % (
-            xml_escape(entity.absolute_url(vid='schemagraph')),
-            xml_escape(_('graphical schema for %s') % entity.name)))
+        self.wview('schemagraph', etype=entity.name)
         # entity schema attributes
         self.w(u'<h2>%s</h2>' % _('CWAttribute_plural'))
         rset = self._cw.execute(
@@ -380,7 +379,7 @@
 
 class CWETypeBoxTab(EntityView):
     __regid__ = 'cwetype-box'
-    __select__ = implements('CWEType')
+    __select__ = is_instance('CWEType')
 
     def cell_call(self, row, col):
         viewer = schemaviewer.SchemaViewer(self._cw)
@@ -393,7 +392,7 @@
 
 class CWETypePermTab(SecurityViewMixIn, EntityView):
     __regid__ = 'cwetype-permissions'
-    __select__ = implements('CWEType') & authenticated_user()
+    __select__ = is_instance('CWEType') & authenticated_user()
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -413,7 +412,7 @@
 
 class CWETypeWorkflowTab(EntityView):
     __regid__ = 'cwetype-workflow'
-    __select__ = (implements('CWEType')
+    __select__ = (is_instance('CWEType')
                   & has_related_entities('workflow_of', 'object'))
 
     def cell_call(self, row, col):
@@ -444,7 +443,7 @@
 class CWETypeViewsTab(EntityView):
     """possible views for this entity type"""
     __regid__ = 'cwetype-views'
-    __select__ = EntityView.__select__ & implements('CWEType')
+    __select__ = EntityView.__select__ & is_instance('CWEType')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -464,7 +463,7 @@
 
 
 class CWETypeOneLineView(baseviews.OneLineView):
-    __select__ = implements('CWEType')
+    __select__ = is_instance('CWEType')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
@@ -478,22 +477,20 @@
 # CWRType ######################################################################
 
 class CWRTypePrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWRType')
+    __select__ = is_instance('CWRType')
     tabs = [_('cwrtype-description'), _('cwrtype-permissions')]
     default_tab = 'cwrtype-description'
 
 
 class CWRTypeDescriptionTab(tabs.PrimaryTab):
     __regid__ = 'cwrtype-description'
-    __select__ = implements('CWRType')
+    __select__ = is_instance('CWRType')
 
     def render_entity_attributes(self, entity):
         super(CWRTypeDescriptionTab, self).render_entity_attributes(entity)
         _ = self._cw._
         if not entity.final:
-            msg = _('graphical schema for %s') % entity.name
-            self.w(tags.img(src=entity.absolute_url(vid='schemagraph'),
-                            alt=msg))
+            self.wview('schemagraph', rtype=entity.name)
         rset = self._cw.execute('Any R,C,R,R, RT WHERE '
                                 'R relation_type RT, RT eid %(x)s, '
                                 'R cardinality C', {'x': entity.eid})
@@ -506,7 +503,7 @@
 
 class CWRTypePermTab(SecurityViewMixIn, EntityView):
     __regid__ = 'cwrtype-permissions'
-    __select__ = implements('CWRType') & authenticated_user()
+    __select__ = is_instance('CWRType') & authenticated_user()
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -517,14 +514,14 @@
 # CWAttribute / CWRelation #####################################################
 
 class RDEFPrimaryView(tabs.TabbedPrimaryView):
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
     tabs = [_('rdef-description'), _('rdef-permissions')]
     default_tab = 'rdef-description'
 
 
 class RDEFDescriptionTab(tabs.PrimaryTab):
     __regid__ = 'rdef-description'
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
 
     def render_entity_attributes(self, entity):
         super(RDEFDescriptionTab, self).render_entity_attributes(entity)
@@ -536,7 +533,7 @@
 
 class RDEFPermTab(SecurityViewMixIn, EntityView):
     __regid__ = 'rdef-permissions'
-    __select__ = implements('CWRelation', 'CWAttribute') & authenticated_user()
+    __select__ = is_instance('CWRelation', 'CWAttribute') & authenticated_user()
 
     def cell_call(self, row, col):
         self.permissions_table(self.cw_rset.get_entity(row, col).yams_schema())
@@ -548,7 +545,7 @@
     for instance)
     """
     __regid__ = 'rdef-name-cell'
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -561,7 +558,7 @@
     """same as RDEFNameView but when the context is the object entity
     """
     __regid__ = 'rdef-object-name-cell'
-    __select__ = implements('CWRelation', 'CWAttribute')
+    __select__ = is_instance('CWRelation', 'CWAttribute')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -572,7 +569,7 @@
 
 class RDEFConstraintsCell(EntityView):
     __regid__ = 'rdef-constraints-cell'
-    __select__ = implements('CWAttribute', 'CWRelation')
+    __select__ = is_instance('CWAttribute', 'CWRelation')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -583,7 +580,7 @@
 
 class CWAttributeOptionsCell(EntityView):
     __regid__ = 'rdef-options-cell'
-    __select__ = implements('CWAttribute')
+    __select__ = is_instance('CWAttribute')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -598,7 +595,7 @@
 
 class CWRelationOptionsCell(EntityView):
     __regid__ = 'rdef-options-cell'
-    __select__ = implements('CWRelation',)
+    __select__ = is_instance('CWRelation',)
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -644,48 +641,152 @@
                            s2d.OneHopRSchemaVisitor):
     pass
 
+class CWSchemaDotPropsHandler(s2d.SchemaDotPropsHandler):
+    def __init__(self, visitor):
+        self.visitor = visitor
+        self.nextcolor = cycle( ('#ff7700', '#000000',
+                                 '#ebbc69', '#888888') ).next
+        self.colors = {}
 
-class SchemaImageView(TmpFileViewMixin, StartupView):
-    __regid__ = 'schemagraph'
-    content_type = 'image/png'
+    def node_properties(self, eschema):
+        """return DOT drawing options for an entity schema include href"""
+        label = ['{',eschema.type,'|']
+        label.append(r'\l'.join('%s (%s)' % (rel.type, eschema.rdef(rel.type).object)
+                                for rel in eschema.ordered_relations()
+                                    if rel.final and self.visitor.should_display_attr(eschema, rel)))
+        label.append(r'\l}') # trailing \l ensure alignement of the last one
+        return {'label' : ''.join(label), 'shape' : "record",
+                'fontname' : "Courier", 'style' : "filled",
+                'href': 'cwetype/%s' % eschema.type,
+                'fontsize': '10px'
+                }
 
-    def _generate(self, tmpfile):
-        """display global schema information"""
-        visitor = FullSchemaVisitor(self._cw, self._cw.vreg.schema,
-                                    skiptypes=skip_types(self._cw))
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
+    def edge_properties(self, rschema, subjnode, objnode):
+        """return default DOT drawing options for a relation schema"""
+        # symmetric rels are handled differently, let yams decide what's best
+        if rschema.symmetric:
+            kwargs = {'label': rschema.type,
+                      'color': '#887788', 'style': 'dashed',
+                      'dir': 'both', 'arrowhead': 'normal', 'arrowtail': 'normal',
+                      'fontsize': '10px', 'href': 'cwrtype/%s' % rschema.type}
+        else:
+            kwargs = {'label': rschema.type,
+                      'color' : 'black',  'style' : 'filled', 'fontsize': '10px',
+                      'href': 'cwrtype/%s' % rschema.type}
+            rdef = rschema.rdef(subjnode, objnode)
+            composite = rdef.composite
+            if rdef.composite == 'subject':
+                kwargs['arrowhead'] = 'none'
+                kwargs['arrowtail'] = 'diamond'
+            elif rdef.composite == 'object':
+                kwargs['arrowhead'] = 'diamond'
+                kwargs['arrowtail'] = 'none'
+            else:
+                kwargs['arrowhead'] = 'open'
+                kwargs['arrowtail'] = 'none'
+            # UML like cardinalities notation, omitting 1..1
+            if rdef.cardinality[1] != '1':
+                kwargs['taillabel'] = s2d.CARD_MAP[rdef.cardinality[1]]
+            if rdef.cardinality[0] != '1':
+                kwargs['headlabel'] = s2d.CARD_MAP[rdef.cardinality[0]]
+            try:
+                kwargs['color'] = self.colors[rschema]
+            except KeyError:
+                kwargs['color'] = self.nextcolor()
+                self.colors[rschema] = kwargs['color']
+        kwargs['fontcolor'] = kwargs['color']
+        # dot label decoration is just awful (1 line underlining the label
+        # + 1 line going to the closest edge spline point)
+        kwargs['decorate'] = 'false'
+        #kwargs['labelfloat'] = 'true'
+        return kwargs
 
 
-class CWETypeSchemaImageView(TmpFileViewMixin, EntityView):
+class SchemaGraphView(StartupView):
     __regid__ = 'schemagraph'
-    __select__ = implements('CWEType')
-    content_type = 'image/png'
 
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
-        eschema = self._cw.vreg.schema.eschema(entity.name)
-        visitor = OneHopESchemaVisitor(self._cw, eschema,
-                                       skiptypes=skip_types(self._cw))
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
-
+    def call(self, etype=None, rtype=None, alt=''):
+        schema = self._cw.vreg.schema
+        if etype:
+            assert rtype is None
+            visitor = OneHopESchemaVisitor(self._cw, schema.eschema(etype),
+                                           skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of the %(etype)s '
+                             'entity type from %(appid)s data model')
+        elif rtype:
+            visitor = OneHopRSchemaVisitor(self._cw, schema.rschema(rtype),
+                                           skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of the %(rtype)s '
+                             'relation type from %(appid)s data model')
+        else:
+            visitor = FullSchemaVisitor(self._cw, schema,
+                                        skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of %(appid)s data model')
+        alt %= {'rtype': rtype, 'etype': etype,
+                'appid': self._cw.vreg.config.appid}
+        prophdlr = CWSchemaDotPropsHandler(visitor)
+        generator = GraphGenerator(DotBackend('schema', 'BT',
+                                              ratio='compress',size=None,
+                                              renderer='dot',
+                                              additionnal_param={
+                                                  'overlap':'false',
+                                                  'splines':'true',
+                                                  'sep':'0.2',
+                                              }))
+        # map file
+        pmap, mapfile = tempfile.mkstemp(".map")
+        os.close(pmap)
+        # image file
+        fd, tmpfile = tempfile.mkstemp('.png')
+        os.close(fd)
+        generator.generate(visitor, prophdlr, tmpfile, mapfile)
+        filekeyid = make_uid()
+        self._cw.session.data[filekeyid] = tmpfile
+        self.w(u'<img src="%s" alt="%s" usemap="#schema" />' % (
+            xml_escape(self._cw.build_url(vid='tmppng', tmpfile=filekeyid)),
+            xml_escape(self._cw._(alt))))
+        stream = open(mapfile, 'r').read()
+        stream = stream.decode(self._cw.encoding)
+        self.w(stream)
+        os.unlink(mapfile)
 
-class CWRTypeSchemaImageView(CWETypeSchemaImageView):
-    __select__ = implements('CWRType')
+# breadcrumbs ##################################################################
+
+class CWRelationIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWRelation')
+    def parent_entity(self):
+        return self.entity.rtype
+
+class CWAttributeIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWAttribute')
+    def parent_entity(self):
+        return self.entity.stype
 
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
-        rschema = self._cw.vreg.schema.rschema(entity.name)
-        visitor = OneHopRSchemaVisitor(self._cw, rschema)
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
+class CWConstraintIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWConstraint')
+    def parent_entity(self):
+        if self.entity.reverse_constrained_by:
+            return self.entity.reverse_constrained_by[0]
+
+class RQLExpressionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('RQLExpression')
+    def parent_entity(self):
+        return self.entity.expression_of
+
+class CWPermissionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('CWPermission')
+    def parent_entity(self):
+        # XXX useless with permission propagation
+        permissionof = getattr(self.entity, 'reverse_require_permission', ())
+        if len(permissionof) == 1:
+            return permissionof[0]
 
 
 # misc: facets, actions ########################################################
 
 class CWFinalFacet(facet.AttributeFacet):
     __regid__ = 'cwfinal-facet'
-    __select__ = facet.AttributeFacet.__select__ & implements('CWEType', 'CWRType')
+    __select__ = facet.AttributeFacet.__select__ & is_instance('CWEType', 'CWRType')
     rtype = 'final'
 
 class ViewSchemaAction(action.Action):
--- a/web/views/sessions.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/sessions.py	Wed Nov 03 16:38:28 2010 +0100
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """web session component: by dfault the session is actually the db connection
 object :/
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.web import InvalidSession
--- a/web/views/startup.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/startup.py	Wed Nov 03 16:38:28 2010 +0100
@@ -26,7 +26,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.view import StartupView
-from cubicweb.selectors import match_user_groups, implements
+from cubicweb.selectors import match_user_groups, is_instance
 from cubicweb.schema import display_name
 from cubicweb.web import ajax_replace_url, uicfg, httpcache
 
@@ -42,7 +42,7 @@
     def call(self, **kwargs):
         """The default view representing the instance's management"""
         self._cw.add_css('cubicweb.manageview.css')
-        self.w(u'<div>\n')
+        self.w(u'<h1>%s</h1>' % self._cw.property_value('ui.site-title'))
         if not self.display_folders():
             self._main_index()
         else:
@@ -53,7 +53,6 @@
             self.folders()
             self.w(u'</td>')
             self.w(u'</tr></table>\n')
-        self.w(u'</div>\n')
 
     def _main_index(self):
         req = self._cw
@@ -79,8 +78,8 @@
             self.w(u'<br/><a href="%s">%s</a>\n' % (xml_escape(href), label))
 
     def folders(self):
-        self.w(u'<h4>%s</h4>\n' % self._cw._('Browse by category'))
-        self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
+        self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by category'))
+        self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1)
 
     def create_links(self):
         self.w(u'<ul class="createLink">')
@@ -93,20 +92,24 @@
         self.w(u'</ul>')
 
     def startup_views(self):
-        self.w(u'<h4>%s</h4>\n' % self._cw._('Startup views'))
+        self.w(u'<h2>%s</h2>\n' % self._cw._('Startup views'))
         self.startupviews_table()
 
     def startupviews_table(self):
         views = self._cw.vreg['views'].possible_views(self._cw, None)
+        if not views:
+            return
+        self.w(u'<ul class="startup">')
         for v in sorted(views, key=lambda x: self._cw._(x.title)):
             if v.category != 'startupview' or v.__regid__ in ('index', 'tree', 'manage'):
                 continue
-            self.w('<p><a href="%s">%s</a></p>' % (
+            self.w('<li><a href="%s">%s</a></li>' % (
                 xml_escape(v.url()), xml_escape(self._cw._(v.title).capitalize())))
+        self.w(u'</ul>')
 
     def entities(self):
         schema = self._cw.vreg.schema
-        self.w(u'<h4>%s</h4>\n' % self._cw._('The repository holds the following entities'))
+        self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by entity type'))
         manager = self._cw.user.matching_groups('managers')
         self.w(u'<table class="startup">')
         if manager:
--- a/web/views/tableview.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/tableview.py	Wed Nov 03 16:38:28 2010 +0100
@@ -16,17 +16,13 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """generic table view, including filtering abilities"""
+
 __docformat__ = "restructuredtext en"
 
-try:
-    from json import dumps
-except ImportError:
-    from simplejson import dumps
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb.selectors import nonempty_rset, match_form_params
-from cubicweb.utils import make_uid
+from cubicweb.utils import make_uid, json_dumps
 from cubicweb.view import EntityView, AnyRsetView
 from cubicweb import tags
 from cubicweb.uilib import toggle_action, limitsize, htmlescape
@@ -77,7 +73,7 @@
         # drop False / None values from vidargs
         vidargs = dict((k, v) for k, v in vidargs.iteritems() if v)
         w(u'<form method="post" cubicweb:facetargs="%s" action="">' %
-          xml_escape(dumps([divid, self.__regid__, False, vidargs])))
+          xml_escape(json_dumps([divid, self.__regid__, False, vidargs])))
         w(u'<fieldset id="%sForm" class="%s">' % (divid, hidden and 'hidden' or ''))
         w(u'<input type="hidden" name="divid" value="%s" />' % divid)
         w(u'<input type="hidden" name="fromformfilter" value="1" />')
@@ -144,11 +140,11 @@
         if mainindex is None:
             displayfilter, displayactions = False, False
         else:
-            if displayfilter is None and 'displayfilter' in req.form:
+            if displayfilter is None and req.form.get('displayfilter'):
                 displayfilter = True
                 if req.form['displayfilter'] == 'shown':
                     hidden = False
-            if displayactions is None and 'displayactions' in req.form:
+            if displayactions is None and req.form.get('displayactions'):
                 displayactions = True
         displaycols = self.displaycols(displaycols, headers)
         fromformfilter = 'fromformfilter' in req.form
@@ -191,13 +187,9 @@
 
     def page_navigation_url(self, navcomp, path, params):
         if hasattr(self, 'divid'):
-            divid = self.divid
-        else:
-            divid = params.get('divid', 'pageContent'),
-        rql = params.pop('rql', self.cw_rset.printable_rql())
-        # latest 'true' used for 'swap' mode
-        return 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
-            dumps(divid), dumps(rql), dumps(self.__regid__), dumps(params))
+            params['divid'] = self.divid
+        params['vid'] = self.__regid__
+        return navcomp.ajax_page_url(**params)
 
     def show_hide_actions(self, divid, currentlydisplayed=False):
         showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
@@ -213,7 +205,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)
@@ -270,6 +262,8 @@
         if val is None:
             return u''
         etype = self.cw_rset.description[row][col]
+        if etype is None:
+            return u''
         if self._cw.vreg.schema.eschema(etype).final:
             entity, rtype = self.cw_rset.related_entity(row, col)
             if entity is None:
@@ -295,7 +289,7 @@
         :param cellvid: cell view (defaults to 'outofcontext')
         """
         etype, val = self.cw_rset.description[row][col], self.cw_rset[row][col]
-        if val is not None and not self._cw.vreg.schema.eschema(etype).final:
+        if val is not None and etype is not None and not self._cw.vreg.schema.eschema(etype).final:
             self.wview(cellvid or 'outofcontext', self.cw_rset, row=row, col=col)
         elif val is None:
             # This is usually caused by a left outer join and in that case,
@@ -325,7 +319,8 @@
     title = None
 
     def call(self, title=None, subvid=None, headers=None, divid=None,
-             paginate=False, displaycols=None, displayactions=None, mainindex=None):
+             paginate=False, displaycols=None, displayactions=None,
+             mainindex=None):
         """Dumps a table displaying a composite query"""
         try:
             actrql = self._cw.form['actualrql']
@@ -348,7 +343,8 @@
             mainindex = self.main_var_index()
         if mainindex is not None:
             actions = self.form_filter(divid, displaycols, displayactions,
-                                       paginate, True)
+                                       displayfilter=True, paginate=paginate,
+                                       hidden=True)
         else:
             actions = ()
         if not subvid and 'subvid' in self._cw.form:
@@ -386,9 +382,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 +403,21 @@
             else:
                 content = entity.printable_value(col)
             infos[col] = content
-        self.w(u"""<tr onmouseover="addElementClass(this, 'highlighted');"
-            onmouseout="removeElementClass(this, 'highlighted')">""")
+        self.w(u"""<tr onmouseover="$(this).addClass('highlighted');"
+            onmouseout="$(this).removeClass('highlighted')">""")
         line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
         self.w(line % infos)
         self.w(u'</tr>\n')
 
-    def table_header(self, ecls):
+    def table_header(self, sample):
         """builds the table's header"""
         self.w(u'<thead><tr>')
-        _ = self._cw._
         for column in self.columns:
             meth = getattr(self, 'header_for_%s' % column, None)
             if meth:
-                colname = meth(ecls)
+                colname = meth(sample)
             else:
-                colname = _(column)
+                colname = self._cw._(column)
             self.w(u'<th>%s</th>' % xml_escape(colname))
         self.w(u'</tr></thead>\n')
 
--- a/web/views/tabs.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/tabs.py	Wed Nov 03 16:38:28 2010 +0100
@@ -55,18 +55,20 @@
             urlparams['rql'] = uilib.rql_for_eid(eid)
         elif rset:
             urlparams['rql'] = rset.printable_rql()
+        if tabid is None:
+            tabid = uilib.domid(vid)
         w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
-            tabid or vid, xml_escape(self._cw.build_url('json', **urlparams))))
+            tabid, xml_escape(self._cw.build_url('json', **urlparams))))
         if show_spinbox:
             w(u'<img src="data/loading.gif" id="%s-hole" alt="%s"/>'
-              % (tabid or vid, self._cw._('(loading ...)')))
+              % (tabid, self._cw._('(loading ...)')))
         else:
-            w(u'<div id="%s-hole"></div>' % (tabid or vid))
+            w(u'<div id="%s-hole"></div>' % tabid)
         w(u'<noscript><p><a class="style: hidden" id="seo-%s" href="%s">%s</a></p></noscript>'
-          % (tabid or vid, xml_escape(self._cw.build_url(**urlparams)), xml_escape('%s (%s)') %
-             (tabid or vid, self._cw._('follow this link if javascript is deactivated'))))
+          % (tabid, xml_escape(self._cw.build_url(**urlparams)), xml_escape('%s (%s)') %
+             (tabid, self._cw._('follow this link if javascript is deactivated'))))
         w(u'</div>')
-        self._prepare_bindings(tabid or vid, reloadable)
+        self._prepare_bindings(tabid, reloadable)
 
     def forceview(self, vid):
         """trigger an event that will force immediate loading of the view
@@ -91,15 +93,16 @@
         cookiename = self.cookie_name
         activetab = cookies.get(cookiename)
         if activetab is None:
-            cookies[cookiename] = default
+            domid = uilib.domid(default)
+            cookies[cookiename] = domid
             self._cw.set_cookie(cookies, cookiename)
-            return default
+            return domid
         return activetab.value
 
     def prune_tabs(self, tabs, default_tab):
         selected_tabs = []
         may_be_active_tab = self.active_tab(default_tab)
-        active_tab = default_tab
+        active_tab = uilib.domid(default_tab)
         viewsvreg = self._cw.vreg['views']
         for tab in tabs:
             try:
@@ -109,13 +112,14 @@
                 tabid, tabkwargs = tab, {}
             tabkwargs.setdefault('rset', self.cw_rset)
             vid = tabkwargs.get('vid', tabid)
+            domid = uilib.domid(tabid)
             try:
                 viewsvreg.select(vid, self._cw, **tabkwargs)
-                selected_tabs.append((tabid, tabkwargs))
             except NoSelectableObject:
                 continue
-            if tabid == may_be_active_tab:
-                active_tab = tabid
+            selected_tabs.append((tabid, domid, tabkwargs))
+            if domid == may_be_active_tab:
+                active_tab = domid
         return selected_tabs, active_tab
 
     def render_tabs(self, tabs, default, entity=None):
@@ -135,21 +139,21 @@
         w(u'<div id="entity-tabs-%s">' % uid)
         w(u'<ul>')
         active_tab_idx = None
-        for i, (tabid, tabkwargs) in enumerate(tabs):
+        for i, (tabid, domid, tabkwargs) in enumerate(tabs):
             w(u'<li>')
-            w(u'<a href="#%s">' % tabid)
-            w(u'<span onclick="set_tab(\'%s\', \'%s\')">' % (tabid, self.cookie_name))
+            w(u'<a href="#%s">' % domid)
+            w(u'<span onclick="set_tab(\'%s\', \'%s\')">' % (domid, self.cookie_name))
             w(tabkwargs.pop('label', self._cw._(tabid)))
             w(u'</span>')
             w(u'</a>')
             w(u'</li>')
-            if tabid == active_tab:
+            if domid == active_tab:
                 active_tab_idx = i
         w(u'</ul>')
         w(u'</div>')
-        for tabid, tabkwargs in tabs:
-            w(u'<div id="%s">' % tabid)
-            tabkwargs.setdefault('tabid', tabid)
+        for tabid, domid, tabkwargs in tabs:
+            w(u'<div id="%s">' % domid)
+            tabkwargs.setdefault('tabid', domid)
             tabkwargs.setdefault('vid', tabid)
             tabkwargs.setdefault('rset', self.cw_rset)
             self.lazyview(**tabkwargs)
@@ -159,9 +163,9 @@
         # XXX make work history: true
         self._cw.add_onload(u"""
   jQuery('#entity-tabs-%(eeid)s > ul').tabs( { selected: %(tabindex)s });
-  set_tab('%(vid)s', '%(cookiename)s');
+  set_tab('%(domid)s', '%(cookiename)s');
 """ % {'tabindex'   : active_tab_idx,
-       'vid'        : active_tab,
+       'domid'        : active_tab,
        'eeid'       : (entity and entity.eid or uid),
        'cookiename' : self.cookie_name})
 
@@ -175,7 +179,7 @@
     class ProjectScreenshotsView(EntityRelationView):
         '''display project's screenshots'''
         __regid__ = title = _('projectscreenshots')
-        __select__ = EntityRelationView.__select__ & implements('Project')
+        __select__ = EntityRelationView.__select__ & is_instance('Project')
         rtype = 'screenshot'
         role = 'subject'
         vid = 'gallery'
@@ -205,6 +209,7 @@
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
         self.render_entity_toolbox(entity)
+        self.w(u'<div class="tabbedprimary"></div>')
         self.render_entity_title(entity)
         self.render_tabs(self.tabs, self.default_tab, entity)
 
--- a/web/views/timeline.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/timeline.py	Wed Nov 03 16:38:28 2010 +0100
@@ -18,16 +18,16 @@
 """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 logilab.common.date import ustrftime
 
-from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
+from cubicweb.selectors import adaptable
 from cubicweb.view import EntityView, StartupView
-from cubicweb.web import json
+from cubicweb.utils import json_dumps
 
 _ = unicode
 
@@ -37,11 +37,12 @@
     should be properties of entity classes or subviews)
     """
     __regid__ = 'timeline-json'
+    __select__ = adaptable('ICalendarable')
+
     binary = True
     templatable = False
     content_type = 'application/json'
 
-    __select__ = implements(ICalendarable)
     date_fmt = '%Y/%m/%d'
 
     def call(self):
@@ -52,7 +53,7 @@
                 events.append(event)
         timeline_data = {'dateTimeFormat': self.date_fmt,
                          'events': events}
-        self.w(json.dumps(timeline_data))
+        self.w(json_dumps(timeline_data))
 
     # FIXME: those properties should be defined by the entity class
     def onclick_url(self, entity):
@@ -74,12 +75,13 @@
         '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
-        event_data = {'start': start.strftime(self.date_fmt),
+        event_data = {'start': ustrftime(start, self.date_fmt),
                       'title': xml_escape(entity.dc_title()),
                       'description': entity.dc_description(format='text/html'),
                       'link': entity.absolute_url(),
@@ -88,7 +90,7 @@
         if onclick:
             event_data['onclick'] = onclick
         if stop:
-            event_data['end'] = stop.strftime(self.date_fmt)
+            event_data['end'] = ustrftime(stop, self.date_fmt)
         return event_data
 
 
@@ -116,7 +118,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/timetable.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/treeview.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,21 +15,98 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Set of tree-building widgets, based on jQuery treeview plugin"""
+"""Set of tree views / tree-building widgets, some based on jQuery treeview
+plugin.
+"""
 
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
-from cubicweb.utils import make_uid
-from cubicweb.interfaces import ITree
-from cubicweb.selectors import implements
+
+from cubicweb.utils import make_uid, json
+from cubicweb.selectors import adaptable
 from cubicweb.view import EntityView
-from cubicweb.web import json
+from cubicweb.mixins import _done_init
+from cubicweb.web.views import baseviews
 
 def treecookiename(treeid):
     return str('%s-treestate' % treeid)
 
+
+class BaseTreeView(baseviews.ListView):
+    """base tree view"""
+    __regid__ = 'tree'
+    __select__ = adaptable('ITree')
+    item_vid = 'treeitem'
+
+    def call(self, done=None, **kwargs):
+        if done is None:
+            done = set()
+        super(BaseTreeView, self).call(done=done, **kwargs)
+
+    def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs):
+        assert maxlevel is None or maxlevel > 0
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<li class="badcontent">%s</li>' % entity)
+            return
+        self.open_item(entity)
+        entity.view(vid or self.item_vid, w=self.w, **kwargs)
+        if maxlevel is not None:
+            maxlevel -= 1
+            if maxlevel == 0:
+                self.close_item(entity)
+                return
+        relatedrset = entity.cw_adapt_to('ITree').children(entities=False)
+        self.wview(self.__regid__, relatedrset, 'null', done=done,
+                   maxlevel=maxlevel, **kwargs)
+        self.close_item(entity)
+
+    def open_item(self, entity):
+        self.w(u'<li class="%s">\n' % entity.__regid__.lower())
+    def close_item(self, entity):
+        self.w(u'</li>\n')
+
+
+class TreePathView(EntityView):
+    """a recursive path view"""
+    __regid__ = 'path'
+    __select__ = adaptable('ITree')
+    item_vid = 'oneline'
+    separator = u'&#160;&gt;&#160;'
+
+    def call(self, **kwargs):
+        self.w(u'<div class="pathbar">')
+        super(TreePathView, self).call(**kwargs)
+        self.w(u'</div>')
+
+    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<span class="badcontent">%s</span>' % entity)
+            return
+        parent = entity.cw_adapt_to('ITree').parent()
+        if parent:
+            parent.view(self.__regid__, w=self.w, done=done)
+            self.w(self.separator)
+        entity.view(vid or self.item_vid, w=self.w)
+
+
+class TreeComboBoxView(TreePathView):
+    """display folder in edition's combobox"""
+    __regid__ = 'combobox'
+    item_vid = 'text'
+    separator = u' > '
+
+# XXX rename regid to ajaxtree/foldabletree or something like that (same for
+# treeitemview)
 class TreeView(EntityView):
+    """ajax tree view, click to expand folder"""
+
     __regid__ = 'treeview'
     itemvid = 'treeitemview'
     subvid = 'oneline'
@@ -111,7 +188,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 +197,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 +208,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 +227,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 +275,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/urlpublishing.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/urlpublishing.py	Wed Nov 03 16:38:28 2010 +0100
@@ -120,6 +120,7 @@
         self.urlpublisher = urlpublisher
         self.vreg = urlpublisher.vreg
 
+
 class RawPathEvaluator(URLPathEvaluator):
     """handle path of the form::
 
@@ -175,18 +176,27 @@
             else:
                 attrname = cls._rest_attr_info()[0]
             value = req.url_unquote(parts.pop(0))
-            rset = self.attr_rset(req, etype, attrname, value)
-        else:
-            rset = self.cls_rset(req, cls)
+            return self.handle_etype_attr(req, cls, attrname, value)
+        return self.handle_etype(req, cls)
+
+    def set_vid_for_rset(self, req, cls, rset):# cls is there to ease overriding
         if rset.rowcount == 0:
             raise NotFound()
+        # we've to set a default vid here, since vid_from_rset may try to use a
+        # table view if fetch_rql include some non final relation
+        if rset.rowcount == 1:
+            req.form.setdefault('vid', 'primary')
+        else: # rset.rowcount >= 1
+            req.form.setdefault('vid', 'sameetypelist')
+
+    def handle_etype(self, req, cls):
+        rset = req.execute(cls.fetch_rql(req.user))
+        self.set_vid_for_rset(req, cls, rset)
         return None, rset
 
-    def cls_rset(self, req, cls):
-        return req.execute(cls.fetch_rql(req.user))
-
-    def attr_rset(self, req, etype, attrname, value):
-        rql = u'Any X WHERE X is %s, X %s %%(x)s' % (etype, attrname)
+    def handle_etype_attr(self, req, cls, attrname, value):
+        rql = cls.fetch_rql(req.user, ['X %s %%(x)s' % (attrname)],
+                            mainvar='X', ordermethod=None)
         if attrname == 'eid':
             try:
                 rset = req.execute(rql, {'x': typed_eid(value)})
@@ -195,7 +205,8 @@
                 raise PathDontMatch()
         else:
             rset = req.execute(rql, {'x': value})
-        return rset
+        self.set_vid_for_rset(req, cls, rset)
+        return None, rset
 
 
 class URLRewriteEvaluator(URLPathEvaluator):
--- a/web/views/urlrewrite.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/urlrewrite.py	Wed Nov 03 16:38:28 2010 +0100
@@ -24,7 +24,7 @@
 
 
 def rgx(pattern, flags=0):
-    """this is just a convenient shortcout to add the $ sign"""
+    """this is just a convenient shortcut to add the $ sign"""
     return re.compile(pattern+'$', flags)
 
 class metarewriter(type):
@@ -163,7 +163,7 @@
     return do_build_rset
 
 def rgx_action(rql=None, args=None, cachekey=None, argsgroups=(), setuser=False,
-               form=None, formgroups=(), transforms={}, controller=None):
+               form=None, formgroups=(), transforms={}, rqlformparams=(), controller=None):
     def do_build_rset(inputurl, uri, req, schema,
                       cachekey=cachekey # necessary to avoid UnboundLocalError
                       ):
@@ -183,6 +183,8 @@
                         kwargs[key] = typed_eid(value)
             if setuser:
                 kwargs['u'] = req.user.eid
+            for param in rqlformparams:
+                kwargs.setdefault(param, req.form.get(param))
             rset = req.execute(rql, kwargs, cachekey)
         else:
             rset = None
@@ -206,7 +208,7 @@
     __regid__ = 'schemabased'
     rules = [
         # rgxp : callback
-        (rgx('/search/(.+)'), build_rset(rql=r'Any X WHERE X has_text %(text)s',
+        (rgx('/search/(.+)'), build_rset(rql=r'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s',
                                          rgxgroups=[('text', 1)])),
         ]
 
--- a/web/views/vcard.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/vcard.py	Wed Nov 03 16:38:28 2010 +0100
@@ -20,7 +20,7 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.view import EntityView
 
 _ = unicode
@@ -33,7 +33,7 @@
     title = _('vcard')
     templatable = False
     content_type = 'text/x-vcard'
-    __select__ = implements('CWUser')
+    __select__ = is_instance('CWUser')
 
     def set_request_content_type(self):
         """overriden to set a .vcf filename"""
--- a/web/views/wdoc.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/wdoc.py	Wed Nov 03 16:38:28 2010 +0100
@@ -62,9 +62,11 @@
         snode = index[section.attrib['insertbefore']]
         node = snode.parent
         idx = node.getchildren().index(snode)
-    else:
+    elif 'appendto' in section.attrib:
         node = index[section.attrib['appendto']]
         idx = None
+    else:
+        node, idx = None, None
     return node, idx
 
 def build_toc(config):
@@ -79,6 +81,8 @@
         toc = parse(fpath).getroot()
         for section in toc:
             node, idx = get_insertion_point(section, index)
+            if node is None:
+                continue
             if idx is None:
                 node.append(section)
             else:
--- a/web/views/workflow.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/workflow.py	Wed Nov 03 16:38:28 2010 +0100
@@ -24,24 +24,23 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-import tempfile
 import os
 
 from logilab.mtconverter import xml_escape
-from logilab.common.graph import escape, GraphGenerator, DotBackend
+from logilab.common.graph import escape
 
 from cubicweb import Unauthorized, view
-from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
+from cubicweb.selectors import (has_related_entities, one_line_rset,
                                 relation_possible, match_form_params,
-                                implements, score_entity)
-from cubicweb.utils import make_uid
-from cubicweb.interfaces import IWorkflowable
+                                score_entity, is_instance, adaptable)
 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
+from cubicweb.web.views.dotgraphview import DotGraphView, DotPropsHandler
 
 _pvs = uicfg.primaryview_section
 _pvs.tag_subject_of(('Workflow', 'initial_state', '*'), 'hidden')
@@ -90,8 +89,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 +100,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 +129,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')
 
@@ -163,7 +163,7 @@
 class WFHistoryVComponent(component.EntityVComponent):
     """display the workflow history for entities supporting it"""
     __regid__ = 'wfhistory'
-    __select__ = WFHistoryView.__select__ & component.EntityVComponent.__select__
+    __select__ = component.EntityVComponent.__select__ & WFHistoryView.__select__
     context = 'navcontentbottom'
     title = _('Workflow history')
 
@@ -184,22 +184,24 @@
 
     def fill_menu(self, box, menu):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        menu.label = u'%s: %s' % (self._cw._('state'), entity.printable_state)
+        menu.label = u'%s: %s' % (self._cw._('state'),
+                                  entity.cw_adapt_to('IWorkflowable').printable_state)
         menu.append_anyway = True
         super(WorkflowActions, self).fill_menu(box, menu)
 
     def actual_actions(self):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         hastr = False
-        for tr in entity.possible_transitions():
+        for tr in iworkflowable.possible_transitions():
             url = entity.absolute_url(vid='statuschange', treid=tr.eid)
             yield self.build_action(self._cw._(tr.name), url)
             hastr = True
         # don't propose to see wf if user can't pass any transition
         if hastr:
-            wfurl = entity.current_workflow.absolute_url()
+            wfurl = iworkflowable.current_workflow.absolute_url()
             yield self.build_action(self._cw._('view workflow'), wfurl)
-        if entity.workflow_history:
+        if iworkflowable.workflow_history:
             wfurl = entity.absolute_url(vid='wfhistory')
             yield self.build_action(self._cw._('view history'), wfurl)
 
@@ -223,14 +225,14 @@
 _abaa.tag_object_of(('WorkflowTransition', 'transition_of', 'Workflow'), True)
 
 class WorkflowPrimaryView(TabbedPrimaryView):
-    __select__ = implements('Workflow')
+    __select__ = is_instance('Workflow')
     tabs = [  _('wf_tab_info'), _('wfgraph'),]
     default_tab = 'wf_tab_info'
 
 
 class CellView(view.EntityView):
     __regid__ = 'cell'
-    __select__ = implements('TrInfo')
+    __select__ = is_instance('TrInfo')
 
     def cell_call(self, row, col, cellvid=None):
         self.w(self.cw_rset.get_entity(row, col).view('reledit', rtype='comment'))
@@ -239,7 +241,7 @@
 class StateInContextView(view.EntityView):
     """convenience trick, State's incontext view should not be clickable"""
     __regid__ = 'incontext'
-    __select__ = implements('State')
+    __select__ = is_instance('State')
 
     def cell_call(self, row, col):
         self.w(xml_escape(self._cw.view('textincontext', self.cw_rset,
@@ -247,7 +249,7 @@
 
 class WorkflowTabTextView(PrimaryTab):
     __regid__ = 'wf_tab_info'
-    __select__ = PrimaryTab.__select__ & one_line_rset() & implements('Workflow')
+    __select__ = PrimaryTab.__select__ & one_line_rset() & is_instance('Workflow')
 
     def render_entity_attributes(self, entity):
         _ = self._cw._
@@ -273,7 +275,7 @@
 
 class TransitionSecurityTextView(view.EntityView):
     __regid__ = 'trsecurity'
-    __select__ = implements('Transition')
+    __select__ = is_instance('Transition')
 
     def cell_call(self, row, col):
         _ = self._cw._
@@ -291,7 +293,7 @@
 
 class TransitionAllowedTextView(view.EntityView):
     __regid__ = 'trfromstates'
-    __select__ = implements('Transition')
+    __select__ = is_instance('Transition')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
@@ -316,7 +318,7 @@
 
 
 class TransitionEditionForm(autoform.AutomaticEntityForm):
-    __select__ = implements('Transition')
+    __select__ = is_instance('Transition')
 
     def workflow_states_for_relation(self, targetrelation):
         eids = self.edited_entity.linked_to('transition_of', 'subject')
@@ -337,7 +339,7 @@
 
 
 class StateEditionForm(autoform.AutomaticEntityForm):
-    __select__ = implements('State')
+    __select__ = is_instance('State')
 
     def subject_allowed_transition_vocabulary(self, rtype, limit=None):
         if not self.edited_entity.has_eid():
@@ -347,19 +349,35 @@
                                                    'allowed_transition')
         return []
 
+class WorkflowIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('Workflow')
+    # XXX what if workflow of multiple types?
+    def parent_entity(self):
+        return self.entity.workflow_of and self.entity.workflow_of[0] or None
+
+class WorkflowItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('BaseTransition', 'State')
+    def parent_entity(self):
+        return self.entity.workflow
+
+class TransitionItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('SubWorkflowExitPoint')
+    def parent_entity(self):
+        return self.entity.reverse_subworkflow_exit[0]
+
+class TrInfoIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = is_instance('TrInfo')
+    def parent_entity(self):
+        return self.entity.for_entity
+
 
 # workflow images ##############################################################
 
-class WorkflowDotPropsHandler(object):
-    def __init__(self, req):
-        self._ = req._
+class WorkflowDotPropsHandler(DotPropsHandler):
 
     def node_properties(self, stateortransition):
         """return default DOT drawing options for a state or transition"""
-        props = {'label': stateortransition.printable_value('name'),
-                 'fontname': 'Courier', 'fontsize':10,
-                 'href': stateortransition.absolute_url(),
-                 }
+        props = super(WorkflowDotPropsHandler, self).node_properties(stateortransition)
         if hasattr(stateortransition, 'state_of'):
             props['shape'] = 'box'
             props['style'] = 'filled'
@@ -373,12 +391,8 @@
                 props['label'] += escape('\n'.join(descr))
         return props
 
-    def edge_properties(self, transition, fromstate, tostate):
-        return {'label': '', 'dir': 'forward',
-                'color': 'black', 'style': 'filled'}
 
-
-class WorkflowVisitor:
+class WorkflowVisitor(object):
     def __init__(self, entity):
         self.entity = entity
 
@@ -397,35 +411,15 @@
             for outgoingstate in transition.potential_destinations():
                 yield transition.eid, outgoingstate.eid, transition
 
-
-class WorkflowGraphView(view.EntityView):
+class WorkflowGraphView(DotGraphView):
     __regid__ = 'wfgraph'
-    __select__ = EntityView.__select__ & one_line_rset() & implements('Workflow')
+    __select__ = EntityView.__select__ & one_line_rset() & is_instance('Workflow')
 
-    def cell_call(self, row, col):
-        entity = self.cw_rset.get_entity(row, col)
-        visitor = WorkflowVisitor(entity)
-        prophdlr = WorkflowDotPropsHandler(self._cw)
-        wfname = 'workflow%s' % str(entity.eid)
-        generator = GraphGenerator(DotBackend(wfname, None,
-                                              ratio='compress', size='30,10'))
-        # map file
-        pmap, mapfile = tempfile.mkstemp(".map", wfname)
-        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="#%s" />' % (
-            xml_escape(entity.absolute_url(vid='tmppng', tmpfile=filekeyid)),
-            xml_escape(self._cw._('graphical workflow for %s') % entity.name),
-            wfname))
-        stream = open(mapfile, 'r').read()
-        stream = stream.decode(self._cw.encoding)
-        self.w(stream)
-        os.unlink(mapfile)
+    def build_visitor(self, entity):
+        return WorkflowVisitor(entity)
+
+    def build_dotpropshandler(self):
+        return WorkflowDotPropsHandler(self._cw)
 
 
 class TmpPngView(TmpFileViewMixin, view.EntityView):
--- a/web/views/xbel.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/xbel.py	Wed Nov 03 16:38:28 2010 +0100
@@ -23,7 +23,7 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import implements
+from cubicweb.selectors import is_instance
 from cubicweb.view import EntityView
 from cubicweb.web.views.xmlrss import XMLView
 
@@ -62,7 +62,7 @@
 
 
 class XbelItemBookmarkView(XbelItemView):
-    __select__ = implements('Bookmark')
+    __select__ = is_instance('Bookmark')
 
     def url(self, entity):
         return entity.actual_url()
--- a/web/views/xmlrss.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/views/xmlrss.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""base xml and rss views
+"""base xml and rss views"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -25,8 +24,10 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import non_final_entity, one_line_rset, appobject_selectable
-from cubicweb.view import EntityView, AnyRsetView, Component
+from cubicweb.selectors import (is_instance, non_final_entity, one_line_rset,
+                                appobject_selectable, adaptable)
+from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
+from cubicweb.view import implements_adapter_compat
 from cubicweb.uilib import simple_sgml_tag
 from cubicweb.web import httpcache, box
 
@@ -120,6 +121,16 @@
 
 # RSS stuff ###################################################################
 
+class IFeedAdapter(EntityAdapter):
+    __regid__ = 'IFeed'
+    __select__ = is_instance('Any')
+
+    @implements_adapter_compat('IFeed')
+    def rss_feed_url(self):
+        """return an url to the rss feed for this entity"""
+        return self.entity.absolute_url(vid='rss')
+
+
 class RSSFeedURL(Component):
     __regid__ = 'rss_feed_url'
     __select__ = non_final_entity()
@@ -130,10 +141,11 @@
 
 class RSSEntityFeedURL(Component):
     __regid__ = 'rss_feed_url'
-    __select__ = non_final_entity() & one_line_rset()
+    __select__ = one_line_rset() & adaptable('IFeed')
 
     def feed_url(self):
-        return self.cw_rset.get_entity(0, 0).rss_feed_url()
+        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        return entity.cw_adapt_to('IFeed').rss_feed_url()
 
 
 class RSSIconBox(box.BoxTemplate):
@@ -147,7 +159,7 @@
 
     def call(self, **kwargs):
         try:
-            rss = self._cw.external_resource('RSS_LOGO')
+            rss = self._cw.uiprops['RSS_LOGO']
         except KeyError:
             self.error('missing RSS_LOGO external resource')
             return
--- a/web/wdoc/userprefs_en.rst	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/wdoc/userprefs_en.rst	Wed Nov 03 16:38:28 2010 +0100
@@ -1,7 +1,7 @@
-User's personnal information are modifiable using user's edit form. You can
-access it through the dropdown-menu under the link on the top-right of the
-window, labeled by your login. In this menu, click the "personal information"
-link to go to this form.
+The personal information describing a User can be modified using the edit form
+of the user. You can access it through the dropdown-menu under the link on the
+top-right of the window, labeled by your login. In this menu, click the
+"profile" link to go to this form.
 
 Each user can as well customize the site appearance using the "user's
 preferences" link in this menu. This will show you a form to configure which
--- a/web/webconfig.py	Tue Jul 27 12:36:03 2010 +0200
+++ b/web/webconfig.py	Wed Nov 03 16:38:28 2010 +0100
@@ -15,16 +15,17 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""common web configuration for twisted/modpython instances
+"""web ui configuration for cubicweb instances"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 import os
 from os.path import join, exists, split
+from warnings import warn
 
 from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
 
 from cubicweb.toolsutils import read_config
 from cubicweb.cwconfig import CubicWebConfiguration, register_persistent_options, merge_options
@@ -77,6 +78,7 @@
     """
     cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')])
     cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views'])
+    uiprops = {'FCKEDITOR_PATH': ''}
 
     options = merge_options(CubicWebConfiguration.options + (
         ('anonymous-user',
@@ -186,7 +188,7 @@
         ('print-traceback',
          {'type' : 'yn',
           'default': CubicWebConfiguration.mode != 'system',
-          'help': 'print the traceback on the error page when an error occured',
+          'help': 'print the traceback on the error page when an error occurred',
           'group': 'web', 'level': 2,
           }),
 
@@ -205,10 +207,16 @@
           '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():
@@ -228,41 +236,13 @@
             return self.__repo
         except AttributeError:
             from cubicweb.dbapi import get_repository
-            if self.repo_method == 'inmemory':
-                repo = get_repository('inmemory', vreg=vreg, config=self)
-            else:
-                repo = get_repository('pyro', self['pyro-instance-id'],
-                                      config=self)
+            repo = get_repository(self.repo_method, vreg=vreg, config=self)
             self.__repo = repo
             return repo
 
     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 +256,37 @@
             user = unicode(user)
         return user, passwd
 
-    def has_resource(self, rid):
-        """return true if an external resource is defined"""
-        return bool(self.ext_resources.get(rid))
+    def locate_resource(self, rid):
+        """return the (directory, filename) where the given resource
+        may be found
+        """
+        return self._fs_locate(rid, 'data')
+
+    def locate_doc_file(self, fname):
+        """return the directory where the given resource may be found"""
+        return self._fs_locate(fname, 'wdoc')[0]
 
     @cached
-    def locate_resource(self, rid):
-        """return the directory where the given resource may be found"""
-        return self._fs_locate(rid, 'data')
-
-    @cached
-    def locate_doc_file(self, fname):
-        """return the directory where the given resource may be found"""
-        return self._fs_locate(fname, 'wdoc')
-
-    def _fs_locate(self, rid, rdirectory):
+    def _fs_path_locate(self, rid, rdirectory):
         """return the directory where the given resource may be found"""
         path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
         for directory in path:
             if exists(join(directory, rdirectory, rid)):
-                return join(directory, rdirectory)
+                return directory
+
+    def _fs_locate(self, rid, rdirectory):
+        """return the (directory, filename) where the given resource
+        may be found
+        """
+        directory = self._fs_path_locate(rid, rdirectory)
+        if directory is None:
+            return None, None
+        if rdirectory == 'data' and rid.endswith('.css'):
+            if self['use-old-css'] and rid == 'cubicweb.css':
+                # @import('cubicweb.css') in css
+                rid = 'cubicweb.old.css'
+            return self.uiprops.process_resource(join(directory, rdirectory), rid), rid
+        return join(directory, rdirectory), rid
 
     def locate_all_files(self, rid, rdirectory='wdoc'):
         """return all files corresponding to the given resource"""
@@ -309,8 +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,77 @@
         if not self.repairing:
             self.global_set_option('base-url', baseurl)
         httpsurl = self['https-url']
-        if httpsurl and httpsurl[-1] != '/':
-            httpsurl += '/'
-            if not self.repairing:
-                self.global_set_option('https-url', httpsurl)
+        if httpsurl:
+            if httpsurl[-1] != '/':
+                httpsurl += '/'
+                if not self.repairing:
+                    self.global_set_option('https-url', httpsurl)
+            if self.debugmode:
+                self.https_datadir_url = httpsurl + 'data/'
+            else:
+                self.https_datadir_url = httpsurl + 'data%s/' % self.instance_md5_version()
+        if self.debugmode:
+            self.datadir_url = baseurl + 'data/'
+        else:
+            self.datadir_url = baseurl + 'data%s/' % self.instance_md5_version()
 
-    def _build_ext_resources(self):
-        libresourcesfile = join(self.shared_dir(), 'data', 'external_resources')
-        self.ext_resources.update(read_config(libresourcesfile))
+    def _build_ui_properties(self):
+        # self.datadir_url[:-1] to remove trailing /
+        from cubicweb.web.propertysheet import PropertySheet
+        cachedir = join(self.appdatahome, 'uicache')
+        self.check_writeable_uid_directory(cachedir)
+        self.uiprops = PropertySheet(
+            cachedir,
+            data=lambda x: self.datadir_url + x,
+            datadir_url=self.datadir_url[:-1])
+        self._init_uiprops(self.uiprops)
+        if self['https-url']:
+            cachedir = join(self.appdatahome, 'uicachehttps')
+            self.check_writeable_uid_directory(cachedir)
+            self.https_uiprops = PropertySheet(
+                cachedir,
+                data=lambda x: self.https_datadir_url + x,
+                datadir_url=self.https_datadir_url[:-1])
+            self._init_uiprops(self.https_uiprops)
+
+    def _init_uiprops(self, uiprops):
+        libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
+        uiprops.load(libuiprops)
         for path in reversed([self.apphome] + self.cubes_path()):
-            resourcesfile = join(path, 'data', 'external_resources')
-            if exists(resourcesfile):
-                self.debug('loading %s', resourcesfile)
-                self.ext_resources.update(read_config(resourcesfile))
-        resourcesfile = join(self.apphome, 'external_resources')
+            self._load_ui_properties_file(uiprops, path)
+        self._load_ui_properties_file(uiprops, self.apphome)
+        datadir_url = uiprops.context['datadir_url']
+        # XXX pre 3.9 css compat
+        if self['use-old-css']:
+            if (datadir_url+'/cubicweb.css') in uiprops['STYLESHEETS']:
+                idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.css')
+                uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.old.css'
+            if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
+                uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
+        cubicweb_js_url = datadir_url + '/cubicweb.js'
+        if cubicweb_js_url not in uiprops['JAVASCRIPTS']:
+            uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
+
+    def _load_ui_properties_file(self, uiprops, path):
+        resourcesfile = join(path, 'data', 'external_resources')
         if exists(resourcesfile):
-            self.debug('loading %s', resourcesfile)
-            self.ext_resources.update(read_config(resourcesfile))
-        for resource in ('STYLESHEETS', 'STYLESHEETS_PRINT',
-                         'IE_STYLESHEETS', 'JAVASCRIPTS'):
-            val = self.ext_resources[resource]
-            if isinstance(val, str):
-                files = [w.strip() for w in val.split(',') if w.strip()]
-                self.ext_resources[resource] = files
+            warn('[3.9] %s file is deprecated, use an uiprops.py file'
+                 % resourcesfile, DeprecationWarning)
+            datadir_url = uiprops.context['datadir_url']
+            for rid, val in read_config(resourcesfile).iteritems():
+                if rid in ('STYLESHEETS', 'STYLESHEETS_PRINT',
+                           'IE_STYLESHEETS', 'JAVASCRIPTS'):
+                    val = [w.strip().replace('DATADIR', datadir_url)
+                           for w in val.split(',') if w.strip()]
+                    if rid == 'IE_STYLESHEETS':
+                        rid = 'STYLESHEETS_IE'
+                else:
+                    val = val.strip().replace('DATADIR', datadir_url)
+                uiprops[rid] = val
+        uipropsfile = join(path, 'uiprops.py')
+        if exists(uipropsfile):
+            self.debug('loading %s', uipropsfile)
+            uiprops.load(uipropsfile)
 
     # static files handling ###################################################
 
@@ -369,3 +408,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	Tue Jul 27 12:36:03 2010 +0200
+++ b/wsgi/handler.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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	Tue Jul 27 12:36:03 2010 +0200
+++ b/xy.py	Wed Nov 03 16:38:28 2010 +0100
@@ -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')