merge with 3.20.10
authorRémi Cardona <remi.cardona@logilab.fr>
Fri, 09 Oct 2015 17:52:14 +0200
changeset 10646 45671fb330f5
parent 10645 57c60a96de70 (diff)
parent 10642 7312eb6c8b59 (current diff)
child 10647 012ea658883b
merge with 3.20.10
.hgtags
__pkginfo__.py
cubicweb.spec
debian/changelog
devtools/test/unittest_testlib.py
devtools/testlib.py
hooks/syncschema.py
predicates.py
server/migractions.py
server/sources/native.py
server/test/data-migractions/migratedapp/schema.py
server/test/data/schema.py
server/test/unittest_migractions.py
web/test/unittest_views_editforms.py
web/views/autoform.py
--- a/.hgignore	Fri Oct 09 09:40:08 2015 +0200
+++ b/.hgignore	Fri Oct 09 17:52:14 2015 +0200
@@ -1,6 +1,8 @@
 \.svn
 ^build$
 ^dist$
+\.egg-info$
+^.tox$
 \.pyc$
 \.pyo$
 \.bak$
@@ -11,11 +13,16 @@
 ^doc/book/en/apidoc$
 \.old$
 syntax: regexp
-.*/data/database/.*\.sqlite
-.*/data/database/.*\.config
+.*/data.*/database/.*\.sqlite
+.*/data.*/database/.*\.config
 .*/data/database/tmpdb.*
 .*/data/ldapdb/.*
+.*/data/uicache/
+.*/data/cubes/.*/i18n/.*\.po
 ^doc/html/
 ^doc/doctrees/
 ^doc/book/en/devweb/js_api/
+^doc/_build
+^doc/js_api/
 data/pgdb/
+data.*/pgdb.*
--- a/.hgtags	Fri Oct 09 09:40:08 2015 +0200
+++ b/.hgtags	Fri Oct 09 17:52:14 2015 +0200
@@ -505,3 +505,9 @@
 8f82e95239625d153a9f1de6e79820d96d9efe8a 3.20.10
 8f82e95239625d153a9f1de6e79820d96d9efe8a debian/3.20.10-1
 8f82e95239625d153a9f1de6e79820d96d9efe8a centos/3.20.10-1
+887c6eef807781560adcd4ecd2dea9011f5a6681 3.21.0
+887c6eef807781560adcd4ecd2dea9011f5a6681 debian/3.21.0-1
+887c6eef807781560adcd4ecd2dea9011f5a6681 centos/3.21.0-1
+a8a0de0298a58306d63dbc998ad60c48bf18c80a 3.21.1
+a8a0de0298a58306d63dbc998ad60c48bf18c80a debian/3.21.1-1
+a8a0de0298a58306d63dbc998ad60c48bf18c80a centos/3.21.1-1
--- a/MANIFEST.in	Fri Oct 09 09:40:08 2015 +0200
+++ b/MANIFEST.in	Fri Oct 09 17:52:14 2015 +0200
@@ -6,9 +6,18 @@
 include man/cubicweb-ctl.1
 
 include doc/*.rst
+include doc/Makefile
 recursive-include doc/book *
 recursive-include doc/tools *.py
 recursive-include doc/tutorials *.rst *.py
+include doc/api/*.rst
+recursive-include doc/_themes *
+recursive-include doc/_static *
+include doc/_templates/*.html
+include doc/changes/*.rst
+recursive-include doc/dev .txt *.rst
+recursive-include doc/images *.png *.svg
+include doc/conf.py
 
 recursive-include misc *.py *.png *.display
 
@@ -25,18 +34,18 @@
 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* *.conf.in *.ldif
-recursive-include devtools/test/data bootstrap_cubes *.py *.txt *.js
+recursive-include devtools/test/data bootstrap_cubes *.py *.txt *.js *.po.ref
 recursive-include web/test/data bootstrap_cubes pouet.css *.py
+recursive-include etwist/test/data *.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
+recursive-include skeleton *.py *.css *.js *.po compat *.in *.tmpl rules
 
 prune doc/book/en/.static
 prune doc/book/fr/.static
 prune doc/html/_sources/
 prune misc/cwfs
-prune goa
-prune doc/book/en/devweb/js_api
+prune doc/js_api
 global-exclude *.pyc
--- a/README	Fri Oct 09 09:40:08 2015 +0200
+++ b/README	Fri Oct 09 17:52:14 2015 +0200
@@ -14,7 +14,7 @@
 Install
 -------
 
-More details at http://docs.cubicweb.org/admin/setup
+More details at http://docs.cubicweb.org/book/admin/setup
 
 Getting started
 ---------------
--- a/__pkginfo__.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/__pkginfo__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 20, 10)
+numversion = (3, 21, 1)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -115,8 +115,6 @@
         [join('share', 'cubicweb', 'cubes', 'shared', 'data'),
          [join(_data_dir, fname) for fname in listdir(_data_dir)
           if not isdir(join(_data_dir, fname))]],
-        [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'timeline'),
-         [join(_data_dir, 'timeline', fname) for fname in listdir(join(_data_dir, 'timeline'))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'images'),
          [join(_data_dir, 'images', fname) for fname in listdir(join(_data_dir, 'images'))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'jquery-treeview'),
--- a/_exceptions.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/_exceptions.py	Fri Oct 09 17:52:14 2015 +0200
@@ -82,6 +82,8 @@
         self.session = session
         assert 'rtypes' in kwargs or 'cstrname' in kwargs
         self.kwargs = kwargs
+        # fill cache while the session is open
+        self.rtypes
 
     @cachedproperty
     def rtypes(self):
@@ -100,6 +102,12 @@
         return None, self.rtypes
 
 
+class ViolatedConstraint(RepositoryError):
+    def __init__(self, cnx, cstrname):
+        self.cnx = cnx
+        self.cstrname = cstrname
+
+
 # security exceptions #########################################################
 
 class Unauthorized(SecurityError):
--- a/_gcdebug.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/_gcdebug.py	Fri Oct 09 17:52:14 2015 +0200
@@ -19,6 +19,10 @@
 import gc, types, weakref
 
 from cubicweb.schema import CubicWebRelationSchema, CubicWebEntitySchema
+try:
+    from cubicweb.web.request import _NeedAuthAccessMock
+except ImportError:
+    _NeedAuthAccessMock = None
 
 listiterator = type(iter([]))
 
@@ -30,6 +34,8 @@
     types.ModuleType, types.FunctionType, types.MethodType,
     types.MemberDescriptorType, types.GetSetDescriptorType,
     )
+if _NeedAuthAccessMock is not None:
+    IGNORE_CLASSES = IGNORE_CLASSES + (_NeedAuthAccessMock,)
 
 def _get_counted_class(obj, classes):
     for cls in classes:
@@ -63,7 +69,8 @@
                 ocounters[key] = 1
         if isinstance(obj, viewreferrersclasses):
             print '   ', obj, referrers(obj, showobjs, maxlevel)
-    return counters, ocounters, gc.garbage
+    garbage = [repr(obj) for obj in gc.garbage]
+    return counters, ocounters, garbage
 
 
 def referrers(obj, showobj=False, maxlevel=1):
--- a/appobject.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/appobject.py	Fri Oct 09 17:52:14 2015 +0200
@@ -16,7 +16,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/>.
 """
-.. _appobject:
 
 The `AppObject` class
 ---------------------
@@ -27,7 +26,6 @@
 We can find a certain number of attributes and methods defined in this class and
 common to all the application objects.
 
-.. autoclass:: AppObject
 """
 __docformat__ = "restructuredtext en"
 
--- a/bin/clone_deps.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-#!/usr/bin/python
-import sys
-
-from subprocess import call as sbp_call, Popen, PIPE
-from urllib import urlopen
-import os
-from os import path as osp, pardir, chdir
-
-
-def find_mercurial():
-    print "trying to find mercurial from the command line ..."
-    print '-' * 20
-    tryhg = sbp_call(['hg', '--version'])
-    if tryhg:
-        print 'mercurial seems to be unavailable, please install it'
-        raise
-    print '-' * 20
-    def hg_call(args):
-        return sbp_call(['hg'] + args)
-
-    return hg_call
-
-
-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/localperms',
-            '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:
-        sys.stderr.write('usage %s [base_url]\n' %  sys.argv[0])
-        sys.exit(1)
-    hg_call = find_mercurial()
-    print len(to_clone), 'repositories will be cloned'
-    base_dir = osp.normpath(osp.join(osp.dirname(__file__), pardir, pardir))
-    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(osp.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:
-        sys.stderr.write('WARNING: The following repositories were not updated (no debian tag found):\n')
-        for path in not_updated:
-            sys.stderr.write('\t-%s\n' % path)
-
-if __name__ == '__main__':
-    main()
-
-
-
--- a/cubicweb.spec	Fri Oct 09 09:40:08 2015 +0200
+++ b/cubicweb.spec	Fri Oct 09 17:52:14 2015 +0200
@@ -7,7 +7,7 @@
 %endif
 
 Name:           cubicweb
-Version:        3.20.10
+Version:        3.21.1
 Release:        logilab.1%{?dist}
 Summary:        CubicWeb is a semantic web application framework
 Source0:        http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz
--- a/cwconfig.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/cwconfig.py	Fri Oct 09 17:52:14 2015 +0200
@@ -279,7 +279,7 @@
     ('default-text-format',
      {'type' : 'choice',
       'choices': ('text/plain', 'text/rest', 'text/html', 'text/markdown'),
-      'default': 'text/html', # use fckeditor in the web ui
+      'default': 'text/plain',
       'help': _('default text format for rich text fields.'),
       'group': 'ui',
       }),
@@ -835,7 +835,7 @@
 
     # set by upgrade command
     verbosity = 0
-
+    cmdline_options = None
     options = CubicWebNoAppConfiguration.options + (
         ('log-file',
          {'type' : 'string',
@@ -843,6 +843,13 @@
           'help': 'file where output logs should be written',
           'group': 'main', 'level': 2,
           }),
+        ('statsd-endpoint',
+         {'type' : 'string',
+          'default': '',
+          'help': 'UDP address of the statsd endpoint; it must be formatted'
+                  'like <ip>:<port>; disabled is unset.',
+          'group': 'main', 'level': 2,
+          }),
         # email configuration
         ('smtp-host',
          {'type' : 'string',
@@ -870,6 +877,18 @@
 the repository',
           'group': 'email', 'level': 1,
           }),
+        ('logstat-interval',
+         {'type' : 'int',
+          'default': 0,
+          'help': 'interval (in seconds) at which stats are dumped in the logstat file; set 0 to disable',
+          'group': 'main', 'level': 2,
+          }),
+        ('logstat-file',
+         {'type' : 'string',
+          'default': Method('default_stats_file'),
+          'help': 'file where stats for the instance should be written',
+          'group': 'main', 'level': 2,
+          }),
         )
 
     @classmethod
@@ -953,6 +972,13 @@
             log_path = os.path.join(_INSTALL_PREFIX, 'var', 'log', 'cubicweb', '%s-%s.log')
             return log_path % (self.appid, self.name)
 
+    def default_stats_file(self):
+        """return default path to the stats file of the instance'server"""
+        logfile = self.default_log_file()
+        if logfile.endswith('.log'):
+            logfile = logfile[:-4]
+        return logfile + '.stats'
+
     def default_pid_file(self):
         """return default path to the pid file of the instance'server"""
         if self.mode == 'system':
@@ -1010,7 +1036,7 @@
         # or site_cubicweb files
         self.load_file_configuration(self.main_config_file())
         # configuration initialization hook
-        self.load_configuration()
+        self.load_configuration(**(self.cmdline_options or {}))
 
     def add_cubes(self, cubes):
         """add given cubes to the list of used cubes"""
@@ -1077,9 +1103,9 @@
         infos.append('cubicweb-%s' % str(self.cubicweb_version()))
         return md5(';'.join(infos)).hexdigest()
 
-    def load_configuration(self):
+    def load_configuration(self, **kw):
         """load instance's configuration files"""
-        super(CubicWebConfiguration, self).load_configuration()
+        super(CubicWebConfiguration, self).load_configuration(**kw)
         if self.apphome and not self.creating:
             # init gettext
             self._gettext_init()
@@ -1102,6 +1128,17 @@
         logconfig = join(self.apphome, 'logging.conf')
         if exists(logconfig):
             logging.config.fileConfig(logconfig)
+        # set the statsd address, if any
+        if self.get('statsd-endpoint'):
+            try:
+                address, port = self.get('statsd-endpoint').split(':')
+                port = int(port)
+            except:
+                self.error('statsd-endpoint: invalid address format ({}); '
+                           'it should be "ip:port"'.format(self.get('statsd-endpoint')))
+            else:
+                import statsd_logger
+                statsd_logger.setup('cubicweb.%s' % self.appid, (address, port))
 
     def available_languages(self, *args):
         """return available translation for an instance, by looking for
--- a/cwctl.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/cwctl.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -25,7 +25,7 @@
 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
 # completion). So import locally in command helpers.
 import sys
-from warnings import warn
+from warnings import warn, filterwarnings
 from os import remove, listdir, system, pathsep
 from os.path import exists, join, isfile, isdir, dirname, abspath
 from urlparse import urlparse
@@ -401,7 +401,7 @@
                            if 'type' in odict
                            and odict.get('level') <= self.config.config_level)
             for section in sections:
-                if section not in ('main', 'email', 'pyro', 'web'):
+                if section not in ('main', 'email', 'web'):
                     print '\n' + underline_title('%s options' % section)
                     config.input_config(section, self.config.config_level)
         # write down configuration
@@ -520,7 +520,12 @@
           'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
           'help': 'debug if -D is set, error otherwise',
           }),
-        )
+        ('param',
+         {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
+          'default': {},
+          'help': 'override <key> configuration file option with <value>.',
+         }),
+       )
 
     def start_instance(self, appid):
         """start the instance's server"""
@@ -534,6 +539,8 @@
                 "- '{ctl} pyramid {appid}' (requires the pyramid cube)\n")
             raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
         config = cwcfg.config_for(appid, debugmode=self['debug'])
+        # override config file values with cmdline options
+        config.cmdline_options = self.config.param
         init_cmdline_log_threshold(config, self['loglevel'])
         if self['profile']:
             config.global_set_option('profile', self.config.profile)
@@ -900,9 +907,7 @@
         ('repo-uri',
          {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
           'help': 'URI of the CubicWeb repository to connect to. URI can be \
-pyro://[host:port] the Pyro name server host; if the pyro nameserver is not set, \
-it will be detected by using a broadcast query, a ZMQ URL or \
-inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
+a ZMQ URL or inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
 directly give URI as instance id instead',
           'group': 'remote'
           }),
@@ -953,7 +958,7 @@
         if self.config.repo_uri:
             warn('[3.16] --repo-uri option is deprecated, directly give the URI as instance id',
                  DeprecationWarning)
-            if urlparse(self.config.repo_uri).scheme in ('pyro', 'inmemory'):
+            if urlparse(self.config.repo_uri).scheme == 'inmemory':
                 appuri = '%s/%s' % (self.config.repo_uri.rstrip('/'), appuri)
 
         from cubicweb.utils import parse_repo_uri
@@ -1135,6 +1140,7 @@
     import os
     sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
     sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0)
+    filterwarnings('default', category=DeprecationWarning)
     cwcfg.load_cwctl_plugins()
     try:
         CWCTL.run(args)
--- a/cwvreg.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/cwvreg.py	Fri Oct 09 17:52:14 2015 +0200
@@ -15,179 +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/>.
-""".. RegistryStore:
-
-The `RegistryStore`
--------------------
-
-The `RegistryStore` can be seen as a two-level dictionary. It contains
-all dynamically loaded objects (subclasses of :ref:`appobject`) to
-build a |cubicweb| application. Basically:
-
-* the first level key returns a *registry*. This key corresponds to the
-  `__registry__` attribute of application object classes
-
-* the second level key returns a list of application objects which
-  share the same identifier. This key corresponds to the `__regid__`
-  attribute of application object classes.
-
-A *registry* holds a specific kind of application objects. There is
-for instance a registry for entity classes, another for views, etc...
-
-The `RegistryStore` has two main responsibilities:
-
-- being the access point to all registries
-
-- handling the registration process at startup time, and during automatic
-  reloading in debug mode.
-
-.. _AppObjectRecording:
-
-Details of the recording process
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. index::
-   vregistry: registration_callback
-
-On startup, |cubicweb| loads application objects defined in its library
-and in cubes used by the instance. Application objects from the
-library are loaded first, then those provided by cubes are loaded in
-dependency order (e.g. if your cube depends on an other, objects from
-the dependency will be loaded first). The layout of the modules or packages
-in a cube  is explained in :ref:`cubelayout`.
-
-For each module:
-
-* by default all objects are registered automatically
-
-* if some objects have to replace other objects, or have to be
-  included only if some condition is met, you'll have to define a
-  `registration_callback(vreg)` function in your module and explicitly
-  register **all objects** in this module, using the api defined
-  below.
-
-.. Note::
-    Once the function `registration_callback(vreg)` is implemented in a module,
-    all the objects from this module have to be explicitly registered as it
-    disables the automatic objects registration.
-
-
-API for objects registration
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here are the registration methods that you can use in the `registration_callback`
-to register your objects to the `RegistryStore` instance given as argument (usually
-named `vreg`):
-
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_all
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_and_replace
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.register
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.unregister
-
-Examples:
-
-.. sourcecode:: python
-
-   # web/views/basecomponents.py
-   def registration_callback(vreg):
-      # register everything in the module except SeeAlsoComponent
-      vreg.register_all(globals().itervalues(), __name__, (SeeAlsoVComponent,))
-      # conditionally register SeeAlsoVComponent
-      if 'see_also' in vreg.schema:
-          vreg.register(SeeAlsoVComponent)
-
-In this example, we register all application object classes defined in the module
-except `SeeAlsoVComponent`. This class is then registered only if the 'see_also'
-relation type is defined in the instance'schema.
-
-.. sourcecode:: python
-
-   # goa/appobjects/sessions.py
-   def registration_callback(vreg):
-      vreg.register(SessionsCleaner)
-      # replace AuthenticationManager by GAEAuthenticationManager
-      vreg.register_and_replace(GAEAuthenticationManager, AuthenticationManager)
-      # replace PersistentSessionManager by GAEPersistentSessionManager
-      vreg.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager)
-
-In this example, we explicitly register classes one by one:
-
-* the `SessionCleaner` class
-* the `GAEAuthenticationManager` to replace the `AuthenticationManager`
-* the `GAEPersistentSessionManager` to replace the `PersistentSessionManager`
-
-If at some point we register a new appobject class in this module, it won't be
-registered at all without modification to the `registration_callback`
-implementation. The previous example will register it though, thanks to the call
-to the `register_all` method.
-
-
-.. _Selection:
-
-Runtime objects selection
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Now that we have all application objects loaded, the question is : when
-I want some specific object, for instance the primary view for a given
-entity, how do I get the proper object ? This is what we call the
-**selection mechanism**.
-
-As explained in the :ref:`Concepts` section:
-
-* each application object has a **selector**, defined by its
-  `__select__` class attribute
-
-* this selector is responsible to return a **score** for a given context
-
-  - 0 score means the object doesn't apply to this context
-
-  - else, the higher the score, the better the object suits the context
-
-* the object with the highest score is selected.
-
-.. Note::
-
-  When no single object has the highest score, an exception is raised in development
-  mode to let you know that the engine was not able to identify the view to
-  apply. This error is silenced in production mode and one of the objects with
-  the highest score is picked.
-
-  In such cases you would need to review your design and make sure
-  your selectors or appobjects are properly defined. Such an error is
-  typically caused by either forgetting to change the __regid__ in a
-  derived class, or by having copy-pasted some code.
-
-For instance, if you are selecting the primary (`__regid__ =
-'primary'`) view (`__registry__ = 'views'`) for a result set
-containing a `Card` entity, two objects will probably be selectable:
-
-* 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__ = 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 `is_instance('Card')` selector will return a higher
-score since it's more specific, so the correct view will be selected as expected.
-
-.. _SelectionAPI:
-
-API for objects selections
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here is the selection API you'll get on every registry. Some of them, as the
-'etypes' registry, containing entity classes, extend it. In those methods,
-`*args, **kwargs` is what we call the **context**. Those arguments are given to
-selectors that will inspect their content and return a score accordingly.
-
-.. automethod:: cubicweb.vregistry.Registry.select
-
-.. automethod:: cubicweb.vregistry.Registry.select_or_none
-
-.. automethod:: cubicweb.vregistry.Registry.possible_objects
-
-.. automethod:: cubicweb.vregistry.Registry.object_by_id
+"""
+Cubicweb registries
 """
 
 __docformat__ = "restructuredtext en"
@@ -229,6 +58,7 @@
     sys.modules.pop('cubicweb.web.uicfg', None)
     sys.modules.pop('cubicweb.web.uihelper', None)
 
+
 def require_appobject(obj):
     """return appobjects required by the given object by searching for
     `appobject_selectable` predicate
@@ -241,11 +71,16 @@
 
 class CWRegistry(Registry):
     def __init__(self, vreg):
+        """
+        :param vreg: the :py:class:`CWRegistryStore` managing this registry.
+        """
         super(CWRegistry, self).__init__(True)
         self.vreg = vreg
 
     @property
     def schema(self):
+        """The :py:class:`cubicweb.schema.CubicWebSchema`
+        """
         return self.vreg.schema
 
     def poss_visible_objects(self, *args, **kwargs):
@@ -269,7 +104,7 @@
 
     def selected(self, winner, args, kwargs):
         """overriden to avoid the default 'instanciation' behaviour, ie
-        winner(*args, **kwargs)
+        `winner(*args, **kwargs)`
         """
         return winner
 
--- a/dataimport.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1173 +0,0 @@
-# -*- coding: utf-8 -*-
-# copyright 2003-2014 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 provides tools to import tabular data.
-
-
-Example of use (run this with `cubicweb-ctl shell instance import-script.py`):
-
-.. sourcecode:: python
-
-  from cubicweb.dataimport import *
-  # define data generators
-  GENERATORS = []
-
-  USERS = [('Prenom', 'firstname', ()),
-           ('Nom', 'surname', ()),
-           ('Identifiant', 'login', ()),
-           ]
-
-  def gen_users(ctl):
-      for row in ctl.iter_and_commit('utilisateurs'):
-          entity = mk_entity(row, USERS)
-          entity['upassword'] = 'motdepasse'
-          ctl.check('login', entity['login'], None)
-          entity = ctl.store.create_entity('CWUser', **entity)
-          email = ctl.store.create_entity('EmailAddress', address=row['email'])
-          ctl.store.relate(entity.eid, 'use_email', email.eid)
-          ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x':entity['eid']})
-
-  CHK = [('login', check_doubles, 'Utilisateurs Login',
-          'Deux utilisateurs ne devraient pas avoir le même login.'),
-         ]
-
-  GENERATORS.append( (gen_users, CHK) )
-
-  # create controller
-  ctl = CWImportController(RQLObjectStore(cnx))
-  ctl.askerror = 1
-  ctl.generators = GENERATORS
-  ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
-  # run
-  ctl.run()
-
-.. BUG file with one column are not parsable
-.. TODO rollback() invocation is not possible yet
-"""
-__docformat__ = "restructuredtext en"
-
-import csv
-import sys
-import threading
-import traceback
-import warnings
-import cPickle
-import os.path as osp
-import inspect
-from base64 import b64encode
-from collections import defaultdict
-from copy import copy
-from datetime import date, datetime, time
-from time import asctime
-from StringIO import StringIO
-
-from logilab.common import shellutils, attrdict
-from logilab.common.date import strptime
-from logilab.common.decorators import cached
-from logilab.common.deprecation import deprecated
-
-from cubicweb import QueryError
-from cubicweb.utils import make_uid
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES
-from cubicweb.server.edition import EditedEntity
-from cubicweb.server.sqlutils import SQL_PREFIX
-from cubicweb.server.utils import eschema_eid
-
-
-def count_lines(stream_or_filename):
-    if isinstance(stream_or_filename, basestring):
-        f = open(stream_or_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', delimiter=',', quotechar='"',
-                  skipfirst=False, withpb=True, skip_empty=True, separator=None,
-                  quote=None):
-    """same as :func:`ucsvreader` but a progress bar is displayed as we iter on rows"""
-    if separator is not None:
-        delimiter = separator
-        warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
-    if quote is not None:
-        quotechar = quote
-        warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
-    if isinstance(stream_or_path, basestring):
-        if not osp.exists(stream_or_path):
-            raise Exception("file doesn't exists: %s" % stream_or_path)
-        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(stream, encoding, delimiter, quotechar,
-                           skipfirst=skipfirst, skip_empty=skip_empty):
-        yield urow
-        if withpb:
-            pb.update()
-    print ' %s rows imported' % rowcount
-
-def ucsvreader(stream, encoding='utf-8', delimiter=',', quotechar='"',
-               skipfirst=False, ignore_errors=False, skip_empty=True,
-               separator=None, quote=None):
-    """A csv reader that accepts files with any encoding and outputs unicode
-    strings
-
-    if skip_empty (the default), lines without any values specified (only
-    separators) will be skipped. This is useful for Excel exports which may be
-    full of such lines.
-    """
-    if separator is not None:
-        delimiter = separator
-        warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
-    if quote is not None:
-        quotechar = quote
-        warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
-    it = iter(csv.reader(stream, delimiter=delimiter, quotechar=quotechar))
-    if not ignore_errors:
-        if skipfirst:
-            it.next()
-        for row in it:
-            decoded = [item.decode(encoding) for item in row]
-            if not skip_empty or any(decoded):
-                yield decoded
-    else:
-        if skipfirst:
-            try:
-                row = it.next()
-            except csv.Error:
-                pass
-        # Safe version, that can cope with error in CSV file
-        while True:
-            try:
-                row = it.next()
-            # End of CSV, break
-            except StopIteration:
-                break
-            # Error in CSV, ignore line and continue
-            except csv.Error:
-                continue
-            decoded = [item.decode(encoding) for item in row]
-            if not skip_empty or any(decoded):
-                yield decoded
-
-
-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 not 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(ucsvreader(open(filename)))
-    """
-    header = reader.next()
-    for row in reader:
-        yield dict(zip(header, row))
-
-def lazydbtable(cu, table, headers, orderby=None):
-    """return an iterator on rows of a sql table. On each row, fetch columns
-    defined in headers and return values as a dictionary.
-
-    >>> data = lazydbtable(cu, 'experimentation', ('id', 'nickname', 'gps'))
-    """
-    sql = 'SELECT %s FROM %s' % (','.join(headers), table,)
-    if orderby:
-        sql += ' ORDER BY %s' % ','.join(orderby)
-    cu.execute(sql)
-    while True:
-        row = cu.fetchone()
-        if row is None:
-            break
-        yield dict(zip(headers, row))
-
-def mk_entity(row, map):
-    """Return a dict made from sanitized mapped values.
-
-    ValueError can be raised on unexpected values found in checkers
-
-    >>> row = {'myname': u'dupont'}
-    >>> map = [('myname', u'name', (call_transform_method('title'),))]
-    >>> mk_entity(row, map)
-    {'name': u'Dupont'}
-    >>> row = {'myname': u'dupont', 'optname': u''}
-    >>> map = [('myname', u'name', (call_transform_method('title'),)),
-    ...        ('optname', u'MARKER', (optional,))]
-    >>> mk_entity(row, map)
-    {'name': u'Dupont', 'optname': None}
-    """
-    res = {}
-    assert isinstance(row, dict)
-    assert isinstance(map, list)
-    for src, dest, funcs in map:
-        try:
-            res[dest] = row[src]
-        except KeyError:
-            continue
-        try:
-            for func in funcs:
-                res[dest] = func(res[dest])
-                if res[dest] is None:
-                    break
-        except ValueError as err:
-            raise ValueError('error with %r field: %s' % (src, err)), None, sys.exc_info()[-1]
-    return res
-
-# user interactions ############################################################
-
-def tell(msg):
-    print msg
-
-def confirm(question):
-    """A confirm function that asks for yes/no/abort and exits on abort."""
-    answer = shellutils.ASK.ask(question, ('Y', 'n', 'abort'), 'Y')
-    if answer == 'abort':
-        sys.exit(1)
-    return answer == 'Y'
-
-
-class catch_error(object):
-    """Helper for @contextmanager decorator."""
-
-    def __init__(self, ctl, key='unexpected error', msg=None):
-        self.ctl = ctl
-        self.key = key
-        self.msg = msg
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, type, value, traceback):
-        if type is not None:
-            if issubclass(type, (KeyboardInterrupt, SystemExit)):
-                return # re-raise
-            if self.ctl.catcherrors:
-                self.ctl.record_error(self.key, None, type, value, traceback)
-                return True # silent
-
-
-# base sanitizing/coercing functions ###########################################
-
-def optional(value):
-    """checker to filter optional field
-
-    If value is undefined (ex: empty string), return None that will
-    break the checkers validation chain
-
-    General use is to add 'optional' check in first condition to avoid
-    ValueError by further checkers
-
-    >>> MAPPER = [(u'value', 'value', (optional, int))]
-    >>> row = {'value': u'XXX'}
-    >>> mk_entity(row, MAPPER)
-    {'value': None}
-    >>> row = {'value': u'100'}
-    >>> mk_entity(row, MAPPER)
-    {'value': 100}
-    """
-    if value:
-        return value
-    return None
-
-def required(value):
-    """raise ValueError if value is empty
-
-    This check should be often found in last position in the chain.
-    """
-    if value:
-        return value
-    raise ValueError("required")
-
-def todatetime(format='%d/%m/%Y'):
-    """return a transformation function to turn string input value into a
-    `datetime.datetime` instance, using given format.
-
-    Follow it by `todate` or `totime` functions from `logilab.common.date` if
-    you want a `date`/`time` instance instead of `datetime`.
-    """
-    def coerce(value):
-        return strptime(value, format)
-    return coerce
-
-def call_transform_method(methodname, *args, **kwargs):
-    """return value returned by calling the given method on input"""
-    def coerce(value):
-        return getattr(value, methodname)(*args, **kwargs)
-    return coerce
-
-def call_check_method(methodname, *args, **kwargs):
-    """check value returned by calling the given method on input is true,
-    else raise ValueError
-    """
-    def check(value):
-        if getattr(value, methodname)(*args, **kwargs):
-            return value
-        raise ValueError('%s not verified on %r' % (methodname, value))
-    return check
-
-# base integrity checking functions ############################################
-
-def check_doubles(buckets):
-    """Extract the keys that have more than one item in their bucket."""
-    return [(k, len(v)) for k, v in buckets.items() if len(v) > 1]
-
-def check_doubles_not_none(buckets):
-    """Extract the keys that have more than one item in their bucket."""
-    return [(k, len(v)) for k, v in buckets.items()
-            if k is not None and len(v) > 1]
-
-# sql generator utility functions #############################################
-
-
-def _import_statements(sql_connect, statements, nb_threads=3,
-                       dump_output_dir=None,
-                       support_copy_from=True, encoding='utf-8'):
-    """
-    Import a bunch of sql statements, using different threads.
-    """
-    try:
-        chunksize = (len(statements) / nb_threads) + 1
-        threads = []
-        for i in xrange(nb_threads):
-            chunks = statements[i*chunksize:(i+1)*chunksize]
-            thread = threading.Thread(target=_execmany_thread,
-                                      args=(sql_connect, chunks,
-                                            dump_output_dir,
-                                            support_copy_from,
-                                            encoding))
-            thread.start()
-            threads.append(thread)
-        for t in threads:
-            t.join()
-    except Exception:
-        print 'Error in import statements'
-
-def _execmany_thread_not_copy_from(cu, statement, data, table=None,
-                                   columns=None, encoding='utf-8'):
-    """ Execute thread without copy from
-    """
-    cu.executemany(statement, data)
-
-def _execmany_thread_copy_from(cu, statement, data, table,
-                               columns, encoding='utf-8'):
-    """ Execute thread with copy from
-    """
-    buf = _create_copyfrom_buffer(data, columns, encoding=encoding)
-    if buf is None:
-        _execmany_thread_not_copy_from(cu, statement, data)
-    else:
-        if columns is None:
-            cu.copy_from(buf, table, null='NULL')
-        else:
-            cu.copy_from(buf, table, null='NULL', columns=columns)
-
-def _execmany_thread(sql_connect, statements, dump_output_dir=None,
-                     support_copy_from=True, encoding='utf-8'):
-    """
-    Execute sql statement. If 'INSERT INTO', try to use 'COPY FROM' command,
-    or fallback to execute_many.
-    """
-    if support_copy_from:
-        execmany_func = _execmany_thread_copy_from
-    else:
-        execmany_func = _execmany_thread_not_copy_from
-    cnx = sql_connect()
-    cu = cnx.cursor()
-    try:
-        for statement, data in statements:
-            table = None
-            columns = None
-            try:
-                if not statement.startswith('INSERT INTO'):
-                    cu.executemany(statement, data)
-                    continue
-                table = statement.split()[2]
-                if isinstance(data[0], (tuple, list)):
-                    columns = None
-                else:
-                    columns = list(data[0])
-                execmany_func(cu, statement, data, table, columns, encoding)
-            except Exception:
-                print 'unable to copy data into table %s' % table
-                # Error in import statement, save data in dump_output_dir
-                if dump_output_dir is not None:
-                    pdata = {'data': data, 'statement': statement,
-                             'time': asctime(), 'columns': columns}
-                    filename = make_uid()
-                    try:
-                        with open(osp.join(dump_output_dir,
-                                           '%s.pickle' % filename), 'w') as fobj:
-                            fobj.write(cPickle.dumps(pdata))
-                    except IOError:
-                        print 'ERROR while pickling in', dump_output_dir, filename+'.pickle'
-                        pass
-                cnx.rollback()
-                raise
-    finally:
-        cnx.commit()
-        cu.close()
-
-
-def _copyfrom_buffer_convert_None(value, **opts):
-    '''Convert None value to "NULL"'''
-    return 'NULL'
-
-def _copyfrom_buffer_convert_number(value, **opts):
-    '''Convert a number into its string representation'''
-    return str(value)
-
-def _copyfrom_buffer_convert_string(value, **opts):
-    '''Convert string value.
-
-    Recognized keywords:
-    :encoding: resulting string encoding (default: utf-8)
-    :replace_sep: character used when input contains characters
-                  that conflict with the column separator.
-    '''
-    encoding = opts.get('encoding','utf-8')
-    replace_sep = opts.get('replace_sep', None)
-    # Remove separators used in string formatting
-    for _char in (u'\t', u'\r', u'\n'):
-        if _char in value:
-            # If a replace_sep is given, replace
-            # the separator
-            # (and thus avoid empty buffer)
-            if replace_sep is None:
-                raise ValueError('conflicting separator: '
-                                 'you must provide the replace_sep option')
-            value = value.replace(_char, replace_sep)
-        value = value.replace('\\', r'\\')
-    if isinstance(value, unicode):
-        value = value.encode(encoding)
-    return value
-
-def _copyfrom_buffer_convert_date(value, **opts):
-    '''Convert date into "YYYY-MM-DD"'''
-    # Do not use strftime, as it yields issue with date < 1900
-    # (http://bugs.python.org/issue1777412)
-    return '%04d-%02d-%02d' % (value.year, value.month, value.day)
-
-def _copyfrom_buffer_convert_datetime(value, **opts):
-    '''Convert date into "YYYY-MM-DD HH:MM:SS.UUUUUU"'''
-    # Do not use strftime, as it yields issue with date < 1900
-    # (http://bugs.python.org/issue1777412)
-    return '%s %s' % (_copyfrom_buffer_convert_date(value, **opts),
-                      _copyfrom_buffer_convert_time(value, **opts))
-
-def _copyfrom_buffer_convert_time(value, **opts):
-    '''Convert time into "HH:MM:SS.UUUUUU"'''
-    return '%02d:%02d:%02d.%06d' % (value.hour, value.minute,
-                                    value.second, value.microsecond)
-
-# (types, converter) list.
-_COPYFROM_BUFFER_CONVERTERS = [
-    (type(None), _copyfrom_buffer_convert_None),
-    ((long, int, float), _copyfrom_buffer_convert_number),
-    (basestring, _copyfrom_buffer_convert_string),
-    (datetime, _copyfrom_buffer_convert_datetime),
-    (date, _copyfrom_buffer_convert_date),
-    (time, _copyfrom_buffer_convert_time),
-]
-
-def _create_copyfrom_buffer(data, columns=None, **convert_opts):
-    """
-    Create a StringIO buffer for 'COPY FROM' command.
-    Deals with Unicode, Int, Float, Date... (see ``converters``)
-
-    :data: a sequence/dict of tuples
-    :columns: list of columns to consider (default to all columns)
-    :converter_opts: keyword arguements given to converters
-    """
-    # Create a list rather than directly create a StringIO
-    # to correctly write lines separated by '\n' in a single step
-    rows = []
-    if columns is None:
-        if isinstance(data[0], (tuple, list)):
-            columns = range(len(data[0]))
-        elif isinstance(data[0], dict):
-            columns = data[0].keys()
-        else:
-            raise ValueError('Could not get columns: you must provide columns.')
-    for row in data:
-        # Iterate over the different columns and the different values
-        # and try to convert them to a correct datatype.
-        # If an error is raised, do not continue.
-        formatted_row = []
-        for col in columns:
-            try:
-                value = row[col]
-            except KeyError:
-                warnings.warn(u"Column %s is not accessible in row %s"
-                              % (col, row), RuntimeWarning)
-                # XXX 'value' set to None so that the import does not end in
-                # error.
-                # Instead, the extra keys are set to NULL from the
-                # database point of view.
-                value = None
-            for types, converter in _COPYFROM_BUFFER_CONVERTERS:
-                if isinstance(value, types):
-                    value = converter(value, **convert_opts)
-                    break
-            else:
-                raise ValueError("Unsupported value type %s" % type(value))
-            # We push the value to the new formatted row
-            # if the value is not None and could be converted to a string.
-            formatted_row.append(value)
-        rows.append('\t'.join(formatted_row))
-    return StringIO('\n'.join(rows))
-
-
-# object stores #################################################################
-
-class ObjectStore(object):
-    """Store objects in memory for *faster* validation (development mode)
-
-    But it will not enforce the constraints of the schema and hence will miss some problems
-
-    >>> store = ObjectStore()
-    >>> user = store.create_entity('CWUser', login=u'johndoe')
-    >>> group = store.create_entity('CWUser', name=u'unknown')
-    >>> store.relate(user.eid, 'in_group', group.eid)
-    """
-    def __init__(self):
-        self.items = []
-        self.eids = {}
-        self.types = {}
-        self.relations = set()
-        self.indexes = {}
-
-    def create_entity(self, etype, **data):
-        data = attrdict(data)
-        data['eid'] = eid = len(self.items)
-        self.items.append(data)
-        self.eids[eid] = data
-        self.types.setdefault(etype, []).append(eid)
-        return data
-
-    def relate(self, eid_from, rtype, eid_to, **kwargs):
-        """Add new relation"""
-        relation = eid_from, rtype, eid_to
-        self.relations.add(relation)
-        return relation
-
-    def commit(self):
-        """this commit method does nothing by default"""
-        return
-
-    def flush(self):
-        """The method is provided so that all stores share a common API"""
-        pass
-
-    @property
-    def nb_inserted_entities(self):
-        return len(self.eids)
-    @property
-    def nb_inserted_types(self):
-        return len(self.types)
-    @property
-    def nb_inserted_relations(self):
-        return len(self.relations)
-
-class RQLObjectStore(ObjectStore):
-    """ObjectStore that works with an actual RQL repository (production mode)"""
-
-    def __init__(self, cnx, commit=None):
-        if commit is not None:
-            warnings.warn('[3.19] commit argument should not be specified '
-                          'as the cnx object already provides it.',
-                          DeprecationWarning, stacklevel=2)
-        super(RQLObjectStore, self).__init__()
-        self._cnx = cnx
-        self._commit = commit or cnx.commit
-
-    def commit(self):
-        return self._commit()
-
-    def rql(self, *args):
-        return self._cnx.execute(*args)
-
-    @property
-    def session(self):
-        warnings.warn('[3.19] deprecated property.', DeprecationWarning,
-                      stacklevel=2)
-        return self._cnx.repo._get_session(self._cnx.sessionid)
-
-    def create_entity(self, *args, **kwargs):
-        entity = self._cnx.create_entity(*args, **kwargs)
-        self.eids[entity.eid] = entity
-        self.types.setdefault(args[0], []).append(entity.eid)
-        return entity
-
-    def relate(self, eid_from, rtype, eid_to, **kwargs):
-        eid_from, rtype, eid_to = super(RQLObjectStore, self).relate(
-            eid_from, rtype, eid_to, **kwargs)
-        self.rql('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                 {'x': int(eid_from), 'y': int(eid_to)})
-
-    @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
-    def find_entities(self, *args, **kwargs):
-        return self._cnx.find(*args, **kwargs).entities()
-
-    @deprecated("[3.19] use cnx.find(*args, **kwargs).one() instead")
-    def find_one_entity(self, *args, **kwargs):
-        return self._cnx.find(*args, **kwargs).one()
-
-# the import controller ########################################################
-
-class CWImportController(object):
-    """Controller of the data import process.
-
-    >>> ctl = CWImportController(store)
-    >>> ctl.generators = list_of_data_generators
-    >>> ctl.data = dict_of_data_tables
-    >>> ctl.run()
-    """
-
-    def __init__(self, store, askerror=0, catcherrors=None, tell=tell,
-                 commitevery=50):
-        self.store = store
-        self.generators = None
-        self.data = {}
-        self.errors = None
-        self.askerror = askerror
-        if  catcherrors is None:
-            catcherrors = askerror
-        self.catcherrors = catcherrors
-        self.commitevery = commitevery # set to None to do a single commit
-        self._tell = tell
-
-    def check(self, type, key, value):
-        self._checks.setdefault(type, {}).setdefault(key, []).append(value)
-
-    def check_map(self, entity, key, map, default):
-        try:
-            entity[key] = map[entity[key]]
-        except KeyError:
-            self.check(key, entity[key], None)
-            entity[key] = default
-
-    def record_error(self, key, msg=None, type=None, value=None, tb=None):
-        tmp = StringIO()
-        if type is None:
-            traceback.print_exc(file=tmp)
-        else:
-            traceback.print_exception(type, value, tb, file=tmp)
-        # use a list to avoid counting a <nb lines> errors instead of one
-        errorlog = self.errors.setdefault(key, [])
-        if msg is None:
-            errorlog.append(tmp.getvalue().splitlines())
-        else:
-            errorlog.append( (msg, tmp.getvalue().splitlines()) )
-
-    def run(self):
-        self.errors = {}
-        if self.commitevery is None:
-            self.tell('Will commit all or nothing.')
-        else:
-            self.tell('Will commit every %s iterations' % self.commitevery)
-        for func, checks in self.generators:
-            self._checks = {}
-            func_name = func.__name__
-            self.tell("Run import function '%s'..." % func_name)
-            try:
-                func(self)
-            except Exception:
-                if self.catcherrors:
-                    self.record_error(func_name, 'While calling %s' % func.__name__)
-                else:
-                    self._print_stats()
-                    raise
-            for key, func, title, help in checks:
-                buckets = self._checks.get(key)
-                if buckets:
-                    err = func(buckets)
-                    if err:
-                        self.errors[title] = (help, err)
-        try:
-            txuuid = self.store.commit()
-            if txuuid is not None:
-                self.tell('Transaction commited (txuuid: %s)' % txuuid)
-        except QueryError as ex:
-            self.tell('Transaction aborted: %s' % ex)
-        self._print_stats()
-        if self.errors:
-            if self.askerror == 2 or (self.askerror and confirm('Display errors ?')):
-                from pprint import pformat
-                for errkey, error in self.errors.items():
-                    self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1])))
-                    self.tell(pformat(sorted(error[1])))
-
-    def _print_stats(self):
-        nberrors = sum(len(err) for err in self.errors.itervalues())
-        self.tell('\nImport statistics: %i entities, %i types, %i relations and %i errors'
-                  % (self.store.nb_inserted_entities,
-                     self.store.nb_inserted_types,
-                     self.store.nb_inserted_relations,
-                     nberrors))
-
-    def get_data(self, key):
-        return self.data.get(key)
-
-    def index(self, name, key, value, unique=False):
-        """create a new index
-
-        If unique is set to True, only first occurence will be kept not the following ones
-        """
-        if unique:
-            try:
-                if value in self.store.indexes[name][key]:
-                    return
-            except KeyError:
-                # we're sure that one is the first occurence; so continue...
-                pass
-        self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
-
-    def tell(self, msg):
-        self._tell(msg)
-
-    def iter_and_commit(self, datakey):
-        """iter rows, triggering commit every self.commitevery iterations"""
-        if self.commitevery is None:
-            return self.get_data(datakey)
-        else:
-            return callfunc_every(self.store.commit,
-                                  self.commitevery,
-                                  self.get_data(datakey))
-
-
-class NoHookRQLObjectStore(RQLObjectStore):
-    """ObjectStore that works with an actual RQL repository (production mode)"""
-
-    def __init__(self, cnx, metagen=None, baseurl=None):
-        super(NoHookRQLObjectStore, self).__init__(cnx)
-        self.source = cnx.repo.system_source
-        self.rschema = cnx.repo.schema.rschema
-        self.add_relation = self.source.add_relation
-        if metagen is None:
-            metagen = MetaGenerator(cnx, baseurl)
-        self.metagen = metagen
-        self._nb_inserted_entities = 0
-        self._nb_inserted_types = 0
-        self._nb_inserted_relations = 0
-        # deactivate security
-        cnx.read_security = False
-        cnx.write_security = False
-
-    def create_entity(self, etype, **kwargs):
-        for k, v in kwargs.iteritems():
-            kwargs[k] = getattr(v, 'eid', v)
-        entity, rels = self.metagen.base_etype_dicts(etype)
-        # make a copy to keep cached entity pristine
-        entity = copy(entity)
-        entity.cw_edited = copy(entity.cw_edited)
-        entity.cw_clear_relation_cache()
-        entity.cw_edited.update(kwargs, skipsec=False)
-        entity_source, extid = self.metagen.init_entity(entity)
-        cnx = self._cnx
-        self.source.add_entity(cnx, entity)
-        self.source.add_info(cnx, entity, entity_source, extid)
-        kwargs = dict()
-        if inspect.getargspec(self.add_relation).keywords:
-            kwargs['subjtype'] = entity.cw_etype
-        for rtype, targeteids in rels.iteritems():
-            # targeteids may be a single eid or a list of eids
-            inlined = self.rschema(rtype).inlined
-            try:
-                for targeteid in targeteids:
-                    self.add_relation(cnx, entity.eid, rtype, targeteid,
-                                      inlined, **kwargs)
-            except TypeError:
-                self.add_relation(cnx, entity.eid, rtype, targeteids,
-                                  inlined, **kwargs)
-        self._nb_inserted_entities += 1
-        return entity
-
-    def relate(self, eid_from, rtype, eid_to, **kwargs):
-        assert not rtype.startswith('reverse_')
-        self.add_relation(self._cnx, eid_from, rtype, eid_to,
-                          self.rschema(rtype).inlined)
-        if self.rschema(rtype).symmetric:
-            self.add_relation(self._cnx, eid_to, rtype, eid_from,
-                              self.rschema(rtype).inlined)
-        self._nb_inserted_relations += 1
-
-    @property
-    def nb_inserted_entities(self):
-        return self._nb_inserted_entities
-    @property
-    def nb_inserted_types(self):
-        return self._nb_inserted_types
-    @property
-    def nb_inserted_relations(self):
-        return self._nb_inserted_relations
-
-
-class MetaGenerator(object):
-    META_RELATIONS = (META_RTYPES
-                      - VIRTUAL_RTYPES
-                      - set(('eid', 'cwuri',
-                             'is', 'is_instance_of', 'cw_source')))
-
-    def __init__(self, cnx, baseurl=None, source=None):
-        self._cnx = cnx
-        if baseurl is None:
-            config = cnx.vreg.config
-            baseurl = config['base-url'] or config.default_base_url()
-        if not baseurl[-1] == '/':
-            baseurl += '/'
-        self.baseurl = baseurl
-        if source is None:
-            source = cnx.repo.system_source
-        self.source = source
-        self.create_eid = cnx.repo.system_source.create_eid
-        self.time = datetime.now()
-        # attributes/relations shared by all entities of the same type
-        self.etype_attrs = []
-        self.etype_rels = []
-        # attributes/relations specific to each entity
-        self.entity_attrs = ['cwuri']
-        #self.entity_rels = [] XXX not handled (YAGNI?)
-        schema = cnx.vreg.schema
-        rschema = schema.rschema
-        for rtype in self.META_RELATIONS:
-            # skip owned_by / created_by if user is the internal manager
-            if cnx.user.eid == -1 and rtype in ('owned_by', 'created_by'):
-                continue
-            if rschema(rtype).final:
-                self.etype_attrs.append(rtype)
-            else:
-                self.etype_rels.append(rtype)
-
-    @cached
-    def base_etype_dicts(self, etype):
-        entity = self._cnx.vreg['etypes'].etype_class(etype)(self._cnx)
-        # entity are "surface" copied, avoid shared dict between copies
-        del entity.cw_extra_kwargs
-        entity.cw_edited = EditedEntity(entity)
-        for attr in self.etype_attrs:
-            genfunc = self.generate(attr)
-            if genfunc:
-                entity.cw_edited.edited_attribute(attr, genfunc(entity))
-        rels = {}
-        for rel in self.etype_rels:
-            genfunc = self.generate(rel)
-            if genfunc:
-                rels[rel] = genfunc(entity)
-        return entity, rels
-
-    def init_entity(self, entity):
-        entity.eid = self.create_eid(self._cnx)
-        extid = entity.cw_edited.get('cwuri')
-        for attr in self.entity_attrs:
-            if attr in entity.cw_edited:
-                # already set, skip this attribute
-                continue
-            genfunc = self.generate(attr)
-            if genfunc:
-                entity.cw_edited.edited_attribute(attr, genfunc(entity))
-        if isinstance(extid, unicode):
-            extid = extid.encode('utf-8')
-        return self.source, extid
-
-    def generate(self, rtype):
-        return getattr(self, 'gen_%s' % rtype, None)
-
-    def gen_cwuri(self, entity):
-        assert self.baseurl, 'baseurl is None while generating cwuri'
-        return u'%s%s' % (self.baseurl, entity.eid)
-
-    def gen_creation_date(self, entity):
-        return self.time
-
-    def gen_modification_date(self, entity):
-        return self.time
-
-    def gen_created_by(self, entity):
-        return self._cnx.user.eid
-
-    def gen_owned_by(self, entity):
-        return self._cnx.user.eid
-
-
-###########################################################################
-## SQL object store #######################################################
-###########################################################################
-class SQLGenObjectStore(NoHookRQLObjectStore):
-    """Controller of the data import process. This version is based
-    on direct insertions throught SQL command (COPY FROM or execute many).
-
-    >>> store = SQLGenObjectStore(cnx)
-    >>> store.create_entity('Person', ...)
-    >>> store.flush()
-    """
-
-    def __init__(self, cnx, dump_output_dir=None, nb_threads_statement=3):
-        """
-        Initialize a SQLGenObjectStore.
-
-        Parameters:
-
-          - cnx: connection on the cubicweb instance
-          - dump_output_dir: a directory to dump failed statements
-            for easier recovery. Default is None (no dump).
-          - nb_threads_statement: number of threads used
-            for SQL insertion (default is 3).
-        """
-        super(SQLGenObjectStore, self).__init__(cnx)
-        ### hijack default source
-        self.source = SQLGenSourceWrapper(
-            self.source, cnx.vreg.schema,
-            dump_output_dir=dump_output_dir,
-            nb_threads_statement=nb_threads_statement)
-        ### XXX This is done in super().__init__(), but should be
-        ### redone here to link to the correct source
-        self.add_relation = self.source.add_relation
-        self.indexes_etypes = {}
-
-    def flush(self):
-        """Flush data to the database"""
-        self.source.flush()
-
-    def relate(self, subj_eid, rtype, obj_eid, **kwargs):
-        if subj_eid is None or obj_eid is None:
-            return
-        # XXX Could subjtype be inferred ?
-        self.source.add_relation(self._cnx, subj_eid, rtype, obj_eid,
-                                 self.rschema(rtype).inlined, **kwargs)
-        if self.rschema(rtype).symmetric:
-            self.source.add_relation(self._cnx, obj_eid, rtype, subj_eid,
-                                     self.rschema(rtype).inlined, **kwargs)
-
-    def drop_indexes(self, etype):
-        """Drop indexes for a given entity type"""
-        if etype not in self.indexes_etypes:
-            cu = self._cnx.cnxset.cu
-            def index_to_attr(index):
-                """turn an index name to (database) attribute name"""
-                return index.replace(etype.lower(), '').replace('idx', '').strip('_')
-            indices = [(index, index_to_attr(index))
-                       for index in self.source.dbhelper.list_indices(cu, etype)
-                       # Do not consider 'cw_etype_pkey' index
-                       if not index.endswith('key')]
-            self.indexes_etypes[etype] = indices
-        for index, attr in self.indexes_etypes[etype]:
-            self._cnx.system_sql('DROP INDEX %s' % index)
-
-    def create_indexes(self, etype):
-        """Recreate indexes for a given entity type"""
-        for index, attr in self.indexes_etypes.get(etype, []):
-            sql = 'CREATE INDEX %s ON cw_%s(%s)' % (index, etype, attr)
-            self._cnx.system_sql(sql)
-
-
-###########################################################################
-## SQL Source #############################################################
-###########################################################################
-
-class SQLGenSourceWrapper(object):
-
-    def __init__(self, system_source, schema,
-                 dump_output_dir=None, nb_threads_statement=3):
-        self.system_source = system_source
-        self._sql = threading.local()
-        # Explicitely backport attributes from system source
-        self._storage_handler = self.system_source._storage_handler
-        self.preprocess_entity = self.system_source.preprocess_entity
-        self.sqlgen = self.system_source.sqlgen
-        self.uri = self.system_source.uri
-        self.eid = self.system_source.eid
-        # Directory to write temporary files
-        self.dump_output_dir = dump_output_dir
-        # Allow to execute code with SQLite backend that does
-        # not support (yet...) copy_from
-        # XXX Should be dealt with in logilab.database
-        spcfrom = system_source.dbhelper.dbapi_module.support_copy_from
-        self.support_copy_from = spcfrom
-        self.dbencoding = system_source.dbhelper.dbencoding
-        self.nb_threads_statement = nb_threads_statement
-        # initialize thread-local data for main thread
-        self.init_thread_locals()
-        self._inlined_rtypes_cache = {}
-        self._fill_inlined_rtypes_cache(schema)
-        self.schema = schema
-        self.do_fti = False
-
-    def _fill_inlined_rtypes_cache(self, schema):
-        cache = self._inlined_rtypes_cache
-        for eschema in schema.entities():
-            for rschema in eschema.ordered_relations():
-                if rschema.inlined:
-                    cache[eschema.type] = SQL_PREFIX + rschema.type
-
-    def init_thread_locals(self):
-        """initializes thread-local data"""
-        self._sql.entities = defaultdict(list)
-        self._sql.relations = {}
-        self._sql.inlined_relations = {}
-        # keep track, for each eid of the corresponding data dict
-        self._sql.eid_insertdicts = {}
-
-    def flush(self):
-        print 'starting flush'
-        _entities_sql = self._sql.entities
-        _relations_sql = self._sql.relations
-        _inlined_relations_sql = self._sql.inlined_relations
-        _insertdicts = self._sql.eid_insertdicts
-        try:
-            # try, for each inlined_relation, to find if we're also creating
-            # the host entity (i.e. the subject of the relation).
-            # In that case, simply update the insert dict and remove
-            # the need to make the
-            # UPDATE statement
-            for statement, datalist in _inlined_relations_sql.iteritems():
-                new_datalist = []
-                # for a given inlined relation,
-                # browse each couple to be inserted
-                for data in datalist:
-                    keys = list(data)
-                    # For inlined relations, it exists only two case:
-                    # (rtype, cw_eid) or (cw_eid, rtype)
-                    if keys[0] == 'cw_eid':
-                        rtype = keys[1]
-                    else:
-                        rtype = keys[0]
-                    updated_eid = data['cw_eid']
-                    if updated_eid in _insertdicts:
-                        _insertdicts[updated_eid][rtype] = data[rtype]
-                    else:
-                        # could not find corresponding insert dict, keep the
-                        # UPDATE query
-                        new_datalist.append(data)
-                _inlined_relations_sql[statement] = new_datalist
-            _import_statements(self.system_source.get_connection,
-                               _entities_sql.items()
-                               + _relations_sql.items()
-                               + _inlined_relations_sql.items(),
-                               dump_output_dir=self.dump_output_dir,
-                               nb_threads=self.nb_threads_statement,
-                               support_copy_from=self.support_copy_from,
-                               encoding=self.dbencoding)
-        finally:
-            _entities_sql.clear()
-            _relations_sql.clear()
-            _insertdicts.clear()
-            _inlined_relations_sql.clear()
-
-    def add_relation(self, cnx, subject, rtype, object,
-                     inlined=False, **kwargs):
-        if inlined:
-            _sql = self._sql.inlined_relations
-            data = {'cw_eid': subject, SQL_PREFIX + rtype: object}
-            subjtype = kwargs.get('subjtype')
-            if subjtype is None:
-                # Try to infer it
-                targets = [t.type for t in
-                           self.schema.rschema(rtype).subjects()]
-                if len(targets) == 1:
-                    subjtype = targets[0]
-                else:
-                    raise ValueError('You should give the subject etype for '
-                                     'inlined relation %s'
-                                     ', as it cannot be inferred: '
-                                     'this type is given as keyword argument '
-                                     '``subjtype``'% rtype)
-            statement = self.sqlgen.update(SQL_PREFIX + subjtype,
-                                           data, ['cw_eid'])
-        else:
-            _sql = self._sql.relations
-            data = {'eid_from': subject, 'eid_to': object}
-            statement = self.sqlgen.insert('%s_relation' % rtype, data)
-        if statement in _sql:
-            _sql[statement].append(data)
-        else:
-            _sql[statement] = [data]
-
-    def add_entity(self, cnx, entity):
-        with self._storage_handler(entity, 'added'):
-            attrs = self.preprocess_entity(entity)
-            rtypes = self._inlined_rtypes_cache.get(entity.cw_etype, ())
-            if isinstance(rtypes, str):
-                rtypes = (rtypes,)
-            for rtype in rtypes:
-                if rtype not in attrs:
-                    attrs[rtype] = None
-            sql = self.sqlgen.insert(SQL_PREFIX + entity.cw_etype, attrs)
-            self._sql.eid_insertdicts[entity.eid] = attrs
-            self._append_to_entities(sql, attrs)
-
-    def _append_to_entities(self, sql, attrs):
-        self._sql.entities[sql].append(attrs)
-
-    def _handle_insert_entity_sql(self, cnx, sql, attrs):
-        # We have to overwrite the source given in parameters
-        # as here, we directly use the system source
-        attrs['asource'] = self.system_source.uri
-        self._append_to_entities(sql, attrs)
-
-    def _handle_is_relation_sql(self, cnx, sql, attrs):
-        self._append_to_entities(sql, attrs)
-
-    def _handle_is_instance_of_sql(self, cnx, sql, attrs):
-        self._append_to_entities(sql, attrs)
-
-    def _handle_source_relation_sql(self, cnx, sql, attrs):
-        self._append_to_entities(sql, attrs)
-
-    # add_info is _copypasted_ from the one in NativeSQLSource. We want it
-    # there because it will use the _handlers of the SQLGenSourceWrapper, which
-    # are not like the ones in the native source.
-    def add_info(self, cnx, entity, source, extid):
-        """add type and source info for an eid into the system table"""
-        # begin by inserting eid/type/source/extid into the entities table
-        if extid is not None:
-            assert isinstance(extid, str)
-            extid = b64encode(extid)
-        attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': extid,
-                 'asource': source.uri}
-        self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
-        # insert core relations: is, is_instance_of and cw_source
-        try:
-            self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)',
-                                         (entity.eid, eschema_eid(cnx, entity.e_schema)))
-        except IndexError:
-            # during schema serialization, skip
-            pass
-        else:
-            for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
-                self._handle_is_relation_sql(cnx,
-                                             'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)',
-                                             (entity.eid, eschema_eid(cnx, eschema)))
-        if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10
-            self._handle_is_relation_sql(cnx, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)',
-                                         (entity.eid, source.eid))
-        # now we can update the full text index
-        if self.do_fti and self.need_fti_indexation(entity.cw_etype):
-            self.index_entity(cnx, entity=entity)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,35 @@
+# copyright 2003-2015 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/>.
+"""Package containing various utilities to import data into cubicweb."""
+
+
+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 not idx % number:
+            func()
+    func()
+
+# import for backward compat
+from cubicweb.dataimport.stores import *
+from cubicweb.dataimport.pgstore import *
+from cubicweb.dataimport.csv import *
+from cubicweb.dataimport.deprecated import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/csv.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,113 @@
+# copyright 2003-2015 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/>.
+"""Functions to help importing CSV data"""
+
+from __future__ import absolute_import
+
+import csv as csvmod
+import warnings
+import os.path as osp
+
+from logilab.common import shellutils
+
+
+def count_lines(stream_or_filename):
+    if isinstance(stream_or_filename, basestring):
+        f = open(stream_or_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', delimiter=',', quotechar='"',
+                  skipfirst=False, withpb=True, skip_empty=True, separator=None,
+                  quote=None):
+    """same as :func:`ucsvreader` but a progress bar is displayed as we iter on rows"""
+    if separator is not None:
+        delimiter = separator
+        warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
+    if quote is not None:
+        quotechar = quote
+        warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
+    if isinstance(stream_or_path, basestring):
+        if not osp.exists(stream_or_path):
+            raise Exception("file doesn't exists: %s" % stream_or_path)
+        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(stream, encoding, delimiter, quotechar,
+                           skipfirst=skipfirst, skip_empty=skip_empty):
+        yield urow
+        if withpb:
+            pb.update()
+    print ' %s rows imported' % rowcount
+
+
+def ucsvreader(stream, encoding='utf-8', delimiter=',', quotechar='"',
+               skipfirst=False, ignore_errors=False, skip_empty=True,
+               separator=None, quote=None):
+    """A csv reader that accepts files with any encoding and outputs unicode
+    strings
+
+    if skip_empty (the default), lines without any values specified (only
+    separators) will be skipped. This is useful for Excel exports which may be
+    full of such lines.
+    """
+    if separator is not None:
+        delimiter = separator
+        warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
+    if quote is not None:
+        quotechar = quote
+        warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
+    it = iter(csvmod.reader(stream, delimiter=delimiter, quotechar=quotechar))
+    if not ignore_errors:
+        if skipfirst:
+            it.next()
+        for row in it:
+            decoded = [item.decode(encoding) for item in row]
+            if not skip_empty or any(decoded):
+                yield decoded
+    else:
+        if skipfirst:
+            try:
+                row = it.next()
+            except csvmod.Error:
+                pass
+        # Safe version, that can cope with error in CSV file
+        while True:
+            try:
+                row = it.next()
+            # End of CSV, break
+            except StopIteration:
+                break
+            # Error in CSV, ignore line and continue
+            except csvmod.Error:
+                continue
+            decoded = [item.decode(encoding) for item in row]
+            if not skip_empty or any(decoded):
+                yield decoded
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/deprecated.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,460 @@
+# copyright 2003-2015 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/>.
+"""Old and deprecated dataimport API that provides tools to import tabular data.
+
+
+Example of use (run this with `cubicweb-ctl shell instance import-script.py`):
+
+.. sourcecode:: python
+
+  from cubicweb.dataimport import *
+  # define data generators
+  GENERATORS = []
+
+  USERS = [('Prenom', 'firstname', ()),
+           ('Nom', 'surname', ()),
+           ('Identifiant', 'login', ()),
+           ]
+
+  def gen_users(ctl):
+      for row in ctl.iter_and_commit('utilisateurs'):
+          entity = mk_entity(row, USERS)
+          entity['upassword'] = 'motdepasse'
+          ctl.check('login', entity['login'], None)
+          entity = ctl.store.prepare_insert_entity('CWUser', **entity)
+          email = ctl.store.prepare_insert_entity('EmailAddress', address=row['email'])
+          ctl.store.prepare_insert_relation(entity, 'use_email', email)
+          ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x': entity})
+
+  CHK = [('login', check_doubles, 'Utilisateurs Login',
+          'Deux utilisateurs ne devraient pas avoir le meme login.'),
+         ]
+
+  GENERATORS.append( (gen_users, CHK) )
+
+  # create controller
+  ctl = CWImportController(RQLObjectStore(cnx))
+  ctl.askerror = 1
+  ctl.generators = GENERATORS
+  ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
+  # run
+  ctl.run()
+
+.. BUG file with one column are not parsable
+.. TODO rollback() invocation is not possible yet
+"""
+
+import sys
+import traceback
+from StringIO import StringIO
+
+from logilab.common import attrdict, shellutils
+from logilab.common.date import strptime
+from logilab.common.deprecation import deprecated, class_deprecated
+
+from cubicweb import QueryError
+from cubicweb.dataimport import callfunc_every
+
+
+@deprecated('[3.21] deprecated')
+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(ucsvreader(open(filename)))
+    """
+    header = reader.next()
+    for row in reader:
+        yield dict(zip(header, row))
+
+
+@deprecated('[3.21] deprecated')
+def lazydbtable(cu, table, headers, orderby=None):
+    """return an iterator on rows of a sql table. On each row, fetch columns
+    defined in headers and return values as a dictionary.
+
+    >>> data = lazydbtable(cu, 'experimentation', ('id', 'nickname', 'gps'))
+    """
+    sql = 'SELECT %s FROM %s' % (','.join(headers), table,)
+    if orderby:
+        sql += ' ORDER BY %s' % ','.join(orderby)
+    cu.execute(sql)
+    while True:
+        row = cu.fetchone()
+        if row is None:
+            break
+        yield dict(zip(headers, row))
+
+
+@deprecated('[3.21] deprecated')
+def tell(msg):
+    print msg
+
+
+@deprecated('[3.21] deprecated')
+def confirm(question):
+    """A confirm function that asks for yes/no/abort and exits on abort."""
+    answer = shellutils.ASK.ask(question, ('Y', 'n', 'abort'), 'Y')
+    if answer == 'abort':
+        sys.exit(1)
+    return answer == 'Y'
+
+
+class catch_error(object):
+    """Helper for @contextmanager decorator."""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.21] deprecated'
+
+    def __init__(self, ctl, key='unexpected error', msg=None):
+        self.ctl = ctl
+        self.key = key
+        self.msg = msg
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        if type is not None:
+            if issubclass(type, (KeyboardInterrupt, SystemExit)):
+                return # re-raise
+            if self.ctl.catcherrors:
+                self.ctl.record_error(self.key, None, type, value, traceback)
+                return True # silent
+
+@deprecated('[3.21] deprecated')
+def mk_entity(row, map):
+    """Return a dict made from sanitized mapped values.
+
+    ValueError can be raised on unexpected values found in checkers
+
+    >>> row = {'myname': u'dupont'}
+    >>> map = [('myname', u'name', (call_transform_method('title'),))]
+    >>> mk_entity(row, map)
+    {'name': u'Dupont'}
+    >>> row = {'myname': u'dupont', 'optname': u''}
+    >>> map = [('myname', u'name', (call_transform_method('title'),)),
+    ...        ('optname', u'MARKER', (optional,))]
+    >>> mk_entity(row, map)
+    {'name': u'Dupont', 'optname': None}
+    """
+    res = {}
+    assert isinstance(row, dict)
+    assert isinstance(map, list)
+    for src, dest, funcs in map:
+        try:
+            res[dest] = row[src]
+        except KeyError:
+            continue
+        try:
+            for func in funcs:
+                res[dest] = func(res[dest])
+                if res[dest] is None:
+                    break
+        except ValueError as err:
+            raise ValueError('error with %r field: %s' % (src, err)), None, sys.exc_info()[-1]
+    return res
+
+
+# base sanitizing/coercing functions ###########################################
+
+@deprecated('[3.21] deprecated')
+def optional(value):
+    """checker to filter optional field
+
+    If value is undefined (ex: empty string), return None that will
+    break the checkers validation chain
+
+    General use is to add 'optional' check in first condition to avoid
+    ValueError by further checkers
+
+    >>> MAPPER = [(u'value', 'value', (optional, int))]
+    >>> row = {'value': u'XXX'}
+    >>> mk_entity(row, MAPPER)
+    {'value': None}
+    >>> row = {'value': u'100'}
+    >>> mk_entity(row, MAPPER)
+    {'value': 100}
+    """
+    if value:
+        return value
+    return None
+
+
+@deprecated('[3.21] deprecated')
+def required(value):
+    """raise ValueError if value is empty
+
+    This check should be often found in last position in the chain.
+    """
+    if value:
+        return value
+    raise ValueError("required")
+
+
+@deprecated('[3.21] deprecated')
+def todatetime(format='%d/%m/%Y'):
+    """return a transformation function to turn string input value into a
+    `datetime.datetime` instance, using given format.
+
+    Follow it by `todate` or `totime` functions from `logilab.common.date` if
+    you want a `date`/`time` instance instead of `datetime`.
+    """
+    def coerce(value):
+        return strptime(value, format)
+    return coerce
+
+
+@deprecated('[3.21] deprecated')
+def call_transform_method(methodname, *args, **kwargs):
+    """return value returned by calling the given method on input"""
+    def coerce(value):
+        return getattr(value, methodname)(*args, **kwargs)
+    return coerce
+
+
+@deprecated('[3.21] deprecated')
+def call_check_method(methodname, *args, **kwargs):
+    """check value returned by calling the given method on input is true,
+    else raise ValueError
+    """
+    def check(value):
+        if getattr(value, methodname)(*args, **kwargs):
+            return value
+        raise ValueError('%s not verified on %r' % (methodname, value))
+    return check
+
+
+# base integrity checking functions ############################################
+
+@deprecated('[3.21] deprecated')
+def check_doubles(buckets):
+    """Extract the keys that have more than one item in their bucket."""
+    return [(k, len(v)) for k, v in buckets.items() if len(v) > 1]
+
+
+@deprecated('[3.21] deprecated')
+def check_doubles_not_none(buckets):
+    """Extract the keys that have more than one item in their bucket."""
+    return [(k, len(v)) for k, v in buckets.items()
+            if k is not None and len(v) > 1]
+
+
+class ObjectStore(object):
+    """Store objects in memory for *faster* validation (development mode)
+
+    But it will not enforce the constraints of the schema and hence will miss some problems
+
+    >>> store = ObjectStore()
+    >>> user = store.prepare_insert_entity('CWUser', login=u'johndoe')
+    >>> group = store.prepare_insert_entity('CWUser', name=u'unknown')
+    >>> store.prepare_insert_relation(user, 'in_group', group)
+    """
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.21] use the new importer API'
+
+    def __init__(self):
+        self.items = []
+        self.eids = {}
+        self.types = {}
+        self.relations = set()
+        self.indexes = {}
+
+    def prepare_insert_entity(self, etype, **data):
+        """Given an entity type, attributes and inlined relations, return an eid for the entity that
+        would be inserted with a real store.
+        """
+        data = attrdict(data)
+        data['eid'] = eid = len(self.items)
+        self.items.append(data)
+        self.eids[eid] = data
+        self.types.setdefault(etype, []).append(eid)
+        return eid
+
+    def prepare_update_entity(self, etype, eid, **kwargs):
+        """Given an entity type and eid, updates the corresponding fake entity with specified
+        attributes and inlined relations.
+        """
+        assert eid in self.types[etype], 'Trying to update with wrong type {}'.format(etype)
+        data = self.eids[eid]
+        data.update(kwargs)
+
+    def prepare_insert_relation(self, eid_from, rtype, eid_to, **kwargs):
+        """Store into the `relations` attribute that a relation ``rtype`` exists between entities
+        with eids ``eid_from`` and ``eid_to``.
+        """
+        relation = eid_from, rtype, eid_to
+        self.relations.add(relation)
+        return relation
+
+    def flush(self):
+        """Nothing to flush for this store."""
+        pass
+
+    def commit(self):
+        """Nothing to commit for this store."""
+        return
+
+    def finish(self):
+        """Nothing to do once import is terminated for this store."""
+        pass
+
+    @property
+    def nb_inserted_entities(self):
+        return len(self.eids)
+
+    @property
+    def nb_inserted_types(self):
+        return len(self.types)
+
+    @property
+    def nb_inserted_relations(self):
+        return len(self.relations)
+
+    @deprecated('[3.21] use prepare_insert_entity instead')
+    def create_entity(self, etype, **data):
+        self.prepare_insert_entity(etype, **data)
+        return attrdict(data)
+
+    @deprecated('[3.21] use prepare_insert_relation instead')
+    def relate(self, eid_from, rtype, eid_to, **kwargs):
+        self.prepare_insert_relation(eid_from, rtype, eid_to, **kwargs)
+
+
+class CWImportController(object):
+    """Controller of the data import process.
+
+    >>> ctl = CWImportController(store)
+    >>> ctl.generators = list_of_data_generators
+    >>> ctl.data = dict_of_data_tables
+    >>> ctl.run()
+    """
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.21] use the new importer API'
+
+    def __init__(self, store, askerror=0, catcherrors=None, tell=tell,
+                 commitevery=50):
+        self.store = store
+        self.generators = None
+        self.data = {}
+        self.errors = None
+        self.askerror = askerror
+        if  catcherrors is None:
+            catcherrors = askerror
+        self.catcherrors = catcherrors
+        self.commitevery = commitevery # set to None to do a single commit
+        self._tell = tell
+
+    def check(self, type, key, value):
+        self._checks.setdefault(type, {}).setdefault(key, []).append(value)
+
+    def check_map(self, entity, key, map, default):
+        try:
+            entity[key] = map[entity[key]]
+        except KeyError:
+            self.check(key, entity[key], None)
+            entity[key] = default
+
+    def record_error(self, key, msg=None, type=None, value=None, tb=None):
+        tmp = StringIO()
+        if type is None:
+            traceback.print_exc(file=tmp)
+        else:
+            traceback.print_exception(type, value, tb, file=tmp)
+        # use a list to avoid counting a <nb lines> errors instead of one
+        errorlog = self.errors.setdefault(key, [])
+        if msg is None:
+            errorlog.append(tmp.getvalue().splitlines())
+        else:
+            errorlog.append( (msg, tmp.getvalue().splitlines()) )
+
+    def run(self):
+        self.errors = {}
+        if self.commitevery is None:
+            self.tell('Will commit all or nothing.')
+        else:
+            self.tell('Will commit every %s iterations' % self.commitevery)
+        for func, checks in self.generators:
+            self._checks = {}
+            func_name = func.__name__
+            self.tell("Run import function '%s'..." % func_name)
+            try:
+                func(self)
+            except Exception:
+                if self.catcherrors:
+                    self.record_error(func_name, 'While calling %s' % func.__name__)
+                else:
+                    self._print_stats()
+                    raise
+            for key, func, title, help in checks:
+                buckets = self._checks.get(key)
+                if buckets:
+                    err = func(buckets)
+                    if err:
+                        self.errors[title] = (help, err)
+        try:
+            txuuid = self.store.commit()
+            if txuuid is not None:
+                self.tell('Transaction commited (txuuid: %s)' % txuuid)
+        except QueryError as ex:
+            self.tell('Transaction aborted: %s' % ex)
+        self._print_stats()
+        if self.errors:
+            if self.askerror == 2 or (self.askerror and confirm('Display errors ?')):
+                from pprint import pformat
+                for errkey, error in self.errors.items():
+                    self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1])))
+                    self.tell(pformat(sorted(error[1])))
+
+    def _print_stats(self):
+        nberrors = sum(len(err) for err in self.errors.itervalues())
+        self.tell('\nImport statistics: %i entities, %i types, %i relations and %i errors'
+                  % (self.store.nb_inserted_entities,
+                     self.store.nb_inserted_types,
+                     self.store.nb_inserted_relations,
+                     nberrors))
+
+    def get_data(self, key):
+        return self.data.get(key)
+
+    def index(self, name, key, value, unique=False):
+        """create a new index
+
+        If unique is set to True, only first occurence will be kept not the following ones
+        """
+        if unique:
+            try:
+                if value in self.store.indexes[name][key]:
+                    return
+            except KeyError:
+                # we're sure that one is the first occurence; so continue...
+                pass
+        self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
+
+    def tell(self, msg):
+        self._tell(msg)
+
+    def iter_and_commit(self, datakey):
+        """iter rows, triggering commit every self.commitevery iterations"""
+        if self.commitevery is None:
+            return self.get_data(datakey)
+        else:
+            return callfunc_every(self.store.commit,
+                                  self.commitevery,
+                                  self.get_data(datakey))
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/importer.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,417 @@
+# copyright 2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact@logilab.fr
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+"""Data import of external entities.
+
+Main entry points:
+
+.. autoclass:: ExtEntitiesImporter
+.. autoclass:: ExtEntity
+
+Utilities:
+
+.. autofunction:: cwuri2eid
+.. autoclass:: RelationMapping
+.. autofunction:: cubicweb.dataimport.importer.use_extid_as_cwuri
+"""
+
+from collections import defaultdict
+import logging
+
+from logilab.mtconverter import xml_escape
+
+
+def cwuri2eid(cnx, etypes, source_eid=None):
+    """Return a dictionary mapping cwuri to eid for entities of the given entity types and / or
+    source.
+    """
+    assert source_eid or etypes, 'no entity types nor source specified'
+    rql = 'Any U, X WHERE X cwuri U'
+    args = {}
+    if len(etypes) == 1:
+        rql += ', X is %s' % etypes[0]
+    elif etypes:
+        rql += ', X is IN (%s)' % ','.join(etypes)
+    if source_eid is not None:
+        rql += ', X cw_source S, S eid %(s)s'
+        args['s'] = source_eid
+    return dict(cnx.execute(rql, args))
+
+
+def use_extid_as_cwuri(extid2eid):
+    """Return a generator of :class:`ExtEntity` objects that will set `cwuri`
+    using entity's extid if the entity does not exist yet and has no `cwuri`
+    defined.
+
+    `extid2eid` is an extid to eid dictionary coming from an
+    :class:`ExtEntitiesImporter` instance.
+
+    Example usage:
+
+    .. code-block:: python
+
+        importer = SKOSExtEntitiesImporter(cnx, store, import_log)
+        set_cwuri = use_extid_as_cwuri(importer.extid2eid)
+        importer.import_entities(set_cwuri(extentities))
+    """
+    def use_extid_as_cwuri_filter(extentities):
+        for extentity in extentities:
+            if extentity.extid not in extid2eid:
+                extentity.values.setdefault('cwuri', set([unicode(extentity.extid)]))
+            yield extentity
+    return use_extid_as_cwuri_filter
+
+
+class RelationMapping(object):
+    """Read-only mapping from relation type to set of related (subject, object) eids.
+
+    If `source` is specified, only returns relations implying entities from
+    this source.
+    """
+
+    def __init__(self, cnx, source=None):
+        self.cnx = cnx
+        self._rql_template = 'Any S,O WHERE S {} O'
+        self._kwargs = {}
+        if source is not None:
+            self._rql_template += ', S cw_source SO, O cw_source SO, SO eid %(s)s'
+            self._kwargs['s'] = source.eid
+
+    def __getitem__(self, rtype):
+        """Return a set of (subject, object) eids already related by `rtype`"""
+        rql = self._rql_template.format(rtype)
+        return set(tuple(x) for x in self.cnx.execute(rql, self._kwargs))
+
+
+class ExtEntity(object):
+    """Transitional representation of an entity for use in data importer.
+
+    An external entity has the following properties:
+
+    * ``extid`` (external id), an identifier for the ext entity,
+
+    * ``etype`` (entity type), a string which must be the name of one entity type in the schema
+      (eg. ``'Person'``, ``'Animal'``, ...),
+
+    * ``values``, a dictionary whose keys are attribute or relation names from the schema (eg.
+      ``'first_name'``, ``'friend'``), and whose values are *sets*
+
+    For instance:
+
+    .. code-block:: python
+
+        ext_entity.extid = 'http://example.org/person/debby'
+        ext_entity.etype = 'Person'
+        ext_entity.values = {'first_name': set([u"Deborah", u"Debby"]),
+                            'friend': set(['http://example.org/person/john'])}
+
+    """
+
+    def __init__(self, etype, extid, values=None):
+        self.etype = etype
+        self.extid = extid
+        if values is None:
+            values = {}
+        self.values = values
+        self._schema = None
+
+    def __repr__(self):
+        return '<%s %s %s>' % (self.etype, self.extid, self.values)
+
+    def iter_rdefs(self):
+        """Yield (key, rtype, role) defined in `.values` dict, with:
+
+        * `key` is the original key in `.values` (i.e. the relation type or a 2-uple (relation type,
+          role))
+
+        * `rtype` is a yams relation type, expected to be found in the schema (attribute or
+          relation)
+
+        * `role` is the role of the entity in the relation, 'subject' or 'object'
+
+        Iteration is done on a copy of the keys so values may be inserted/deleted during it.
+        """
+        for key in list(self.values):
+            if isinstance(key, tuple):
+                rtype, role = key
+                assert role in ('subject', 'object'), key
+                yield key, rtype, role
+            else:
+                yield key, key, 'subject'
+
+    def prepare(self, schema):
+        """Prepare an external entity for later insertion:
+
+        * ensure attributes and inlined relations have a single value
+        * turn set([value]) into value and remove key associated to empty set
+        * remove non inlined relations and return them as a [(e1key, relation, e2key)] list
+
+        Return a list of non inlined relations that may be inserted later, each relations defined by
+        a 3-tuple (subject extid, relation type, object extid).
+
+        Take care the importer may call this method several times.
+        """
+        assert self._schema is None, 'prepare() has already been called for %s' % self
+        self._schema = schema
+        eschema = schema.eschema(self.etype)
+        deferred = []
+        entity_dict = self.values
+        for key, rtype, role in self.iter_rdefs():
+            rschema = schema.rschema(rtype)
+            if rschema.final or (rschema.inlined and role == 'subject'):
+                assert len(entity_dict[key]) <= 1, \
+                    "more than one value for %s: %s (%s)" % (rtype, entity_dict[key], self.extid)
+                if entity_dict[key]:
+                    entity_dict[rtype] = entity_dict[key].pop()
+                    if key != rtype:
+                        del entity_dict[key]
+                    if (rschema.final and eschema.has_metadata(rtype, 'format')
+                            and not rtype + '_format' in entity_dict):
+                        entity_dict[rtype + '_format'] = u'text/plain'
+                else:
+                    del entity_dict[key]
+            else:
+                for target_extid in entity_dict.pop(key):
+                    if role == 'subject':
+                        deferred.append((self.extid, rtype, target_extid))
+                    else:
+                        deferred.append((target_extid, rtype, self.extid))
+        return deferred
+
+    def is_ready(self, extid2eid):
+        """Return True if the ext entity is ready, i.e. has all the URIs used in inlined relations
+        currently existing.
+        """
+        assert self._schema, 'prepare() method should be called first on %s' % self
+        # as .prepare has been called, we know that .values only contains subject relation *type* as
+        # key (no more (rtype, role) tuple)
+        schema = self._schema
+        entity_dict = self.values
+        for rtype in entity_dict:
+            rschema = schema.rschema(rtype)
+            if not rschema.final:
+                # .prepare() should drop other cases from the entity dict
+                assert rschema.inlined
+                if not entity_dict[rtype] in extid2eid:
+                    return False
+        # entity is ready, replace all relation's extid by eids
+        for rtype in entity_dict:
+            rschema = schema.rschema(rtype)
+            if rschema.inlined:
+                entity_dict[rtype] = extid2eid[entity_dict[rtype]]
+        return True
+
+
+class ExtEntitiesImporter(object):
+    """This class is responsible for importing externals entities, that is instances of
+    :class:`ExtEntity`, into CubicWeb entities.
+
+    :param schema: the CubicWeb's instance schema
+    :param store: a CubicWeb `Store`
+    :param extid2eid: optional {extid: eid} dictionary giving information on existing entities. It
+        will be completed during import. You may want to use :func:`cwuri2eid` to build it.
+    :param existing_relation: optional {rtype: set((subj eid, obj eid))} mapping giving information on
+        existing relations of a given type. You may want to use :class:`RelationMapping` to build it.
+    :param  etypes_order_hint: optional ordered iterable on entity types, giving an hint on the order in
+        which they should be attempted to be imported
+    :param  import_log: optional object implementing the :class:`SimpleImportLog` interface to record
+        events occuring during the import
+    :param  raise_on_error: optional boolean flag - default to false, indicating whether errors should
+        be raised or logged. You usually want them to be raised during test but to be logged in
+        production.
+
+    Instances of this class are meant to import external entities through :meth:`import_entities`
+    which handles a stream of :class:`ExtEntity`. One may then plug arbitrary filters into the
+    external entities stream.
+
+    .. automethod:: import_entities
+
+    """
+
+    def __init__(self, schema, store, extid2eid=None, existing_relations=None,
+                 etypes_order_hint=(), import_log=None, raise_on_error=False):
+        self.schema = schema
+        self.store = store
+        self.extid2eid = extid2eid if extid2eid is not None else {}
+        self.existing_relations = (existing_relations if existing_relations is not None
+                                   else defaultdict(set))
+        self.etypes_order_hint = etypes_order_hint
+        if import_log is None:
+            import_log = SimpleImportLog('<unspecified>')
+        self.import_log = import_log
+        self.raise_on_error = raise_on_error
+        # set of created/updated eids
+        self.created = set()
+        self.updated = set()
+
+    def import_entities(self, ext_entities):
+        """Import given external entities (:class:`ExtEntity`) stream (usually a generator)."""
+        # {etype: [etype dict]} of entities that are in the import queue
+        queue = {}
+        # order entity dictionaries then create/update them
+        deferred = self._import_entities(ext_entities, queue)
+        # create deferred relations that don't exist already
+        missing_relations = self.prepare_insert_deferred_relations(deferred)
+        self._warn_about_missing_work(queue, missing_relations)
+
+    def _import_entities(self, ext_entities, queue):
+        extid2eid = self.extid2eid
+        deferred = {}  # non inlined relations that may be deferred
+        self.import_log.record_debug('importing entities')
+        for ext_entity in self.iter_ext_entities(ext_entities, deferred, queue):
+            try:
+                eid = extid2eid[ext_entity.extid]
+            except KeyError:
+                self.prepare_insert_entity(ext_entity)
+            else:
+                if ext_entity.values:
+                    self.prepare_update_entity(ext_entity, eid)
+        return deferred
+
+    def iter_ext_entities(self, ext_entities, deferred, queue):
+        """Yield external entities in an order which attempts to satisfy
+        schema constraints (inlined / cardinality) and to optimize the import.
+        """
+        schema = self.schema
+        extid2eid = self.extid2eid
+        for ext_entity in ext_entities:
+            # check data in the transitional representation and prepare it for
+            # later insertion in the database
+            for subject_uri, rtype, object_uri in ext_entity.prepare(schema):
+                deferred.setdefault(rtype, set()).add((subject_uri, object_uri))
+            if not ext_entity.is_ready(extid2eid):
+                queue.setdefault(ext_entity.etype, []).append(ext_entity)
+                continue
+            yield ext_entity
+            # check for some entities in the queue that may now be ready. We'll have to restart
+            # search for ready entities until no one is generated
+            new = True
+            while new:
+                new = False
+                for etype in self.etypes_order_hint:
+                    if etype in queue:
+                        new_queue = []
+                        for ext_entity in queue[etype]:
+                            if ext_entity.is_ready(extid2eid):
+                                yield ext_entity
+                                # may unlock entity previously handled within this loop
+                                new = True
+                            else:
+                                new_queue.append(ext_entity)
+                        if new_queue:
+                            queue[etype][:] = new_queue
+                        else:
+                            del queue[etype]
+
+    def prepare_insert_entity(self, ext_entity):
+        """Call the store to prepare insertion of the given external entity"""
+        eid = self.store.prepare_insert_entity(ext_entity.etype, **ext_entity.values)
+        self.extid2eid[ext_entity.extid] = eid
+        self.created.add(eid)
+        return eid
+
+    def prepare_update_entity(self, ext_entity, eid):
+        """Call the store to prepare update of the given external entity"""
+        self.store.prepare_update_entity(ext_entity.etype, eid, **ext_entity.values)
+        self.updated.add(eid)
+
+    def prepare_insert_deferred_relations(self, deferred):
+        """Call the store to insert deferred relations (not handled during insertion/update for
+        entities). Return a list of relations `[(subj ext id, obj ext id)]` that may not be inserted
+        because the target entities don't exists yet.
+        """
+        prepare_insert_relation = self.store.prepare_insert_relation
+        rschema = self.schema.rschema
+        extid2eid = self.extid2eid
+        missing_relations = []
+        for rtype, relations in deferred.items():
+            self.import_log.record_debug('importing %s %s relations' % (len(relations), rtype))
+            symmetric = rschema(rtype).symmetric
+            existing = self.existing_relations[rtype]
+            for subject_uri, object_uri in relations:
+                try:
+                    subject_eid = extid2eid[subject_uri]
+                    object_eid = extid2eid[object_uri]
+                except KeyError:
+                    missing_relations.append((subject_uri, rtype, object_uri))
+                    continue
+                if (subject_eid, object_eid) not in existing:
+                    prepare_insert_relation(subject_eid, rtype, object_eid)
+                    existing.add((subject_eid, object_eid))
+                    if symmetric:
+                        existing.add((object_eid, subject_eid))
+        return missing_relations
+
+    def _warn_about_missing_work(self, queue, missing_relations):
+        error = self.import_log.record_error
+        if queue:
+            msgs = ["can't create some entities, is there some cycle or "
+                    "missing data?"]
+            for ext_entities in queue.values():
+                for ext_entity in ext_entities:
+                    msgs.append(str(ext_entity))
+            map(error, msgs)
+            if self.raise_on_error:
+                raise Exception('\n'.join(msgs))
+        if missing_relations:
+            msgs = ["can't create some relations, is there missing data?"]
+            for subject_uri, rtype, object_uri in missing_relations:
+                msgs.append("%s %s %s" % (subject_uri, rtype, object_uri))
+            map(error, msgs)
+            if self.raise_on_error:
+                raise Exception('\n'.join(msgs))
+
+
+class SimpleImportLog(object):
+    """Fake CWDataImport log using a simple text format.
+
+    Useful to display logs in the UI instead of storing them to the
+    database.
+    """
+
+    def __init__(self, filename):
+        self.logs = []
+        self.filename = filename
+
+    def record_debug(self, msg, path=None, line=None):
+        self._log(logging.DEBUG, msg, path, line)
+
+    def record_info(self, msg, path=None, line=None):
+        self._log(logging.INFO, msg, path, line)
+
+    def record_warning(self, msg, path=None, line=None):
+        self._log(logging.WARNING, msg, path, line)
+
+    def record_error(self, msg, path=None, line=None):
+        self._log(logging.ERROR, msg, path, line)
+
+    def record_fatal(self, msg, path=None, line=None):
+        self._log(logging.FATAL, msg, path, line)
+
+    def _log(self, severity, msg, path, line):
+        encodedmsg = u'%s\t%s\t%s\t%s' % (severity, self.filename,
+                                          line or u'', msg)
+        self.logs.append(encodedmsg)
+
+
+class HTMLImportLog(SimpleImportLog):
+    """Fake CWDataImport log using a simple HTML format."""
+    def __init__(self, filename):
+        super(HTMLImportLog, self).__init__(xml_escape(filename))
+
+    def _log(self, severity, msg, path, line):
+        encodedmsg = u'%s\t%s\t%s\t%s<br/>' % (severity, self.filename,
+                                               line or u'', xml_escape(msg))
+        self.logs.append(encodedmsg)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/pgstore.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,472 @@
+# copyright 2003-2015 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/>.
+"""Postgres specific store"""
+
+import threading
+import warnings
+import cPickle
+import os.path as osp
+from StringIO import StringIO
+from time import asctime
+from datetime import date, datetime, time
+from collections import defaultdict
+from base64 import b64encode
+
+from cubicweb.utils import make_uid
+from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.dataimport.stores import NoHookRQLObjectStore
+
+def _import_statements(sql_connect, statements, nb_threads=3,
+                       dump_output_dir=None,
+                       support_copy_from=True, encoding='utf-8'):
+    """
+    Import a bunch of sql statements, using different threads.
+    """
+    try:
+        chunksize = (len(statements) / nb_threads) + 1
+        threads = []
+        for i in xrange(nb_threads):
+            chunks = statements[i*chunksize:(i+1)*chunksize]
+            thread = threading.Thread(target=_execmany_thread,
+                                      args=(sql_connect, chunks,
+                                            dump_output_dir,
+                                            support_copy_from,
+                                            encoding))
+            thread.start()
+            threads.append(thread)
+        for t in threads:
+            t.join()
+    except Exception:
+        print 'Error in import statements'
+
+def _execmany_thread_not_copy_from(cu, statement, data, table=None,
+                                   columns=None, encoding='utf-8'):
+    """ Execute thread without copy from
+    """
+    cu.executemany(statement, data)
+
+def _execmany_thread_copy_from(cu, statement, data, table,
+                               columns, encoding='utf-8'):
+    """ Execute thread with copy from
+    """
+    buf = _create_copyfrom_buffer(data, columns, encoding=encoding)
+    if buf is None:
+        _execmany_thread_not_copy_from(cu, statement, data)
+    else:
+        if columns is None:
+            cu.copy_from(buf, table, null='NULL')
+        else:
+            cu.copy_from(buf, table, null='NULL', columns=columns)
+
+def _execmany_thread(sql_connect, statements, dump_output_dir=None,
+                     support_copy_from=True, encoding='utf-8'):
+    """
+    Execute sql statement. If 'INSERT INTO', try to use 'COPY FROM' command,
+    or fallback to execute_many.
+    """
+    if support_copy_from:
+        execmany_func = _execmany_thread_copy_from
+    else:
+        execmany_func = _execmany_thread_not_copy_from
+    cnx = sql_connect()
+    cu = cnx.cursor()
+    try:
+        for statement, data in statements:
+            table = None
+            columns = None
+            try:
+                if not statement.startswith('INSERT INTO'):
+                    cu.executemany(statement, data)
+                    continue
+                table = statement.split()[2]
+                if isinstance(data[0], (tuple, list)):
+                    columns = None
+                else:
+                    columns = list(data[0])
+                execmany_func(cu, statement, data, table, columns, encoding)
+            except Exception:
+                print 'unable to copy data into table %s' % table
+                # Error in import statement, save data in dump_output_dir
+                if dump_output_dir is not None:
+                    pdata = {'data': data, 'statement': statement,
+                             'time': asctime(), 'columns': columns}
+                    filename = make_uid()
+                    try:
+                        with open(osp.join(dump_output_dir,
+                                           '%s.pickle' % filename), 'w') as fobj:
+                            fobj.write(cPickle.dumps(pdata))
+                    except IOError:
+                        print 'ERROR while pickling in', dump_output_dir, filename+'.pickle'
+                        pass
+                cnx.rollback()
+                raise
+    finally:
+        cnx.commit()
+        cu.close()
+
+
+def _copyfrom_buffer_convert_None(value, **opts):
+    '''Convert None value to "NULL"'''
+    return 'NULL'
+
+def _copyfrom_buffer_convert_number(value, **opts):
+    '''Convert a number into its string representation'''
+    return str(value)
+
+def _copyfrom_buffer_convert_string(value, **opts):
+    '''Convert string value.
+
+    Recognized keywords:
+    :encoding: resulting string encoding (default: utf-8)
+    '''
+    encoding = opts.get('encoding','utf-8')
+    escape_chars = ((u'\\', ur'\\'), (u'\t', u'\\t'), (u'\r', u'\\r'),
+                    (u'\n', u'\\n'))
+    for char, replace in escape_chars:
+        value = value.replace(char, replace)
+    if isinstance(value, unicode):
+        value = value.encode(encoding)
+    return value
+
+def _copyfrom_buffer_convert_date(value, **opts):
+    '''Convert date into "YYYY-MM-DD"'''
+    # Do not use strftime, as it yields issue with date < 1900
+    # (http://bugs.python.org/issue1777412)
+    return '%04d-%02d-%02d' % (value.year, value.month, value.day)
+
+def _copyfrom_buffer_convert_datetime(value, **opts):
+    '''Convert date into "YYYY-MM-DD HH:MM:SS.UUUUUU"'''
+    # Do not use strftime, as it yields issue with date < 1900
+    # (http://bugs.python.org/issue1777412)
+    return '%s %s' % (_copyfrom_buffer_convert_date(value, **opts),
+                      _copyfrom_buffer_convert_time(value, **opts))
+
+def _copyfrom_buffer_convert_time(value, **opts):
+    '''Convert time into "HH:MM:SS.UUUUUU"'''
+    return '%02d:%02d:%02d.%06d' % (value.hour, value.minute,
+                                    value.second, value.microsecond)
+
+# (types, converter) list.
+_COPYFROM_BUFFER_CONVERTERS = [
+    (type(None), _copyfrom_buffer_convert_None),
+    ((long, int, float), _copyfrom_buffer_convert_number),
+    (basestring, _copyfrom_buffer_convert_string),
+    (datetime, _copyfrom_buffer_convert_datetime),
+    (date, _copyfrom_buffer_convert_date),
+    (time, _copyfrom_buffer_convert_time),
+]
+
+def _create_copyfrom_buffer(data, columns=None, **convert_opts):
+    """
+    Create a StringIO buffer for 'COPY FROM' command.
+    Deals with Unicode, Int, Float, Date... (see ``converters``)
+
+    :data: a sequence/dict of tuples
+    :columns: list of columns to consider (default to all columns)
+    :converter_opts: keyword arguements given to converters
+    """
+    # Create a list rather than directly create a StringIO
+    # to correctly write lines separated by '\n' in a single step
+    rows = []
+    if columns is None:
+        if isinstance(data[0], (tuple, list)):
+            columns = range(len(data[0]))
+        elif isinstance(data[0], dict):
+            columns = data[0].keys()
+        else:
+            raise ValueError('Could not get columns: you must provide columns.')
+    for row in data:
+        # Iterate over the different columns and the different values
+        # and try to convert them to a correct datatype.
+        # If an error is raised, do not continue.
+        formatted_row = []
+        for col in columns:
+            try:
+                value = row[col]
+            except KeyError:
+                warnings.warn(u"Column %s is not accessible in row %s"
+                              % (col, row), RuntimeWarning)
+                # XXX 'value' set to None so that the import does not end in
+                # error.
+                # Instead, the extra keys are set to NULL from the
+                # database point of view.
+                value = None
+            for types, converter in _COPYFROM_BUFFER_CONVERTERS:
+                if isinstance(value, types):
+                    value = converter(value, **convert_opts)
+                    break
+            else:
+                raise ValueError("Unsupported value type %s" % type(value))
+            # We push the value to the new formatted row
+            # if the value is not None and could be converted to a string.
+            formatted_row.append(value)
+        rows.append('\t'.join(formatted_row))
+    return StringIO('\n'.join(rows))
+
+
+class SQLGenObjectStore(NoHookRQLObjectStore):
+    """Controller of the data import process. This version is based
+    on direct insertions throught SQL command (COPY FROM or execute many).
+
+    >>> store = SQLGenObjectStore(cnx)
+    >>> store.create_entity('Person', ...)
+    >>> store.flush()
+    """
+
+    def __init__(self, cnx, dump_output_dir=None, nb_threads_statement=3):
+        """
+        Initialize a SQLGenObjectStore.
+
+        Parameters:
+
+          - cnx: connection on the cubicweb instance
+          - dump_output_dir: a directory to dump failed statements
+            for easier recovery. Default is None (no dump).
+          - nb_threads_statement: number of threads used
+            for SQL insertion (default is 3).
+        """
+        super(SQLGenObjectStore, self).__init__(cnx)
+        ### hijack default source
+        self.source = SQLGenSourceWrapper(
+            self.source, cnx.vreg.schema,
+            dump_output_dir=dump_output_dir,
+            nb_threads_statement=nb_threads_statement)
+        ### XXX This is done in super().__init__(), but should be
+        ### redone here to link to the correct source
+        self.add_relation = self.source.add_relation
+        self.indexes_etypes = {}
+
+    def flush(self):
+        """Flush data to the database"""
+        self.source.flush()
+
+    def relate(self, subj_eid, rtype, obj_eid, **kwargs):
+        if subj_eid is None or obj_eid is None:
+            return
+        # XXX Could subjtype be inferred ?
+        self.source.add_relation(self._cnx, subj_eid, rtype, obj_eid,
+                                 self.rschema(rtype).inlined, **kwargs)
+        if self.rschema(rtype).symmetric:
+            self.source.add_relation(self._cnx, obj_eid, rtype, subj_eid,
+                                     self.rschema(rtype).inlined, **kwargs)
+
+    def drop_indexes(self, etype):
+        """Drop indexes for a given entity type"""
+        if etype not in self.indexes_etypes:
+            cu = self._cnx.cnxset.cu
+            def index_to_attr(index):
+                """turn an index name to (database) attribute name"""
+                return index.replace(etype.lower(), '').replace('idx', '').strip('_')
+            indices = [(index, index_to_attr(index))
+                       for index in self.source.dbhelper.list_indices(cu, etype)
+                       # Do not consider 'cw_etype_pkey' index
+                       if not index.endswith('key')]
+            self.indexes_etypes[etype] = indices
+        for index, attr in self.indexes_etypes[etype]:
+            self._cnx.system_sql('DROP INDEX %s' % index)
+
+    def create_indexes(self, etype):
+        """Recreate indexes for a given entity type"""
+        for index, attr in self.indexes_etypes.get(etype, []):
+            sql = 'CREATE INDEX %s ON cw_%s(%s)' % (index, etype, attr)
+            self._cnx.system_sql(sql)
+
+
+###########################################################################
+## SQL Source #############################################################
+###########################################################################
+
+class SQLGenSourceWrapper(object):
+
+    def __init__(self, system_source, schema,
+                 dump_output_dir=None, nb_threads_statement=3):
+        self.system_source = system_source
+        self._sql = threading.local()
+        # Explicitely backport attributes from system source
+        self._storage_handler = self.system_source._storage_handler
+        self.preprocess_entity = self.system_source.preprocess_entity
+        self.sqlgen = self.system_source.sqlgen
+        self.uri = self.system_source.uri
+        self.eid = self.system_source.eid
+        # Directory to write temporary files
+        self.dump_output_dir = dump_output_dir
+        # Allow to execute code with SQLite backend that does
+        # not support (yet...) copy_from
+        # XXX Should be dealt with in logilab.database
+        spcfrom = system_source.dbhelper.dbapi_module.support_copy_from
+        self.support_copy_from = spcfrom
+        self.dbencoding = system_source.dbhelper.dbencoding
+        self.nb_threads_statement = nb_threads_statement
+        # initialize thread-local data for main thread
+        self.init_thread_locals()
+        self._inlined_rtypes_cache = {}
+        self._fill_inlined_rtypes_cache(schema)
+        self.schema = schema
+        self.do_fti = False
+
+    def _fill_inlined_rtypes_cache(self, schema):
+        cache = self._inlined_rtypes_cache
+        for eschema in schema.entities():
+            for rschema in eschema.ordered_relations():
+                if rschema.inlined:
+                    cache[eschema.type] = SQL_PREFIX + rschema.type
+
+    def init_thread_locals(self):
+        """initializes thread-local data"""
+        self._sql.entities = defaultdict(list)
+        self._sql.relations = {}
+        self._sql.inlined_relations = {}
+        # keep track, for each eid of the corresponding data dict
+        self._sql.eid_insertdicts = {}
+
+    def flush(self):
+        print 'starting flush'
+        _entities_sql = self._sql.entities
+        _relations_sql = self._sql.relations
+        _inlined_relations_sql = self._sql.inlined_relations
+        _insertdicts = self._sql.eid_insertdicts
+        try:
+            # try, for each inlined_relation, to find if we're also creating
+            # the host entity (i.e. the subject of the relation).
+            # In that case, simply update the insert dict and remove
+            # the need to make the
+            # UPDATE statement
+            for statement, datalist in _inlined_relations_sql.iteritems():
+                new_datalist = []
+                # for a given inlined relation,
+                # browse each couple to be inserted
+                for data in datalist:
+                    keys = list(data)
+                    # For inlined relations, it exists only two case:
+                    # (rtype, cw_eid) or (cw_eid, rtype)
+                    if keys[0] == 'cw_eid':
+                        rtype = keys[1]
+                    else:
+                        rtype = keys[0]
+                    updated_eid = data['cw_eid']
+                    if updated_eid in _insertdicts:
+                        _insertdicts[updated_eid][rtype] = data[rtype]
+                    else:
+                        # could not find corresponding insert dict, keep the
+                        # UPDATE query
+                        new_datalist.append(data)
+                _inlined_relations_sql[statement] = new_datalist
+            _import_statements(self.system_source.get_connection,
+                               _entities_sql.items()
+                               + _relations_sql.items()
+                               + _inlined_relations_sql.items(),
+                               dump_output_dir=self.dump_output_dir,
+                               nb_threads=self.nb_threads_statement,
+                               support_copy_from=self.support_copy_from,
+                               encoding=self.dbencoding)
+        finally:
+            _entities_sql.clear()
+            _relations_sql.clear()
+            _insertdicts.clear()
+            _inlined_relations_sql.clear()
+
+    def add_relation(self, cnx, subject, rtype, object,
+                     inlined=False, **kwargs):
+        if inlined:
+            _sql = self._sql.inlined_relations
+            data = {'cw_eid': subject, SQL_PREFIX + rtype: object}
+            subjtype = kwargs.get('subjtype')
+            if subjtype is None:
+                # Try to infer it
+                targets = [t.type for t in
+                           self.schema.rschema(rtype).subjects()]
+                if len(targets) == 1:
+                    subjtype = targets[0]
+                else:
+                    raise ValueError('You should give the subject etype for '
+                                     'inlined relation %s'
+                                     ', as it cannot be inferred: '
+                                     'this type is given as keyword argument '
+                                     '``subjtype``'% rtype)
+            statement = self.sqlgen.update(SQL_PREFIX + subjtype,
+                                           data, ['cw_eid'])
+        else:
+            _sql = self._sql.relations
+            data = {'eid_from': subject, 'eid_to': object}
+            statement = self.sqlgen.insert('%s_relation' % rtype, data)
+        if statement in _sql:
+            _sql[statement].append(data)
+        else:
+            _sql[statement] = [data]
+
+    def add_entity(self, cnx, entity):
+        with self._storage_handler(entity, 'added'):
+            attrs = self.preprocess_entity(entity)
+            rtypes = self._inlined_rtypes_cache.get(entity.cw_etype, ())
+            if isinstance(rtypes, str):
+                rtypes = (rtypes,)
+            for rtype in rtypes:
+                if rtype not in attrs:
+                    attrs[rtype] = None
+            sql = self.sqlgen.insert(SQL_PREFIX + entity.cw_etype, attrs)
+            self._sql.eid_insertdicts[entity.eid] = attrs
+            self._append_to_entities(sql, attrs)
+
+    def _append_to_entities(self, sql, attrs):
+        self._sql.entities[sql].append(attrs)
+
+    def _handle_insert_entity_sql(self, cnx, sql, attrs):
+        # We have to overwrite the source given in parameters
+        # as here, we directly use the system source
+        attrs['asource'] = self.system_source.uri
+        self._append_to_entities(sql, attrs)
+
+    def _handle_is_relation_sql(self, cnx, sql, attrs):
+        self._append_to_entities(sql, attrs)
+
+    def _handle_is_instance_of_sql(self, cnx, sql, attrs):
+        self._append_to_entities(sql, attrs)
+
+    def _handle_source_relation_sql(self, cnx, sql, attrs):
+        self._append_to_entities(sql, attrs)
+
+    # add_info is _copypasted_ from the one in NativeSQLSource. We want it
+    # there because it will use the _handlers of the SQLGenSourceWrapper, which
+    # are not like the ones in the native source.
+    def add_info(self, cnx, entity, source, extid):
+        """add type and source info for an eid into the system table"""
+        # begin by inserting eid/type/source/extid into the entities table
+        if extid is not None:
+            assert isinstance(extid, str)
+            extid = b64encode(extid)
+        attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': extid,
+                 'asource': source.uri}
+        self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
+        # insert core relations: is, is_instance_of and cw_source
+        try:
+            self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)',
+                                         (entity.eid, eschema_eid(cnx, entity.e_schema)))
+        except IndexError:
+            # during schema serialization, skip
+            pass
+        else:
+            for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
+                self._handle_is_relation_sql(cnx,
+                                             'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)',
+                                             (entity.eid, eschema_eid(cnx, eschema)))
+        if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10
+            self._handle_is_relation_sql(cnx, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)',
+                                         (entity.eid, source.eid))
+        # now we can update the full text index
+        if self.do_fti and self.need_fti_indexation(entity.cw_etype):
+            self.index_entity(cnx, entity=entity)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/stores.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,323 @@
+# copyright 2003-2015 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/>.
+"""
+Stores are responsible to insert properly formatted entities and relations into the database. They
+have the following API::
+
+    >>> user_eid = store.prepare_insert_entity('CWUser', login=u'johndoe')
+    >>> group_eid = store.prepare_insert_entity('CWUser', name=u'unknown')
+    >>> store.relate(user_eid, 'in_group', group_eid)
+    >>> store.flush()
+    >>> store.commit()
+    >>> store.finish()
+
+Some store **requires a flush** to copy data in the database, so if you want to have store
+independant code you should explicitly call it. (There may be multiple flushes during the
+process, or only one at the end if there is no memory issue). This is different from the
+commit which validates the database transaction. At last, the `finish()` method should be called in
+case the store requires additional work once everything is done.
+
+* ``prepare_insert_entity(<entity type>, **kwargs) -> eid``: given an entity
+  type, attributes and inlined relations, return the eid of the entity to be
+  inserted, *with no guarantee that anything has been inserted in database*,
+
+* ``prepare_update_entity(<entity type>, eid, **kwargs) -> None``: given an
+  entity type and eid, promise for update given attributes and inlined
+  relations *with no guarantee that anything has been inserted in database*,
+
+* ``prepare_insert_relation(eid_from, rtype, eid_to) -> None``: indicate that a
+  relation ``rtype`` should be added between entities with eids ``eid_from``
+  and ``eid_to``. Similar to ``prepare_insert_entity()``, *there is no
+  guarantee that the relation will be inserted in database*,
+
+* ``flush() -> None``: flush any temporary data to database. May be called
+  several times during an import,
+
+* ``commit() -> None``: commit the database transaction,
+
+* ``finish() -> None``: additional stuff to do after import is terminated.
+
+.. autoclass:: cubicweb.dataimport.stores.RQLObjectStore
+.. autoclass:: cubicweb.dataimport.stores.NoHookRQLObjectStore
+.. autoclass:: cubicweb.dataimport.stores.MetaGenerator
+"""
+import inspect
+import warnings
+from datetime import datetime
+from copy import copy
+
+from logilab.common.deprecation import deprecated
+from logilab.common.decorators import cached
+
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES
+from cubicweb.server.edition import EditedEntity
+
+
+class RQLObjectStore(object):
+    """Store that works by making RQL queries, hence with all the cubicweb's machinery activated.
+    """
+
+    def __init__(self, cnx, commit=None):
+        if commit is not None:
+            warnings.warn('[3.19] commit argument should not be specified '
+                          'as the cnx object already provides it.',
+                          DeprecationWarning, stacklevel=2)
+        self._cnx = cnx
+        self._commit = commit or cnx.commit
+        # XXX 3.21 deprecated attributes
+        self.eids = {}
+        self.types = {}
+
+    def rql(self, *args):
+        """Execute a RQL query. This is NOT part of the store API."""
+        return self._cnx.execute(*args)
+
+    def prepare_insert_entity(self, *args, **kwargs):
+        """Given an entity type, attributes and inlined relations, returns the inserted entity's
+        eid.
+        """
+        entity = self._cnx.create_entity(*args, **kwargs)
+        self.eids[entity.eid] = entity
+        self.types.setdefault(args[0], []).append(entity.eid)
+        return entity.eid
+
+    def prepare_update_entity(self, etype, eid, **kwargs):
+        """Given an entity type and eid, updates the corresponding entity with specified attributes
+        and inlined relations.
+        """
+        entity = self._cnx.entity_from_eid(eid)
+        assert entity.cw_etype == etype, 'Trying to update with wrong type {}'.format(etype)
+        # XXX some inlined relations may already exists
+        entity.cw_set(**kwargs)
+
+    def prepare_insert_relation(self, eid_from, rtype, eid_to, **kwargs):
+        """Insert into the database a  relation ``rtype`` between entities with eids ``eid_from``
+        and ``eid_to``.
+        """
+        self.rql('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+                 {'x': int(eid_from), 'y': int(eid_to)})
+
+    def flush(self):
+        """Nothing to flush for this store."""
+        pass
+
+    def commit(self):
+        """Commit the database transaction."""
+        return self._commit()
+
+    @property
+    def session(self):
+        warnings.warn('[3.19] deprecated property.', DeprecationWarning, stacklevel=2)
+        return self._cnx.repo._get_session(self._cnx.sessionid)
+
+    @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
+    def find_entities(self, *args, **kwargs):
+        return self._cnx.find(*args, **kwargs).entities()
+
+    @deprecated("[3.19] use cnx.find(*args, **kwargs).one() instead")
+    def find_one_entity(self, *args, **kwargs):
+        return self._cnx.find(*args, **kwargs).one()
+
+    @deprecated('[3.21] use prepare_insert_entity instead')
+    def create_entity(self, *args, **kwargs):
+        eid = self.prepare_insert_entity(*args, **kwargs)
+        return self._cnx.entity_from_eid(eid)
+
+    @deprecated('[3.21] use prepare_insert_relation instead')
+    def relate(self, eid_from, rtype, eid_to, **kwargs):
+        self.prepare_insert_relation(eid_from, rtype, eid_to, **kwargs)
+
+
+class NoHookRQLObjectStore(RQLObjectStore):
+    """Store that works by accessing low-level CubicWeb's source API, with all hooks deactivated. It
+    must be given a metadata generator object to handle metadata which are usually handled by hooks
+    (see :class:`MetaGenerator`).
+    """
+
+    def __init__(self, cnx, metagen=None):
+        super(NoHookRQLObjectStore, self).__init__(cnx)
+        self.source = cnx.repo.system_source
+        self.rschema = cnx.repo.schema.rschema
+        self.add_relation = self.source.add_relation
+        if metagen is None:
+            metagen = MetaGenerator(cnx)
+        self.metagen = metagen
+        self._nb_inserted_entities = 0
+        self._nb_inserted_types = 0
+        self._nb_inserted_relations = 0
+        # deactivate security
+        cnx.read_security = False
+        cnx.write_security = False
+
+    def prepare_insert_entity(self, etype, **kwargs):
+        """Given an entity type, attributes and inlined relations, returns the inserted entity's
+        eid.
+        """
+        for k, v in kwargs.iteritems():
+            kwargs[k] = getattr(v, 'eid', v)
+        entity, rels = self.metagen.base_etype_dicts(etype)
+        # make a copy to keep cached entity pristine
+        entity = copy(entity)
+        entity.cw_edited = copy(entity.cw_edited)
+        entity.cw_clear_relation_cache()
+        entity.cw_edited.update(kwargs, skipsec=False)
+        entity_source, extid = self.metagen.init_entity(entity)
+        cnx = self._cnx
+        self.source.add_info(cnx, entity, entity_source, extid)
+        self.source.add_entity(cnx, entity)
+        kwargs = dict()
+        if inspect.getargspec(self.add_relation).keywords:
+            kwargs['subjtype'] = entity.cw_etype
+        for rtype, targeteids in rels.iteritems():
+            # targeteids may be a single eid or a list of eids
+            inlined = self.rschema(rtype).inlined
+            try:
+                for targeteid in targeteids:
+                    self.add_relation(cnx, entity.eid, rtype, targeteid,
+                                      inlined, **kwargs)
+            except TypeError:
+                self.add_relation(cnx, entity.eid, rtype, targeteids,
+                                  inlined, **kwargs)
+        self._nb_inserted_entities += 1
+        return entity.eid
+
+    # XXX: prepare_update_entity is inherited from RQLObjectStore, it should be reimplemented to
+    # actually skip hooks as prepare_insert_entity
+
+    def prepare_insert_relation(self, eid_from, rtype, eid_to, **kwargs):
+        """Insert into the database a  relation ``rtype`` between entities with eids ``eid_from``
+        and ``eid_to``.
+        """
+        assert not rtype.startswith('reverse_')
+        self.add_relation(self._cnx, eid_from, rtype, eid_to,
+                          self.rschema(rtype).inlined)
+        if self.rschema(rtype).symmetric:
+            self.add_relation(self._cnx, eid_to, rtype, eid_from,
+                              self.rschema(rtype).inlined)
+        self._nb_inserted_relations += 1
+
+    @property
+    @deprecated('[3.21] deprecated')
+    def nb_inserted_entities(self):
+        return self._nb_inserted_entities
+
+    @property
+    @deprecated('[3.21] deprecated')
+    def nb_inserted_types(self):
+        return self._nb_inserted_types
+
+    @property
+    @deprecated('[3.21] deprecated')
+    def nb_inserted_relations(self):
+        return self._nb_inserted_relations
+
+
+class MetaGenerator(object):
+    """Class responsible for generating standard metadata for imported entities. You may want to
+    derive it to add application specific's metadata.
+
+    Parameters:
+    * `cnx`: connection to the repository
+    * `baseurl`: optional base URL to be used for `cwuri` generation - default to config['base-url']
+    * `source`: optional source to be used as `cw_source` for imported entities
+    """
+    META_RELATIONS = (META_RTYPES
+                      - VIRTUAL_RTYPES
+                      - set(('eid', 'cwuri',
+                             'is', 'is_instance_of', 'cw_source')))
+
+    def __init__(self, cnx, baseurl=None, source=None):
+        self._cnx = cnx
+        if baseurl is None:
+            config = cnx.vreg.config
+            baseurl = config['base-url'] or config.default_base_url()
+        if not baseurl[-1] == '/':
+            baseurl += '/'
+        self.baseurl = baseurl
+        if source is None:
+            source = cnx.repo.system_source
+        self.source = source
+        self.create_eid = cnx.repo.system_source.create_eid
+        self.time = datetime.now()
+        # attributes/relations shared by all entities of the same type
+        self.etype_attrs = []
+        self.etype_rels = []
+        # attributes/relations specific to each entity
+        self.entity_attrs = ['cwuri']
+        #self.entity_rels = [] XXX not handled (YAGNI?)
+        schema = cnx.vreg.schema
+        rschema = schema.rschema
+        for rtype in self.META_RELATIONS:
+            # skip owned_by / created_by if user is the internal manager
+            if cnx.user.eid == -1 and rtype in ('owned_by', 'created_by'):
+                continue
+            if rschema(rtype).final:
+                self.etype_attrs.append(rtype)
+            else:
+                self.etype_rels.append(rtype)
+
+    @cached
+    def base_etype_dicts(self, etype):
+        entity = self._cnx.vreg['etypes'].etype_class(etype)(self._cnx)
+        # entity are "surface" copied, avoid shared dict between copies
+        del entity.cw_extra_kwargs
+        entity.cw_edited = EditedEntity(entity)
+        for attr in self.etype_attrs:
+            genfunc = self.generate(attr)
+            if genfunc:
+                entity.cw_edited.edited_attribute(attr, genfunc(entity))
+        rels = {}
+        for rel in self.etype_rels:
+            genfunc = self.generate(rel)
+            if genfunc:
+                rels[rel] = genfunc(entity)
+        return entity, rels
+
+    def init_entity(self, entity):
+        entity.eid = self.create_eid(self._cnx)
+        extid = entity.cw_edited.get('cwuri')
+        for attr in self.entity_attrs:
+            if attr in entity.cw_edited:
+                # already set, skip this attribute
+                continue
+            genfunc = self.generate(attr)
+            if genfunc:
+                entity.cw_edited.edited_attribute(attr, genfunc(entity))
+        if isinstance(extid, unicode):
+            extid = extid.encode('utf-8')
+        return self.source, extid
+
+    def generate(self, rtype):
+        return getattr(self, 'gen_%s' % rtype, None)
+
+    def gen_cwuri(self, entity):
+        assert self.baseurl, 'baseurl is None while generating cwuri'
+        return u'%s%s' % (self.baseurl, entity.eid)
+
+    def gen_creation_date(self, entity):
+        return self.time
+
+    def gen_modification_date(self, entity):
+        return self.time
+
+    def gen_created_by(self, entity):
+        return self._cnx.user.eid
+
+    def gen_owned_by(self, entity):
+        return self._cnx.user.eid
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/data/people.csv	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,3 @@
+# uri,name,knows
+http://www.example.org/alice,Alice,
+http://www.example.org/bob,Bob,http://www.example.org/alice
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/data/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,29 @@
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+from yams.buildobjs import EntityType, String, SubjectRelation
+
+from cubicweb.schema import RQLConstraint
+
+
+class Personne(EntityType):
+    nom = String(required=True)
+    prenom = String()
+    enfant = SubjectRelation('Personne', inlined=True, cardinality='?*')
+    connait = SubjectRelation('Personne', symmetric=True,
+                              constraints=[RQLConstraint('NOT S identity O')])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/test_csv.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,72 @@
+# copyright 2003-2015 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.dataimport.csv"""
+
+from StringIO import StringIO
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.dataimport import csv
+
+
+class UcsvreaderTC(TestCase):
+
+    def test_empty_lines_skipped(self):
+        stream = StringIO('''a,b,c,d,
+1,2,3,4,
+,,,,
+,,,,
+''')
+        self.assertEqual([[u'a', u'b', u'c', u'd', u''],
+                          [u'1', u'2', u'3', u'4', u''],
+                          ],
+                         list(csv.ucsvreader(stream)))
+        stream.seek(0)
+        self.assertEqual([[u'a', u'b', u'c', u'd', u''],
+                          [u'1', u'2', u'3', u'4', u''],
+                          [u'', u'', u'', u'', u''],
+                          [u'', u'', u'', u'', u'']
+                          ],
+                         list(csv.ucsvreader(stream, skip_empty=False)))
+
+    def test_skip_first(self):
+        stream = StringIO('a,b,c,d,\n1,2,3,4,\n')
+        reader = csv.ucsvreader(stream, skipfirst=True, ignore_errors=True)
+        self.assertEqual(list(reader),
+                         [[u'1', u'2', u'3', u'4', u'']])
+
+        stream.seek(0)
+        reader = csv.ucsvreader(stream, skipfirst=True, ignore_errors=False)
+        self.assertEqual(list(reader),
+                         [[u'1', u'2', u'3', u'4', u'']])
+
+        stream.seek(0)
+        reader = csv.ucsvreader(stream, skipfirst=False, ignore_errors=True)
+        self.assertEqual(list(reader),
+                         [[u'a', u'b', u'c', u'd', u''],
+                          [u'1', u'2', u'3', u'4', u'']])
+
+        stream.seek(0)
+        reader = csv.ucsvreader(stream, skipfirst=False, ignore_errors=False)
+        self.assertEqual(list(reader),
+                         [[u'a', u'b', u'c', u'd', u''],
+                          [u'1', u'2', u'3', u'4', u'']])
+
+
+if __name__ == '__main__':
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/test_pgstore.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,92 @@
+# coding: utf-8
+# copyright 2003-2015 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.dataimport.pgstore"""
+
+import datetime as DT
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.dataimport import pgstore
+
+
+class CreateCopyFromBufferTC(TestCase):
+
+    # test converters
+
+    def test_convert_none(self):
+        cnvt = pgstore._copyfrom_buffer_convert_None
+        self.assertEqual('NULL', cnvt(None))
+
+    def test_convert_number(self):
+        cnvt = pgstore._copyfrom_buffer_convert_number
+        self.assertEqual('42', cnvt(42))
+        self.assertEqual('42', cnvt(42L))
+        self.assertEqual('42.42', cnvt(42.42))
+
+    def test_convert_string(self):
+        cnvt = pgstore._copyfrom_buffer_convert_string
+        # simple
+        self.assertEqual('babar', cnvt('babar'))
+        # unicode
+        self.assertEqual('\xc3\xa9l\xc3\xa9phant', cnvt(u'éléphant'))
+        self.assertEqual('\xe9l\xe9phant', cnvt(u'éléphant', encoding='latin1'))
+        # escaping
+        self.assertEqual('babar\\tceleste\\n', cnvt('babar\tceleste\n'))
+        self.assertEqual(r'C:\\new\tC:\\test', cnvt('C:\\new\tC:\\test'))
+
+    def test_convert_date(self):
+        cnvt = pgstore._copyfrom_buffer_convert_date
+        self.assertEqual('0666-01-13', cnvt(DT.date(666, 1, 13)))
+
+    def test_convert_time(self):
+        cnvt = pgstore._copyfrom_buffer_convert_time
+        self.assertEqual('06:06:06.000100', cnvt(DT.time(6, 6, 6, 100)))
+
+    def test_convert_datetime(self):
+        cnvt = pgstore._copyfrom_buffer_convert_datetime
+        self.assertEqual('0666-06-13 06:06:06.000000', cnvt(DT.datetime(666, 6, 13, 6, 6, 6)))
+
+    # test buffer
+    def test_create_copyfrom_buffer_tuple(self):
+        data = ((42, 42L, 42.42, u'éléphant', DT.date(666, 1, 13), DT.time(6, 6, 6),
+                 DT.datetime(666, 6, 13, 6, 6, 6)),
+                (6, 6L, 6.6, u'babar', DT.date(2014, 1, 14), DT.time(4, 2, 1),
+                 DT.datetime(2014, 1, 1, 0, 0, 0)))
+        results = pgstore._create_copyfrom_buffer(data)
+        # all columns
+        expected = '''42\t42\t42.42\téléphant\t0666-01-13\t06:06:06.000000\t0666-06-13 06:06:06.000000
+6\t6\t6.6\tbabar\t2014-01-14\t04:02:01.000000\t2014-01-01 00:00:00.000000'''
+        self.assertMultiLineEqual(expected, results.getvalue())
+        # selected columns
+        results = pgstore._create_copyfrom_buffer(data, columns=(1, 3, 6))
+        expected = '''42\téléphant\t0666-06-13 06:06:06.000000
+6\tbabar\t2014-01-01 00:00:00.000000'''
+        self.assertMultiLineEqual(expected, results.getvalue())
+
+    def test_create_copyfrom_buffer_dict(self):
+        data = (dict(integer=42, double=42.42, text=u'éléphant',
+                     date=DT.datetime(666, 6, 13, 6, 6, 6)),
+                dict(integer=6, double=6.6, text=u'babar',
+                     date=DT.datetime(2014, 1, 1, 0, 0, 0)))
+        results = pgstore._create_copyfrom_buffer(data, ('integer', 'text'))
+        expected = '''42\téléphant\n6\tbabar'''
+        self.assertMultiLineEqual(expected, results.getvalue())
+
+if __name__ == '__main__':
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/test_stores.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,88 @@
+# copyright 2003-2015 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.dataimport.stores"""
+
+import datetime as DT
+
+from cubicweb.dataimport import stores
+from cubicweb.devtools.testlib import CubicWebTC
+
+
+class RQLObjectStoreTC(CubicWebTC):
+
+    def test_all(self):
+        with self.admin_access.repo_cnx() as cnx:
+            store = stores.RQLObjectStore(cnx)
+            # Check data insertion
+            group_eid = store.prepare_insert_entity('CWGroup', name=u'grp')
+            user_eid = store.prepare_insert_entity('CWUser', login=u'lgn',
+                                                   upassword=u'pwd')
+            store.prepare_insert_relation(user_eid, 'in_group', group_eid)
+            cnx.commit()
+            users = cnx.execute('CWUser X WHERE X login "lgn"')
+            self.assertEqual(1, len(users))
+            self.assertEqual(user_eid, users.one().eid)
+            groups = cnx.execute('CWGroup X WHERE U in_group X, U login "lgn"')
+            self.assertEqual(1, len(users))
+            self.assertEqual(group_eid, groups.one().eid)
+            # Check data update
+            self.set_description('Check data update')
+            store.prepare_update_entity('CWGroup', group_eid, name=u'new_grp')
+            cnx.commit()
+            group = cnx.execute('CWGroup X WHERE X name "grp"')
+            self.assertEqual(len(group), 0)
+            group = cnx.execute('CWGroup X WHERE X name "new_grp"')
+            self.assertEqual, len(group), 1
+            # Check data update with wrong type
+            with self.assertRaises(AssertionError):
+                store.prepare_update_entity('CWUser', group_eid, name=u'new_user')
+            cnx.commit()
+            group = cnx.execute('CWGroup X WHERE X name "new_user"')
+            self.assertEqual(len(group), 0)
+            group = cnx.execute('CWGroup X WHERE X name "new_grp"')
+            self.assertEqual(len(group), 1)
+
+
+class MetaGeneratorTC(CubicWebTC):
+
+    def test_dont_generate_relation_to_internal_manager(self):
+        with self.admin_access.repo_cnx() as cnx:
+            metagen = stores.MetaGenerator(cnx)
+            self.assertIn('created_by', metagen.etype_rels)
+            self.assertIn('owned_by', metagen.etype_rels)
+        with self.repo.internal_cnx() as cnx:
+            metagen = stores.MetaGenerator(cnx)
+            self.assertNotIn('created_by', metagen.etype_rels)
+            self.assertNotIn('owned_by', metagen.etype_rels)
+
+    def test_dont_generate_specified_values(self):
+        with self.admin_access.repo_cnx() as cnx:
+            metagen = stores.MetaGenerator(cnx)
+            # hijack gen_modification_date to ensure we don't go through it
+            metagen.gen_modification_date = None
+            md = DT.datetime.now() - DT.timedelta(days=1)
+            entity, rels = metagen.base_etype_dicts('CWUser')
+            entity.cw_edited.update(dict(modification_date=md))
+            with cnx.ensure_cnx_set:
+                metagen.init_entity(entity)
+            self.assertEqual(entity.cw_edited['modification_date'], md)
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/unittest_importer.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# copyright 2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact@logilab.fr
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+"""Tests for cubicweb.dataimport.importer"""
+
+from collections import defaultdict
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb import ValidationError
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.dataimport import RQLObjectStore, ucsvreader
+from cubicweb.dataimport.importer import (ExtEntity, ExtEntitiesImporter, SimpleImportLog,
+                                          RelationMapping, use_extid_as_cwuri)
+
+
+class RelationMappingTC(CubicWebTC):
+
+    def test_nosource(self):
+        with self.admin_access.repo_cnx() as cnx:
+            alice_eid = cnx.create_entity('Personne', nom=u'alice').eid
+            bob_eid = cnx.create_entity('Personne', nom=u'bob', connait=alice_eid).eid
+            cnx.commit()
+            mapping = RelationMapping(cnx)
+            self.assertEqual(mapping['connait'],
+                             set([(bob_eid, alice_eid), (alice_eid, bob_eid)]))
+
+    def test_with_source(self):
+        with self.admin_access.repo_cnx() as cnx:
+            alice_eid = cnx.create_entity('Personne', nom=u'alice').eid
+            bob_eid = cnx.create_entity('Personne', nom=u'bob', connait=alice_eid).eid
+            cnx.commit()
+            mapping = RelationMapping(cnx, cnx.find('CWSource', name=u'system').one())
+            self.assertEqual(mapping['connait'],
+                             set([(bob_eid, alice_eid), (alice_eid, bob_eid)]))
+
+
+class ExtEntitiesImporterTC(CubicWebTC):
+
+    def importer(self, cnx):
+        store = RQLObjectStore(cnx)
+        return ExtEntitiesImporter(self.schema, store, raise_on_error=True)
+
+    def test_simple_import(self):
+        with self.admin_access.repo_cnx() as cnx:
+            importer = self.importer(cnx)
+            personne = ExtEntity('Personne', 1, {'nom': set([u'de la lune']),
+                                                 'prenom': set([u'Jean'])})
+            importer.import_entities([personne])
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X is Personne')
+            entity = rset.get_entity(0, 0)
+            self.assertEqual(entity.nom, u'de la lune')
+            self.assertEqual(entity.prenom, u'Jean')
+
+    def test_import_missing_required_attribute(self):
+        """Check import of ext entity with missing required attribute"""
+        with self.admin_access.repo_cnx() as cnx:
+            importer = self.importer(cnx)
+            tag = ExtEntity('Personne', 2, {'prenom': set([u'Jean'])})
+            self.assertRaises(ValidationError, importer.import_entities, [tag])
+
+    def test_import_inlined_relation(self):
+        """Check import of ext entities with inlined relation"""
+        with self.admin_access.repo_cnx() as cnx:
+            importer = self.importer(cnx)
+            richelieu = ExtEntity('Personne', 3, {'nom': set([u'Richelieu']),
+                                                  'enfant': set([4])})
+            athos = ExtEntity('Personne', 4, {'nom': set([u'Athos'])})
+            importer.import_entities([athos, richelieu])
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+            entity = rset.get_entity(0, 0)
+            self.assertEqual(entity.enfant[0].nom, 'Athos')
+
+    def test_import_non_inlined_relation(self):
+        """Check import of ext entities with non inlined relation"""
+        with self.admin_access.repo_cnx() as cnx:
+            importer = self.importer(cnx)
+            richelieu = ExtEntity('Personne', 5, {'nom': set([u'Richelieu']),
+                                                  'connait': set([6])})
+            athos = ExtEntity('Personne', 6, {'nom': set([u'Athos'])})
+            importer.import_entities([athos, richelieu])
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+            entity = rset.get_entity(0, 0)
+            self.assertEqual(entity.connait[0].nom, 'Athos')
+            rset = cnx.execute('Any X WHERE X is Personne, X nom "Athos"')
+            entity = rset.get_entity(0, 0)
+            self.assertEqual(entity.connait[0].nom, 'Richelieu')
+
+    def test_import_missing_inlined_relation(self):
+        """Check import of ext entity with missing inlined relation"""
+        with self.admin_access.repo_cnx() as cnx:
+            importer = self.importer(cnx)
+            richelieu = ExtEntity('Personne', 7,
+                                  {'nom': set([u'Richelieu']), 'enfant': set([8])})
+            self.assertRaises(Exception, importer.import_entities, [richelieu])
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+            self.assertEqual(len(rset), 0)
+
+    def test_import_missing_non_inlined_relation(self):
+        """Check import of ext entity with missing non-inlined relation"""
+        with self.admin_access.repo_cnx() as cnx:
+            importer = self.importer(cnx)
+            richelieu = ExtEntity('Personne', 9,
+                                  {'nom': set([u'Richelieu']), 'connait': set([10])})
+            self.assertRaises(Exception, importer.import_entities, [richelieu])
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+            entity = rset.get_entity(0, 0)
+            self.assertEqual(entity.nom, u'Richelieu')
+            self.assertEqual(len(entity.connait), 0)
+
+    def test_update(self):
+        """Check update of ext entity"""
+        with self.admin_access.repo_cnx() as cnx:
+            importer = self.importer(cnx)
+            # First import
+            richelieu = ExtEntity('Personne', 11,
+                                  {'nom': {u'Richelieu Diacre'}})
+            importer.import_entities([richelieu])
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X is Personne')
+            entity = rset.get_entity(0, 0)
+            self.assertEqual(entity.nom, u'Richelieu Diacre')
+            # Second import
+            richelieu = ExtEntity('Personne', 11,
+                                  {'nom': {u'Richelieu Cardinal'}})
+            importer.import_entities([richelieu])
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X is Personne')
+            self.assertEqual(len(rset), 1)
+            entity = rset.get_entity(0, 0)
+            self.assertEqual(entity.nom, u'Richelieu Cardinal')
+
+
+class UseExtidAsCwuriTC(TestCase):
+
+    def test(self):
+        personne = ExtEntity('Personne', 1, {'nom': set([u'de la lune']),
+                                             'prenom': set([u'Jean'])})
+        mapping = {}
+        set_cwuri = use_extid_as_cwuri(mapping)
+        list(set_cwuri((personne,)))
+        self.assertIn('cwuri', personne.values)
+        self.assertEqual(personne.values['cwuri'], set(['1']))
+        mapping[1] = 'whatever'
+        personne.values.pop('cwuri')
+        list(set_cwuri((personne,)))
+        self.assertNotIn('cwuri', personne.values)
+
+
+def extentities_from_csv(fpath):
+    """Yield ExtEntity read from `fpath` CSV file."""
+    with open(fpath) as f:
+        for uri, name, knows in ucsvreader(f, skipfirst=True, skip_empty=False):
+            yield ExtEntity('Personne', uri,
+                            {'nom': set([name]), 'connait': set([knows])})
+
+
+class DataimportFunctionalTC(CubicWebTC):
+
+    def test_csv(self):
+        extenties = extentities_from_csv(self.datapath('people.csv'))
+        with self.admin_access.repo_cnx() as cnx:
+            store = RQLObjectStore(cnx)
+            importer = ExtEntitiesImporter(self.schema, store)
+            importer.import_entities(extenties)
+            cnx.commit()
+            rset = cnx.execute('String N WHERE X nom N, X connait Y, Y nom "Alice"')
+            self.assertEqual(rset[0][0], u'Bob')
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/dbapi.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,836 +0,0 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# 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/>.
-"""DB-API 2.0 compliant module
-
-Take a look at http://www.python.org/peps/pep-0249.html
-
-(most parts of this document are reported here in docstrings)
-"""
-
-__docformat__ = "restructuredtext en"
-
-from threading import currentThread
-from logging import getLogger
-from time import time, clock
-from itertools import count
-from warnings import warn
-from os.path import join
-from uuid import uuid4
-from urlparse import  urlparse
-
-from logilab.common.logging_ext import set_log_methods
-from logilab.common.decorators import monkeypatch, cachedproperty
-from logilab.common.deprecation import deprecated
-
-from cubicweb import (ETYPE_NAME_MAP, AuthenticationError, ProgrammingError,
-                      cwvreg, cwconfig)
-from cubicweb.repoapi import get_repository
-from cubicweb.req import RequestSessionBase
-
-
-_MARKER = object()
-
-def _fake_property_value(self, name):
-    try:
-        return super(DBAPIRequest, self).property_value(name)
-    except KeyError:
-        return ''
-
-def fake(*args, **kwargs):
-    return None
-
-def multiple_connections_fix():
-    """some monkey patching necessary when an application has to deal with
-    several connections to different repositories. It tries to hide buggy class
-    attributes since classes are not designed to be shared among multiple
-    registries.
-    """
-    defaultcls = cwvreg.CWRegistryStore.REGISTRY_FACTORY[None]
-
-    etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
-    orig_etype_class = etypescls.orig_etype_class = etypescls.etype_class
-    @monkeypatch(defaultcls)
-    def etype_class(self, etype):
-        """return an entity class for the given entity type.
-        Try to find out a specific class for this kind of entity or
-        default to a dump of the class registered for 'Any'
-        """
-        usercls = orig_etype_class(self, etype)
-        if etype == 'Any':
-            return usercls
-        usercls.e_schema = self.schema.eschema(etype)
-        return usercls
-
-def multiple_connections_unfix():
-    etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
-    etypescls.etype_class = etypescls.orig_etype_class
-
-
-class ConnectionProperties(object):
-    def __init__(self, cnxtype=None, close=True, log=False):
-        if cnxtype is not None:
-            warn('[3.16] cnxtype argument is deprecated', DeprecationWarning,
-                 stacklevel=2)
-        self.cnxtype = cnxtype
-        self.log_queries = log
-        self.close_on_del = close
-
-
-@deprecated('[3.19] the dbapi is deprecated. Have a look at the new repoapi.')
-def _repo_connect(repo, login, **kwargs):
-    """Constructor to create a new connection to the given CubicWeb repository.
-
-    Returns a Connection instance.
-
-    Raises AuthenticationError if authentication failed
-    """
-    cnxid = repo.connect(unicode(login), **kwargs)
-    cnx = Connection(repo, cnxid, kwargs.get('cnxprops'))
-    if cnx.is_repo_in_memory:
-        cnx.vreg = repo.vreg
-    return cnx
-
-def connect(database, login=None,
-            cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
-    """Constructor for creating a connection to the CubicWeb repository.
-    Returns a :class:`Connection` object.
-
-    Typical usage::
-
-      cnx = connect('myinstance', login='me', password='toto')
-
-    `database` may be:
-
-    * a simple instance id for in-memory connection
-
-    * a uri like scheme://host:port/instanceid where scheme may be one of
-      'pyro', 'inmemory' or 'zmqpickle'
-
-      * if scheme is 'pyro', <host:port> determine the name server address. If
-        not specified (e.g. 'pyro:///instanceid'), it will be detected through a
-        broadcast query. The instance id is the name of the instance in the name
-        server and may be prefixed by a group (e.g.
-        'pyro:///:cubicweb.instanceid')
-
-      * if scheme is handled by ZMQ (eg 'tcp'), you should not specify an
-        instance id
-
-    Other arguments:
-
-    :login:
-      the user login to use to authenticate.
-
-    :cnxprops:
-      a :class:`ConnectionProperties` instance, allowing to specify
-      the connection method (eg in memory or pyro). A Pyro connection will be
-      established if you don't specify that argument.
-
-    :setvreg:
-      flag telling if a registry should be initialized for the connection.
-      Don't change this unless you know what you're doing.
-
-    :mulcnx:
-      Will disappear at some point. Try to deal with connections to differents
-      instances in the same process unless specified otherwise by setting this
-      flag to False. Don't change this unless you know what you're doing.
-
-    :initlog:
-      flag telling if logging should be initialized. You usually don't want
-      logging initialization when establishing the connection from a process
-      where it's already initialized.
-
-    :kwargs:
-      there goes authentication tokens. You usually have to specify a password
-      for the given user, using a named 'password' argument.
-    """
-    if not urlparse(database).scheme:
-        warn('[3.16] give an qualified URI as database instead of using '
-             'host/cnxprops to specify the connection method',
-             DeprecationWarning, stacklevel=2)
-        if cnxprops and cnxprops.cnxtype == 'zmq':
-            database = kwargs.pop('host')
-        elif cnxprops and cnxprops.cnxtype == 'inmemory':
-            database = 'inmemory://' + database
-        else:
-            host = kwargs.pop('host', None)
-            if host is None:
-                host = ''
-            group = kwargs.pop('group', None)
-            if group is None:
-                group = 'cubicweb'
-            database = 'pyro://%s/%s.%s' % (host, group, database)
-    puri = urlparse(database)
-    method = puri.scheme.lower()
-    if method == 'inmemory':
-        config = cwconfig.instance_configuration(puri.netloc)
-    else:
-        config = cwconfig.CubicWebNoAppConfiguration()
-    repo = get_repository(database, config=config)
-    if method == 'inmemory':
-        vreg = repo.vreg
-    elif setvreg:
-        if mulcnx:
-            multiple_connections_fix()
-        vreg = cwvreg.CWRegistryStore(config, initlog=initlog)
-        schema = repo.get_schema()
-        for oldetype, newetype in ETYPE_NAME_MAP.items():
-            if oldetype in schema:
-                print 'aliasing', newetype, 'to', oldetype
-                schema._entities[newetype] = schema._entities[oldetype]
-        vreg.set_schema(schema)
-    else:
-        vreg = None
-    cnx = _repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
-    cnx.vreg = vreg
-    return cnx
-
-def in_memory_repo(config):
-    """Return and in_memory Repository object from a config (or vreg)"""
-    if isinstance(config, cwvreg.CWRegistryStore):
-        vreg = config
-        config = None
-    else:
-        vreg = None
-    # get local access to the repository
-    return get_repository('inmemory://', config=config, vreg=vreg)
-
-def in_memory_repo_cnx(config, login, **kwargs):
-    """useful method for testing and scripting to get a dbapi.Connection
-    object connected to an in-memory repository instance
-    """
-    # connection to the CubicWeb repository
-    repo = in_memory_repo(config)
-    return repo, _repo_connect(repo, login, **kwargs)
-
-# XXX web only method, move to webconfig?
-def anonymous_session(vreg):
-    """return a new anonymous session
-
-    raises an AuthenticationError if anonymous usage is not allowed
-    """
-    anoninfo = vreg.config.anonymous_user()
-    if anoninfo[0] is None: # no anonymous user
-        raise AuthenticationError('anonymous access is not authorized')
-    anon_login, anon_password = anoninfo
-    # use vreg's repository cache
-    repo = vreg.config.repository(vreg)
-    anon_cnx = _repo_connect(repo, anon_login, password=anon_password)
-    anon_cnx.vreg = vreg
-    return DBAPISession(anon_cnx, anon_login)
-
-
-class _NeedAuthAccessMock(object):
-    def __getattribute__(self, attr):
-        raise AuthenticationError()
-    def __nonzero__(self):
-        return False
-
-class DBAPISession(object):
-    def __init__(self, cnx, login=None):
-        self.cnx = cnx
-        self.data = {}
-        self.login = login
-        # dbapi session identifier is the same as the first connection
-        # identifier, but may later differ in case of auto-reconnection as done
-        # by the web authentication manager (in cw.web.views.authentication)
-        if cnx is not None:
-            self.sessionid = cnx.sessionid
-        else:
-            self.sessionid = uuid4().hex
-
-    @property
-    def anonymous_session(self):
-        return not self.cnx or self.cnx.anonymous_connection
-
-    def __repr__(self):
-        return '<DBAPISession %r>' % self.sessionid
-
-
-class DBAPIRequest(RequestSessionBase):
-    #: Request language identifier eg: 'en'
-    lang = None
-
-    def __init__(self, vreg, session=None):
-        super(DBAPIRequest, self).__init__(vreg)
-        #: 'language' => translation_function() mapping
-        try:
-            # no vreg or config which doesn't handle translations
-            self.translations = vreg.config.translations
-        except AttributeError:
-            self.translations = {}
-        #: cache entities built during the request
-        self._eid_cache = {}
-        if session is not None:
-            self.set_session(session)
-        else:
-            # these args are initialized after a connection is
-            # established
-            self.session = DBAPISession(None)
-            self.cnx = self.user = _NeedAuthAccessMock()
-        self.set_default_language(vreg)
-
-    def get_option_value(self, option, foreid=None):
-        if foreid is not None:
-            warn('[3.19] foreid argument is deprecated', DeprecationWarning,
-                 stacklevel=2)
-        return self.cnx.get_option_value(option)
-
-    def set_session(self, session):
-        """method called by the session handler when the user is authenticated
-        or an anonymous connection is open
-        """
-        self.session = session
-        if session.cnx:
-            self.cnx = session.cnx
-            self.execute = session.cnx.cursor(self).execute
-            self.user = self.cnx.user(self)
-            self.set_entity_cache(self.user)
-
-    def execute(self, *args, **kwargs): # pylint: disable=E0202
-        """overriden when session is set. By default raise authentication error
-        so authentication is requested.
-        """
-        raise AuthenticationError()
-
-    def set_default_language(self, vreg):
-        try:
-            lang = vreg.property_value('ui.language')
-        except Exception: # property may not be registered
-            lang = 'en'
-        try:
-            self.set_language(lang)
-        except KeyError:
-            # this occurs usually during test execution
-            self._ = self.__ = unicode
-            self.pgettext = lambda x, y: unicode(y)
-
-    # server-side service call #################################################
-
-    def call_service(self, regid, **kwargs):
-        return self.cnx.call_service(regid, **kwargs)
-
-    # entities cache management ###############################################
-
-    def entity_cache(self, eid):
-        return self._eid_cache[eid]
-
-    def set_entity_cache(self, entity):
-        self._eid_cache[entity.eid] = entity
-
-    def cached_entities(self):
-        return self._eid_cache.values()
-
-    def drop_entity_cache(self, eid=None):
-        if eid is None:
-            self._eid_cache = {}
-        else:
-            del self._eid_cache[eid]
-
-    # low level session data management #######################################
-
-    @deprecated('[3.19] use session or transaction data')
-    def get_shared_data(self, key, default=None, pop=False, txdata=False):
-        """see :meth:`Connection.get_shared_data`"""
-        return self.cnx.get_shared_data(key, default, pop, txdata)
-
-    @deprecated('[3.19] use session or transaction data')
-    def set_shared_data(self, key, value, txdata=False, querydata=None):
-        """see :meth:`Connection.set_shared_data`"""
-        if querydata is not None:
-            txdata = querydata
-            warn('[3.10] querydata argument has been renamed to txdata',
-                 DeprecationWarning, stacklevel=2)
-        return self.cnx.set_shared_data(key, value, txdata)
-
-    # server session compat layer #############################################
-
-    def entity_metas(self, eid):
-        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
-        return self.cnx.entity_metas(eid)
-
-    def source_defs(self):
-        """return the definition of sources used by the repository."""
-        return self.cnx.source_defs()
-
-    @deprecated('[3.19] use .entity_metas(eid) instead')
-    def describe(self, eid, asdict=False):
-        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
-        return self.cnx.describe(eid, asdict)
-
-    # these are overridden by set_log_methods below
-    # only defining here to prevent pylint from complaining
-    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi'))
-
-
-
-# cursor / connection objects ##################################################
-
-class Cursor(object):
-    """These objects represent a database cursor, which is used to manage the
-    context of a fetch operation. Cursors created from the same connection are
-    not isolated, i.e., any changes done to the database by a cursor are
-    immediately visible by the other cursors. Cursors created from different
-    connections are isolated.
-    """
-
-    def __init__(self, connection, repo, req=None):
-        """This read-only attribute return a reference to the Connection
-        object on which the cursor was created.
-        """
-        self.connection = connection
-        """optionnal issuing request instance"""
-        self.req = req
-        self._repo = repo
-        self._sessid = connection.sessionid
-
-    def close(self):
-        """no effect"""
-        pass
-
-    def _txid(self):
-        return self.connection._txid(self)
-
-    def execute(self, rql, args=None, build_descr=True):
-        """execute a rql query, return resulting rows and their description in
-        a :class:`~cubicweb.rset.ResultSet` object
-
-        * `rql` should be a Unicode string or a plain ASCII string, containing
-          the rql query
-
-        * `args` the optional args dictionary associated to the query, with key
-          matching named substitution in `rql`
-
-        * `build_descr` is a boolean flag indicating if the description should
-          be built on select queries (if false, the description will be en empty
-          list)
-
-        on INSERT queries, there will be one row for each inserted entity,
-        containing its eid
-
-        on SET queries, XXX describe
-
-        DELETE queries returns no result.
-
-        .. Note::
-          to maximize the rql parsing/analyzing cache performance, you should
-          always use substitute arguments in queries, i.e. avoid query such as::
-
-            execute('Any X WHERE X eid 123')
-
-          use::
-
-            execute('Any X WHERE X eid %(x)s', {'x': 123})
-        """
-        rset = self._repo.execute(self._sessid, rql, args,
-                                  build_descr=build_descr, **self._txid())
-        rset.req = self.req
-        return rset
-
-
-class LogCursor(Cursor):
-    """override the standard cursor to log executed queries"""
-
-    def execute(self, operation, parameters=None, build_descr=True):
-        """override the standard cursor to log executed queries"""
-        tstart, cstart = time(), clock()
-        rset = Cursor.execute(self, operation, parameters, build_descr=build_descr)
-        self.connection.executed_queries.append((operation, parameters,
-                                                 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 %s' % self.sessionid)
-        return func(self, *args, **kwargs)
-    return decorator
-
-class Connection(object):
-    """DB-API 2.0 compatible Connection object for CubicWeb
-    """
-    # make exceptions available through the connection object
-    ProgrammingError = ProgrammingError
-    # attributes that may be overriden per connection instance
-    cursor_class = Cursor
-    vreg = None
-    _closed = None
-
-    def __init__(self, repo, cnxid, cnxprops=None):
-        self._repo = repo
-        self.sessionid = cnxid
-        self._close_on_del = getattr(cnxprops, 'close_on_del', True)
-        self._web_request = False
-        if cnxprops and cnxprops.log_queries:
-            self.executed_queries = []
-            self.cursor_class = LogCursor
-
-    @property
-    def is_repo_in_memory(self):
-        """return True if this is a local, aka in-memory, connection to the
-        repository
-        """
-        try:
-            from cubicweb.server.repository import Repository
-        except ImportError:
-            # code not available, no way
-            return False
-        return isinstance(self._repo, Repository)
-
-    @property # could be a cached property but we want to prevent assigment to
-              # catch potential programming error.
-    def anonymous_connection(self):
-        login = self._repo.user_info(self.sessionid)[1]
-        anon_login = self.vreg.config.get('anonymous-user')
-        return login == anon_login
-
-    def __repr__(self):
-        if self.anonymous_connection:
-            return '<Connection %s (anonymous)>' % self.sessionid
-        return '<Connection %s>' % self.sessionid
-
-    def __enter__(self):
-        return self.cursor()
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        if exc_type is None:
-            self.commit()
-        else:
-            self.rollback()
-            return False #propagate the exception
-
-    def __del__(self):
-        """close the remote connection if necessary"""
-        if self._closed is None and self._close_on_del:
-            try:
-                self.close()
-            except Exception:
-                pass
-
-    # server-side service call #################################################
-
-    @check_not_closed
-    def call_service(self, regid, **kwargs):
-        return self._repo.call_service(self.sessionid, regid, **kwargs)
-
-    # connection initialization methods ########################################
-
-    def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
-        config = self.vreg.config
-        if cubes is _MARKER:
-            cubes = self._repo.get_cubes()
-        elif cubes is None:
-            cubes = ()
-        else:
-            if not isinstance(cubes, (list, tuple)):
-                cubes = (cubes,)
-            if expand:
-                cubes = config.expand_cubes(cubes)
-        if subpath is None:
-            subpath = esubpath = ('entities', 'views')
-        else:
-            esubpath = subpath
-        if 'views' in subpath:
-            esubpath = list(subpath)
-            esubpath.remove('views')
-            esubpath.append(join('web', 'views'))
-        # first load available configs, necessary for proper persistent
-        # properties initialization
-        config.load_available_configs()
-        # then init cubes
-        config.init_cubes(cubes)
-        # then load appobjects into the registry
-        vpath = config.build_appobjects_path(reversed(config.cubes_path()),
-                                             evobjpath=esubpath,
-                                             tvobjpath=subpath)
-        self.vreg.register_objects(vpath)
-
-    def use_web_compatible_requests(self, baseurl, sitetitle=None):
-        """monkey patch DBAPIRequest to fake a cw.web.request, so you should
-        able to call html views using rset from a simple dbapi connection.
-
-        You should call `load_appobjects` at some point to register those views.
-        """
-        DBAPIRequest.property_value = _fake_property_value
-        DBAPIRequest.next_tabindex = count().next
-        DBAPIRequest.relative_path = fake
-        DBAPIRequest.url = fake
-        DBAPIRequest.get_page_data = fake
-        DBAPIRequest.set_page_data = fake
-        # XXX could ask the repo for it's base-url configuration
-        self.vreg.config.set_option('base-url', baseurl)
-        self.vreg.config.uiprops = {}
-        self.vreg.config.datadir_url = baseurl + '/data'
-        # XXX why is this needed? if really needed, could be fetched by a query
-        if sitetitle is not None:
-            self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle}
-        self._web_request = True
-
-    def request(self):
-        if self._web_request:
-            from cubicweb.web.request import DBAPICubicWebRequestBase
-            req = DBAPICubicWebRequestBase(self.vreg, False)
-            req.get_header = lambda x, default=None: default
-            req.set_session = lambda session: DBAPIRequest.set_session(
-                req, session)
-            req.relative_path = lambda includeparams=True: ''
-        else:
-            req = DBAPIRequest(self.vreg)
-        req.set_session(DBAPISession(self))
-        return req
-
-    @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
-        eid, login, groups, properties = self._repo.user_info(self.sessionid,
-                                                              props)
-        if req is None:
-            req = self.request()
-        rset = req.eid_rset(eid, 'CWUser')
-        if self.vreg is not None and 'etypes' in self.vreg:
-            user = self.vreg['etypes'].etype_class('CWUser')(
-                req, rset, row=0, groups=groups, properties=properties)
-        else:
-            from cubicweb.entity import Entity
-            user = Entity(req, rset, row=0)
-        user.cw_attr_cache['login'] = login # cache login
-        return user
-
-    @check_not_closed
-    def check(self):
-        """raise `BadConnectionId` if the connection is no more valid, else
-        return its latest activity timestamp.
-        """
-        return self._repo.check_session(self.sessionid)
-
-    def _txid(self, cursor=None): # pylint: disable=E0202
-        # XXX could now handle various isolation level!
-        # return a dict as bw compat trick
-        return {'txid': currentThread().getName()}
-
-    # session data methods #####################################################
-
-    @check_not_closed
-    def get_shared_data(self, key, default=None, pop=False, txdata=False):
-        """return value associated to key in the session's data dictionary or
-        session's transaction's data if `txdata` is true.
-
-        If pop is True, value will be removed from the dictionary.
-
-        If key isn't defined in the dictionary, value specified by the
-        `default` argument will be returned.
-        """
-        return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
-
-    @check_not_closed
-    def set_shared_data(self, key, value, txdata=False):
-        """set value associated to `key` in shared data
-
-        if `txdata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction.
-        """
-        return self._repo.set_shared_data(self.sessionid, key, value, txdata)
-
-    # meta-data accessors ######################################################
-
-    @check_not_closed
-    def source_defs(self):
-        """Return the definition of sources used by the repository."""
-        return self._repo.source_defs()
-
-    @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.
-
-        `foreid` argument is deprecated and now useless (as of 3.19).
-        """
-        if foreid is not None:
-            warn('[3.19] foreid argument is deprecated', DeprecationWarning,
-                 stacklevel=2)
-        return self._repo.get_option_value(option)
-
-
-    @check_not_closed
-    def entity_metas(self, eid):
-        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
-        try:
-            return self._repo.entity_metas(self.sessionid, eid, **self._txid())
-        except AttributeError:
-            # talking to pre 3.19 repository
-            metas = self._repo.describe(self.sessionid, eid, **self._txid())
-            if len(metas) == 3: # even older backward compat
-                metas = list(metas)
-                metas.append(metas[1])
-            return dict(zip(('type', 'source', 'extid', 'asource'), metas))
-
-
-    @deprecated('[3.19] use .entity_metas(eid) instead')
-    @check_not_closed
-    def describe(self, eid, asdict=False):
-        try:
-            metas = self._repo.entity_metas(self.sessionid, eid, **self._txid())
-        except AttributeError:
-            metas = self._repo.describe(self.sessionid, eid, **self._txid())
-            # talking to pre 3.19 repository
-            if len(metas) == 3: # even older backward compat
-                metas = list(metas)
-                metas.append(metas[1])
-            if asdict:
-                return dict(zip(('type', 'source', 'extid', 'asource'), metas))
-            return metas[:-1]
-        if asdict:
-            metas['asource'] = meta['source'] # XXX pre 3.19 client compat
-            return metas
-        return metas['type'], metas['source'], metas['extid']
-
-
-    # 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).
-
-        The connection will be unusable from this point forward; an Error (or
-        subclass) exception will be raised if any operation is attempted with
-        the connection. The same applies to all cursor objects trying to use the
-        connection.  Note that closing a connection without committing the
-        changes first will cause an implicit rollback to be performed.
-        """
-        self._repo.close(self.sessionid, **self._txid())
-        del self._repo # necessary for proper garbage collection
-        self._closed = 1
-
-    # 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.
-
-        Managers may filter according to user (eid) who has done the transaction
-        using the `ueid` argument. Others will only see their own transactions.
-
-        Additional filtering capabilities is provided by using the following
-        named arguments:
-
-        * `etype` to get only transactions creating/updating/deleting entities
-          of the given type
-
-        * `eid` to get only transactions applied to entity of the given eid
-
-        * `action` to get only transactions doing the given action (action in
-          'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
-          'D'.
-
-        * `public`: when additional filtering is provided, their are by default
-          only searched in 'public' actions, unless a `public` argument is given
-          and set to false.
-        """
-        actionfilters.update(self._txid())
-        txinfos = self._repo.undoable_transactions(self.sessionid, ueid,
-                                                   **actionfilters)
-        if req is None:
-            req = self.request()
-        for txinfo in txinfos:
-            txinfo.req = req
-        return txinfos
-
-    @check_not_closed
-    def transaction_info(self, txuuid, req=None):
-        """Return transaction object for the given uid.
-
-        raise `NoSuchTransaction` if not found or if session's user is not
-        allowed (eg not in managers group and the transaction doesn't belong to
-        him).
-        """
-        txinfo = self._repo.transaction_info(self.sessionid, txuuid,
-                                             **self._txid())
-        if req is None:
-            req = self.request()
-        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.
-
-        If public is true, return only 'public' actions, eg not ones triggered
-        under the cover by hooks, else return all actions.
-
-        raise `NoSuchTransaction` if the transaction is not found or if
-        session's user is not allowed (eg not in managers group and the
-        transaction doesn't belong to him).
-        """
-        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.
-
-        raise `NoSuchTransaction` if not found or if session's user is not
-        allowed (eg not in managers group and the transaction doesn't belong to
-        him).
-        """
-        return self._repo.undo_transaction(self.sessionid, txuuid,
-                                           **self._txid())
-
-in_memory_cnx = deprecated('[3.16] use _repo_connect instead)')(_repo_connect)
--- a/debian/changelog	Fri Oct 09 09:40:08 2015 +0200
+++ b/debian/changelog	Fri Oct 09 17:52:14 2015 +0200
@@ -1,3 +1,15 @@
+cubicweb (3.21.1-1) unstable; urgency=medium
+
+  * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Tue, 28 Jul 2015 18:05:55 +0200
+
+cubicweb (3.21.0-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Fri, 10 Jul 2015 17:04:11 +0200
+
 cubicweb (3.20.10-1) unstable; urgency=medium
 
   * New upstream release.
--- a/debian/control	Fri Oct 09 09:40:08 2015 +0200
+++ b/debian/control	Fri Oct 09 17:52:14 2015 +0200
@@ -20,7 +20,7 @@
  python-lxml,
 Standards-Version: 3.9.1
 Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.6
+X-Python-Version: >= 2.6
 
 Package: cubicweb
 Architecture: all
@@ -58,10 +58,10 @@
  | python-pysqlite2,
  python-passlib
 Recommends:
- pyro (<< 4.0.0),
- cubicweb-documentation (= ${source:Version})
+ cubicweb-documentation (= ${source:Version}),
 Suggests:
- python-zmq
+ python-zmq,
+ python-cwclientlib (>= 0.4.0),
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -109,7 +109,6 @@
  cubicweb-ctl (= ${source:Version}),
  python-twisted-web
 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.
@@ -137,6 +136,7 @@
 Breaks:
  cubicweb-inlinedit (<< 1.1.1),
  cubicweb-bootstrap (<< 0.6.6),
+ cubicweb-folder (<< 1.10.0),
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-common.install	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,4 @@
+usr/lib/python2*/*-packages/cubicweb/entities/
+usr/lib/python2*/*-packages/cubicweb/ext/
+usr/share/cubicweb/cubes/
+usr/lib/python2*/*-packages/cubicweb/*.py
--- a/debian/cubicweb-common.install.in	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/entities/
-usr/lib/PY_VERSION/*-packages/cubicweb/ext/
-usr/share/cubicweb/cubes/
-usr/lib/PY_VERSION/*-packages/cubicweb/*.py
--- a/debian/cubicweb-ctl.cubicweb.init	Fri Oct 09 09:40:08 2015 +0200
+++ b/debian/cubicweb-ctl.cubicweb.init	Fri Oct 09 17:52:14 2015 +0200
@@ -4,16 +4,14 @@
 # Provides:          cubicweb
 # Required-Start:    $remote_fs $syslog $local_fs $network
 # Required-Stop:     $remote_fs $syslog $local_fs $network
-# Should-Start:      postgresql pyro-nsd
-# Should-Stop:       postgresql pyro-nsd
+# Should-Start:      postgresql
+# Should-Stop:       postgresql
 # Default-Start:     2 3 4 5
 # Default-Stop:      0 1 6
 # Short-Description: Start cubicweb application at boot time
 ### END INIT INFO
 
 # FIXME Seems to be inadequate here
-# FIXME If related to pyro, try instead:
-# export PYRO_STORAGE="/tmp"
 cd /tmp
 
 # FIXME Work-around about the following lintian error
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-ctl.install	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,3 @@
+usr/bin/cubicweb-ctl usr/bin/
+usr/lib/python2*/*-packages/cubicweb/cwctl.py
+../cubicweb-ctl.bash_completion etc/bash_completion.d/cubicweb-ctl
--- a/debian/cubicweb-ctl.install.in	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-usr/bin/cubicweb-ctl usr/bin/
-usr/lib/PY_VERSION/*-packages/cubicweb/cwctl.py
-../cubicweb-ctl.bash_completion etc/bash_completion.d/cubicweb-ctl
--- a/debian/cubicweb-ctl.postinst	Fri Oct 09 09:40:08 2015 +0200
+++ b/debian/cubicweb-ctl.postinst	Fri Oct 09 17:52:14 2015 +0200
@@ -10,32 +10,6 @@
     ;;
 esac
 
-if [ "$1" = configure ]; then
-    # XXX bw compat: erudi -> cubicweb migration
-    if [ -e "/etc/erudi.d/" ]; then
-      mv /etc/erudi.d/* /etc/cubicweb.d/ && (
-	  echo 'moved /etc/erudi.d/* to /etc/cubicweb.d/'
-	  sed -i s/ginco/cubicweb/g /etc/*/*.py
-	  sed -i s/erudi/cubicweb/ */*.conf
-	  ) || true # empty dir
-    fi
-    if [ -e "/var/log/erudi/" ]; then
-      mv /var/log/erudi/* /var/log/cubicweb/ && (
-	  echo 'moved /var/log/erudi/* to /var/log/cubicweb/'
-	  ) || true # empty dir
-    fi
-    if [ -e "/var/lib/erudi/backup" ]; then
-      mv /var/lib/erudi/backup/* /var/lib/cubicweb/backup/ && (
-	  echo 'moved /var/lib/erudi/backup/* to /var/lib/cubicweb/backup/'
-	  ) || true # empty dir
-    fi
-    if [ -e "/var/lib/erudi/instances" ]; then
-      mv /var/lib/erudi/instances/* /var/lib/cubicweb/instances/ && (
-	  echo 'moved /var/lib/erudi/instances/* to /var/lib/cubicweb/instances/'
-	  ) || true # empty dir
-    fi
-fi
-
 #DEBHELPER#
 
 exit 0
--- a/debian/cubicweb-ctl.postrm	Fri Oct 09 09:40:08 2015 +0200
+++ b/debian/cubicweb-ctl.postrm	Fri Oct 09 17:52:14 2015 +0200
@@ -1,8 +1,15 @@
 #!/bin/sh -e
-if [ "$1" = "purge" ] ; then
+
+if [ "$1" = "remove" ]; then
         update-rc.d cubicweb remove >/dev/null
 fi
  
+if [ "$1" = "purge" ] ; then
+        rm -rf /etc/cubicweb.d/
+        rm -rf /var/log/cubicweb/
+        rm -rf /var/lib/cubicweb/
+fi
+
 #DEBHELPER#
  
 exit 0
--- a/debian/cubicweb-ctl.prerm	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-#! /bin/sh -e
- 
-case "$1" in
-    purge)
-	rm -rf /etc/cubicweb.d/
-	rm -rf /var/log/cubicweb/
-	rm -rf /var/lib/cubicweb/
-    ;;
-esac
- 
-#DEBHELPER#
- 
-exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-dev.install	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,10 @@
+usr/lib/python2*/*-packages/cubicweb/devtools/
+usr/lib/python2*/*-packages/cubicweb/skeleton/
+usr/lib/python2*/*-packages/cubicweb/test
+usr/lib/python2*/*-packages/cubicweb/entities/test
+usr/lib/python2*/*-packages/cubicweb/ext/test
+usr/lib/python2*/*-packages/cubicweb/server/test
+usr/lib/python2*/*-packages/cubicweb/sobjects/test
+usr/lib/python2*/*-packages/cubicweb/hooks/test
+usr/lib/python2*/*-packages/cubicweb/web/test
+usr/lib/python2*/*-packages/cubicweb/etwist/test
--- a/debian/cubicweb-dev.install.in	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/devtools/
-usr/lib/PY_VERSION/*-packages/cubicweb/skeleton/
-usr/lib/PY_VERSION/*-packages/cubicweb/test
-usr/lib/PY_VERSION/*-packages/cubicweb/entities/test
-usr/lib/PY_VERSION/*-packages/cubicweb/ext/test
-usr/lib/PY_VERSION/*-packages/cubicweb/server/test
-usr/lib/PY_VERSION/*-packages/cubicweb/sobjects/test
-usr/lib/PY_VERSION/*-packages/cubicweb/hooks/test
-usr/lib/PY_VERSION/*-packages/cubicweb/web/test
-usr/lib/PY_VERSION/*-packages/cubicweb/etwist/test
--- a/debian/cubicweb-doc	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-Document: cubicweb-doc
-Title: CubicWeb documentation
-Author: Logilab
-Abstract: Some base documentation for CubicWeb users and developpers
-Section: Apps/Programming
-
-Format: HTML
-Index: /usr/share/doc/cubicweb-documentation/index.html
-Files: /usr/share/doc/cubicweb-documentation/*.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-documentation.doc-base	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,9 @@
+Document: cubicweb-doc
+Title: CubicWeb documentation
+Author: Logilab
+Abstract: Some base documentation for CubicWeb users and developpers
+Section: Apps/Programming
+
+Format: HTML
+Index: /usr/share/doc/cubicweb-documentation/index.html
+Files: /usr/share/doc/cubicweb-documentation/*.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-documentation.install	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,2 @@
+../../doc/book usr/share/doc/cubicweb-documentation
+../../doc/_build/html usr/share/doc/cubicweb-documentation
--- a/debian/cubicweb-documentation.install.in	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-../../doc/book usr/share/doc/cubicweb-documentation
-../../doc/html usr/share/doc/cubicweb-documentation
-../../debian/cubicweb-doc usr/share/doc-base/cubicweb-doc
--- a/debian/cubicweb-documentation.postinst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-#! /bin/sh -e
-#
-
-if [ "$1" = configure ]; then
-  if which install-docs >/dev/null 2>&1; then
-    install-docs -i /usr/share/doc-base/cubicweb-doc
-  fi
-fi
-
-
-#DEBHELPER#
-
-exit 0
--- a/debian/cubicweb-documentation.prerm	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-#! /bin/sh -e
-#
-
-if [ "$1" = remove -o "$1" = upgrade ]; then
-  if which install-docs >/dev/null 2>&1; then
-    install-docs -r cubicweb-doc
-  fi
-fi
-
-#DEBHELPER#
-
-exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-server.install	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,5 @@
+usr/lib/python2*/*-packages/cubicweb/server/
+usr/lib/python2*/*-packages/cubicweb/hooks/
+usr/lib/python2*/*-packages/cubicweb/sobjects/
+usr/lib/python2*/*-packages/cubicweb/schemas/
+usr/share/cubicweb/migration/
--- a/debian/cubicweb-server.install.in	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/server/
-usr/lib/PY_VERSION/*-packages/cubicweb/hooks/
-usr/lib/PY_VERSION/*-packages/cubicweb/sobjects/
-usr/lib/PY_VERSION/*-packages/cubicweb/schemas/
-usr/share/cubicweb/migration/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-twisted.install	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+usr/lib/python2*/*-packages/cubicweb/etwist/
--- a/debian/cubicweb-twisted.install.in	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/etwist/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-web.install	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,4 @@
+usr/lib/python2*/*-packages/cubicweb/web
+usr/lib/python2*/*-packages/cubicweb/wsgi
+usr/share/cubicweb/cubes/shared/data
+usr/share/cubicweb/cubes/shared/wdoc
--- a/debian/cubicweb-web.install.in	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/web
-usr/lib/PY_VERSION/*-packages/cubicweb/wsgi
-usr/share/cubicweb/cubes/shared/data
-usr/share/cubicweb/cubes/shared/wdoc
--- a/debian/pycompat	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-2
--- a/debian/rules	Fri Oct 09 09:40:08 2015 +0200
+++ b/debian/rules	Fri Oct 09 17:52:14 2015 +0200
@@ -5,8 +5,6 @@
 # Uncomment this to turn on verbose mode.
 #export DH_VERBOSE=1
 
-PY_VERSION:=$(shell pyversions -d)
-
 build: build-stamp
 build-stamp:
 	dh_testdir
@@ -17,7 +15,7 @@
 	# documentation build is now made optional since it can break for old
 	# distributions and we don't want to block a new release of Cubicweb
 	# because of documentation issues.
-	-PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all
+	-PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc all
 	rm -rf debian/pythonpath
 	touch build-stamp
 
@@ -27,10 +25,10 @@
 	rm -rf build
 	#rm -rf debian/cubicweb-*/
 	find . -name "*.pyc" -delete
-	rm -f $(basename $(wildcard debian/*.in))
+	-$(MAKE) -C doc clean
 	dh_clean
 
-install: build $(basename $(wildcard debian/*.in))
+install: build
 	dh_testdir
 	dh_testroot
 	dh_clean
@@ -49,30 +47,28 @@
 	dh_lintian
 
 	# Remove unittests directory (should be available in cubicweb-dev only)
-	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/server/test
-	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/hooks/test
-	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/sobjects/test
-	rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/*-packages/cubicweb/web/test
-	rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/*-packages/cubicweb/etwist/test
-	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/*-packages/cubicweb/ext/test
-	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/*-packages/cubicweb/entities/test
+	rm -rf debian/cubicweb-server/usr/lib/python2*/*-packages/cubicweb/server/test
+	rm -rf debian/cubicweb-server/usr/lib/python2*/*-packages/cubicweb/hooks/test
+	rm -rf debian/cubicweb-server/usr/lib/python2*/*-packages/cubicweb/sobjects/test
+	rm -rf debian/cubicweb-web/usr/lib/python2*/*-packages/cubicweb/web/test
+	rm -rf debian/cubicweb-twisted/usr/lib/python2*/*-packages/cubicweb/etwist/test
+	rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/ext/test
+	rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/entities/test
 
 
-%: %.in
-	sed "s/PY_VERSION/${PY_VERSION}/g" < $< > $@
-
 # Build architecture-independent files here.
 binary-indep: build install
 	dh_testdir
 	dh_testroot -i
 	dh_python2 -i
+	dh_python2 -i /usr/share/cubicweb
 	dh_installinit -i -n --name cubicweb -u"defaults 99"
 	dh_installlogrotate -i
 	dh_installdocs -i -A README
 	dh_installman -i
-	dh_installchangelogs -i
+	dh_installchangelogs -i -Xdoc/changes
 	dh_link -i
-	dh_compress -i -X.py -X.ini -X.xml -X.js -X.rst -X.txt
+	dh_compress -i -X.py -X.ini -X.xml -X.js -X.rst -X.txt -Xchangelog.html
 	dh_fixperms -i
 	dh_installdeb -i
 	dh_gencontrol  -i
--- a/devtools/__init__.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -26,7 +26,6 @@
 import shutil
 import pickle
 import glob
-import random
 import subprocess
 import warnings
 import tempfile
@@ -93,8 +92,6 @@
 DEFAULT_PSQL_SOURCES = DEFAULT_SOURCES.copy()
 DEFAULT_PSQL_SOURCES['system'] = DEFAULT_SOURCES['system'].copy()
 DEFAULT_PSQL_SOURCES['system']['db-driver'] = 'postgres'
-DEFAULT_PSQL_SOURCES['system']['db-host'] = '/tmp'
-DEFAULT_PSQL_SOURCES['system']['db-port'] = str(random.randrange(5432, 2**16))
 DEFAULT_PSQL_SOURCES['system']['db-user'] = unicode(getpass.getuser())
 DEFAULT_PSQL_SOURCES['system']['db-password'] = None
 
@@ -176,8 +173,8 @@
         return self._apphome
     appdatahome = apphome
 
-    def load_configuration(self):
-        super(TestServerConfiguration, self).load_configuration()
+    def load_configuration(self, **kw):
+        super(TestServerConfiguration, self).load_configuration(**kw)
         # no undo support in tests
         self.global_set_option('undo-enabled', 'n')
 
@@ -237,10 +234,6 @@
     def available_languages(self, *args):
         return self.cw_languages()
 
-    def pyro_enabled(self):
-        # but export PYRO_MULTITHREAD=0 or you get problems with sqlite and
-        # threads
-        return True
 
 # XXX merge with BaseApptestConfiguration ?
 class ApptestConfiguration(BaseApptestConfiguration):
@@ -251,7 +244,7 @@
     skip_db_create_and_restore = False
 
     def __init__(self, appid, apphome=None,
-                 log_threshold=logging.CRITICAL, sourcefile=None):
+                 log_threshold=logging.WARNING, sourcefile=None):
         BaseApptestConfiguration.__init__(self, appid, apphome,
                                           log_threshold=log_threshold)
         self.init_repository = sourcefile is None
@@ -303,6 +296,14 @@
         # pure consistency check
         assert self.system_source['db-driver'] == self.DRIVER
 
+        # some handlers want to store info here, avoid a warning
+        from cubicweb.server.sources.native import NativeSQLSource
+        NativeSQLSource.options += (
+            ('global-db-name',
+             {'type': 'string', 'help': 'for internal use only'
+            }),
+        )
+
     def _ensure_test_backup_db_dir(self):
         """Return path of directory for database backup.
 
@@ -398,9 +399,9 @@
 
     def _new_repo(self, config):
         """Factory method to create a new Repository Instance"""
-        from cubicweb.dbapi import in_memory_repo
+        from cubicweb.repoapi import _get_inmemory_repo
         config._cubes = None
-        repo = in_memory_repo(config)
+        repo = _get_inmemory_repo(config)
         # extending Repository class
         repo._has_started = False
         repo._needs_refresh = False
@@ -498,7 +499,7 @@
             repo = self.get_repo(startup=True)
             cnx = self.get_cnx()
             with cnx:
-                pre_setup_func(cnx._cnx, self.config)
+                pre_setup_func(cnx, self.config)
                 cnx.commit()
         self.backup_database(test_db_id)
 
@@ -533,6 +534,48 @@
 
 ### postgres test database handling ############################################
 
+def startpgcluster(pyfile):
+    """Start a postgresql cluster next to pyfile"""
+    datadir = join(os.path.dirname(pyfile), 'data',
+                   'pgdb-%s' % os.path.splitext(os.path.basename(pyfile))[0])
+    if not exists(datadir):
+        try:
+            subprocess.check_call(['initdb', '-D', datadir, '-E', 'utf-8', '--locale=C'])
+
+        except OSError, err:
+            if err.errno == errno.ENOENT:
+                raise OSError('"initdb" could not be found. '
+                              'You should add the postgresql bin folder to your PATH '
+                              '(/usr/lib/postgresql/9.1/bin for example).')
+            raise
+    datadir = os.path.abspath(datadir)
+    pgport = '5432'
+    env = os.environ.copy()
+    sockdir = tempfile.mkdtemp(prefix='cwpg')
+    DEFAULT_PSQL_SOURCES['system']['db-host'] = sockdir
+    DEFAULT_PSQL_SOURCES['system']['db-port'] = pgport
+    options = '-h "" -k %s -p %s' % (sockdir, pgport)
+    options += ' -c fsync=off -c full_page_writes=off'
+    options += ' -c synchronous_commit=off'
+    try:
+        subprocess.check_call(['pg_ctl', 'start', '-w', '-D', datadir,
+                               '-o', options],
+                              env=env)
+    except OSError, err:
+        if err.errno == errno.ENOENT:
+            raise OSError('"pg_ctl" could not be found. '
+                          'You should add the postgresql bin folder to your PATH '
+                          '(/usr/lib/postgresql/9.1/bin for example).')
+        raise
+
+
+def stoppgcluster(pyfile):
+    """Kill the postgresql cluster running next to pyfile"""
+    datadir = join(os.path.dirname(pyfile), 'data',
+                   'pgdb-%s' % os.path.splitext(os.path.basename(pyfile))[0])
+    subprocess.call(['pg_ctl', 'stop', '-D', datadir, '-m', 'fast'])
+
+
 class PostgresTestDataBaseHandler(TestDataBaseHandler):
     DRIVER = 'postgres'
 
@@ -542,45 +585,11 @@
 
     __CTL = set()
 
-    @classmethod
-    def killall(cls):
-        for datadir in cls.__CTL:
-            subprocess.call(['pg_ctl', 'stop', '-D', datadir, '-m', 'fast'])
-
     def __init__(self, *args, **kwargs):
         super(PostgresTestDataBaseHandler, self).__init__(*args, **kwargs)
-        datadir = realpath(join(self.config.apphome, 'pgdb'))
-        if datadir in self.__CTL:
-            return
-        if not exists(datadir):
-            try:
-                subprocess.check_call(['initdb', '-D', datadir, '-E', 'utf-8', '--locale=C'])
-
-            except OSError, err:
-                if err.errno == errno.ENOENT:
-                    raise OSError('"initdb" could not be found. '
-                                  'You should add the postgresql bin folder to your PATH '
-                                  '(/usr/lib/postgresql/9.1/bin for example).')
-                raise
-        port = self.system_source['db-port']
-        directory = self.system_source['db-host']
-        env = os.environ.copy()
-        env['PGPORT'] = str(port)
-        env['PGHOST'] = str(directory)
-        options = '-h "" -k %s -p %s' % (directory, port)
-        options += ' -c fsync=off -c full_page_writes=off'
-        options += ' -c synchronous_commit=off'
-        try:
-            subprocess.check_call(['pg_ctl', 'start', '-w', '-D', datadir,
-                                   '-o', options],
-                                  env=env)
-        except OSError, err:
-            if err.errno == errno.ENOENT:
-                raise OSError('"pg_ctl" could not be found. '
-                              'You should add the postgresql bin folder to your PATH '
-                              '(/usr/lib/postgresql/9.1/bin for example).')
-            raise
-        self.__CTL.add(datadir)
+        if 'global-db-name' not in self.system_source:
+            self.system_source['global-db-name'] = self.system_source['db-name']
+            self.system_source['db-name'] = self.system_source['db-name'] + str(os.getpid())
 
     @property
     @cached
@@ -589,6 +598,10 @@
         return get_db_helper('postgres')
 
     @property
+    def dbname(self):
+        return self.system_source['global-db-name']
+
+    @property
     def dbcnx(self):
         try:
             return self._cnx
@@ -614,13 +627,18 @@
             return backup_name
         return None
 
+    def has_cache(self, db_id):
+        backup_name = self._backup_name(db_id)
+        return (super(PostgresTestDataBaseHandler, self).has_cache(db_id)
+                and backup_name in self.helper.list_databases(self.cursor))
+
     def init_test_database(self):
         """initialize a fresh postgresql database used for testing purpose"""
         from cubicweb.server import init_repository
         from cubicweb.server.serverctl import system_source_cnx, createdb
         # connect on the dbms system base to create our base
         try:
-            self._drop(self.dbname)
+            self._drop(self.system_source['db-name'])
             createdb(self.helper, self.system_source, self.dbcnx, self.cursor)
             self.dbcnx.commit()
             cnx = system_source_cnx(self.system_source, special_privs='LANGUAGE C',
@@ -698,7 +716,7 @@
         """Actual restore of the current database.
 
         Use the value tostored in db_cache as input """
-        self._drop(self.dbname)
+        self._drop(self.system_source['db-name'])
         createdb(self.helper, self.system_source, self.dbcnx, self.cursor,
                  template=backup_coordinates)
         self.dbcnx.commit()
@@ -770,7 +788,7 @@
         dbfile = self.absolute_dbfile()
         backup_file = self.absolute_backup_file(db_id, 'sqlite')
         shutil.copy(dbfile, backup_file)
-        # Usefull to debug WHO write a database
+        # Useful to debug WHO writes a database
         # backup_stack = self.absolute_backup_file(db_id, '.stack')
         #with open(backup_stack, 'w') as backup_stack_file:
         #    import traceback
@@ -799,7 +817,6 @@
 
 import atexit
 atexit.register(SQLiteTestDataBaseHandler._cleanup_all_tmpdb)
-atexit.register(PostgresTestDataBaseHandler.killall)
 
 
 def install_sqlite_patch(querier):
--- a/devtools/data/qunit.css	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/data/qunit.css	Fri Oct 09 17:52:14 2015 +0200
@@ -1,119 +1,291 @@
+/*!
+ * QUnit 1.18.0
+ * http://qunitjs.com/
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2015-04-03T10:23Z
+ */
 
-ol#qunit-tests {
-	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
-	margin:0;
-	padding:0;
-	list-style-position:inside;
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+	font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+	margin: 0;
+	padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+	padding: 0.5em 0 0.5em 1em;
 
-	font-size: smaller;
+	color: #8699A4;
+	background-color: #0D3349;
+
+	font-size: 1.5em;
+	line-height: 1em;
+	font-weight: 400;
+
+	border-radius: 5px 5px 0 0;
+}
+
+#qunit-header a {
+	text-decoration: none;
+	color: #C2CCD1;
 }
-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;
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+	color: #FFF;
+}
+
+#qunit-testrunner-toolbar label {
+	display: inline-block;
+	padding: 0 0.5em 0 0.1em;
+}
+
+#qunit-banner {
+	height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+	padding: 0.5em 1em 0.5em 1em;
+	color: #5E740B;
+	background-color: #EEE;
+	overflow: hidden;
 }
-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;
+
+#qunit-userAgent {
+	padding: 0.5em 1em 0.5em 1em;
+	background-color: #2B81AF;
+	color: #FFF;
+	text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+#qunit-modulefilter-container {
+	float: right;
+	padding: 0.2em;
+}
+
+.qunit-url-config {
+	display: inline-block;
+	padding: 0.1em;
+}
+
+.qunit-filter {
+	display: block;
+	float: right;
+	margin-left: 1em;
+}
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+	list-style-position: inside;
+}
+
+#qunit-tests li {
+	padding: 0.4em 1em 0.4em 1em;
+	border-bottom: 1px solid #FFF;
+	list-style-position: inside;
 }
-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;
+
+#qunit-tests > li {
+	display: none;
+}
+
+#qunit-tests li.running,
+#qunit-tests li.pass,
+#qunit-tests li.fail,
+#qunit-tests li.skipped {
+	display: list-item;
+}
+
+#qunit-tests.hidepass li.running,
+#qunit-tests.hidepass li.pass {
+	visibility: hidden;
+	position: absolute;
+	width:   0px;
+	height:  0px;
+	padding: 0;
+	border:  0;
+	margin:  0;
+}
+
+#qunit-tests li strong {
+	cursor: pointer;
+}
+
+#qunit-tests li.skipped strong {
+	cursor: default;
+}
+
+#qunit-tests li a {
+	padding: 0.5em;
+	color: #C2CCD1;
+	text-decoration: none;
 }
 
-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;
+#qunit-tests li p a {
+	padding: 0.25em;
+	color: #6B6464;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+	color: #000;
+}
+
+#qunit-tests li .runtime {
+	float: right;
+	font-size: smaller;
 }
-ol#qunit-tests li.pass{
-	background-color:#D2E0E6;
-	color:#528CE0;
+
+.qunit-assert-list {
+	margin-top: 0.5em;
+	padding: 0.5em;
+
+	background-color: #FFF;
+
+	border-radius: 5px;
 }
-ol#qunit-tests li.fail{
-	background-color:#EE5757;
-	color:#000;
+
+.qunit-collapsed {
+	display: none;
+}
+
+#qunit-tests table {
+	border-collapse: collapse;
+	margin-top: 0.2em;
 }
-ol#qunit-tests li strong {
-	cursor:pointer;
+
+#qunit-tests th {
+	text-align: right;
+	vertical-align: top;
+	padding: 0 0.5em 0 0;
+}
+
+#qunit-tests td {
+	vertical-align: top;
+}
+
+#qunit-tests pre {
+	margin: 0;
+	white-space: pre-wrap;
+	word-wrap: break-word;
 }
-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;
+
+#qunit-tests del {
+	background-color: #E0F2BE;
+	color: #374E0C;
+	text-decoration: none;
+}
+
+#qunit-tests ins {
+	background-color: #FFCACA;
+	color: #500;
+	text-decoration: none;
 }
-h2#qunit-banner{
-	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
-	height:5px;
-	margin:0;
-	padding:0;
+
+/*** Test Counts */
+
+#qunit-tests b.counts                       { color: #000; }
+#qunit-tests b.passed                       { color: #5E740B; }
+#qunit-tests b.failed                       { color: #710909; }
+
+#qunit-tests li li {
+	padding: 5px;
+	background-color: #FFF;
+	border-bottom: none;
+	list-style-position: inside;
 }
-h2#qunit-banner.qunit-pass{
-	background-color:#C6E746;
-}
-h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar {
-	background-color:#EE5757;
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+	color: #3C510C;
+	background-color: #FFF;
+	border-left: 10px solid #C6E746;
 }
-#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;
+
+#qunit-tests .pass                          { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name               { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected           { color: #999; }
+
+#qunit-banner.qunit-pass                    { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+	color: #710909;
+	background-color: #FFF;
+	border-left: 10px solid #EE5757;
+	white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+	border-radius: 0 0 5px 5px;
 }
-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;
+
+#qunit-tests .fail                          { color: #000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name             { color: #000; }
+
+#qunit-tests .fail .test-actual             { color: #EE5757; }
+#qunit-tests .fail .test-expected           { color: #008000; }
+
+#qunit-banner.qunit-fail                    { background-color: #EE5757; }
+
+/*** Skipped tests */
+
+#qunit-tests .skipped {
+	background-color: #EBECE9;
+}
+
+#qunit-tests .qunit-skipped-label {
+	background-color: #F4FF77;
+	display: inline-block;
+	font-style: normal;
+	color: #366097;
+	line-height: 1.8em;
+	padding: 0 0.5em;
+	margin: -0.4em 0.4em -0.4em 0;
 }
-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;
+
+/** Result */
+
+#qunit-testresult {
+	padding: 0.5em 1em 0.5em 1em;
+
+	color: #2B81AF;
+	background-color: #D2E0E6;
+
+	border-bottom: 1px solid #FFF;
 }
-strong b.fail{
-	color:#710909;
-	}
-strong b.pass{
-	color:#5E740B;
-	}
+#qunit-testresult .module-name {
+	font-weight: 700;
+}
+
+/** Fixture */
+
+#qunit-fixture {
+	position: absolute;
+	top: -10000px;
+	left: -10000px;
+	width: 1000px;
+	height: 1000px;
+}
--- a/devtools/data/qunit.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/data/qunit.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,643 +1,713 @@
-/*
- * QUnit - A JavaScript Unit Testing Framework
- * 
- * http://docs.jquery.com/QUnit
+/*!
+ * QUnit 1.18.0
+ * http://qunitjs.com/
  *
- * Copyright (c) 2009 John Resig, Jörn Zaefferer
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * and GPL (GPL-LICENSE.txt) licenses.
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2015-04-03T10:23Z
  */
 
-(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 );
+(function( window ) {
+
+var QUnit,
+	config,
+	onErrorFnPrev,
+	loggingCallbacks = {},
+	fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ),
+	toString = Object.prototype.toString,
+	hasOwn = Object.prototype.hasOwnProperty,
+	// Keep a local reference to Date (GH-283)
+	Date = window.Date,
+	now = Date.now || function() {
+		return new Date().getTime();
+	},
+	globalStartCalled = false,
+	runStarted = false,
+	setTimeout = window.setTimeout,
+	clearTimeout = window.clearTimeout,
+	defined = {
+		document: window.document !== undefined,
+		setTimeout: window.setTimeout !== undefined,
+		sessionStorage: (function() {
+			var x = "qunit-test-string";
+			try {
+				sessionStorage.setItem( x, x );
+				sessionStorage.removeItem( x );
+				return true;
+			} catch ( e ) {
+				return false;
+			}
+		}())
+	},
+	/**
+	 * Provides a normalized error string, correcting an issue
+	 * with IE 7 (and prior) where Error.prototype.toString is
+	 * not properly implemented
+	 *
+	 * Based on http://es5.github.com/#x15.11.4.4
+	 *
+	 * @param {String|Error} error
+	 * @return {String} error message
+	 */
+	errorString = function( error ) {
+		var name, message,
+			errorString = error.toString();
+		if ( errorString.substring( 0, 7 ) === "[object" ) {
+			name = error.name ? error.name.toString() : "Error";
+			message = error.message ? error.message.toString() : "";
+			if ( name && message ) {
+				return name + ": " + message;
+			} else if ( name ) {
+				return name;
+			} else if ( message ) {
+				return message;
+			} else {
+				return "Error";
+			}
+		} else {
+			return errorString;
 		}
 	},
-	
-	// 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 );
+	/**
+	 * Makes a clone of an object using only Array or Object as base,
+	 * and copies over the own enumerable properties.
+	 *
+	 * @param {Object} obj
+	 * @return {Object} New object with only the own properties (recursively).
+	 */
+	objectValues = function( obj ) {
+		var key, val,
+			vals = QUnit.is( "array", obj ) ? [] : {};
+		for ( key in obj ) {
+			if ( hasOwn.call( obj, key ) ) {
+				val = obj[ key ];
+				vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
 			}
-
-			config.currentModule = name;
-			config.moduleTestEnvironment = testEnvironment;
-			config.moduleStats = { all: 0, bad: 0 };
-
-			QUnit.moduleStart( name, testEnvironment );
-		});
+		}
+		return vals;
+	};
+
+QUnit = {};
+
+/**
+ * Config object: Maintain internal state
+ * Later exposed as QUnit.config
+ * `config` initialized at top of scope
+ */
+config = {
+	// The queue of tests to run
+	queue: [],
+
+	// block until document ready
+	blocking: true,
+
+	// by default, run previously failed tests first
+	// very useful in combination with "Hide passed tests" checked
+	reorder: true,
+
+	// by default, modify document.title when suite is done
+	altertitle: true,
+
+	// by default, scroll to top of the page when suite is done
+	scrolltop: true,
+
+	// when enabled, all tests must call expect()
+	requireExpects: false,
+
+	// depth up-to which object will be dumped
+	maxDepth: 5,
+
+	// add checkboxes that are persisted in the query-string
+	// when enabled, the id is set to `true` as a `QUnit.config` property
+	urlConfig: [
+		{
+			id: "hidepassed",
+			label: "Hide passed tests",
+			tooltip: "Only show tests and assertions that fail. Stored as query-strings."
+		},
+		{
+			id: "noglobals",
+			label: "Check for Globals",
+			tooltip: "Enabling this will test if any test introduces new properties on the " +
+				"`window` object. Stored as query-strings."
+		},
+		{
+			id: "notrycatch",
+			label: "No try-catch",
+			tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
+				"exceptions in IE reasonable. Stored as query-strings."
+		}
+	],
+
+	// Set of all modules.
+	modules: [],
+
+	// The first unnamed module
+	currentModule: {
+		name: "",
+		tests: []
 	},
 
-	asyncTest: function(testName, expected, callback) {
+	callbacks: {}
+};
+
+// Push a loose unnamed module to the modules collection
+config.modules.push( config.currentModule );
+
+// Initialize more QUnit.config and QUnit.urlParams
+(function() {
+	var i, current,
+		location = window.location || { search: "", protocol: "file:" },
+		params = location.search.slice( 1 ).split( "&" ),
+		length = params.length,
+		urlParams = {};
+
+	if ( params[ 0 ] ) {
+		for ( i = 0; i < length; i++ ) {
+			current = params[ i ].split( "=" );
+			current[ 0 ] = decodeURIComponent( current[ 0 ] );
+
+			// allow just a key to turn on a flag, e.g., test.html?noglobals
+			current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
+			if ( urlParams[ current[ 0 ] ] ) {
+				urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
+			} else {
+				urlParams[ current[ 0 ] ] = current[ 1 ];
+			}
+		}
+	}
+
+	if ( urlParams.filter === true ) {
+		delete urlParams.filter;
+	}
+
+	QUnit.urlParams = urlParams;
+
+	// String search anywhere in moduleName+testName
+	config.filter = urlParams.filter;
+
+	if ( urlParams.maxDepth ) {
+		config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ?
+			Number.POSITIVE_INFINITY :
+			urlParams.maxDepth;
+	}
+
+	config.testId = [];
+	if ( urlParams.testId ) {
+
+		// Ensure that urlParams.testId is an array
+		urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," );
+		for ( i = 0; i < urlParams.testId.length; i++ ) {
+			config.testId.push( urlParams.testId[ i ] );
+		}
+	}
+
+	// Figure out if we're running the tests from a server or not
+	QUnit.isLocal = location.protocol === "file:";
+
+	// Expose the current QUnit version
+	QUnit.version = "1.18.0";
+}());
+
+// Root QUnit object.
+// `QUnit` initialized at top of scope
+extend( QUnit, {
+
+	// call on start of module test to prepend name to all tests
+	module: function( name, testEnvironment ) {
+		var currentModule = {
+			name: name,
+			testEnvironment: testEnvironment,
+			tests: []
+		};
+
+		// DEPRECATED: handles setup/teardown functions,
+		// beforeEach and afterEach should be used instead
+		if ( testEnvironment && testEnvironment.setup ) {
+			testEnvironment.beforeEach = testEnvironment.setup;
+			delete testEnvironment.setup;
+		}
+		if ( testEnvironment && testEnvironment.teardown ) {
+			testEnvironment.afterEach = testEnvironment.teardown;
+			delete testEnvironment.teardown;
+		}
+
+		config.modules.push( currentModule );
+		config.currentModule = currentModule;
+	},
+
+	// DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
+	asyncTest: function( testName, expected, callback ) {
 		if ( arguments.length === 2 ) {
 			callback = expected;
-			expected = 0;
+			expected = null;
 		}
 
-		QUnit.test(testName, expected, callback, true);
+		QUnit.test( testName, expected, callback, true );
 	},
-	
-	test: function(testName, expected, callback, async) {
-		var name = testName, testEnvironment, testEnvironmentArg;
+
+	test: function( testName, expected, callback, async ) {
+		var test;
 
 		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();
-				}
-			}
+
+		test = new Test({
+			testName: testName,
+			expected: expected,
+			async: async,
+			callback: callback
+		});
+
+		test.queue();
+	},
+
+	skip: function( testName ) {
+		var test = new Test({
+			testName: testName,
+			skip: true
 		});
 
-		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" );
+		test.queue();
+	},
+
+	// DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
+	// In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
+	start: function( count ) {
+		var globalStartAlreadyCalled = globalStartCalled;
+
+		if ( !config.current ) {
+			globalStartCalled = true;
+
+			if ( runStarted ) {
+				throw new Error( "Called start() outside of a test context while already started" );
+			} else if ( globalStartAlreadyCalled || count > 1 ) {
+				throw new Error( "Called start() outside of a test context too many times" );
+			} else if ( config.autostart ) {
+				throw new Error( "Called start() outside of a test context when " +
+					"QUnit.config.autostart was true" );
+			} else if ( !config.pageLoaded ) {
+
+				// The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
+				config.autostart = true;
+				return;
 			}
-
-			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++;
-					}
-				}
+		} else {
+
+			// If a test is running, adjust its semaphore
+			config.current.semaphore -= count || 1;
+
+			// Don't start until equal number of stop-calls
+			if ( config.current.semaphore > 0 ) {
+				return;
 			}
 
-			QUnit.testDone( testName, bad, config.assertions.length );
-
-			if ( !window.setTimeout && !config.queue.length ) {
-				done();
+			// throw an Error if start is called more often than stop
+			if ( config.current.semaphore < 0 ) {
+				config.current.semaphore = 0;
+
+				QUnit.pushFailure(
+					"Called start() while already started (test's semaphore was 0 already)",
+					sourceFromStacktrace( 2 )
+				);
+				return;
 			}
-		});
-
-		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;
+
+		resumeProcessing();
 	},
 
-	/**
-	 * 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);
+	// DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
+	stop: function( count ) {
+
+		// If there isn't a test running, don't allow QUnit.stop() to be called
+		if ( !config.current ) {
+			throw new Error( "Called stop() outside of a test context" );
+		}
+
+		// If a test is running, adjust its semaphore
+		config.current.semaphore += count || 1;
+
+		pauseProcessing();
 	},
-	
-	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);
-		}
-	},
-	
+
+	config: config,
+
 	// Safe object type checking
 	is: function( type, obj ) {
-		return Object.prototype.toString.call( obj ) === "[object "+ type +"]";
+		return QUnit.objectType( obj ) === 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--;
+
+	objectType: function( obj ) {
+		if ( typeof obj === "undefined" ) {
+			return "undefined";
+		}
+
+		// Consider: typeof null === object
+		if ( obj === null ) {
+			return "null";
+		}
+
+		var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
+			type = match && match[ 1 ] || "";
+
+		switch ( type ) {
+			case "Number":
+				if ( isNaN( obj ) ) {
+					return "nan";
+				}
+				return "number";
+			case "String":
+			case "Boolean":
+			case "Array":
+			case "Date":
+			case "RegExp":
+			case "Function":
+				return type.toLowerCase();
+		}
+		if ( typeof obj === "object" ) {
+			return "object";
+		}
+		return undefined;
+	},
+
+	extend: extend,
+
+	load: function() {
+		config.pageLoaded = true;
+
+		// Initialize the configuration options
+		extend( config, {
+			stats: { all: 0, bad: 0 },
+			moduleStats: { all: 0, bad: 0 },
+			started: 0,
+			updateRate: 1000,
+			autostart: true,
+			filter: ""
+		}, true );
+
+		config.blocking = false;
+
+		if ( config.autostart ) {
+			resumeProcessing();
 		}
 	}
-	
-	// 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;
+});
+
+// Register logging callbacks
+(function() {
+	var i, l, key,
+		callbacks = [ "begin", "done", "log", "testStart", "testDone",
+			"moduleStart", "moduleDone" ];
+
+	function registerLoggingCallback( key ) {
+		var loggingCallback = function( callback ) {
+			if ( QUnit.objectType( callback ) !== "function" ) {
+				throw new Error(
+					"QUnit logging methods require a callback function as their first parameters."
+				);
+			}
+
+			config.callbacks[ key ].push( callback );
+		};
+
+		// DEPRECATED: This will be removed on QUnit 2.0.0+
+		// Stores the registered functions allowing restoring
+		// at verifyLoggingCallbacks() if modified
+		loggingCallbacks[ key ] = loggingCallback;
+
+		return loggingCallback;
 	}
-	
-	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" : "";
-				}
+
+	for ( i = 0, l = callbacks.length; i < l; i++ ) {
+		key = callbacks[ i ];
+
+		// Initialize key collection of logging callback
+		if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) {
+			config.callbacks[ key ] = [];
+		}
+
+		QUnit[ key ] = registerLoggingCallback( key );
+	}
+})();
+
+// `onErrorFnPrev` initialized at top of scope
+// Preserve other handlers
+onErrorFnPrev = window.onerror;
+
+// Cover uncaught exceptions
+// Returning true will suppress the default browser handler,
+// returning false will let it run.
+window.onerror = function( error, filePath, linerNr ) {
+	var ret = false;
+	if ( onErrorFnPrev ) {
+		ret = onErrorFnPrev( error, filePath, linerNr );
+	}
+
+	// Treat return value as window.onerror itself does,
+	// Only do our handling if not suppressed.
+	if ( ret !== true ) {
+		if ( QUnit.config.current ) {
+			if ( QUnit.config.current.ignoreGlobalErrors ) {
+				return true;
 			}
-		});
-		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 );
+			QUnit.pushFailure( error, filePath + ":" + linerNr );
+		} else {
+			QUnit.test( "global failure", extend(function() {
+				QUnit.pushFailure( error, filePath + ":" + linerNr );
+			}, { validTest: true } ) );
+		}
+		return false;
 	}
 
-	var main = id('main');
-	if ( main ) {
-		config.fixture = main.innerHTML;
-	}
-
-	if ( window.jQuery ) {
-		config.ajaxSettings = window.jQuery.ajaxSettings;
-	}
-
-	QUnit.start();
-});
+	return ret;
+};
 
 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;
-	}
+	var runtime, passed;
 
 	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;
+	if ( config.previousModule ) {
+		runLoggingCallbacks( "moduleDone", {
+			name: config.previousModule.name,
+			tests: config.previousModule.tests,
+			failed: config.moduleStats.bad,
+			passed: config.moduleStats.all - config.moduleStats.bad,
+			total: config.moduleStats.all,
+			runtime: now() - config.moduleStats.started
+		});
 	}
-
-	QUnit.done( config.stats.bad, config.stats.all );
+	delete config.previousModule;
+
+	runtime = now() - config.started;
+	passed = config.stats.all - config.stats.bad;
+
+	runLoggingCallbacks( "done", {
+		failed: config.stats.bad,
+		passed: passed,
+		total: config.stats.all,
+		runtime: runtime
+	});
 }
 
-function validTest( name ) {
-	var i = config.filters.length,
-		run = false;
-
-	if ( !i ) {
-		return true;
+// Doesn't support IE6 to IE9, it will return undefined on these browsers
+// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
+function extractStacktrace( e, offset ) {
+	offset = offset === undefined ? 4 : offset;
+
+	var stack, include, i;
+
+	if ( e.stack ) {
+		stack = e.stack.split( "\n" );
+		if ( /^error$/i.test( stack[ 0 ] ) ) {
+			stack.shift();
+		}
+		if ( fileName ) {
+			include = [];
+			for ( i = offset; i < stack.length; i++ ) {
+				if ( stack[ i ].indexOf( fileName ) !== -1 ) {
+					break;
+				}
+				include.push( stack[ i ] );
+			}
+			if ( include.length ) {
+				return include.join( "\n" );
+			}
+		}
+		return stack[ offset ];
+
+	// Support: Safari <=6 only
+	} else if ( e.sourceURL ) {
+
+		// exclude useless self-reference for generated Error objects
+		if ( /qunit.js$/.test( e.sourceURL ) ) {
+			return;
+		}
+
+		// for actual exceptions, this is useful
+		return e.sourceURL + ":" + e.line;
 	}
-	
-	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;
+}
+
+function sourceFromStacktrace( offset ) {
+	var error = new Error();
+
+	// Support: Safari <=7 only, IE <=10 - 11 only
+	// Not all browsers generate the `stack` property for `new Error()`, see also #636
+	if ( !error.stack ) {
+		try {
+			throw error;
+		} catch ( err ) {
+			error = err;
 		}
 	}
 
-	return run;
+	return extractStacktrace( error, offset );
 }
 
-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 ) {
+function synchronize( callback, last ) {
+	if ( QUnit.objectType( callback ) === "array" ) {
+		while ( callback.length ) {
+			synchronize( callback.shift() );
+		}
+		return;
+	}
 	config.queue.push( callback );
 
 	if ( config.autorun && !config.blocking ) {
-		process();
+		process( last );
 	}
 }
 
-function process() {
-	var start = (new Date()).getTime();
+function process( last ) {
+	function next() {
+		process( last );
+	}
+	var start = now();
+	config.depth = ( config.depth || 0 ) + 1;
 
 	while ( config.queue.length && !config.blocking ) {
-		if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
+		if ( !defined.setTimeout || config.updateRate <= 0 ||
+				( ( now() - start ) < config.updateRate ) ) {
+			if ( config.current ) {
+
+				// Reset async tracking for each phase of the Test lifecycle
+				config.current.usedAsync = false;
+			}
 			config.queue.shift()();
-
 		} else {
-			setTimeout( process, 13 );
+			setTimeout( next, 13 );
 			break;
 		}
 	}
+	config.depth--;
+	if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
+		done();
+	}
+}
+
+function begin() {
+	var i, l,
+		modulesLog = [];
+
+	// If the test run hasn't officially begun yet
+	if ( !config.started ) {
+
+		// Record the time of the test run's beginning
+		config.started = now();
+
+		verifyLoggingCallbacks();
+
+		// Delete the loose unnamed module if unused.
+		if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
+			config.modules.shift();
+		}
+
+		// Avoid unnecessary information by not logging modules' test environments
+		for ( i = 0, l = config.modules.length; i < l; i++ ) {
+			modulesLog.push({
+				name: config.modules[ i ].name,
+				tests: config.modules[ i ].tests
+			});
+		}
+
+		// The test run is officially beginning now
+		runLoggingCallbacks( "begin", {
+			totalTests: Test.count,
+			modules: modulesLog
+		});
+	}
+
+	config.blocking = false;
+	process( true );
+}
+
+function resumeProcessing() {
+	runStarted = true;
+
+	// A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
+	if ( defined.setTimeout ) {
+		setTimeout(function() {
+			if ( config.current && config.current.semaphore > 0 ) {
+				return;
+			}
+			if ( config.timeout ) {
+				clearTimeout( config.timeout );
+			}
+
+			begin();
+		}, 13 );
+	} else {
+		begin();
+	}
+}
+
+function pauseProcessing() {
+	config.blocking = true;
+
+	if ( config.testTimeout && defined.setTimeout ) {
+		clearTimeout( config.timeout );
+		config.timeout = setTimeout(function() {
+			if ( config.current ) {
+				config.current.semaphore = 0;
+				QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
+			} else {
+				throw new Error( "Test timed out" );
+			}
+			resumeProcessing();
+		}, config.testTimeout );
+	}
 }
 
 function saveGlobal() {
 	config.pollution = [];
-	
+
 	if ( config.noglobals ) {
 		for ( var key in window ) {
-			config.pollution.push( key );
+			if ( hasOwn.call( window, key ) ) {
+				// in Opera sometimes DOM element ids show up here, ignore them
+				if ( /^qunit-test-output/.test( key ) ) {
+					continue;
+				}
+				config.pollution.push( key );
+			}
 		}
 	}
 }
 
-function checkPollution( name ) {
-	var old = config.pollution;
+function checkPollution() {
+	var newGlobals,
+		deletedGlobals,
+		old = config.pollution;
+
 	saveGlobal();
-	
-	var newGlobals = diff( old, config.pollution );
+
+	newGlobals = diff( config.pollution, old );
 	if ( newGlobals.length > 0 ) {
-		ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
-		config.expected++;
+		QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
 	}
 
-	var deletedGlobals = diff( config.pollution, old );
+	deletedGlobals = diff( old, config.pollution );
 	if ( deletedGlobals.length > 0 ) {
-		ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
-		config.expected++;
+		QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
 	}
 }
 
 // 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);
+	var i, j,
+		result = a.slice();
+
+	for ( i = 0; i < result.length; i++ ) {
+		for ( j = 0; j < b.length; j++ ) {
+			if ( result[ i ] === b[ j ] ) {
+				result.splice( i, 1 );
 				i--;
 				break;
 			}
@@ -646,424 +716,3113 @@
 	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) {
+function extend( a, b, undefOnly ) {
 	for ( var prop in b ) {
-		a[prop] = b[prop];
+		if ( hasOwn.call( b, prop ) ) {
+
+			// Avoid "Member not found" error in IE8 caused by messing with window.constructor
+			if ( !( prop === "constructor" && a === window ) ) {
+				if ( b[ prop ] === undefined ) {
+					delete a[ prop ];
+				} else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
+					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 );
+function runLoggingCallbacks( key, args ) {
+	var i, l, callbacks;
+
+	callbacks = config.callbacks[ key ];
+	for ( i = 0, l = callbacks.length; i < l; i++ ) {
+		callbacks[ i ]( args );
+	}
+}
+
+// DEPRECATED: This will be removed on 2.0.0+
+// This function verifies if the loggingCallbacks were modified by the user
+// If so, it will restore it, assign the given callback and print a console warning
+function verifyLoggingCallbacks() {
+	var loggingCallback, userCallback;
+
+	for ( loggingCallback in loggingCallbacks ) {
+		if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
+
+			userCallback = QUnit[ loggingCallback ];
+
+			// Restore the callback function
+			QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
+
+			// Assign the deprecated given callback
+			QUnit[ loggingCallback ]( userCallback );
+
+			if ( window.console && window.console.warn ) {
+				window.console.warn(
+					"QUnit." + loggingCallback + " was replaced with a new value.\n" +
+					"Please, check out the documentation on how to apply logging callbacks.\n" +
+					"Reference: http://api.qunitjs.com/category/callbacks/"
+				);
+			}
+		}
+	}
+}
+
+// from jquery.js
+function inArray( elem, array ) {
+	if ( array.indexOf ) {
+		return array.indexOf( elem );
+	}
+
+	for ( var i = 0, length = array.length; i < length; i++ ) {
+		if ( array[ i ] === elem ) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+function Test( settings ) {
+	var i, l;
+
+	++Test.count;
+
+	extend( this, settings );
+	this.assertions = [];
+	this.semaphore = 0;
+	this.usedAsync = false;
+	this.module = config.currentModule;
+	this.stack = sourceFromStacktrace( 3 );
+
+	// Register unique strings
+	for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
+		if ( this.module.tests[ i ].name === this.testName ) {
+			this.testName += " ";
+		}
+	}
+
+	this.testId = generateHash( this.module.name, this.testName );
+
+	this.module.tests.push({
+		name: this.testName,
+		testId: this.testId
+	});
+
+	if ( settings.skip ) {
+
+		// Skipped tests will fully ignore any sent callback
+		this.callback = function() {};
+		this.async = false;
+		this.expected = 0;
 	} else {
-		fn();
+		this.assert = new Assert( this );
 	}
 }
 
-function id(name) {
-	return !!(typeof document !== "undefined" && document && document.getElementById) &&
-		document.getElementById( name );
+Test.count = 0;
+
+Test.prototype = {
+	before: function() {
+		if (
+
+			// Emit moduleStart when we're switching from one module to another
+			this.module !== config.previousModule ||
+
+				// They could be equal (both undefined) but if the previousModule property doesn't
+				// yet exist it means this is the first test in a suite that isn't wrapped in a
+				// module, in which case we'll just emit a moduleStart event for 'undefined'.
+				// Without this, reporters can get testStart before moduleStart  which is a problem.
+				!hasOwn.call( config, "previousModule" )
+		) {
+			if ( hasOwn.call( config, "previousModule" ) ) {
+				runLoggingCallbacks( "moduleDone", {
+					name: config.previousModule.name,
+					tests: config.previousModule.tests,
+					failed: config.moduleStats.bad,
+					passed: config.moduleStats.all - config.moduleStats.bad,
+					total: config.moduleStats.all,
+					runtime: now() - config.moduleStats.started
+				});
+			}
+			config.previousModule = this.module;
+			config.moduleStats = { all: 0, bad: 0, started: now() };
+			runLoggingCallbacks( "moduleStart", {
+				name: this.module.name,
+				tests: this.module.tests
+			});
+		}
+
+		config.current = this;
+
+		this.testEnvironment = extend( {}, this.module.testEnvironment );
+		delete this.testEnvironment.beforeEach;
+		delete this.testEnvironment.afterEach;
+
+		this.started = now();
+		runLoggingCallbacks( "testStart", {
+			name: this.testName,
+			module: this.module.name,
+			testId: this.testId
+		});
+
+		if ( !config.pollution ) {
+			saveGlobal();
+		}
+	},
+
+	run: function() {
+		var promise;
+
+		config.current = this;
+
+		if ( this.async ) {
+			QUnit.stop();
+		}
+
+		this.callbackStarted = now();
+
+		if ( config.notrycatch ) {
+			promise = this.callback.call( this.testEnvironment, this.assert );
+			this.resolvePromise( promise );
+			return;
+		}
+
+		try {
+			promise = this.callback.call( this.testEnvironment, this.assert );
+			this.resolvePromise( promise );
+		} catch ( e ) {
+			this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
+				this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
+
+			// else next test will carry the responsibility
+			saveGlobal();
+
+			// Restart the tests if they're blocking
+			if ( config.blocking ) {
+				QUnit.start();
+			}
+		}
+	},
+
+	after: function() {
+		checkPollution();
+	},
+
+	queueHook: function( hook, hookName ) {
+		var promise,
+			test = this;
+		return function runHook() {
+			config.current = test;
+			if ( config.notrycatch ) {
+				promise = hook.call( test.testEnvironment, test.assert );
+				test.resolvePromise( promise, hookName );
+				return;
+			}
+			try {
+				promise = hook.call( test.testEnvironment, test.assert );
+				test.resolvePromise( promise, hookName );
+			} catch ( error ) {
+				test.pushFailure( hookName + " failed on " + test.testName + ": " +
+					( error.message || error ), extractStacktrace( error, 0 ) );
+			}
+		};
+	},
+
+	// Currently only used for module level hooks, can be used to add global level ones
+	hooks: function( handler ) {
+		var hooks = [];
+
+		// Hooks are ignored on skipped tests
+		if ( this.skip ) {
+			return hooks;
+		}
+
+		if ( this.module.testEnvironment &&
+				QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) {
+			hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) );
+		}
+
+		return hooks;
+	},
+
+	finish: function() {
+		config.current = this;
+		if ( config.requireExpects && this.expected === null ) {
+			this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
+				"not called.", this.stack );
+		} else if ( this.expected !== null && this.expected !== this.assertions.length ) {
+			this.pushFailure( "Expected " + this.expected + " assertions, but " +
+				this.assertions.length + " were run", this.stack );
+		} else if ( this.expected === null && !this.assertions.length ) {
+			this.pushFailure( "Expected at least one assertion, but none were run - call " +
+				"expect(0) to accept zero assertions.", this.stack );
+		}
+
+		var i,
+			bad = 0;
+
+		this.runtime = now() - this.started;
+		config.stats.all += this.assertions.length;
+		config.moduleStats.all += this.assertions.length;
+
+		for ( i = 0; i < this.assertions.length; i++ ) {
+			if ( !this.assertions[ i ].result ) {
+				bad++;
+				config.stats.bad++;
+				config.moduleStats.bad++;
+			}
+		}
+
+		runLoggingCallbacks( "testDone", {
+			name: this.testName,
+			module: this.module.name,
+			skipped: !!this.skip,
+			failed: bad,
+			passed: this.assertions.length - bad,
+			total: this.assertions.length,
+			runtime: this.runtime,
+
+			// HTML Reporter use
+			assertions: this.assertions,
+			testId: this.testId,
+
+			// DEPRECATED: this property will be removed in 2.0.0, use runtime instead
+			duration: this.runtime
+		});
+
+		// QUnit.reset() is deprecated and will be replaced for a new
+		// fixture reset function on QUnit 2.0/2.1.
+		// It's still called here for backwards compatibility handling
+		QUnit.reset();
+
+		config.current = undefined;
+	},
+
+	queue: function() {
+		var bad,
+			test = this;
+
+		if ( !this.valid() ) {
+			return;
+		}
+
+		function run() {
+
+			// each of these can by async
+			synchronize([
+				function() {
+					test.before();
+				},
+
+				test.hooks( "beforeEach" ),
+
+				function() {
+					test.run();
+				},
+
+				test.hooks( "afterEach" ).reverse(),
+
+				function() {
+					test.after();
+				},
+				function() {
+					test.finish();
+				}
+			]);
+		}
+
+		// `bad` initialized at top of scope
+		// defer when previous test run passed, if storage is available
+		bad = QUnit.config.reorder && defined.sessionStorage &&
+				+sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
+
+		if ( bad ) {
+			run();
+		} else {
+			synchronize( run, true );
+		}
+	},
+
+	push: function( result, actual, expected, message ) {
+		var source,
+			details = {
+				module: this.module.name,
+				name: this.testName,
+				result: result,
+				message: message,
+				actual: actual,
+				expected: expected,
+				testId: this.testId,
+				runtime: now() - this.started
+			};
+
+		if ( !result ) {
+			source = sourceFromStacktrace();
+
+			if ( source ) {
+				details.source = source;
+			}
+		}
+
+		runLoggingCallbacks( "log", details );
+
+		this.assertions.push({
+			result: !!result,
+			message: message
+		});
+	},
+
+	pushFailure: function( message, source, actual ) {
+		if ( !this instanceof Test ) {
+			throw new Error( "pushFailure() assertion outside test context, was " +
+				sourceFromStacktrace( 2 ) );
+		}
+
+		var details = {
+				module: this.module.name,
+				name: this.testName,
+				result: false,
+				message: message || "error",
+				actual: actual || null,
+				testId: this.testId,
+				runtime: now() - this.started
+			};
+
+		if ( source ) {
+			details.source = source;
+		}
+
+		runLoggingCallbacks( "log", details );
+
+		this.assertions.push({
+			result: false,
+			message: message
+		});
+	},
+
+	resolvePromise: function( promise, phase ) {
+		var then, message,
+			test = this;
+		if ( promise != null ) {
+			then = promise.then;
+			if ( QUnit.objectType( then ) === "function" ) {
+				QUnit.stop();
+				then.call(
+					promise,
+					QUnit.start,
+					function( error ) {
+						message = "Promise rejected " +
+							( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
+							" " + test.testName + ": " + ( error.message || error );
+						test.pushFailure( message, extractStacktrace( error, 0 ) );
+
+						// else next test will carry the responsibility
+						saveGlobal();
+
+						// Unblock
+						QUnit.start();
+					}
+				);
+			}
+		}
+	},
+
+	valid: function() {
+		var include,
+			filter = config.filter && config.filter.toLowerCase(),
+			module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
+			fullName = ( this.module.name + ": " + this.testName ).toLowerCase();
+
+		// Internally-generated tests are always valid
+		if ( this.callback && this.callback.validTest ) {
+			return true;
+		}
+
+		if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) {
+			return false;
+		}
+
+		if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) {
+			return false;
+		}
+
+		if ( !filter ) {
+			return true;
+		}
+
+		include = filter.charAt( 0 ) !== "!";
+		if ( !include ) {
+			filter = filter.slice( 1 );
+		}
+
+		// If the filter matches, we need to honour include
+		if ( fullName.indexOf( filter ) !== -1 ) {
+			return include;
+		}
+
+		// Otherwise, do the opposite
+		return !include;
+	}
+
+};
+
+// Resets the test setup. Useful for tests that modify the DOM.
+/*
+DEPRECATED: Use multiple tests instead of resetting inside a test.
+Use testStart or testDone for custom cleanup.
+This method will throw an error in 2.0, and will be removed in 2.1
+*/
+QUnit.reset = function() {
+
+	// Return on non-browser environments
+	// This is necessary to not break on node tests
+	if ( typeof window === "undefined" ) {
+		return;
+	}
+
+	var fixture = defined.document && document.getElementById &&
+			document.getElementById( "qunit-fixture" );
+
+	if ( fixture ) {
+		fixture.innerHTML = config.fixture;
+	}
+};
+
+QUnit.pushFailure = function() {
+	if ( !QUnit.config.current ) {
+		throw new Error( "pushFailure() assertion outside test context, in " +
+			sourceFromStacktrace( 2 ) );
+	}
+
+	// Gets current test obj
+	var currentTest = QUnit.config.current;
+
+	return currentTest.pushFailure.apply( currentTest, arguments );
+};
+
+// Based on Java's String.hashCode, a simple but not
+// rigorously collision resistant hashing function
+function generateHash( module, testName ) {
+	var hex,
+		i = 0,
+		hash = 0,
+		str = module + "\x1C" + testName,
+		len = str.length;
+
+	for ( ; i < len; i++ ) {
+		hash  = ( ( hash << 5 ) - hash ) + str.charCodeAt( i );
+		hash |= 0;
+	}
+
+	// Convert the possibly negative integer hash code into an 8 character hex string, which isn't
+	// strictly necessary but increases user understanding that the id is a SHA-like hash
+	hex = ( 0x100000000 + hash ).toString( 16 );
+	if ( hex.length < 8 ) {
+		hex = "0000000" + hex;
+	}
+
+	return hex.slice( -8 );
 }
 
+function Assert( testContext ) {
+	this.test = testContext;
+}
+
+// Assert helpers
+QUnit.assert = Assert.prototype = {
+
+	// Specify the number of expected assertions to guarantee that failed test
+	// (no assertions are run at all) don't slip through.
+	expect: function( asserts ) {
+		if ( arguments.length === 1 ) {
+			this.test.expected = asserts;
+		} else {
+			return this.test.expected;
+		}
+	},
+
+	// Increment this Test's semaphore counter, then return a single-use function that
+	// decrements that counter a maximum of once.
+	async: function() {
+		var test = this.test,
+			popped = false;
+
+		test.semaphore += 1;
+		test.usedAsync = true;
+		pauseProcessing();
+
+		return function done() {
+			if ( !popped ) {
+				test.semaphore -= 1;
+				popped = true;
+				resumeProcessing();
+			} else {
+				test.pushFailure( "Called the callback returned from `assert.async` more than once",
+					sourceFromStacktrace( 2 ) );
+			}
+		};
+	},
+
+	// Exports test.push() to the user API
+	push: function( /* result, actual, expected, message */ ) {
+		var assert = this,
+			currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
+
+		// Backwards compatibility fix.
+		// Allows the direct use of global exported assertions and QUnit.assert.*
+		// Although, it's use is not recommended as it can leak assertions
+		// to other tests from async tests, because we only get a reference to the current test,
+		// not exactly the test where assertion were intended to be called.
+		if ( !currentTest ) {
+			throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) );
+		}
+
+		if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) {
+			currentTest.pushFailure( "Assertion after the final `assert.async` was resolved",
+				sourceFromStacktrace( 2 ) );
+
+			// Allow this assertion to continue running anyway...
+		}
+
+		if ( !( assert instanceof Assert ) ) {
+			assert = currentTest.assert;
+		}
+		return assert.test.push.apply( assert.test, arguments );
+	},
+
+	ok: function( result, message ) {
+		message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " +
+			QUnit.dump.parse( result ) );
+		this.push( !!result, result, true, message );
+	},
+
+	notOk: function( result, message ) {
+		message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " +
+			QUnit.dump.parse( result ) );
+		this.push( !result, result, false, message );
+	},
+
+	equal: function( actual, expected, message ) {
+		/*jshint eqeqeq:false */
+		this.push( expected == actual, actual, expected, message );
+	},
+
+	notEqual: function( actual, expected, message ) {
+		/*jshint eqeqeq:false */
+		this.push( expected != actual, actual, expected, message );
+	},
+
+	propEqual: function( actual, expected, message ) {
+		actual = objectValues( actual );
+		expected = objectValues( expected );
+		this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+	},
+
+	notPropEqual: function( actual, expected, message ) {
+		actual = objectValues( actual );
+		expected = objectValues( expected );
+		this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+	},
+
+	deepEqual: function( actual, expected, message ) {
+		this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+	},
+
+	notDeepEqual: function( actual, expected, message ) {
+		this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+	},
+
+	strictEqual: function( actual, expected, message ) {
+		this.push( expected === actual, actual, expected, message );
+	},
+
+	notStrictEqual: function( actual, expected, message ) {
+		this.push( expected !== actual, actual, expected, message );
+	},
+
+	"throws": function( block, expected, message ) {
+		var actual, expectedType,
+			expectedOutput = expected,
+			ok = false,
+			currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current;
+
+		// 'expected' is optional unless doing string comparison
+		if ( message == null && typeof expected === "string" ) {
+			message = expected;
+			expected = null;
+		}
+
+		currentTest.ignoreGlobalErrors = true;
+		try {
+			block.call( currentTest.testEnvironment );
+		} catch (e) {
+			actual = e;
+		}
+		currentTest.ignoreGlobalErrors = false;
+
+		if ( actual ) {
+			expectedType = QUnit.objectType( expected );
+
+			// we don't want to validate thrown error
+			if ( !expected ) {
+				ok = true;
+				expectedOutput = null;
+
+			// expected is a regexp
+			} else if ( expectedType === "regexp" ) {
+				ok = expected.test( errorString( actual ) );
+
+			// expected is a string
+			} else if ( expectedType === "string" ) {
+				ok = expected === errorString( actual );
+
+			// expected is a constructor, maybe an Error constructor
+			} else if ( expectedType === "function" && actual instanceof expected ) {
+				ok = true;
+
+			// expected is an Error object
+			} else if ( expectedType === "object" ) {
+				ok = actual instanceof expected.constructor &&
+					actual.name === expected.name &&
+					actual.message === expected.message;
+
+			// expected is a validation function which returns true if validation passed
+			} else if ( expectedType === "function" && expected.call( {}, actual ) === true ) {
+				expectedOutput = null;
+				ok = true;
+			}
+		}
+
+		currentTest.assert.push( ok, actual, expectedOutput, message );
+	}
+};
+
+// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word
+// Known to us are: Closure Compiler, Narwhal
+(function() {
+	/*jshint sub:true */
+	Assert.prototype.raises = Assert.prototype[ "throws" ];
+}());
+
 // 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";
+QUnit.equiv = (function() {
+
+	// Call the o related callback with the given arguments.
+	function bindCallbacks( o, callbacks, args ) {
+		var prop = QUnit.objectType( o );
+		if ( prop ) {
+			if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
+				return callbacks[ prop ].apply( callbacks, args );
+			} else {
+				return callbacks[ prop ]; // or undefined
+			}
+		}
+	}
+
+	// the real equiv function
+	var innerEquiv,
+
+		// stack to decide between skip/abort functions
+		callers = [],
+
+		// stack to avoiding loops from circular referencing
+		parents = [],
+		parentsB = [],
+
+		getProto = Object.getPrototypeOf || function( obj ) {
+			/* jshint camelcase: false, proto: true */
+			return obj.__proto__;
+		},
+		callbacks = (function() {
+
+			// for string, boolean, number and null
+			function useStrictEquality( b, a ) {
+
+				/*jshint eqeqeq:false */
+				if ( b instanceof a.constructor || a instanceof b.constructor ) {
+
+					// to catch short annotation 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 QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
+				},
+
+				"regexp": function( b, a ) {
+					return QUnit.objectType( b ) === "regexp" &&
+
+						// the regex itself
+						a.source === b.source &&
+
+						// and its modifiers
+						a.global === b.global &&
+
+						// (gmi) ...
+						a.ignoreCase === b.ignoreCase &&
+						a.multiline === b.multiline &&
+						a.sticky === b.sticky;
+				},
+
+				// - 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, len, loop, aCircular, bCircular;
+
+					// b could be an object literal here
+					if ( QUnit.objectType( 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 );
+					parentsB.push( b );
+					for ( i = 0; i < len; i++ ) {
+						loop = false;
+						for ( j = 0; j < parents.length; j++ ) {
+							aCircular = parents[ j ] === a[ i ];
+							bCircular = parentsB[ j ] === b[ i ];
+							if ( aCircular || bCircular ) {
+								if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
+									loop = true;
+								} else {
+									parents.pop();
+									parentsB.pop();
+									return false;
+								}
+							}
+						}
+						if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
+							parents.pop();
+							parentsB.pop();
+							return false;
+						}
+					}
+					parents.pop();
+					parentsB.pop();
+					return true;
+				},
+
+				"object": function( b, a ) {
+
+					/*jshint forin:false */
+					var i, j, loop, aCircular, bCircular,
+						// Default to true
+						eq = true,
+						aProperties = [],
+						bProperties = [];
+
+					// comparing constructors is more strict than using
+					// instanceof
+					if ( a.constructor !== b.constructor ) {
+
+						// Allow objects with no prototype to be equivalent to
+						// objects with Object as their constructor.
+						if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) ||
+							( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) {
+							return false;
+						}
+					}
+
+					// stack constructor before traversing properties
+					callers.push( a.constructor );
+
+					// track reference to avoid circular references
+					parents.push( a );
+					parentsB.push( b );
+
+					// be strict: don't ensure hasOwnProperty and go deep
+					for ( i in a ) {
+						loop = false;
+						for ( j = 0; j < parents.length; j++ ) {
+							aCircular = parents[ j ] === a[ i ];
+							bCircular = parentsB[ j ] === b[ i ];
+							if ( aCircular || bCircular ) {
+								if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
+									loop = true;
+								} else {
+									eq = false;
+									break;
+								}
+							}
+						}
+						aProperties.push( i );
+						if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
+							eq = false;
+							break;
+						}
+					}
+
+					parents.pop();
+					parentsB.pop();
+					callers.pop(); // unstack, we are done
+
+					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 = [].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" ||
+					QUnit.objectType( a ) !== QUnit.objectType( b ) ) {
+
+				// don't lose time with error prone cases
+				return false;
+			} else {
+				return bindCallbacks( a, callbacks, [ b, a ] );
+			}
+
+			// apply transition with (1..n) arguments
+		}( args[ 0 ], args[ 1 ] ) ) &&
+			innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) );
+	};
+
+	return innerEquiv;
+}());
+
+// Based on jsDump by Ariel Flesler
+// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
+QUnit.dump = (function() {
+	function quote( str ) {
+		return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
+	}
+	function literal( o ) {
+		return o + "";
+	}
+	function join( pre, arr, post ) {
+		var s = dump.separator(),
+			base = dump.indent(),
+			inner = dump.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, stack ) {
+		var i = arr.length,
+			ret = new Array( i );
+
+		if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
+			return "[object Array]";
+		}
+
+		this.up();
+		while ( i-- ) {
+			ret[ i ] = this.parse( arr[ i ], undefined, stack );
+		}
+		this.down();
+		return join( "[", ret, "]" );
+	}
+
+	var reName = /^function (\w+)/,
+		dump = {
+
+			// objType is used mostly internally, you can fix a (custom) type in advance
+			parse: function( obj, objType, stack ) {
+				stack = stack || [];
+				var res, parser, parserType,
+					inStack = inArray( obj, stack );
+
+				if ( inStack !== -1 ) {
+					return "recursion(" + ( inStack - stack.length ) + ")";
+				}
+
+				objType = objType || this.typeOf( obj  );
+				parser = this.parsers[ objType ];
+				parserType = typeof parser;
+
+				if ( parserType === "function" ) {
+					stack.push( obj );
+					res = parser.call( this, obj, stack );
+					stack.pop();
+					return res;
+				}
+				return ( parserType === "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 !== undefined &&
+						obj.document !== undefined &&
+						obj.nodeType === undefined ) {
+					type = "window";
+				} else if ( obj.nodeType === 9 ) {
+					type = "document";
+				} else if ( obj.nodeType ) {
+					type = "node";
+				} else if (
+
+					// native arrays
+					toString.call( obj ) === "[object Array]" ||
+
+					// NodeList objects
+					( typeof obj.length === "number" && obj.item !== undefined &&
+					( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null &&
+					obj[ 0 ] === undefined ) ) )
+				) {
+					type = "array";
+				} else if ( obj.constructor === Error.prototype.constructor ) {
+					type = "error";
+				} else {
+					type = typeof obj;
+				}
+				return type;
+			},
+			separator: function() {
+				return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? "&#160;" : " ";
+			},
+			// extra can be a number, shortcut for increasing-calling-decreasing
+			indent: function( extra ) {
+				if ( !this.multiline ) {
+					return "";
+				}
+				var chr = this.indentChar;
+				if ( this.HTML ) {
+					chr = chr.replace( /\t/g, "   " ).replace( / /g, "&#160;" );
+				}
+				return new 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,
+			maxDepth: QUnit.config.maxDepth,
+
+			// This is the list of parsers, to modify them, use dump.setParser
+			parsers: {
+				window: "[Window]",
+				document: "[Document]",
+				error: function( error ) {
+					return "Error(\"" + error.message + "\")";
+				},
+				unknown: "[Unknown]",
+				"null": "null",
+				"undefined": "undefined",
+				"function": function( fn ) {
+					var ret = "function",
+
+						// functions never have name in IE
+						name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ];
+
+					if ( name ) {
+						ret += " " + name;
+					}
+					ret += "( ";
+
+					ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" );
+					return join( ret, dump.parse( fn, "functionCode" ), "}" );
+				},
+				array: array,
+				nodelist: array,
+				"arguments": array,
+				object: function( map, stack ) {
+					var keys, key, val, i, nonEnumerableProperties,
+						ret = [];
+
+					if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
+						return "[object Object]";
+					}
+
+					dump.up();
+					keys = [];
+					for ( key in map ) {
+						keys.push( key );
+					}
+
+					// Some properties are not always enumerable on Error objects.
+					nonEnumerableProperties = [ "message", "name" ];
+					for ( i in nonEnumerableProperties ) {
+						key = nonEnumerableProperties[ i ];
+						if ( key in map && inArray( key, keys ) < 0 ) {
+							keys.push( key );
+						}
+					}
+					keys.sort();
+					for ( i = 0; i < keys.length; i++ ) {
+						key = keys[ i ];
+						val = map[ key ];
+						ret.push( dump.parse( key, "key" ) + ": " +
+							dump.parse( val, undefined, stack ) );
+					}
+					dump.down();
+					return join( "{", ret, "}" );
+				},
+				node: function( node ) {
+					var len, i, val,
+						open = dump.HTML ? "&lt;" : "<",
+						close = dump.HTML ? "&gt;" : ">",
+						tag = node.nodeName.toLowerCase(),
+						ret = open + tag,
+						attrs = node.attributes;
+
+					if ( attrs ) {
+						for ( i = 0, len = attrs.length; i < len; i++ ) {
+							val = attrs[ i ].nodeValue;
+
+							// IE6 includes all attributes in .attributes, even ones not explicitly
+							// set. Those have values like undefined, null, 0, false, "" or
+							// "inherit".
+							if ( val && val !== "inherit" ) {
+								ret += " " + attrs[ i ].nodeName + "=" +
+									dump.parse( val, "attribute" );
+							}
+						}
+					}
+					ret += close;
+
+					// Show content of TextNode or CDATASection
+					if ( node.nodeType === 3 || node.nodeType === 4 ) {
+						ret += node.nodeValue;
+					}
+
+					return ret + open + "/" + tag + close;
+				},
+
+				// function calls it internally, it's the arguments part of the function
+				functionArgs: function( fn ) {
+					var args,
+						l = fn.length;
+
+					if ( !l ) {
+						return "";
+					}
+
+					args = new Array( l );
+					while ( l-- ) {
+
+						// 97 is 'a'
+						args[ l ] = String.fromCharCode( 97 + l );
+					}
+					return " " + args.join( ", " ) + " ";
+				},
+				// object calls it internally, the key part of an item in a map
+				key: quote,
+				// function calls it internally, it's the content of the function
+				functionCode: "[code]",
+				// node calls it internally, it's an html attribute value
+				attribute: quote,
+				string: quote,
+				date: quote,
+				regexp: literal,
+				number: literal,
+				"boolean": literal
+			},
+			// if true, entities are escaped ( <, >, \t, space and \n )
+			HTML: false,
+			// indentation unit
+			indentChar: "  ",
+			// if true, items in a collection, are separated by a \n, else just a space.
+			multiline: true
+		};
+
+	return dump;
+}());
+
+// back compat
+QUnit.jsDump = QUnit.dump;
+
+// For browser, export only select globals
+if ( typeof window !== "undefined" ) {
+
+	// Deprecated
+	// Extend assert methods to QUnit and Global scope through Backwards compatibility
+	(function() {
+		var i,
+			assertions = Assert.prototype;
+
+		function applyCurrent( current ) {
+			return function() {
+				var assert = new Assert( QUnit.config.current );
+				current.apply( assert, arguments );
+			};
+		}
+
+		for ( i in assertions ) {
+			QUnit[ i ] = applyCurrent( assertions[ i ] );
+		}
+	})();
+
+	(function() {
+		var i, l,
+			keys = [
+				"test",
+				"module",
+				"expect",
+				"asyncTest",
+				"start",
+				"stop",
+				"ok",
+				"notOk",
+				"equal",
+				"notEqual",
+				"propEqual",
+				"notPropEqual",
+				"deepEqual",
+				"notDeepEqual",
+				"strictEqual",
+				"notStrictEqual",
+				"throws"
+			];
+
+		for ( i = 0, l = keys.length; i < l; i++ ) {
+			window[ keys[ i ] ] = QUnit[ keys[ i ] ];
+		}
+	})();
+
+	window.QUnit = QUnit;
+}
+
+// For nodejs
+if ( typeof module !== "undefined" && module && module.exports ) {
+	module.exports = QUnit;
+
+	// For consistency with CommonJS environments' exports
+	module.exports.QUnit = QUnit;
+}
+
+// For CommonJS with exports, but without module.exports, like Rhino
+if ( typeof exports !== "undefined" && exports ) {
+	exports.QUnit = QUnit;
+}
+
+if ( typeof define === "function" && define.amd ) {
+	define( function() {
+		return QUnit;
+	} );
+	QUnit.config.autostart = false;
+}
+
+// Get a reference to the global object, like window in browsers
+}( (function() {
+	return this;
+})() ));
+
+/*istanbul ignore next */
+// jscs:disable maximumLineLength
+/*
+ * This file is a modified version of google-diff-match-patch's JavaScript implementation
+ * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js),
+ * modifications are licensed as more fully set forth in LICENSE.txt.
+ *
+ * The original source of google-diff-match-patch is attributable and licensed as follows:
+ *
+ * Copyright 2006 Google Inc.
+ * http://code.google.com/p/google-diff-match-patch/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * More Info:
+ *  https://code.google.com/p/google-diff-match-patch/
+ *
+ * Usage: QUnit.diff(expected, actual)
+ *
+ * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) === "the  quick <del>brown </del> fox jump<ins>s</ins><del>ed</del over"
+ */
+QUnit.diff = (function() {
+
+    function DiffMatchPatch() {
+
+        // Defaults.
+        // Redefine these in your program to override the defaults.
+
+        // Number of seconds to map a diff before giving up (0 for infinity).
+        this.DiffTimeout = 1.0;
+        // Cost of an empty edit operation in terms of edit characters.
+        this.DiffEditCost = 4;
+    }
+
+    //  DIFF FUNCTIONS
+
+    /**
+     * The data structure representing a diff is an array of tuples:
+     * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
+     * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
+     */
+    var DIFF_DELETE = -1,
+		DIFF_INSERT = 1,
+		DIFF_EQUAL = 0;
+
+    /**
+     * Find the differences between two texts.  Simplifies the problem by stripping
+     * any common prefix or suffix off the texts before diffing.
+     * @param {string} text1 Old string to be diffed.
+     * @param {string} text2 New string to be diffed.
+     * @param {boolean=} optChecklines Optional speedup flag. If present and false,
+     *     then don't run a line-level diff first to identify the changed areas.
+     *     Defaults to true, which does a faster, slightly less optimal diff.
+     * @param {number} optDeadline Optional time when the diff should be complete
+     *     by.  Used internally for recursive calls.  Users should set DiffTimeout
+     *     instead.
+     * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+     */
+    DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) {
+        var deadline, checklines, commonlength,
+			commonprefix, commonsuffix, diffs;
+        // Set a deadline by which time the diff must be complete.
+        if ( typeof optDeadline === "undefined" ) {
+            if ( this.DiffTimeout <= 0 ) {
+                optDeadline = Number.MAX_VALUE;
             } 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
+                optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000;
             }
         }
-    }
-    
-    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;
+        deadline = optDeadline;
+
+        // Check for null inputs.
+        if ( text1 === null || text2 === null ) {
+            throw new Error( "Null input. (DiffMain)" );
+        }
+
+        // Check for equality (speedup).
+        if ( text1 === text2 ) {
+            if ( text1 ) {
+                return [
+                    [ DIFF_EQUAL, text1 ]
+                ];
+            }
+            return [];
+        }
+
+        if ( typeof optChecklines === "undefined" ) {
+            optChecklines = true;
+        }
+
+        checklines = optChecklines;
+
+        // Trim off common prefix (speedup).
+        commonlength = this.diffCommonPrefix( text1, text2 );
+        commonprefix = text1.substring( 0, commonlength );
+        text1 = text1.substring( commonlength );
+        text2 = text2.substring( commonlength );
+
+        // Trim off common suffix (speedup).
+        /////////
+        commonlength = this.diffCommonSuffix( text1, text2 );
+        commonsuffix = text1.substring( text1.length - commonlength );
+        text1 = text1.substring( 0, text1.length - commonlength );
+        text2 = text2.substring( 0, text2.length - commonlength );
+
+        // Compute the diff on the middle block.
+        diffs = this.diffCompute( text1, text2, checklines, deadline );
+
+        // Restore the prefix and suffix.
+        if ( commonprefix ) {
+            diffs.unshift( [ DIFF_EQUAL, commonprefix ] );
+        }
+        if ( commonsuffix ) {
+            diffs.push( [ DIFF_EQUAL, commonsuffix ] );
+        }
+        this.diffCleanupMerge( diffs );
+        return diffs;
+    };
+
+    /**
+     * Reduce the number of edits by eliminating operationally trivial equalities.
+     * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+     */
+    DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) {
+        var changes, equalities, equalitiesLength, lastequality,
+			pointer, preIns, preDel, postIns, postDel;
+        changes = false;
+        equalities = []; // Stack of indices where equalities are found.
+        equalitiesLength = 0; // Keeping our own length var is faster in JS.
+        /** @type {?string} */
+        lastequality = null;
+        // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+        pointer = 0; // Index of current position.
+        // Is there an insertion operation before the last equality.
+        preIns = false;
+        // Is there a deletion operation before the last equality.
+        preDel = false;
+        // Is there an insertion operation after the last equality.
+        postIns = false;
+        // Is there a deletion operation after the last equality.
+        postDel = false;
+        while ( pointer < diffs.length ) {
+            if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found.
+                if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) {
+                    // Candidate found.
+                    equalities[ equalitiesLength++ ] = pointer;
+                    preIns = postIns;
+                    preDel = postDel;
+                    lastequality = diffs[ pointer ][ 1 ];
+                } else {
+                    // Not a candidate, and can never become one.
+                    equalitiesLength = 0;
+                    lastequality = null;
+                }
+                postIns = postDel = false;
+            } else { // An insertion or deletion.
+                if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) {
+                    postDel = true;
+                } else {
+                    postIns = true;
+                }
+                /*
+                 * Five types to be split:
+                 * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
+                 * <ins>A</ins>X<ins>C</ins><del>D</del>
+                 * <ins>A</ins><del>B</del>X<ins>C</ins>
+                 * <ins>A</del>X<ins>C</ins><del>D</del>
+                 * <ins>A</ins><del>B</del>X<del>C</del>
+                 */
+                if ( lastequality && ( ( preIns && preDel && postIns && postDel ) ||
+                        ( ( lastequality.length < this.DiffEditCost / 2 ) &&
+                            ( preIns + preDel + postIns + postDel ) === 3 ) ) ) {
+                    // Duplicate record.
+                    diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] );
+                    // Change second copy to insert.
+                    diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
+                    equalitiesLength--; // Throw away the equality we just deleted;
+                    lastequality = null;
+                    if (preIns && preDel) {
+                        // No changes made which could affect previous entry, keep going.
+                        postIns = postDel = true;
+                        equalitiesLength = 0;
+                    } else {
+                        equalitiesLength--; // Throw away the previous equality.
+                        pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
+                        postIns = postDel = false;
+                    }
+                    changes = true;
+                }
+            }
+            pointer++;
+        }
+
+        if ( changes ) {
+            this.diffCleanupMerge( diffs );
+        }
+    };
+
+    /**
+     * Convert a diff array into a pretty HTML report.
+     * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+     * @param {integer} string to be beautified.
+     * @return {string} HTML representation.
+     */
+    DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) {
+        var op, data, x, html = [];
+        for ( x = 0; x < diffs.length; x++ ) {
+            op = diffs[x][0]; // Operation (insert, delete, equal)
+            data = diffs[x][1]; // Text of change.
+            switch ( op ) {
+                case DIFF_INSERT:
+                    html[x] = "<ins>" + data + "</ins>";
+                    break;
+                case DIFF_DELETE:
+                    html[x] = "<del>" + data + "</del>";
+                    break;
+                case DIFF_EQUAL:
+                    html[x] = "<span>" + data + "</span>";
+                    break;
             }
         }
-
-        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;
+        return html.join("");
+    };
+
+    /**
+     * Determine the common prefix of two strings.
+     * @param {string} text1 First string.
+     * @param {string} text2 Second string.
+     * @return {number} The number of characters common to the start of each
+     *     string.
+     */
+    DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) {
+        var pointermid, pointermax, pointermin, pointerstart;
+        // Quick check for common null cases.
+        if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) {
+            return 0;
+        }
+        // Binary search.
+        // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+        pointermin = 0;
+        pointermax = Math.min( text1.length, text2.length );
+        pointermid = pointermax;
+        pointerstart = 0;
+        while ( pointermin < pointermid ) {
+            if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) {
+                pointermin = pointermid;
+                pointerstart = pointermin;
+            } else {
+                pointermax = pointermid;
+            }
+            pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
+        }
+        return pointermid;
+    };
+
+    /**
+     * Determine the common suffix of two strings.
+     * @param {string} text1 First string.
+     * @param {string} text2 Second string.
+     * @return {number} The number of characters common to the end of each string.
+     */
+    DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) {
+        var pointermid, pointermax, pointermin, pointerend;
+        // Quick check for common null cases.
+        if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) {
+            return 0;
+        }
+        // Binary search.
+        // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+        pointermin = 0;
+        pointermax = Math.min(text1.length, text2.length);
+        pointermid = pointermax;
+        pointerend = 0;
+        while ( pointermin < pointermid ) {
+            if (text1.substring( text1.length - pointermid, text1.length - pointerend ) ===
+                text2.substring( text2.length - pointermid, text2.length - pointerend ) ) {
+                pointermin = pointermid;
+                pointerend = pointermin;
+            } else {
+                pointermax = pointermid;
+            }
+            pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
+        }
+        return pointermid;
+    };
+
+    /**
+     * Find the differences between two texts.  Assumes that the texts do not
+     * have any common prefix or suffix.
+     * @param {string} text1 Old string to be diffed.
+     * @param {string} text2 New string to be diffed.
+     * @param {boolean} checklines Speedup flag.  If false, then don't run a
+     *     line-level diff first to identify the changed areas.
+     *     If true, then run a faster, slightly less optimal diff.
+     * @param {number} deadline Time when the diff should be complete by.
+     * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) {
+        var diffs, longtext, shorttext, i, hm,
+			text1A, text2A, text1B, text2B,
+			midCommon, diffsA, diffsB;
+
+        if ( !text1 ) {
+            // Just add some text (speedup).
+            return [
+                [ DIFF_INSERT, text2 ]
+            ];
+        }
+
+        if (!text2) {
+            // Just delete some text (speedup).
+            return [
+                [ DIFF_DELETE, text1 ]
+            ];
+        }
+
+        longtext = text1.length > text2.length ? text1 : text2;
+        shorttext = text1.length > text2.length ? text2 : text1;
+        i = longtext.indexOf( shorttext );
+        if ( i !== -1 ) {
+            // Shorter text is inside the longer text (speedup).
+            diffs = [
+                [ DIFF_INSERT, longtext.substring( 0, i ) ],
+                [ DIFF_EQUAL, shorttext ],
+                [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ]
+            ];
+            // Swap insertions for deletions if diff is reversed.
+            if ( text1.length > text2.length ) {
+                diffs[0][0] = diffs[2][0] = DIFF_DELETE;
+            }
+            return diffs;
+        }
+
+        if ( shorttext.length === 1 ) {
+            // Single character string.
+            // After the previous speedup, the character can't be an equality.
+            return [
+                [ DIFF_DELETE, text1 ],
+                [ DIFF_INSERT, text2 ]
+            ];
+        }
+
+        // Check to see if the problem can be split in two.
+        hm = this.diffHalfMatch(text1, text2);
+        if (hm) {
+            // A half-match was found, sort out the return data.
+            text1A = hm[0];
+            text1B = hm[1];
+            text2A = hm[2];
+            text2B = hm[3];
+            midCommon = hm[4];
+            // Send both pairs off for separate processing.
+            diffsA = this.DiffMain(text1A, text2A, checklines, deadline);
+            diffsB = this.DiffMain(text1B, text2B, checklines, deadline);
+            // Merge the results.
+            return diffsA.concat([
+                [ DIFF_EQUAL, midCommon ]
+            ], diffsB);
+        }
+
+        if (checklines && text1.length > 100 && text2.length > 100) {
+            return this.diffLineMode(text1, text2, deadline);
+        }
+
+        return this.diffBisect(text1, text2, deadline);
+    };
+
+    /**
+     * Do the two texts share a substring which is at least half the length of the
+     * longer text?
+     * This speedup can produce non-minimal diffs.
+     * @param {string} text1 First string.
+     * @param {string} text2 Second string.
+     * @return {Array.<string>} Five element Array, containing the prefix of
+     *     text1, the suffix of text1, the prefix of text2, the suffix of
+     *     text2 and the common middle.  Or null if there was no match.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) {
+        var longtext, shorttext, dmp,
+			text1A, text2B, text2A, text1B, midCommon,
+			hm1, hm2, hm;
+        if (this.DiffTimeout <= 0) {
+            // Don't risk returning a non-optimal diff if we have unlimited time.
+            return null;
+        }
+        longtext = text1.length > text2.length ? text1 : text2;
+        shorttext = text1.length > text2.length ? text2 : text1;
+        if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
+            return null; // Pointless.
+        }
+        dmp = this; // 'this' becomes 'window' in a closure.
+
+        /**
+         * Does a substring of shorttext exist within longtext such that the substring
+         * is at least half the length of longtext?
+         * Closure, but does not reference any external variables.
+         * @param {string} longtext Longer string.
+         * @param {string} shorttext Shorter string.
+         * @param {number} i Start index of quarter length substring within longtext.
+         * @return {Array.<string>} Five element Array, containing the prefix of
+         *     longtext, the suffix of longtext, the prefix of shorttext, the suffix
+         *     of shorttext and the common middle.  Or null if there was no match.
+         * @private
+         */
+        function diffHalfMatchI(longtext, shorttext, i) {
+            var seed, j, bestCommon, prefixLength, suffixLength,
+				bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB;
+            // Start with a 1/4 length substring at position i as a seed.
+            seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
+            j = -1;
+            bestCommon = "";
+            while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
+                prefixLength = dmp.diffCommonPrefix(longtext.substring(i),
+                    shorttext.substring(j));
+                suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i),
+                    shorttext.substring(0, j));
+                if (bestCommon.length < suffixLength + prefixLength) {
+                    bestCommon = shorttext.substring(j - suffixLength, j) +
+                        shorttext.substring(j, j + prefixLength);
+                    bestLongtextA = longtext.substring(0, i - suffixLength);
+                    bestLongtextB = longtext.substring(i + prefixLength);
+                    bestShorttextA = shorttext.substring(0, j - suffixLength);
+                    bestShorttextB = shorttext.substring(j + prefixLength);
                 }
-                
-                //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 (bestCommon.length * 2 >= longtext.length) {
+                return [ bestLongtextA, bestLongtextB,
+                    bestShorttextA, bestShorttextB, bestCommon
+                ];
+            } else {
+                return null;
+            }
+        }
+
+        // First check if the second quarter is the seed for a half-match.
+        hm1 = diffHalfMatchI(longtext, shorttext,
+            Math.ceil(longtext.length / 4));
+        // Check again based on the third quarter.
+        hm2 = diffHalfMatchI(longtext, shorttext,
+            Math.ceil(longtext.length / 2));
+        if (!hm1 && !hm2) {
+            return null;
+        } else if (!hm2) {
+            hm = hm1;
+        } else if (!hm1) {
+            hm = hm2;
+        } else {
+            // Both matched.  Select the longest.
+            hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
+        }
+
+        // A half-match was found, sort out the return data.
+        text1A, text1B, text2A, text2B;
+        if (text1.length > text2.length) {
+            text1A = hm[0];
+            text1B = hm[1];
+            text2A = hm[2];
+            text2B = hm[3];
+        } else {
+            text2A = hm[0];
+            text2B = hm[1];
+            text1A = hm[2];
+            text1B = hm[3];
+        }
+        midCommon = hm[4];
+        return [ text1A, text1B, text2A, text2B, midCommon ];
+    };
+
+    /**
+     * Do a quick line-level diff on both strings, then rediff the parts for
+     * greater accuracy.
+     * This speedup can produce non-minimal diffs.
+     * @param {string} text1 Old string to be diffed.
+     * @param {string} text2 New string to be diffed.
+     * @param {number} deadline Time when the diff should be complete by.
+     * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) {
+        var a, diffs, linearray, pointer, countInsert,
+			countDelete, textInsert, textDelete, j;
+        // Scan the text on a line-by-line basis first.
+        a = this.diffLinesToChars(text1, text2);
+        text1 = a.chars1;
+        text2 = a.chars2;
+        linearray = a.lineArray;
+
+        diffs = this.DiffMain(text1, text2, false, deadline);
+
+        // Convert the diff back to original text.
+        this.diffCharsToLines(diffs, linearray);
+        // Eliminate freak matches (e.g. blank lines)
+        this.diffCleanupSemantic(diffs);
+
+        // Rediff any replacement blocks, this time character-by-character.
+        // Add a dummy entry at the end.
+        diffs.push( [ DIFF_EQUAL, "" ] );
+        pointer = 0;
+        countDelete = 0;
+        countInsert = 0;
+        textDelete = "";
+        textInsert = "";
+        while (pointer < diffs.length) {
+            switch ( diffs[pointer][0] ) {
+                case DIFF_INSERT:
+                    countInsert++;
+                    textInsert += diffs[pointer][1];
+                    break;
+                case DIFF_DELETE:
+                    countDelete++;
+                    textDelete += diffs[pointer][1];
+                    break;
+                case DIFF_EQUAL:
+                    // Upon reaching an equality, check for prior redundancies.
+                    if (countDelete >= 1 && countInsert >= 1) {
+                        // Delete the offending records and add the merged ones.
+                        diffs.splice(pointer - countDelete - countInsert,
+                            countDelete + countInsert);
+                        pointer = pointer - countDelete - countInsert;
+                        a = this.DiffMain(textDelete, textInsert, false, deadline);
+                        for (j = a.length - 1; j >= 0; j--) {
+                            diffs.splice( pointer, 0, a[j] );
                         }
+                        pointer = pointer + a.length;
                     }
-                    if (!loop && ! innerEquiv(a[i], b[i])) {
-                        parents.pop();
-                        return false;
-                    }
+                    countInsert = 0;
+                    countDelete = 0;
+                    textDelete = "";
+                    textInsert = "";
+                    break;
+            }
+            pointer++;
+        }
+        diffs.pop(); // Remove the dummy entry at the end.
+
+        return diffs;
+    };
+
+    /**
+     * Find the 'middle snake' of a diff, split the problem in two
+     * and return the recursively constructed diff.
+     * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+     * @param {string} text1 Old string to be diffed.
+     * @param {string} text2 New string to be diffed.
+     * @param {number} deadline Time at which to bail if not yet complete.
+     * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) {
+        var text1Length, text2Length, maxD, vOffset, vLength,
+			v1, v2, x, delta, front, k1start, k1end, k2start,
+			k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2;
+        // Cache the text lengths to prevent multiple calls.
+        text1Length = text1.length;
+        text2Length = text2.length;
+        maxD = Math.ceil((text1Length + text2Length) / 2);
+        vOffset = maxD;
+        vLength = 2 * maxD;
+        v1 = new Array(vLength);
+        v2 = new Array(vLength);
+        // Setting all elements to -1 is faster in Chrome & Firefox than mixing
+        // integers and undefined.
+        for (x = 0; x < vLength; x++) {
+            v1[x] = -1;
+            v2[x] = -1;
+        }
+        v1[vOffset + 1] = 0;
+        v2[vOffset + 1] = 0;
+        delta = text1Length - text2Length;
+        // If the total number of characters is odd, then the front path will collide
+        // with the reverse path.
+        front = (delta % 2 !== 0);
+        // Offsets for start and end of k loop.
+        // Prevents mapping of space beyond the grid.
+        k1start = 0;
+        k1end = 0;
+        k2start = 0;
+        k2end = 0;
+        for (d = 0; d < maxD; d++) {
+            // Bail out if deadline is reached.
+            if ((new Date()).getTime() > deadline) {
+                break;
+            }
+
+            // Walk the front path one step.
+            for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+                k1Offset = vOffset + k1;
+                if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) {
+                    x1 = v1[k1Offset + 1];
+                } else {
+                    x1 = v1[k1Offset - 1] + 1;
                 }
-                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;
+                y1 = x1 - k1;
+                while (x1 < text1Length && y1 < text2Length &&
+                    text1.charAt(x1) === text2.charAt(y1)) {
+                    x1++;
+                    y1++;
                 }
-
-                // 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;
+                v1[k1Offset] = x1;
+                if (x1 > text1Length) {
+                    // Ran off the right of the graph.
+                    k1end += 2;
+                } else if (y1 > text2Length) {
+                    // Ran off the bottom of the graph.
+                    k1start += 2;
+                } else if (front) {
+                    k2Offset = vOffset + delta - k1;
+                    if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) {
+                        // Mirror x2 onto top-left coordinate system.
+                        x2 = text1Length - v2[k2Offset];
+                        if (x1 >= x2) {
+                            // Overlap detected.
+                            return this.diffBisectSplit(text1, text2, x1, y1, deadline);
+                        }
                     }
                 }
-
-                callers.pop(); // unstack, we are done
-                parents.pop();
-
-                for (i in b) {
-                    bProperties.push(i); // collect b's properties
+            }
+
+            // Walk the reverse path one step.
+            for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+                k2Offset = vOffset + k2;
+                if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) {
+                    x2 = v2[k2Offset + 1];
+                } else {
+                    x2 = v2[k2Offset - 1] + 1;
+                }
+                y2 = x2 - k2;
+                while (x2 < text1Length && y2 < text2Length &&
+                    text1.charAt(text1Length - x2 - 1) ===
+                    text2.charAt(text2Length - y2 - 1)) {
+                    x2++;
+                    y2++;
+                }
+                v2[k2Offset] = x2;
+                if (x2 > text1Length) {
+                    // Ran off the left of the graph.
+                    k2end += 2;
+                } else if (y2 > text2Length) {
+                    // Ran off the top of the graph.
+                    k2start += 2;
+                } else if (!front) {
+                    k1Offset = vOffset + delta - k2;
+                    if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) {
+                        x1 = v1[k1Offset];
+                        y1 = vOffset + x1 - k1Offset;
+                        // Mirror x2 onto top-left coordinate system.
+                        x2 = text1Length - x2;
+                        if (x1 >= x2) {
+                            // Overlap detected.
+                            return this.diffBisectSplit(text1, text2, x1, y1, deadline);
+                        }
+                    }
+                }
+            }
+        }
+        // Diff took too long and hit the deadline or
+        // number of diffs equals number of characters, no commonality at all.
+        return [
+            [ DIFF_DELETE, text1 ],
+            [ DIFF_INSERT, text2 ]
+        ];
+    };
+
+    /**
+     * Given the location of the 'middle snake', split the diff in two parts
+     * and recurse.
+     * @param {string} text1 Old string to be diffed.
+     * @param {string} text2 New string to be diffed.
+     * @param {number} x Index of split point in text1.
+     * @param {number} y Index of split point in text2.
+     * @param {number} deadline Time at which to bail if not yet complete.
+     * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) {
+        var text1a, text1b, text2a, text2b, diffs, diffsb;
+        text1a = text1.substring(0, x);
+        text2a = text2.substring(0, y);
+        text1b = text1.substring(x);
+        text2b = text2.substring(y);
+
+        // Compute both diffs serially.
+        diffs = this.DiffMain(text1a, text2a, false, deadline);
+        diffsb = this.DiffMain(text1b, text2b, false, deadline);
+
+        return diffs.concat(diffsb);
+    };
+
+    /**
+     * Reduce the number of edits by eliminating semantically trivial equalities.
+     * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+     */
+    DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) {
+        var changes, equalities, equalitiesLength, lastequality,
+			pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1,
+			lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2;
+        changes = false;
+        equalities = []; // Stack of indices where equalities are found.
+        equalitiesLength = 0; // Keeping our own length var is faster in JS.
+        /** @type {?string} */
+        lastequality = null;
+        // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+        pointer = 0; // Index of current position.
+        // Number of characters that changed prior to the equality.
+        lengthInsertions1 = 0;
+        lengthDeletions1 = 0;
+        // Number of characters that changed after the equality.
+        lengthInsertions2 = 0;
+        lengthDeletions2 = 0;
+        while (pointer < diffs.length) {
+            if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found.
+                equalities[equalitiesLength++] = pointer;
+                lengthInsertions1 = lengthInsertions2;
+                lengthDeletions1 = lengthDeletions2;
+                lengthInsertions2 = 0;
+                lengthDeletions2 = 0;
+                lastequality = diffs[pointer][1];
+            } else { // An insertion or deletion.
+                if (diffs[pointer][0] === DIFF_INSERT) {
+                    lengthInsertions2 += diffs[pointer][1].length;
+                } else {
+                    lengthDeletions2 += diffs[pointer][1].length;
+                }
+                // Eliminate an equality that is smaller or equal to the edits on both
+                // sides of it.
+                if (lastequality && (lastequality.length <=
+                        Math.max(lengthInsertions1, lengthDeletions1)) &&
+                    (lastequality.length <= Math.max(lengthInsertions2,
+                        lengthDeletions2))) {
+                    // Duplicate record.
+                    diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] );
+                    // Change second copy to insert.
+                    diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
+                    // Throw away the equality we just deleted.
+                    equalitiesLength--;
+                    // Throw away the previous equality (it needs to be reevaluated).
+                    equalitiesLength--;
+                    pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
+                    lengthInsertions1 = 0; // Reset the counters.
+                    lengthDeletions1 = 0;
+                    lengthInsertions2 = 0;
+                    lengthDeletions2 = 0;
+                    lastequality = null;
+                    changes = true;
                 }
-
-                // Ensures identical properties name
-                return eq && innerEquiv(aProperties.sort(), bProperties.sort());
+            }
+            pointer++;
+        }
+
+        // Normalize the diff.
+        if (changes) {
+            this.diffCleanupMerge(diffs);
+        }
+
+        // Find any overlaps between deletions and insertions.
+        // e.g: <del>abcxxx</del><ins>xxxdef</ins>
+        //   -> <del>abc</del>xxx<ins>def</ins>
+        // e.g: <del>xxxabc</del><ins>defxxx</ins>
+        //   -> <ins>def</ins>xxx<del>abc</del>
+        // Only extract an overlap if it is as big as the edit ahead or behind it.
+        pointer = 1;
+        while (pointer < diffs.length) {
+            if (diffs[pointer - 1][0] === DIFF_DELETE &&
+                diffs[pointer][0] === DIFF_INSERT) {
+                deletion = diffs[pointer - 1][1];
+                insertion = diffs[pointer][1];
+                overlapLength1 = this.diffCommonOverlap(deletion, insertion);
+                overlapLength2 = this.diffCommonOverlap(insertion, deletion);
+                if (overlapLength1 >= overlapLength2) {
+                    if (overlapLength1 >= deletion.length / 2 ||
+                        overlapLength1 >= insertion.length / 2) {
+                        // Overlap found.  Insert an equality and trim the surrounding edits.
+                        diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] );
+                        diffs[pointer - 1][1] =
+                            deletion.substring(0, deletion.length - overlapLength1);
+                        diffs[pointer + 1][1] = insertion.substring(overlapLength1);
+                        pointer++;
+                    }
+                } else {
+                    if (overlapLength2 >= deletion.length / 2 ||
+                        overlapLength2 >= insertion.length / 2) {
+                        // Reverse overlap found.
+                        // Insert an equality and swap and trim the surrounding edits.
+                        diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] );
+                        diffs[pointer - 1][0] = DIFF_INSERT;
+                        diffs[pointer - 1][1] =
+                            insertion.substring(0, insertion.length - overlapLength2);
+                        diffs[pointer + 1][0] = DIFF_DELETE;
+                        diffs[pointer + 1][1] =
+                            deletion.substring(overlapLength2);
+                        pointer++;
+                    }
+                }
+                pointer++;
             }
+            pointer++;
+        }
+    };
+
+    /**
+     * Determine if the suffix of one string is the prefix of another.
+     * @param {string} text1 First string.
+     * @param {string} text2 Second string.
+     * @return {number} The number of characters common to the end of the first
+     *     string and the start of the second string.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) {
+        var text1Length, text2Length, textLength,
+			best, length, pattern, found;
+        // Cache the text lengths to prevent multiple calls.
+        text1Length = text1.length;
+        text2Length = text2.length;
+        // Eliminate the null case.
+        if (text1Length === 0 || text2Length === 0) {
+            return 0;
+        }
+        // Truncate the longer string.
+        if (text1Length > text2Length) {
+            text1 = text1.substring(text1Length - text2Length);
+        } else if (text1Length < text2Length) {
+            text2 = text2.substring(0, text1Length);
+        }
+        textLength = Math.min(text1Length, text2Length);
+        // Quick check for the worst case.
+        if (text1 === text2) {
+            return textLength;
+        }
+
+        // Start by looking for a single character match
+        // and increase length until no match is found.
+        // Performance analysis: http://neil.fraser.name/news/2010/11/04/
+        best = 0;
+        length = 1;
+        while (true) {
+            pattern = text1.substring(textLength - length);
+            found = text2.indexOf(pattern);
+            if (found === -1) {
+                return best;
+            }
+            length += found;
+            if (found === 0 || text1.substring(textLength - length) ===
+                text2.substring(0, length)) {
+                best = length;
+                length++;
+            }
+        }
+    };
+
+    /**
+     * Split two texts into an array of strings.  Reduce the texts to a string of
+     * hashes where each Unicode character represents one line.
+     * @param {string} text1 First string.
+     * @param {string} text2 Second string.
+     * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
+     *     An object containing the encoded text1, the encoded text2 and
+     *     the array of unique strings.
+     *     The zeroth element of the array of unique strings is intentionally blank.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) {
+        var lineArray, lineHash, chars1, chars2;
+        lineArray = []; // e.g. lineArray[4] === 'Hello\n'
+        lineHash = {}; // e.g. lineHash['Hello\n'] === 4
+
+        // '\x00' is a valid character, but various debuggers don't like it.
+        // So we'll insert a junk entry to avoid generating a null character.
+        lineArray[0] = "";
+
+        /**
+         * Split a text into an array of strings.  Reduce the texts to a string of
+         * hashes where each Unicode character represents one line.
+         * Modifies linearray and linehash through being a closure.
+         * @param {string} text String to encode.
+         * @return {string} Encoded string.
+         * @private
+         */
+        function diffLinesToCharsMunge(text) {
+            var chars, lineStart, lineEnd, lineArrayLength, line;
+            chars = "";
+            // Walk the text, pulling out a substring for each line.
+            // text.split('\n') would would temporarily double our memory footprint.
+            // Modifying text would create many large strings to garbage collect.
+            lineStart = 0;
+            lineEnd = -1;
+            // Keeping our own length variable is faster than looking it up.
+            lineArrayLength = lineArray.length;
+            while (lineEnd < text.length - 1) {
+                lineEnd = text.indexOf("\n", lineStart);
+                if (lineEnd === -1) {
+                    lineEnd = text.length - 1;
+                }
+                line = text.substring(lineStart, lineEnd + 1);
+                lineStart = lineEnd + 1;
+
+                if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
+                    (lineHash[line] !== undefined)) {
+                    chars += String.fromCharCode( lineHash[ line ] );
+                } else {
+                    chars += String.fromCharCode(lineArrayLength);
+                    lineHash[line] = lineArrayLength;
+                    lineArray[lineArrayLength++] = line;
+                }
+            }
+            return chars;
+        }
+
+        chars1 = diffLinesToCharsMunge(text1);
+        chars2 = diffLinesToCharsMunge(text2);
+        return {
+            chars1: chars1,
+            chars2: chars2,
+            lineArray: lineArray
         };
-    }();
-
-    innerEquiv = function () { // can take multiple arguments
-        var args = Array.prototype.slice.apply(arguments);
-        if (args.length < 2) {
-            return true; // end transition
+    };
+
+    /**
+     * Rehydrate the text in a diff from a string of line hashes to real lines of
+     * text.
+     * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+     * @param {!Array.<string>} lineArray Array of unique strings.
+     * @private
+     */
+    DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) {
+        var x, chars, text, y;
+        for ( x = 0; x < diffs.length; x++ ) {
+            chars = diffs[x][1];
+            text = [];
+            for ( y = 0; y < chars.length; y++ ) {
+                text[y] = lineArray[chars.charCodeAt(y)];
+            }
+            diffs[x][1] = text.join("");
+        }
+    };
+
+    /**
+     * Reorder and merge like edit sections.  Merge equalities.
+     * Any edit section can move as long as it doesn't cross an equality.
+     * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+     */
+    DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) {
+        var pointer, countDelete, countInsert, textInsert, textDelete,
+			commonlength, changes;
+        diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end.
+        pointer = 0;
+        countDelete = 0;
+        countInsert = 0;
+        textDelete = "";
+        textInsert = "";
+        commonlength;
+        while (pointer < diffs.length) {
+            switch ( diffs[ pointer ][ 0 ] ) {
+                case DIFF_INSERT:
+                    countInsert++;
+                    textInsert += diffs[pointer][1];
+                    pointer++;
+                    break;
+                case DIFF_DELETE:
+                    countDelete++;
+                    textDelete += diffs[pointer][1];
+                    pointer++;
+                    break;
+                case DIFF_EQUAL:
+                    // Upon reaching an equality, check for prior redundancies.
+                    if (countDelete + countInsert > 1) {
+                        if (countDelete !== 0 && countInsert !== 0) {
+                            // Factor out any common prefixies.
+                            commonlength = this.diffCommonPrefix(textInsert, textDelete);
+                            if (commonlength !== 0) {
+                                if ((pointer - countDelete - countInsert) > 0 &&
+                                    diffs[pointer - countDelete - countInsert - 1][0] ===
+                                    DIFF_EQUAL) {
+                                    diffs[pointer - countDelete - countInsert - 1][1] +=
+                                        textInsert.substring(0, commonlength);
+                                } else {
+                                    diffs.splice( 0, 0, [ DIFF_EQUAL,
+                                        textInsert.substring( 0, commonlength )
+                                     ] );
+                                    pointer++;
+                                }
+                                textInsert = textInsert.substring(commonlength);
+                                textDelete = textDelete.substring(commonlength);
+                            }
+                            // Factor out any common suffixies.
+                            commonlength = this.diffCommonSuffix(textInsert, textDelete);
+                            if (commonlength !== 0) {
+                                diffs[pointer][1] = textInsert.substring(textInsert.length -
+                                    commonlength) + diffs[pointer][1];
+                                textInsert = textInsert.substring(0, textInsert.length -
+                                    commonlength);
+                                textDelete = textDelete.substring(0, textDelete.length -
+                                    commonlength);
+                            }
+                        }
+                        // Delete the offending records and add the merged ones.
+                        if (countDelete === 0) {
+                            diffs.splice( pointer - countInsert,
+                                countDelete + countInsert, [ DIFF_INSERT, textInsert ] );
+                        } else if (countInsert === 0) {
+                            diffs.splice( pointer - countDelete,
+                                countDelete + countInsert, [ DIFF_DELETE, textDelete ] );
+                        } else {
+                            diffs.splice( pointer - countDelete - countInsert,
+                                countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] );
+                        }
+                        pointer = pointer - countDelete - countInsert +
+                            (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1;
+                    } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
+                        // Merge this equality with the previous one.
+                        diffs[pointer - 1][1] += diffs[pointer][1];
+                        diffs.splice(pointer, 1);
+                    } else {
+                        pointer++;
+                    }
+                    countInsert = 0;
+                    countDelete = 0;
+                    textDelete = "";
+                    textInsert = "";
+                    break;
+            }
+        }
+        if (diffs[diffs.length - 1][1] === "") {
+            diffs.pop(); // Remove the dummy entry at the end.
+        }
+
+        // Second pass: look for single edits surrounded on both sides by equalities
+        // which can be shifted sideways to eliminate an equality.
+        // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
+        changes = false;
+        pointer = 1;
+        // Intentionally ignore the first and last element (don't need checking).
+        while (pointer < diffs.length - 1) {
+            if (diffs[pointer - 1][0] === DIFF_EQUAL &&
+                diffs[pointer + 1][0] === DIFF_EQUAL) {
+                // This is a single edit surrounded by equalities.
+                if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length -
+                        diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) {
+                    // Shift the edit over the previous equality.
+                    diffs[pointer][1] = diffs[pointer - 1][1] +
+                        diffs[pointer][1].substring(0, diffs[pointer][1].length -
+                            diffs[pointer - 1][1].length);
+                    diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
+                    diffs.splice(pointer - 1, 1);
+                    changes = true;
+                } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) ===
+                    diffs[ pointer + 1 ][ 1 ] ) {
+                    // Shift the edit over the next equality.
+                    diffs[pointer - 1][1] += diffs[pointer + 1][1];
+                    diffs[pointer][1] =
+                        diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
+                        diffs[pointer + 1][1];
+                    diffs.splice(pointer + 1, 1);
+                    changes = true;
+                }
+            }
+            pointer++;
         }
-
-        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));
+        // If shifts were made, the diff needs reordering and another shift sweep.
+        if (changes) {
+            this.diffCleanupMerge(diffs);
+        }
+    };
+
+    return function(o, n) {
+		var diff, output, text;
+        diff = new DiffMatchPatch();
+        output = diff.DiffMain(o, n);
+        //console.log(output);
+        diff.diffCleanupEfficiency(output);
+        text = diff.diffPrettyHtml(output);
+
+        return text;
     };
-
-    return innerEquiv;
-
-}();
+}());
+// jscs:enable
+
+(function() {
+
+// Deprecated QUnit.init - Ref #530
+// Re-initialize the configuration options
+QUnit.init = function() {
+	var tests, banner, result, qunit,
+		config = QUnit.config;
+
+	config.stats = { all: 0, bad: 0 };
+	config.moduleStats = { all: 0, bad: 0 };
+	config.started = 0;
+	config.updateRate = 1000;
+	config.blocking = false;
+	config.autostart = true;
+	config.autorun = false;
+	config.filter = "";
+	config.queue = [];
+
+	// Return on non-browser environments
+	// This is necessary to not break on node tests
+	if ( typeof window === "undefined" ) {
+		return;
+	}
+
+	qunit = id( "qunit" );
+	if ( qunit ) {
+		qunit.innerHTML =
+			"<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
+			"<h2 id='qunit-banner'></h2>" +
+			"<div id='qunit-testrunner-toolbar'></div>" +
+			"<h2 id='qunit-userAgent'></h2>" +
+			"<ol id='qunit-tests'></ol>";
+	}
+
+	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 );
+	}
+
+	if ( tests ) {
+		result = document.createElement( "p" );
+		result.id = "qunit-testresult";
+		result.className = "result";
+		tests.parentNode.insertBefore( result, tests );
+		result.innerHTML = "Running...<br />&#160;";
+	}
+};
+
+// Don't load the HTML Reporter on non-Browser environments
+if ( typeof window === "undefined" ) {
+	return;
+}
+
+var config = QUnit.config,
+	hasOwn = Object.prototype.hasOwnProperty,
+	defined = {
+		document: window.document !== undefined,
+		sessionStorage: (function() {
+			var x = "qunit-test-string";
+			try {
+				sessionStorage.setItem( x, x );
+				sessionStorage.removeItem( x );
+				return true;
+			} catch ( e ) {
+				return false;
+			}
+		}())
+	},
+	modulesList = [];
+
+/**
+* Escape text for attribute or text content.
+*/
+function escapeText( s ) {
+	if ( !s ) {
+		return "";
+	}
+	s = s + "";
+
+	// Both single quotes and double quotes (for attributes)
+	return s.replace( /['"<>&]/g, function( s ) {
+		switch ( s ) {
+		case "'":
+			return "&#039;";
+		case "\"":
+			return "&quot;";
+		case "<":
+			return "&lt;";
+		case ">":
+			return "&gt;";
+		case "&":
+			return "&amp;";
+		}
+	});
+}
+
+/**
+ * @param {HTMLElement} elem
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvent( elem, type, fn ) {
+	if ( elem.addEventListener ) {
+
+		// Standards-based browsers
+		elem.addEventListener( type, fn, false );
+	} else if ( elem.attachEvent ) {
+
+		// support: IE <9
+		elem.attachEvent( "on" + type, function() {
+			var event = window.event;
+			if ( !event.target ) {
+				event.target = event.srcElement || document;
+			}
+
+			fn.call( elem, event );
+		});
+	}
+}
 
 /**
- * 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}
+ * @param {Array|NodeList} elems
+ * @param {string} type
+ * @param {Function} fn
  */
-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";
+function addEvents( elems, type, fn ) {
+	var i = elems.length;
+	while ( i-- ) {
+		addEvent( elems[ i ], type, fn );
+	}
+}
+
+function hasClass( elem, name ) {
+	return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0;
+}
+
+function addClass( elem, name ) {
+	if ( !hasClass( elem, name ) ) {
+		elem.className += ( elem.className ? " " : "" ) + name;
+	}
+}
+
+function toggleClass( elem, name ) {
+	if ( hasClass( elem, name ) ) {
+		removeClass( elem, name );
+	} else {
+		addClass( elem, name );
+	}
+}
+
+function removeClass( elem, name ) {
+	var set = " " + elem.className + " ";
+
+	// Class name may appear multiple times
+	while ( set.indexOf( " " + name + " " ) >= 0 ) {
+		set = set.replace( " " + name + " ", " " );
+	}
+
+	// trim for prettiness
+	elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" );
+}
+
+function id( name ) {
+	return defined.document && document.getElementById && document.getElementById( name );
+}
+
+function getUrlConfigHtml() {
+	var i, j, val,
+		escaped, escapedTooltip,
+		selection = false,
+		len = config.urlConfig.length,
+		urlConfigHtml = "";
+
+	for ( i = 0; i < len; i++ ) {
+		val = config.urlConfig[ i ];
+		if ( typeof val === "string" ) {
+			val = {
+				id: val,
+				label: val
+			};
+		}
+
+		escaped = escapeText( val.id );
+		escapedTooltip = escapeText( val.tooltip );
+
+		if ( config[ val.id ] === undefined ) {
+			config[ val.id ] = QUnit.urlParams[ val.id ];
+		}
+
+		if ( !val.value || typeof val.value === "string" ) {
+			urlConfigHtml += "<input id='qunit-urlconfig-" + escaped +
+				"' name='" + escaped + "' type='checkbox'" +
+				( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) +
+				( config[ val.id ] ? " checked='checked'" : "" ) +
+				" title='" + escapedTooltip + "' /><label for='qunit-urlconfig-" + escaped +
+				"' title='" + escapedTooltip + "'>" + val.label + "</label>";
+		} else {
+			urlConfigHtml += "<label for='qunit-urlconfig-" + escaped +
+				"' title='" + escapedTooltip + "'>" + val.label +
+				": </label><select id='qunit-urlconfig-" + escaped +
+				"' name='" + escaped + "' title='" + escapedTooltip + "'><option></option>";
+
+			if ( QUnit.is( "array", val.value ) ) {
+				for ( j = 0; j < val.value.length; j++ ) {
+					escaped = escapeText( val.value[ j ] );
+					urlConfigHtml += "<option value='" + escaped + "'" +
+						( config[ val.id ] === val.value[ j ] ?
+							( selection = true ) && " selected='selected'" : "" ) +
+						">" + escaped + "</option>";
+				}
 			} else {
-				type = typeof obj;
+				for ( j in val.value ) {
+					if ( hasOwn.call( val.value, j ) ) {
+						urlConfigHtml += "<option value='" + escapeText( j ) + "'" +
+							( config[ val.id ] === j ?
+								( selection = true ) && " selected='selected'" : "" ) +
+							">" + escapeText( val.value[ j ] ) + "</option>";
+					}
+				}
+			}
+			if ( config[ val.id ] && !selection ) {
+				escaped = escapeText( config[ val.id ] );
+				urlConfigHtml += "<option value='" + escaped +
+					"' selected='selected' disabled='disabled'>" + escaped + "</option>";
+			}
+			urlConfigHtml += "</select>";
+		}
+	}
+
+	return urlConfigHtml;
+}
+
+// Handle "click" events on toolbar checkboxes and "change" for select menus.
+// Updates the URL with the new state of `config.urlConfig` values.
+function toolbarChanged() {
+	var updatedUrl, value,
+		field = this,
+		params = {};
+
+	// Detect if field is a select menu or a checkbox
+	if ( "selectedIndex" in field ) {
+		value = field.options[ field.selectedIndex ].value || undefined;
+	} else {
+		value = field.checked ? ( field.defaultValue || true ) : undefined;
+	}
+
+	params[ field.name ] = value;
+	updatedUrl = setUrl( params );
+
+	if ( "hidepassed" === field.name && "replaceState" in window.history ) {
+		config[ field.name ] = value || false;
+		if ( value ) {
+			addClass( id( "qunit-tests" ), "hidepass" );
+		} else {
+			removeClass( id( "qunit-tests" ), "hidepass" );
+		}
+
+		// It is not necessary to refresh the whole page
+		window.history.replaceState( null, "", updatedUrl );
+	} else {
+		window.location = updatedUrl;
+	}
+}
+
+function setUrl( params ) {
+	var key,
+		querystring = "?";
+
+	params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params );
+
+	for ( key in params ) {
+		if ( hasOwn.call( params, key ) ) {
+			if ( params[ key ] === undefined ) {
+				continue;
+			}
+			querystring += encodeURIComponent( key );
+			if ( params[ key ] !== true ) {
+				querystring += "=" + encodeURIComponent( params[ key ] );
 			}
-			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;
+			querystring += "&";
+		}
+	}
+	return location.protocol + "//" + location.host +
+		location.pathname + querystring.slice( 0, -1 );
+}
+
+function applyUrlParams() {
+	var selectedModule,
+		modulesList = id( "qunit-modulefilter" ),
+		filter = id( "qunit-filter-input" ).value;
+
+	selectedModule = modulesList ?
+		decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) :
+		undefined;
+
+	window.location = setUrl({
+		module: ( selectedModule === "" ) ? undefined : selectedModule,
+		filter: ( filter === "" ) ? undefined : filter,
+
+		// Remove testId filter
+		testId: undefined
+	});
+}
+
+function toolbarUrlConfigContainer() {
+	var urlConfigContainer = document.createElement( "span" );
+
+	urlConfigContainer.innerHTML = getUrlConfigHtml();
+	addClass( urlConfigContainer, "qunit-url-config" );
+
+	// For oldIE support:
+	// * Add handlers to the individual elements instead of the container
+	// * Use "click" instead of "change" for checkboxes
+	addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged );
+	addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged );
+
+	return urlConfigContainer;
+}
+
+function toolbarLooseFilter() {
+	var filter = document.createElement( "form" ),
+		label = document.createElement( "label" ),
+		input = document.createElement( "input" ),
+		button = document.createElement( "button" );
+
+	addClass( filter, "qunit-filter" );
+
+	label.innerHTML = "Filter: ";
+
+	input.type = "text";
+	input.value = config.filter || "";
+	input.name = "filter";
+	input.id = "qunit-filter-input";
+
+	button.innerHTML = "Go";
+
+	label.appendChild( input );
+
+	filter.appendChild( label );
+	filter.appendChild( button );
+	addEvent( filter, "submit", function( ev ) {
+		applyUrlParams();
+
+		if ( ev && ev.preventDefault ) {
+			ev.preventDefault();
+		}
+
+		return false;
+	});
+
+	return filter;
+}
+
+function toolbarModuleFilterHtml() {
+	var i,
+		moduleFilterHtml = "";
+
+	if ( !modulesList.length ) {
+		return false;
+	}
+
+	modulesList.sort(function( a, b ) {
+		return a.localeCompare( b );
+	});
+
+	moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label>" +
+		"<select id='qunit-modulefilter' name='modulefilter'><option value='' " +
+		( QUnit.urlParams.module === undefined ? "selected='selected'" : "" ) +
+		">< All Modules ></option>";
+
+	for ( i = 0; i < modulesList.length; i++ ) {
+		moduleFilterHtml += "<option value='" +
+			escapeText( encodeURIComponent( modulesList[ i ] ) ) + "' " +
+			( QUnit.urlParams.module === modulesList[ i ] ? "selected='selected'" : "" ) +
+			">" + escapeText( modulesList[ i ] ) + "</option>";
+	}
+	moduleFilterHtml += "</select>";
+
+	return moduleFilterHtml;
+}
+
+function toolbarModuleFilter() {
+	var toolbar = id( "qunit-testrunner-toolbar" ),
+		moduleFilter = document.createElement( "span" ),
+		moduleFilterHtml = toolbarModuleFilterHtml();
+
+	if ( !toolbar || !moduleFilterHtml ) {
+		return false;
+	}
+
+	moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
+	moduleFilter.innerHTML = moduleFilterHtml;
+
+	addEvent( moduleFilter.lastChild, "change", applyUrlParams );
+
+	toolbar.appendChild( moduleFilter );
+}
+
+function appendToolbar() {
+	var toolbar = id( "qunit-testrunner-toolbar" );
+
+	if ( toolbar ) {
+		toolbar.appendChild( toolbarUrlConfigContainer() );
+		toolbar.appendChild( toolbarLooseFilter() );
+	}
+}
+
+function appendHeader() {
+	var header = id( "qunit-header" );
+
+	if ( header ) {
+		header.innerHTML = "<a href='" +
+			setUrl({ filter: undefined, module: undefined, testId: undefined }) +
+			"'>" + header.innerHTML + "</a> ";
+	}
+}
+
+function appendBanner() {
+	var banner = id( "qunit-banner" );
+
+	if ( banner ) {
+		banner.className = "";
+	}
+}
+
+function appendTestResults() {
+	var tests = id( "qunit-tests" ),
+		result = id( "qunit-testresult" );
+
+	if ( result ) {
+		result.parentNode.removeChild( result );
+	}
+
+	if ( tests ) {
+		tests.innerHTML = "";
+		result = document.createElement( "p" );
+		result.id = "qunit-testresult";
+		result.className = "result";
+		tests.parentNode.insertBefore( result, tests );
+		result.innerHTML = "Running...<br />&#160;";
+	}
+}
+
+function storeFixture() {
+	var fixture = id( "qunit-fixture" );
+	if ( fixture ) {
+		config.fixture = fixture.innerHTML;
+	}
+}
+
+function appendUserAgent() {
+	var userAgent = id( "qunit-userAgent" );
+
+	if ( userAgent ) {
+		userAgent.innerHTML = "";
+		userAgent.appendChild(
+			document.createTextNode(
+				"QUnit " + QUnit.version  + "; " + navigator.userAgent
+			)
+		);
+	}
+}
+
+function appendTestsList( modules ) {
+	var i, l, x, z, test, moduleObj;
+
+	for ( i = 0, l = modules.length; i < l; i++ ) {
+		moduleObj = modules[ i ];
+
+		if ( moduleObj.name ) {
+			modulesList.push( moduleObj.name );
+		}
+
+		for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) {
+			test = moduleObj.tests[ x ];
+
+			appendTest( test.name, test.testId, moduleObj.name );
+		}
+	}
+}
+
+function appendTest( name, testId, moduleName ) {
+	var title, rerunTrigger, testBlock, assertList,
+		tests = id( "qunit-tests" );
+
+	if ( !tests ) {
+		return;
+	}
+
+	title = document.createElement( "strong" );
+	title.innerHTML = getNameHtml( name, moduleName );
+
+	rerunTrigger = document.createElement( "a" );
+	rerunTrigger.innerHTML = "Rerun";
+	rerunTrigger.href = setUrl({ testId: testId });
+
+	testBlock = document.createElement( "li" );
+	testBlock.appendChild( title );
+	testBlock.appendChild( rerunTrigger );
+	testBlock.id = "qunit-test-output-" + testId;
+
+	assertList = document.createElement( "ol" );
+	assertList.className = "qunit-assert-list";
+
+	testBlock.appendChild( assertList );
+
+	tests.appendChild( testBlock );
+}
+
+// HTML Reporter initialization and load
+QUnit.begin(function( details ) {
+	var qunit = id( "qunit" );
+
+	// Fixture is the only one necessary to run without the #qunit element
+	storeFixture();
+
+	if ( qunit ) {
+		qunit.innerHTML =
+			"<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
+			"<h2 id='qunit-banner'></h2>" +
+			"<div id='qunit-testrunner-toolbar'></div>" +
+			"<h2 id='qunit-userAgent'></h2>" +
+			"<ol id='qunit-tests'></ol>";
+	}
+
+	appendHeader();
+	appendBanner();
+	appendTestResults();
+	appendUserAgent();
+	appendToolbar();
+	appendTestsList( details.modules );
+	toolbarModuleFilter();
+
+	if ( qunit && config.hidepassed ) {
+		addClass( qunit.lastChild, "hidepass" );
+	}
+});
+
+QUnit.done(function( details ) {
+	var i, key,
+		banner = id( "qunit-banner" ),
+		tests = id( "qunit-tests" ),
+		html = [
+			"Tests completed in ",
+			details.runtime,
+			" milliseconds.<br />",
+			"<span class='passed'>",
+			details.passed,
+			"</span> assertions of <span class='total'>",
+			details.total,
+			"</span> passed, <span class='failed'>",
+			details.failed,
+			"</span> failed."
+		].join( "" );
+
+	if ( banner ) {
+		banner.className = details.failed ? "qunit-fail" : "qunit-pass";
+	}
+
+	if ( tests ) {
+		id( "qunit-testresult" ).innerHTML = html;
+	}
+
+	if ( config.altertitle && defined.document && document.title ) {
+
+		// show ✖ for good, ✔ for bad suite result in title
+		// use escape sequences in case file gets loaded with non-utf-8-charset
+		document.title = [
+			( details.failed ? "\u2716" : "\u2714" ),
+			document.title.replace( /^[\u2714\u2716] /i, "" )
+		].join( " " );
+	}
+
+	// clear own sessionStorage items if all tests passed
+	if ( config.reorder && defined.sessionStorage && details.failed === 0 ) {
+		for ( i = 0; i < sessionStorage.length; i++ ) {
+			key = sessionStorage.key( i++ );
+			if ( key.indexOf( "qunit-test-" ) === 0 ) {
+				sessionStorage.removeItem( key );
+			}
+		}
+	}
+
+	// scroll back to top to show results
+	if ( config.scrolltop && window.scrollTo ) {
+		window.scrollTo( 0, 0 );
+	}
+});
+
+function getNameHtml( name, module ) {
+	var nameHtml = "";
+
+	if ( module ) {
+		nameHtml = "<span class='module-name'>" + escapeText( module ) + "</span>: ";
+	}
+
+	nameHtml += "<span class='test-name'>" + escapeText( name ) + "</span>";
+
+	return nameHtml;
+}
+
+QUnit.testStart(function( details ) {
+	var running, testBlock, bad;
+
+	testBlock = id( "qunit-test-output-" + details.testId );
+	if ( testBlock ) {
+		testBlock.className = "running";
+	} else {
+
+		// Report later registered tests
+		appendTest( details.name, details.testId, details.module );
+	}
+
+	running = id( "qunit-testresult" );
+	if ( running ) {
+		bad = QUnit.config.reorder && defined.sessionStorage &&
+			+sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name );
+
+		running.innerHTML = ( bad ?
+			"Rerunning previously failed test: <br />" :
+			"Running: <br />" ) +
+			getNameHtml( details.name, details.module );
+	}
+
+});
+
+QUnit.log(function( details ) {
+	var assertList, assertLi,
+		message, expected, actual,
+		testItem = id( "qunit-test-output-" + details.testId );
+
+	if ( !testItem ) {
+		return;
+	}
+
+	message = escapeText( details.message ) || ( details.result ? "okay" : "failed" );
+	message = "<span class='test-message'>" + message + "</span>";
+	message += "<span class='runtime'>@ " + details.runtime + " ms</span>";
+
+	// pushFailure doesn't provide details.expected
+	// when it calls, it's implicit to also not show expected and diff stuff
+	// Also, we need to check details.expected existence, as it can exist and be undefined
+	if ( !details.result && hasOwn.call( details, "expected" ) ) {
+		expected = escapeText( QUnit.dump.parse( details.expected ) );
+		actual = escapeText( QUnit.dump.parse( details.actual ) );
+		message += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" +
+			expected +
+			"</pre></td></tr>";
+
+		if ( actual !== expected ) {
+			message += "<tr class='test-actual'><th>Result: </th><td><pre>" +
+				actual + "</pre></td></tr>" +
+				"<tr class='test-diff'><th>Diff: </th><td><pre>" +
+				QUnit.diff( expected, actual ) + "</pre></td></tr>";
+		} else {
+			if ( expected.indexOf( "[object Array]" ) !== -1 ||
+					expected.indexOf( "[object Object]" ) !== -1 ) {
+				message += "<tr class='test-message'><th>Message: </th><td>" +
+					"Diff suppressed as the depth of object is more than current max depth (" +
+					QUnit.config.maxDepth + ").<p>Hint: Use <code>QUnit.dump.maxDepth</code> to " +
+					" run with a higher max depth or <a href='" + setUrl({ maxDepth: -1 }) + "'>" +
+					"Rerun</a> without max depth.</p></td></tr>";
+			}
+		}
+
+		if ( details.source ) {
+			message += "<tr class='test-source'><th>Source: </th><td><pre>" +
+				escapeText( details.source ) + "</pre></td></tr>";
+		}
+
+		message += "</table>";
+
+	// this occours when pushFailure is set and we have an extracted stack trace
+	} else if ( !details.result && details.source ) {
+		message += "<table>" +
+			"<tr class='test-source'><th>Source: </th><td><pre>" +
+			escapeText( details.source ) + "</pre></td></tr>" +
+			"</table>";
+	}
+
+	assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
+
+	assertLi = document.createElement( "li" );
+	assertLi.className = details.result ? "pass" : "fail";
+	assertLi.innerHTML = message;
+	assertList.appendChild( assertLi );
+});
+
+QUnit.testDone(function( details ) {
+	var testTitle, time, testItem, assertList,
+		good, bad, testCounts, skipped,
+		tests = id( "qunit-tests" );
+
+	if ( !tests ) {
+		return;
+	}
+
+	testItem = id( "qunit-test-output-" + details.testId );
+
+	assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
+
+	good = details.passed;
+	bad = details.failed;
+
+	// store result when possible
+	if ( config.reorder && defined.sessionStorage ) {
+		if ( bad ) {
+			sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad );
+		} else {
+			sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name );
+		}
+	}
+
+	if ( bad === 0 ) {
+		addClass( assertList, "qunit-collapsed" );
+	}
+
+	// testItem.firstChild is the test name
+	testTitle = testItem.firstChild;
+
+	testCounts = bad ?
+		"<b class='failed'>" + bad + "</b>, " + "<b class='passed'>" + good + "</b>, " :
+		"";
+
+	testTitle.innerHTML += " <b class='counts'>(" + testCounts +
+		details.assertions.length + ")</b>";
+
+	if ( details.skipped ) {
+		testItem.className = "skipped";
+		skipped = document.createElement( "em" );
+		skipped.className = "qunit-skipped-label";
+		skipped.innerHTML = "skipped";
+		testItem.insertBefore( skipped, testTitle );
+	} else {
+		addEvent( testTitle, "click", function() {
+			toggleClass( assertList, "qunit-collapsed" );
+		});
+
+		testItem.className = bad ? "fail" : "pass";
+
+		time = document.createElement( "span" );
+		time.className = "runtime";
+		time.innerHTML = details.runtime + " ms";
+		testItem.insertBefore( time, assertList );
+	}
+});
+
+if ( defined.document ) {
+	if ( document.readyState === "complete" ) {
+		QUnit.load();
+	} else {
+		addEvent( window, "load", QUnit.load );
+	}
+} else {
+	config.pageLoaded = true;
+	config.autorun = true;
+}
+
 })();
-
-})(this);
--- a/devtools/devctl.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/devctl.py	Fri Oct 09 17:52:14 2015 +0200
@@ -73,10 +73,12 @@
         return None
     def init_log(self):
         pass
-    def load_configuration(self):
+    def load_configuration(self, **kw):
         pass
     def default_log_file(self):
         return None
+    def default_stats_file(self):
+        return None
 
 
 def cleanup_sys_modules(config):
@@ -580,8 +582,8 @@
 # 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 this program. If not, see <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
 ''',
 
         'GPL': '''\
@@ -592,7 +594,8 @@
 #
 # This program 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 General Public License for more details.
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
 #
 # You should have received a copy of the GNU General Public License along with
 # this program. If not, see <http://www.gnu.org/licenses/>.
@@ -834,21 +837,11 @@
             p.wait()
 
 
-class GenerateQUnitHTML(Command):
-    """Generate a QUnit html file to see test in your browser"""
-    name = "qunit-html"
-    arguments = '<test file> [<dependancy js file>...]'
-
-    def run(self, args):
-        from cubicweb.devtools.qunit import make_qunit_html
-        print make_qunit_html(args[0], args[1:])
-
 for cmdcls in (UpdateCubicWebCatalogCommand,
                UpdateCubeCatalogCommand,
                #LiveServerCommand,
                NewCubeCommand,
                ExamineLogCommand,
                GenerateSchema,
-               GenerateQUnitHTML,
                ):
     CWCTL.register(cmdcls)
--- a/devtools/fake.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/fake.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,6 +20,8 @@
 
 __docformat__ = "restructuredtext en"
 
+from contextlib import contextmanager
+
 from logilab.database import get_db_helper
 
 from cubicweb.req import RequestSessionBase
@@ -159,6 +161,10 @@
     # for use with enabled_security context manager
     read_security = write_security = True
 
+    @contextmanager
+    def running_hooks_ops(self):
+        yield
+
 class FakeRepo(object):
     querier = None
     def __init__(self, schema, vreg=None, config=None):
@@ -173,7 +179,7 @@
     def internal_session(self):
         return FakeSession(self)
 
-    def extid2eid(self, source, extid, etype, session, insert=True):
+    def extid2eid(self, source, extid, etype, cnx, insert=True):
         try:
             return self.extids[extid]
         except KeyError:
@@ -181,10 +187,10 @@
                 return None
             self._count += 1
             eid = self._count
-            entity = source.before_entity_insertion(session, extid, etype, eid)
+            entity = source.before_entity_insertion(cnx, extid, etype, eid)
             self.extids[extid] = eid
             self.eids[eid] = extid
-            source.after_entity_insertion(session, extid, entity)
+            source.after_entity_insertion(cnx, extid, entity)
             return eid
 
 
--- a/devtools/httptest.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/httptest.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -78,8 +78,6 @@
         self.global_set_option('port', port) # force rewrite here
         return 'http://127.0.0.1:%d/' % self['port']
 
-    def pyro_enabled(self):
-        return False
 
 
 class CubicWebServerTC(CubicWebTC):
@@ -139,7 +137,6 @@
             passwd = self.admpassword
         if passwd is None:
             passwd = user
-        self.login(user)
         response = self.web_get("login?__login=%s&__password=%s" %
                                 (user, passwd))
         assert response.status == httplib.SEE_OTHER, response.status
--- a/devtools/qunit.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/qunit.py	Fri Oct 09 17:52:14 2015 +0200
@@ -29,7 +29,9 @@
 from logilab.common.shellutils import getlogin
 
 import cubicweb
+from cubicweb.view import View
 from cubicweb.web.controller import Controller
+from cubicweb.web.views.staticcontrollers import StaticFileController, STATIC_CONTROLLERS
 from cubicweb.devtools.httptest import CubicWebServerTC
 
 
@@ -66,7 +68,7 @@
         self.firefox_cmd = ['firefox', '-no-remote']
         if os.name == 'posix':
             self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'),
-                                '-a', '-s', '-noreset -screen 0 640x480x8'] + self.firefox_cmd
+                                '-a', '-s', '-noreset -screen 0 800x600x24'] + self.firefox_cmd
 
     def start(self, url):
         self.stop()
@@ -102,11 +104,14 @@
             test_queue = self.test_queue
         self._qunit_controller = MyQUnitResultController
         self.vreg.register(MyQUnitResultController)
+        self.vreg.register(QUnitView)
+        self.vreg.register(CWSoftwareRootStaticController)
 
     def tearDown(self):
         super(QUnitTestCase, self).tearDown()
         self.vreg.unregister(self._qunit_controller)
-
+        self.vreg.unregister(QUnitView)
+        self.vreg.unregister(CWSoftwareRootStaticController)
 
     def abspath(self, path):
         """use self.__module__ to build absolute path if necessary"""
@@ -130,35 +135,21 @@
                 yield js_test
 
     @with_tempdir
-    def _test_qunit(self, test_file, depends=(), data_files=(), timeout=30):
+    def _test_qunit(self, test_file, depends=(), data_files=(), timeout=10):
         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', delete=False)
-        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)
+        QUnitView.test_file = test_file
+        QUnitView.depends = depends
 
         while not self.test_queue.empty():
             self.test_queue.get(False)
 
         browser = FirefoxHelper()
-        # start firefox once to let it init the profile (and run system-wide
-        # add-ons post setup, blegh), and then kill it ...
-        browser.start('about:blank')
-        import time; time.sleep(5)
-        browser.stop()
-        # ... then actually run the test file
-        browser.start(html_test_file.name)
+        browser.start(self.config['base-url'] + "?vid=qunit")
         test_count = 0
         error = False
         def raise_exception(cls, *data):
@@ -220,100 +211,114 @@
 
     def handle_log(self):
         result = self._cw.form['result']
-        message = self._cw.form['message']
-        self._log_stack.append('%s: %s' % (result, message))
+        message = self._cw.form.get('message', '<no message>')
+        actual = self._cw.form.get('actual')
+        expected = self._cw.form.get('expected')
+        source = self._cw.form.get('source')
+        log = '%s: %s' % (result, message)
+        if result == 'false' and actual is not None and expected is not None:
+            log += ' (got: %s, expected: %s)' % (actual, expected)
+            if source is not None:
+                log += '\n' + source
+        self._log_stack.append(log)
 
 
-def cw_path(*paths):
-  return file_path(osp.join(cubicweb.CW_SOFTWARE_ROOT, *paths))
-
-def file_path(path):
-    return 'file://' + osp.abspath(path)
+class QUnitView(View):
+    __regid__ = 'qunit'
 
-def build_js_script(host):
-    return """
-    var host = '%s';
+    templatable = False
 
-    QUnit.moduleStart = function (name) {
-      jQuery.ajax({
-                  url: host+'/qunit_result',
-                 data: {"event": "module_start",
-                        "name": name},
-                 async: false});
-    }
+    depends = None
+    test_file = None
 
-    QUnit.testDone = function (name, failures, total) {
-      jQuery.ajax({
-                  url: host+'/qunit_result',
-                 data: {"event": "test_done",
-                        "name": name,
-                        "failures": failures,
-                        "total":total},
-                 async: false});
-    }
+    def call(self, **kwargs):
+        w = self.w
+        req = self._cw
+        data = {
+            'jquery': req.data_url('jquery.js'),
+            'web_test': req.build_url('cwsoftwareroot/devtools/data'),
+        }
+        w(u'''<!DOCTYPE html>
+        <html>
+        <head>
+        <meta http-equiv="content-type" content="application/html; charset=UTF-8"/>
+        <!-- JS lib used as testing framework -->
+        <link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
+        <script src="%(jquery)s" 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)
+        w(u'<!-- result report tools -->')
+        w(u'<script type="text/javascript">')
+        w(u"var BASE_URL = '%s';" % req.base_url())
+        w(u'''
+            QUnit.moduleStart(function (details) {
+              jQuery.ajax({
+                          url: BASE_URL + 'qunit_result',
+                         data: {"event": "module_start",
+                                "name": details.name},
+                         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.testDone(function (details) {
+              jQuery.ajax({
+                          url: BASE_URL + 'qunit_result',
+                         data: {"event": "test_done",
+                                "name": details.name,
+                                "failures": details.failed,
+                                "total": details.total},
+                         async: false});
+            });
 
-    QUnit.log = function (result, message) {
-      jQuery.ajax({
-                   url: host+'/qunit_result',
-                   data: {"event": "log",
-                          "result": result,
-                          "message": message},
-                   async: false});
-    }
-    """ % host
+            QUnit.done(function (details) {
+              jQuery.ajax({
+                           url: BASE_URL + 'qunit_result',
+                           data: {"event": "done",
+                                  "failures": details.failed,
+                                  "total": details.total},
+                           async: false});
+            });
 
-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'),
-        }
+            QUnit.log(function (details) {
+              jQuery.ajax({
+                           url: BASE_URL + 'qunit_result',
+                           data: {"event": "log",
+                                  "result": details.result,
+                                  "actual": details.actual,
+                                  "expected": details.expected,
+                                  "source": details.source,
+                                  "message": details.message},
+                           async: false});
+            });''')
+        w(u'</script>')
+        w(u'<!-- Test script dependencies (tested code for example) -->')
 
-    html = ['''<!DOCTYPE html>
-<html>
-  <head>
-    <meta http-equiv="content-type" content="application/html; charset=UTF-8"/>
-    <!-- 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) -->')
+        prefix = len(cubicweb.CW_SOFTWARE_ROOT) + 1
+        for dep in self.depends:
+            dep = req.build_url('cwsoftwareroot/') + dep[prefix:]
+            w(u'    <script src="%s" type="text/javascript"></script>' % dep)
 
-    for dep in depends:
-        html.append('    <script src="%s" type="text/javascript"></script>' % file_path(dep))
+        w(u'    <!-- Test script itself -->')
+        test_url = req.build_url('cwsoftwareroot/') + self.test_file[prefix:]
+        w(u'    <script src="%s" type="text/javascript"></script>' % test_url)
+        w(u'''  </head>
+        <body>
+        <div id="qunit-fixture"></div>
+        <div id="qunit"></div>
+        </body>
+        </html>''')
+
 
-    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"></ol>
-  </body>
-</html>''')
-    return u'\n'.join(html)
+class CWSoftwareRootStaticController(StaticFileController):
+    __regid__ = 'cwsoftwareroot'
 
+    def publish(self, rset=None):
+        staticdir = cubicweb.CW_SOFTWARE_ROOT
+        relpath = self.relpath[len(self.__regid__) + 1:]
+        return self.static_file(osp.join(staticdir, relpath))
+
+
+STATIC_CONTROLLERS.append(CWSoftwareRootStaticController)
 
 
 if __name__ == '__main__':
--- a/devtools/repotest.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/repotest.py	Fri Oct 09 17:52:14 2015 +0200
@@ -259,12 +259,11 @@
 
     def qexecute(self, rql, args=None, build_descr=True):
         with self.session.new_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                try:
-                    return self.o.execute(cnx, rql, args, build_descr)
-                finally:
-                    if rql.startswith(('INSERT', 'DELETE', 'SET')):
-                        cnx.commit()
+            try:
+                return self.o.execute(cnx, rql, args, build_descr)
+            finally:
+                if rql.startswith(('INSERT', 'DELETE', 'SET')):
+                    cnx.commit()
 
 
 class BasePlannerTC(BaseQuerierTC):
--- a/devtools/test/data/cubes/i18ntestcube/views.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/test/data/cubes/i18ntestcube/views.py	Fri Oct 09 17:52:14 2015 +0200
@@ -26,9 +26,6 @@
 
 _myafs = MyAFS()
 
-# XXX useless ASA logilab.common.registry is fixed
-_myafs.__module__ = "cubes.i18ntestcube.views"
-
 _myafs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined')
 
 afs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined')
--- a/devtools/test/data/js_examples/test_simple_failure.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/test/data/js_examples/test_simple_failure.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,18 +1,18 @@
 $(document).ready(function() {
 
-  module("air");
+  QUnit.module("air");
 
-  test("test 1", function() {
-      equals(2, 4);
+  QUnit.test("test 1", function (assert) {
+      assert.equal(2, 4);
   });
 
-  test("test 2", function() {
-      equals('', '45');
-      equals('1024', '32');
+  QUnit.test("test 2", function (assert) {
+      assert.equal('', '45');
+      assert.equal('1024', '32');
   });
 
-  module("able");
-  test("test 3", function() {
-      same(1, 1);
+  QUnit.module("able");
+  QUnit.test("test 3", function (assert) {
+      assert.deepEqual(1, 1);
   });
 });
--- a/devtools/test/data/js_examples/test_simple_success.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/test/data/js_examples/test_simple_success.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,17 +1,17 @@
 $(document).ready(function() {
 
-  module("air");
+  QUnit.module("air");
 
-  test("test 1", function() {
-      equals(2, 2);
+  QUnit.test("test 1", function (assert) {
+      assert.equal(2, 2);
   });
 
-  test("test 2", function() {
-      equals('45', '45');
+  QUnit.test("test 2", function (assert) {
+      assert.equal('45', '45');
   });
 
-  module("able");
-  test("test 3", function() {
-      same(1, 1);
+  QUnit.module("able");
+  QUnit.test("test 3", function (assert) {
+      assert.deepEqual(1, 1);
   });
 });
--- a/devtools/test/data/js_examples/test_with_dep.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/test/data/js_examples/test_with_dep.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,9 +1,9 @@
 $(document).ready(function() {
 
-  module("air");
+  QUnit.module("air");
 
-  test("test 1", function() {
-      equals(a, 4);
+  QUnit.test("test 1", function (assert) {
+      assert.equal(a, 4);
   });
 
 });
--- a/devtools/test/data/js_examples/test_with_ordered_deps.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/test/data/js_examples/test_with_ordered_deps.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,9 +1,9 @@
 $(document).ready(function() {
 
-  module("air");
+  QUnit.module("air");
 
-  test("test 1", function() {
-      equals(b, 6);
+  QUnit.test("test 1", function (assert) {
+      assert.equal(b, 6);
   });
 
 });
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,3 @@
+Twisted
+webtest
+cubicweb-person
--- a/devtools/test/unittest_i18n.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/test/unittest_i18n.py	Fri Oct 09 17:52:14 2015 +0200
@@ -20,8 +20,9 @@
 
 import os, os.path as osp
 import sys
+import subprocess
 
-from logilab.common.testlib import TestCase, unittest_main
+from unittest import TestCase, main
 
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
 
@@ -52,28 +53,23 @@
 class cubePotGeneratorTC(TestCase):
     """test case for i18n pot file generator"""
 
-    def setUp(self):
-        self._CUBES_PATH = CubicWebNoAppConfiguration.CUBES_PATH[:]
-        CubicWebNoAppConfiguration.CUBES_PATH.append(osp.join(DATADIR, 'cubes'))
-        CubicWebNoAppConfiguration.cls_adjust_sys_path()
-
-    def tearDown(self):
-        CubicWebNoAppConfiguration.CUBES_PATH[:] = self._CUBES_PATH
-
     def test_i18ncube(self):
-        # MUST import here to make, since the import statement fire
-        # the cube paths setup (and then must occur after the setUp)
-        from cubicweb.devtools.devctl import update_cube_catalogs
+        env = os.environ.copy()
+        env['CW_CUBES_PATH'] = osp.join(DATADIR, 'cubes')
+        if 'PYTHONPATH' in env:
+            env['PYTHONPATH'] += os.pathsep
+        else:
+            env['PYTHONPATH'] = ''
+        env['PYTHONPATH'] += DATADIR
+        cwctl = osp.abspath(osp.join(osp.dirname(__file__), '../../bin/cubicweb-ctl'))
+        with open(os.devnull, 'w') as devnull:
+            subprocess.check_call([sys.executable, cwctl, 'i18ncube', 'i18ntestcube'],
+                                  env=env, stdout=devnull)
         cube = osp.join(DATADIR, 'cubes', 'i18ntestcube')
         msgs = load_po(osp.join(cube, 'i18n', 'en.po.ref'))
-        update_cube_catalogs(cube)
         newmsgs = load_po(osp.join(cube, 'i18n', 'en.po'))
         self.assertEqual(msgs, newmsgs)
 
+
 if __name__ == '__main__':
-    # XXX dirty hack to make this test runnable using python (works
-    # fine with pytest, but not with python directly if this hack is
-    # not present)
-    # XXX to remove ASA logilab.common is fixed
-    sys.path.append('')
-    unittest_main()
+    main()
--- a/devtools/test/unittest_testlib.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/test/unittest_testlib.py	Fri Oct 09 17:52:14 2015 +0200
@@ -42,7 +42,7 @@
                                 '__maineid': 0,
                                 '__type:0': 'Entity',
                                 '_cw_entity_fields:0': '__type,field',
-                                '_cw_fields': 'file,encoding',
+                                '_cw_fields': 'encoding,file',
                                 'eid': [0],
                                 'encoding': u'utf-8',
                                 'field:0': 'value',
--- a/devtools/testlib.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/devtools/testlib.py	Fri Oct 09 17:52:14 2015 +0200
@@ -156,30 +156,6 @@
 cwconfig.SMTP = MockSMTP
 
 
-class TestCaseConnectionProxy(object):
-    """thin wrapper around `cubicweb.repoapi.ClientConnection` 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):
-        # already open
-        return self.cnx
-
-    def __exit__(self, exctype, exc, tb):
-        try:
-            return self.cnx.__exit__(exctype, exc, tb)
-        finally:
-            self.testcase.restore_connection()
-
 # Repoaccess utility ###############################################3###########
 
 class RepoAccess(object):
@@ -189,8 +165,7 @@
 
     A repo access can create three type of object:
 
-    .. automethod:: cubicweb.testlib.RepoAccess.repo_cnx
-    .. automethod:: cubicweb.testlib.RepoAccess.client_cnx
+    .. automethod:: cubicweb.testlib.RepoAccess.cnx
     .. automethod:: cubicweb.testlib.RepoAccess.web_request
 
     The RepoAccess need to be closed to destroy the associated Session.
@@ -225,16 +200,13 @@
         return session
 
     @contextmanager
-    def repo_cnx(self):
+    def cnx(self):
         """Context manager returning a server side connection for the user"""
         with self._session.new_cnx() as cnx:
             yield cnx
 
-    @contextmanager
-    def client_cnx(self):
-        """Context manager returning a client side connection for the user"""
-        with repoapi.ClientConnection(self._session) as cnx:
-            yield cnx
+    # aliases for bw compat
+    client_cnx = repo_cnx = cnx
 
     @contextmanager
     def web_request(self, url=None, headers={}, method='GET', **kwargs):
@@ -247,9 +219,10 @@
         """
         req = self.requestcls(self._repo.vreg, url=url, headers=headers,
                               method=method, form=kwargs)
-        clt_cnx = repoapi.ClientConnection(self._session)
-        req.set_cnx(clt_cnx)
-        with clt_cnx:
+        with self._session.new_cnx() as cnx:
+            if 'ecache' in cnx.transaction_data:
+                del cnx.transaction_data['ecache']
+            req.set_cnx(cnx)
             yield req
 
     def close(self):
@@ -261,7 +234,7 @@
     @contextmanager
     def shell(self):
         from cubicweb.server.migractions import ServerMigrationHelper
-        with repoapi.ClientConnection(self._session) as cnx:
+        with self._session.new_cnx() as cnx:
             mih = ServerMigrationHelper(None, repo=self._repo, cnx=cnx,
                                         interactive=False,
                                         # hack so it don't try to load fs schema
@@ -294,17 +267,12 @@
     requestcls = fake.FakeRequest
     tags = TestCase.tags | Tags('cubicweb', 'cw_repo')
     test_db_id = DEFAULT_EMPTY_DB_ID
-    _cnxs = set() # establised connection
-                  # stay on connection for leak detection purpose
 
     # anonymous is logged by default in cubicweb test cases
     anonymous_allowed = True
 
     def __init__(self, *args, **kwargs):
         self._admin_session = None
-        self._admin_clt_cnx = None
-        self._current_session = None
-        self._current_clt_cnx = None
         self.repo = None
         self._open_access = set()
         super(CubicWebTC, self).__init__(*args, **kwargs)
@@ -315,6 +283,7 @@
         """provide a new RepoAccess object for a given user
 
         The access is automatically closed at the end of the test."""
+        login = unicode(login)
         access = RepoAccess(self.repo, login, self.requestcls)
         self._open_access.add(access)
         return access
@@ -326,92 +295,11 @@
             except BadConnectionId:
                 continue # already closed
 
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def set_cnx(self, cnx):
-        assert getattr(cnx, '_session', None) is not None
-        if cnx is self._admin_clt_cnx:
-            self._pop_custom_cnx()
-        else:
-            self._cnxs.add(cnx) # register the cnx to make sure it is removed
-            self._current_session = cnx._session
-            self._current_clt_cnx = cnx
-
     @property
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def cnx(self):
-        # XXX we want to deprecate this
-        clt_cnx = self._current_clt_cnx
-        if clt_cnx is None:
-            clt_cnx = self._admin_clt_cnx
-        return clt_cnx
-
-    def _close_cnx(self):
-        """ensure that all cnx used by a test have been closed"""
-        for cnx in list(self._cnxs):
-            if cnx._open and not cnx._session.closed:
-                cnx.rollback()
-                cnx.close()
-            self._cnxs.remove(cnx)
-
-    @property
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
     def session(self):
-        """return current server side session"""
-        # XXX We want to use a srv_connection instead and deprecate this
-        # property
-        session = self._current_session
-        if session is None:
-            session = self._admin_session
-            # bypassing all sanity to use the same repo cnx in the session
-            #
-            # we can't call set_cnx as the Connection is not managed by the
-            # session.
-            session._Session__threaddata.cnx = self._admin_clt_cnx._cnx
-        else:
-            session._Session__threaddata.cnx = self.cnx._cnx
-        session.set_cnxset()
-        return session
-
-    @property
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def websession(self):
-        return self.session
-
-    @property
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def adminsession(self):
-        """return current server side session (using default manager account)"""
+        """return admin session"""
         return self._admin_session
 
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def login(self, login, **kwargs):
-        """return a connection for the given login/password"""
-        __ = kwargs.pop('autoclose', True) # not used anymore
-        if login == self.admlogin:
-            # undo any previous login, if we're not used as a context manager
-            self.restore_connection()
-            return self.cnx
-        else:
-            if not kwargs:
-                kwargs['password'] = str(login)
-            clt_cnx = repoapi.connect(self.repo, login, **kwargs)
-        self.set_cnx(clt_cnx)
-        clt_cnx.__enter__()
-        return TestCaseConnectionProxy(self, clt_cnx)
-
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def restore_connection(self):
-        self._pop_custom_cnx()
-
-    def _pop_custom_cnx(self):
-        if self._current_clt_cnx is not None:
-            if self._current_clt_cnx._open:
-                self._current_clt_cnx.close()
-            if not  self._current_session.closed:
-                self.repo.close(self._current_session.sessionid)
-            self._current_clt_cnx = None
-            self._current_session = None
-
     #XXX this doesn't need to a be classmethod anymore
     def _init_repo(self):
         """init the repository and connection to it.
@@ -425,64 +313,8 @@
         login = unicode(db_handler.config.default_admin_config['login'])
         self.admin_access = self.new_access(login)
         self._admin_session = self.admin_access._session
-        self._admin_clt_cnx = repoapi.ClientConnection(self._admin_session)
-        self._cnxs.add(self._admin_clt_cnx)
-        self._admin_clt_cnx.__enter__()
         self.config.repository = lambda x=None: self.repo
 
-    # db api ##################################################################
-
-    @nocoverage
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def cursor(self, req=None):
-        if req is not None:
-            return req.cnx
-        else:
-            return self.cnx
-
-    @nocoverage
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def execute(self, rql, args=None, req=None):
-        """executes <rql>, builds a resultset, and returns a couple (rset, req)
-        where req is a FakeRequest
-        """
-        req = req or self.request(rql=rql)
-        return req.execute(unicode(rql), args)
-
-    @nocoverage
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def commit(self):
-        try:
-            return self.cnx.commit()
-        finally:
-            self.session.set_cnxset() # ensure cnxset still set after commit
-
-    @nocoverage
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def rollback(self):
-        try:
-            self.cnx.rollback()
-        except ProgrammingError:
-            pass # connection closed
-        finally:
-            self.session.set_cnxset() # ensure cnxset still set after commit
-
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def request(self, rollbackfirst=False, url=None, headers={}, **kwargs):
-        """return a web ui request"""
-        if rollbackfirst:
-            self.cnx.rollback()
-        req = self.requestcls(self.vreg, url=url, headers=headers, form=kwargs)
-        req.set_cnx(self.cnx)
-        return req
-
-    # server side db api #######################################################
-
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def sexecute(self, rql, args=None):
-        self.session.set_cnxset()
-        return self.session.execute(rql, args)
-
 
     # config management ########################################################
 
@@ -549,15 +381,6 @@
         """return the application schema"""
         return self.vreg.schema
 
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def shell(self):
-        """return a shell session object"""
-        from cubicweb.server.migractions import ServerMigrationHelper
-        return ServerMigrationHelper(None, repo=self.repo, cnx=self.cnx,
-                                     interactive=False,
-                                     # hack so it don't try to load fs schema
-                                     schema=1)
-
     def set_option(self, optname, value):
         self.config.global_set_option(optname, value)
 
@@ -578,24 +401,17 @@
                 self.skipTest('repository is not initialised: %r' % previous_failure)
             try:
                 self._init_repo()
-                self.addCleanup(self._close_cnx)
             except Exception as ex:
                 self.__class__._repo_init_failed = ex
                 raise
             self.addCleanup(self._close_access)
         self.setup_database()
-        self._admin_clt_cnx.commit()
         MAILBOX[:] = [] # reset mailbox
 
     def tearDown(self):
         # XXX hack until logilab.common.testlib is fixed
-        if self._admin_clt_cnx is not None:
-            if self._admin_clt_cnx._open:
-                self._admin_clt_cnx.close()
-            self._admin_clt_cnx = None
         if self._admin_session is not None:
-            if not self._admin_session.closed:
-                self.repo.close(self._admin_session.sessionid)
+            self.repo.close(self._admin_session.sessionid)
             self._admin_session = None
         while self._cleanups:
             cleanup, args, kwargs = self._cleanups.pop(-1)
@@ -635,20 +451,11 @@
     def create_user(self, req, login=None, groups=('users',), password=None,
                     email=None, commit=True, **kwargs):
         """create and return a new user entity"""
-        if isinstance(req, basestring):
-            warn('[3.12] create_user arguments are now (req, login[, groups, password, commit, **kwargs])',
-                 DeprecationWarning, stacklevel=2)
-            if not isinstance(groups, (tuple, list)):
-                password = groups
-                groups = login
-            elif isinstance(login, tuple):
-                groups = login
-            login = req
-            assert not isinstance(self, type)
-            req = self._admin_clt_cnx
         if password is None:
-            password = login.encode('utf8')
-        user = req.create_entity('CWUser', login=unicode(login),
+            password = login
+        if login is not None:
+            login = unicode(login)
+        user = req.create_entity('CWUser', login=login,
                                  upassword=password, **kwargs)
         req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
                     % ','.join(repr(str(g)) for g in groups),
@@ -919,7 +726,7 @@
             if entity_fields:
                 form[eid_param('_cw_entity_fields', entity.eid)] = ','.join(entity_fields)
         if fields:
-            form['_cw_fields'] = ','.join(fields)
+            form['_cw_fields'] = ','.join(sorted(fields))
         return form
 
     @deprecated('[3.19] use .admin_request_from_url instead')
@@ -1039,8 +846,8 @@
     def assertAuthSuccess(self, req, origsession, nbsessions=1):
         sh = self.app.session_handler
         session = self.app.get_session(req)
-        clt_cnx = repoapi.ClientConnection(session)
-        req.set_cnx(clt_cnx)
+        cnx = repoapi.Connection(session)
+        req.set_cnx(cnx)
         self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
         self.assertEqual(session.login, origsession.login)
         self.assertEqual(session.anonymous_session, False)
@@ -1213,7 +1020,8 @@
 
     def assertDocTestFile(self, testfile):
         # doctest returns tuple (failure_count, test_count)
-        result = self.shell().process_script(testfile)
+        with self.admin_access.shell() as mih:
+            result = mih.process_script(testfile)
         if result[0] and result[1]:
             raise self.failureException("doctest file '%s' failed"
                                         % testfile)
@@ -1326,7 +1134,7 @@
         """this method populates the database with `how_many` entities
         of each possible type. It also inserts random relations between them
         """
-        with self.admin_access.repo_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             with cnx.security_enabled(read=False, write=False):
                 self._auto_populate(cnx, how_many)
                 cnx.commit()
--- a/doc/3.14.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,164 +0,0 @@
-Whats new in CubicWeb 3.14
-==========================
-
-First notice CW 3.14 depends on yams 0.34 (which is incompatible with prior
-cubicweb releases regarding instance re-creation).
-
-API changes
------------
-
-* `Entity.fetch_rql` `restriction` argument has been deprecated and should be
-  replaced with a call to the new `Entity.fetch_rqlst` method, get the returned
-  value (a rql `Select` node) and use the RQL syntax tree API to include the
-  above-mentionned restrictions.
-
-  Backward compat is kept with proper warning.
-
-* `Entity.fetch_order` and `Entity.fetch_unrelated_order` class methods have been
-  replaced by `Entity.cw_fetch_order` and `Entity.cw_fetch_unrelated_order` with
-  a different prototype:
-
-  - instead of taking (attr, var) as two string argument, they now take (select,
-    attr, var) where select is the rql syntax tree beinx constructed and var the
-    variable *node*.
-
-  - instead of returning some string to be inserted in the ORDERBY clause, it has
-    to modify the syntax tree
-
-  Backward compat is kept with proper warning, BESIDE cases below:
-
-  - custom order method return **something else the a variable name with or
-    without the sorting order** (e.g. cases where you sort on the value of a
-    registered procedure as it was done in the tracker for instance). In such
-    case, an error is logged telling that this sorting is ignored until API
-    upgrade.
-
-  - client code use direct access to one of those methods on an entity (no code
-    known to do that).
-
-* `Entity._rest_attr_info` class method has been renamed to
-  `Entity.cw_rest_attr_info`
-
-  No backward compat yet since this is a protected method an no code is known to
-  use it outside cubicweb itself.
-
-* `AnyEntity.linked_to` has been removed as part of a refactoring of this
-  functionality (link a entity to another one at creation step). It was replaced
-  by a `EntityFieldsForm.linked_to` property.
-
-  In the same refactoring, `cubicweb.web.formfield.relvoc_linkedto`,
-  `cubicweb.web.formfield.relvoc_init` and
-  `cubicweb.web.formfield.relvoc_unrelated` were removed and replaced by
-  RelationField methods with the same names, that take a form as a parameter.
-
-  **No backward compatibility yet**. It's still time to cry for it.
-  Cubes known to be affected: tracker, vcsfile, vcreview.
-
-* `CWPermission` entity type and its associated require_permission relation type
-  (abstract) and require_group relation definitions have been moved to a new
-  `localperms` cube. With this have gone some functions from the
-  `cubicweb.schemas` package as well as some views. This makes cubicweb itself
-  smaller while you get all the local permissions stuff into a single,
-  documented, place.
-
-  Backward compat is kept for existing instances, **though you should have
-  installed the localperms cubes**. A proper error should be displayed when
-  trying to migrate to 3.14 an instance the use `CWPermission` without the new
-  cube installed. For new instances / test, you should add a dependancy on the
-  new cube in cubes using this feature, along with a dependancy on cubicweb >=
-  3.14.
-
-* jQuery has been updated to 1.6.4 and jquery-tablesorter to 2.0.5. No backward
-  compat issue known.
-
-* Table views refactoring : new `RsetTableView` and `EntityTableView`, as well as
-  rewritten an enhanced version of `PyValTableView` on the same bases, with logic
-  moved to some column renderers and a layout. Those should be well documented
-  and deprecates former `TableView`, `EntityAttributesTableView` and `CellView`,
-  which are however kept for backward compat, with some warnings that may not be
-  very clear unfortunatly (you may see your own table view subclass name here,
-  which doesn't make the problem that clear). Notice that `_cw.view('table',
-  rset, *kwargs)` will be routed to the new `RsetTableView` or to the old
-  `TableView` depending on given extra arguments. See #1986413.
-
-* `display_name` don't call .lower() anymore. This may leads to changes in your
-  user interface. Different msgid for upper/lower cases version of entity type
-  names, as this is the only proper way to handle this with some languages.
-
-* `IEditControlAdapter` has been deprecated in favor of `EditController`
-  overloading, which was made easier by adding dedicated selectors called
-  `match_edited_type` and `match_form_id`.
-
-* Pre 3.6 API backward compat has been dropped, though *data* migration
-  compatibility has been kept. You may have to fix errors due to old API usage
-  for your instance before to be able to run migration, but then you should be
-  able to upgrade even a pre 3.6 database.
-
-* Deprecated `cubicweb.web.views.iprogress` in favor of new `iprogress` cube.
-
-* Deprecated `cubicweb.web.views.flot` in favor of new `jqplot` cube.
-
-
-Unintrusive API changes
------------------------
-
-* Refactored properties forms (eg user preferences and site wide properties) as
-  well as pagination components to ease overridding.
-
-* New `cubicweb.web.uihelper` module with high-level helpers for uicfg.
-
-* New `anonymized_request` decorator to temporary run stuff as an anonymous
-  user, whatever the currently logged in user.
-
-* New 'verbatimattr' attribute view.
-
-* New facet and form widget for Integer used to store binary mask.
-
-* New `js_href` function to generated proper javascript href.
-
-* `match_kwargs` and `match_form_params` selectors both accept a new
-  `once_is_enough` argument.
-
-* `printable_value` is now a method of request, and may be given dict of
-   formatters to use.
-
-* `[Rset]TableView` allows to set None in 'headers', meaning the label should be
-  fetched from the result set as done by default.
-
-* Field vocabulary computation on entity creation now takes `__linkto`
-  information into accounet.
-
-* Started a `cubicweb.pylintext` pylint plugin to help pylint analyzing cubes.
-
-
-RQL
----
-
-* Support for HAVING in 'SET' and 'DELETE' queries.
-
-* new `AT_TZ` function to get back a timestamp at a given time-zone.
-
-* new `WEEKDAY` date extraction function
-
-
-User interface changes
-----------------------
-
-* Datafeed source now present an history of the latest import's log, including
-  global status and debug/info/warning/error messages issued during
-  imports. Import logs older than a configurable amount of time are automatically
-  deleted.
-
-* Breadcrumbs component is properly kept when creating an entity with '__linkto'.
-
-* users and groups management now really lead to that (i.e. includes *groups*
-  management).
-
-* New 'jsonp' controller with 'jsonexport' and 'ejsonexport' views.
-
-
-Configuration
-------------
-
-* Added option 'resources-concat' to make javascript/css files concatenation
-  optional.
--- a/doc/3.15.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-What's new in CubicWeb 3.15?
-============================
-
-New functionnalities
---------------------
-
-* Add Zmq server, based on the cutting edge ZMQ (http://www.zeromq.org/) socket
-  library.  This allows to access distant instance, in a similar way as Pyro.
-
-* Publish/subscribe mechanism using ZMQ for communication among cubicweb
-  instances.  The new zmq-address-sub and zmq-address-pub configuration variables
-  define where this communication occurs.  As of this release this mechanism is
-  used for entity cache invalidation.
-
-* Improved WSGI support. While there is still some caveats, most of the code
-  which was twisted only is now generic and allows related functionalities to work
-  with a WSGI front-end.
-
-* Full undo/transaction support : undo of modification has eventually been
-  implemented, and the configuration simplified (basically you activate it or not
-  on an instance basis).
-
-* Controlling HTTP status code used is not much more easier :
-
-  - `WebRequest` now has a `status_out` attribut to control the response status ;
-
-  - most web-side exceptions take an optional ``status`` argument.
-
-API changes
------------
-
-* The base registry implementation has been moved to a new
-  `logilab.common.registry` module (see #1916014). This includes code from :
-
-  * `cubicweb.vreg` (the whole things that was in there)
-  * `cw.appobject` (base selectors and all).
-
-  In the process, some renaming was done:
-
-  * the top level registry is now `RegistryStore` (was `VRegistry`), but that
-    should not impact cubicweb client code ;
-
-  * former selectors functions are now known as "predicate", though you still use
-    predicates to build an object'selector ;
-
-  * for consistency, the `objectify_selector` decoraror has hence be renamed to
-    `objectify_predicate` ;
-
-  * on the CubicWeb side, the `selectors` module has been renamed to
-    `predicates`.
-
-  Debugging refactoring dropped the more need for the `lltrace` decorator.  There
-  should be full backward compat with proper deprecation warnings.  Notice the
-  `yes` predicate and `objectify_predicate` decorator, as well as the
-  `traced_selection` function should now be imported from the
-  `logilab.common.registry` module.
-
-* All login forms are now submitted to <app_root>/login. Redirection to requested
-  page is now handled by the login controller (it was previously handle by the
-  session manager).
-
-* `Publisher.publish` has been renamed to `Publisher.handle_request`. This
-  method now contains generic version of logic previously handled by
-  Twisted. `Controller.publish` is **not** affected.
-
-Unintrusive API changes
------------------------
-
-* New 'ldapfeed' source type, designed to replace 'ldapuser' source with
-  data-feed (i.e. copy based) source ideas.
-
-* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
-
-* A new registry called `services` has appeared, where you can register
-  server-side `cubicweb.server.Service` child classes. Their `call` method can be
-  invoked from a web-side AppObject instance using new `self._cw.call_service`
-  method or a server-side one using `self.session.call_service`. This is a new
-  way to call server-side methods, much cleaner than monkey patching the
-  Repository class, which becomes a deprecated way to perform similar tasks.
-
-* a new `ajax-func` registry now hosts all remote functions (i.e. functions
-  callable through the `asyncRemoteExec` JS api). A convenience `ajaxfunc`
-  decorator will let you expose your python function easily without all the
-  appobject standard boilerplate. Backward compatibility is preserved.
-
-* the 'json' controller is now deprecated in favor of the 'ajax' one.
-
-* `WebRequest.build_url` can now take a __secure__ argument. When True cubicweb
-  try to generate an https url.
-
-
-User interface changes
-----------------------
-
-A new 'undohistory' view expose the undoable transactions and give access to undo
-some of them.
--- a/doc/3.16.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-What's new in CubicWeb 3.16?
-============================
-
-New functionalities
---------------------
-
-* Add a new dataimport store (`SQLGenObjectStore`). This store enables a fast
-  import of data (entity creation, link creation) in CubicWeb, by directly
-  flushing information in SQL.  This may only be used with PostgreSQL, as it
-  requires the 'COPY FROM' command.
-
-
-API changes
------------
-
-* Orm: `set_attributes` and `set_relations` are unified (and
-  deprecated) in favor of `cw_set` that works in all cases.
-
-* db-api/configuration: all the external repository connection information is
-  now in an URL (see `#2521848 <http://www.cubicweb.org/2521848>`_),
-  allowing to drop specific options of pyro nameserver host, group, etc and fix
-  broken `ZMQ <http://www.zeromq.org/>`_ source. Configuration related changes:
-
-  * Dropped 'pyro-ns-host', 'pyro-instance-id', 'pyro-ns-group' from the client side
-    configuration, in favor of 'repository-uri'. **NO MIGRATION IS DONE**,
-    supposing there is no web-only configuration in the wild.
-
-  * Stop discovering the connection method through `repo_method` class attribute
-    of the configuration, varying according to the configuration class. This is
-    a first step on the way to a simpler configuration handling.
-
-  DB-API related changes:
-
-  * Stop indicating the connection method using `ConnectionProperties`.
-
-  * Drop `_cnxtype` attribute from `Connection` and `cnxtype` from
-    `Session`. The former is replaced by a `is_repo_in_memory` property
-    and the later is totaly useless.
-
-  * Turn `repo_connect` into `_repo_connect` to mark it as a private function.
-
-  * Deprecate `in_memory_cnx` which becomes useless, use `_repo_connect` instead
-    if necessary.
-
-* the "tcp://" uri scheme used for `ZMQ <http://www.zeromq.org/>`_
-  communications (in a way reminiscent of Pyro) is now named
-  "zmqpickle-tcp://", so as to make room for future zmq-based lightweight
-  communications (without python objects pickling).
-
-* Request.base_url gets a `secure=True` optional parameter that yields
-  an https url if possible, allowing hook-generated content to send
-  secure urls (e.g. when sending mail notifications)
-
-* Dataimport ucsvreader gets a new boolean `ignore_errors`
-  parameter.
-
-
-Unintrusive API changes
------------------------
-
-* Drop of `cubicweb.web.uicfg.AutoformSectionRelationTags.bw_tag_map`,
-  deprecated since 3.6.
-
-
-User interface changes
-----------------------
-
-* The RQL search bar has now some auto-completion support. It means
-  relation types or entity types can be suggested while typing. It is
-  an awesome improvement over the current behaviour !
-
-* The `action box` associated with `table` views (from `tableview.py`)
-  has been transformed into a nice-looking series of small tabs; it
-  means that the possible actions are immediately visible and need not
-  be discovered by clicking on an almost invisible icon on the upper
-  right.
-
-* The `uicfg` module has moved to web/views/ and ui configuration
-  objects are now selectable. This will reduce the amount of
-  subclassing and whole methods replacement usually needed to
-  customize the ui behaviour in many cases.
-
-* Remove changelog view, as neither cubicweb nor known
-  cubes/applications were properly feeding related files.
-
-
-Other changes
--------------
-
-* 'pyrorql' sources will be automatically updated to use an URL to locate the source
-  rather than configuration option. 'zmqrql' sources were broken before this change,
-  so no upgrade is needed...
-
-* Debugging filters for Hooks and Operations have been added.
-
-* Some cubicweb-ctl commands used to show the output of `msgcat` and
-  `msgfmt`; they don't anymore.
--- a/doc/3.17.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-What's new in CubicWeb 3.17?
-============================
-
-New functionalities
---------------------
-
-* add a command to compare db schema and file system schema
-  (see `#464991 <http://www.cubicweb.org/464991>`_)
-
-* Add CubicWebRequestBase.content with the content of the HTTP request (see #2742453)
-  (see `#2742453 <http://www.cubicweb.org/2742453>`_)
-
-* Add directive bookmark to ReST rendering
-  (see `#2545595 <http://www.cubicweb.org/ticket/2545595>`_)
-
-* Allow user defined final type
-  (see `#124342 <https://www.logilab.org/ticket/124342>`_)
-
-
-API changes
------------
-
-* drop typed_eid() in favour of int() (see `#2742462 <http://www.cubicweb.org/2742462>`_)
-
-* The SIOC views and adapters have been removed from CubicWeb and moved to the
-  `sioc` cube.
-
-* The web page embedding views and adapters have been removed from CubicWeb and
-  moved to the `embed` cube.
-
-* The email sending views and controllers have been removed from CubicWeb and
-  moved to the `massmailing` cube.
-
-* ``RenderAndSendNotificationView`` is deprecated in favor of
-  ``ActualNotificationOp`` the new operation use the more efficient *data*
-  idiom.
-
-* Looping task can now have a interval <= ``0``. Negative interval disable the
-  looping task entirely.
-
-* We now serve html instead of xhtml.
-  (see `#2065651 <http://www.cubicweb.org/ticket/2065651>`_)
-
-
-Deprecation
----------------------
-
-* ``ldapuser`` have been deprecated. It'll be fully dropped in the next
-  version. If you are still using ldapuser switch to ``ldapfeed`` **NOW**!
-
-* ``hijack_user`` have been deprecated. It will be dropped soon.
-
-Deprecated Code Drops
-----------------------
-
-* The progress views and adapters have been removed from CubicWeb. These
-  classes were deprecated since 3.14.0. They are still available in the
-  `iprogress` cube.
-
-* API deprecated since 3.7 have been dropped.
--- a/doc/3.18.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-What's new in CubicWeb 3.18?
-============================
-
-The migration script does not handle sqlite nor mysql instances.
-
-
-New functionalities
---------------------
-
-* add a security debugging tool
-  (see `#2920304 <http://www.cubicweb.org/2920304>`_)
-
-* introduce an `add` permission on attributes, to be interpreted at
-  entity creation time only and allow the implementation of complex
-  `update` rules that don't block entity creation (before that the
-  `update` attribute permission was interpreted at entity creation and
-  update time)
-
-* the primary view display controller (uicfg) now has a
-  `set_fields_order` method similar to the one available for forms
-
-* new method `ResultSet.one(col=0)` to retrive a single entity and enforce the
-  result has only one row (see `#3352314 https://www.cubicweb.org/ticket/3352314`_)
-
-* new method `RequestSessionBase.find` to look for entities
-  (see `#3361290 https://www.cubicweb.org/ticket/3361290`_)
-
-* the embedded jQuery copy has been updated to version 1.10.2, and jQuery UI to
-  version 1.10.3.
-
-* initial support for wsgi for the debug mode, available through the new
-  ``wsgi`` cubicweb-ctl command, which can use either python's builtin
-  wsgi server or the werkzeug module if present.
-
-* a ``rql-table`` directive is now available in ReST fields
-
-* cubicweb-ctl upgrade can now generate the static data resource directory
-  directly, without a manual call to gen-static-datadir.
-
-API changes
------------
-
-* not really an API change, but the entity permission checks are now
-  systematically deferred to an operation, instead of a) trying in a
-  hook and b) if it failed, retrying later in an operation
-
-* The default value storage for attributes is no longer String, but
-  Bytes.  This opens the road to storing arbitrary python objects, e.g.
-  numpy arrays, and fixes a bug where default values whose truth value
-  was False were not properly migrated.
-
-* `symmetric` relations are no more handled by an rql rewrite but are
-  now handled with hooks (from the `activeintegrity` category); this
-  may have some consequences for applications that do low-level database
-  manipulations or at times disable (some) hooks.
-
-* `unique together` constraints (multi-columns unicity constraints)
-  get a `name` attribute that maps the CubicWeb contraint entities to
-  corresponding backend index.
-
-* BreadCrumbEntityVComponent's open_breadcrumbs method now includes
-  the first breadcrumbs separator
-
-* entities can be compared for equality and hashed
-
-* the ``on_fire_transition`` predicate accepts a sequence of possible
-  transition names
-
-* the GROUP_CONCAT rql aggregate function no longer repeats duplicate
-  values, on the sqlite and postgresql backends
-
-Deprecation
----------------------
-
-* ``pyrorql`` sources have been deprecated. Multisource will be fully dropped
-  in the next version. If you are still using pyrorql, switch to ``datafeed``
-  **NOW**!
-
-* the old multi-source system
-
-* `find_one_entity` and `find_entities` in favor of `find`
-  (see `#3361290 https://www.cubicweb.org/ticket/3361290`_)
-
-* the `TmpFileViewMixin` and `TmpPngView` classes (see `#3400448
-  https://www.cubicweb.org/ticket/3400448`_)
-
-Deprecated Code Drops
-----------------------
-
-* ``ldapuser`` have been dropped; use ``ldapfeed`` now
-  (see `#2936496 <http://www.cubicweb.org/2936496>`_)
-
-* action ``GotRhythm`` was removed, make sure you do not
-  import it in your cubes (even to unregister it)
-  (see `#3093362 <http://www.cubicweb.org/3093362>`_)
-
-* all 3.8 backward compat is gone
-
-* all 3.9 backward compat (including the javascript side) is gone
-
-* the ``twisted`` (web-only) instance type has been removed
--- a/doc/3.19.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-What's new in CubicWeb 3.19?
-============================
-
-New functionalities
---------------------
-
-* implement Cross Origin Resource Sharing (CORS)
-  (see `#2491768 <http://www.cubicweb.org/2491768>`_)
-
-* system_source.create_eid can get a range of IDs, to reduce overhead of batch
-  entity creation
-
-Behaviour Changes
------------------
-
-* The anonymous property of Session and Connection are now computed from the
-  related user login. If it matches the ``anonymous-user`` in the config the
-  connection is anonymous. Beware that the ``anonymous-user`` config is web
-  specific. Therefore, no session may be anonymous in a repository only setup.
-
-
-New Repository Access API
--------------------------
-
-Connection replaces Session
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A new explicit Connection object replaces Session as the main repository entry
-point. Connection holds all the necessary methods to be used server-side
-(``execute``, ``commit``, ``rollback``, ``call_service``, ``entity_from_eid``,
-etc...). One obtains a new Connection object using ``session.new_cnx()``.
-Connection objects need to have an explicit begin and end. Use them as a context
-manager to never miss an end::
-
-    with session.new_cnx() as cnx:
-        cnx.execute('INSERT Elephant E, E name "Babar"')
-        cnx.commit()
-        cnx.execute('INSERT Elephant E, E name "Celeste"')
-        cnx.commit()
-    # Once you get out of the "with" clause, the connection is closed.
-
-Using the same Connection object in multiple threads will give you access to the
-same Transaction. However, Connection objects are not thread safe (hence at your
-own risks).
-
-``repository.internal_session`` is deprecated in favor of
-``repository.internal_cnx``. Note that internal connections are now `safe` by default,
-i.e. the integrity hooks are enabled.
-
-Backward compatibility is preserved on Session.
-
-
-dbapi vs repoapi
-~~~~~~~~~~~~~~~~
-
-A new API has been introduced to replace the dbapi. It is called `repoapi`.
-
-There are three relevant functions for now:
-
-* ``repoapi.get_repository`` returns a Repository object either from an
-  URI when used as ``repoapi.get_repository(uri)`` or from a config
-  when used as ``repoapi.get_repository(config=config)``.
-
-* ``repoapi.connect(repo, login, **credentials)`` returns a ClientConnection
-  associated with the user identified by the credentials. The
-  ClientConnection is associated with its own Session that is closed
-  when the ClientConnection is closed. A ClientConnection is a
-  Connection-like object to be used client side.
-
-* ``repoapi.anonymous_cnx(repo)`` returns a ClientConnection associated
-  with the anonymous user if described in the config.
-
-
-repoapi.ClientConnection replace dbapi.Connection and company
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-On the client/web side, the Request is now using a ``repoapi.ClientConnection``
-instead of a ``dbapi.connection``. The ``ClientConnection`` has multiple backward
-compatible methods to make it look like a ``dbapi.Cursor`` and ``dbapi.Connection``.
-
-Session used on the Web side are now the same than the one used Server side.
-Some backward compatibility methods have been installed on the server side Session
-to ease the transition.
-
-The authentication stack has been altered to use the ``repoapi`` instead of
-the ``dbapi``. Cubes adding new element to this stack are likely to break.
-
-Session data can be accessed using the cnx.data dictionary, while
-transaction data is available through cnx.transaction_data.  These
-replace the [gs]et_shared_data methods with optional txid kwarg.
-
-New API in tests
-~~~~~~~~~~~~~~~~
-
-All current methods and attributes used to access the repo on ``CubicWebTC`` are
-deprecated. You may now use a ``RepoAccess`` object. A ``RepoAccess`` object is
-linked to a new ``Session`` for a specified user. It is able to create
-``Connection``, ``ClientConnection`` and web side requests linked to this
-session::
-
-    access = self.new_access('babar') # create a new RepoAccess for user babar
-    with access.repo_cnx() as cnx:
-        # some work with server side cnx
-        cnx.execute(...)
-        cnx.commit()
-        cnx.execute(...)
-        cnx.commit()
-
-    with access.client_cnx() as cnx:
-        # some work with client side cnx
-        cnx.execute(...)
-        cnx.commit()
-
-    with access.web_request(elephant='babar') as req:
-        # some work with client side cnx
-        elephant_name = req.form['elephant']
-        req.execute(...)
-        req.cnx.commit()
-
-By default ``testcase.admin_access`` contains a ``RepoAccess`` object for the
-default admin session.
-
-
-API changes
------------
-
-* ``RepositorySessionManager.postlogin`` is now called with two arguments,
-  request and session. And this now happens before the session is linked to the
-  request.
-
-* ``SessionManager`` and ``AuthenticationManager`` now take a repo object at
-  initialization time instead of a vreg.
-
-* The ``async`` argument of ``_cw.call_service`` has been dropped. All calls are
-  now  synchronous. The zmq notification bus looks like a good replacement for
-  most async use cases.
-
-* ``repo.stats()`` is now deprecated. The same information is available through
-  a service (``_cw.call_service('repo_stats')``).
-
-* ``repo.gc_stats()`` is now deprecated. The same information is available through
-  a service (``_cw.call_service('repo_gc_stats')``).
-
-* ``repo.register_user()`` is now deprecated.  The functionality is now
-  available through a service (``_cw.call_service('register_user')``).
-
-* ``request.set_session`` no longer takes an optional ``user`` argument.
-
-* CubicwebTC does not have repo and cnx as class attributes anymore. They are
-  standard instance attributes. ``set_cnx`` and ``_init_repo`` class methods
-  become instance methods.
-
-* ``set_cnxset`` and ``free_cnxset`` are deprecated. cnxset are now
-  automatically managed.
-
-* The implementation of cascading deletion when deleting `composite`
-  entities has changed. There comes a semantic change: merely deleting
-  a composite relation does not entail any more the deletion of the
-  component side of the relation.
-
-* ``_cw.user_callback`` and ``_cw.user_rql_callback`` are deprecated.  Users
-  are encouraged to write an actual controller (e.g. using ``ajaxfunc``)
-  instead of storing a closure in the session data.
-
-* A new ``entity.cw_linkable_rql`` method provides the rql to fetch all entities
-  that are already or may be related to the current entity using the given
-  relation.
-
-
-Deprecated Code Drops
-----------------------
-
-* session.hijack_user mechanism has been dropped.
-
-* EtypeRestrictionComponent has been removed, its functionality has been
-  replaced by facets a while ago.
-
-* the old multi-source support has been removed.  Only copy-based sources
-  remain, such as datafeed or ldapfeed.
-
--- a/doc/3.20.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-What's new in CubicWeb 3.20
-===========================
-
-New features
-------------
-
-* virtual relations: a new ComputedRelation class can be used in
-  schema.py; its `rule` attribute is an RQL snippet that defines the new
-  relation.
-
-* computed attributes: an attribute can now be defined with a `formula`
-  argument (also an RQL snippet); it will be read-only, and updated
-  automatically.
-
-  Both of these features are described in `CWEP-002`_, and the updated
-  "Data model" chapter of the CubicWeb book.
-
-* cubicweb-ctl plugins can use the ``cubicweb.utils.admincnx`` function
-  to get a Connection object from an instance name.
-
-* new 'tornado' wsgi backend
-
-* session cookies have the HttpOnly flag, so they're no longer exposed to
-  javascript
-
-* rich text fields can be formatted as markdown
-
-* the edit controller detects concurrent editions, and raises a ValidationError
-  if an entity was modified between form generation and submission
-
-* cubicweb can use a postgresql "schema" (namespace) for its tables
-
-* "cubicweb-ctl configure" can be used to set values of the admin user
-  credentials in the sources configuration file
-
-* in debug mode, setting the _cwtracehtml parameter on a request allows tracing
-  where each bit of output is produced
-
-.. _CWEP-002: http://hg.logilab.org/review/cwep/file/tip/CWEP-002.rst
-
-
-API Changes
------------
-
-* ``ucsvreader()`` and ``ucsvreader_pb()`` from the ``dataimport`` module have
-  2 new keyword arguments ``delimiter`` and ``quotechar`` to replace the
-  ``separator`` and ``quote`` arguments respectively. This makes the API match
-  that of Python's ``csv.reader()``.  The old arguments are still supported
-  though deprecated.
-
-* the migration environment's ``remove_cube`` function is now called ``drop_cube``.
-
-* cubicweb.old.css is now cubicweb.css.  The previous "new"
-  cubicweb.css, along with its cubicweb.reset.css companion, have been
-  removed.
-
-* the jquery-treeview plugin was updated to its latest version
-
-
-Deprecated Code Drops
-----------------------
-
-* most of 3.10 and 3.11 backward compat is gone; this includes:
-  - CtxComponent.box_action() and CtxComponent.build_link()
-  - cubicweb.devtools.htmlparser.XMLDemotingValidator
-  - various methods and properties on Entities, replaced by cw_edited and cw_attr_cache
-  - 'commit_event' method on hooks, replaced by 'postcommit_event'
-  - server.hook.set_operation(), replaced by Operation.get_instance(...).add_data()
-  - View.div_id(), View.div_class() and View.create_url()
-  - `*VComponent` classes
-  - in forms, Field.value() and Field.help() must take the form and the field itself as arguments
-  - form.render() must get `w` as a named argument, and renderer.render() must take `w` as first argument
-  - in breadcrumbs, the optional `recurs` argument must be a set, not False
-  - cubicweb.web.views.idownloadable.{download_box,IDownloadableLineView}
-  - primary views no longer have `render_entity_summary` and `summary` methods
-  - WFHistoryVComponent's `cell_call` method is replaced by `render_body`
-  - cubicweb.dataimport.ObjectStore.add(), replaced by create_entity
-  - ManageView.{folders,display_folders}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/Makefile	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,88 @@
+SRC=.
+
+# You can set these sphinx variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+#BUILDDIR      = build
+BUILDDIR      = _build
+CWDIR         = ..
+JSDIR         = ${CWDIR}/web/data
+JSTORST       = tools/pyjsrest.py
+BUILDJS       = js_api
+
+# Internal variables for sphinx
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d ${BUILDDIR}/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+
+
+.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  all       to make standalone HTML files, developer manual and API doc"
+	@echo "  html      to make standalone HTML files"
+	@echo "---  "
+	@echo "  pickle    to make pickle files (usable by e.g. sphinx-web)"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview over all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+
+clean:
+	rm -f *.html
+	-rm -rf ${BUILDDIR}/html ${BUILDDIR}/doctrees
+	-rm -rf ${BUILDJS}
+
+all: html
+
+# run sphinx ###
+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
+	@echo
+	@echo "Build finished; now you can process the pickle files or run"
+	@echo "  sphinx-web ${BUILDDIR}/pickle"
+	@echo "to start the sphinx-web server."
+
+web: pickle
+
+htmlhelp:
+	mkdir -p ${BUILDDIR}/htmlhelp ${BUILDDIR}/doctrees
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) ${BUILDDIR}/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in ${BUILDDIR}/htmlhelp."
+
+latex:
+	mkdir -p ${BUILDDIR}/latex ${BUILDDIR}/doctrees
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) ${BUILDDIR}/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in ${BUILDDIR}/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	mkdir -p ${BUILDDIR}/changes ${BUILDDIR}/doctrees
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) ${BUILDDIR}/changes
+	@echo
+	@echo "The overview file is in ${BUILDDIR}/changes."
+
+linkcheck:
+	mkdir -p ${BUILDDIR}/linkcheck ${BUILDDIR}/doctrees
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) ${BUILDDIR}/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in ${BUILDDIR}/linkcheck/output.txt."
Binary file doc/_static/cubicweb.png has changed
Binary file doc/_static/logilab.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_static/sphinx-default.css	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,861 @@
+/**
+ * Sphinx Doc Design
+ */
+
+html, body {
+    background: white;
+}
+
+body {
+    font-family: Verdana, sans-serif;
+    font-size: 100%;
+    background-color: white;
+    color: black;
+    margin: 0;
+    padding: 0;
+}
+
+/* :::: LAYOUT :::: */
+
+div.logilablogo {
+    padding: 10px 10px 10px 10px;
+    height:75;
+}
+
+
+div.document {
+    background-color: white;
+}
+
+div.documentwrapper {
+    float: left;
+    width: 100%;
+}
+
+div.bodywrapper {
+    margin: 0 0 0 230px;
+}
+
+div.body {
+    background-color: white;
+    padding: 0 20px 30px 20px;
+    border-left:solid;
+    border-left-color:#e2e2e2;
+    border-left-width:thin;
+}
+
+div.sphinxsidebarwrapper {
+    padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+    float: left;
+    width: 230px;
+    margin-left: -100%;
+    font-size: 90%;
+}
+
+div.clearer {
+    clear: both;
+}
+
+div.footer {
+    color: #ff4500;
+    width: 100%;
+    padding: 9px 0 9px 0;
+    text-align: center;
+    font-size: 75%;
+}
+
+div.footer a {
+    color: #ff4500;
+    text-decoration: underline;
+}
+
+div.related {
+    background-color: #ff7700;
+    color: white;
+    width: 100%;
+    height: 30px;
+    line-height: 30px;
+    font-size: 90%;
+}
+
+div.related h3 {
+    display: none;
+}
+
+div.related ul {
+    margin: 0;
+    padding: 0 0 0 10px;
+    list-style: none;
+}
+
+div.related li {
+    display: inline;
+}
+
+div.related li.right {
+    float: right;
+    margin-right: 5px;
+}
+
+div.related a {
+    color: white;
+    font-weight:bold;
+}
+
+/* ::: TOC :::: */
+
+div.sphinxsidebar {
+    border-style:solid;
+    border-color: white;
+/*    background-color:#e2e2e2;*/
+    padding-bottom:5px;
+}
+
+div.sphinxsidebar h3 {
+    font-family: Verdana, sans-serif;
+    color: black;
+    font-size: 1.2em;
+    font-weight: normal;
+    margin: 0;
+    padding: 0;
+    font-weight:bold;
+    font-style:italic;
+}
+
+div.sphinxsidebar h4 {
+    font-family: Verdana, sans-serif;
+    color: black;
+    font-size: 1.1em;
+    font-weight: normal;
+    margin: 5px 0 0 0;
+    padding: 0;
+    font-weight:bold;
+    font-style:italic;
+}
+
+div.sphinxsidebar p {
+    color: black;
+}
+
+div.sphinxsidebar p.topless {
+    margin: 5px 10px 10px 10px;
+}
+
+div.sphinxsidebar ul {
+    margin: 10px;
+    padding: 0;
+    list-style: none;
+    color: black;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+    margin-left: 20px;
+    list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+div.sphinxsidebar a {
+    color: black;
+    text-decoration: none;
+}
+
+div.sphinxsidebar form {
+    margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #e2e2e2;
+    font-family: sans-serif;
+    font-size: 1em;
+    padding-bottom: 5px;
+}
+
+/* :::: MODULE CLOUD :::: */
+div.modulecloud {
+    margin: -5px 10px 5px 10px;
+    padding: 10px;
+    line-height: 160%;
+    border: 1px solid #cbe7e5;
+    background-color: #f2fbfd;
+}
+
+div.modulecloud a {
+    padding: 0 5px 0 5px;
+}
+
+/* :::: SEARCH :::: */
+ul.search {
+    margin: 10px 0 0 20px;
+    padding: 0;
+}
+
+ul.search li {
+    padding: 5px 0 5px 20px;
+    background-image: url(file.png);
+    background-repeat: no-repeat;
+    background-position: 0 7px;
+}
+
+ul.search li a {
+    font-weight: bold;
+}
+
+ul.search li div.context {
+    color: #888;
+    margin: 2px 0 0 30px;
+    text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+    font-weight: bold;
+}
+
+/* :::: COMMON FORM STYLES :::: */
+
+div.actions {
+    padding: 5px 10px 5px 10px;
+    border-top: 1px solid #cbe7e5;
+    border-bottom: 1px solid #cbe7e5;
+    background-color: #e0f6f4;
+}
+
+form dl {
+    color: #333;
+}
+
+form dt {
+    clear: both;
+    float: left;
+    min-width: 110px;
+    margin-right: 10px;
+    padding-top: 2px;
+}
+
+input#homepage {
+    display: none;
+}
+
+div.error {
+    margin: 5px 20px 0 0;
+    padding: 5px;
+    border: 1px solid #d00;
+    font-weight: bold;
+}
+
+/* :::: INLINE COMMENTS :::: */
+
+div.inlinecomments {
+    position: absolute;
+    right: 20px;
+}
+
+div.inlinecomments a.bubble {
+    display: block;
+    float: right;
+    background-image: url(style/comment.png);
+    background-repeat: no-repeat;
+    width: 25px;
+    height: 25px;
+    text-align: center;
+    padding-top: 3px;
+    font-size: 0.9em;
+    line-height: 14px;
+    font-weight: bold;
+    color: black;
+}
+
+div.inlinecomments a.bubble span {
+    display: none;
+}
+
+div.inlinecomments a.emptybubble {
+    background-image: url(style/nocomment.png);
+}
+
+div.inlinecomments a.bubble:hover {
+    background-image: url(style/hovercomment.png);
+    text-decoration: none;
+    color: #3ca0a4;
+}
+
+div.inlinecomments div.comments {
+    float: right;
+    margin: 25px 5px 0 0;
+    max-width: 50em;
+    min-width: 30em;
+    border: 1px solid #2eabb0;
+    background-color: #f2fbfd;
+    z-index: 150;
+}
+
+div#comments {
+    border: 1px solid #2eabb0;
+    margin-top: 20px;
+}
+
+div#comments div.nocomments {
+    padding: 10px;
+    font-weight: bold;
+}
+
+div.inlinecomments div.comments h3,
+div#comments h3 {
+    margin: 0;
+    padding: 0;
+    background-color: #2eabb0;
+    color: white;
+    border: none;
+    padding: 3px;
+}
+
+div.inlinecomments div.comments div.actions {
+    padding: 4px;
+    margin: 0;
+    border-top: none;
+}
+
+div#comments div.comment {
+    margin: 10px;
+    border: 1px solid #2eabb0;
+}
+
+div.inlinecomments div.comment h4,
+div.commentwindow div.comment h4,
+div#comments div.comment h4 {
+    margin: 10px 0 0 0;
+    background-color: #2eabb0;
+    color: white;
+    border: none;
+    padding: 1px 4px 1px 4px;
+}
+
+div#comments div.comment h4 {
+    margin: 0;
+}
+
+div#comments div.comment h4 a {
+    color: #d5f4f4;
+}
+
+div.inlinecomments div.comment div.text,
+div.commentwindow div.comment div.text,
+div#comments div.comment div.text {
+    margin: -5px 0 -5px 0;
+    padding: 0 10px 0 10px;
+}
+
+div.inlinecomments div.comment div.meta,
+div.commentwindow div.comment div.meta,
+div#comments div.comment div.meta {
+    text-align: right;
+    padding: 2px 10px 2px 0;
+    font-size: 95%;
+    color: #538893;
+    border-top: 1px solid #cbe7e5;
+    background-color: #e0f6f4;
+}
+
+div.commentwindow {
+    position: absolute;
+    width: 500px;
+    border: 1px solid #cbe7e5;
+    background-color: #f2fbfd;
+    display: none;
+    z-index: 130;
+}
+
+div.commentwindow h3 {
+    margin: 0;
+    background-color: #2eabb0;
+    color: white;
+    border: none;
+    padding: 5px;
+    font-size: 1.5em;
+    cursor: pointer;
+}
+
+div.commentwindow div.actions {
+    margin: 10px -10px 0 -10px;
+    padding: 4px 10px 4px 10px;
+    color: #538893;
+}
+
+div.commentwindow div.actions input {
+    border: 1px solid #2eabb0;
+    background-color: white;
+    color: #135355;
+    cursor: pointer;
+}
+
+div.commentwindow div.form {
+    padding: 0 10px 0 10px;
+}
+
+div.commentwindow div.form input,
+div.commentwindow div.form textarea {
+    border: 1px solid #3c9ea2;
+    background-color: white;
+    color: black;
+}
+
+div.commentwindow div.error {
+    margin: 10px 5px 10px 5px;
+    background-color: #fbe5dc;
+    display: none;
+}
+
+div.commentwindow div.form textarea {
+    width: 99%;
+}
+
+div.commentwindow div.preview {
+    margin: 10px 0 10px 0;
+    background-color: #70d0d4;
+    padding: 0 1px 1px 25px;
+}
+
+div.commentwindow div.preview h4 {
+    margin: 0 0 -5px -20px;
+    padding: 4px 0 0 4px;
+    color: white;
+    font-size: 1.3em;
+}
+
+div.commentwindow div.preview div.comment {
+    background-color: #f2fbfd;
+}
+
+div.commentwindow div.preview div.comment h4 {
+    margin: 10px 0 0 0!important;
+    padding: 1px 4px 1px 4px!important;
+    font-size: 1.2em;
+}
+
+/* :::: SUGGEST CHANGES :::: */
+div#suggest-changes-box input, div#suggest-changes-box textarea {
+    border: 1px solid #ccc;
+    background-color: white;
+    color: black;
+}
+
+div#suggest-changes-box textarea {
+    width: 99%;
+    height: 400px;
+}
+
+
+/* :::: PREVIEW :::: */
+div.preview {
+    background-image: url(style/preview.png);
+    padding: 0 20px 20px 20px;
+    margin-bottom: 30px;
+}
+
+
+/* :::: INDEX PAGE :::: */
+
+table.contentstable {
+    width: 90%;
+}
+
+table.contentstable p.biglink {
+    line-height: 150%;
+}
+
+a.biglink {
+    font-size: 1.3em;
+}
+
+span.linkdescr {
+    font-style: italic;
+    padding-top: 5px;
+    font-size: 90%;
+}
+
+/* :::: INDEX STYLES :::: */
+
+table.indextable td {
+    text-align: left;
+    vertical-align: top;
+}
+
+table.indextable dl, table.indextable dd {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+table.indextable tr.pcap {
+    height: 10px;
+}
+
+table.indextable tr.cap {
+    margin-top: 10px;
+    background-color: #f2f2f2;
+}
+
+img.toggler {
+    margin-right: 3px;
+    margin-top: 3px;
+    cursor: pointer;
+}
+
+form.pfform {
+    margin: 10px 0 20px 0;
+}
+
+/* :::: GLOBAL STYLES :::: */
+
+.docwarning {
+    background-color: #ffe4e4;
+    padding: 10px;
+    margin: 0 -20px 0 -20px;
+    border-bottom: 1px solid #f66;
+}
+
+p.subhead {
+    font-weight: bold;
+    margin-top: 20px;
+}
+
+a {
+    color: orangered;
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+    font-family: 'Verdana', sans-serif;
+    background-color: white;
+    font-weight: bold;
+    color: black;
+    border-bottom: 1px solid #ccc;
+    margin: 20px -20px 10px -20px;
+    padding: 3px 0 3px 10px;
+}
+
+div.body h1 { margin-top: 10pt; font-size: 150%; }
+div.body h2 { font-size: 120%; }
+div.body h3 { font-size: 100%; }
+div.body h4 { font-size: 80%; }
+div.body h5 { font-size: 600%; }
+div.body h6 { font-size: 40%; }
+
+a.headerlink {
+    color: #c60f0f;
+    font-size: 0.8em;
+    padding: 0 4px 0 4px;
+    text-decoration: none;
+    visibility: hidden;
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink {
+    visibility: visible;
+}
+
+a.headerlink:hover {
+    background-color: #c60f0f;
+    color: white;
+}
+
+div.body p, div.body dd, div.body li {
+    text-align: justify;
+    line-height: 130%;
+}
+
+div.body p.caption {
+    text-align: inherit;
+}
+
+div.body td {
+    text-align: left;
+}
+
+ul.fakelist {
+    list-style: none;
+    margin: 10px 0 10px 20px;
+    padding: 0;
+}
+
+.field-list ul {
+    padding-left: 1em;
+}
+
+.first {
+    margin-top: 0 !important;
+}
+
+/* "Footnotes" heading */
+p.rubric {
+    margin-top: 30px;
+    font-weight: bold;
+}
+
+/* "Topics" */
+
+div.topic {
+    background-color: #eee;
+    border: 1px solid #ccc;
+    padding: 0 7px 0 7px;
+    margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+    font-size: 1.1em;
+    font-weight: bold;
+    margin-top: 10px;
+}
+
+/* Admonitions */
+
+div.admonition {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    padding: 7px;
+}
+
+div.admonition dt {
+    font-weight: bold;
+}
+
+div.admonition dl {
+    margin-bottom: 0;
+}
+
+div.admonition p {
+    display: inline;
+}
+
+div.seealso {
+    background-color: #ffc;
+    border: 1px solid #ff6;
+}
+
+div.warning {
+    background-color: #ffe4e4;
+    border: 1px solid #f66;
+}
+
+div.note {
+    background-color: #eee;
+    border: 1px solid #ccc;
+}
+
+p.admonition-title {
+    margin: 0px 10px 5px 0px;
+    font-weight: bold;
+    display: inline;
+}
+
+p.admonition-title:after {
+    content: ":";
+}
+
+div.body p.centered {
+    text-align: center;
+    margin-top: 25px;
+}
+
+table.docutils {
+    border: 0;
+}
+
+table.docutils td, table.docutils th {
+    padding: 1px 8px 1px 0;
+    border-top: 0;
+    border-left: 0;
+    border-right: 0;
+    border-bottom: 1px solid #aaa;
+}
+
+table.field-list td, table.field-list th {
+    border: 0 !important;
+}
+
+table.footnote td, table.footnote th {
+    border: 0 !important;
+}
+
+.field-list ul {
+    margin: 0;
+    padding-left: 1em;
+}
+
+.field-list p {
+    margin: 0;
+}
+
+dl {
+    margin-bottom: 15px;
+    clear: both;
+}
+
+dd p {
+    margin-top: 0px;
+}
+
+dd ul, dd table {
+    margin-bottom: 10px;
+}
+
+dd {
+    margin-top: 3px;
+    margin-bottom: 10px;
+    margin-left: 30px;
+}
+
+.refcount {
+    color: #060;
+}
+
+dt:target,
+.highlight {
+    background-color: #fbe54e;
+}
+
+dl.glossary dt {
+    font-weight: bold;
+    font-size: 1.1em;
+}
+
+th {
+    text-align: left;
+    padding-right: 5px;
+}
+
+pre {
+    padding: 5px;
+    background-color: #efc;
+    color: #333;
+    border: 1px solid #ac9;
+    border-left: none;
+    border-right: none;
+    overflow: auto;
+}
+
+td.linenos pre {
+    padding: 5px 0px;
+    border: 0;
+    background-color: transparent;
+    color: #aaa;
+}
+
+table.highlighttable {
+    margin-left: 0.5em;
+}
+
+table.highlighttable td {
+    padding: 0 0.5em 0 0.5em;
+}
+
+tt {
+    background-color: #ecf0f3;
+    padding: 0 1px 0 1px;
+    font-size: 0.95em;
+}
+
+tt.descname {
+    background-color: transparent;
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+tt.descclassname {
+    background-color: transparent;
+}
+
+tt.xref, a tt {
+    background-color: transparent;
+    font-weight: bold;
+}
+
+.footnote:target  { background-color: #ffa }
+
+h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
+    background-color: transparent;
+}
+
+.optional {
+    font-size: 1.3em;
+}
+
+.versionmodified {
+    font-style: italic;
+}
+
+form.comment {
+    margin: 0;
+    padding: 10px 30px 10px 30px;
+    background-color: #eee;
+}
+
+form.comment h3 {
+    background-color: #326591;
+    color: white;
+    margin: -10px -30px 10px -30px;
+    padding: 5px;
+    font-size: 1.4em;
+}
+
+form.comment input,
+form.comment textarea {
+    border: 1px solid #ccc;
+    padding: 2px;
+    font-family: sans-serif;
+    font-size: 100%;
+}
+
+form.comment input[type="text"] {
+    width: 240px;
+}
+
+form.comment textarea {
+    width: 100%;
+    height: 200px;
+    margin-bottom: 10px;
+}
+
+.system-message {
+    background-color: #fda;
+    padding: 5px;
+    border: 3px solid red;
+}
+
+/* :::: PRINT :::: */
+@media print {
+    div.document,
+    div.documentwrapper,
+    div.bodywrapper {
+        margin: 0;
+        width : 100%;
+    }
+
+    div.sphinxsidebar,
+    div.related,
+    div.footer,
+    div#comments div.new-comment-box,
+    #top-link {
+        display: none;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_templates/layout.html	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,196 @@
+{%- block doctype -%}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+{%- endblock %}
+{%- set reldelim1 = reldelim1 is not defined and ' &raquo;' or reldelim1 %}
+{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
+{%- macro relbar() %}
+    <div class="related">
+      <h3>Navigation</h3>
+      <ul>
+        {%- for rellink in rellinks %}
+        <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
+          <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags }}"
+             accesskey="{{ rellink[2] }}">{{ rellink[3] }}</a>
+          {%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
+        {%- endfor %}
+        {%- block rootrellink %}
+        <li><a href="{{ pathto('index') }}">{{ shorttitle }}</a>{{ reldelim1 }}</li>
+        {%- endblock %}
+        {%- for parent in parents %}
+          <li><a href="{{ parent.link|e }}" accesskey="U">{{ parent.title }}</a>{{ reldelim1 }}</li>
+        {%- endfor %}
+        {%- block relbaritems %}{% endblock %}
+      </ul>
+    </div>
+{%- endmacro %}
+{%- macro sidebar() %}
+      {%- if builder != 'htmlhelp' %}
+      <div class="sphinxsidebar">
+        <div class="sphinxsidebarwrapper">
+          {%- block sidebarlogo %}
+          {%- if logo %}
+            <p class="logo"><img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/></p>
+          {%- endif %}
+          {%- endblock %}
+          {%- block sidebartoc %}
+          {%- if display_toc %}
+            <h3>Table Of Contents</h3>
+            {{ toc }}
+          {%- endif %}
+          {%- endblock %}
+          {%- block sidebarrel %}
+          {%- if prev %}
+            <h4>Previous topic</h4>
+            <p class="topless"><a href="{{ prev.link|e }}" title="previous chapter">{{ prev.title }}</a></p>
+          {%- endif %}
+          {%- if next %}
+            <h4>Next topic</h4>
+            <p class="topless"><a href="{{ next.link|e }}" title="next chapter">{{ next.title }}</a></p>
+          {%- endif %}
+          {%- endblock %}
+          {%- if sourcename %}
+            <!--<h3>This Page</h3>
+            <ul class="this-page-menu">
+            {%- if builder == 'web' %}
+              <li><a href="#comments">Comments ({{ comments|length }} so far)</a></li>
+              <li><a href="{{ pathto('@edit/' + sourcename)|e }}">Suggest Change</a></li>
+              <li><a href="{{ pathto('@source/' + sourcename)|e }}">Show Source</a></li>
+            {%- elif builder == 'html' %}
+              <li><a href="{{ pathto('_sources/' + sourcename, true)|e }}">Show Source</a></li>
+            {%- endif %}
+            </ul>-->
+          {%- endif %}
+          {%- if customsidebar %}
+          {{ rendertemplate(customsidebar) }}
+          {%- endif %}
+          {%- block sidebarsearch %}
+          {%- if pagename != "search" %}
+            <h3>{{ builder == 'web' and 'Keyword' or 'Quick' }} search</h3>
+            <form class="search" action="{{ pathto('search') }}" method="get">
+              <input type="text" name="q" size="18" /> <input type="submit" value="Go" />
+              <input type="hidden" name="check_keywords" value="yes" />
+              <input type="hidden" name="area" value="default" />
+            </form>
+            {%- if builder == 'web' %}
+            <p style="font-size: 90%">Enter a module, class or function name.</p>
+            {%- endif %}
+          {%- endif %}
+          {%- endblock %}
+        </div>
+      </div>
+      {%- endif %}
+{%- endmacro -%}
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    {%- if builder != 'htmlhelp' %}
+      {%- set titlesuffix = " &mdash; " + docstitle %}
+    {%- endif %}
+    <title>{{ title|striptags }}{{ titlesuffix }}</title>
+    {%- if builder == 'web' %}
+    <link rel="stylesheet" href="{{ pathto('index') }}?do=stylesheet{%
+      if in_admin_panel %}&admin=yes{% endif %}" type="text/css" />
+    {%- for link, type, title in page_links %}
+    <link rel="alternate" type="{{ type|e(true) }}" title="{{ title|e(true) }}" href="{{ link|e(true) }}" />
+    {%- endfor %}
+    {%- else %}
+    <link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
+    <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
+    {%- endif %}
+    {%- if builder != 'htmlhelp' %}
+    <script type="text/javascript">
+      var DOCUMENTATION_OPTIONS = {
+          URL_ROOT:    '{{ pathto("", 1) }}',
+          VERSION:     '{{ release }}',
+          COLLAPSE_MODINDEX: false,
+          FILE_SUFFIX: '{{ file_suffix }}'
+      };
+    </script>
+    <script type="text/javascript" src="{{ pathto('_static/jquery.js', 1) }}"></script>
+    <script type="text/javascript" src="{{ pathto('_static/interface.js', 1) }}"></script>
+    <script type="text/javascript" src="{{ pathto('_static/doctools.js', 1) }}"></script>
+    <script type="text/javascript" src="{{ pathto('_static/searchtools.js', 1) }}"></script>
+    {%- if use_opensearch %}
+    <link rel="search" type="application/opensearchdescription+xml"
+          title="Search within {{ docstitle }}"
+          href="{{ pathto('_static/opensearch.xml', 1) }}"/>
+    {%- endif %}
+    {%- if favicon %}
+    <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
+    {%- endif %}
+    {%- endif %}
+{%- block rellinks %}
+    {%- if hasdoc('about') %}
+    <link rel="author" title="About these documents" href="{{ pathto('about') }}" />
+    {%- endif %}
+    <link rel="contents" title="Global table of contents" href="{{ pathto('contents') }}" />
+    <link rel="index" title="Global index" href="{{ pathto('genindex') }}" />
+    <link rel="search" title="Search" href="{{ pathto('search') }}" />
+    {%- if hasdoc('copyright') %}
+    <link rel="copyright" title="Copyright" href="{{ pathto('copyright') }}" />
+    {%- endif %}
+    <link rel="top" title="{{ docstitle }}" href="{{ pathto('index') }}" />
+    {%- if parents %}
+    <link rel="up" title="{{ parents[-1].title|striptags }}" href="{{ parents[-1].link|e }}" />
+    {%- endif %}
+    {%- if next %}
+    <link rel="next" title="{{ next.title|striptags }}" href="{{ next.link|e }}" />
+    {%- endif %}
+    {%- if prev %}
+    <link rel="prev" title="{{ prev.title|striptags }}" href="{{ prev.link|e }}" />
+    {%- endif %}
+{%- endblock %}
+{%- block extrahead %}{% endblock %}
+  </head>
+  <body>
+
+{% block logilablogo %}
+<div class="logilablogo">
+	<a class="logogo" href="http://www.cubicweb.org"><img border="0" src="{{ pathto('_static/cubicweb.png', 1) }}"/></a>
+  </div>
+{% endblock %}
+
+{%- block relbar1 %}{{ relbar() }}{% endblock %}
+
+{%- block sidebar1 %}{# possible location for sidebar #}{% endblock %}
+
+{%- block document %}
+    <div class="document">
+      <div class="documentwrapper">
+      {%- if builder != 'htmlhelp' %}
+        <div class="bodywrapper">
+      {%- endif %}
+          <div class="body">
+            {% block body %}{% endblock %}
+          </div>
+      {%- if builder != 'htmlhelp' %}
+        </div>
+      {%- endif %}
+      </div>
+{%- endblock %}
+
+{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
+      <div class="clearer"></div>
+    </div>
+
+{%- block relbar2 %}{{ relbar() }}{% endblock %}
+
+{%- block footer %}
+    <div class="footer">
+    {%- if hasdoc('copyright') %}
+      &copy; <a href="{{ pathto('copyright') }}">Copyright</a> {{ copyright }}.
+    {%- else %}
+      &copy; Copyright {{ copyright }}.
+    {%- endif %}
+    {%- if last_updated %}
+      Last updated on {{ last_updated }}.
+    {%- endif %}
+    {%- if show_sphinx %}
+      Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
+    {%- endif %}
+    </div>
+{%- endblock %}
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/layout.html	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,61 @@
+{% extends "basic/layout.html" %}
+
+{%- block extrahead %}
+<!--[if lte IE 6]>
+<link rel="stylesheet" href="{{ pathto('_static/ie6.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
+<![endif]-->
+{%- if theme_favicon %}
+<link rel="shortcut icon" href="{{ pathto('_static/'+theme_favicon, 1) }}"/>
+{%- endif %}
+
+{%- if theme_canonical_url %}
+<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
+{%- endif %}
+{% endblock %}
+
+{% block header %}
+
+{% if theme_in_progress|tobool %}
+    <img style="position: fixed; display: block; width: 165px; height: 165px; bottom: 60px; right: 0; border: 0;" src="{{ pathto('_static/in_progress.png', 1) }}" alt="Documentation in progress" />
+{% endif %}
+
+{% if theme_outdated|tobool %}
+    <div style="bottom: 60px; right: 20px;position: fixed;"><a href="{{ latest_url }}" class="btn btn-large btn-danger"><strong>&gt;</strong> Read the latest version of this page</a></div>
+{% endif %}
+
+<div class="header-small">
+	{%- if theme_logo %}
+	{% set img, ext = theme_logo.split('.', -1) %}
+	<div class="logo-small">
+		<a href="{{ pathto(master_doc) }}">
+      		<img class="logo" src="{{ pathto('_static/%s-small.%s' % (img, ext), 1)}}" alt="Logo"/>
+		</a>
+  	</div>
+  	{%- endif %}
+</div>
+{% endblock %}
+
+{%- macro relbar() %}
+<div class="related">
+	<h3>{{ _('Navigation') }}</h3>
+	<ul>
+		{%- for rellink in rellinks %}
+		<li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
+			<a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags|e }}"
+			{{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a>
+			{%- if not loop.first %}{{ reldelim2 }}{% endif %}
+		</li>
+		{%- endfor %}
+    	{%- block rootrellink %}
+    	<li><a href="{{ pathto(master_doc) }}">{{ docstitle|e }}</a>{{ reldelim1 }}</li>
+    	{%- endblock %}
+    	{%- for parent in parents %}
+          <li><a href="{{ parent.link|e }}" {% if loop.last %}{{ accesskey("U") }}{% endif %}>{{ parent.title }}</a>{{ reldelim1 }}</li>
+        {%- endfor %}
+        {%- block relbaritems %} {% endblock %}
+  	</ul>
+</div>
+{%- endmacro %}
+
+{%- block sidebarlogo %}{%- endblock %}
+{%- block sidebarsourcelink %}{%- endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/cubicweb.css_t	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,33 @@
+/*
+ * cubicweb.css_t
+ * ~~~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet -- cubicweb theme.
+ *
+ * :copyright: Copyright 2014 by the Cubicweb team, see AUTHORS.
+ * :license: LGPL, see LICENSE for details.
+ *
+ */
+ 
+@import url("pyramid.css");
+
+div.header-small {
+  background-image: linear-gradient(white, #e2e2e2);
+  border-bottom: 1px solid #bbb;
+}
+
+div.logo-small {
+  padding: 10px;
+}
+
+img.logo {
+  width: 150px;
+}
+
+div.related a {
+  color: #e6820e;
+}
+
+a, a .pre {
+  color: #e6820e;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/cubicweb.ico	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+../../../../web/data/favicon.ico
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/logo-cubicweb-small.svg	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+logo-cubicweb.svg
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/logo-cubicweb.svg	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+../../../../web/data/logo-cubicweb.svg
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/theme.conf	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,12 @@
+[theme]
+inherit = pyramid
+pygments_style = sphinx.pygments_styles.PyramidStyle
+stylesheet = cubicweb.css
+
+
+[options]
+logo = logo-cubicweb.svg
+favicon = cubicweb.ico
+in_progress = false
+outdated = false
+canonical_url = 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/__init__.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,115 @@
+.. _index_module:
+
+:mod:`cubicweb`
+===============
+
+.. automodule:: cubicweb
+
+    Exceptions
+    ----------
+
+    Base exceptions
+    ~~~~~~~~~~~~~~~
+
+    .. autoexception:: ProgrammingError
+        :show-inheritance:
+
+    .. autoexception:: CubicWebException
+        :show-inheritance:
+
+    .. autoexception:: InternalError
+        :show-inheritance:
+
+    .. autoexception:: SecurityError
+        :show-inheritance:
+
+    .. autoexception:: RepositoryError
+        :show-inheritance:
+
+    .. autoexception:: SourceException
+        :show-inheritance:
+
+    .. autoexception:: CubicWebRuntimeError
+        :show-inheritance:
+
+    Repository exceptions
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    .. autoexception:: ConnectionError
+        :show-inheritance:
+
+    .. autoexception:: AuthenticationError
+        :show-inheritance:
+
+    .. autoexception:: BadConnectionId
+        :show-inheritance:
+
+    .. autoexception:: UnknownEid
+        :show-inheritance:
+
+    .. autoexception:: UniqueTogetherError
+        :show-inheritance:
+
+    Security Exceptions
+    ~~~~~~~~~~~~~~~~~~~
+
+    .. autoexception:: Unauthorized
+        :show-inheritance:
+
+    .. autoexception:: Forbidden
+        :show-inheritance:
+
+    Source exceptions
+    ~~~~~~~~~~~~~~~~~
+
+    .. autoexception:: EidNotInSource
+        :show-inheritance:
+
+    Registry exceptions
+    ~~~~~~~~~~~~~~~~~~~
+
+    .. autoexception:: UnknownProperty
+        :show-inheritance:
+
+    Query exceptions
+    ~~~~~~~~~~~~~~~~
+
+    .. autoexception:: QueryError
+        :show-inheritance:
+
+    .. autoexception:: NotAnEntity
+        :show-inheritance:
+
+    .. autoexception:: MultipleResultsError
+        :show-inheritance:
+
+    .. autoexception:: NoResultError
+        :show-inheritance:
+
+    .. autoexception:: UndoTransactionException
+        :show-inheritance:
+
+
+    Misc
+    ~~~~
+
+    .. autoexception:: ConfigurationError
+        :show-inheritance:
+
+    .. autoexception:: ExecutionError
+        :show-inheritance:
+
+    .. autoexception:: BadCommandUsage
+        :show-inheritance:
+
+    .. autoexception:: ValidationError
+        :show-inheritance:
+
+
+    Utilities
+    ---------
+
+    .. autoclass:: Binary
+    .. autoclass:: CubicWebEventManager
+    .. autofunction:: onevent
+    .. autofunction:: validation_error
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/appobject.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,10 @@
+.. _appobject_module:
+
+:mod:`cubicweb.appobject`
+=========================
+
+.. automodule:: cubicweb.appobject
+
+   .. autoclass:: AppObject
+      :show-inheritance:
+      :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/cwvreg.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,45 @@
+.. _cwvreg_module:
+
+:mod:`cubicweb.cwvreg`
+======================
+
+.. automodule:: cubicweb.cwvreg
+
+    .. autoclass:: CWRegistryStore
+        :show-inheritance:
+        :members:
+        :undoc-members:
+
+    .. autoclass:: CWRegistry
+        :show-inheritance:
+        :members: schema, poss_visible_objects, select
+
+    .. autoclass:: InstancesRegistry
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: ETypeRegistry
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: ViewsRegistry
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: ActionsRegistry
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: CtxComponentsRegistry
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: BwCompatCWRegistry
+        :show-inheritance:
+        :members:
+
+
+:mod:`logilab.common.registry`
+==============================
+
+.. automodule:: logilab.common.registry
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/dataimport.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,63 @@
+.. _dataimport_module:
+
+:mod:`cubicweb.dataimport`
+==========================
+
+.. automodule:: cubicweb.dataimport
+
+    Utilities
+    ---------
+
+    .. autofunction:: count_lines
+
+    .. autofunction:: ucsvreader_pb
+
+    .. autofunction:: ucsvreader
+
+    .. autofunction:: callfunc_every
+
+    .. autofunction:: lazytable
+
+    .. autofunction:: lazydbtable
+
+    .. autofunction:: mk_entity
+
+    Sanitizing/coercing functions
+    -----------------------------
+
+    .. autofunction:: optional
+    .. autofunction:: required
+    .. autofunction:: todatetime
+    .. autofunction:: call_transform_method
+    .. autofunction:: call_check_method
+
+    Integrity functions
+    -------------------
+
+    .. autofunction:: check_doubles
+    .. autofunction:: check_doubles_not_none
+
+    Object Stores
+    -------------
+
+    .. autoclass:: ObjectStore
+        :members:
+
+    .. autoclass:: RQLObjectStore
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: NoHookRQLObjectStore
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: SQLGenObjectStore
+        :show-inheritance:
+        :members:
+
+    Import Controller
+    -----------------
+
+    .. autoclass:: CWImportController
+        :show-inheritance:
+        :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/predicates.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,57 @@
+.. _predicates_module:
+
+:mod:`cubicweb.predicates`
+==========================
+
+.. automodule:: cubicweb.predicates
+
+   .. autoclass:: cubicweb.appobject.yes
+   .. autoclass:: cubicweb.predicates.match_kwargs
+   .. autoclass:: cubicweb.predicates.appobject_selectable
+   .. autoclass:: cubicweb.predicates.adaptable
+   .. autoclass:: cubicweb.predicates.configuration_values
+   
+   .. autoclass:: cubicweb.predicates.none_rset
+   .. autoclass:: cubicweb.predicates.any_rset
+   .. autoclass:: cubicweb.predicates.nonempty_rset
+   .. autoclass:: cubicweb.predicates.empty_rset
+   .. autoclass:: cubicweb.predicates.one_line_rset
+   .. autoclass:: cubicweb.predicates.multi_lines_rset
+   .. autoclass:: cubicweb.predicates.multi_columns_rset
+   .. autoclass:: cubicweb.predicates.paginated_rset
+   .. autoclass:: cubicweb.predicates.sorted_rset
+   .. autoclass:: cubicweb.predicates.one_etype_rset
+   .. autoclass:: cubicweb.predicates.multi_etypes_rset
+   
+   .. autoclass:: cubicweb.predicates.non_final_entity
+   .. autoclass:: cubicweb.predicates.is_instance
+   .. autoclass:: cubicweb.predicates.score_entity
+   .. autoclass:: cubicweb.predicates.rql_condition
+   .. autoclass:: cubicweb.predicates.relation_possible
+   .. autoclass:: cubicweb.predicates.partial_relation_possible
+   .. autoclass:: cubicweb.predicates.has_related_entities
+   .. autoclass:: cubicweb.predicates.partial_has_related_entities
+   .. autoclass:: cubicweb.predicates.has_permission
+   .. autoclass:: cubicweb.predicates.has_add_permission
+   .. autoclass:: cubicweb.predicates.has_mimetype
+   .. autoclass:: cubicweb.predicates.is_in_state
+   .. autofunction:: cubicweb.predicates.on_fire_transition
+   
+   .. autoclass:: cubicweb.predicates.match_user_groups
+   
+   .. autoclass:: cubicweb.predicates.no_cnx
+   .. autoclass:: cubicweb.predicates.anonymous_user
+   .. autoclass:: cubicweb.predicates.authenticated_user
+   .. autoclass:: cubicweb.predicates.match_form_params
+   .. autoclass:: cubicweb.predicates.match_search_state
+   .. autoclass:: cubicweb.predicates.match_context_prop
+   .. autoclass:: cubicweb.predicates.match_context
+   .. autoclass:: cubicweb.predicates.match_view
+   .. autoclass:: cubicweb.predicates.primary_view
+   .. autoclass:: cubicweb.predicates.contextual
+   .. autoclass:: cubicweb.predicates.specified_etype_implements
+   .. autoclass:: cubicweb.predicates.attribute_edited
+   .. autoclass:: cubicweb.predicates.match_transition
+   
+   .. autoclass:: cubicweb.predicates.match_exception
+   .. autoclass:: cubicweb.predicates.debug_mode
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/req.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,11 @@
+.. _req_module:
+
+:mod:`cubicweb.req`
+===================
+
+.. automodule:: cubicweb.req
+
+    .. autoexception:: FindEntityError
+
+    .. autoclass:: RequestSessionBase
+        :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/rset.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,10 @@
+.. _rset_module:
+
+:mod:`cubicweb.rset`
+====================
+
+.. automodule:: cubicweb.rset
+
+    .. autoclass:: ResultSet
+        :members:
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/urlpublishing.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,36 @@
+.. _urlpublishing_module:
+
+:mod:`cubicweb.web.views.urlpublishing`
+=======================================
+
+.. automodule:: cubicweb.web.views.urlpublishing
+
+    .. autoexception:: PathDontMatch
+
+    .. autoclass:: URLPublisherComponent
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: URLPathEvaluator
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: RawPathEvaluator
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: EidPathEvaluator
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: RestPathEvaluator
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: URLRewriteEvaluator
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: ActionPathEvaluator
+        :show-inheritance:
+        :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/urlrewrite.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,18 @@
+.. _urlrewrite_module:
+
+:mod:`cubicweb.web.views.urlrewrite`
+=======================================
+
+.. automodule:: cubicweb.web.views.urlrewrite
+
+    .. autoclass:: URLRewriter
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: SimpleReqRewriter
+        :show-inheritance:
+        :members:
+
+    .. autoclass:: SchemaBasedRewriter
+        :show-inheritance:
+        :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/web.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,21 @@
+.. _web_module:
+
+:mod:`cubicweb.web`
+===================
+
+.. automodule:: cubicweb.web
+
+    Exceptions
+    ----------
+
+    .. autoexception:: DirectResponse
+    .. autoexception:: InvalidSession
+    .. autoexception:: PublishException
+    .. autoexception:: LogOut
+    .. autoexception:: Redirect
+    .. autoexception:: StatusResponse
+    .. autoexception:: RequestError
+    .. autoexception:: NothingToEdit
+    .. autoexception:: ProcessFormError
+    .. autoexception:: NotFound
+    .. autoexception:: RemoteCallFailed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/MERGE_ME-tut-create-app.en.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,386 @@
+.. -*- coding: utf-8 -*-
+
+
+Tutoriel : créer votre première application web pour Google AppEngine
+=====================================================================
+
+[TRANSLATE ME TO FRENCH]
+
+This tutorial will guide you step by step to build a blog application 
+and discover the unique features of `LAX`. It assumes that you followed
+the :ref:`installation` guidelines and that both the `AppEngine SDK` and the
+`LAX` framework are setup on your computer.
+
+Creating a new application
+--------------------------
+
+We choosed in this tutorial to develop a blog as an example of web application
+and will go through each required steps/actions to have it running with `LAX`.
+When you installed `LAX`, you saw a directory named ``skel``. Make a copy of
+this directory and call it ``BlogDemo``.
+
+The location of this directory does not matter. But once decided, make sure your ``PYTHONPATH`` is properly set (:ref:`installation`).
+
+
+Defining a schema
+-----------------
+
+With `LAX`, the schema/datamodel is the core of the application. This is where
+you will define the type of content you have to hanlde in your application.
+
+Let us start with something simple and improve on it iteratively. 
+
+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 by the class EntityType 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`.
+
+Running the application
+-----------------------
+
+Defining this simple schema is enough to get us started. Make sure you
+followed the setup steps described in detail in the installation
+chapter (especially visiting http://localhost:8080/_load as an
+administrator), then launch the application with the command::
+
+   python dev_appserver.py BlogDemo
+
+and point your browser at http://localhost:8080/ (if it is easier for
+you, use the on-line demo at http://lax.appspot.com/).
+
+.. image:: images/lax-book.00-login.en.png
+   :alt: login screen
+
+After you log in, you will see the home page of your application. It
+lists the entity types: Blog and BlogEntry. If these links read
+``blog_plural`` and ``blogentry_plural`` it is because
+internationalization (i18n) is not working for you yet. Please ignore
+this for now.
+
+.. image:: images/lax-book.01-start.en.png
+   :alt: home page
+
+Creating system entities
+------------------------
+You can only create new users if you decided not to use google authentication.
+
+
+[WRITE ME : create users manages permissions etc]
+
+
+
+Creating application entites
+----------------------------
+
+Create a Blog
+~~~~~~~~~~~~~
+
+Let us create a few of these entities. 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 ``Validate``.
+
+.. image:: images/lax-book.02-create-blog.en.png
+   :alt: from to create blog
+
+Click on the logo at top left to get back to the home page, then
+follow the Blog link that will list for you all the existing Blog.
+You should be seeing a list with a single item ``Tech-blog`` you
+just created.
+
+.. image:: images/lax-book.03-list-one-blog.en.png
+   :alt: displaying a list of a single 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``.
+
+.. image:: images/lax-book.04-detail-one-blog.en.png
+  :alt: displaying the detailed view of a blog
+
+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.
+
+.. image:: images/lax-book.05-list-two-blog.en.png
+   :alt: displaying a list of two blogs
+
+
+Create a BlogEntry
+~~~~~~~~~~~~~~~~~~
+
+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 ``Validate``. 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``. 
+
+You could also have, at the time you started to fill the form for a
+new entity BlogEntry, hit ``Apply`` instead of ``Validate`` and the 
+combobox titled ``add relation`` would have showed up.
+
+.. image:: images/lax-book.06-add-relation-entryof.en.png
+   :alt: editing a blog entry to add a relation to a blog
+
+Validate the changes by clicking ``Validate``. The entity BlogEntry
+that is displayed now includes a link to the entity Blog named
+``MyLife``.
+
+.. image:: images/lax-book.07-detail-one-blogentry.en.png
+   :alt: displaying the detailed view of a blogentry
+
+Remember that all of this was handled by the framework and that the
+only input that was provided so far is the schema. To get a graphical
+view of the schema, run the ``laxctl genschema BlogDemo`` command as
+explained in the installation section and point your browser to the
+URL http://localhost:8080/schema
+
+.. image:: images/lax-book.08-schema.en.png
+   :alt: graphical view of the schema (aka data-model)
+
+Site configuration
+------------------
+
+.. image:: images/lax-book.03-site-config-panel.en.png
+
+This panel allows you to configure the appearance of your application site.
+Six menus are available and we will go through each of them to explain how
+to use them.
+
+Navigation
+~~~~~~~~~~
+This menu provides you a way to adjust some navigation options depending on
+your needs, such as the number of entities to display by page of results.
+Follows the detailled list of available options:
+  
+* navigation.combobox-limit: maximum number of entities to display in related
+  combo box (sample format: 23)
+* navigation.page-size: maximum number of objects displayed by page of results 
+  (sample format: 23)
+* navigation.related-limit: maximum number of related entities to display in 
+  the primary view (sample format: 23)
+* navigation.short-line-size: maximum number of characters in short description
+  (sample format: 23)
+
+UI
+~~
+This menu provides you a way to customize the user interface settings such as
+date format or encoding in the produced html.
+Follows the detailled list of available options:
+
+* ui.date-format : how to format date in the ui ("man strftime" for format description)
+* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
+  description)
+* ui.default-text-format : default text format for rich text fields.
+* ui.encoding : user interface encoding
+* ui.fckeditor : 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.
+* ui.float-format : how to format float numbers in the ui
+* ui.language : language of the user interface
+* ui.main-template : id of main template used to render pages
+* ui.site-title	: site title, which is displayed right next to the logo in the header
+* ui.time-format : how to format time in the ui ("man strftime" for format description)
+
+
+Actions
+~~~~~~~
+This menu provides a way to configure the context in which you expect the actions
+to be displayed to the user and if you want the action to be visible or not. 
+You must have notice that when you view a list of entities, an action box is 
+available on the left column which display some actions as well as a drop-down 
+menu for more actions. 
+
+The context available are:
+
+* mainactions : actions listed in the left box
+* moreactions : actions listed in the `more` menu of the left box
+* addrelated : add actions listed in the left box
+* useractions : actions listed in the first section of drop-down menu 
+  accessible from the right corner user login link
+* siteactions : actions listed in the second section of drop-down menu
+  accessible from the right corner user login link
+* hidden : select this to hide the specific action
+
+Boxes
+~~~~~
+The application has already a pre-defined set of boxes you can use right away. 
+This configuration section allows you to place those boxes where you want in the
+application interface to customize it. 
+
+The available boxes are:
+
+* actions box : box listing the applicable actions on the displayed data
+
+* boxes_blog_archives_box : box listing the blog archives 
+
+* possible views box : box listing the possible views for the displayed data
+
+* rss box : RSS icon to get displayed data as a RSS thread
+
+* search box : search box
+
+* startup views box : box listing the configuration options available for 
+  the application site, such as `Preferences` and `Site Configuration`
+
+Components
+~~~~~~~~~~
+[WRITE ME]
+
+Contextual components
+~~~~~~~~~~~~~~~~~~~~~
+[WRITE ME]
+
+Set-up a workflow
+-----------------
+
+Before starting, make sure you refresh your mind by reading [link to
+definition_workflow chapter].
+
+We want to create a workflow to control the quality of the BlogEntry 
+submitted on your application. When a BlogEntry is created by a user
+its state should be `submitted`. To be visible to all, it needs to
+be in the state `published`. To move from `submitted` to `published`
+we need a transition that we can name `approve_blogentry`.
+
+We do not want every user to be allowed to change the state of a 
+BlogEntry. We need to define a group of user, `moderators`, and 
+this group will have appropriate permissions to approve BlogEntry
+to be published and visible to all.
+
+There are two ways to create a workflow, form the user interface,
+and also by defining it in ``migration/postcreate.py``. This script
+is executed each time a new ``./bin/laxctl db-init`` is done. 
+If you create the states and transitions through the user interface
+this means that next time you will need to initialize the database
+you will have to re-create all the entities. 
+We strongly recommand you create the workflow in ``migration\postcreate.py``
+and we will now show you how.
+The user interface would only be a reference for you to view the states 
+and transitions but is not the appropriate interface to define your
+application workflow.
+
+Update the schema
+~~~~~~~~~~~~~~~~~
+To enable a BlogEntry to have a State, we have to define a relation
+``in_state`` in the schema of BlogEntry. Please do as follows, add
+the line ``in_state (...)``::
+
+  class BlogEntry(EntityType):
+      title = String(maxsize=100, required=True)
+      publish_date = Date(default='TODAY')
+      text_format = String(meta=True, internationalizable=True, maxsize=50,
+                           default='text/rest', constraints=[format_constraint])
+      text = String(fulltextindexed=True)
+      category = String(vocabulary=('important','business'))
+      entry_of = SubjectRelation('Blog', cardinality='?*')
+      in_state = SubjectRelation('State', cardinality='1*')
+
+As you updated the schema, you will have re-execute ``./bin/laxctl db-init``
+to initialize the database and migrate your existing entities.
+[WRITE ABOUT MIGRATION]
+
+Create states, transitions and group permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+At the time the ``postcreate.py`` script is executed, several methods
+can be used. They are all defined in the ``class ServerMigrationHelper``.
+We will only discuss the method we use to create a wrokflow here.
+
+To define our workflow for BlogDemo, please add the following lines
+to ``migration/postcreate.py``::
+  
+  _ = unicode
+
+  moderators      = add_entity('CWGroup', name=u"moderators")
+
+  submitted = add_state(_('submitted'), 'BlogEntry', initial=True)
+  published = add_state(_('published'), 'BlogEntry')
+
+  add_transition(_('approve_blogentry'), 'BlogEntry', (submitted,), published, ('moderators', 'managers'),)
+
+  checkpoint()
+
+``add_entity`` is used here to define the new group of users that we
+need to define the transitions, `moderators`.
+If this group required by the transition is not defined before the
+transition is created, it will not create the relation `transition 
+require the group moderator`.
+
+``add_state`` expects as the first argument the name of the state you are
+willing to create, then the entity type on which the state can be applied, 
+and an optionnal argument to set if the state is the initial state
+of the entity type or not.
+
+``add_transition`` expects as the first argument the name of the 
+transition, then the entity type on which we can apply the transition,
+then the list of possible initial states from which the transition
+can be applied, the target state of the transition, and the permissions
+(e.g. list of the groups of users who can apply the transition).
+
+.. image:: images/lax-book.03-transitions-view.en.png
+
+You can now notice that in the actions box of a BlogEntry, the state
+is now listed as well as the possible transitions from this state
+defined by the workflow. This transition, as defined in the workflow,
+will only being displayed for the users belonging to the group
+moderators of managers.
+
+Change view permission
+~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+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 ?
+~~~~~~~~~~~~~~
+
+Although the application is fully functionnal, its look is very
+basic. In the following section we will learn to create views to
+customize how data is displayed.
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/MERGE_ME-tut-create-gae-app.en.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,218 @@
+.. -*- coding: utf-8 -*-
+
+.. _tutorielGAE:
+
+Tutoriel : créer votre première application web pour Google AppEngine
+=====================================================================
+
+Ce tutoriel va vous guider pas à pas a construire une apllication web 
+de gestion de Blog afin de vous faire découvrir les fonctionnalités de
+*CubicWeb*.
+
+Nous supposons que vous avec déjà suivi le guide :ref:`installationGAE`.
+
+
+Créez une nouvelle application
+------------------------------
+
+Nous choisissons dans ce tutoriel de développer un blog comme un exemple
+d'application web et nous allons expliciter toutes les étapes nécessaires
+à sa réalisation.  
+
+::
+  
+  cubicweb-ctl newgapp blogdemo
+
+`newgapp` est la commande permettant de créer une instance *CubicWeb* pour
+le datastore.
+
+Assurez-vous que votre variable d'environnement ``PYTHONPATH`` est correctement
+initialisée (:ref:`installationGAE`)
+
+Définissez un schéma
+--------------------
+
+Le modèle de données ou schéma est au coeur d'une application *CubicWeb*.
+C'est là où vous allez devoir définir le type de contenu que votre application
+devra gérer.
+
+Commençons par un schéma simple que nous améliorerons progressivemment.
+
+Une fois votre instance ``blogdemo`` crée, vous trouverez un fichier ``schema.py``
+contenant la définition des entités suivantes : ``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='?*')
+
+
+Un ``Blog`` a un titre et une description. Le titre est une chaîne 
+de caractères requise par la classe parente EntityType and ne doit
+pas excéder 50 caractères. La description est une chaîne de 
+caractères sans contraintes.
+
+Une ``BlogEntry`` a un titre, une date de publication et du texte
+étant son contenu. Le titre est une chaîne de caractères qui ne 
+doit pas excéder 100 caractères. La date de publication est de type Date et a
+pour valeur par défaut TODAY, ce qui signifie que lorsqu'une 
+``BlogEntry`` sera créée, sa date de publication sera la date
+courante a moins de modifier ce champ. Le texte est une chaîne de
+caractères qui sera indexée en plein texte et sans contraintes.
+
+Une ``BlogEntry`` a aussi une relation nommée ``entry_of`` qui la
+relie à un ``Blog``. La cardinalité ``?*`` signifie que BlogEntry
+peut faire partie de zero a un Blog (``?`` signifie `zero ou un`) et
+qu'un Blog peut avoir une infinité de BlogEntry (``*`` signifie
+`n'importe quel nombre incluant zero`). 
+Par soucis de complétude, nous rappellerons que ``+`` signifie
+`un ou plus`.
+
+Lancez l'application
+--------------------
+
+Définir ce simple schéma est suffisant pour commencer. Assurez-vous 
+que vous avez suivi les étapes décrites dans la section installation
+(en particulier visitez http://localhost:8080/_load en tant qu'administrateur
+afin d'initialiser le datastore), puis lancez votre application avec la commande ::
+   
+   python dev_appserver.py BlogDemo
+
+puis dirigez vous vers http://localhost:8080/ (ou si c'est plus facile
+vous pouvez utiliser la démo en ligne http://lax.appspot.com/).
+[FIXME] -- changer la demo en ligne en quelque chose qui marche (!)
+
+.. image:: images/lax-book.00-login.en.png
+   :alt: login screen
+
+Après vous être authentifié, vous arrivez sur la page d'accueil de votre 
+application. Cette page liste les types d'entités accessibles dans votre
+application, en l'occurrence : Blog et Articles. Si vous lisez ``blog_plural``
+et ``blogentry_plural`` cela signifie que l'internationalisation (i18n)
+n'a pas encore fonctionné. Ignorez cela pour le moment.
+
+.. image:: images/lax-book.01-start.en.png
+   :alt: home page
+
+Créez des entités système
+-------------------------
+
+Vous ne pourrez créer de nouveaux utilisateurs que dans le cas où vous
+avez choisi de ne pas utiliser l'authentification Google.
+
+
+[WRITE ME : create users manages permissions etc]
+
+
+
+Créez des entités applicatives
+------------------------------
+
+Créez un Blog
+~~~~~~~~~~~~~
+
+Créons à présent quelques entités. Cliquez sur `[+]` sur la
+droite du lien Blog. Appelez cette nouvelle entité Blog ``Tech-Blog``
+et tapez pour la description ``everything about technology``,
+puis validez le formulaire d'édition en cliquant sur le bouton
+``Validate``.
+
+
+.. image:: images/lax-book.02-create-blog.en.png
+   :alt: from to create blog
+
+En cliquant sur le logo situé dans le coin gauche de la fenêtre,
+vous allez être redirigé vers la page d'accueil. Ensuite, si vous allez 
+sur le lien Blog, vous devriez voir la liste des entités Blog, en particulier
+celui que vous venez juste de créer ``Tech-Blog``.
+
+.. image:: images/lax-book.03-list-one-blog.en.png
+   :alt: displaying a list of a single blog
+
+Si vous cliquez sur ``Tech-Blog`` vous devriez obtenir une description
+détaillée, ce qui dans notre cas, n'est rien de plus que le titre
+et la phrase ``everything about technology``
+
+
+.. image:: images/lax-book.04-detail-one-blog.en.png
+   :alt: displaying the detailed view of a blog
+
+Maintenant retournons sur la page d'accueil et créons un nouveau
+Blog ``MyLife`` et retournons sur la page d'accueil, puis suivons
+le lien Blog et nous constatons qu'à présent deux blogs sont listés.
+
+.. image:: images/lax-book.05-list-two-blog.en.png
+   :alt: displaying a list of two blogs
+
+Créons un article
+~~~~~~~~~~~~~~~~~
+
+Revenons sur la page d'accueil et cliquons sur `[+]` à droite du lien
+`articles`. Appellons cette nouvelle entité ``Hello World`` et introduisons
+un peut de texte avant de ``Valider``. Vous venez d'ajouter un article
+sans avoir précisé à quel Blog il appartenait. Dans la colonne de gauche
+se trouve une boite intitulé ``actions``, cliquez sur le menu ``modifier``.
+Vous êtes de retour sur le formulaire d'édition de l'article que vous 
+venez de créer, à ceci près que ce formulaire a maintenant une nouvelle
+section intitulée ``ajouter relation``. Choisissez ``entry_of`` dans ce menu,
+cela va faire apparaitre une deuxième menu déroulant dans lequel vous
+allez pouvoir séléctionner le Blog ``MyLife``.
+
+Vous auriez pu aussi, au moment où vous avez crée votre article, sélectionner
+``appliquer`` au lieu de ``valider`` et le menu ``ajouter relation`` serait apparu.
+
+.. image:: images/lax-book.06-add-relation-entryof.en.png
+   :alt: editing a blog entry to add a relation to a blog
+
+Validez vos modifications en cliquant sur ``Valider``. L'entité article
+qui est listée contient maintenant un lien vers le Blog auquel il 
+appartient, ``MyLife``.
+
+.. image:: images/lax-book.07-detail-one-blogentry.en.png
+   :alt: displaying the detailed view of a blogentry
+
+Rappelez-vous que pour le moment, tout a été géré par la plate-forme
+*CubicWeb* et que la seule chose qui a été fournie est le schéma de
+données. D'ailleurs pour obtenir une vue graphique du schéma, exécutez
+la commande ``laxctl genschema blogdemo`` et vous pourrez visualiser
+votre schéma a l'URL suivante : http://localhost:8080/schema
+
+.. image:: images/lax-book.08-schema.en.png
+   :alt: graphical view of the schema (aka data-model)
+
+
+Change view permission
+~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+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 ?
+~~~~~~~~~~~~~~
+
+Although the application is fully functionnal, its look is very
+basic. In the following section we will learn to create views to
+customize how data is displayed.
+
+
--- a/doc/book/README	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-====
-Book
-====
-
-----
-Part
-----
-
-Chapter
-=======
-
-.. _Level1AnchorForLaterReference:
-
-Level 1 section
----------------
-
-Level 2 section
-~~~~~~~~~~~~~~~
-
-Level 3 section
-```````````````
-
-
-
-*CubicWeb*
-
-
-inline directives:
-  :file:`directory/file`
-  :envvar:`AN_ENV_VARIABLE`
-  :command:`command --option arguments`
-
-  :ref:, :mod:
-
-
-.. sourcecode:: python
-
-   class SomePythonCode:
-     ...
-
-.. XXX a comment, wont be rendered
-
-
-a [foot note]_
-
-.. [foot note] the foot note content
-
-
-Boxes
-=====
-
-- warning box: 
-    .. warning::
-
-       Warning content
-- note box:
-    .. note::
-
-       Note content
-
-
-
-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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/additionnal_services/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,14 @@
+Additional services
+===================
+
+In this chapter, we introduce services crossing the *web -
+repository - administration* organisation of the first parts of the
+CubicWeb book. Those services can be either proper services (like the
+undo functionality) or mere *topical cross-sections* across CubicWeb.
+
+.. toctree::
+   :maxdepth: 2
+
+   undo
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/additionnal_services/undo.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,337 @@
+Undoing changes in CubicWeb
+---------------------------
+
+Many desktop applications offer the possibility for the user to
+undo its last changes : this *undo feature* has now been
+integrated into the CubicWeb framework. This document will
+introduce you to the *undo feature* both from the end-user and the
+application developer point of view.
+
+But because a semantic web application and a common desktop
+application are not the same thing at all, especially as far as
+undoing is concerned, we will first introduce *what* is the *undo
+feature* for now.
+
+What's *undoing* in a CubicWeb application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+What is an *undo feature* is quite intuitive in the context of a
+desktop application. But it is a bit subtler in the context of a
+Semantic Web application. This section introduces some of the main
+differences between a classical desktop and a Semantic Web
+applications to keep in mind in order to state precisely *what we
+want*.
+
+The notion transactions
+```````````````````````
+
+A CubicWeb application acts upon an *Entity-Relationship* model,
+described by a schema. This allows to ensure some data integrity
+properties. It also implies that changes are made by all-or-none
+groups called *transactions*, such that the data integrity is
+preserved whether the transaction is completely applied *or* none
+of it is applied.
+
+A transaction can thus include more actions than just those
+directly required by the main purpose of the user.  For example,
+when a user *just* writes a new blog entry, the underlying
+*transaction* holds several *actions* as illustrated below :
+
+* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
+
+  #. Created Blog entry : Torototo
+  #. Added relation : Torototo owned by admin
+  #. Added relation : Torototo blog entry of Undo Blog
+  #. Added relation : Torototo in state draft (draft)
+  #. Added relation : Torototo created by admin
+
+Because of the very nature (all-or-none) of the transactions, the
+"undoable stuff" are the transactions and not the actions !
+
+Public and private actions within a transaction
+```````````````````````````````````````````````
+
+Actually, within the *transaction* "Created Blog entry :
+Torototo", two of those *actions* are said to be *public* and
+the others are said to be *private*. *Public* here means that the
+public actions (1 and 3) were directly requested by the end user ;
+whereas *private* means that the other actions (2, 4, 5) were
+triggered "under the hood" to fulfill various requirements for the
+user operation (ensuring integrity, security, ... ).
+
+And because quite a lot of actions can be triggered by a "simple"
+end-user request, most of which the end-user is not (and does not
+need or wish to be) aware, only the so-called public actions will
+appear [1]_ in the description of the an undoable transaction.
+
+* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
+
+  #. Created Blog entry : Torototo
+  #. Added relation : Torototo blog entry of Undo Blog
+
+But note that both public and private actions will be undone
+together when the transaction is undone.
+
+(In)dependent transactions : the simple case
+````````````````````````````````````````````
+
+A CubicWeb application can be used *simultaneously* by different users
+(whereas a single user works on an given office document at a
+given time), so that there is not always a single history
+time-line in the CubicWeb case. Moreover CubicWeb provides
+security through the mechanism of *permissions* granted to each
+user. This can lead to some transactions *not* being undoable in
+some contexts.
+
+In the simple case two (unprivileged) users Alice and Bob make
+relatively independent changes : then both Alice and Bob can undo
+their changes. But in some case there is a clean dependency
+between Alice's and Bob's actions or between actions of one of
+them. For example let's suppose that :
+
+- Alice has created a blog,
+- then has published a first post inside,
+- then Bob has published a second post in the same blog,
+- and finally Alice has updated its post contents.
+
+Then it is clear that Alice can undo her contents changes and Bob
+can undo his post creation independently. But Alice can not undo
+her post creation while she has not first undone her changes.
+It is also clear that Bob should *not* have the
+permissions to undo any of Alice's transactions.
+
+
+More complex dependencies between transactions
+``````````````````````````````````````````````
+
+But more surprising things can quickly happen. Going back to the
+previous example, Alice *can* undo the creation of the blog after
+Bob has published its post in it ! But this is possible only
+because the schema does not *require* for a post to be in a
+blog. Would the *blog entry of* relation have been mandatory, then
+Alice could not have undone the blog creation because it would
+have broken integrity constraint for Bob's post.
+
+When a user attempts to undo a transaction the system will check
+whether a later transaction has explicit dependency on the
+would-be-undone transaction. In this case the system will not even
+attempt the undo operation and inform the user.
+
+If no such dependency is detected the system will attempt the undo
+operation but it can fail, typically because of integrity
+constraint violations. In such a case the undo operation is
+completely [3]_ rollbacked.
+
+
+The *undo feature* for CubicWeb end-users
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The exposition of the undo feature to the end-user through a Web
+interface is still quite basic and will be improved toward a
+greater usability. But it is already fully functional.  For now
+there are two ways to access the *undo feature* as long as the it
+has been activated in the instance configuration file with the
+option *undo-support=yes*.
+
+Immediately after having done the change to be canceled through
+the **undo** link in the message. This allows to undo an
+hastily action immediately. For example, just after having
+validated the creation of the blog entry *A second blog entry* we
+get the following message, allowing to undo the creation.
+
+.. image:: /images/undo_mesage_w600.png
+   :width: 600px
+   :alt: Screenshot of the undo link in the message
+   :align: center
+
+At any time we can access the **undo-history view** accessible from the
+start-up page.
+
+.. image:: /images/undo_startup-link_w600.png
+   :width: 600px
+   :alt: Screenshot of the startup menu with access to the history view
+   :align: center
+
+This view will provide inspection of the transaction and their (public)
+actions. Each transaction provides its own **undo** link. Only the
+transactions the user has permissions to see and undo will be shown.
+
+.. image:: /images/undo_history-view_w600.png
+   :width: 600px
+   :alt: Screenshot of the undo history main view
+   :align: center
+
+If the user attempts to undo a transaction which can't be undone or
+whose undoing fails, then a message will explain the situation and
+no partial undoing will be left behind.
+
+This is all for the end-user side of the undo mechanism : this is
+quite simple indeed ! Now, in the following section, we are going
+to introduce the developer side of the undo mechanism.
+
+The *undo feature* for CubicWeb application developers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A word of warning : this section is intended for developers,
+already having some knowledge of what's under CubicWeb's hood. If
+it is not *yet* the case, please refer to CubicWeb documentation
+http://docs.cubicweb.org/ .
+
+Overview
+````````
+
+The core of the undo mechanisms is at work in the *native source*,
+beyond the RQL. This does mean that *transactions* and *actions*
+are *no entities*. Instead they are represented at the SQL level
+and exposed through the *DB-API* supported by the repository
+*Connection* objects.
+
+Once the *undo feature* has been activated in the instance
+configuration file with the option *undo-support=yes*, each
+mutating operation (cf. [2]_) will be recorded in some special SQL
+table along with its associated transaction. Transaction are
+identified by a *txuuid* through which the functions of the
+*DB-API* handle them.
+
+On the web side the last commited transaction *txuuid* is
+remembered in the request's data to allow for imediate undoing
+whereas the *undo-history view* relies upon the *DB-API* to list
+the accessible transactions. The actual undoing is performed by
+the *UndoController* accessible at URL of the form
+`www.my.host/my/instance/undo?txuuid=...`
+
+The repository side
+```````````````````
+
+Please refer to the file `cubicweb/server/sources/native.py` and
+`cubicweb/transaction.py` for the details.
+
+The undoing information is mainly stored in three SQL tables:
+
+`transactions`
+    Stores the txuuid, the user eid and the date-and-time of
+    the transaction. This table is referenced by the two others.
+
+`tx_entity_actions`
+    Stores the undo information for actions on entities.
+
+`tx_relation_actions`
+    Stores the undo information for the actions on relations.
+
+When the undo support is activated, entries are added to those
+tables for each mutating operation on the data repository, and are
+deleted on each transaction undoing.
+
+Those table are accessible through the following methods of the
+repository `Connection` object :
+
+`undoable_transactions`
+    Returns a list of `Transaction` objects accessible to the user
+    and according to the specified filter(s) if any.
+
+`tx_info`
+    Returns a `Transaction` object from a `txuuid`
+
+`undo_transaction`
+    Returns the list of `Action` object for the given `txuuid`.
+
+    NB:  By default it only return *public* actions.
+
+The web side
+````````````
+
+The exposure of the *undo feature* to the end-user through the Web
+interface relies on the *DB-API* introduced above. This implies
+that the *transactions* and *actions* are not *entities* linked by
+*relations* on which the usual views can be applied directly.
+
+That's why the file `cubicweb/web/views/undohistory.py` defines
+some dedicated views to access the undo information :
+
+`UndoHistoryView`
+    This is a *StartupView*, the one accessible from the home
+    page of the instance which list all transactions.
+
+`UndoableTransactionView`
+    This view handles the display of a single `Transaction` object.
+
+`UndoableActionBaseView`
+    This (abstract) base class provides private methods to build
+    the display of actions whatever their nature.
+
+`Undoable[Add|Remove|Create|Delete|Update]ActionView`
+    Those views all inherit from `UndoableActionBaseView` and
+    each handles a specific kind of action.
+
+`UndoableActionPredicate`
+    This predicate is used as a *selector* to pick the appropriate
+    view for actions.
+
+Apart from this main *undo-history view* a `txuuid` is stored in
+the request's data `last_undoable_transaction` in order to allow
+immediate undoing of a hastily validated operation. This is
+handled in `cubicweb/web/application.py` in the `main_publish` and
+`add_undo_link_to_msg` methods for the storing and displaying
+respectively.
+
+Once the undo information is accessible, typically through a
+`txuuid` in an *undo* URL, the actual undo operation can be
+performed by the `UndoController` defined in
+`cubicweb/web/views/basecontrollers.py`. This controller basically
+extracts the `txuuid` and performs a call to `undo_transaction` and
+in case of an undo-specific error, lets the top level publisher
+handle it as a validation error.
+
+
+Conclusion
+~~~~~~~~~~
+
+The undo mechanism relies upon a low level recording of the
+mutating operation on the repository. Those records are accessible
+through some method added to the *DB-API* and exposed to the
+end-user either through a whole history view of through an
+immediate undoing link in the message box.
+
+The undo feature is functional but the interface and configuration
+options are still quite reduced. One major improvement would be to
+be able to filter with a finer grain which transactions or actions
+one wants to see in the *undo-history view*. Another critical
+improvement would be to enable the undo feature on a part only of
+the entity-relationship schema to avoid storing too much useless
+data and reduce the underlying overhead.
+
+But both functionality are related to the strong design choice not
+to represent transactions and actions as entities and
+relations. This has huge benefits in terms of safety and conceptual
+simplicity but prevents from using lots of convenient CubicWeb
+features such as *facets* to access undo information.
+
+Before developing further the undo feature or eventually revising
+this design choice, it appears that some return of experience is
+strongly needed. So don't hesitate to try the undo feature in your
+application and send us some feedback.
+
+
+Notes
+~~~~~
+
+.. [1] The end-user Web interface could be improved to enable
+       user to choose whether he wishes to see private actions.
+
+.. [2] There is only five kind of elementary actions (beyond
+       merely accessing data for reading):
+
+       * **C** : creating an entity
+       * **D** : deleting an entity
+       * **U** : updating an entity attributes
+       * **A** : adding a relation
+       * **R** : removing a relation
+
+.. [3] Meaning none of the actions in the transaction is
+       undone. Depending upon the application, it might make sense
+       to enable *partial* undo. That is to say undo in which some
+       actions could not be undo without preventing to undo the
+       others actions in the transaction (as long as it does not
+       break schema integrity). This is not forbidden by the
+       back-end but is deliberately not supported by the front-end
+       (for now at least).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/additional-tips.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,64 @@
+
+.. _Additional Tips:
+
+Backups (mostly with postgresql)
+--------------------------------
+
+It is always a good idea to backup. If your system does not do that,
+you should set it up. Note that whenever you do an upgrade,
+`cubicweb-ctl` offers you to backup your database.  There are a number
+of ways for doing backups.
+
+Using postgresql (and only that)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before you
+go ahead, make sure the following permissions are correct ::
+
+   # chgrp postgres /var/lib/cubicweb/backup
+   # chmod g+ws /var/lib/cubicweb/backup
+   # chgrp postgres /etc/cubicweb.d/*<instance>*/sources
+   # chmod g+r /etc/cubicweb.d/*<instance>*/sources
+
+Simply use the pg_dump in a cron installed for `postgres` user on the database server::
+
+    # m h  dom mon dow   command
+    0 2 * * * pg_dump -Fc --username=cubicweb --no-owner <instance> > /var/backups/<instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
+
+Using :command:`cubicweb-ctl db-dump`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The CubicWeb way is to use the :command:`db-dump` command. For that,
+you have to put your passwords in a user-only-readable file at the
+home directory of root user.  The file is `.pgpass` (`chmod 0600`), in
+this case for a socket run connection to PostgreSQL ::
+
+    /var/run/postgresql:5432:<instance>:<database user>:<database password>
+
+The postgres documentation for the `.pgpass` format can be found `here`_
+
+Then add the following command to the crontab of the user (`crontab -e`)::
+
+    # m h  dom mon dow   command
+    0 2 * * * cubicweb-ctl db-dump <instance>
+
+
+Backup ninja
+~~~~~~~~~~~~
+
+You can use a combination `backup-ninja`_ (which has a postgres script in the
+example directory), `backuppc`)_ (for versionning).
+
+Please note that in the *CubicWeb way* it adds a second location for your
+password which is error-prone.
+
+.. _`here` : http://www.postgresql.org/docs/current/static/libpq-pgpass.html
+.. _`backup-ninja` : https://labs.riseup.net/code/projects/show/backupninja/
+.. _`backuppc` : http://backuppc.sourceforge.net/
+
+.. warning::
+
+  Remember that these indications will fail you whenever you use
+  another database backend than postgres. Also it does properly handle
+  externally managed data such as files (using the Bytes File System
+  Storage).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/config.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,229 @@
+.. -*- coding: utf-8 -*-
+
+.. _ConfigEnv:
+
+Set-up of a *CubicWeb* environment
+==================================
+
+You can `configure the database`_ system of your choice:
+
+  - `PostgreSQL configuration`_
+  - `MySql configuration`_
+  - `SQLServer configuration`_
+  - `SQLite configuration`_
+
+For advanced features, have a look to:
+
+  - `Cubicweb resources configuration`_
+
+.. _`configure the database`: DatabaseInstallation_
+.. _`PostgreSQL configuration`: PostgresqlConfiguration_
+.. _`MySql configuration`: MySqlConfiguration_
+.. _`SQLServer configuration`: SQLServerConfiguration_
+.. _`SQLite configuration`: SQLiteConfiguration_
+.. _`Cubicweb resources configuration`: RessourcesConfiguration_
+
+
+
+.. _RessourcesConfiguration:
+
+Cubicweb resources configuration
+--------------------------------
+
+.. autodocstring:: cubicweb.cwconfig
+
+
+.. _DatabaseInstallation:
+
+Databases configuration
+-----------------------
+
+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. CubicWeb is known to run with
+Postgresql (recommended), SQLServer and SQLite, and may run with MySQL.
+
+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:
+
+PostgreSQL
+~~~~~~~~~~
+
+Many Linux distributions ship with the appropriate PostgreSQL packages.
+Basically, you need to install the following packages:
+
+* `postgresql` and `postgresql-client`, which will pull the respective
+  versioned packages (e.g. `postgresql-9.1` and `postgresql-client-9.1`) and,
+  optionally,
+* a `postgresql-plpython-X.Y` package with a version corresponding to that of
+  the aforementioned packages (e.g. `postgresql-plpython-9.1`).
+
+If you run postgres version prior to 8.3, you'll also need the
+`postgresql-contrib-8.X` package for full-text search extension.
+
+If you run postgres on another host than the |cubicweb| repository, you should
+install the `postgresql-client` package on the |cubicweb| host, and others on the
+database host.
+
+For extra details concerning installation, please refer to the `PostgreSQL
+project online documentation`_.
+
+.. _`PostgreSQL project online documentation`: http://www.postgresql.org/docs
+
+
+Database cluster
+++++++++++++++++
+
+If you already have an existing cluster and PostgreSQL server running, you do
+not need to execute the initilization step of your PostgreSQL database unless
+you want a specific cluster for |cubicweb| databases or if your existing
+cluster doesn't use the UTF8 encoding (see note below).
+
+To initialize a PostgreSQL cluster, use the command ``initdb``::
+
+    $ initdb -E UTF8 -D /path/to/pgsql
+
+Notice the encoding specification. This is necessary since |cubicweb| usually
+want UTF8 encoded database. If you use a cluster with the wrong encoding, you'll
+get error like::
+
+  new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
+  HINT:  Use the same encoding as in the template database, or use template0 as template.
+
+Once initialized, start the database server PostgreSQL with the command::
+
+  $ postgres -D /path/to/psql
+
+If you cannot execute this command due to permission issues, please make sure
+that your username has write access on the database.  ::
+
+  $ chown username /path/to/pgsql
+
+Database authentication
++++++++++++++++++++++++
+
+The database authentication is configured in `pg_hba.conf`. It can be either set
+to `ident sameuser` or `md5`.  If set to `md5`, make sure to use an existing
+user of your database.  If set to `ident sameuser`, make sure that your client's
+operating system user name has a matching user in the database. If not, please
+do as follow to create a user::
+
+  $ su
+  $ su - postgres
+  $ createuser -s -P username
+
+The option `-P` (for password prompt), will encrypt the password with the
+method set in the configuration file :file:`pg_hba.conf`.  If you do not use this
+option `-P`, then the default value will be null and you will need to set it
+with::
+
+  $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
+
+The above login/password will be requested when you will create an instance with
+`cubicweb-ctl create` to initialize the database of your instance.
+
+Notice that the `cubicweb-ctl db-create` does database initialization that
+may requires a postgres superuser. That's why a login/password is explicitly asked
+at this step, so you can use there a superuser without using this user when running
+the instance. Things that require special privileges at this step:
+
+* database creation, require the 'create database' permission
+* install the plpython extension language (require superuser)
+* install the tsearch extension for postgres version prior to 8.3 (require superuser)
+
+To avoid using a super user each time you create an install, a nice trick is to
+install plpython (and tsearch when needed) on the special `template1` database,
+so they will be installed automatically when cubicweb databases are created
+without even with needs for special access rights. To do so, run ::
+
+  # Installation of plpythonu language by default ::
+  $ createlang -U pgadmin plpythonu template1
+  $ psql -U pgadmin template1
+  template1=# update pg_language set lanpltrusted=TRUE where lanname='plpythonu';
+
+Where `pgadmin` is a postgres superuser. The last command is necessary since by
+default plpython is an 'untrusted' language and as such can't be used by non
+superuser. This update fix that problem by making it trusted.
+
+To install the tsearch plain-text index extension on postgres prior to 8.3, run::
+
+    cat /usr/share/postgresql/8.X/contrib/tsearch2.sql | psql -U username template1
+
+
+.. _MySqlConfiguration:
+
+MySql
+~~~~~
+.. warning::
+    CubicWeb's MySQL support is not commonly used, so things may or may not work properly.
+
+You must add the following lines in ``/etc/mysql/my.cnf`` file::
+
+    transaction-isolation=READ-COMMITTED
+    default-storage-engine=INNODB
+    default-character-set=utf8
+    max_allowed_packet = 128M
+
+.. Note::
+    It is unclear whether mysql supports indexed string of arbitrary length or
+    not.
+
+
+.. _SQLServerConfiguration:
+
+SQLServer
+~~~~~~~~~
+
+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)::
+
+  [system]
+  db-driver=sqlserver2005
+  db-user=someuser
+  # database password not needed
+  #db-password=toto123
+  #db-create/init may ask for a pwd: just say anything
+  db-extra-arguments=Trusted_Connection
+  db-encoding=utf8
+
+
+You need to change the default settings on the database by running::
+
+ ALTER DATABASE <databasename> SET READ_COMMITTED_SNAPSHOT ON;
+
+The ALTER DATABASE command above requires some permissions that your
+user may not have. In that case you will have to ask your local DBA to
+run the query for you.
+
+You can check that the setting is correct by running the following
+query which must return '1'::
+
+   SELECT is_read_committed_snapshot_on
+     FROM sys.databases WHERE name='<databasename>';
+
+
+
+.. _SQLiteConfiguration:
+
+SQLite
+~~~~~~
+
+SQLite has the great advantage of requiring almost no configuration. Simply
+use 'sqlite' as db-driver, and set path to the dabase as db-name. Don't specify
+anything for db-user and db-password, they will be ignore anyway.
+
+.. Note::
+  SQLite is great for testing and to play with cubicweb but is not suited for
+  production environments.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/create-instance.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,100 @@
+.. -*- coding: utf-8 -*-
+
+Creation of your first instance
+===============================
+
+Instance creation
+-----------------
+
+Now that we created a cube, we can create an instance and access it via a web
+browser. We will use a `all-in-one` configuration to simplify things ::
+
+  cubicweb-ctl create -c all-in-one mycube myinstance
+
+.. note::
+  Please note that we created a new cube for a demo purposes but
+  you could have used an existing cube available in our standard library
+  such as blog or person for example.
+
+A series of questions will be prompted to you, the default answer is usually
+sufficient. You can anyway modify the configuration later on by editing
+configuration files. When a login/password are requested to access the database
+please use the credentials you created at the time you configured the database
+(:ref:`PostgresqlConfiguration`).
+
+It is important to distinguish here the user used to access the database and the
+user used to login to the cubicweb instance. When an instance starts, it uses
+the login/password for the database to get the schema and handle low level
+transaction. But, when :command:`cubicweb-ctl create` asks for a manager
+login/psswd of *CubicWeb*, it refers to the user you will use during the
+development to administrate your web instance. It will be possible, later on,
+to use this user to create other users for your final web instance.
+
+
+Instance administration
+-----------------------
+
+start / stop
+~~~~~~~~~~~~
+
+When this command is completed, the definition of your instance is
+located in :file:`~/etc/cubicweb.d/myinstance/*`. To launch it, you
+just type ::
+
+  cubicweb-ctl start -D myinstance
+
+The option `-D` specifies the *debug mode* : the instance is not
+running in server mode and does not disconnect from the terminal,
+which simplifies debugging in case the instance is not properly
+launched. You can see how it looks by visiting the URL
+`http://localhost:8080` (the port number depends of your
+configuration). To login, please use the cubicweb administrator
+login/password you defined when you created the instance.
+
+To shutdown the instance, Crtl-C in the terminal window is enough.
+If you did not use the option `-D`, then type ::
+
+  cubicweb-ctl stop myinstance
+
+This is it! All is settled down to start developping your data model...
+
+.. note::
+
+  The output of `cubicweb-ctl start -D myinstance` can be
+  overwhelming. It is possible to reduce the log level with the
+  `--loglevel` parameter as in `cubicweb-ctl start -D myinstance -l
+  info` to filter out all logs under `info` gravity.
+
+upgrade
+~~~~~~~
+
+A manual upgrade step is necessary whenever a new version of CubicWeb or
+a cube is installed, in order to synchronise the instance's
+configuration and schema with the new code.  The command is::
+
+  cubicweb-ctl upgrade myinstance
+
+A series of questions will be asked. It always starts with a proposal
+to make a backup of your sources (where it applies). Unless you know
+exactly what you are doing (i.e. typically fiddling in debug mode, but
+definitely NOT migrating a production instance), you should answer YES
+to that.
+
+The remaining questions concern the migration steps of |cubicweb|,
+then of the cubes that form the whole application, in reverse
+dependency order.
+
+In principle, if the migration scripts have been properly written and
+tested, you should answer YES to all questions.
+
+Somtimes, typically while debugging a migration script, something goes
+wrong and the migration fails. Unfortunately the databse may be in an
+incoherent state. You have two options here:
+
+* fix the bug, restore the database and restart the migration process
+  from scratch (quite recommended in a production environement)
+
+* try to replay the migration up to the last successful commit, that
+  is answering NO to all questions up to the step that failed, and
+  finish by answering YES to the remaining questions.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/cubicweb-ctl.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,111 @@
+.. -*- coding: utf-8 -*-
+
+.. _cubicweb-ctl:
+
+``cubicweb-ctl`` tool
+=====================
+
+`cubicweb-ctl` is the swiss knife to manage *CubicWeb* instances.
+The general syntax is ::
+
+  cubicweb-ctl <command> [options command] <arguments commands>
+
+To view available commands ::
+
+  cubicweb-ctl
+  cubicweb-ctl --help
+
+Please note that the commands available depends on the *CubicWeb* packages
+and cubes that have been installed.
+
+To view the help menu on specific command ::
+
+  cubicweb-ctl <command> --help
+
+Listing available cubes and instance
+-------------------------------------
+
+* ``list``, provides a list of the available configuration, cubes
+  and instances.
+
+
+Creation of a new cube
+-----------------------
+
+Create your new cube cube ::
+
+   cubicweb-ctl newcube
+
+This will create a new cube in
+``/path/to/grshell-cubicweb/cubes/<mycube>`` for a Mercurial
+installation, or in ``/usr/share/cubicweb/cubes`` for a debian
+packages installation.
+
+Create an instance
+-------------------
+
+You must ensure `~/etc/cubicweb.d/` exists prior to this. On windows, the
+'~' part will probably expand to 'Documents and Settings/user'.
+
+To create an instance from an existing cube, execute the following
+command ::
+
+   cubicweb-ctl create <cube_name> <instance_name>
+
+This command will create the configuration files of an instance in
+``~/etc/cubicweb.d/<instance_name>``.
+
+The tool ``cubicweb-ctl`` executes the command ``db-create`` and
+``db-init`` when you run ``create`` so that you can complete an
+instance creation in a single command. But of course it is possible
+to issue these separate commands separately, at a later stage.
+
+Command to create/initialize an instance database
+-------------------------------------------------
+
+* ``db-create``, creates the system database of an instance (tables and
+  extensions only)
+* ``db-init``, initializes the system database of an instance
+  (schema, groups, users, workflows...)
+
+Commands to control instances
+-----------------------------
+
+* ``start``, starts one or more or all instances
+
+of special interest::
+
+  start -D
+
+will start in debug mode (under windows, starting without -D will not
+work; you need instead to setup your instance as a service).
+
+* ``stop``, stops one or more or all instances
+* ``restart``, restarts one or more or all instances
+* ``status``, returns the status of the instance(s)
+
+Commands to maintain instances
+------------------------------
+
+* ``upgrade``, launches the existing instances migration when a new version
+  of *CubicWeb* or the cubes installed is available
+* ``shell``, opens a (Python based) migration shell for manual maintenance of the instance
+* ``db-dump``, creates a dump of the system database
+* ``db-restore``, restores a dump of the system database
+* ``db-check``, checks data integrity of an instance. If the automatic correction
+  is activated, it is recommanded to create a dump before this operation.
+* ``schema-sync``, synchronizes the persistent schema of an instance with
+  the instance schema. It is recommanded to create a dump before this operation.
+
+Commands to maintain i18n catalogs
+----------------------------------
+* ``i18ncubicweb``, regenerates messages catalogs of the *CubicWeb* library
+* ``i18ncube``, regenerates the messages catalogs of a cube
+* ``i18ninstance``, recompiles the messages catalogs of an instance.
+  This is automatically done while upgrading.
+
+See also chapter :ref:`internationalization`.
+
+Other commands
+--------------
+* ``delete``, deletes an instance (configuration files and database)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,28 @@
+.. -*- coding: utf-8 -*-
+
+.. _Part3:
+
+--------------
+Administration
+--------------
+
+This part is for installation and administration of the *CubicWeb* framework and
+instances based on that framework.
+
+.. toctree::
+   :maxdepth: 1
+   :numbered:
+
+   setup
+   setup-windows
+   config
+   cubicweb-ctl
+   create-instance
+   instance-config
+   site-config
+   multisources
+   ldap
+   migration
+   additional-tips
+   rql-logs
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/instance-config.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,200 @@
+.. -*- coding: utf-8 -*-
+
+
+Configure an instance
+=====================
+
+While creating an instance, a configuration file is generated in::
+
+    $ (CW_INSTANCES_DIR) / <instance> / <configuration name>.conf
+
+For example::
+
+    /etc/cubicweb.d/myblog/all-in-one.conf
+
+It is a simple text file in the INI format
+(http://en.wikipedia.org/wiki/INI_file). In the following description,
+each option name is prefixed with its own section and followed by its
+default value if necessary, e.g. "`<section>.<option>` [value]."
+
+.. _`WebServerConfig`:
+
+Configuring the Web server
+--------------------------
+:`web.auth-model` [cookie]:
+    authentication mode, cookie or http
+:`web.realm`:
+    realm of the instance in http authentication mode
+:`web.http-session-time` [0]:
+    period of inactivity of an HTTP session before it closes automatically.
+    Duration in seconds, 0 meaning no expiration (or more exactly at the
+    closing of the browser client)
+
+:`main.anonymous-user`, `main.anonymous-password`:
+    login and password to use to connect to the RQL server with
+    HTTP anonymous connection. CWUser account should exist.
+
+:`main.base-url`:
+    url base site to be used to generate the urls of web pages
+
+Https configuration
+```````````````````
+It is possible to make a site accessible for anonymous http connections
+and https for authenticated users. This requires to
+use apache (for example) for redirection and the variable `main.https-url`
+of configuration file.
+
+For this to work you have to activate the following apache modules :
+
+* rewrite
+* proxy
+* http_proxy
+
+The command on Debian based systems for that is ::
+
+  a2enmod rewrite http_proxy proxy
+  /etc/init.d/apache2 restart
+
+:Example:
+
+   For an apache redirection of a site accessible via `http://localhost/demo`
+   and `https://localhost/demo` and actually running on port 8080, it
+   takes to the http:::
+
+     ProxyPreserveHost On
+     RewriteEngine On
+     RewriteCond %{REQUEST_URI} ^/demo
+     RewriteRule ^/demo$ /demo/
+     RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P]
+
+   and for the https:::
+
+     ProxyPreserveHost On
+     RewriteEngine On
+     RewriteCond %{REQUEST_URI} ^/ demo
+     RewriteRule ^/demo$/demo/
+     RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]
+
+
+   and we will file in the all-in-one.conf of the instance:::
+
+     base-url = http://localhost/demo
+     https-url = https://localhost/demo
+
+Notice that if you simply want a site accessible through https, not *both* http
+and https, simply set `base-url` to the https url and the first section into your
+apache configuration (as you would have to do for an http configuration with an
+apache front-end).
+
+Setting up the web client
+-------------------------
+:`web.embed-allowed`:
+    regular expression matching sites which could be "embedded" in
+    the site (controllers 'embed')
+:`web.submit-url`:
+    url where the bugs encountered in the instance can be mailed to
+
+
+RQL server configuration
+------------------------
+:`main.host`:
+    host name if it can not be detected correctly
+:`main.pid-file`:
+    file where will be written the server pid
+:`main.uid`:
+    user account to use for launching the server when it is
+    root launched by init
+:`main.session-time [30*60]`:
+    timeout of a RQL session
+:`main.query-log-file`:
+    file where all requests RQL executed by the server are written
+
+
+Configuring e-mail
+------------------
+RQL and web server side:
+
+:`email.mangle-mails [no]`:
+    indicates whether the email addresses must be displayed as is or
+    transformed
+
+RQL server side:
+
+:`email.smtp-host [mail]`:
+    hostname hosting the SMTP server to use for outgoing mail
+:`email.smtp-port [25]`:
+    SMTP server port to use for outgoing mail
+:`email.sender-name`:
+    name to use for outgoing mail of the instance
+:`email.sender-addr`:
+    address for outgoing mail of the instance
+:`email.default dest-addrs`:
+    destination addresses by default, if used by the configuration of the
+    dissemination of the model (separated by commas)
+:`email.supervising-addrs`:
+    destination addresses of e-mails of supervision (separated by
+    commas)
+
+
+Configuring logging
+-------------------
+:`main.log-threshold`:
+    level of filtering messages (DEBUG, INFO, WARNING, ERROR)
+:`main.log-file`:
+    file to write messages
+
+
+.. _PersistentProperties:
+
+Configuring persistent properties
+---------------------------------
+Other configuration settings are in the form of entities `CWProperty`
+in the database. It must be edited via the web interface or by
+RQL queries.
+
+:`ui.encoding`:
+    Character encoding to use for the web
+:`navigation.short-line-size`:
+    number of characters for "short" display
+:`navigation.page-size`:
+    maximum number of entities to show per results page
+:`navigation.related-limit`:
+    number of related entities to show up on primary entity view
+:`navigation.combobox-limit`:
+    number of entities unrelated to show up on the drop-down lists of
+    the sight on an editing entity view
+
+Cross-Origin Resource Sharing
+-----------------------------
+
+CubicWeb provides some support for the CORS_ protocol. For now, the
+provided implementation only deals with access to a CubicWeb instance
+as a whole. Support for a finer granularity may be considered in the
+future.
+
+Specificities of the provided implementation:
+
+- ``Access-Control-Allow-Credentials`` is always true
+- ``Access-Control-Allow-Origin`` header in response will never be
+  ``*``
+- ``Access-Control-Expose-Headers`` can be configured globally (see below)
+- ``Access-Control-Max-Age`` can be configured globally (see below)
+- ``Access-Control-Allow-Methods`` can be configured globally (see below)
+- ``Access-Control-Allow-Headers`` can be configured globally (see below)
+
+
+A few parameters can be set to configure the CORS_ capabilities of CubicWeb.
+
+.. _CORS: http://www.w3.org/TR/cors/
+
+:`access-control-allow-origin`:
+   comma-separated list of allowed origin domains or "*" for any domain
+:`access-control-allow-methods`:
+   comma-separated list of allowed HTTP methods
+:`access-control-max-age`:
+   maximum age of cross-origin resource sharing (in seconds)
+:`access-control-allow-headers`:
+   comma-separated list of allowed HTTP custom headers (used in simple requests)
+:`access-control-expose-headers`:
+   comma-separated list of allowed HTTP custom headers (used in preflight requests)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/ldap.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,134 @@
+.. _LDAP:
+
+LDAP integration
+================
+
+Overview
+--------
+
+Using LDAP as a source for user credentials and information is quite
+easy. The most difficult part lies in building an LDAP schema or
+using an existing one.
+
+At cube creation time, one is asked if more sources are wanted. LDAP
+is one possible option at this time. Of course, it is always possible
+to set it up later using the `CWSource` entity type, which we discuss
+there.
+
+It is possible to add as many LDAP sources as wanted, which translates
+in as many `CWSource` entities as needed.
+
+The general principle of the LDAP source is, given a proper
+configuration, to create local users matching the users available in
+the directory and deriving local user attributes from directory users
+attributes. Then a periodic task ensures local user information
+synchronization with the directory.
+
+Users handled by such a source should not be edited directly from
+within the application instance itself. Rather, updates should happen
+at the LDAP server level.
+
+Credential checks are _always_ done against the LDAP server.
+
+.. Note::
+
+  There are currently two ldap source types: the older `ldapuser` and
+  the newer `ldapfeed`. The older will be deprecated anytime soon, as
+  the newer has now gained all the features of the old and does not
+  suffer from some of its illnesses.
+
+  The ldapfeed creates real `CWUser` entities, and then
+  activate/deactivate them depending on their presence/absence in the
+  corresponding LDAP source. Their attribute and state
+  (activated/deactivated) are hence managed by the source mechanism;
+  they should not be altered by other means (as such alterations may
+  be overridden in some subsequent source synchronisation).
+
+
+Configuration of an LDAPfeed source
+-----------------------------------
+
+Additional sources are created at cube creation time or later through the
+user interface.
+
+Configure an `ldapfeed` source from the user interface under `Manage` then
+`data sources`:
+
+* At this point `type` has been set to `ldapfeed`.
+
+* The `parser` attribute shall be set to `ldapfeed`.
+
+* The `url` attribute shall be set to an URL such as ldap://ldapserver.domain/.
+
+* The `configuration` attribute contains many options. They are described in
+  detail in the next paragraph.
+
+
+Options of an LDAPfeed source
+-----------------------------
+
+Let us enumerate the options by categories (LDAP server connection,
+LDAP schema mapping information).
+
+LDAP server connection options:
+
+* `auth-mode`, (choices are simple, cram_md5, digest_md5, gssapi, support
+  for the later being partial as of now)
+
+* `auth-realm`, realm to use when using gssapi/kerberos authentication
+
+* `data-cnx-dn`, user dn to use to open data connection to the ldap (eg
+  used to respond to rql queries)
+
+* `data-cnx-password`, password to use to open data connection to the
+  ldap (eg used to respond to rql queries)
+
+If the LDAP server accepts anonymous binds, then it is possible to
+leave data-cnx-dn and data-cnx-password empty. This is, however, quite
+unlikely in practice. Beware that the LDAP server might hide attributes
+such as "userPassword" while the rest of the attributes remain visible
+through an anonymous binding.
+
+LDAP schema mapping options:
+
+* `user-base-dn`, base DN to lookup for users
+
+* `user-scope`, user search scope (valid values: "BASE", "ONELEVEL",
+  "SUBTREE")
+
+* `user-classes`, classes of user (with Active Directory, you want to
+  say "user" here)
+
+* `user-filter`, additional filters to be set in the ldap query to
+  find valid users
+
+* `user-login-attr`, attribute used as login on authentication (with
+  Active Directory, you want to use "sAMAccountName" here)
+
+* `user-default-group`, name of a group in which ldap users will be by
+  default. You can set multiple groups by separating them by a comma
+
+* `user-attrs-map`, map from ldap user attributes to cubicweb
+  attributes (with Active Directory, you want to use
+  sAMAccountName:login,mail:email,givenName:firstname,sn:surname)
+
+
+Other notes
+-----------
+
+* Cubicweb is able to start if ldap cannot be reached, even on
+  cubicweb-ctl start ... If some source ldap server cannot be used
+  while an instance is running, the corresponding users won't be
+  authenticated but their status will not change (e.g. they will not
+  be deactivated)
+
+* The user-base-dn is a key that helps cubicweb map CWUsers to LDAP
+  users: beware updating it
+
+* When a user is removed from an LDAP source, it is deactivated in the
+  CubicWeb instance; when a deactivated user comes back in the LDAP
+  source, it (automatically) is activated again
+
+* You can use the :class:`CWSourceHostConfig` to have variants for a source
+  configuration according to the host the instance is running on. To do so
+  go on the source's view from the sources management view.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/migration.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,38 @@
+.. -*- coding: utf-8 -*-
+
+Migrating cubicweb instances - benefits from a distributed architecture
+=======================================================================
+
+Migrate apache & cubicweb
+-------------------------
+
+**Aim** : do the migration for N cubicweb instances hosted on a server to another with no downtime.
+
+**Prerequisites** : have an explicit definition of the database host (not default or localhost). In our case, the database is hosted on another host.
+
+**Steps** :
+
+1. *on new machine* : install your environment (*pseudocode*) ::
+
+     apt-get install cubicweb cubicweb-applications apache2
+
+2. *on old machine* : copy your cubicweb and apache configuration to the new machine ::
+
+    scp /etc/cubicweb.d/ newmachine:/etc/cubicweb.d/
+    scp /etc/apache2/sites-available/ newmachine:/etc/apache2/sites-available/
+
+3. *on new machine* : start your instances ::
+
+     cubicweb start
+
+4. *on new machine* : enable sites and modules for apache and start it, test it using by modifying your /etc/host file.
+
+5. change dns entry from your oldmachine to newmachine
+
+6. shutdown your *old machine* (if it doesn't host other services or your database)
+
+7. That's it.
+
+**Possible enhancements** : use right from the start a pound server behind your apache, that way you can add backends and smoothily migrate by shuting down backends that pound will take into account.
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/multisources.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,6 @@
+Multiple sources of data
+========================
+
+Data sources include SQL, LDAP, RQL, mercurial and subversion.
+
+.. XXX feed me
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/rql-logs.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,14 @@
+.. -*- coding: utf-8 -*-
+
+RQL logs
+========
+
+You can configure the *CubicWeb* instance to keep a log
+of the queries executed against your database. To do so,
+edit the configuration file of your instance
+``.../etc/cubicweb.d/myapp/all-in-one.conf`` and uncomment the
+variable ``query-log-file``::
+
+  # web instance query log file
+  query-log-file=/tmp/rql-myapp.log
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/setup-windows.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,146 @@
+.. -*- coding: utf-8 -*-
+
+.. _SetUpWindowsEnv:
+
+Installing a development environement on Windows
+================================================
+
+Setting up a Windows development environment is not too complicated
+but it requires a series of small steps.
+
+We propose an example of a typical |cubicweb| installation on Windows
+from sources. We assume everything goes into ``C:\\`` and for any
+package, without version specification, "the latest is
+the greatest".
+
+Mind that adjusting the installation drive should be straightforward.
+
+
+
+Install the required elements
+-----------------------------
+
+|cubicweb| requires some base elements that must be installed to run
+correctly. So, first of all, you must install them :
+
+* python >= 2.6 and < 3
+  (`Download Python <http://www.python.org/download/>`_).
+  You can also consider the Python(x,y) distribution
+  (`Download Python(x,y) <http://code.google.com/p/pythonxy/wiki/Downloads>`_)
+  as it makes things easier for Windows user by wrapping in a single installer
+  python 2.7 plus numerous useful third-party modules and
+  applications (including Eclipse + pydev, which is an arguably good
+  IDE for Python under Windows).
+
+* `Twisted <http://twistedmatrix.com/trac/>`_ is an event-driven
+  networking engine
+  (`Download Twisted <http://twistedmatrix.com/trac/>`_)
+
+* `lxml <http://codespeak.net/lxml/>`_ library
+  (version >=2.2.1) allows working with XML and HTML
+  (`Download lxml <http://pypi.python.org/pypi/lxml/2.2.1>`_)
+
+* `Postgresql <http://www.postgresql.org/>`_,
+  an object-relational database system
+  (`Download Postgresql <http://www.enterprisedb.com/products/pgdownload.do#windows>`_)
+  and its python drivers
+  (`Download psycopg <http://www.stickpeople.com/projects/python/win-psycopg/#Version2>`_)
+
+* A recent version of `gettext`
+  (`Download gettext <http://download.logilab.org/pub/gettext/gettext-0.17-win32-setup.exe>`_).
+
+* `rql <http://www.logilab.org/project/rql>`_,
+  the recent version of the Relationship Query Language parser.
+
+Install optional elements
+-------------------------
+
+We recommend you to install the following elements. They are not
+mandatory but they activate very interesting features in |cubicweb|:
+
+* `python-ldap <http://pypi.python.org/pypi/python-ldap>`_
+  provides access to LDAP/Active directory directories
+  (`Download python-ldap <http://www.osuch.org/python-ldap>`_).
+
+* `graphviz <http://www.graphviz.org/>`_
+  which allow schema drawings.
+  (`Download graphviz <http://www.graphviz.org/Download_windows.php>`_).
+  It is quite recommended (albeit not mandatory).
+
+Other elements will activate more features once installed. Take a look
+at :ref:`InstallDependencies`.
+
+Useful tools
+------------
+
+Some additional tools could be useful to develop :ref:`cubes <AvailableCubes>`
+with the framework.
+
+* `mercurial <http://mercurial.selenic.com/>`_ and its standard windows GUI
+  (`TortoiseHG <http://tortoisehg.bitbucket.org/>`_) allow you to get the source
+  code of |cubicweb| from control version repositories. So you will be able to
+  get the latest development version and pre-release bugfixes in an easy way
+  (`Download mercurial <http://bitbucket.org/tortoisehg/stable/wiki/download>`_).
+
+* You can also consider the ssh client `Putty` in order to peruse
+  mercurial over ssh (`Download <http://www.putty.org/>`_).
+
+* If you are an Eclipse user, mercurial can be integrated using the
+  `MercurialEclipse` plugin
+  (`Home page <http://www.vectrace.com/mercurialeclipse/>`_).
+
+Getting the sources
+-------------------
+
+There are two ways to get the sources of |cubicweb| and its
+:ref:`cubes <AvailableCubes>`:
+
+* download the latest release (:ref:`SourceInstallation`)
+* get the development version using Mercurial
+  (:ref:`MercurialInstallation`)
+
+Environment variables
+---------------------
+
+You will need some convenience environment variables once all is set up. These
+variables are settable through the GUI by getting at the `System properties`
+window (by righ-clicking on `My Computer` -> `properties`).
+
+In the `advanced` tab, there is an `Environment variables` button. Click on
+it. That opens a small window allowing edition of user-related and system-wide
+variables.
+
+We will consider only user variables. First, the ``PATH`` variable. Assuming
+you are logged as user *Jane*, add the following paths, separated by
+semi-colons::
+
+  C:\Documents and Settings\Jane\My Documents\Python\cubicweb\cubicweb\bin
+  C:\Program Files\Graphviz2.24\bin
+
+The ``PYTHONPATH`` variable should also contain::
+
+  C:\Documents and Settings\Jane\My Documents\Python\cubicweb\
+
+From now, on a fresh `cmd` shell, you should be able to type::
+
+  cubicweb-ctl list
+
+... and get a meaningful output.
+
+Running an instance as a service
+--------------------------------
+
+This currently assumes that the instances configurations is located at
+``C:\\etc\\cubicweb.d``. For a cube 'my_instance', you will find
+``C:\\etc\\cubicweb.d\\my_instance\\win32svc.py``.
+
+Now, register your instance as a windows service with::
+
+  win32svc install
+
+Then start the service with::
+
+  net start cubicweb-my_instance
+
+In case this does not work, you should be able to see error reports in
+the application log, using the windows event log viewer.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/setup.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,239 @@
+.. -*- coding: utf-8 -*-
+
+.. _SetUpEnv:
+
+Installation of a *CubicWeb* environment
+========================================
+
+Official releases are available from the `CubicWeb.org forge`_ and from
+`PyPI`_. Since CubicWeb is developed using `Agile software development
+<http://en.wikipedia.org/wiki/Agile_software_development>`_ techniques, releases
+happen frequently. In a version numbered X.Y.Z, X changes after a few years when
+the API breaks, Y changes after a few weeks when features are added and Z
+changes after a few days when bugs are fixed.
+
+Depending on your needs, you will chose a different way to install CubicWeb on
+your system:
+
+- `Installation on Debian/Ubuntu`_
+- `Installation on Windows`_
+- `Installation in a virtualenv`_
+- `Installation with pip`_
+- `Installation with easy_install`_
+- `Installation from tarball`_
+
+If you are a power-user and need the very latest features, you will
+
+- `Install from version control`_
+
+Once the software is installed, move on to :ref:`ConfigEnv` for better control
+and advanced features of |cubicweb|.
+
+.. _`Installation on Debian/Ubuntu`: DebianInstallation_
+.. _`Installation on Windows`: WindowsInstallation_
+.. _`Installation in a virtualenv`: VirtualenvInstallation_
+.. _`Installation with pip`: PipInstallation_
+.. _`Installation with easy_install`: EasyInstallInstallation_
+.. _`Installation from tarball`: TarballInstallation_
+.. _`Install from version control`: MercurialInstallation_
+
+
+.. _DebianInstallation:
+
+Debian/Ubuntu install
+---------------------
+
+|cubicweb| is packaged for Debian/Ubuntu (and derived
+distributions). Their integrated package-management system make
+installation and upgrade much easier for users since
+dependencies (like databases) are automatically installed.
+
+Depending on the distribution you are using, add the appropriate line to your
+`list of sources` (for example by editing ``/etc/apt/sources.list``), replacing
+``<release>`` with e.g. ``wheezy`` or ``trusty``::
+
+  deb http://download.logilab.org/production/ <release>/
+
+The repositories are signed with `Logilab's gnupg key`_. You can download
+and register the key to avoid warnings::
+
+  wget -O/etc/apt/trusted.gpg.d/logilab.gpg https://www.logilab.fr/logilab-debian-keyring.gpg
+
+Update your list of packages and perform the installation::
+
+  apt-get update
+  apt-get install cubicweb cubicweb-dev
+
+``cubicweb`` installs the framework itself, allowing you to create new
+instances. ``cubicweb-dev`` installs the development environment
+allowing you to develop new cubes.
+
+There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
+list of available cubes using ``apt-cache search cubicweb`` or at the
+`CubicWeb.org forge`_.
+
+.. note::
+
+  `cubicweb-dev` will install basic sqlite support. You can easily setup
+  :ref:`cubicweb with other database <DatabaseInstallation>` using the following
+  virtual packages :
+
+  * `cubicweb-postgresql-support` contains the necessary dependencies for
+    using :ref:`cubicweb with postgresql datatabase <PostgresqlConfiguration>`
+
+  * `cubicweb-mysql-support` contains the necessary dependencies for using
+    :ref:`cubicweb with mysql database <MySqlConfiguration>`.
+
+.. _`list of sources`: http://wiki.debian.org/SourcesList
+.. _`Logilab's gnupg key`: https://www.logilab.fr/logilab-debian-keyring.gpg
+.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
+
+.. _WindowsInstallation:
+
+Windows Install
+---------------
+
+You need to have `python`_ version >= 2.5 and < 3 installed.
+
+If you want an automated install, your best option is probably the
+:ref:`EasyInstallInstallation`. EasyInstall is a tool that helps users to
+install python packages along with their dependencies, searching for suitable
+pre-compiled binaries on the `The Python Package Index`_.
+
+If you want better control over the process as well as a suitable development
+environment or if you are having problems with `easy_install`, read on to
+:ref:`SetUpWindowsEnv`.
+
+.. _python:  http://www.python.org/
+.. _`The Python Package Index`: http://pypi.python.org
+
+.. _VirtualenvInstallation:
+
+`Virtualenv` install
+--------------------
+
+|cubicweb| can be safely installed, used and contained inside a
+`virtualenv`_. You can use either :ref:`pip <PipInstallation>` or
+:ref:`easy_install <EasyInstallInstallation>` to install |cubicweb|
+inside an activated virtual environment.
+
+.. _PipInstallation:
+
+`pip` install
+-------------
+
+`pip <http://pip.openplans.org/>`_ is a python tool that helps downloading,
+building, installing, and managing Python packages and their dependencies. It
+is fully compatible with `virtualenv`_ and installs the packages from sources
+published on the `The Python Package Index`_.
+
+.. _`virtualenv`: http://virtualenv.openplans.org/
+
+A working compilation chain is needed to build the modules that include C
+extensions. If you really do not want to compile anything, installing `lxml <http://lxml.de/>`_,
+`Twisted Web <http://twistedmatrix.com/trac/wiki/Downloads/>`_ and `libgecode
+<http://www.gecode.org/>`_ will help.
+
+For Debian, these minimal dependencies can be obtained by doing::
+
+  apt-get install gcc python-pip python-dev python-lxml
+
+or, if you prefer to get as much as possible from pip::
+
+  apt-get install gcc python-pip python-dev libxslt1-dev libxml2-dev
+
+For Windows, you can install pre-built packages (possible `source
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/>`_). For a minimal setup, install:
+
+- pip http://www.lfd.uci.edu/~gohlke/pythonlibs/#pip
+- setuptools http://www.lfd.uci.edu/~gohlke/pythonlibs/#setuptools
+- libxml-python http://www.lfd.uci.edu/~gohlke/pythonlibs/#libxml-python>
+- lxml http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml and
+- twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
+
+Make sure to choose the correct architecture and version of Python.
+
+Finally, install |cubicweb| and its dependencies, by running::
+
+  pip install cubicweb
+
+Many other :ref:`cubes <AvailableCubes>` are available. A list is available at
+`PyPI <http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
+or at the `CubicWeb.org forge`_.
+
+For example, installing the *blog cube* is achieved by::
+
+  pip install cubicweb-blog
+
+.. _EasyInstallInstallation:
+
+`easy_install` install
+----------------------
+
+.. note::
+
+   If you are not a Windows user and you have a compilation environment, we
+   recommend you to use the PipInstallation_.
+
+`easy_install`_ is a python utility that helps downloading, installing, and
+managing python packages and their dependencies.
+
+Install |cubicweb| and its dependencies, run::
+
+  easy_install cubicweb
+
+There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
+list of available cubes on `PyPI
+<http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
+or at the `CubicWeb.org Forge`_.
+
+For example, installing the *blog cube* is achieved by::
+
+  easy_install cubicweb-blog
+
+.. note::
+
+  If you encounter problem with :ref:`cubes <AvailableCubes>` installation,
+  consider using :ref:`PipInstallation` which is more stable
+  but can not installed pre-compiled binaries.
+
+.. _`easy_install`: http://packages.python.org/distribute/easy_install.html
+
+
+.. _SourceInstallation:
+
+Install from source
+-------------------
+
+.. _TarballInstallation:
+
+You can download the archive containing the sources from
+`http://download.logilab.org/pub/cubicweb/ <http://download.logilab.org/pub/cubicweb/>`_.
+
+Make sure you also have all the :ref:`InstallDependencies`.
+
+Once uncompressed, you can install the framework from inside the uncompressed
+folder with::
+
+  python setup.py install
+
+Or you can run |cubicweb| directly from the source directory by
+setting the :ref:`resource mode <RessourcesConfiguration>` to `user`. This will
+ease the development with the framework.
+
+There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
+list of availble cubes at the `CubicWeb.org Forge`_.
+
+
+.. _MercurialInstallation:
+
+Install from version control system
+-----------------------------------
+
+To keep-up with on-going development, clone the :ref:`Mercurial
+<MercurialPresentation>` repository::
+
+  hg clone -u 'last(tag())' http://hg.logilab.org/cubicweb # stable version
+  hg clone http://hg.logilab.org/cubicweb # development branch
+
+Make sure you also have all the :ref:`InstallDependencies`.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/site-config.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,94 @@
+.. -*- coding: utf-8 -*-
+
+User interface for web site configuration
+=========================================
+
+.. image:: ../../images/lax-book_03-site-config-panel_en.png
+
+This panel allows you to configure the appearance of your instance site.
+Six menus are available and we will go through each of them to explain how
+to use them.
+
+Navigation
+~~~~~~~~~~
+This menu provides you a way to adjust some navigation options depending on
+your needs, such as the number of entities to display by page of results.
+Follows the detailled list of available options :
+
+* navigation.combobox-limit : maximum number of entities to display in related
+  combo box (sample format: 23)
+* navigation.page-size : maximum number of objects displayed by page of results
+  (sample format: 23)
+* navigation.related-limit : maximum number of related entities to display in
+  the primary view (sample format: 23)
+* navigation.short-line-size : maximum number of characters in short description
+  (sample format: 23)
+
+UI
+~~
+This menu provides you a way to customize the user interface settings such as
+date format or encoding in the produced html.
+Follows the detailled list of available options :
+
+* ui.date-format : how to format date in the ui ("man strftime" for format description)
+* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
+  description)
+* ui.default-text-format : default text format for rich text fields.
+* ui.encoding : user interface encoding
+* ui.fckeditor :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.
+* ui.float-format : how to format float numbers in the ui
+* ui.language : language of the user interface
+* ui.main-template : id of main template used to render pages
+* ui.site-title	: site title, which is displayed right next to the logo in the header
+* ui.time-format : how to format time in the ui ("man strftime" for format description)
+
+
+Actions
+~~~~~~~
+This menu provides a way to configure the context in which you expect the actions
+to be displayed to the user and if you want the action to be visible or not.
+You must have notice that when you view a list of entities, an action box is
+available on the left column which display some actions as well as a drop-down
+menu for more actions.
+
+The context available are :
+
+* mainactions : actions listed in the left box
+* moreactions : actions listed in the `more` menu of the left box
+* addrelated : add actions listed in the left box
+* useractions : actions listed in the first section of drop-down menu
+  accessible from the right corner user login link
+* siteactions : actions listed in the second section of drop-down menu
+  accessible from the right corner user login link
+* hidden : select this to hide the specific action
+
+Boxes
+~~~~~
+The instance has already a pre-defined set of boxes you can use right away.
+This configuration section allows you to place those boxes where you want in the
+instance interface to customize it.
+
+The available boxes are :
+
+* actions box : box listing the applicable actions on the displayed data
+
+* boxes_blog_archives_box : box listing the blog archives
+
+* possible views box : box listing the possible views for the displayed data
+
+* rss box : RSS icon to get displayed data as a RSS thread
+
+* search box : search box
+
+* startup views box : box listing the configuration options available for
+  the instance site, such as `Preferences` and `Site Configuration`
+
+Components
+~~~~~~~~~~
+[WRITE ME]
+
+Contextual components
+~~~~~~~~~~~~~~~~~~~~~
+[WRITE ME]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/depends.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,58 @@
+.. -*- coding: utf-8 -*-
+
+.. _InstallDependencies:
+
+Installation dependencies
+=========================
+
+When you run CubicWeb from source, either by downloading the tarball or
+cloning the mercurial tree, here is the list of tools and libraries you need
+to have installed in order for CubicWeb to work:
+
+* yapps - http://theory.stanford.edu/~amitp/yapps/ -
+  http://pypi.python.org/pypi/Yapps2
+
+* pygraphviz - http://networkx.lanl.gov/pygraphviz/ -
+  http://pypi.python.org/pypi/pygraphviz
+
+* docutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
+
+* lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml
+
+* twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted
+
+* logilab-common - http://www.logilab.org/project/logilab-common -
+  http://pypi.python.org/pypi/logilab-common/
+
+* logilab-database - http://www.logilab.org/project/logilab-database -
+  http://pypi.python.org/pypi/logilab-database/
+
+* logilab-constraint - http://www.logilab.org/project/logilab-constraint -
+  http://pypi.python.org/pypi/constraint/
+
+* logilab-mtconverter - http://www.logilab.org/project/logilab-mtconverter -
+  http://pypi.python.org/pypi/logilab-mtconverter
+
+* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql
+
+* yams - http://www.logilab.org/project/yams - http://pypi.python.org/pypi/yams
+
+* indexer - http://www.logilab.org/project/indexer -
+  http://pypi.python.org/pypi/indexer
+
+* passlib - https://code.google.com/p/passlib/ -
+  http://pypi.python.org/pypi/passlib
+
+If you're using a Postgresql database (recommended):
+
+* psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2
+* plpythonu extension
+
+Other optional packages:
+
+* fyzz - http://www.logilab.org/project/fyzz -
+  http://pypi.python.org/pypi/fyzz *to activate Sparql querying*
+
+
+Any help with the packaging of CubicWeb for more than Debian/Ubuntu (including
+eggs, buildouts, etc) will be greatly appreciated.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/docstrings-conventions.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,106 @@
+Javascript docstrings
+=====================
+
+Whereas in Python source code we only need to include a module docstrings
+using the directive `.. automodule:: mypythonmodule`, we will have to
+explicitely define Javascript modules and functions in the doctrings since
+there is no native directive to include Javascript files.
+
+Rest generation
+---------------
+
+`pyjsrest` is a small utility parsing Javascript doctrings and generating the
+corresponding Restructured file used by Sphinx to generate HTML documentation.
+This script will have the following structure::
+
+  ===========
+  filename.js
+  ===========
+  .. module:: filename.js
+
+We use the `.. module::` directive to register a javascript library
+as a Python module for Sphinx. This provides an entry in the module index.
+
+The contents of the docstring found in the javascript file will be added as is
+following the module declaration. No treatment will be done on the doctring.
+All the documentation structure will be in the docstrings and will comply
+with the following rules.
+
+Docstring structure
+-------------------
+
+Basically we document javascript with RestructuredText docstring
+following the same convention as documenting Python code.
+
+The doctring in Javascript files must be contained in standard
+Javascript comment signs, starting with `/**` and ending with `*/`,
+such as::
+
+ /**
+  * My comment starts here.
+  * This is the second line prefixed with a `*`.
+  * ...
+  * ...
+  * All the follwing line will be prefixed with a `*` followed by a space.
+  * ...
+  * ...
+  */
+
+
+Comments line prefixed by `//` will be ignored. They are reserved for source
+code comments dedicated to developers.
+
+
+Javscript functions docstring
+-----------------------------
+
+By default, the `function` directive describes a module-level function.
+
+`function` directive
+~~~~~~~~~~~~~~~~~~~~
+
+Its purpose is to define the function prototype such as::
+
+    .. function:: loadxhtml(url, data, reqtype, mode)
+
+If any namespace is used, we should add it in the prototype for now,
+until we define an appropriate directive::
+
+    .. function:: jQuery.fn.loadxhtml(url, data, reqtype, mode)
+
+Function parameters
+~~~~~~~~~~~~~~~~~~~
+
+We will define function parameters as a bulleted list, where the
+parameter name will be backquoted and followed by its description.
+
+Example of a javascript function docstring::
+
+    .. function:: loadxhtml(url, data, reqtype, mode)
+
+    cubicweb loadxhtml plugin to make jquery handle xhtml response
+
+    fetches `url` and replaces this's content with the result
+
+    Its arguments are:
+
+    * `url`
+
+    * `mode`, how the replacement should be done (default is 'replace')
+       Possible values are :
+           - 'replace' to replace the node's content with the generated HTML
+           - 'swap' to replace the node itself with the generated HTML
+           - 'append' to append the generated HTML to the node's content
+
+
+Optional parameter specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Javascript functions handle arguments not listed in the function signature.
+In the javascript code, they will be flagged using `/* ... */`. In the docstring,
+we flag those optional arguments the same way we would define it in
+Python::
+
+    .. function:: asyncRemoteExec(fname, arg1=None, arg2=None)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/faq.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,433 @@
+.. -*- coding: utf-8 -*-
+
+Frequently Asked Questions (FAQ)
+================================
+
+
+Generalities
+````````````
+
+Why do you use the LGPL license to prevent me from doing X ?
+------------------------------------------------------------
+
+LGPL means that *if* you redistribute your application, you need to
+redistribute the changes you made to CubicWeb under the LGPL licence.
+
+Publishing a web site has nothing to do with redistributing source
+code according to the terms of the LGPL. A fair amount of companies
+use modified LGPL code for internal use. And someone could publish a
+*CubicWeb* component under a BSD licence for others to plug into a
+LGPL framework without any problem. The only thing we are trying to
+prevent here is someone taking the framework and packaging it as
+closed source to his own clients.
+
+Why does not CubicWeb have a template language ?
+------------------------------------------------
+
+There are enough template languages out there. You can use your
+preferred template language if you want. [explain how to use a
+template language]
+
+*CubicWeb* does not define its own templating language as this was
+not our goal. Based on our experience, we realized that
+we could gain productivity by letting designers use design tools
+and developpers develop without the use of the templating language
+as an intermediary that could not be anyway efficient for both parties.
+Python is the templating language that we use in *CubicWeb*, but again,
+it does not prevent you from using a templating language.
+
+Moreover, CubicWeb currently supports `simpletal`_ out of the box and
+it is also possible to use the `cwtags`_ library to build html trees
+using the `with statement`_ with more comfort than raw strings.
+
+.. _`simpletal`: http://www.owlfish.com/software/simpleTAL/
+.. _`cwtags`: http://www.cubicweb.org/project/cwtags
+.. _`with statement`: http://www.python.org/dev/peps/pep-0343/
+
+Why do you think using pure python is better than using a template language ?
+-----------------------------------------------------------------------------
+
+Python is an Object Oriented Programming language and as such it
+already provides a consistent and strong architecture and syntax
+a templating language would not reach.
+
+Using Python instead of a template langage for describing the user interface
+makes it to maintain with real functions/classes/contexts without the need of
+learning a new dialect. By using Python, we use standard OOP techniques and
+this is a key factor in a robust application.
+
+CubicWeb looks pretty recent. Is it stable ?
+--------------------------------------------
+
+It is constantly evolving, piece by piece.  The framework has evolved since
+2001 and data has been migrated from one schema to the other ever since. There
+is a well-defined way to handle data and schema migration.
+
+You can see the roadmap there:
+http://www.cubicweb.org/project/cubicweb?tab=projectroadmap_tab.
+
+
+Why is the RQL query language looking similar to X ?
+----------------------------------------------------
+
+It may remind you of SQL but it is higher level than SQL, more like
+SPARQL. Except that SPARQL did not exist when we started the project.
+With version 3.4, CubicWeb has support for SPARQL.
+
+The RQL language is what is going to make a difference with django-
+like frameworks for several reasons.
+
+1. accessing data is *much* easier with it. One can write complex
+   queries with RQL that would be tedious to define and hard to maintain
+   using an object/filter suite of method calls.
+
+2. it offers an abstraction layer allowing your applications to run
+   on multiple back-ends. That means not only various SQL backends
+   (postgresql, sqlite, sqlserver, mysql), but also non-SQL data stores like
+   LDAP directories and subversion/mercurial repositories (see the `vcsfile`
+   component).
+
+Which ajax library is CubicWeb using ?
+--------------------------------------
+
+CubicWeb uses jQuery_ and provides a few helpers on top of that. Additionally,
+some jQuery plugins are provided (some are provided in specific cubes).
+
+.. _jQuery: http://jquery.com
+
+
+Development
+```````````
+
+How to change the instance logo ?
+---------------------------------
+
+The logo is managed by css. You must provide a custom css that will contain
+the code below: 
+
+::
+   
+     #logo {
+        background-image: url("logo.jpg");
+     }
+
+
+``logo.jpg`` is in ``mycube/data`` directory.
+
+How to create an anonymous user ?
+---------------------------------
+
+This allows to browse the site without being authenticated. In the
+``all-in-one.conf`` file of your instance, define the anonymous user
+as follows ::
+
+  # login of the CubicWeb user account to use for anonymous user (if you want to
+  # allow anonymous)
+  anonymous-user=anon
+
+  # password of the CubicWeb user account matching login
+  anonymous-password=anon
+
+You also must ensure that this `anon` user is a registered user of
+the DB backend. If not, you can create through the administation
+interface of your instance by adding a user with in the group `guests`.
+
+.. note::
+    While creating a new instance, you can decide to allow access
+    to anonymous user, which will automatically execute what is
+    decribed above.
+
+
+How to format an entity date attribute ?
+----------------------------------------
+
+If your schema has an attribute of type `Date` or `Datetime`, you usually want to
+format it when displaying it. First, you should define your preferred format
+using the site configuration panel
+``http://appurl/view?vid=systempropertiesform`` and then set ``ui.date`` and/or
+``ui.datetime``.  Then in the view code, use:
+
+.. sourcecode:: python
+
+    entity.printable_value(date_attribute)
+
+which will always return a string whatever the attribute's type (so it's
+recommended also for other attribute types). By default it expects to generate
+HTML, so it deals with rich text formating, xml escaping...
+
+How to update a database after a schema modification ?
+------------------------------------------------------
+
+It depends on what has been modified in the schema.
+
+* update the permissions and properties of an entity or a relation:
+  ``sync_schema_props_perms('MyEntityOrRelation')``.
+
+* add an attribute: ``add_attribute('MyEntityType', 'myattr')``.
+
+* add a relation: ``add_relation_definition('SubjRelation', 'MyRelation', 'ObjRelation')``.
+
+I get `NoSelectableObject` exceptions, how do I debug selectors ?
+-----------------------------------------------------------------
+
+You just need to put the appropriate context manager around view/component
+selection. One standard place for components is in cubicweb/vregistry.py: 
+
+.. sourcecode:: python
+
+    def possible_objects(self, *args, **kwargs):
+        """return an iterator on possible objects in this registry for the given
+        context
+        """
+        from logilab.common.registry import traced_selection
+        with traced_selection():
+            for appobjects in self.itervalues():
+                try:
+                    yield self._select_best(appobjects, *args, **kwargs)
+                except NoSelectableObject:
+                    continue
+
+This will yield additional WARNINGs, like this::
+
+    2009-01-09 16:43:52 - (cubicweb.selectors) WARNING: selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
+
+For views, you can put this context in `cubicweb/web/views/basecontrollers.py` in
+the `ViewController`:
+
+.. sourcecode:: python
+
+    def _select_view_and_rset(self, rset):
+        ...
+        try:
+            from logilab.common.registry import traced_selection
+            with traced_selection():
+                view = self._cw.vreg['views'].select(vid, req, rset=rset)
+        except ObjectNotFound:
+            self.warning("the view %s could not be found", vid)
+            req.set_message(req._("The view %s could not be found") % vid)
+            vid = vid_from_rset(req, rset, self._cw.vreg.schema)
+            view = self._cw.vreg['views'].select(vid, req, rset=rset)
+        ...
+
+I get "database is locked" when executing tests
+-----------------------------------------------
+
+If you have "database is locked" as error when you are executing security tests,
+it is usually because commit or rollback are missing before login() calls.
+
+You can also use a context manager, to avoid such errors, as described
+here: :ref:`securitytest`.
+
+
+What are hooks used for ?
+-------------------------
+
+Hooks are executed around (actually before or after) events.  The most common
+events are data creation, update and deletion.  They permit additional constraint
+checking (those not expressible at the schema level), pre and post computations
+depending on data movements.
+
+As such, they are a vital part of the framework.
+
+Other kinds of hooks, called Operations, are available
+for execution just before commit.
+
+For more information, read :ref:`hooks` section.
+
+
+Configuration
+`````````````
+
+How to configure a LDAP source ?
+--------------------------------
+
+See :ref:`LDAP`.
+
+How to import LDAP users in |cubicweb| ?
+----------------------------------------
+
+  Here is a useful script which enables you to import LDAP users
+  into your *CubicWeb* instance by running the following:
+
+.. sourcecode:: python
+
+    import os
+    import pwd
+    import sys
+
+    from logilab.database import get_connection
+
+    def getlogin():
+        """avoid using os.getlogin() because of strange tty/stdin problems
+        (man 3 getlogin)
+        Another solution would be to use $LOGNAME, $USER or $USERNAME
+        """
+        return pwd.getpwuid(os.getuid())[0]
+
+
+    try:
+        database = sys.argv[1]
+    except IndexError:
+        print 'USAGE: python ldap2system.py <database>'
+        sys.exit(1)
+
+    if raw_input('update %s db ? [y/n]: ' % database).strip().lower().startswith('y'):
+        cnx = get_connection(user=getlogin(), database=database)
+        cursor = cnx.cursor()
+
+        insert = ('INSERT INTO euser (creation_date, eid, modification_date, login, '
+                  ' firstname, surname, last_login_time, upassword) '
+                  "VALUES (%(mtime)s, %(eid)s, %(mtime)s, %(login)s, %(firstname)s, "
+                  "%(surname)s, %(mtime)s, './fqEz5LeZnT6');")
+        update = "UPDATE entities SET source='system' WHERE eid=%(eid)s;"
+        cursor.execute("SELECT eid,type,source,extid,mtime FROM entities WHERE source!='system'")
+        for eid, type, source, extid, mtime in cursor.fetchall():
+            if type != 'CWUser':
+                print "don't know what to do with entity type", type
+                continue
+            if source != 'ldapuser':
+                print "don't know what to do with source type", source
+                continue
+            ldapinfos = dict(x.strip().split('=') for x in extid.split(','))
+            login = ldapinfos['uid']
+            firstname = ldapinfos['uid'][0].upper()
+            surname = ldapinfos['uid'][1:].capitalize()
+            if login != 'jcuissinat':
+                args = dict(eid=eid, type=type, source=source, login=login,
+                            firstname=firstname, surname=surname, mtime=mtime)
+                print args
+                cursor.execute(insert, args)
+                cursor.execute(update, args)
+
+        cnx.commit()
+        cnx.close()
+
+
+Security
+````````
+
+How to reset the password for user joe ?
+----------------------------------------
+
+If you want to reset the admin password for ``myinstance``, do::
+
+    $ cubicweb-ctl reset-admin-pwd myinstance
+
+You need to generate a new encrypted password::
+
+    $ python
+    >>> from cubicweb.server.utils import crypt_password
+    >>> crypt_password('joepass')
+    'qHO8282QN5Utg'
+    >>>
+
+and paste it in the database::
+
+    $ psql mydb
+    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 import Binary
+    >>> 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.cw_set(upassword=Binary(crypted))
+
+Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file.
+
+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 !
+---------------------------------------------------------
+
+You are probably getting errors such as ::
+
+  remove {'PR': 'Project', 'C': 'CWUser'} from solutions since your_user has no read access to cost
+
+This is because you have to put your user in the "users" group. The user has to
+be in both groups.
+
+How is security implemented ?
+------------------------------
+
+The basis for security is a mapping from operations to groups or
+arbitrary RQL expressions. These mappings are scoped to entities and
+relations.
+
+This is an example for an Entity Type definition:
+
+.. sourcecode:: python
+
+    class Version(EntityType):
+        """a version is defining the content of a particular project's
+        release"""
+        # definition of attributes is voluntarily missing
+        __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'),)}
+
+The above means that permission to read a Version is granted to any
+user that is part of one of the groups 'managers', 'users', 'guests'.
+The 'add' permission is granted to users in group 'managers' or
+'logilab' or to users in group G, if G is linked by a permission
+entity named "add_version" to the version's project.
+
+An example for a Relation Definition (RelationType both defines a
+relation type and implicitly one relation definition, on which the
+permissions actually apply):
+
+.. sourcecode:: python
+
+    class version_of(RelationType):
+        """link a version to its project. A version is necessarily linked
+        to one and only one project. """
+        # some lines voluntarily missing
+        __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'),) }
+
+The main difference lies in the basic available operations (there is
+no 'update' operation) and the usage of an RRQLExpression (rql
+expression for a relation) instead of an ERQLExpression (rql
+expression for an entity).
+
+You can find additional information in the section :ref:`securitymodel`.
+
+Is it possible to bypass security from the UI (web front) part ?
+----------------------------------------------------------------
+
+No. Only Hooks/Operations can do that.
+
+Can PostgreSQL and CubicWeb authentication work with kerberos ?
+----------------------------------------------------------------
+
+If you have PostgreSQL set up to accept kerberos authentication, you can set
+the db-host, db-name and db-user parameters in the `sources` configuration
+file while leaving the password blank. It should be enough for your
+instance to connect to postgresql with a kerberos ticket.
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,19 @@
+.. -*- coding: utf-8 -*-
+
+.. _Part4:
+
+----------
+Appendixes
+----------
+
+The following chapters are reference material.
+
+.. toctree::
+   :maxdepth: 1
+   :numbered:
+
+   faq
+   rql/index
+   mercurial
+   depends
+   docstrings-conventions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/mercurial.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,131 @@
+.. -*- coding: utf-8 -*-
+
+.. _MercurialPresentation:
+
+Introducing Mercurial
+=====================
+
+Introduction
+````````````
+Mercurial_ manages a distributed repository containing revisions
+trees (each revision indicates the changes required to obtain the
+next, and so on). Locally, we have a repository containing revisions
+tree, and a working directory. It is possible
+to put in its working directory, one of the versions of its local repository,
+modify and then push it in its repository.
+It is also possible to get revisions from another repository or to export
+its own revisions from the local repository to another repository.
+
+.. _Mercurial: http://www.selenic.com/mercurial/
+
+In contrast to CVS/Subversion, we usually create a repository per
+project to manage.
+
+In a collaborative development, we usually create a central repository
+accessible to all developers of the project. These central repository is used
+as a reference. According to their needs, everyone can have a local repository,
+that they will have to synchronize with the central repository from time to time.
+
+
+Major commands
+``````````````
+* Create a local repository::
+
+     hg clone ssh://myhost//home/src/repo
+
+* See the contents of the local repository (graphical tool in Qt)::
+
+     hgview
+
+* Add a sub-directory or file in the current directory::
+
+     hg add subdir
+
+* Move to the working directory a specific revision (or last
+  revision) from the local repository::
+
+     hg update [identifier-revision]
+     hg up [identifier-revision]
+
+* Get in its local repository, the tree of revisions contained in a
+  remote repository (this does not change the local directory)::
+
+     hg pull ssh://myhost//home/src/repo
+     hg pull -u ssh://myhost//home/src/repo # equivalent to pull + update
+
+* See what are the heads of branches of the local repository if a `pull`
+  returned a new branch::
+
+     hg heads
+
+* Submit the working directory in the local repository (and create a new
+  revision)::
+
+     hg commit
+     hg ci
+
+* Merge with the mother revision of local directory, another revision from
+  the local respository (the new revision will be then two mothers
+  revisions)::
+
+     hg merge identifier-revision
+
+* Export to a remote repository, the tree of revisions in its content
+  local respository (this does not change the local directory)::
+
+     hg push ssh://myhost//home/src/repo
+
+* See what local revisions are not in another repository::
+
+     hg outgoing ssh://myhost//home/src/repo
+
+* See what are the revisions of a repository not found locally::
+
+     hg incoming ssh://myhost//home/src/repo
+
+* See what is the revision of the local repository which has been taken out
+  from the working directory and amended::
+
+     hg parent
+
+* See the differences between the working directory and the mother revision
+  of the local repository, possibly to submit them in the local repository::
+
+     hg diff
+     hg commit-tool
+     hg ct
+
+
+Best Practices
+``````````````
+* Remember to `hg pull -u` regularly, and particularly before
+   a `hg commit`.
+
+* Remember to `hg push` when your repository contains a version
+  relatively stable of your changes.
+
+* If a `hg pull -u` created a new branch head:
+
+   1. find its identifier with `hg head`
+   2. merge with `hg merge`
+   3. `hg ci`
+   4. `hg push`
+
+Installation of the guestrepo extension
+```````````````````````````````````````
+
+Set up the guestrepo extension by getting a copy of the sources
+from https://bitbucket.org/selinc/guestrepo and adding the following
+lines to your ``~/.hgrc``: ::
+
+   [extensions]
+   guestrepo=/path/to/guestrepo/guestrepo
+
+
+More information
+````````````````
+
+For more information about Mercurial, please refer to the Mercurial project online documentation_.
+
+.. _documentation: http://www.selenic.com/mercurial/wiki/
+
Binary file doc/book/annexes/rql/Graph-ex.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/debugging.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,55 @@
+.. -*- coding: utf-8 -*-
+
+.. _DEBUGGING:
+
+Debugging RQL
+-------------
+
+Available levels
+~~~~~~~~~~~~~~~~
+
+Server debugging flags. They may be combined using binary operators.
+
+.. autodata:: cubicweb.server.DBG_NONE
+.. autodata:: cubicweb.server.DBG_RQL
+.. autodata:: cubicweb.server.DBG_SQL
+.. autodata:: cubicweb.server.DBG_REPO
+.. autodata:: cubicweb.server.DBG_MS
+.. autodata:: cubicweb.server.DBG_HOOKS
+.. autodata:: cubicweb.server.DBG_OPS
+.. autodata:: cubicweb.server.DBG_MORE
+.. autodata:: cubicweb.server.DBG_ALL
+
+
+Enable verbose output
+~~~~~~~~~~~~~~~~~~~~~
+
+To debug your RQL statements, it can be useful to enable a verbose output:
+
+.. sourcecode:: python
+
+    from cubicweb import server
+    server.set_debug(server.DBG_RQL|server.DBG_SQL|server.DBG_ALL)
+
+.. autofunction:: cubicweb.server.set_debug
+
+Another example showing how to debug hooks at a specific code site:
+
+.. sourcecode:: python
+
+    from cubicweb.server import debugged, DBG_HOOKS
+    with debugged(DBG_HOOKS):
+        person.cw_set(works_for=company)
+
+
+Detect largest RQL queries
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+See `Profiling and performance` chapter (see :ref:`PROFILING`).
+
+
+API
+~~~
+
+.. autoclass:: cubicweb.server.debugged
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/implementation.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,159 @@
+
+
+Implementation
+--------------
+
+BNF grammar
+~~~~~~~~~~~
+
+The terminal elements are in capital letters, non-terminal in lowercase.
+The value of the terminal elements (between quotes) is a Python regular
+expression.
+::
+
+     statement ::= (select | delete | insert | update) ';'
+
+
+     # select specific rules
+     select      ::= 'DISTINCT'? E_TYPE selected_terms restriction? group? sort?
+
+     selected_terms ::= expression ( ',' expression)*
+
+     group       ::= 'GROUPBY' VARIABLE ( ',' VARIABLE)*
+
+     sort        ::= 'ORDERBY' sort_term ( ',' sort_term)*
+
+     sort_term   ::=  VARIABLE sort_method =?
+
+     sort_method ::= 'ASC' | 'DESC'
+
+
+     # delete specific rules
+     delete ::= 'DELETE' (variables_declaration | relations_declaration) restriction?
+
+
+     # insert specific rules
+     insert ::= 'INSERT' variables_declaration ( ':' relations_declaration)? restriction?
+
+
+     # update specific rules
+     update ::= 'SET' relations_declaration restriction
+
+
+     # common rules
+     variables_declaration ::= E_TYPE VARIABLE (',' E_TYPE VARIABLE)*
+
+     relations_declaration ::= simple_relation (',' simple_relation)*
+
+     simple_relation ::= VARIABLE R_TYPE expression
+
+     restriction ::= 'WHERE' relations
+
+     relations   ::= relation (LOGIC_OP relation)*
+                   | '(' relations')'
+
+     relation    ::= 'NOT'? VARIABLE R_TYPE COMP_OP? expression
+                   | 'NOT'? R_TYPE VARIABLE 'IN' '(' expression (',' expression)* ')'
+
+     expression  ::= var_or_func_or_const (MATH_OP var_or_func_or_const) *
+                   | '(' expression ')'
+
+     var_or_func_or_const ::= VARIABLE | function | constant
+
+     function    ::= FUNCTION '(' expression ( ',' expression) * ')'
+
+     constant    ::= KEYWORD | STRING | FLOAT | INT
+
+     # tokens
+     LOGIC_OP ::= ',' | 'OR' | 'AND'
+     MATH_OP  ::= '+' | '-' | '/' | '*'
+     COMP_OP  ::= '>' | '>=' | '=' | '<=' | '<' | '~=' | 'LIKE'
+
+     FUNCTION ::= 'MIN' | 'MAX' | 'SUM' | 'AVG' | 'COUNT' | 'UPPER' | 'LOWER'
+
+     VARIABLE ::= '[A-Z][A-Z0-9]*'
+     E_TYPE   ::= '[A-Z]\w*'
+     R_TYPE   ::= '[a-z_]+'
+
+     KEYWORD  ::= 'TRUE' | 'FALSE' | 'NULL' | 'TODAY' | 'NOW'
+     STRING   ::= "'([^'\]|\\.)*'" |'"([^\"]|\\.)*\"'
+     FLOAT    ::= '\d+\.\d*'
+     INT      ::= '\d+'
+
+
+Internal representation (syntactic tree)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The tree research does not contain the selected variables
+(e.g. there is only what follows "WHERE").
+
+The insertion tree does not contain the variables inserted or relations
+defined on these variables (e.g. there is only what follows "WHERE").
+
+The removal tree does not contain the deleted variables and relations
+(e.g. there is only what follows the "WHERE").
+
+The update tree does not contain the variables and relations updated
+(e.g. there is only what follows the "WHERE").
+
+::
+
+     Select         ((Relationship | And | Or)?, Group?, Sort?)
+     Insert         (Relations | And | Or)?
+     Delete         (Relationship | And | Or)?
+     Update         (Relations | And | Or)?
+
+     And            ((Relationship | And | Or), (Relationship | And | Or))
+     Or             ((Relationship | And | Or), (Relationship | And | Or))
+
+     Relationship   ((VariableRef, Comparison))
+
+     Comparison     ((Function | MathExpression | Keyword | Constant | VariableRef) +)
+
+     Function       (())
+     MathExpression ((MathExpression | Keyword | Constant | VariableRef), (MathExpression | Keyword | Constant | VariableRef))
+
+     Group          (VariableRef +)
+     Sort           (SortTerm +)
+     SortTerm       (VariableRef +)
+
+     VariableRef    ()
+     Variable       ()
+     Keyword        ()
+     Constant       ()
+
+
+Known limitations
+~~~~~~~~~~~~~~~~~
+
+- The current implementation does not support linking two relations of type 'is'
+  with an OR. I do not think that the negation is supported on this type of
+  relation (XXX to be confirmed).
+
+- missing COALESCE and certainly other things...
+
+- writing an rql query requires knowledge of the used schema (with real relation
+  names and entities, not those viewed in the user interface). On the other
+  hand, we cannot really bypass that, and it is the job of a user interface to
+  hide the RQL.
+
+
+Topics
+~~~~~~
+
+It would be convenient to express the schema matching
+relations (non-recursive rules)::
+
+     Document class Type <-> Document occurence_of Fiche class Type
+     Sheet class Type    <-> Form collection Collection class Type
+
+Therefore 1. becomes::
+
+     Document X where
+     X class C, C name 'Cartoon'
+     X owned_by U, U login 'syt'
+     X available true
+
+I'm not sure that we should handle this at RQL level ...
+
+There should also be a special relation 'anonymous'.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,14 @@
+.. _RQLChapter:
+
+Relation Query Language (RQL)
+=============================
+
+This chapter describes the Relation Query Language syntax and its implementation in CubicWeb.
+
+.. toctree::
+   :maxdepth: 2
+
+   intro
+   language
+   debugging
+   implementation
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/intro.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,162 @@
+
+.. _rql_intro:
+
+Introduction
+------------
+
+Goals of RQL
+~~~~~~~~~~~~
+
+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).
+
+Comparison with existing languages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+SQL
+```
+
+RQL may remind of SQL but works at a higher abstraction level (the *CubicWeb*
+framework generates SQL from RQL to fetch data from relation databases). RQL is
+focused on browsing relations. The user needs only to know about the *CubicWeb*
+data model he is querying, but not about the underlying SQL model.
+
+Sparql
+``````
+
+The query language most similar to RQL is SPARQL_, defined by the W3C to serve
+for the semantic web.
+
+Versa
+`````
+
+We should look in more detail, but here are already some ideas for the moment
+... Versa_ is the language most similar to what we wanted to do, but the model
+underlying data being RDF, there are some things such as namespaces or
+handling of the RDF types which does not interest us. On the functionality
+level, Versa_ is very comprehensive including through many functions of
+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
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Search (`Any`)
+   Extract entities and attributes of entities.
+
+Insert entities (`INSERT`)
+   Insert new entities or relations in the database.
+   It can also directly create relationships for the newly created entities.
+
+Update entities, create relations (`SET`)
+   Update existing entities in the database,
+   or create relations between existing entities.
+
+Delete entities or relationship (`DELETE`)
+   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`_
+
+
+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)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/language.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,804 @@
+.. -*- coding: utf-8 -*-
+
+.. _RQL:
+
+RQL syntax
+----------
+
+.. _RQLKeywords:
+
+Reserved keywords
+~~~~~~~~~~~~~~~~~
+
+::
+
+  AND, ASC, BEING, DELETE, DESC, DISTINCT, EXISTS, FALSE, GROUPBY,
+  HAVING, ILIKE, INSERT, LIKE, LIMIT, NOT, NOW, NULL, OFFSET,
+  OR, ORDERBY, SET, TODAY, TRUE, UNION, WHERE, WITH
+
+The keywords are not case sensitive. You should not use them when defining your
+schema, or as RQL variable names.
+
+
+.. _RQLCase:
+
+Case
+~~~~
+
+* Variables should be all upper-cased.
+
+* Relation should be all lower-cased and match exactly names of relations defined
+  in the schema.
+
+* Entity types should start with an upper cased letter and be followed by at least
+  a lower cased latter.
+
+
+.. _RQLVariables:
+
+Variables and typing
+~~~~~~~~~~~~~~~~~~~~
+
+Entities and values to browse and/or select are represented in the query by
+*variables* that must be written in capital letters.
+
+With RQL, we do not distinguish between entities and attributes. The value of an
+attribute is considered as an entity of a particular type (see below), linked to
+one (real) entity by a relation called the name of the attribute, where the
+entity is the subject and the attribute the object.
+
+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.
+
+We can restrict the possible types for a variable using the special relation
+**is** in the restrictions.
+
+
+.. _VirtualRelations:
+
+Virtual relations
+~~~~~~~~~~~~~~~~~
+
+Those relations may only be used in RQL query but are not actual attributes of
+your entities.
+
+* `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 is the same as another
+  when you've to use two different variables for querying purpose. On the
+  opposite it's also useful together with the ``NOT`` operator to tell that two
+  variables should not identify the same entity
+
+
+.. _RQLLiterals:
+
+Literal expressions
+~~~~~~~~~~~~~~~~~~~
+
+Bases types supported by RQL are those supported by yams schema. Literal values
+are expressed as explained below:
+
+* string should be between double or single quotes. If the value contains a
+  quote, it should be preceded by a backslash '\\'
+
+* floats separator is dot '.'
+
+* boolean values are ``TRUE`` and ``FALSE`` keywords
+
+* date and time should be expressed as a string with ISO notation : YYYY/MM/DD
+  [hh:mm], or using keywords ``TODAY`` and ``NOW``
+
+You may also use the ``NULL`` keyword, meaning 'unspecified'.
+
+
+.. _RQLOperators:
+
+Operators
+~~~~~~~~~
+
+.. _RQLLogicalOperators:
+
+Logical operators
+`````````````````
+::
+
+     AND, OR, NOT, ','
+
+',' is equivalent to 'AND' but with the smallest among the priority of logical
+operators (see :ref:`RQLOperatorsPriority`).
+
+.. _RQLMathematicalOperators:
+
+Mathematical operators
+``````````````````````
+
++----------+---------------------+-----------+--------+
+| Operator |    Description      | Example   | Result |
++==========+=====================+===========+========+
+|  `+`     | addition            | 2 + 3     | 5      |
++----------+---------------------+-----------+--------+
+|  `-`     | subtraction         | 2 - 3     | -1     |
++----------+---------------------+-----------+--------+
+|  `*`     | multiplication      | 2 * 3     | 6      |
++----------+---------------------+-----------+--------+
+|  /       | division            | 4 / 2     | 2      |
++----------+---------------------+-----------+--------+
+|  %       | modulo (remainder)  | 5 % 4     | 1      |
++----------+---------------------+-----------+--------+
+|  ^       | exponentiation      | 2.0 ^ 3.0 | 8      |
++----------+---------------------+-----------+--------+
+|  &       | bitwise AND         | 91 & 15   | 11     |
++----------+---------------------+-----------+--------+
+|  `|`     | bitwise OR          | 32 | 3    | 35     |
++----------+---------------------+-----------+--------+
+|  #       | bitwise XOR         | 17 # 5    | 20     |
++----------+---------------------+-----------+--------+
+|  ~       | bitwise NOT         | ~1        | -2     |
++----------+---------------------+-----------+--------+
+|  <<      | bitwise shift left  | 1 << 4    | 16     |
++----------+---------------------+-----------+--------+
+|  >>      | bitwise shift right | 8 >> 2    | 2      |
++----------+---------------------+-----------+--------+
+
+
+Notice integer division truncates results depending on the backend behaviour. For
+instance, postgresql does.
+
+
+.. _RQLComparisonOperators:
+
+Comparison operators
+````````````````````
+ ::
+
+     =, !=, <, <=, >=, >, IN
+
+
+The syntax to use comparison operators is:
+
+    `VARIABLE attribute <operator> VALUE`
+
+The `=` operator is the default operator and can be omitted, i.e. :
+
+    `VARIABLE attribute = VALUE`
+
+is equivalent to
+
+    `VARIABLE attribute VALUE`
+
+
+The operator `IN` provides a list of possible values:
+
+.. sourcecode:: sql
+
+    Any X WHERE X name IN ('chauvat', 'fayolle', 'di mascio', 'thenault')
+
+
+.. _RQLStringOperators:
+
+String operators
+````````````````
+::
+
+  LIKE, ILIKE, ~=, REGEXP
+
+The ``LIKE`` string operator can be used with the special character `%` in
+a string as wild-card:
+
+.. sourcecode:: sql
+
+     -- match every entity whose name starts with 'Th'
+     Any X WHERE X name ~= 'Th%'
+     -- match every entity whose name endswith 'lt'
+     Any X WHERE X name LIKE '%lt'
+     -- match every entity whose name contains a 'l' and a 't'
+     Any X WHERE X name LIKE '%l%t%'
+
+``ILIKE`` is the case insensitive version of ``LIKE``. It's not
+available on all backend (e.g. sqlite doesn't support it). If not available for
+your backend, ``ILIKE`` will behave like ``LIKE``.
+
+`~=` is a shortcut version of ``ILIKE``, or of ``LIKE`` when the
+former is not available on the back-end.
+
+
+The ``REGEXP`` is an alternative to ``LIKE`` that supports POSIX
+regular expressions:
+
+.. sourcecode:: sql
+
+   -- match entities whose title starts with a digit
+   Any X WHERE X title REGEXP "^[0-9].*"
+
+
+The underlying SQL operator used is back-end-dependent :
+
+- the ``~`` operator is used for postgresql,
+- the ``REGEXP`` operator for mysql and sqlite.
+
+Other back-ends are not supported yet.
+
+
+.. _RQLOperatorsPriority:
+
+Operators priority
+``````````````````
+
+#. `(`, `)`
+#. `^`, `<<`, `>>`
+#. `*`, `/`, `%`, `&`
+#. `+`, `-`, `|`, `#`
+#. `NOT`
+#. `AND`
+#. `OR`
+#. `,`
+
+
+.. _RQLSearchQuery:
+
+Search Query
+~~~~~~~~~~~~
+
+Simplified grammar of search query: ::
+
+   [ `DISTINCT`] `Any` V1 (, V2) \*
+   [ `GROUPBY` V1 (, V2) \*] [ `ORDERBY` <orderterms>]
+   [ `LIMIT` <value>] [ `OFFSET` <value>]
+   [ `WHERE` <triplet restrictions>]
+   [ `WITH` V1 (, V2)\* BEING (<query>)]
+   [ `HAVING` <other restrictions>]
+   [ `UNION` <query>]
+
+Selection
+`````````
+
+The fist occuring clause is the selection of terms that should be in the result
+set.  Terms may be variable, literals, function calls, arithmetic, etc. and each
+term is separated by a comma.
+
+There will be as much column in the result set as term in this clause, respecting
+order.
+
+Syntax for function call is somewhat intuitive, for instance:
+
+.. sourcecode:: sql
+
+    Any UPPER(N) WHERE P firstname N
+
+
+Grouping and aggregating
+````````````````````````
+
+The ``GROUPBY`` keyword is followed by a list of terms on which results
+should be grouped. They are usually used with aggregate functions, responsible to
+aggregate values for each group (see :ref:`RQLAggregateFunctions`).
+
+For grouped queries, all selected variables must be either aggregated (i.e. used
+by an aggregate function) or grouped (i.e. listed in the ``GROUPBY``
+clause).
+
+
+Sorting
+```````
+
+The ``ORDERBY`` keyword if followed by the definition of the selection
+order: variable or column number followed by sorting method (``ASC``,
+``DESC``), ``ASC`` being the default. If the sorting method is not
+specified, then the sorting is ascendant (`ASC`).
+
+
+Pagination
+``````````
+
+The ``LIMIT`` and ``OFFSET`` keywords may be respectively used to
+limit the number of results and to tell from which result line to start (for
+instance, use `LIMIT 20` to get the first 20 results, then `LIMIT 20 OFFSET 20`
+to get the next 20.
+
+
+Restrictions
+````````````
+
+The ``WHERE`` keyword introduce one of the "main" part of the query, where
+you "define" variables and add some restrictions telling what you're interested
+in.
+
+It's a list of triplets "subject relation object", e.g. `V1 relation
+(V2 | <static value>)`. Triplets are separated using :ref:`RQLLogicalOperators`.
+
+.. note::
+
+  About the negation operator (``NOT``):
+
+  * ``NOT X relation Y`` is equivalent to ``NOT EXISTS(X relation Y)``
+
+  * ``Any X WHERE NOT X owned_by U`` means "entities that have no relation
+    ``owned_by``".
+
+  * ``Any X WHERE NOT X owned_by U, U login "syt"`` means "the entity have no
+     relation ``owned_by`` with the user syt". They may have a relation "owned_by"
+     with another user.
+
+In this clause, you can also use ``EXISTS`` when you want to know if some
+expression is true and do not need the complete set of elements that make it
+true. Testing for existence is much faster than fetching the complete set of
+results, especially when you think about using ``OR`` against several expressions. For instance
+if you want to retrieve versions which are in state "ready" or tagged by
+"priority", you should write :
+
+.. sourcecode:: sql
+
+    Any X ORDERBY PN,N
+    WHERE X num N, X version_of P, P name PN,
+          EXISTS(X in_state S, S name "ready")
+          OR EXISTS(T tags X, T name "priority")
+
+not
+
+.. sourcecode:: sql
+
+    Any X ORDERBY PN,N
+    WHERE X num N, X version_of P, P name PN,
+          (X in_state S, S name "ready")
+          OR (T tags X, T name "priority")
+
+Both queries aren't at all equivalent :
+
+* the former will retrieve all versions, then check for each one which are in the
+  matching state of or tagged by the expected tag,
+
+* the later will retrieve all versions, state and tags (cartesian product!),
+  compute join and then exclude each row which are in the matching state or
+  tagged by the expected tag. This implies that you won't get any result if the
+  in_state or tag tables are empty (ie there is no such relation in the
+  application). This is usually NOT what you want.
+
+Another common case where you may want to use ``EXISTS`` is when you
+find yourself using ``DISTINCT`` at the beginning of your query to
+remove duplicate results. The typical case is when you have a
+multivalued relation such as Version version_of Project and you want
+to retrieve projects which have a version:
+
+.. sourcecode:: sql
+
+  Any P WHERE V version_of P
+
+will return each project number of versions times. So you may be
+tempted to use:
+
+.. sourcecode:: sql
+
+  DISTINCT Any P WHERE V version_of P
+
+This will work, but is not efficient, as it will use the ``SELECT
+DISTINCT`` SQL predicate, which needs to retrieve all projects, then
+sort them and discard duplicates, which can have a very high cost for
+large result sets. So the best way to write this is:
+
+.. sourcecode:: sql
+
+  Any P WHERE EXISTS(V version_of P)
+
+
+You can also use the question mark (`?`) to mark optional relations. This allows
+you to select entities related **or not** to another. It is a similar concept
+to `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).
+
+You must use the `?` behind a variable to specify that the relation to
+that variable is optional. For instance:
+
+- Bugs of a project attached or not to a version
+
+   .. sourcecode:: sql
+
+       Any X, V WHERE X concerns P, P eid 42, X corrected_in V?
+
+  You will get a result set containing all the project's tickets, with either the
+  version in which it's fixed or None for tickets not related to a version.
+
+
+- All cards and the project they document if any
+
+  .. sourcecode:: sql
+
+       Any C, P WHERE C is Card, P? documented_by C
+
+Notice you may also use outer join:
+
+- on the RHS of attribute relation, e.g.
+
+  .. sourcecode:: sql
+
+       Any X WHERE X ref XR, Y name XR?
+
+  so that Y is outer joined on X by ref/name attributes comparison
+
+
+- on any side of an ``HAVING`` expression, e.g.
+
+  .. sourcecode:: sql
+
+       Any X WHERE X creation_date XC, Y creation_date YC
+       HAVING YEAR(XC)=YEAR(YC)?
+
+  so that Y is outer joined on X by comparison of the year extracted from their
+  creation date.
+
+  .. sourcecode:: sql
+
+       Any X WHERE X creation_date XC, Y creation_date YC
+       HAVING YEAR(XC)?=YEAR(YC)
+
+  would outer join X on Y instead.
+
+
+Having restrictions
+```````````````````
+
+The ``HAVING`` clause, as in SQL, may be used to restrict a query
+according to value returned by an aggregate function, e.g.
+
+.. sourcecode:: sql
+
+    Any X GROUPBY X WHERE X relation Y HAVING COUNT(Y) > 10
+
+It may however be used for something else: In the ``WHERE`` clause, we are
+limited to triplet expressions, so some things may not be expressed there. Let's
+take an example : if you want to get people whose upper-cased first name equals to
+another person upper-cased first name. There is no proper way to express this
+using triplet, so you should use something like:
+
+.. sourcecode:: sql
+
+    Any X WHERE X firstname XFN, Y firstname YFN, NOT X identity Y HAVING UPPER(XFN) = UPPER(YFN)
+
+Another example: imagine you want person born in 2000:
+
+.. sourcecode:: sql
+
+    Any X WHERE X birthday XB HAVING YEAR(XB) = 2000
+
+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.
+
+
+Sub-queries
+```````````
+
+The ``WITH`` keyword introduce sub-queries clause. Each sub-query has the
+form:
+
+  V1(,V2) BEING (rql query)
+
+Variables at the left of the ``BEING`` keyword defines into which
+variables results from the sub-query will be mapped to into the outer query.
+Sub-queries are separated from each other using a comma.
+
+Let's say we want to retrieve for each project its number of versions and its
+number of tickets. Due to the nature of relational algebra behind the scene, this
+can't be achieved using a single query. You have to write something along the
+line of:
+
+.. sourcecode:: sql
+
+  Any X, VC, TC WHERE X identity XX
+  WITH X, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
+       XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
+
+Notice that we can't reuse a same variable name as alias for two different
+sub-queries, hence the usage of 'X' and 'XX' in this example, which are then
+unified using the special `identity` relation (see :ref:`VirtualRelations`).
+
+.. warning::
+
+  Sub-queries define a new variable scope, so even if a variable has the same name
+  in the outer query and in the sub-query, they technically **aren't** the same
+  variable. So:
+
+  .. sourcecode:: sql
+
+     Any W, REF WITH W, REF BEING
+         (Any W, REF WHERE W is Workcase, W ref REF,
+                           W concerned_by D, D name "Logilab")
+
+  could be written:
+
+  .. sourcecode:: sql
+
+     Any W, REF WITH W, REF BEING
+        (Any W1, REF1 WHERE W1 is Workcase, W1 ref REF1,
+                            W1 concerned_by D, D name "Logilab")
+
+  Also, when a variable is coming from a sub-query, you currently can't reference
+  its attribute or inlined relations in the outer query, you've to fetch them in
+  the sub-query. For instance, let's say we want to sort by project name in our
+  first example, we would have to write:
+
+  .. sourcecode:: sql
+
+
+    Any X, VC, TC ORDERBY XN WHERE X identity XX
+    WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X,XN WHERE V version_of X, X name XN),
+         XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
+
+  instead of:
+
+  .. sourcecode:: sql
+
+    Any X, VC, TC ORDERBY XN WHERE X identity XX, X name XN,
+    WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
+         XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
+
+  which would result in a SQL execution error.
+
+
+Union
+`````
+
+You may get a result set containing the concatenation of several queries using
+the ``UNION``. The selection of each query should have the same number of
+columns.
+
+.. sourcecode:: sql
+
+    (Any X, XN WHERE X is Person, X surname XN) UNION (Any X,XN WHERE X is Company, X name XN)
+
+
+.. _RQLFunctions:
+
+Available functions
+~~~~~~~~~~~~~~~~~~~
+
+Below is the list of aggregate and transformation functions that are supported
+nativly by the framework. Notice that cubes may define additional functions.
+
+.. _RQLAggregateFunctions:
+
+Aggregate functions
+```````````````````
++------------------------+----------------------------------------------------------+
+| ``COUNT(Any)``         | return the number of rows                                |
++------------------------+----------------------------------------------------------+
+| ``MIN(Any)``           | return the minimum value                                 |
++------------------------+----------------------------------------------------------+
+| ``MAX(Any)``           | return the maximum value                                 |
++------------------------+----------------------------------------------------------+
+| ``AVG(Any)``           | return the average value                                 |
++------------------------+----------------------------------------------------------+
+| ``SUM(Any)``           | return the sum of values                                 |
++------------------------+----------------------------------------------------------+
+| ``COMMA_JOIN(String)`` | return each value separated by a comma (for string only) |
++------------------------+----------------------------------------------------------+
+
+All aggregate functions above take a single argument. Take care some aggregate
+functions (e.g. ``MAX``, ``MIN``) may return `None` if there is no
+result row.
+
+.. _RQLStringFunctions:
+
+String transformation functions
+```````````````````````````````
+
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``UPPER(String)``                             | upper case the string                                           |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``LOWER(String)``                             | lower case the string                                           |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``LENGTH(String)``                            | return the length of the string                                 |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``SUBSTRING(String, start, length)``          | extract from the string a string starting at given index and of |
+|                                               | given length                                                    |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``LIMIT_SIZE(String, max size)``              | if the length of the string is greater than given max size,     |
+|                                               | strip it and add ellipsis ("..."). The resulting string will    |
+|                                               | hence have max size + 3 characters                              |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``TEXT_LIMIT_SIZE(String, format, max size)`` | similar to the above, but allow to specify the MIME type of the |
+|                                               | text contained by the string. Supported formats are text/html,  |
+|                                               | text/xhtml and text/xml. All others will be considered as plain |
+|                                               | text. For non plain text format, sgml tags will be first removed|
+|                                               | before limiting the string.                                     |
++-----------------------------------------------+-----------------------------------------------------------------+
+
+.. _RQLDateFunctions:
+
+Date extraction functions
+`````````````````````````
+
++----------------------+----------------------------------------+
+| ``YEAR(Date)``       | return the year of a date or datetime  |
++----------------------+----------------------------------------+
+| ``MONTH(Date)``      | return the month of a date or datetime |
++----------------------+----------------------------------------+
+| ``DAY(Date)``        | return the day of a date or datetime   |
++----------------------+----------------------------------------+
+| ``HOUR(Datetime)``   | return the hours of a datetime         |
++----------------------+----------------------------------------+
+| ``MINUTE(Datetime)`` | return the minutes of a datetime       |
++----------------------+----------------------------------------+
+| ``SECOND(Datetime)`` | return the seconds of a datetime       |
++----------------------+----------------------------------------+
+| ``WEEKDAY(Date)``    | return the day of week of a date or    |
+|                      | datetime.  Sunday == 1, Saturday == 7. |
++----------------------+----------------------------------------+
+
+.. _RQLOtherFunctions:
+
+Other functions
+```````````````
++-------------------+--------------------------------------------------------------------+
+| ``ABS(num)``      |  return the absolute value of a number                             |
++-------------------+--------------------------------------------------------------------+
+| ``RANDOM()``      | return a pseudo-random value from 0.0 to 1.0                       |
++-------------------+--------------------------------------------------------------------+
+| ``FSPATH(X)``     | expect X to be an attribute whose value is stored in a             |
+|                   | :class:`BFSStorage` and return its path on the file system         |
++-------------------+--------------------------------------------------------------------+
+| ``FTIRANK(X)``    | expect X to be an entity used in a has_text relation, and return a |
+|                   | number corresponding to the rank order of each resulting entity    |
++-------------------+--------------------------------------------------------------------+
+| ``CAST(Type, X)`` | expect X to be an attribute and return it casted into the given    |
+|                   | final type                                                         |
++-------------------+--------------------------------------------------------------------+
+
+
+.. _RQLExamples:
+
+Examples
+~~~~~~~~
+
+- *Search for the object of identifier 53*
+
+  .. sourcecode:: sql
+
+        Any X WHERE X eid 53
+
+- *Search material such as comics, owned by syt and available*
+
+  .. sourcecode:: sql
+
+        Any X WHERE X is Document,
+                    X occurence_of F, F class C, C name 'Comics',
+                    X owned_by U, U login 'syt',
+                    X available TRUE
+
+- *Looking for people working for eurocopter interested in training*
+
+  .. sourcecode:: sql
+
+        Any P WHERE P is Person, P work_for S, S name 'Eurocopter',
+                    P interested_by T, T name 'training'
+
+- *Search note less than 10 days old written by jphc or ocy*
+
+  .. sourcecode:: sql
+
+        Any N WHERE N is Note, N written_on D, D day> (today -10),
+                    N written_by P, P name 'jphc' or P name 'ocy'
+
+- *Looking for people interested in training or living in Paris*
+
+  .. sourcecode:: sql
+
+        Any P WHERE P is Person, EXISTS(P interested_by T, T name 'training') OR
+                    (P city 'Paris')
+
+- *The surname and firstname of all people*
+
+  .. sourcecode:: sql
+
+        Any N, P WHERE 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
+  to all the selected variables. We could write here
+
+  .. sourcecode:: sql
+
+        String N, P WHERE X is Person, X name N, X first_name P
+
+
+  Note: You can not specify several types with * ... where X is FirstType or X is SecondType*.
+  To specify several types explicitly, you have to do
+
+
+  .. sourcecode:: sql
+
+        Any X WHERE X is IN (FirstType, SecondType)
+
+
+.. _RQLInsertQuery:
+
+Insertion query
+~~~~~~~~~~~~~~~
+
+    `INSERT` <entity type> V1 (, <entity type> V2) \ * `:` <assignments>
+    [ `WHERE` <restriction>]
+
+:assignments:
+   list of relations to assign in the form `V1 relationship V2 | <static value>`
+
+The restriction can define variables used in assignments.
+
+Caution, if a restriction is specified, the insertion is done for
+*each line result returned by the restriction*.
+
+- *Insert a new person named 'foo'*
+
+  .. sourcecode:: sql
+
+        INSERT Person X: X name 'foo'
+
+- *Insert a new person named 'foo', another called 'nice' and a 'friend' relation
+  between them*
+
+  .. sourcecode:: sql
+
+        INSERT Person X, Person Y: X name 'foo', Y name 'nice', X friend Y
+
+- *Insert a new person named 'foo' and a 'friend' relation with an existing
+  person called 'nice'*
+
+  .. sourcecode:: sql
+
+        INSERT Person X: X name 'foo', X friend  Y WHERE Y name 'nice'
+
+.. _RQLSetQuery:
+
+Update and relation creation queries
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    `SET` <assignements>
+    [ `WHERE` <restriction>]
+
+Caution, if a restriction is specified, the update is done *for
+each result line returned by the restriction*.
+
+- *Renaming of the person named 'foo' to 'bar' with the first name changed*
+
+  .. sourcecode:: sql
+
+        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'*
+
+  .. sourcecode:: sql
+
+        SET X know Y  WHERE X friend Y
+
+
+.. _RQLDeleteQuery:
+
+Deletion query
+~~~~~~~~~~~~~~
+
+    `DELETE` (<entity type> V) | (V1 relation v2 ),...
+    [ `WHERE` <restriction>]
+
+Caution, if a restriction is specified, the deletion is made *for
+each line result returned by the restriction*.
+
+- *Deletion of the person named 'foo'*
+
+  .. sourcecode:: sql
+
+        DELETE Person X WHERE X name 'foo'
+
+- *Removal of all relations of type 'friend' from the person named 'foo'*
+
+  .. sourcecode:: sql
+
+        DELETE X friend Y WHERE X is Person, X name 'foo'
+
+
+.. _Yapps: http://theory.stanford.edu/~amitp/yapps/
+.. _Left outer join: http://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/available-cubes.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,66 @@
+.. _AvailableCubes:
+
+Available cubes
+---------------
+
+An instance is made of several basic cubes. In the set of available
+basic cubes we can find for example:
+
+Base entity types
+~~~~~~~~~~~~~~~~~
+* addressbook_: PhoneNumber and PostalAddress
+* card_: Card, generic documenting card
+* event_: Event (define events, display them in calendars)
+* file_: File (to allow users to upload and store binary or text files)
+* link_: Link (to collect links to web resources)
+* mailinglist_: MailingList (to reference a mailing-list and the URLs
+  for its archives and its admin interface)
+* person_: Person (easily mixed with addressbook)
+* task_: Task (something to be done between start and stop date)
+* zone_: Zone (to define places within larger places, for example a
+  city in a state in a country)
+
+
+Classification
+~~~~~~~~~~~~~~
+* folder_: Folder (to organize things by grouping them in folders)
+* keyword_: Keyword (to define classification schemes)
+* tag_: Tag (to tag anything)
+
+Other features
+~~~~~~~~~~~~~~
+* basket_: Basket (like a shopping cart)
+* blog_: a blogging system using Blog and BlogEntry entity types
+* comment_: system to attach comment threads to entities)
+* email_: archiving management for emails (`Email`, `Emailpart`,
+  `Emailthread`), trigger action in cubicweb through email
+
+
+
+
+
+.. _addressbook: http://www.cubicweb.org/project/cubicweb-addressbook
+.. _basket: http://www.cubicweb.org/project/cubicweb-basket
+.. _card: http://www.cubicweb.org/project/cubicweb-card
+.. _blog: http://www.cubicweb.org/project/cubicweb-blog
+.. _comment: http://www.cubicweb.org/project/cubicweb-comment
+.. _email: http://www.cubicweb.org/project/cubicweb-email
+.. _event: http://www.cubicweb.org/project/cubicweb-event
+.. _file: http://www.cubicweb.org/project/cubicweb-file
+.. _folder: http://www.cubicweb.org/project/cubicweb-folder
+.. _keyword: http://www.cubicweb.org/project/cubicweb-keyword
+.. _link: http://www.cubicweb.org/project/cubicweb-link
+.. _mailinglist: http://www.cubicweb.org/project/cubicweb-mailinglist
+.. _person: http://www.cubicweb.org/project/cubicweb-person
+.. _tag: http://www.cubicweb.org/project/cubicweb-tag
+.. _task: http://www.cubicweb.org/project/cubicweb-task
+.. _zone: http://www.cubicweb.org/project/cubicweb-zone
+
+To declare the use of a cube, once installed, add the name of the cube
+and its dependency relation in the `__depends_cubes__` dictionary
+defined in the file `__pkginfo__.py` of your own component.
+
+.. note::
+  The listed cubes above are available as debian-packages on `CubicWeb's forge`_.
+
+.. _`CubicWeb's forge`: http://www.cubicweb.org/project?vtitle=All%20cubicweb%20projects
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/cc-newcube.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,57 @@
+Creating a new cube from scratch
+--------------------------------
+
+Let's start by creating the cube environment in which we will develop ::
+
+  cd ~/cubes
+  # use cubicweb-ctl to generate a template for the cube
+  # will ask some questions, most with nice default
+  cubicweb-ctl newcube mycube
+  # makes the cube source code managed by mercurial
+  cd mycube
+  hg init
+  hg add .
+  hg ci
+
+If all went well, you should see the cube you just created in the list
+returned by ``cubicweb-ctl list`` in the  *Available cubes* section.
+If not, please refer to :ref:`ConfigurationEnv`.
+
+To reuse an existing cube, add it to the list named
+``__depends_cubes__`` which is defined in :file:`__pkginfo__.py`.
+This variable is used for the instance packaging (dependencies handled
+by system utility tools such as APT) and to find used cubes when the
+database for the instance is created.
+
+On a Unix system, the available cubes are usually stored in the
+directory :file:`/usr/share/cubicweb/cubes`. If you are using the
+cubicweb mercurial repository (:ref:`SourceInstallation`), the cubes
+are searched in the directory
+:file:`/path/to/cubicweb_toplevel/cubes`. In this configuration
+cubicweb itself ought to be located at
+:file:`/path/to/cubicweb_toplevel/cubicweb`.
+
+.. note::
+
+    Please note that if you do not wish to use default directory for your cubes
+    library, you should set the :envvar:`CW_CUBES_PATH` environment variable to
+    add extra directories where cubes will be search, and you'll then have to use
+    the option `--directory` to specify where you would like to place the source
+    code of your cube:
+
+    ``cubicweb-ctl newcube --directory=/path/to/cubes/library mycube``
+
+
+.. XXX resurrect once live-server is back
+.. Usage of :command:`cubicweb-ctl liveserver`
+.. -------------------------------------------
+
+.. To quickly test a new cube, you can also use the `liveserver` command for cubicweb-ctl
+.. which allows to create an instance in memory (using an SQLite database by
+.. default) and make it accessible through a web server ::
+
+..   cubicweb-ctl live-server mycube
+
+.. or by using an existing database (SQLite or Postgres)::
+
+..   cubicweb-ctl live-server -s myfile_sources mycube
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,11 @@
+Cubes
+=====
+
+This chapter describes how to define your own cubes and reuse already available cubes.
+
+.. toctree::
+   :maxdepth: 1
+
+   layout.rst
+   cc-newcube.rst
+   available-cubes.rst
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/layout.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,153 @@
+
+.. _foundationsCube:
+
+.. _cubelayout:
+
+Standard structure for a cube
+-----------------------------
+
+A cube is structured as follows:
+
+::
+
+  mycube/
+  |
+  |-- data/
+  |   |-- cubes.mycube.css
+  |   |-- cubes.mycube.js
+  |   `-- external_resources
+  |
+  |-- debian/
+  |   |-- changelog
+  |   |-- compat
+  |   |-- control
+  |   |-- copyright
+  |   |-- cubicweb-mycube.prerm
+  |   `-- rules
+  |
+  |-- entities.py
+  |
+  |-- i18n/
+  |   |-- en.po
+  |   |-- es.po
+  |   `-- fr.po
+  |
+  |-- __init__.py
+  |
+  |-- MANIFEST.in
+  |
+  |-- migration/
+  |   |-- postcreate.py
+  |   `-- precreate.py
+  |
+  |-- __pkginfo__.py
+  |
+  |-- schema.py
+  |
+  |-- setup.py
+  |
+  |-- site_cubicweb.py
+  |
+  |-- hooks.py
+  |
+  |-- test/
+  |   |-- data/
+  |   |   `-- bootstrap_cubes
+  |   |-- pytestconf.py
+  |   |-- realdb_test_mycube.py
+  |   `-- test_mycube.py
+  |
+  `-- views.py
+
+
+We can use subpackages instead of python modules for ``views.py``, ``entities.py``,
+``schema.py`` or ``hooks.py``. For example, we could have:
+
+::
+
+  mycube/
+  |
+  |-- entities.py
+  |-- hooks.py
+  `-- views/
+      |-- __init__.py
+      |-- forms.py
+      |-- primary.py
+      `-- widgets.py
+
+
+where :
+
+* ``schema`` contains the schema definition (server side only)
+* ``entities`` contains the entity definitions (server side and web interface)
+* ``hooks`` contains hooks and/or views notifications (server side only)
+* ``views`` contains the web interface components (web interface only)
+* ``test`` contains tests related to the cube (not installed)
+* ``i18n`` contains message catalogs for supported languages (server side and
+  web interface)
+* ``data`` contains data files for static content (images, css,
+  javascript code)...(web interface only)
+* ``migration`` contains initialization files for new instances (``postcreate.py``)
+  and a file containing dependencies of the component depending on the version
+  (``depends.map``)
+* ``debian`` contains all the files managing debian packaging (you will find
+  the usual files ``control``, ``rules``, ``changelog``... not installed)
+* file ``__pkginfo__.py`` provides component meta-data, especially the distribution
+  and the current version (server side and web interface) or sub-cubes used by
+  the cube.
+
+
+At least you should have the file ``__pkginfo__.py``.
+
+
+The :file:`__init__.py` and :file:`site_cubicweb.py` files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX WRITEME
+
+The :file:`__pkginfo__.py` file
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX detail steps of instance creation
+
+
+External resources such as image, javascript and css files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX naming convention external_resources file
+
+
+Out-of the box testing
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
+
+
+Packaging and distribution
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/dataimport.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,85 @@
+.. -*- coding: utf-8 -*-
+
+.. _dataimport:
+
+Dataimport
+==========
+
+*CubicWeb* is designed to manipulate huge of amount of data, and provides utilities to do so.
+
+The main entry point is :mod:`cubicweb.dataimport.importer` which defines an
+:class:`ExtEntitiesImporter` class responsible for importing data from an external source in the
+form :class:`ExtEntity` objects. An :class:`ExtEntity` is a transitional representation of an
+entity to be imported in the CubicWeb instance; building this representation is usually
+domain-specific -- e.g. dependent of the kind of data source (RDF, CSV, etc.) -- and is thus the
+responsibility of the end-user.
+
+Along with the importer, a *store* must be selected, which is responsible for insertion of data into
+the database. There exists different kind of stores_, allowing to insert data within different
+levels of the *CubicWeb* API and with different speed/security tradeoffs. Those keeping all the
+*CubicWeb* hooks and security will be slower but the possible errors in insertion (bad data types,
+integrity error, ...) will be handled.
+
+
+Example
+-------
+
+Consider the following schema snippet.
+
+.. code-block:: python
+
+    class Person(EntityType):
+        name = String(required=True)
+
+    class knows(RelationDefinition):
+        subject = 'Person'
+        object = 'Person'
+
+along with some data in a ``people.csv`` file::
+
+    # uri,name,knows
+    http://www.example.org/alice,Alice,
+    http://www.example.org/bob,Bob,http://www.example.org/alice
+
+The following code (using a shell context) defines a function `extentities_from_csv` to read
+`Person` external entities coming from a CSV file and calls the :class:`ExtEntitiesImporter` to
+insert corresponding entities and relations into the CubicWeb instance.
+
+.. code-block:: python
+
+    from cubicweb.dataimport import ucsvreader, RQLObjectStore
+    from cubicweb.dataimport.importer import ExtEntity, ExtEntitiesImporter
+
+    def extentities_from_csv(fpath):
+        """Yield Person ExtEntities read from `fpath` CSV file."""
+        with open(fpath) as f:
+            for uri, name, knows in ucsvreader(f, skipfirst=True, skip_empty=False):
+                yield ExtEntity('Personne', uri,
+                                {'nom': set([name]), 'connait': set([knows])})
+
+    extenties = extentities_from_csv('people.csv')
+    store = RQLObjectStore(cnx)
+    importer = ExtEntitiesImporter(schema, store)
+    importer.import_entities(extenties)
+    commit()
+    rset = cnx.execute('String N WHERE X nom N, X connait Y, Y nom "Alice"')
+    assert rset[0][0] == u'Bob', rset
+
+Importer API
+------------
+
+.. automodule:: cubicweb.dataimport.importer
+
+
+Stores
+~~~~~~
+
+.. automodule:: cubicweb.dataimport.stores
+
+
+SQLGenObjectStore
+-----------------
+
+This store relies on *COPY FROM*/execute many sql commands to directly push data using SQL commands
+rather than using the whole *CubicWeb* API. For now, **it only works with PostgresSQL** as it requires
+the *COPY FROM* command.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/baseschema.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,42 @@
+.. _pre_defined_entity_types:
+
+Pre-defined entities in the library
+-----------------------------------
+
+The library defines a set of entity schemas that are required by the system
+or commonly used in *CubicWeb* instances.
+
+
+Entity types used to store the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* _`CWEType`, entity type
+* _`CWRType`, relation type
+* _`CWRelation`, relation definition
+* _`CWAttribute`, attribute relation definition
+* _`CWConstraint`,  `CWConstraintType`, `RQLExpression`
+
+Entity types used to manage users and permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* _`CWUser`, system users
+* _`CWGroup`, users groups
+
+Entity types used to manage workflows
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* :ref:`Workflow <Workflow>`, workflow entity, linked to some entity types which may use this workflow
+* _`State`, workflow state
+* _`Transition`, workflow transition
+* _`TrInfo`, record of a transition trafic for an entity
+
+Other entity types
+~~~~~~~~~~~~~~~~~~
+* _`CWCache`, cache entities used to improve performances
+* _`CWProperty`, used to configure the instance
+
+* _`EmailAddress`, email address, used by the system to send notifications
+  to the users and also used by others optionnals schemas
+
+* _`Bookmark`, an entity type used to allow a user to customize his links within
+  the instance
+
+* _`ExternalUri`, used for semantic web site to indicate that an entity is the
+  same as another from an external site
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/define-workflows.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,160 @@
+.. -*- coding: utf-8 -*-
+
+.. _Workflow:
+
+Defining a Workflow
+===================
+
+General
+-------
+
+A workflow describes how certain entities have to evolve between different
+states. Hence we have a set of states, and a "transition graph", i.e. a set of
+possible transitions from one state to another state.
+
+We will define a simple workflow for a blog, with only the following two states:
+`submitted` and `published`. You may want to take a look at :ref:`TutosBase` if
+you want to quickly setup an instance running a blog.
+
+Setting up a workflow
+---------------------
+
+We want to create a workflow to control the quality of the BlogEntry
+submitted on the instance. When a BlogEntry is created by a user
+its state should be `submitted`. To be visible to all, it has to
+be in the state `published`. To move it from `submitted` to `published`,
+we need a transition that we can call `approve_blogentry`.
+
+A BlogEntry state should not be modifiable by every user.
+So we have to define a group of users, `moderators`, and
+this group will have appropriate permissions to publish a BlogEntry.
+
+There are two ways to create a workflow: from the user interface, or
+by defining it in ``migration/postcreate.py``. This script is executed
+each time a new ``cubicweb-ctl db-init`` is done.  We strongly
+recommend to create the workflow in ``migration/postcreate.py`` and we
+will now show you how. Read `Two bits of warning`_ to understand why.
+
+The state of an entity is managed by the `in_state` attribute which
+can be added to your entity schema by inheriting from
+`cubicweb.schema.WorkflowableEntityType`.
+
+
+About our example of BlogEntry, we must have:
+
+.. sourcecode:: python
+
+  from cubicweb.schema import WorkflowableEntityType
+
+  class BlogEntry(WorkflowableEntityType):
+      ...
+
+
+Creating states, transitions and group permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :mod:`postcreate` script is executed in a special environment,
+adding several |cubicweb| primitives that can be used.
+
+They are all defined in the :class:`ServerMigrationHelper` class.
+We will only discuss the methods we use to create a workflow in this example.
+
+A workflow is a collection of entities of type ``State`` and of type
+``Transition`` which are standard *CubicWeb* entity types.
+
+To define a workflow for BlogDemo, please add the following lines
+to ``migration/postcreate.py``:
+
+.. sourcecode:: python
+
+  _ = unicode
+
+  moderators = add_entity('CWGroup', name=u"moderators")
+
+This adds the `moderators` user group.
+
+.. sourcecode:: python
+
+  wf = add_workflow(u'blog publication workflow', 'BlogEntry')
+
+At first, instanciate a new workflow object with a gentle description
+and the concerned entity types (this one can be a tuple for multiple
+value).
+
+.. sourcecode:: python
+
+  submitted = wf.add_state(_('submitted'), initial=True)
+  published = wf.add_state(_('published'))
+
+This will create two entities of type ``State``, one with name
+'submitted', and the other with name 'published'.
+
+``add_state`` expects as first argument the name of the state you want
+to create and an optional argument to say if it is supposed to be the
+initial state of the entity type.
+
+.. sourcecode:: python
+
+  wf.add_transition(_('approve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
+
+This will create an entity of type ``Transition`` with name
+`approve_blogentry` which will be linked to the ``State`` entities
+created before.
+
+``add_transition`` expects
+
+  * as the first argument: the name of the transition
+  * then the list of states on which the transition can be triggered,
+  * the target state of the transition,
+  * and the permissions
+    (e.g. a list of user groups who can apply the transition; the user
+    has to belong to at least one of the listed group to perform the action).
+
+.. sourcecode:: python
+
+  checkpoint()
+
+.. note::
+  Do not forget to add the `_()` in front of all states and
+  transitions names while creating a workflow so that they will be
+  identified by the i18n catalog scripts.
+
+In addition to the user groups (one of which the user needs to belong
+to), we could have added a RQL condition.  In this case, the user can
+only perform the action if the two conditions are satisfied.
+
+If we use an RQL condition on a transition, we can use the following variables:
+
+* `X`, the entity on which we may pass the transition
+* `U`, the user executing that may pass the transition
+
+
+.. image:: ../../../images/03-transitions-view_en.png
+
+You can notice that in the action box of a BlogEntry, the state is now
+listed as well as the possible transitions for the current state
+defined by the workflow.
+
+The transitions will only be displayed for users having the right permissions.
+In our example, the transition `approve_blogentry` will only be displayed
+for the users belonging to the group `moderators` or `managers`.
+
+
+Two bits of warning
+~~~~~~~~~~~~~~~~~~~
+
+We could perfectly use the administration interface to do these
+operations. It is a convenient thing to do at times (when doing
+development, to quick-check things). But it is not recommended beyond
+that because it is a bit complicated to do it right and it will be
+only local to your instance (or, said a bit differently, such a
+workflow only exists in an instance database). Furthermore, you cannot
+write unit tests against deployed instances, and experience shows it
+is mandatory to have tests for any mildly complicated workflow
+setup.
+
+Indeed, if you create the states and transitions through the user
+interface, next time you initialize the database you will have to
+re-create all the workflow entities. The user interface should only be
+a reference for you to view the states and transitions, but is not the
+appropriate interface to define your application workflow.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/definition.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,912 @@
+.. -*- coding: utf-8 -*-
+
+.. _datamodel_definition:
+
+Yams *schema*
+-------------
+
+The **schema** is the core piece of a *CubicWeb* instance as it
+defines and handles the data model. It is based on entity types that
+are either already defined in `Yams`_ and the *CubicWeb* standard
+library; or more specific types defined in cubes. The schema for a
+cube is defined in a `schema` python module or package.
+
+.. _`Yams`: http://www.logilab.org/project/yams
+
+.. _datamodel_overview:
+
+Overview
+~~~~~~~~
+
+The core idea of the yams schema is not far from the classical
+`Entity-relationship`_ model. But while an E/R model (or `logical
+model`) traditionally has to be manually translated to a lower-level
+data description language (such as the SQL `create table`
+sublanguage), also often described as the `physical model`, no such
+step is required with |yams| and |cubicweb|.
+
+.. _`Entity-relationship`: http://en.wikipedia.org/wiki/Entity-relationship_model
+
+This is because in addition to high-level, logical |yams| models, one
+uses the |rql| data manipulation language to query, insert, update and
+delete data. |rql| abstracts as much of the underlying SQL database as
+a |yams| schema abstracts from the physical layout. The vagaries of
+SQL are avoided.
+
+As a bonus point, such abstraction make it quite comfortable to build
+or use different backends to which |rql| queries apply.
+
+So, as in the E/R formalism, the building blocks are ``entities``
+(:ref:`EntityType`), ``relationships`` (:ref:`RelationType`,
+:ref:`RelationDefinition`) and ``attributes`` (handled like relation
+with |yams|).
+
+Let us detail a little the divergences between E/R and |yams|:
+
+* all relationship are binary which means that to represent a
+  non-binary relationship, one has to use an entity,
+* relationships do not support attributes (yet, see:
+  http://www.cubicweb.org/ticket/341318), hence the need to reify it
+  as an entity if need arises,
+* all entities have an `eid` attribute (an integer) that is its
+  primary key (but it is possible to declare uniqueness on other
+  attributes)
+
+Also |yams| supports the notions of:
+
+* entity inheritance (quite experimental yet, and completely
+  undocumented),
+* relation type: that is, relationships can be established over a set
+  of couple of entity types (henre the distinction made between
+  `RelationType` and `RelationDefinition` below)
+
+Finally |yams| has a few concepts of its own:
+
+* relationships being oriented and binary, we call the left hand
+  entity type the `subject` and the right hand entity type the
+  `object`
+
+.. note::
+
+   The |yams| schema is available at run time through the .schema
+   attribute of the `vregistry`.  It's an instance of
+   :class:`cubicweb.schema.Schema`, which extends
+   :class:`yams.schema.Schema`.
+
+.. _EntityType:
+
+Entity type
+~~~~~~~~~~~
+
+An entity type is an instance of :class:`yams.schema.EntitySchema`. Each entity type has
+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``, ``BigInt``, ``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:
+
+.. autofunction:: yams.buildobjs.RichString
+
+The ``__unique_together__`` class attribute is a list of tuples of names of
+attributes or inlined relations.  For each tuple, CubicWeb ensures the unicity
+of the combination.  For example:
+
+.. sourcecode:: python
+
+  class State(EntityType):
+      __unique_together__ = [('name', 'state_of')]
+
+      name = String(required=True)
+      state_of = SubjectRelation('Workflow', cardinality='1*',
+                                 composite='object', inlined=True)
+
+
+You can find more base entity types in
+:ref:`pre_defined_entity_types`.
+
+.. XXX yams inheritance
+
+.. _RelationType:
+
+Relation type
+~~~~~~~~~~~~~
+
+A relation type is an instance of
+:class:`yams.schema.RelationSchema`. A relation type is simply a
+semantic definition of a kind of relationship that may occur in an
+application.
+
+It may be referenced by zero, one or more relation definitions.
+
+It is important to choose a good name, at least to avoid conflicts
+with some semantically different relation defined in other cubes
+(since there's only a shared name space for these names).
+
+A relation type holds the following properties (which are hence shared
+between all relation definitions of that type):
+
+* `inlined`: boolean handling the physical optimization for archiving
+  the relation in the subject entity table, instead of creating a specific
+  table for the relation. This applies to relations where cardinality
+  of subject->relation->object is 0..1 (`?`) or 1..1 (`1`) for *all* its relation
+  definitions.
+
+* `symmetric`: boolean indicating that the relation is symmetrical, which
+  means that `X relation Y` implies `Y relation X`.
+
+.. _RelationDefinition:
+
+Relation definition
+~~~~~~~~~~~~~~~~~~~
+
+A relation definition is an instance of
+:class:`yams.schema.RelationDefinition`. It is a complete triplet
+"<subject entity type> <relation type> <object entity type>".
+
+When creating a new instance of that class, the corresponding
+:class:`RelationType` instance is created on the fly if necessary.
+
+Properties
+``````````
+
+The available properties for relation definitions are enumerated
+here. There are several kind of properties, as some relation
+definitions are actually attribute definitions, and other are not.
+
+Some properties may be completely optional, other may have a default
+value.
+
+Common properties for attributes and relations:
+
+* `description`: a unicode string describing an attribute or a
+  relation. By default this string will be used in the editing form of
+  the entity, which means that it is supposed to help the end-user and
+  should be flagged by the function `_` to be properly
+  internationalized.
+
+* `constraints`: a list of conditions/constraints that the relation has to
+  satisfy (c.f. `Constraints`_)
+
+* `cardinality`: a two character string specifying the cardinality of
+  the relation. The first character defines the cardinality of the
+  relation on the subject, and the second on the object. When a
+  relation can have multiple subjects or objects, the cardinality
+  applies to all, not on a one-to-one basis (so it must be
+  consistent...). Default value is '**'. The possible values are
+  inspired from regular expression syntax:
+
+    * `1`: 1..1
+    * `?`: 0..1
+    * `+`: 1..n
+    * `*`: 0..n
+
+Attributes properties:
+
+* `unique`: boolean indicating if the value of the attribute has to be
+  unique or not within all entities of the same type (false by
+  default)
+
+* `indexed`: boolean indicating if an index needs to be created for
+  this attribute in the database (false by default). This is useful
+  only if you know that you will have to run numerous searches on the
+  value of this attribute.
+
+* `default`: default value of the attribute. In case of date types, the values
+  which could be used correspond to the RQL keywords `TODAY` and `NOW`.
+
+* `metadata`: Is also accepted as an argument of the attribute contructor. It is
+  not really an attribute property. see `Metadata`_ for details.
+
+Properties for `String` attributes:
+
+* `fulltextindexed`: boolean indicating if the attribute is part of
+  the full text index (false by default) (*applicable on the type
+  `Byte` as well*)
+
+* `internationalizable`: boolean indicating if the value of the
+  attribute is internationalizable (false by default)
+
+Relation properties:
+
+* `composite`: string indicating that the subject (composite ==
+  'subject') is composed of the objects of the relations. For the
+  opposite case (when the object is composed of the subjects of the
+  relation), we just set 'object' as value. The composition implies
+  that when the relation is deleted (so when the composite is deleted,
+  at least), the composed are also deleted.
+
+* `fulltext_container`: string indicating if the value if the full
+  text indexation of the entity on one end of the relation should be
+  used to find the entity on the other end. The possible values are
+  'subject' or 'object'. For instance the use_email relation has that
+  property set to 'subject', since when performing a full text search
+  people want to find the entity using an email address, and not the
+  entity representing the email address.
+
+Constraints
+```````````
+
+By default, the available constraint types are:
+
+General Constraints
+......................
+
+* `SizeConstraint`: allows to specify a minimum and/or maximum size on
+  string (generic case of `maxsize`)
+
+* `BoundaryConstraint`: allows to specify a minimum and/or maximum value
+  on numeric types and date
+
+.. sourcecode:: python
+
+   from yams.constraints import BoundaryConstraint, TODAY, NOW, Attribute
+
+   class DatedEntity(EntityType):
+      start = Date(constraints=[BoundaryConstraint('>=', TODAY())])
+      end = Date(constraints=[BoundaryConstraint('>=', Attribute('start'))])
+
+   class Before(EntityType);
+      last_time = DateTime(constraints=[BoundaryConstraint('<=', NOW())])
+
+* `IntervalBoundConstraint`: allows to specify an interval with
+  included values
+
+.. sourcecode:: python
+
+     class Node(EntityType):
+         latitude = Float(constraints=[IntervalBoundConstraint(-90, +90)])
+
+* `UniqueConstraint`: identical to "unique=True"
+
+* `StaticVocabularyConstraint`: identical to "vocabulary=(...)"
+
+Constraints can be dependent on a fixed value (90, Date(2015,3,23)) or a variable.
+In this second case, yams can handle :
+
+* `Attribute`: compare to the value of another attribute.
+* `TODAY`: compare to the current Date.
+* `NOW`: compare to the current Datetime.
+
+RQL Based Constraints
+......................
+
+RQL based constraints may take three arguments. The first one is the ``WHERE``
+clause of a RQL query used by the constraint. The second argument ``mainvars``
+is the ``Any`` clause of the query. By default this include `S` reserved for the
+subject of the relation and `O` for the object. Additional variables could be
+specified using ``mainvars``. The argument expects a single string with all
+variable's name separated by spaces. The last one, ``msg``, is the error message
+displayed when the constraint fails. As RQLVocabularyConstraint never fails the
+third argument is not available.
+
+* `RQLConstraint`: allows to specify a RQL query that has to be satisfied
+  by the subject and/or the object of relation. In this query the variables
+  `S` and `O` are reserved for the relation subject and object entities.
+
+* `RQLVocabularyConstraint`: similar to the previous type of constraint except
+  that it does not express a "strong" constraint, which means it is only used to
+  restrict the values listed in the drop-down menu of editing form, but it does
+  not prevent another entity to be selected.
+
+* `RQLUniqueConstraint`: allows to the specify a RQL query that ensure that an
+  attribute is unique in a specific context. The Query must **never** return more
+  than a single result to be satisfied. In this query the variables `S` is
+  reserved for the relation subject entity. The other variables should be
+  specified with the second constructor argument (mainvars). This constraint type
+  should be used when __unique_together__ doesn't fit.
+
+.. XXX note about how to add new constraint
+
+.. _securitymodel:
+
+The security model
+~~~~~~~~~~~~~~~~~~
+
+The security model of `CubicWeb` is based on `Access Control List`.
+The main principles are:
+
+* users and groups of users
+* a user belongs to at least one group of user
+* permissions (`read`, `update`, `create`, `delete`)
+* permissions are assigned to groups (and not to users)
+
+For *CubicWeb* in particular:
+
+* we associate rights at the entities/relations schema level
+
+* the default groups are: `managers`, `users` and `guests`
+
+* 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
+
+Setting permissions is done with the class attribute `__permissions__`
+of entity types and relation definitions. The value of this attribute
+is a dictionary where the keys are the access types (action), and the
+values are the authorized groups or rql expressions.
+
+For an entity type, the possible actions are `read`, `add`, `update` and
+`delete`.
+
+For a relation, the possible actions are `read`, `add`, and `delete`.
+
+For an attribute, the possible actions are `read`, `add` and `update`,
+and they are a refinement of an entity type permission.
+
+.. note::
+
+   By default, the permissions of an entity type attributes are
+   equivalent to the permissions of the entity type itself.
+
+   It is possible to provide custom attribute permissions which are
+   stronger than, or are more lenient than the entity type
+   permissions.
+
+   In a situation where all attributes were given custom permissions,
+   the entity type permissions would not be checked, except for the
+   `delete` action.
+
+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.
+
+Default permissions
+```````````````````
+
+The default permissions for ``EntityType`` are:
+
+.. sourcecode:: python
+
+   __permissions__ = {
+        'read': ('managers', 'users', 'guests',),
+        'update': ('managers', 'owners',),
+        'delete': ('managers', 'owners'),
+        'add': ('managers', 'users',)
+        }
+
+The default permissions for relations are:
+
+.. sourcecode:: python
+
+   __permissions__ = {'read': ('managers', 'users', 'guests',),
+                    'delete': ('managers', 'users'),
+                    'add': ('managers', 'users',)}
+
+The default permissions for attributes are:
+
+.. sourcecode:: python
+
+   __permissions__ = {'read': ('managers', 'users', 'guests',),
+                      'add': ('managers', ERQLExpression('U has_add_permission X'),
+                      'update': ('managers', ERQLExpression('U has_update_permission X')),}
+
+.. note::
+
+   The default permissions for attributes are not syntactically
+   equivalent to the default permissions of the entity types, but the
+   rql expressions work by delegating to the entity type permissions.
+
+
+The standard user groups
+````````````````````````
+
+* `guests`
+
+* `users`
+
+* `managers`
+
+* `owners`: virtual group corresponding to the entity's owner.
+  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).
+
+
+Use of RQL expression for write permissions
+```````````````````````````````````````````
+
+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.
+
+To use RQL expression in entity type permission:
+
+* 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
+
+For RQL expressions on a relation type, the principles are the same except for
+the following:
+
+* you have to use the class :class:`~cubicweb.schema.RRQLExpression` instead of
+  :class:`~cubicweb.schema.ERQLExpression`
+
+* 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.
+
+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.
+
+
+.. sourcecode:: python
+
+  class my_relation(RelationDefinition):
+    __permissions__ = {'read': ('managers', 'users'),
+                       'add': ('managers', RRQLExpression('U has_update_permission S')),
+                       'delete': ('managers', RRQLExpression('U has_update_permission S'))
+		       }
+
+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, 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
+````````````````````````````````````````
+
+The principles are the same but with the following restrictions:
+
+* you can not use rql expression for the `read` permission of relations and
+  attributes,
+
+* 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
+
+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:
+
+
+Derived attributes and relations
+--------------------------------
+
+.. note:: **TODO** Check organisation of the whole chapter of the documentation
+
+Cubicweb offers the possibility to *query* data using so called
+*computed* relations and attributes. Those are *seen* by RQL requests
+as normal attributes and relations but are actually derived from other
+attributes and relations. In a first section we'll informally review
+two typical use cases. Then we see how to use computed attributes and
+relations in your schema. Last we will consider various significant
+aspects of their implementation and the impact on their usage.
+
+Motivating use cases
+~~~~~~~~~~~~~~~~~~~~
+
+Computed (or reified) relations
+```````````````````````````````
+
+It often arises that one must represent a ternary relation, or a
+family of relations. For example, in the context of an exhibition
+catalog you might want to link all *contributors* to the *work* they
+contributed to, but this contribution can be as *illustrator*,
+*author*, *performer*, ...
+
+The classical way to describe this kind of information within an
+entity-relationship schema is to *reify* the relation, that is turn
+the relation into a entity. In our example the schema will have a
+*Contribution* entity type used to represent the family of the
+contribution relations.
+
+
+.. sourcecode:: python
+
+    class ArtWork(EntityType):
+        name = String()
+        ...
+
+    class Person(EntityType):
+        name = String()
+        ...
+
+    class Contribution(EntityType):
+        contributor = SubjectRelation('Person', cardinality='1*', inlined=True)
+        manifestation = SubjectRelation('ArtWork')
+        role = SubjectRelation('Role')
+
+    class Role(EntityType):
+        name = String()
+
+But then, in order to query the illustrator(s) ``I`` of a work ``W``,
+one has to write::
+
+    Any I, W WHERE C is Contribution, C contributor I, C manifestation W,
+                   C role R, R name 'illustrator'
+
+whereas we would like to be able to simply write::
+
+    Any I, W WHERE I illustrator_of W
+
+This is precisely what the computed relations allow.
+
+
+Computed (or synthesized) attribute
+```````````````````````````````````
+
+Assuming a trivial schema for describing employees in companies, one
+can be interested in the total of salaries payed by a company for
+all its employees. One has to write::
+
+    Any C, SUM(SA) GROUPBY S WHERE E works_for C, E salary SA
+
+whereas it would be most convenient to simply write::
+
+    Any C, TS WHERE C total_salary TS
+
+And this is again what computed attributes provide.
+
+
+Using computed attributes and relations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Computed (or reified) relations
+```````````````````````````````
+
+In the above case we would define the *computed relation*
+``illustrator_of`` in the schema by:
+
+.. sourcecode:: python
+
+    class illustrator_of(ComputedRelation):
+        rule  = ('C is Contribution, C contributor S, C manifestation O,'
+                 'C role R, R name "illustrator"')
+
+You will note that:
+
+* the ``S`` and ``O`` RQL variables implicitly identify the subject and
+  object of the defined computed relation, akin to what happens in
+  RRQLExpression
+* the possible subject and object entity types are inferred from the rule;
+* computed relation definitions always have empty *add* and *delete* permissions
+* *read* permissions can be defined, permissions from the relations used in the
+  rewrite rule **are not considered** ;
+* nothing else may be defined on the `ComputedRelation` subclass beside
+  description, permissions and rule (e.g. no cardinality, composite, etc.,).
+  `BadSchemaDefinition` is raised on attempt to specify other attributes;
+* computed relations can not be used in 'SET' and 'DELETE' rql queries
+  (`BadQuery` exception raised).
+
+
+NB: The fact that the *add* and *delete* permissions are *empty* even
+for managers is expected to make the automatic UI not attempt to edit
+them.
+
+Computed (or synthesized) attributes
+````````````````````````````````````
+
+In the above case we would define the *computed attribute*
+``total_salary`` on the ``Company`` entity type in the schema by:
+
+.. sourcecode:: python
+
+    class Company(EntityType):
+        name = String()
+        total_salary = Int(formula='Any SUM(SA) GROUPBY E WHERE P works_for X, E salary SA')
+
+* the ``X`` RQL variable implicitly identifies the entity holding the
+  computed attribute, akin to what happens in ERQLExpression;
+* the type inferred from the formula is checked against the declared type, and
+  `BadSchemaDefinition` is raised if they don't match;
+* the computed attributes always have empty *update* permissions
+* `BadSchemaDefinition` is raised on attempt to set 'update' permissions;
+* 'read' permissions can be defined, permissions regarding the formula
+  **are not considered**;
+* other attribute's property (inlined, ...) can be defined as for normal attributes;
+* Similarly to computed relation, computed attribute can't be used in 'SET' and
+  'DELETE' rql queries (`BadQuery` exception raised).
+
+
+API and implementation
+~~~~~~~~~~~~~~~~~~~~~~
+
+Representation in the data backend
+``````````````````````````````````
+
+Computed relations have no direct representation at the SQL table
+level.  Instead, each time a query is issued the query is rewritten to
+replace the computed relation by its equivalent definition and the
+resulting rewritten query is performed in the usual way.
+
+On the contrary, computed attributes are represented as a column in the
+table for their host entity type, just like normal attributes. Their
+value is kept up-to-date with respect to their defintion by a system
+of hooks (also called triggers in most RDBMS) which recomputes them
+when the relations and attributes they depend on are modified.
+
+Yams API
+````````
+
+When accessing the schema through the *yams API* (not when defining a
+schema in a ``schema.py`` file) the computed attributes and relations
+are represented as follows:
+
+relations
+    The ``yams.RelationSchema`` class has a new ``rule`` attribute
+    holding the rule as a string. If this attribute is set all others
+    must not be set.
+attributes
+    A new property ``formula`` is added on class
+    ``yams.RelationDefinitionSchema`` alomng with a new keyword
+    argument ``formula`` on the initializer.
+
+Migration
+`````````
+
+The migrations are to be handled as summarized in the array below.
+
++------------+---------------------------------------------------+---------------------------------------+
+|            | Computed rtype                                    | Computed attribute                    |
++============+===================================================+=======================================+
+| add        | * add_relation_type                               | * add_attribute                       |
+|            | * add_relation_definition should trigger an error | * add_relation_definition             |
++------------+---------------------------------------------------+---------------------------------------+
+| modify     | * sync_schema_prop_perms:                         | * sync_schema_prop_perms:             |
+|            |   checks the rule is                              |                                       |
+| (rule or   |   synchronized with the database                  |   - empty the cache,                  |
+| formula)   |                                                   |   - check formula,                    |
+|            |                                                   |   - make sure all the values get      |
+|            |                                                   |     updated                           |
++------------+---------------------------------------------------+---------------------------------------+
+| del        | * drop_relation_type                              | * drop_attribute                      |
+|            | * drop_relation_definition should trigger an error| * drop_relation_definition            |
++------------+---------------------------------------------------+---------------------------------------+
+
+
+Defining your schema using yams
+-------------------------------
+
+Entity type definition
+~~~~~~~~~~~~~~~~~~~~~~
+
+An entity type is defined by a Python class which inherits from
+:class:`yams.buildobjs.EntityType`.  The class definition contains the
+description of attributes and relations for the defined entity type.
+The class name corresponds to the entity type name. It is expected to
+be defined in the module ``mycube.schema``.
+
+:Note on schema definition:
+
+ The code in ``mycube.schema`` is not meant to be executed. The class
+ EntityType mentioned above is different from the EntitySchema class
+ described in the previous chapter. EntityType is a helper class to
+ make Entity definition easier. Yams will process EntityType classes
+ and create EntitySchema instances from these class definitions. Similar
+ manipulation happen for relations.
+
+When defining a schema using python files, you may use the following shortcuts:
+
+- `required`: boolean indicating if the attribute is required, ed subject cardinality is '1'
+
+- `vocabulary`: specify static possible values of an attribute
+
+- `maxsize`: integer providing the maximum size of a string (no limit by default)
+
+For example:
+
+.. sourcecode:: python
+
+  class Person(EntityType):
+    """A person with the properties and the relations necessary for my
+    application"""
+
+    last_name = String(required=True, fulltextindexed=True)
+    first_name = String(required=True, fulltextindexed=True)
+    title = String(vocabulary=('Mr', 'Mrs', 'Miss'))
+    date_of_birth = Date()
+    works_for = SubjectRelation('Company', cardinality='?*')
+
+
+The entity described above defines three attributes of type String,
+last_name, first_name and title, an attribute of type Date for the date of
+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
+ usage is to use ``CamelCase`` names.
+
+ Attribute and relation names must start with a lowercase letter. The
+ common usage is to use ``underscore_separated_words``. Attribute and
+ relation names starting with a single underscore are permitted, to
+ denote a somewhat "protected" or "private" attribute.
+
+ In any case, identifiers starting with "CW" or "cw" are reserved for
+ internal use by the framework.
+
+ .. _Metadata:
+
+ Some attribute using the name of another attribute as prefix are considered
+ metadata.  For example, if an EntityType have both a ``data`` and
+ ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata
+ of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch
+ metadata related to an attribute. There are only three valid metadata names:
+ ``format``, ``encoding`` and ``name``.
+
+
+The name of the Python attribute corresponds to the name of the attribute
+or the relation in *CubicWeb* application.
+
+An attribute is defined in the schema as follows::
+
+    attr_name = AttrType(*properties, metadata={})
+
+where
+
+* `AttrType`: is one of the type listed in EntityType_,
+
+* `properties`: is a list of the attribute needs to satisfy (see `Properties`_
+  for more details),
+
+* `metadata`: is a dictionary of meta attributes related to ``attr_name``.
+  Dictionary keys are the name of the meta attribute. Dictionary values
+  attributes objects (like the content of ``AttrType``). For each entry of the
+  metadata dictionary a ``<attr_name>_<key> = <value>`` attribute is
+  automaticaly added to the EntityType.  see `Metadata`_ section for details
+  about valid key.
+
+
+ ---
+
+While building your schema
+
+* it is possible to use the attribute `meta` to flag an entity type as a `meta`
+  (e.g. used to describe/categorize other entities)
+
+.. XXX the paragraph below needs clarification and / or moving out in
+.. another place
+
+*Note*: if you end up with an `if` in the definition of your entity, this probably
+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
+
+
+Definition of relations
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. 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
+a description of the properties of this type of relation, and could as well
+contain a string for the subject and a string for the object. This allows to create
+new definition of associated relations, (so that the class can have the
+definition properties from the relation) for example ::
+
+  class locked_by(RelationType):
+    """relation on all entities indicating that they are locked"""
+    inlined = True
+    cardinality = '?*'
+    subject = '*'
+    object = 'CWUser'
+
+If provided, the `subject` and `object` attributes denote the subject
+and object of the various relation definitions related to the relation
+type. Allowed values for these attributes are:
+
+* a string corresponding to an entity type
+* a tuple of string corresponding to multiple entity types
+* 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`
+attribute in the EntityType class. The first argument of `SubjectRelation` gives
+the entity type for the object of the relation.
+
+:Naming convention:
+
+ Although this way of defining relations uses a Python class, the
+ naming convention defined earlier prevails over the PEP8 conventions
+ used in the framework: relation type class names use
+ ``underscore_separated_words``.
+
+:Historical note:
+
+   It has been historically possible to use `ObjectRelation` which
+   defines a relation in the opposite direction. This feature is
+   deprecated and therefore should not be used in newly written code.
+
+:Future deprecation note:
+
+  In an even more remote future, it is quite possible that the
+  SubjectRelation shortcut will become deprecated, in favor of the
+  RelationType declaration which offers some advantages in the context
+  of reusable cubes.
+
+
+
+
+Handling schema changes
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Also, it should be clear that to properly handle data migration, an
+instance's schema is stored in the database, so the python schema file
+used to defined it is only read when the instance is created or
+upgraded.
+
+.. XXX complete me
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,12 @@
+Data model
+==========
+
+This chapter describes how you define a schema and how to make it evolves as the time goes.
+
+.. toctree::
+   :maxdepth: 1
+
+   definition
+   metadata
+   baseschema
+   define-workflows
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/metadata.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,37 @@
+
+Metadata
+--------
+
+.. index::
+   schema: meta-data;
+   schema: eid; creation_date; modification_data; cwuri
+   schema: created_by; owned_by; is; is_instance;
+
+Each entity type in |cubicweb| has at least the following meta-data attributes and relations:
+
+`eid`
+  entity's identifier which is unique in an instance. We usually call this identifier `eid` for historical reason.
+
+`creation_date`
+  Date and time of the creation of the entity.
+
+`modification_date`
+  Date and time of the latest modification of an entity.
+
+`cwuri`
+  Reference URL of the entity, which is not expected to change.
+
+`created_by`
+  Relation to the :ref:`users <CWUser>` who has created the entity
+
+`owned_by`
+  Relation to :ref:`users <CWUser>` whom the entity belongs; usually the creator but not
+  necessary, and it could have multiple owners notably for permission control
+
+`is`
+  Relation to the :ref:`entity type <CWEType>` of which type the entity is.
+
+`is_instance`
+  Relation to the :ref:`entity types <CWEType>` of which type the
+  entity is an instance of.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/devcore/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,8 @@
+Core APIs
+=========
+
+.. toctree::
+   :maxdepth: 1
+
+   reqbase.rst
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/devcore/reqbase.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,40 @@
+Request and ResultSet methods
+-----------------------------
+
+Those are methods you'll find on both request objects and on
+repository session.
+
+Request methods
+~~~~~~~~~~~~~~~
+
+`URL handling`:
+
+* `build_url(*args, **kwargs)`, returns an absolute URL based on the
+  given arguments. The *controller* supposed to handle the response,
+  can be specified through the first positional parameter (the
+  connection is theoretically done automatically :).
+
+`Data formatting`:
+
+* `format_date(date, date_format=None, time=False)` returns a string for a
+  date time according to instance's configuration
+
+* `format_time(time)` returns a string for a date time according to
+  instance's configuration
+
+`And more...`:
+
+* `tal_render(template, variables)`, renders a precompiled page template with
+  variables in the given dictionary as context
+
+
+Result set methods
+~~~~~~~~~~~~~~~~~~
+
+* `get_entity(row, col)`, returns the entity corresponding to the data position
+  in the *result set*
+
+* `complete_entity(row, col, skip_bytes=True)`, is equivalent to `get_entity` but
+  also call the method `complete()` on the entity before returning it
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/adapters.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,167 @@
+.. _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.
+
+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.predicates.implements` selector. You should instead use
+   :class:`cubicweb.predicates.is_instance` when you want to select on an entity
+   type, or :class:`cubicweb.predicates.adaptable` when you want to select on a
+   service.
+
+
+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.predicates 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.predicates 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.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/application-logic.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,177 @@
+How to use entities objects and adapters
+----------------------------------------
+
+The previous chapters detailed the classes and methods available to
+the developer at the so-called `ORM`_ level. However they say little
+about the common patterns of usage of these objects.
+
+.. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
+
+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
+same role as database triggers, but in a way that is independent of
+the actual data sources).
+
+So a lot of an application's business rules will be written in Hooks
+(or Operations).
+
+On the web side, views also typically operate using entity
+objects. Obvious entity methods for use in views are the Dublin Core
+methods like ``dc_title``. For separation of concerns reasons, one
+should ensure no ui logic pervades the entities level, and also no
+business logic should creep into the views.
+
+In the duration of a transaction, entities objects can be instantiated
+many times, in views and hooks, even for the same database entity. For
+instance, in a classic CubicWeb deployment setup, the repository and
+the web front-end are separated process communicating over the
+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``,
+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.cw_set(x=42)`` notation or a plain
+RQL ``SET`` expression.
+
+In views, it would be preferable to encapsulate the necessary logic in
+a method of an adapter for the concerned entity class(es). But of
+course, this advice is also reasonable 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 objects adapters: it's where an
+important part of the application logic lies (the other part being
+located in the Hook/Operations).
+
+Anatomy of an entity class
+--------------------------
+
+We can look now at a real life example coming from the `tracker`_
+cube. Let us begin to study the ``entities/project.py`` content.
+
+.. sourcecode:: python
+
+    from cubicweb.entities.adapters import ITreeAdapter
+
+    class ProjectAdapter(ITreeAdapter):
+        __select__ = is_instance('Project')
+        tree_relation = 'subproject_of'
+
+    class Project(AnyEntity):
+        __regid__ = 'Project'
+        fetch_attrs, cw_fetch_order = fetch_config(('name', 'description',
+                                                    'description_format', 'summary'))
+
+        TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
+
+        def dc_title(self):
+            return self.name
+
+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
+tree-like relations, but also because it is perfectly fine and useful
+to use such an interface in Hooks.
+
+In fact, Tree nature is a property of the data model that cannot be
+fully and portably expressed at the level of database entities (think
+about the transitive closure of the child relation). This is a further
+argument to implement it at entity class level.
+
+``fetch_attrs`` configures which attributes should be pre-fetched when using ORM
+methods retrieving entity of this type. In a same manner, the ``cw_fetch_order`` is
+a class method allowing to control sort order. More on this in :ref:`FetchAttrs`.
+
+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
+consumed 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
+responsible of ensuring XHTML compliance), or a console or file
+oriented output (which would have the necessary context about the
+needed byte stream encoding).
+
+.. note::
+
+  The Dublin Core `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, continuing the
+Project class.
+
+.. sourcecode:: python
+
+    def latest_version(self, states=('published',), reverse=None):
+        """returns the latest version(s) for the project in one of the given
+        states.
+
+        when no states specified, returns the latest published version.
+        """
+        order = 'DESC'
+        if reverse is not None:
+            warn('reverse argument is deprecated',
+                 DeprecationWarning, stacklevel=1)
+            if reverse:
+                order = 'ASC'
+        rset = self.versions_in_state(states, order, True)
+        if rset:
+            return rset.get_entity(0, 0)
+        return None
+
+    def versions_in_state(self, states, order='ASC', limit=False):
+        """returns version(s) for the project in one of the given states, sorted
+        by version number.
+
+        If limit is true, limit result to one version.
+        If reverse, versions are returned from the smallest to the greatest.
+        """
+        if limit:
+            order += ' LIMIT 1'
+        rql = 'Any V,N ORDERBY version_sort_value(N) %s ' \
+              'WHERE V num N, V in_state S, S name IN (%s), ' \
+              'V version_of P, P eid %%(p)s' % (order, ','.join(repr(s) for s in states))
+        return self._cw.execute(rql, {'p': self.eid})
+
+.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker/
+
+These few lines exhibit the important properties we want to outline:
+
+* entity code is concerned with the application domain
+
+* it is NOT concerned with database consistency (this is the realm of
+  Hooks/Operations); in other words, it assumes a consistent world
+
+* it is NOT (directly) concerned with end-user interfaces
+
+* however it can be used in both contexts
+
+* it does not create or manipulate the internal object's state
+
+* it plays freely with RQL expression as needed
+
+* it is not concerned with internationalization
+
+* it does not raise exceptions
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/data-as-objects.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,142 @@
+Access to persistent data
+--------------------------
+
+Python-level access to persistent data is provided by the
+:class:`Entity <cubicweb.entity>` class.
+
+.. 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:
+
+* the attributes defined in the schema appear as attributes of these classes
+
+* the relations defined in the schema appear as attributes of these classes,
+  but are lists of instances
+
+`Formatting and output generation`:
+
+* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
+  (and returns a unicode string)
+
+* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
+
+* :meth:`rest_path()`, returns a relative REST URL to get the entity
+
+* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
+  returns a string enabling the display of an attribute value in a given format
+  (the value is automatically recovered if necessary)
+
+`Data handling`:
+
+* :meth:`as_rset()`, converts the entity into an equivalent result set simulating the
+  request `Any X WHERE X eid _eid_`
+
+* :meth:`complete(skip_bytes=True)`, executes a request that recovers at
+  once all the missing attributes of an entity
+
+* :meth:`get_value(name)`, returns the value associated to the attribute name given
+  in parameter
+
+* :meth:`related(rtype, role='subject', limit=None, entities=False)`,
+  returns a list of entities related to the current entity by the
+  relation given in parameter
+
+* :meth:`unrelated(rtype, targettype, role='subject', limit=None)`,
+  returns a result set corresponding to the entities not (yet)
+  related to the current entity by the relation given in parameter
+  and satisfying its constraints
+
+* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the
+  corresponding values given named parameters. 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).
+
+* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid
+  given in the parameters on the current entity
+
+* :meth:`cw_delete()` allows to delete the entity
+
+
+The :class:`AnyEntity` 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.
+
+The class `AnyEntity` is a sub-class of Entity that add methods to it,
+and helps specializing (by further subclassing) the handling of a
+given entity type.
+
+Most methods defined for `AnyEntity`, in addition to `Entity`, add
+support for the `Dublin Core`_ metadata.
+
+.. _`Dublin Core`: http://dublincore.org/
+
+`Standard meta-data (Dublin Core)`:
+
+* :meth:`dc_title()`, returns a unicode string corresponding to the
+  meta-data `Title` (used by default is the first non-meta attribute
+  of the entity schema)
+
+* :meth:`dc_long_title()`, same as dc_title but can return a more
+  detailed title
+
+* :meth:`dc_description(format='text/plain')`, returns a unicode string
+  corresponding to the meta-data `Description` (looks for a
+  description attribute by default)
+
+* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data
+  `Authors` (owners by default)
+
+* :meth:`dc_creator()`, returns a unicode string corresponding to the
+  creator of the entity
+
+* :meth:`dc_date(date_format=None)`, returns a unicode string corresponding to
+  the meta-data `Date` (update date by default)
+
+* :meth:`dc_type(form='')`, returns a string to display the entity type by
+  specifying the preferred form (`plural` for a plural form)
+
+* :meth:`dc_language()`, returns the language used by the entity
+
+Inheritance
+-----------
+
+When describing a data model, entities can inherit from other entities as is
+common in object-oriented programming.
+
+You have the possibility to redefine whatever pleases you, as follow:
+
+.. sourcecode:: python
+
+    from cubes.OTHER_CUBE import entities
+
+    class EntityExample(entities.EntityExample):
+
+        def dc_long_title(self):
+            return '%s (%s)' % (self.name, self.description)
+
+The most specific entity definition will always the one used by the
+ORM. For instance, the new EntityExample above in mycube replaces the
+one in OTHER_CUBE. These types are stored in the `etype` section of
+the `vregistry`.
+
+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`).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,13 @@
+Data as objects
+===============
+
+In this chapter, we will introduce the objects that are used to handle
+the logic associated to the data stored in the database.
+
+.. toctree::
+   :maxdepth: 1
+
+   data-as-objects
+   load-sort
+   adapters
+   application-logic
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/load-sort.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,40 @@
+
+.. _FetchAttrs:
+
+Loaded attributes and default sorting management
+````````````````````````````````````````````````
+
+* The class attribute `fetch_attrs` allows to define in an entity class a list of
+  names of attributes that should be automatically loaded when entities of this
+  type are fetched from the database using ORM methods retrieving entity of this
+  type (such as :meth:`related` and :meth:`unrelated`). You can also put relation
+  names in there, but we are limited to *subject relations of cardinality `?` or
+  `1`*.
+
+* The :meth:`cw_fetch_order` and :meth:`cw_fetch_unrelated_order` class methods
+  are respectively responsible to control how entities will be sorted when:
+
+  - retrieving all entities of a given type, or entities related to another
+
+  - retrieving a list of entities for use in drop-down lists enabling relations
+    creation in the editing view of an entity
+
+By default entities will be listed on their modification date descending,
+i.e. you'll get entities recently modified first. While this is usually a good
+default in drop-down list, you'll probably want to change `cw_fetch_order`.
+
+This may easily be done using the :func:`~cubicweb.entities.fetch_config`
+function, which simplifies the definition of attributes to load and sorting by
+returning a list of attributes to pre-load (considering automatically the
+attributes of `AnyEntity`) and a sorting function as described below:
+
+.. autofunction:: cubicweb.entities.fetch_config
+
+In you want something else (such as sorting on the result of a registered
+procedure), here is the prototype of those methods:
+
+
+.. automethod:: cubicweb.entity.Entity.cw_fetch_order
+
+.. automethod:: cubicweb.entity.Entity.cw_fetch_unrelated_order
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/fti.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,159 @@
+.. _fti:
+
+Full Text Indexing in CubicWeb
+------------------------------
+
+When an attribute is tagged as *fulltext-indexable* in the datamodel,
+CubicWeb will automatically trigger hooks to update the internal
+fulltext index (i.e the ``appears`` SQL table) each time this attribute
+is modified.
+
+CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
+fulltext on demand:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
+
+You can also rebuild the fulltext index for a given set of entity types:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
+
+In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
+will be rebuilt.
+
+
+Standard FTI process
+~~~~~~~~~~~~~~~~~~~~
+
+Considering an entity type ``ET``, the default *fti* process is to :
+
+1. fetch all entities of type ``ET``
+
+2. for each entity, adapt it to ``IFTIndexable`` (see
+   :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
+
+3. call
+   :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
+   the adapter which is supposed to return a dictionary *weight* ->
+   *list of words* as expected by
+   :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
+   tokenization of each attribute value is done by
+   :meth:`~logilab.database.fti.tokenize`.
+
+
+See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
+
+
+Yams and ``fulltext_container``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible in the datamodel to indicate that fulltext-indexed
+attributes defined for an entity type will be used to index not the
+entity itself but a related entity. This is especially useful for
+composite entities. Let's take a look at (a simplified version of)
+the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
+
+.. sourcecode:: python
+
+  class CWUser(WorkflowableEntityType):
+      login     = String(required=True, unique=True, maxsize=64)
+      upassword = Password(required=True)
+
+  class EmailAddress(EntityType):
+      address = String(required=True,  fulltextindexed=True,
+                       indexed=True, unique=True, maxsize=128)
+
+
+  class use_email_relation(RelationDefinition):
+      name = 'use_email'
+      subject = 'CWUser'
+      object = 'EmailAddress'
+      cardinality = '*?'
+      composite = 'subject'
+
+
+The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
+and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
+in your application, if you use fulltext search to look for an email address, CubicWeb
+will return the ``EmailAddress`` itself. But the objects we'd like to index
+are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
+
+The simplest way to achieve that is to tag the ``use_email`` relation in
+the datamodel:
+
+.. sourcecode:: python
+
+  class use_email(RelationType):
+      fulltext_container = 'subject'
+
+
+Customizing how entities are fetched during ``db-rebuild-fti``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``db-rebuild-fti`` will call the
+:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
+method on your entity type.
+
+.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
+
+Now, suppose you've got a _huge_ table to index, you probably don't want to
+get all entities at once. So here's a simple customized example that will
+process block of 10000 entities:
+
+.. sourcecode:: python
+
+
+    class MyEntityClass(AnyEntity):
+        __regid__ = 'MyEntityClass'
+
+    @classmethod
+    def cw_fti_index_rql_queries(cls, req):
+        # get the default RQL method and insert LIMIT / OFFSET instructions
+        base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
+        selected, restrictions = base_rql.split(' WHERE ')
+        rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
+            selected, restrictions)
+        # count how many entities you'll have to index
+        count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
+        # iterate by blocks of 10000 entities
+        chunksize = 10000
+        for offset in xrange(0, count, chunksize):
+            print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
+            yield rql_template % {'limit': chunksize, 'offset': offset}
+
+Since you have access to ``req``, you can more or less fetch whatever you want.
+
+
+Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also customize the FTI process by providing your own ``get_words()``
+implementation:
+
+.. sourcecode:: python
+
+    from cubicweb.entities.adapters import IFTIndexableAdapter
+
+    class SearchIndexAdapter(IFTIndexableAdapter):
+        __regid__ = 'IFTIndexable'
+        __select__ = is_instance('MyEntityClass')
+
+        def fti_containers(self, _done=None):
+            """this should yield any entity that must be considered to
+            fulltext-index self.entity
+
+            CubicWeb's default implementation will look for yams'
+            ``fulltex_container`` property.
+            """
+            yield self.entity
+            yield self.entity.some_related_entity
+
+
+        def get_words(self):
+            # implement any logic here
+            # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
+            # for the actual signification of 'C'
+            return {'C': ['any', 'word', 'I', 'want']}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,25 @@
+.. _Part2:
+
+----------------------
+Repository development
+----------------------
+
+This part is about developing applications with the *CubicWeb*
+framework. It is not concerned with the web system, which is a
+separate layer and has its own whole chapter.
+
+.. toctree::
+   :maxdepth: 2
+   :numbered:
+
+   cubes/index
+   vreg.rst
+   datamodel/index
+   entityclasses/index
+   devcore/index
+   repo/index
+   testing.rst
+   migration.rst
+   profiling.rst
+   fti.rst
+   dataimport
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/migration.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,250 @@
+.. -*- coding: utf-8 -*-
+
+.. _migration:
+
+Migration
+=========
+
+One of the main design goals of *CubicWeb* was to support iterative and agile
+development. For this purpose, multiple actions are provided to facilitate the
+improvement of an instance, and in particular to handle the changes to be
+applied to the data model, without loosing existing data.
+
+The current version of a cube (and of cubicweb itself) is provided in the file
+`__pkginfo__.py` as a tuple of 3 integers.
+
+Migration scripts management
+----------------------------
+
+Migration scripts has to be located in the directory `migration` of your
+cube and named accordingly:
+
+::
+
+  <version n° X.Y.Z>[_<description>]_<mode>.py
+
+in which :
+
+* X.Y.Z is the model version number to which the script enables to migrate.
+
+* *mode* (between the last "_" and the extension ".py") is used for
+  distributed installation. It indicates to which part
+  of the application (RQL server, web server) the script applies.
+  Its value could be :
+
+  * `common`, applies to the RQL server as well as the web server and updates
+    files on the hard drive (configuration files migration for example).
+
+  * `web`, applies only to the web server and updates files on the hard drive.
+
+  * `repository`, applies only to the RQL server and updates files on the
+    hard drive.
+
+  * `Any`, applies only to the RQL server and updates data in the database
+    (schema and data migration for example).
+
+Again in the directory `migration`, the file `depends.map` allows to indicate
+that for the migration to a particular model version, you always have to first
+migrate to a particular *CubicWeb* version. This file can contain comments (lines
+starting with `#`) and a dependency is listed as follows: ::
+
+  <model version n° X.Y.Z> : <cubicweb version n° X.Y.Z>
+
+For example: ::
+
+  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
+
+Base context
+------------
+
+The following identifiers are pre-defined in migration scripts:
+
+* `config`, instance configuration
+
+* `interactive_mode`, boolean indicating that the script is executed in
+  an interactive mode or not
+
+* `versions_map`, dictionary of migrated versions  (key are cubes
+  names, including 'cubicweb', values are (from version, to version)
+
+* `confirm(question)`, function asking the user and returning true
+  if the user answers yes, false otherwise (always returns true in
+  non-interactive mode)
+
+* `_()` is equivalent to `unicode` allowing to flag the strings to
+  internationalize in the migration scripts.
+
+In the `repository` scripts, the following identifiers are also defined:
+
+* `commit(ask_confirm=True)`, request confirming and executing a "commit"
+
+* `schema`, instance schema (readen from the database)
+
+* `fsschema`, installed schema on the file system (e.g. schema of
+  the updated model and cubicweb)
+
+* `repo`, repository object
+
+* `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`
+scripts:
+
+* `add_attribute(etype, attrname, attrtype=None, commit=True)`, adds a new
+  attribute to an existing entity type. If the attribute type is not specified,
+  then it is extracted from the updated schema.
+
+* `drop_attribute(etype, attrname, commit=True)`, removes an attribute from an
+  existing entity type.
+
+* `rename_attribute(etype, oldname, newname, commit=True)`, renames an attribute
+
+* `add_entity_type(etype, auto=True, commit=True)`, adds a new entity type.
+  If `auto` is True, all the relations using this entity type and having a known
+  entity type on the other hand will automatically be added.
+
+* `drop_entity_type(etype, commit=True)`, removes an entity type and all the
+  relations using it.
+
+* `rename_entity_type(oldname, newname, commit=True)`, renames an entity type
+
+* `add_relation_type(rtype, addrdef=True, commit=True)`, adds a new relation
+  type. If `addrdef` is True, all the relations definitions of this type will
+  be added.
+
+* `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
+  definitions of this type.
+
+* `rename_relation_type(oldname, newname, commit=True)`, renames a relation type.
+
+* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
+  relation definition.
+
+* `drop_relation_definition(subjtype, rtype, objtype, commit=True)`, removes
+  a relation definition.
+
+* `sync_schema_props_perms(ertype=None, syncperms=True, syncprops=True, syncrdefs=True, commit=True)`,
+  synchronizes properties and/or permissions on:
+  - the whole schema if ertype is None
+  - an entity or relation type schema if ertype is a string
+  - a relation definition  if ertype is a 3-uple (subject, relation, object)
+
+* `change_relation_props(subjtype, rtype, objtype, commit=True, **kwargs)`, changes
+  properties of a relation definition by using the named parameters of the properties
+  to change.
+
+* `set_widget(etype, rtype, widget, commit=True)`, changes the widget used for the
+  relation <rtype> of entity type <etype>.
+
+* `set_size_constraint(etype, rtype, size, commit=True)`, changes the size constraints
+  for the relation <rtype> of entity type <etype>.
+
+Data migration
+--------------
+The following functions for data migration are available in `repository` scripts:
+
+* `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL
+  query, either to interrogate or update. A result set object is returned.
+
+* `add_entity(etype, *args, **kwargs)`, adds a new entity of the given type.
+  The attribute and relation values are specified as named positional
+  arguments.
+
+Workflow creation
+-----------------
+
+The following functions for workflow creation are available in `repository`
+scripts:
+
+* `add_workflow(label, workflowof, initial=False, commit=False, **kwargs)`, adds a new workflow
+  for a given type(s)
+
+You can find more details about workflows in the chapter :ref:`Workflow` .
+
+Configuration migration
+-----------------------
+
+The following functions for configuration migration are available in all
+scripts:
+
+* `option_renamed(oldname, newname)`, indicates that an option has been renamed
+
+* `option_group_change(option, oldgroup, newgroup)`, indicates that an option does not
+  belong anymore to the same group.
+
+* `option_added(oldname, newname)`, indicates that an option has been added.
+
+* `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
+--------------------------
+Those functions are only used for low level operations that could not be
+accomplished otherwise or to repair damaged databases during interactive
+session. They are available in `repository` scripts:
+
+* `sql(sql, args=None, ask_confirm=True)`, executes an arbitrary SQL query on the system source
+* `add_entity_type_table(etype, commit=True)`
+* `add_relation_type_table(rtype, commit=True)`
+* `uninline_relation(rtype, commit=True)`
+
+
+[FIXME] Add explanation on how to use cubicweb-ctl shell
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/profiling.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,57 @@
+.. _PROFILING:
+
+Profiling and performance
+=========================
+
+If you feel that one of your pages takes more time than it should to be
+generated, chances are that you're making too many RQL queries.  Obviously,
+there are other reasons but experience tends to show this is the first thing to
+track down. Luckily, CubicWeb provides a configuration option to log RQL
+queries. In your ``all-in-one.conf`` file, set the **query-log-file** option::
+
+    # web application query log file
+    query-log-file=/home/user/myapp-rql.log
+
+Then restart your application, reload your page and stop your application.
+The file ``myapp-rql.log`` now contains the list of RQL queries that were
+executed during your test. It's a simple text file containing lines such as::
+
+    Any A WHERE X eid %(x)s, X lastname A {'x': 448} -- (0.002 sec, 0.010 CPU sec)
+    Any A WHERE X eid %(x)s, X firstname A {'x': 447} -- (0.002 sec, 0.000 CPU sec)
+
+The structure of each line is::
+
+    <RQL QUERY> <QUERY ARGS IF ANY> -- <TIME SPENT>
+
+CubicWeb also provides the **exlog** command to examine and summarize data found
+in such a file:
+
+.. sourcecode:: sh
+
+    $ cubicweb-ctl exlog /home/user/myapp-rql.log
+    0.07 50 Any A WHERE X eid %(x)s, X firstname A {}
+    0.05 50 Any A WHERE X eid %(x)s, X lastname A {}
+    0.01 1 Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E employees X, X modification_date AA {}
+    0.01 1 Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s {, }
+    0.01 1 Any B,T,P ORDERBY lower(T) WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, U eid %(x)s {}
+    0.01 1 Any A,B,C,D WHERE A eid %(x)s,A name B,A creation_date C,A modification_date D {}
+
+This command sorts and uniquifies queries so that it's easy to see where
+is the hot spot that needs optimization.
+
+Do not neglect to set the **fetch_attrs** attribute you can define in your
+entity classes because it can greatly reduce the number of queries executed (see
+:ref:`FetchAttrs`).
+
+You should also know about the **profile** option in the ``all-in-on.conf``. If
+set, this option will make your application run in an `hotshot`_ session and
+store the results in the specified file.
+
+.. _hotshot: http://docs.python.org/library/hotshot.html#module-hotshot
+
+Last but no least, if you're using the PostgreSQL database backend, VACUUMing
+your database can significantly improve the performance of the queries (by
+updating the statistics used by the query optimizer). Nowadays, this is done
+automatically from time to time, but if you've just imported a large amount of
+data in your db, you will want to vacuum it (with the analyse option on). Read
+the documentation of your database for more information.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/hooks.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,279 @@
+.. -*- coding: utf-8 -*-
+.. _hooks:
+
+Hooks and Operations
+====================
+
+.. autodocstring:: cubicweb.server.hook
+
+
+Example using dataflow hooks
+----------------------------
+
+We will use a very simple example to show hooks usage. Let us start with the
+following schema.
+
+.. sourcecode:: python
+
+   class Person(EntityType):
+       age = Int(required=True)
+
+We would like to add a range constraint over a person's age. Let's write an hook
+(supposing yams can not handle this nativly, which is wrong). It shall be placed
+into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
+`mycube/hooks/... package` containing hooks in various modules.
+
+.. sourcecode:: python
+
+   from cubicweb import ValidationError
+   from cubicweb.predicates import is_instance
+   from cubicweb.server.hook import Hook
+
+   class PersonAgeRange(Hook):
+        __regid__ = 'person_age_range'
+        __select__ = Hook.__select__ & is_instance('Person')
+        events = ('before_add_entity', 'before_update_entity')
+
+        def __call__(self):
+	    if 'age' in self.entity.cw_edited:
+                if 0 <= self.entity.age <= 120:
+                   return
+		msg = self._cw._('age must be between 0 and 120')
+		raise ValidationError(self.entity.eid, {'age': msg})
+
+In our example the base `__select__` is augmented with an `is_instance` selector
+matching the desired entity type.
+
+The `events` tuple is used specify that our hook should be called before the
+entity is added or updated.
+
+Then in the hook's `__call__` method, we:
+
+* check if the 'age' attribute is edited
+* if so, check the value is in the range
+* if not, raise a validation error properly
+
+Now Let's augment our schema with new `Company` entity type with some relation to
+`Person` (in 'mycube/schema.py').
+
+.. sourcecode:: python
+
+   class Company(EntityType):
+        name = String(required=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+        subsidiary_of = SubjectRelation('Company', cardinality='*?')
+
+
+We would like to constrain the company's bosses to have a minimum (legal)
+age. Let's write an hook for this, which will be fired when the `boss` relation
+is established (still supposing we could not specify that kind of thing in the
+schema).
+
+.. sourcecode:: python
+
+   class CompanyBossLegalAge(Hook):
+        __regid__ = 'company_boss_legal_age'
+        __select__ = Hook.__select__ & match_rtype('boss')
+        events = ('before_add_relation',)
+
+        def __call__(self):
+            boss = self._cw.entity_from_eid(self.eidto)
+            if boss.age < 18:
+                msg = self._cw._('the minimum age for a boss is 18')
+                raise ValidationError(self.eidfrom, {'boss': msg})
+
+.. Note::
+
+    We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
+    proper relation type.
+
+    The essential difference with respect to an entity hook is that there is no
+    self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
+    represent the subject and object **eid** of the relation.
+
+Suppose we want to check that there is no cycle by the `subsidiary_of`
+relation. This is best achieved in an operation since all relations are likely to
+be set at commit time.
+
+.. sourcecode:: python
+
+    from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
+
+    def check_cycle(self, session, eid, rtype, role='subject'):
+        parents = set([eid])
+        parent = session.entity_from_eid(eid)
+        while parent.related(rtype, role):
+            parent = parent.related(rtype, role)[0]
+            if parent.eid in parents:
+                msg = session._('detected %s cycle' % rtype)
+                raise ValidationError(eid, {rtype: msg})
+            parents.add(parent.eid)
+
+
+    class CheckSubsidiaryCycleOp(Operation):
+
+        def precommit_event(self):
+            check_cycle(self.session, self.eidto, 'subsidiary_of')
+
+
+    class CheckSubsidiaryCycleHook(Hook):
+        __regid__ = 'check_no_subsidiary_cycle'
+        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
+        events = ('after_add_relation',)
+
+        def __call__(self):
+            CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
+
+
+Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
+exceptions are usually programming errors.
+
+In the above example, our hook will instantiate an operation each time the hook
+is called, i.e. each time the `subsidiary_of` relation is set. There is an
+alternative method to schedule an operation from a hook, using the
+:func:`get_instance` class method.
+
+.. sourcecode:: python
+
+   from cubicweb.server.hook import set_operation
+
+   class CheckSubsidiaryCycleHook(Hook):
+       __regid__ = 'check_no_subsidiary_cycle'
+       events = ('after_add_relation',)
+       __select__ = Hook.__select__ & match_rtype('subsidiary_of')
+
+       def __call__(self):
+           CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)
+
+   class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):
+
+       def precommit_event(self):
+           for eid in self.get_data():
+               check_cycle(self.session, eid, self.rtype)
+
+
+Here, we call :func:`set_operation` so that we will simply accumulate eids of
+entities to check at the end in a single `CheckSubsidiaryCycleOp`
+operation. Value are stored in a set associated to the
+'subsidiary_cycle_detection' transaction data key. The set initialization and
+operation creation are handled nicely by :func:`set_operation`.
+
+A more realistic example can be found in the advanced tutorial chapter
+:ref:`adv_tuto_security_propagation`.
+
+
+Inter-instance communication
+----------------------------
+
+If your application consists of several instances, you may need some means to
+communicate between them.  Cubicweb provides a publish/subscribe mechanism
+using ØMQ_.  In order to use it, use
+:meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
+`repo.app_instances_bus` object.  The `callback` will get the message (as a
+list).  A message can be sent by calling
+:meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
+The first element of the message is the topic which is used for filtering and
+dispatching messages.
+
+.. _ØMQ: http://www.zeromq.org/
+
+.. sourcecode:: python
+
+  class FooHook(hook.Hook):
+      events = ('server_startup',)
+      __regid__ = 'foo_startup'
+
+      def __call__(self):
+          def callback(msg):
+              self.info('received message: %s', ' '.join(msg))
+          self.repo.app_instances_bus.add_subscription('hello', callback)
+
+.. sourcecode:: python
+
+  def do_foo(self):
+      actually_do_foo()
+      self._cw.repo.app_instances_bus.publish(['hello', 'world'])
+
+The `zmq-address-pub` configuration variable contains the address used
+by the instance for sending messages, e.g. `tcp://*:1234`.  The
+`zmq-address-sub` variable contains a comma-separated list of addresses
+to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
+
+
+Hooks writing tips
+------------------
+
+Reminder
+~~~~~~~~
+
+You should never use the `entity.foo = 42` notation to update an entity. It will
+not do what you expect (updating the database). Instead, use the
+:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
+:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
+'before_update_entity' event.
+
+
+How to choose between a before and an after event ?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`before_*` hooks give you access to the old attribute (or relation)
+values. You can also intercept and update edited values in the case of
+entity modification before they reach the database.
+
+Else the question is: should I need to do things before or after the actual
+modification ? If the answer is "it doesn't matter", use an 'after' event.
+
+
+Validation Errors
+~~~~~~~~~~~~~~~~~
+
+When a hook which is responsible to maintain the consistency of the
+data model detects an error, it must use a specific exception named
+:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
+:exc:`~cubicweb.ValidationError` is a programming error. Raising it
+entails aborting the current transaction.
+
+This exception is used to convey enough information up to the user
+interface. Hence its constructor is different from the default Exception
+constructor. It accepts, positionally:
+
+* an entity eid (**not the entity itself**),
+
+* a dict whose keys represent attribute (or relation) names and values
+  an end-user facing message (hence properly translated) relating the
+  problem.
+
+.. sourcecode:: python
+
+  raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
+                                    'temperature': self._cw._('too hot')})
+
+
+Checking for object created/deleted in the current transaction
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In hooks, you can use the
+:meth:`~cubicweb.server.session.Session.added_in_transaction` or
+:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
+object to check if an eid has been created or deleted during the hook's
+transaction.
+
+This is useful to enable or disable some stuff if some entity is being added or
+deleted.
+
+.. sourcecode:: python
+
+   if self._cw.deleted_in_transaction(self.eidto):
+      return
+
+
+Peculiarities of inlined relations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
+for details) are inserted in the database at the same time as entity attributes.
+
+This may have some side effect, for instance when creating an entity
+and setting an inlined relation in the same rql query, then at
+`before_add_relation` time, the relation will already exist in the
+database (it is otherwise not the case).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,13 @@
+.. -*- coding: utf-8 -*-
+
+Repository customization
+++++++++++++++++++++++++
+.. toctree::
+   :maxdepth: 1
+
+   sessions
+   hooks
+   notifications
+   tasks
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/notifications.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,29 @@
+.. -*- coding: utf-8 -*-
+
+Notifications management
+========================
+
+CubicWeb provides a machinery to ease notifications handling. To use it for a
+notification:
+
+* write a view inheriting from
+  :class:`~cubicweb.sobjects.notification.NotificationView`.  The usual view api
+  is used to generated the email (plain text) content, and additional
+  :meth:`~cubicweb.sobjects.notification.NotificationView.subject` and
+  :meth:`~cubicweb.sobjects.notification.NotificationView.recipients` methods
+  are used to build the email's subject and
+  recipients. :class:`NotificationView` provides default implementation for both
+  methods.
+
+* write a hook for event that should trigger this notification, select the view
+  (without rendering it), and give it to
+  :func:`cubicweb.hooks.notification.notify_on_commit` so that the notification
+  will be sent if the transaction succeed.
+
+
+.. XXX explain recipient finder and provide example
+
+API details
+~~~~~~~~~~~
+.. autoclass:: cubicweb.sobjects.notification.NotificationView
+.. autofunction:: cubicweb.hooks.notification.notify_on_commit
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/sessions.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,319 @@
+.. -*- coding: utf-8 -*-
+
+Sessions
+========
+
+Sessions are objects linked to an authenticated user.  The `Session.new_cnx`
+method returns a new Connection linked to that session.
+
+Connections
+===========
+
+Connections provide the `.execute` method to query the data sources, along with
+`.commit` and `.rollback` methods for transaction management.
+
+Kinds of connections
+--------------------
+
+There are two kinds of connections.
+
+* `normal connections` are the most common: they are related to users and
+  carry security checks coming with user credentials
+
+* `internal connections` have all the powers; they are also used in only a
+  few situations where you don't already have an adequate session at
+  hand, like: user authentication, data synchronisation in
+  multi-source contexts
+
+Normal connections are typically named `_cw` in most appobjects or
+sometimes just `session`.
+
+Internal connections are available from the `Repository` object and are
+to be used like this:
+
+.. sourcecode:: python
+
+   with self.repo.internal_cnx() as cnx:
+       do_stuff_with(cnx)
+       cnx.commit()
+
+Connections should always be used as context managers, to avoid leaks.
+
+
+Python/RQL API
+~~~~~~~~~~~~~~
+
+The Python API developped to interface with RQL is inspired from the standard db-api,
+but since `execute` returns its results directly, there is no `cursor` concept.
+
+.. sourcecode:: python
+
+   execute(rqlstring, args=None, build_descr=True)
+
+:rqlstring: the RQL query to execute (unicode)
+:args: if the query contains substitutions, a dictionary containing the values to use
+
+The `Connection` object owns the methods `commit` and `rollback`. You
+*should never need to use them* during the development of the web
+interface based on the *CubicWeb* framework as it determines the end
+of the transaction depending on the query execution success. They are
+however useful in other contexts such as tests or custom controllers.
+
+.. note::
+
+  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.
+
+.. note::
+
+   A :exc:`ValidationError` has a `entity` attribute. In CubicWeb,
+   this atttribute is set to the entity's eid (not a reference to the
+   entity itself).
+
+Executing RQL queries from a view or a hook
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When you're within code of the web interface, the Connection is handled by the
+request object. You should not have to access it directly, but use the
+`execute` method directly available on the request, eg:
+
+.. sourcecode:: python
+
+   rset = self._cw.execute(rqlstring, kwargs)
+
+Similarly, on the server side (eg in hooks), there is no request object (since
+you're directly inside the data-server), so you'll have to use the execute method
+of the Connection object.
+
+Proper usage of `.execute`
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Let's say you want to get T which is in configuration C, this translates to:
+
+.. sourcecode:: python
+
+   self._cw.execute('Any T WHERE T in_conf C, C eid %s' % entity.eid)
+
+But it must be written in a syntax that will benefit from the use
+of a cache on the RQL server side:
+
+.. sourcecode:: python
+
+   self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid})
+
+The syntax tree is built once for the "generic" RQL and can be re-used
+with a number of different eids.  The rql IN operator is an exception
+to this rule.
+
+.. sourcecode:: python
+
+   self._cw.execute('Any T WHERE T in_conf C, C name IN (%s)'
+                    % ','.join(['foo', 'bar']))
+
+Alternatively, some of the common data related to an entity can be
+obtained from the `entity.related()` method (which is used under the
+hood by the ORM when you use attribute access notation on an entity to
+get a relation. The initial request would then be translated to:
+
+.. sourcecode:: python
+
+   entity.related('in_conf', 'object')
+
+Additionally this benefits from the fetch_attrs policy (see :ref:`FetchAttrs`)
+optionally defined on the class element, which says which attributes must be
+also loaded when the entity is loaded through the ORM.
+
+.. _resultset:
+
+The `ResultSet` API
+~~~~~~~~~~~~~~~~~~~
+
+ResultSet instances are a very commonly manipulated object. They have
+a rich API as seen below, but we would like to highlight a bunch of
+methods that are quite useful in day-to-day practice:
+
+* `__str__()` (applied by `print`) gives a very useful overview of both
+  the underlying RQL expression and the data inside; unavoidable for
+  debugging purposes
+
+* `printable_rql()` returns a well formed RQL expression as a
+  string; it is very useful to build views
+
+* `entities()` returns a generator on all entities of the result set
+
+* `get_entity(row, col)` gets the entity at row, col coordinates; one
+  of the most used result set methods
+
+.. autoclass:: cubicweb.rset.ResultSet
+   :members:
+   :noindex:
+
+
+Authentication and management of sessions
+-----------------------------------------
+
+The authentication process is a ballet involving a few dancers:
+
+* through its `get_session` 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 (named `LoginPasswordRetriever`)
+        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 delegate work to an authentifier
+          class that defines 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 plethora 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 `trustedauth`_ cube.
+
+.. _`trustedauth`: http://www.cubicweb.org/project/cubicweb-trustedauth
+
+Repository authentication plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On the repository side, it is possible to register a source
+authentifier using the following kind of code:
+
+.. 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
+
+ class XFooUserRetriever(authentication.LoginPasswordRetriever):
+     """ 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
+
+Full Session and Connection API
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autoclass:: cubicweb.server.session.Session
+.. autoclass:: cubicweb.server.session.Connection
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/tasks.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,9 @@
+.. -*- coding: utf-8 -*-
+
+Tasks
+=========
+
+[WRITE ME]
+
+* repository tasks
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/testing.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,559 @@
+.. -*- coding: utf-8 -*-
+
+Tests
+=====
+
+Unit tests
+----------
+
+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,
+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, automatic tests check that views are valid
+XHTML. See :ref:`automatic_views_tests` for details.
+
+Most unit tests need a live database to work against. This is achieved
+by CubicWeb using automatically sqlite (bundled with Python, see
+http://docs.python.org/library/sqlite3.html) as a backend.
+
+The database is stored in the mycube/test/tmpdb,
+mycube/test/tmpdb-template files. If it does not (yet) exist, it will
+be built automatically when the test suite starts.
+
+.. warning::
+
+  Whenever the schema changes (new entities, attributes, relations)
+  one must delete these two files. Changes concerned only with entity
+  or relation type properties (constraints, cardinalities,
+  permissions) and generally dealt with using the
+  `sync_schema_props_perms()` function of the migration environment do
+  not need a database regeneration step.
+
+.. _hook_test:
+
+Unit test by example
+````````````````````
+
+We start with an example extracted from the keyword cube (available
+from http://www.cubicweb.org/project/cubicweb-keyword).
+
+.. sourcecode:: python
+
+    from cubicweb.devtools.testlib import CubicWebTC
+    from cubicweb import ValidationError
+
+    class ClassificationHooksTC(CubicWebTC):
+
+        def setup_database(self):
+            with self.admin_access.repo_cnx() as cnx:
+                group_etype = cnx.find('CWEType', name='CWGroup').one()
+                c1 = cnx.create_entity('Classification', name=u'classif1',
+                                       classifies=group_etype)
+                user_etype = cnx.find('CWEType', name='CWUser').one()
+                c2 = cnx.create_entity('Classification', name=u'classif2',
+                                       classifies=user_etype)
+                self.kw1eid = cnx.create_entity('Keyword', name=u'kwgroup', included_in=c1).eid
+                cnx.commit()
+
+        def test_cannot_create_cycles(self):
+            with self.admin_access.repo_cnx() as cnx:
+                kw1 = cnx.entity_from_eid(self.kw1eid)
+                # direct obvious cycle
+                with self.assertRaises(ValidationError):
+                    kw1.cw_set(subkeyword_of=kw1)
+                cnx.rollback()
+                # testing indirect cycles
+                kw3 = cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
+                                  'SK subkeyword_of K WHERE C name "classif1", K eid %(k)s'
+                                  {'k': kw1}).get_entity(0,0)
+                kw3.cw_set(reverse_subkeyword_of=kw1)
+                self.assertRaises(ValidationError, cnx.commit)
+
+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.
+
+The test case itself checks that an Operation does its job of
+preventing cycles amongst Keyword entities.
+
+The `create_entity` method of connection (or request) objects allows
+to create an entity. You can link this entity to other 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::
+
+   the :meth:`commit` method is not called automatically. You have to
+   call it explicitly if needed (notably to test operations). It is a
+   good practice to regenerate entities with :meth:`entity_from_eid`
+   after a commit to avoid request cache effects.
+
+You can see an example of security tests in the
+:ref:`adv_tuto_security`.
+
+It is possible to have these tests run continuously using `apycot`_.
+
+.. _apycot: http://www.cubicweb.org/project/apycot
+
+.. _securitytest:
+
+Managing connections or users
++++++++++++++++++++++++++++++
+
+Since unit tests are done with the SQLITE backend and this does not
+support multiple connections at a time, you must be careful when
+simulating security, changing users.
+
+By default, tests run with a user with admin privileges. Connections
+using these credentials are accessible through the `admin_access` object
+of the test classes.
+
+The `repo_cnx()` method returns a connection object that can be used as a
+context manager:
+
+.. sourcecode:: python
+
+   # admin_access is a pre-cooked session wrapping object
+   # it is built with:
+   # self.admin_access = self.new_access('admin')
+   with self.admin_access.repo_cnx() as cnx:
+       cnx.execute(...)
+       self.create_user(cnx, login='user1')
+       cnx.commit()
+
+   user1access = self.new_access('user1')
+   with user1access.web_request() as req:
+       req.execute(...)
+       req.cnx.commit()
+
+On exit of the context manager, a rollback is issued, which releases
+the connection. Don't forget to issue the `cnx.commit()` calls!
+
+.. warning::
+
+   Do not use references kept to the entities created with a
+   connection from another one!
+
+Email notifications tests
+`````````````````````````
+
+When running tests, potentially generated e-mails are not really sent
+but are found in the list `MAILBOX` of module
+:mod:`cubicweb.devtools.testlib`.
+
+You can test your notifications by analyzing the contents of this list, which
+contains objects with two attributes:
+
+* `recipients`, the list of recipients
+* `msg`, email.Message object
+
+Let us look at a simple example from the ``blog`` cube.
+
+.. sourcecode:: python
+
+    from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
+
+    class BlogTestsCubicWebTC(CubicWebTC):
+        """test blog specific behaviours"""
+
+        def test_notifications(self):
+            with self.admin_access.web_request() as req:
+                cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
+                                    description=u'cubicweb is beautiful')
+                blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
+                                                 content=u'cubicweb hop')
+                blog_entry_1.cw_set(entry_of=cubicweb_blog)
+                blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
+                                                 content=u'cubicweb yes')
+                blog_entry_2.cw_set(entry_of=cubicweb_blog)
+                self.assertEqual(len(MAILBOX), 0)
+                req.cnx.commit()
+                self.assertEqual(len(MAILBOX), 2)
+                mail = MAILBOX[0]
+                self.assertEqual(mail.subject, '[data] hop')
+                mail = MAILBOX[1]
+                self.assertEqual(mail.subject, '[data] yes')
+
+Visible actions tests
+`````````````````````
+
+It is easy to write unit tests to test actions which are visible to
+a 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):
+            with self.admin_access.repo_cnx() as cnx:
+                self.confeid = cnx.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).eid
+
+        def test_admin(self):
+            with self.admin_access.web_request() as req:
+                rset = req.find('Conference').one()
+                self.assertListEqual(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.assertListEqual(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.confeid}),
+                                       ])
+
+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 a 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 :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
+invoked and tested.
+
+Depending on the schema, hooks and operations constraints, it is not
+always possible for the automatic auto_populate to proceed.
+
+It is possible of course to completely redefine auto_populate. A
+lighter solution is to give hints (fill some class attributes) about
+what entities and relations have to be skipped by the auto_populate
+mechanism. These are:
+
+* `no_auto_populate`, may contain a list of entity types to skip
+* `ignored_relations`, may contain a list of relation types to skip
+* `application_rql`, may contain a list of rql expressions that
+  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.
+
+Cache heavy database setup
+-------------------------------
+
+Some test suites require a complex setup of the database that takes
+seconds (or even minutes) to complete. Doing the whole setup for each
+individual test makes the whole run very slow. The ``CubicWebTC``
+class offer a simple way to prepare a specific database once for
+multiple tests. The `test_db_id` class attribute of your
+``CubicWebTC`` subclass must be set to a unique identifier and the
+:meth:`pre_setup_database` class method must build the cached content. As
+the :meth:`pre_setup_database` method is not garanteed to be called
+every time a test method is run, you must not set any class attribute
+to be used during test *there*. Databases for each `test_db_id` are
+automatically created if not already in cache. Clearing the cache is
+up to the user. Cache files are found in the :file:`data/database`
+subdirectory of your test directory.
+
+.. warning::
+
+  Take care to always have the same :meth:`pre_setup_database`
+  function for all classes with a given `test_db_id` otherwise your
+  tests will have unpredictable results depending on the first
+  encountered one.
+
+
+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):
+            with self.admin_access.web_request() as req:
+            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, req=req)
+
+
+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 comma-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. 
+
+Literate programming
+--------------------
+
+CubicWeb provides some literate programming capabilities. The :ref:`cubicweb-ctl`
+`shell` command accepts different file formats. If your file ends with `.txt`
+or `.rst`, the file will be parsed by :mod:`doctest.testfile` with CubicWeb's
+:ref:`migration` API enabled in it.
+
+Create a `scenario.txt` file in the `test/` directory and fill with some content.
+Refer to the :mod:`doctest.testfile` `documentation`_.
+
+.. _documentation: http://docs.python.org/library/doctest.html
+
+Then, you can run it directly by::
+
+    $ cubicweb-ctl shell <cube_instance> test/scenario.txt
+
+When your scenario file is ready, put it in a new test case to be able to run
+it automatically.
+
+.. sourcecode:: python
+
+      from os.path import dirname, join
+      from logilab.common.testlib import unittest_main
+      from cubicweb.devtools.testlib import CubicWebTC
+
+      class AcceptanceTC(CubicWebTC):
+
+              def test_scenario(self):
+                      self.assertDocTestFile(join(dirname(__file__), 'scenario.txt'))
+
+      if __name__ == '__main__':
+              unittest_main()
+
+Skipping a scenario
+```````````````````
+
+If you want to set up initial conditions that you can't put in your unit test
+case, you have to use a :exc:`KeyboardInterrupt` exception only because of the
+way :mod:`doctest` module will catch all the exceptions internally.
+
+    >>> if condition_not_met:
+    ...     raise KeyboardInterrupt('please, check your fixture.')
+
+Passing paramaters
+``````````````````
+Using extra arguments to parametrize your scenario is possible by prepending them
+by double dashes.
+
+Please refer to the `cubicweb-ctl shell --help` usage.
+
+.. important::
+    Your scenario file must be utf-8 encoded.
+
+Test APIS
+---------
+
+Using Pytest
+````````````
+
+The `pytest` utility (shipping with `logilab-common`_, which is a
+mandatory dependency of CubicWeb) extends the Python unittest
+functionality and is the preferred way to run the CubicWeb test
+suites. Bare unittests also work the usual way.
+
+.. _logilab-common: http://www.logilab.org/project/logilab-common
+
+To use it, you may:
+
+* just launch `pytest` in your cube to execute all tests (it will
+  discover them automatically)
+* launch `pytest unittest_foo.py` to execute one test file
+* launch `pytest unittest_foo.py bar` to execute all test methods and
+  all test cases whose name contains `bar`
+
+Additionally, the `-x` option tells pytest to exit at the first error
+or failure. The `-i` option tells pytest to drop into pdb whenever an
+exception occurs in a test.
+
+When the `-x` option has been used and the run stopped on a test, it
+is possible, after having fixed the test, to relaunch pytest with the
+`-R` option to tell it to start testing again from where it previously
+failed.
+
+Using the `TestCase` base class
+```````````````````````````````
+
+The base class of CubicWebTC is logilab.common.testlib.TestCase, which
+provides a lot of convenient assertion methods.
+
+.. autoclass:: logilab.common.testlib.TestCase
+   :members:
+
+CubicWebTC API
+``````````````
+.. autoclass:: cubicweb.devtools.testlib.CubicWebTC
+   :members:
+
+
+What you need to know about request and session
+-----------------------------------------------
+
+.. image:: ../../images/request_session.png
+
+First, remember to think that some code run on a client side, some
+other on the repository side. More precisely:
+
+* client side: web interface, raw repoapi connection (cubicweb-ctl shell for
+  instance);
+
+* repository side: RQL query execution, that may trigger hooks and operation.
+
+The client interacts with the repository through a repoapi connection.
+
+
+.. note::
+
+   These distinctions are going to disappear in cubicweb 3.21 (if not
+   before).
+
+A repoapi connection is tied to a session in the repository. The connection and
+request objects are inaccessible from repository code / the session object is
+inaccessible from client code (theoretically at least).
+
+The web interface provides a request class.  That `request` object provides
+access to all cubicweb resources, eg:
+
+* the registry (which itself provides access to the schema and the
+  configuration);
+
+* an underlying repoapi connection (when using req.execute, you actually call the
+  repoapi);
+
+* other specific resources depending on the client type (url generation according
+  to base url, form parameters, etc.).
+
+
+A `session` provides an api similar to a request regarding RQL execution and
+access to global resources (registry and all), but also has the following
+responsibilities:
+
+* handle transaction data, that will live during the time of a single
+  transaction. This includes the database connections that will be used to
+  execute RQL queries.
+
+* handle persistent data that may be used across different (web) requests
+
+* security and hooks control (not possible through a request)
+
+
+The `_cw` attribute
+```````````````````
+The `_cw` attribute available on every application object provides access to all
+cubicweb resources, i.e.:
+
+- For code running on the client side (eg web interface view), `_cw` is a request
+  instance.
+
+- For code running on the repository side (hooks and operation), `_cw` is a
+  Connection or Session instance.
+
+
+Beware some views may be called with a session (e.g. notifications) or with a
+request.
+
+
+Request, session and transaction
+````````````````````````````````
+
+In the web interface, an HTTP request is handled by a single request, which will
+be thrown away once the response is sent.
+
+The web publisher handles the transaction:
+
+* commit / rollback is done automatically
+
+* you should not commit / rollback explicitly, except if you really
+  need it
+
+Let's detail the process:
+
+1. an incoming RQL query comes from a client to the web stack
+
+2. the web stack opens an authenticated database connection for the
+   request, which is associated to a user session
+
+3. the query is executed (through the repository connection)
+
+4. this query may trigger hooks. Hooks and operations may execute some rql queries
+   through `cnx.execute`.
+
+5. the repository gets the result of the query in 1. If it was a RQL read query,
+   the database connection is released. If it was a write query, the connection
+   is then tied to the session until the transaction is commited or rolled back.
+
+6. results are sent back to the client
+
+This implies several things:
+
+* when using a request, or code executed in hooks, this database
+  connection handling is totally transparent
+
+* however, take care when writing tests: you are usually faking /
+  testing both the server and the client side, so you have to decide
+  when to use RepoAccess.client_cnx or RepoAccess.repo_cnx. Ask
+  yourself "where will the code I want to test be running, client or
+  repository side?". The response is usually: use a repo (since the
+  "client connection" concept is going away in a couple of releases).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/vreg.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,471 @@
+The Registry, selectors and application objects
+===============================================
+
+This chapter deals with some of the  core concepts of the |cubicweb| framework
+which make it different from other frameworks (and maybe not easy to
+grasp at a first glance). To be able to do advanced development with
+|cubicweb| you need a good understanding of what is explained below.
+
+This chapter goes deep into details. You don't have to remember them
+all but keep it in mind so you can go back there later.
+
+An overview of AppObjects, the VRegistry and Selectors is given in the
+:ref:`VRegistryIntro` chapter.
+
+
+
+The :class:`CWRegistryStore`
+----------------------------
+
+The :class:`CWRegistryStore <cubicweb.cwvreg.CWRegistryStore>` can be
+seen as a two-level dictionary. It contains all dynamically loaded
+objects (subclasses of :class:`AppObject <cubicweb.appobject.AppObject>`)
+to build a |cubicweb| application. Basically:
+
+* the first level key returns a *registry*. This key corresponds to the
+  `__registry__` attribute of application object classes
+
+* the second level key returns a list of application objects which
+  share the same identifier. This key corresponds to the `__regid__`
+  attribute of application object classes.
+
+A *registry* holds a specific kind of application objects. There is
+for instance a registry for entity classes, another for views, etc...
+
+The :class:`CWRegistryStore <cubicweb.cwvreg.CWRegistryStore>` has two
+main responsibilities:
+
+- being the access point to all registries
+
+- handling the registration process at startup time, and during automatic
+  reloading in debug mode.
+
+
+Details of the recording process
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. index::
+   vregistry: registration_callback
+
+On startup, |cubicweb| loads application objects defined in its library
+and in cubes used by the instance. Application objects from the
+library are loaded first, then those provided by cubes are loaded in
+dependency order (e.g. if your cube depends on an other, objects from
+the dependency will be loaded first). The layout of the modules or packages
+in a cube  is explained in :ref:`cubelayout`.
+
+For each module:
+
+* by default all objects are registered automatically
+
+* if some objects have to replace other objects, or have to be
+  included only if some condition is met, you'll have to define a
+  `registration_callback(vreg)` function in your module and explicitly
+  register **all objects** in this module, using the api defined
+  below.
+
+.. Note::
+    Once the function `registration_callback(vreg)` is implemented in a module,
+    all the objects from this module have to be explicitly registered as it
+    disables the automatic objects registration.
+
+
+API for objects registration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here are the registration methods that you can use in the
+`registration_callback` to register your objects to the
+:class:`CWRegistryStore` instance given as argument (usually named
+`vreg`):
+
+- :py:meth:`register_all() <cubicweb.cwvreg.CWRegistryStore.register_all>`
+- :py:meth:`register_and_replace() <cubicweb.cwvreg.CWRegistryStore.register_and_replace>`
+- :py:meth:`register() <cubicweb.cwvreg.CWRegistryStore.register>`
+- :py:meth:`unregister() <logilab.common.registry.RegistryStore.unregister>`
+
+Examples:
+
+.. sourcecode:: python
+
+   # web/views/basecomponents.py
+   def registration_callback(vreg):
+      # register everything in the module except SeeAlsoComponent
+      vreg.register_all(globals().itervalues(), __name__, (SeeAlsoVComponent,))
+      # conditionally register SeeAlsoVComponent
+      if 'see_also' in vreg.schema:
+          vreg.register(SeeAlsoVComponent)
+
+In this example, we register all application object classes defined in the module
+except `SeeAlsoVComponent`. This class is then registered only if the 'see_also'
+relation type is defined in the instance'schema.
+
+.. sourcecode:: python
+
+   # goa/appobjects/sessions.py
+   def registration_callback(vreg):
+      vreg.register(SessionsCleaner)
+      # replace AuthenticationManager by GAEAuthenticationManager
+      vreg.register_and_replace(GAEAuthenticationManager, AuthenticationManager)
+      # replace PersistentSessionManager by GAEPersistentSessionManager
+      vreg.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager)
+
+In this example, we explicitly register classes one by one:
+
+* the `SessionCleaner` class
+* the `GAEAuthenticationManager` to replace the `AuthenticationManager`
+* the `GAEPersistentSessionManager` to replace the `PersistentSessionManager`
+
+If at some point we register a new appobject class in this module, it won't be
+registered at all without modification to the `registration_callback`
+implementation. The previous example will register it though, thanks to the call
+to the `register_all` method.
+
+
+
+Runtime objects selection
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that we have all application objects loaded, the question is : when
+I want some specific object, for instance the primary view for a given
+entity, how do I get the proper object ? This is what we call the
+**selection mechanism**.
+
+As explained in the :ref:`Concepts` section:
+
+* each application object has a **selector**, defined by its
+  `__select__` class attribute
+
+* this selector is responsible to return a **score** for a given context
+
+  - 0 score means the object doesn't apply to this context
+
+  - else, the higher the score, the better the object suits the context
+
+* the object with the highest score is selected.
+
+.. Note::
+
+  When no single object has the highest score, an exception is raised in development
+  mode to let you know that the engine was not able to identify the view to
+  apply. This error is silenced in production mode and one of the objects with
+  the highest score is picked.
+
+  In such cases you would need to review your design and make sure
+  your selectors or appobjects are properly defined. Such an error is
+  typically caused by either forgetting to change the __regid__ in a
+  derived class, or by having copy-pasted some code.
+
+For instance, if you are selecting the primary (`__regid__ =
+'primary'`) view (`__registry__ = 'views'`) for a result set
+containing a `Card` entity, two objects will probably be selectable:
+
+* 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__ = 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 `is_instance('Card')` selector will return a higher
+score since it's more specific, so the correct view will be selected as expected.
+
+
+API for objects selections
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is the selection API you'll get on every registry. Some of them, as the
+'etypes' registry, containing entity classes, extend it. In those methods,
+`*args, **kwargs` is what we call the **context**. Those arguments are given to
+selectors that will inspect their content and return a score accordingly.
+
+:py:meth:`select() <logilab.common.registry.Registry.select>`
+
+:py:meth:`select_or_none() <logilab.common.registry.Registry.select_or_none>`
+
+:py:meth:`possible_objects() <logilab.common.registry.Registry.possible_objects>`
+
+:py:meth:`object_by_id() <logilab.common.registry.Registry.object_by_id>`
+
+
+The `AppObject` class
+---------------------
+
+The :py:class:`cubicweb.appobject.AppObject` class is the base class
+for all dynamically loaded objects (application objects) accessible
+through the :py:class:`cubicweb.cwvreg.CWRegistryStore`.
+
+
+Predicates and selectors
+------------------------
+
+Predicates are scoring functions that are called by the registry to tell whenever
+an appobject can be selected in a given context. Predicates may be chained
+together using operators to build a selector. A selector is the glue that tie
+views to the data model or whatever input context. Using them appropriately is an
+essential part of the construction of well behaved cubes.
+
+Of course you may have to write your own set of predicates as your needs grows
+and you get familiar with the framework (see :ref:`CustomPredicates`).
+
+A predicate is a class testing a particular aspect of a context. A selector is
+built by combining existant predicates or even selectors.
+
+Using and combining existant predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can combine predicates using the `&`, `|` and `~` operators.
+
+When two predicates are combined using the `&` operator, it means that
+both should return a positive score. On success, the sum of scores is
+returned.
+
+When two predicates are combined using the `|` operator, it means that
+one of them should return a positive score. On success, the first
+positive score is returned.
+
+You can also "negate" a predicate by precedeing it by the `~` unary operator.
+
+Of course you can use parenthesis to balance expressions.
+
+Example
+~~~~~~~
+
+The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
+the blog entity itself.
+
+To do that, one defines a method on entity classes that returns the
+RSS stream url for a given entity. The default implementation on
+:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
+as base for all others) and a specific implementation on `Blog` will
+do what we want.
+
+But when we have a result set containing several `Blog` entities (or
+different entities), we don't know on which entity to call the
+aforementioned method. In this case, we keep the generic behaviour.
+
+Hence we have two cases here, one for a single-entity rsets, the other for
+multi-entities rsets.
+
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
+
+.. sourcecode:: python
+
+  class RSSIconBox(box.Box):
+    ''' just display the RSS icon on uniform result set '''
+    __select__ = box.Box.__select__ & non_final_entity()
+
+It takes into account:
+
+* the inherited selection criteria (one has to look them up in the class
+  hierarchy to know the details)
+
+* :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets
+  containing non final entities (a 'final entity' being synonym for entity
+  attributes type, eg `String`, `Int`, etc)
+
+This matches our second case. Hence we have to provide a specific component for
+the first case:
+
+.. sourcecode:: python
+
+  class EntityRSSIconBox(RSSIconBox):
+    '''just display the RSS icon on uniform result set for a single entity'''
+    __select__ = RSSIconBox.__select__ & one_line_rset()
+
+Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which
+filters result sets of size 1. Thus, on a result set containing multiple
+entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
+selectable. However for a result set with one entity, the `EntityRSSIconBox`
+class will have a higher score than `RSSIconBox`, which is what we wanted.
+
+Of course, once this is done, you have to:
+
+* fill in the call method of `EntityRSSIconBox`
+
+* provide the default implementation of the method returning the RSS stream url
+  on :class:`~cubicweb.entities.AnyEntity`
+
+* redefine this method on `Blog`.
+
+
+When to use selectors?
+~~~~~~~~~~~~~~~~~~~~~~
+
+Selectors are to be used whenever arises the need of dispatching on the shape or
+content of a result set or whatever else context (value in request form params,
+authenticated user groups, etc...). That is, almost all the time.
+
+Here is a quick example:
+
+.. sourcecode:: python
+
+    class UserLink(component.Component):
+	'''if the user is the anonymous user, build a link to login else a link
+	to the connected user object with a logout link
+	'''
+	__regid__ = 'loggeduserlink'
+
+	def call(self):
+	    if self._cw.session.anonymous_session:
+		# display login link
+		...
+	    else:
+		# display a link to the connected user object with a loggout link
+		...
+
+The proper way to implement this with |cubicweb| is two have two different
+classes sharing the same identifier but with different selectors so you'll get
+the correct one according to the context.
+
+.. sourcecode:: python
+
+    class UserLink(component.Component):
+	'''display a link to the connected user object with a loggout link'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & authenticated_user()
+
+	def call(self):
+            # display useractions and siteactions
+	    ...
+
+    class AnonUserLink(component.Component):
+	'''build a link to login'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & anonymous_user()
+
+	def call(self):
+	    # display login link
+            ...
+
+The big advantage, aside readability once you're familiar with the
+system, is that your cube becomes much more easily customizable by
+improving componentization.
+
+
+.. _CustomPredicates:
+
+Defining your own predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can use the :py:func:`objectify_predicate <logilab.common.registry.objectify_predicate>`
+decorator to easily write your own predicates as simple python
+functions.
+
+In other cases, you can take a look at the following abstract base classes:
+
+- :py:class:`ExpectedValuePredicate <cubicweb.predicates.ExpectedValuePredicate>`
+- :py:class:`EClassPredicate <cubicweb.predicates.EClassPredicate>`
+- :py:class:`EntityPredicate <cubicweb.predicates.EntityPredicate>`
+
+
+.. _DebuggingSelectors:
+
+Debugging selection
+~~~~~~~~~~~~~~~~~~~
+
+Once in a while, one needs to understand why a view (or any
+application object) is, or is not selected appropriately. Looking at
+which predicates fired (or did not) is the way. The
+:class:`traced_selection <logilab.common.registry.traced_selection>`
+context manager to help with that, *if you're running your instance in
+debug mode*.
+
+
+Base predicates
+---------------
+
+Here is a description of generic predicates provided by CubicWeb that should suit
+most of your needs.
+
+Bare predicates
+~~~~~~~~~~~~~~~
+
+Those predicates are somewhat dumb, which doesn't mean they're not (very) useful.
+
+- :py:class:`yes <cubicweb.appobject.yes>`
+- :py:class:`match_kwargs <cubicweb.predicates.match_kwargs>`
+- :py:class:`appobject_selectable <cubicweb.predicates.appobject_selectable>`
+- :py:class:`adaptable <cubicweb.predicates.adaptable>`
+- :py:class:`configuration_values <cubicweb.predicates.configuration_values>`
+
+
+Result set predicates
+~~~~~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for a result set in the context ('rset' argument or
+the input context) and match or not according to its shape. Some of these
+predicates have different behaviour if a particular cell of the result set is
+specified using 'row' and 'col' arguments of the input context or not.
+
+- :py:class:`none_rset <cubicweb.predicates.none_rset>`
+- :py:class:`any_rset <cubicweb.predicates.any_rset>`
+- :py:class:`nonempty_rset <cubicweb.predicates.nonempty_rset>`
+- :py:class:`empty_rset <cubicweb.predicates.empty_rset>`
+- :py:class:`one_line_rset <cubicweb.predicates.one_line_rset>`
+- :py:class:`multi_lines_rset <cubicweb.predicates.multi_lines_rset>`
+- :py:class:`multi_columns_rset <cubicweb.predicates.multi_columns_rset>`
+- :py:class:`paginated_rset <cubicweb.predicates.paginated_rset>`
+- :py:class:`sorted_rset <cubicweb.predicates.sorted_rset>`
+- :py:class:`one_etype_rset <cubicweb.predicates.one_etype_rset>`
+- :py:class:`multi_etypes_rset <cubicweb.predicates.multi_etypes_rset>`
+
+
+Entity predicates
+~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for either an `entity` argument in the input context,
+or entity found in the result set ('rset' argument or the input context) and
+match or not according to entity's (instance or class) properties.
+
+- :py:class:`non_final_entity <cubicweb.predicates.non_final_entity>`
+- :py:class:`is_instance <cubicweb.predicates.is_instance>`
+- :py:class:`score_entity <cubicweb.predicates.score_entity>`
+- :py:class:`rql_condition <cubicweb.predicates.rql_condition>`
+- :py:class:`relation_possible <cubicweb.predicates.relation_possible>`
+- :py:class:`partial_relation_possible <cubicweb.predicates.partial_relation_possible>`
+- :py:class:`has_related_entities <cubicweb.predicates.has_related_entities>`
+- :py:class:`partial_has_related_entities <cubicweb.predicates.partial_has_related_entities>`
+- :py:class:`has_permission <cubicweb.predicates.has_permission>`
+- :py:class:`has_add_permission <cubicweb.predicates.has_add_permission>`
+- :py:class:`has_mimetype <cubicweb.predicates.has_mimetype>`
+- :py:class:`is_in_state <cubicweb.predicates.is_in_state>`
+- :py:func:`on_fire_transition <cubicweb.predicates.on_fire_transition>`
+
+
+Logged user predicates
+~~~~~~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for properties of the user issuing the request.
+
+- :py:class:`match_user_groups <cubicweb.predicates.match_user_groups>`
+
+
+Web request predicates
+~~~~~~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for properties of *web* request, they can not be
+used on the data repository side.
+
+- :py:class:`no_cnx <cubicweb.predicates.no_cnx>`
+- :py:class:`anonymous_user <cubicweb.predicates.anonymous_user>`
+- :py:class:`authenticated_user <cubicweb.predicates.authenticated_user>`
+- :py:class:`match_form_params <cubicweb.predicates.match_form_params>`
+- :py:class:`match_search_state <cubicweb.predicates.match_search_state>`
+- :py:class:`match_context_prop <cubicweb.predicates.match_context_prop>`
+- :py:class:`match_context <cubicweb.predicates.match_context>`
+- :py:class:`match_view <cubicweb.predicates.match_view>`
+- :py:class:`primary_view <cubicweb.predicates.primary_view>`
+- :py:class:`contextual <cubicweb.predicates.contextual>`
+- :py:class:`specified_etype_implements <cubicweb.predicates.specified_etype_implements>`
+- :py:class:`attribute_edited <cubicweb.predicates.attribute_edited>`
+- :py:class:`match_transition <cubicweb.predicates.match_transition>`
+
+
+Other predicates
+~~~~~~~~~~~~~~~~
+
+- :py:class:`match_exception <cubicweb.predicates.match_exception>`
+- :py:class:`debug_mode <cubicweb.predicates.debug_mode>`
+
+You'll also find some other (very) specific predicates hidden in other modules
+than :mod:`cubicweb.predicates`.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/ajax.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,12 @@
+.. _ajax:
+
+Ajax
+----
+
+CubicWeb provides a few helpers to facilitate *javascript <-> python* communications.
+
+You can, for instance, register some python functions that will become
+callable from javascript through ajax calls. All the ajax URLs are handled
+by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
+
+.. automodule:: cubicweb.web.views.ajaxcontroller
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/controllers.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,93 @@
+.. _controllers:
+
+Controllers
+-----------
+
+Overview
+++++++++
+
+Controllers are responsible for taking action upon user requests
+(loosely following the terminology of the MVC meta pattern).
+
+The following controllers are provided out-of-the box in CubicWeb. We
+list them by category. They are all defined in
+(:mod:`cubicweb.web.views.basecontrollers`).
+
+`Browsing`:
+
+* the View controller is associated with most browsing actions within a
+  CubicWeb application: it always instantiates a
+  :ref:`the_main_template_layout` 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 JSonpController is a wrapper around the ``ViewController`` that
+  provides jsonp_ services. Padding can be specified with the
+  ``callback`` request parameter. Only *jsonexport* / *ejsonexport*
+  views can be used. If another ``vid`` is specified, it will be
+  ignored and replaced by *jsonexport*. Request is anonymized
+  to avoid returning sensitive data and reduce the risks of CSRF attacks;
+
+* the Login/Logout controllers make effective user login or logout
+  requests
+
+
+.. _jsonp: http://en.wikipedia.org/wiki/JSONP
+
+`Edition`:
+
+* the Edit controller (see :ref:`edit_controller`) handles CRUD
+  operations in response to a form being submitted; it works in close
+  association with the Forms, to which it delegates some of the work
+
+* the ``Form validator controller`` provides form validation from Ajax
+  context, using the Edit controller, to implement the classic form
+  handling loop (user edits, hits `submit/apply`, validation occurs
+  server-side by way of the Form validator controller, and the UI is
+  decorated with failure information, either global or per-field ,
+  until it is valid)
+
+`Other`:
+
+* the ``SendMail controller`` (web/views/basecontrollers.py) is reponsible
+  for outgoing email notifications
+
+* the MailBugReport controller (web/views/basecontrollers.py) allows
+  to quickly have a `reportbug` feature in one's application
+
+* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`
+  (:mod:`cubicweb.web.views.ajaxcontroller`) provides
+  services for Ajax calls, typically using JSON as a serialization format
+  for input, and sometimes using either JSON or XML for output. See
+  :ref:`ajax` chapter for more information.
+
+
+Registration
+++++++++++++
+
+All controllers (should) live in the 'controllers' namespace within
+the global registry.
+
+Concrete controllers
+++++++++++++++++++++
+
+Most API details should be resolved by source code inspection, as the
+various controllers have differing goals. See for instance the
+:ref:`edit_controller` chapter.
+
+:mod:`cubicweb.web.controller` contains the top-level abstract
+Controller class and its unimplemented entry point
+`publish(rset=None)` method.
+
+A handful of helpers are also provided there:
+
+* process_rql builds a result set from an rql query typically issued
+  from the browser (and available through _cw.form['rql'])
+
+* validate_cache will force cache validation handling with respect to
+  the HTTP Cache directives (that were typically originally issued
+  from a previous server -> client response); concrete Controller
+  implementations dealing with HTTP (thus, for instance, not the
+  SendMail controller) may very well call this in their publication
+  process.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/css.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,30 @@
+.. -*- coding: utf-8 -*-
+
+CSS Stylesheet
+---------------
+Conventions
+~~~~~~~~~~~
+
+.. XXX external_resources variable
+..    naming convention
+..    request.add_css
+
+
+Extending / overriding existing styles
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We cannot modify the order in which the application is reading the CSS. In
+the case we want to create new CSS style, the best is to define it a in a new
+CSS located under ``myapp/data/`` and use those new styles while writing
+customized views and templates.
+
+If you want to modify an existing CSS styling property, you will have to use
+``!important`` declaration to override the existing property. The application
+apply a higher priority on the default CSS and you can not change that.
+Customized CSS will not be read first.
+
+
+CubicWeb stylesheets
+~~~~~~~~~~~~~~~~~~~~
+
+.. XXX explain diffenrent files and main classes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/dissection.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,316 @@
+
+.. _form_dissection:
+
+Dissection of an entity form
+----------------------------
+
+This is done (again) with a vanilla instance of the `tracker`_
+cube. We will populate the database with a bunch of entities and see
+what kind of job the automatic entity form does.
+
+.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
+
+Populating the database
+~~~~~~~~~~~~~~~~~~~~~~~
+
+We should start by setting up a bit of context: a project with two
+unpublished versions, and a ticket linked to the project and the first
+version.
+
+.. sourcecode:: python
+
+ >>> p = rql('INSERT Project P: P name "cubicweb"')
+ >>> for num in ('0.1.0', '0.2.0'):
+ ...  rql('INSERT Version V: V num "%s", V version_of P WHERE P eid %%(p)s' % num, {'p': p[0][0]})
+ ...
+ <resultset 'INSERT Version V: V num "0.1.0", V version_of P WHERE P eid %(p)s' (1 rows): [765L] (('Version',))>
+ <resultset 'INSERT Version V: V num "0.2.0", V version_of P WHERE P eid %(p)s' (1 rows): [766L] (('Version',))>
+ >>> t = rql('INSERT Ticket T: T title "let us write more doc", T done_in V, '
+             'T concerns P WHERE V num "0.1.0"', P eid %(p)s', {'p': p[0][0]})
+ >>> commit()
+
+Now let's see what the edition form builds for us.
+
+.. sourcecode:: python
+
+ >>> cnx.use_web_compatible_requests('http://fakeurl.com')
+ >>> req = cnx.request()
+ >>> form = req.vreg['forms'].select('edition', req, rset=rql('Ticket T'))
+ >>> html = form.render()
+
+.. note::
+
+  In order to play interactively with web side application objects, we have to
+  cheat a bit to have request object that will looks like HTTP request object, by
+  calling :meth:`use_web_compatible_requests()` on the connection.
+
+This creates an automatic entity form. The ``.render()`` call yields
+an html (unicode) string. The html output is shown below (with
+internal fieldset omitted).
+
+Looking at the html output
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The form enveloppe
+''''''''''''''''''
+
+.. sourcecode:: html
+
+ <div class="iformTitle"><span>main informations</span></div>
+ <div class="formBody">
+  <form action="http://crater:9999/validateform" method="post" enctype="application/x-www-form-urlencoded"
+        id="entityForm" onsubmit="return freezeFormButtons(&#39;entityForm&#39;);"
+        class="entityForm" target="eformframe">
+    <div id="progress">validating...</div>
+    <fieldset>
+      <input name="__form_id" type="hidden" value="edition" />
+      <input name="__errorurl" type="hidden" value="http://perdu.com#entityForm" />
+      <input name="__domid" type="hidden" value="entityForm" />
+      <input name="__type:763" type="hidden" value="Ticket" />
+      <input name="eid" type="hidden" value="763" />
+      <input name="__maineid" type="hidden" value="763" />
+      <input name="_cw_edited_fields:763" type="hidden"
+             value="concerns-subject,done_in-subject,priority-subject,type-subject,title-subject,description-subject,__type,_cw_generic_field" />
+      ...
+    </fieldset>
+    <iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0);"></iframe>
+   </form>
+ </div>
+
+The main fieldset encloses a set of hidden fields containing various
+metadata, that will be used by the `edit controller` to process it
+back correctly.
+
+The `freezeFormButtons(...)` javascript callback defined on the
+``onlick`` event of the form element prevents accidental multiple
+clicks in a row.
+
+The ``action`` of the form is mapped to the ``validateform`` controller
+(situated in :mod:`cubicweb.web.views.basecontrollers`).
+
+A full explanation of the validation loop is given in
+:ref:`validation_process`.
+
+.. _attributes_section:
+
+The attributes section
+''''''''''''''''''''''
+
+We can have a look at some of the inner nodes of the form. Some fields
+are omitted as they are redundant for our purposes.
+
+.. sourcecode:: html
+
+      <fieldset class="default">
+        <table class="attributeForm">
+          <tr class="title_subject_row">
+            <th class="labelCol"><label class="required" for="title-subject:763">title</label></th>
+            <td>
+              <input id="title-subject:763" maxlength="128" name="title-subject:763" size="45"
+                     tabindex="1" type="text" value="let us write more doc" />
+            </td>
+          </tr>
+          ... (description field omitted) ...
+          <tr class="priority_subject_row">
+            <th class="labelCol"><label class="required" for="priority-subject:763">priority</label></th>
+            <td>
+              <select id="priority-subject:763" name="priority-subject:763" size="1" tabindex="4">
+                <option value="important">important</option>
+                <option selected="selected" value="normal">normal</option>
+                <option value="minor">minor</option>
+              </select>
+              <div class="helper">importance</div>
+            </td>
+          </tr>
+          ... (type field omitted) ...
+          <tr class="concerns_subject_row">
+            <th class="labelCol"><label class="required" for="concerns-subject:763">concerns</label></th>
+            <td>
+              <select id="concerns-subject:763" name="concerns-subject:763" size="1" tabindex="6">
+                <option selected="selected" value="760">Foo</option>
+              </select>
+            </td>
+          </tr>
+          <tr class="done_in_subject_row">
+            <th class="labelCol"><label for="done_in-subject:763">done in</label></th>
+            <td>
+              <select id="done_in-subject:763" name="done_in-subject:763" size="1" tabindex="7">
+                <option value="__cubicweb_internal_field__"></option>
+                <option selected="selected" value="761">Foo 0.1.0</option>
+                <option value="762">Foo 0.2.0</option>
+              </select>
+              <div class="helper">version in which this ticket will be / has been  done</div>
+            </td>
+          </tr>
+        </table>
+      </fieldset>
+
+
+Note that the whole form layout has been computed by the form
+renderer. It is the renderer which produces the table
+structure. Otherwise, the fields html structure is emitted by their
+associated widget.
+
+While it is called the `attributes` section of the form, it actually
+contains attributes and *mandatory relations*. For each field, we
+observe:
+
+* a dedicated row with a specific class, such as ``title_subject_row``
+  (responsability of the form renderer)
+
+* an html widget (input, select, ...) with:
+
+  * an id built from the ``rtype-role:eid`` pattern
+
+  * a name built from the same pattern
+
+  * possible values or preselected options
+
+The relations section
+'''''''''''''''''''''
+
+.. sourcecode:: html
+
+      <fieldset class="This ticket :">
+        <legend>This ticket :</legend>
+        <table class="attributeForm">
+          <tr class="_cw_generic_field_None_row">
+            <td colspan="2">
+              <table id="relatedEntities">
+                <tr><th>&#160;</th><td>&#160;</td></tr>
+                <tr id="relationSelectorRow_763" class="separator">
+                  <th class="labelCol">
+                    <select id="relationSelector_763" tabindex="8"
+                            onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,763);">
+                      <option value="">select a relation</option>
+                      <option value="appeared_in_subject">appeared in</option>
+                      <option value="custom_workflow_subject">custom workflow</option>
+                      <option value="depends_on_object">dependency of</option>
+                      <option value="depends_on_subject">depends on</option>
+                      <option value="identical_to_subject">identical to</option>
+                      <option value="see_also_subject">see also</option>
+                    </select>
+                  </th>
+                  <td id="unrelatedDivs_763"></td>
+                </tr>
+              </table>
+            </td>
+          </tr>
+        </table>
+      </fieldset>
+
+The optional relations are grouped into a drop-down combo
+box. Selection of an item triggers a javascript function which will:
+
+* show already related entities in the div of id `relatedentities`
+  using a two-colown layout, with an action to allow deletion of
+  individual relations (there are none in this example)
+
+* provide a relation selector in the div of id `relationSelector_EID`
+  to allow the user to set up relations and trigger dynamic action on
+  the last div
+
+* fill the div of id `unrelatedDivs_EID` with a dynamically computed
+  selection widget allowing direct selection of an unrelated (but
+  relatable) entity or a switch towards the `search mode` of
+  |cubicweb| which allows full browsing and selection of an entity
+  using a dedicated action situated in the left column boxes.
+
+
+The buttons zone
+''''''''''''''''
+
+Finally comes the buttons zone.
+
+.. sourcecode:: html
+
+      <table width="100%">
+        <tbody>
+          <tr>
+            <td align="center">
+              <button class="validateButton" tabindex="9" type="submit" value="validate">
+                <img alt="OK_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/ok.png" />
+                validate
+              </button>
+            </td>
+            <td style="align: right; width: 50%;">
+              <button class="validateButton"
+                      onclick="postForm(&#39;__action_apply&#39;, &#39;button_apply&#39;, &#39;entityForm&#39;)"
+                      tabindex="10" type="button" value="apply">
+                <img alt="APPLY_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/plus.png" />
+                apply
+              </button>
+              <button class="validateButton"
+                      onclick="postForm(&#39;__action_cancel&#39;, &#39;button_cancel&#39;, &#39;entityForm&#39;)"
+                      tabindex="11" type="button" value="cancel">
+                <img alt="CANCEL_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/cancel.png" />
+                cancel
+              </button>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+The most notable artifacts here are the ``postForm(...)`` calls
+defined on click events on these buttons. This function basically
+submits the form.
+
+.. _validation_process:
+
+The form validation process
+---------------------------
+
+Validation loop
+~~~~~~~~~~~~~~~
+
+On form submission, the form.action is invoked. Basically, the
+``validateform`` controller is called and its output lands in the
+specified ``target``, an invisible ``<iframe>`` at the end of the
+form.
+
+Hence, the main page is not replaced, only the iframe contents. The
+``validateform`` controller only outputs a tiny javascript fragment
+which is then immediately executed.
+
+.. sourcecode:: html
+
+ <iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0);">
+   <script type="text/javascript">
+     window.parent.handleFormValidationResponse('entityForm', null, null,
+                                                [false, [2164, {"name-subject": "required field"}], null],
+                                                null);
+   </script>
+ </iframe>
+
+The ``window.parent`` part ensures the javascript function is called
+on the right context (that is: the form element). We will describe its
+parameters:
+
+* first comes the form id (`entityForm`)
+
+* then two optional callbacks for the success and failure case
+
+* an array containing:
+
+  * a boolean which indicates status (success or failure), and then, on error:
+
+    * an array structured as ``[eid, {'rtype-role': 'error msg'}, ...]``
+
+  * on success:
+
+    * a url (string) representing the next thing to jump to
+
+Given the array structure described above, it is quite simple to
+manipulate the DOM to show the errors at appropriate places.
+
+Explanation
+~~~~~~~~~~~
+
+This mecanism may seem a bit overcomplicated but we have to deal with
+two realities:
+
+* in the (strict) XHTML world, there are no iframes (hence the dynamic
+  inclusion, tolerated by Firefox)
+
+* no (or not all) browser(s) support file input field handling through
+  ajax.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/editcontroller.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,113 @@
+.. _edit_controller:
+
+The `edit controller`
+---------------------
+
+It can be found in (:mod:`cubicweb.web.views.editcontroller`). This
+controller processes data received from an html form to create or
+update entities.
+
+Edition handling
+~~~~~~~~~~~~~~~~
+
+The parameters related to entities to edit are specified as follows
+(first seen in :ref:`attributes_section`)::
+
+  <rtype-role>:<entity eid>
+
+where entity eid could be a letter in case of an entity to create. We
+name those parameters as *qualified*.
+
+* Retrieval of entities to edit is done by using the forms parameters
+  `eid` and `__type`
+
+* For all the attributes and the relations of an entity to edit
+  (attributes and relations are handled a bit differently but these
+  details are not much relevant here) :
+
+   * using the ``rtype``, ``role`` and ``__type`` information, fetch
+     an appropriate field instance
+
+   * check if the field has been modified (if not, proceed to the next
+     relation)
+
+   * build an rql expression to update the entity
+
+At the end, all rql expressions are executed.
+
+* For each entity to edit:
+
+   * if a qualified parameter `__linkto` is specified, its value has
+     to be a string (or a list of strings) such as: ::
+
+        <relation type>:<eids>:<target>
+
+     where <target> is either `subject` or `object` and each eid could
+     be separated from the others by a `_`. Target specifies if the
+     *edited entity* is subject or object of the relation and each
+     relation specified will be inserted.
+
+    * if a qualified parameter `__clone_eid` is specified for an entity, the
+      relations of the specified entity passed as value of this parameter are
+      copied on the edited entity.
+
+    * if a qualified parameter `__delete` is specified, its value must be
+      a string or a list of string such as follows: ::
+
+          <subjects eids>:<relation type>:<objects eids>
+
+      where each eid subject or object can be seperated from the other
+      by `_`. Each specified relation will be deleted.
+
+
+* If no entity is edited but the form contains the parameters `__linkto`
+  and `eid`, this one is interpreted by using the value specified for `eid`
+  to designate the entity on which to add the relations.
+
+.. note::
+
+   * if the parameter `__action_delete` is found, all the entities specified
+     as to be edited will be deleted.
+
+   * if the parameter `__action_cancel` is found, no action is completed.
+
+   * if the parameter `__action_apply` is found, the editing is
+     applied normally but the redirection is done on the form (see
+     :ref:`RedirectionControl`).
+
+   * if no entity is found to be edited and if there is no parameter
+     `__action_delete`, `__action_cancel`, `__linkto`, `__delete` or
+     `__insert`, an error is raised.
+
+   * using the parameter `__message` in the form will allow to use its value
+     as a message to provide the user once the editing is completed.
+
+
+.. _RedirectionControl:
+
+Redirection control
+~~~~~~~~~~~~~~~~~~~
+Once editing is completed, there is still an issue left: where should we go
+now? If nothing is specified, the controller will do his job but it does not
+mean we will be happy with the result. We can control that by using the
+following parameters:
+
+* `__redirectpath`: path of the URL (relative to the root URL of the site,
+  no form parameters
+
+* `__redirectparams`: forms parameters to add to the path
+
+* `__redirectrql`: redirection RQL request
+
+* `__redirectvid`: redirection view identifier
+
+* `__errorurl`: initial form URL, used for redirecting in case a validation
+  error is raised during editing. If this one is not specified, an error page
+  is displayed instead of going back to the form (which is, if necessary,
+  responsible for displaying the errors)
+
+* `__form_id`: initial view form identifier, used if `__action_apply` is
+  found
+
+In general we use either `__redirectpath` and `__redirectparams` or
+`__redirectrql` and `__redirectvid`.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/examples.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,232 @@
+Examples
+--------
+
+(Automatic) Entity form
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Looking at some cubes available on the `cubicweb forge`_ we find some
+with form manipulation. The following example comes from the the
+`conference`_ cube. It extends the change state form for the case
+where a ``Talk`` entity is getting into ``submitted`` state. The goal
+is to select reviewers for the submitted talk.
+
+.. _`cubicweb forge`: http://www.cubicweb.org/view?rql=Any+P+ORDERBY+N+WHERE+P+name+LIKE+%22cubicweb-%25%22%2C+P+is+Project%2C+P+name+N
+.. _`conference`: http://www.cubicweb.org/project/cubicweb-conference
+
+.. sourcecode:: python
+
+ from cubicweb.web import formfields as ff, formwidgets as fwdgs
+ class SendToReviewerStatusChangeView(ChangeStateFormView):
+     __select__ = (ChangeStateFormView.__select__ &
+                   is_instance('Talk') &
+                   rql_condition('X in_state S, S name "submitted"'))
+
+     def get_form(self, entity, transition, **kwargs):
+         form = super(SendToReviewerStatusChangeView, self).get_form(entity, transition, **kwargs)
+         relation = ff.RelationField(name='reviews', role='object',
+                                     eidparam=True,
+                                     label=_('select reviewers'),
+                                     widget=fwdgs.Select(multiple=True))
+         form.append_field(relation)
+         return form
+
+Simple extension of a form can be done from within the `FormView`
+wrapping the form. FormView instances have a handy ``get_form`` method
+that returns the form to be rendered. Here we add a ``RelationField``
+to the base state change form.
+
+One notable point is the ``eidparam`` argument: it tells both the
+field and the ``edit controller`` that the field is linked to a
+specific entity.
+
+It is hence entirely possible to add ad-hoc fields that will be
+processed by some specialized instance of the edit controller.
+
+
+Ad-hoc fields form
+~~~~~~~~~~~~~~~~~~
+
+We want to define a form doing something else than editing an entity. The idea is
+to propose a form to send an email to entities in a resultset which implements
+:class:`IEmailable`.  Let's take a simplified version of what you'll find in
+:mod:`cubicweb.web.views.massmailing`.
+
+Here is the source code:
+
+.. sourcecode:: python
+
+    def sender_value(form, field):
+	return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
+
+    def recipient_choices(form, field):
+	return [(e.get_email(), e.eid)
+                 for e in form.cw_rset.entities()
+		 if e.get_email()]
+
+    def recipient_value(form, field):
+	return [e.eid for e in form.cw_rset.entities()
+                if e.get_email()]
+
+    class MassMailingForm(forms.FieldsForm):
+	__regid__ = 'massmailing'
+
+	needs_js = ('cubicweb.widgets.js',)
+	domid = 'sendmail'
+	action = 'sendmail'
+
+	sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
+				label=_('From:'),
+				value=sender_value)
+
+	recipient = ff.StringField(widget=CheckBox(),
+	                           label=_('Recipients:'),
+				   choices=recipient_choices,
+				   value=recipients_value)
+
+	subject = ff.StringField(label=_('Subject:'), max_length=256)
+
+	mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
+						    inputid='mailbody'))
+
+	form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
+				  _('send email'), 'SEND_EMAIL_ICON'),
+			ImgButton('cancelbutton', "javascript: history.back()",
+				  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
+
+Let's detail what's going on up there. Our form will hold four fields:
+
+* a sender field, which is disabled and will simply contains the user's name and
+  email
+
+* a recipients field, which will be displayed as a list of users in the context
+  result set with checkboxes so user can still choose who will receive his mailing
+  by checking or not the checkboxes. By default all of them will be checked since
+  field's value return a list containing same eids as those returned by the
+  vocabulary function.
+
+* a subject field, limited to 256 characters (hence we know a
+  :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
+  :class:`~cubicweb.web.formfields.StringField`)
+
+* a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
+  and whose definition won't be shown here. Notice though that we tell this form
+  need this javascript file by using `needs_js`
+
+Last but not least, we add two buttons control: one to post the form using
+javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
+set to 'sendmail', which is our form DOM id as specified by its `domid`
+attribute), another to cancel the form which will go back to the previous page
+using another javascript call. Also we specify an image to use as button icon as a
+resource identifier (see :ref:`uiprops`) given as last argument to
+:class:`cubicweb.web.formwidgets.ImgButton`.
+
+To see this form, we still have to wrap it in a view. This is pretty simple:
+
+.. sourcecode:: python
+
+    class MassMailingFormView(form.FormViewMixIn, EntityView):
+	__regid__ = 'massmailing'
+	__select__ = is_instance(IEmailable) & authenticated_user()
+
+	def call(self):
+	    form = self._cw.vreg['forms'].select('massmailing', self._cw,
+	                                         rset=self.cw_rset)
+	    form.render(w=self.w)
+
+As you see, we simply define a view with proper selector so it only apply to a
+result set containing :class:`IEmailable` entities, and so that only users in the
+managers or users group can use it. Then in the `call()` method for this view we
+simply select the above form and call its `.render()` method with our output
+stream as argument.
+
+When this form is submitted, a controller with id 'sendmail' will be called (as
+specified using `action`). This controller will be responsible to actually send
+the mail to specified recipients.
+
+Here is what it looks like:
+
+.. sourcecode:: python
+
+   class SendMailController(Controller):
+       __regid__ = 'sendmail'
+       __select__ = (authenticated_user() &
+                     match_form_params('recipient', 'mailbody', 'subject'))
+
+       def publish(self, rset=None):
+           body = self._cw.form['mailbody']
+           subject = self._cw.form['subject']
+           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)))
+           recipients = list(rset.entities())
+           msg = format_mail({'email' : self._cw.user.get_email(),
+                              'name' : self._cw.user.dc_title()},
+                             recipients, body, subject)
+           if not self._cw.vreg.config.sendmails([(msg, recipients)]):
+               msg = self._cw._('could not connect to the SMTP server')
+           else:
+               msg = self._cw._('emails successfully sent')
+           raise Redirect(self._cw.build_url(__message=msg))
+
+
+The entry point of a controller is the publish method. In that case we simply get
+back post values in request's `form` attribute, get user instances according
+to eids found in the 'recipient' form value, and send email after calling
+:func:`format_mail` to get a proper email message. If we can't send email or
+if we successfully sent email, we redirect to the index page with proper message
+to inform the user.
+
+Also notice that our controller has a selector that deny access to it
+to anonymous users (we don't want our instance to be used as a spam
+relay), but also checks if the expected parameters are specified in
+forms. That avoids later defensive programming (though it's not enough
+to handle all possible error cases).
+
+To conclude our example, suppose we wish a different form layout and that existent
+renderers are not satisfying (we would check that first of course :). We would then
+have to define our own renderer:
+
+.. sourcecode:: python
+
+    class MassMailingFormRenderer(formrenderers.FormRenderer):
+        __regid__ = 'massmailing'
+
+        def _render_fields(self, fields, w, form):
+            w(u'<table class="headersform">')
+            for field in fields:
+                if field.name == 'mailbody':
+                    w(u'</table>')
+                    w(u'<div id="toolbar">')
+                    w(u'<ul>')
+                    for button in form.form_buttons:
+                        w(u'<li>%s</li>' % button.render(form))
+                    w(u'</ul>')
+                    w(u'</div>')
+                    w(u'<div>')
+                    w(field.render(form, self))
+                    w(u'</div>')
+                else:
+                    w(u'<tr>')
+                    w(u'<td class="hlabel">%s</td>' %
+                      self.render_label(form, field))
+                    w(u'<td class="hvalue">')
+                    w(field.render(form, self))
+                    w(u'</td></tr>')
+
+        def render_buttons(self, w, form):
+            pass
+
+We simply override the `_render_fields` and `render_buttons` method of the base form renderer
+to arrange fields as we desire it: here we'll have first a two columns table with label and
+value of the sender, recipients and subject field (form order respected), then form controls,
+then a div containing the textarea for the email's content.
+
+To bind this renderer to our form, we should add to our form definition above:
+
+.. sourcecode:: python
+
+    form_renderer_id = 'massmailing'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/form.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,377 @@
+.. _webform:
+
+HTML form construction
+----------------------
+
+CubicWeb provides the somewhat usual form / field / widget / renderer abstraction
+to provide generic building blocks which will greatly help you in building forms
+properly integrated with CubicWeb (coherent display, error handling, etc...),
+while keeping things as flexible as possible.
+
+A ``form`` basically only holds a set of ``fields``, and has te be bound to a
+``renderer`` which is responsible to layout them. Each field is bound to a
+``widget`` that will be used to fill in value(s) for that field (at form
+generation time) and 'decode' (fetch and give a proper Python type to) values
+sent back by the browser.
+
+The ``field`` should be used according to the type of what you want to edit.
+E.g. if you want to edit some date, you'll have to use the
+:class:`cubicweb.web.formfields.DateField`. Then you can choose among multiple
+widgets to edit it, for instance :class:`cubicweb.web.formwidgets.TextInput` (a
+bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple
+calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery
+calendar).  You can of course also write your own widget.
+
+Exploring the available forms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A small excursion into a |cubicweb| shell is the quickest way to
+discover available forms (or application objects in general).
+
+.. sourcecode:: python
+
+ >>> from pprint import pprint
+ >>> pprint( session.vreg['forms'] )
+ {'base': [<class 'cubicweb.web.views.forms.FieldsForm'>,
+           <class 'cubicweb.web.views.forms.EntityFieldsForm'>],
+  'changestate': [<class 'cubicweb.web.views.workflow.ChangeStateForm'>,
+                  <class 'cubes.tracker.views.forms.VersionChangeStateForm'>],
+  'composite': [<class 'cubicweb.web.views.forms.CompositeForm'>,
+                <class 'cubicweb.web.views.forms.CompositeEntityForm'>],
+  'deleteconf': [<class 'cubicweb.web.views.editforms.DeleteConfForm'>],
+  'edition': [<class 'cubicweb.web.views.autoform.AutomaticEntityForm'>,
+              <class 'cubicweb.web.views.workflow.TransitionEditionForm'>,
+              <class 'cubicweb.web.views.workflow.StateEditionForm'>],
+  'logform': [<class 'cubicweb.web.views.basetemplates.LogForm'>],
+  'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>],
+  'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>],
+  'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]}
+
+
+The two most important form families here (for all practical purposes) are `base`
+and `edition`. Most of the time one wants alterations of the
+:class:`AutomaticEntityForm` to generate custom forms to handle edition of an
+entity.
+
+The Automatic Entity Form
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: cubicweb.web.views.autoform
+
+Anatomy of a choices function
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Let's have a look at the `ticket_done_in_choices` function given to
+the `choices` parameter of the relation tag that is applied to the
+('Ticket', 'done_in', '*') relation definition, as it is both typical
+and sophisticated enough. This is a code snippet from the `tracker`_
+cube.
+
+.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
+
+The ``Ticket`` entity type can be related to a ``Project`` and a
+``Version``, respectively through the ``concerns`` and ``done_in``
+relations. When a user is about to edit a ticket, we want to fill the
+combo box for the ``done_in`` relation with values pertinent with
+respect to the context. The important context here is:
+
+* creation or modification (we cannot fetch values the same way in
+  either case)
+
+* ``__linkto`` url parameter given in a creation context
+
+.. sourcecode:: python
+
+    from cubicweb.web import formfields
+
+    def ticket_done_in_choices(form, field):
+        entity = form.edited_entity
+        # first see if its specified by __linkto form parameters
+        linkedto = form.linked_to[('done_in', 'subject')]
+        if linkedto:
+            return linkedto
+        # it isn't, get initial values
+        vocab = field.relvoc_init(form)
+        veid = None
+        # try to fetch the (already or pending) related version and project
+        if not entity.has_eid():
+            peids = form.linked_to[('concerns', 'subject')]
+            peid = peids and peids[0]
+        else:
+            peid = entity.project.eid
+            veid = entity.done_in and entity.done_in[0].eid
+        if peid:
+            # we can complete the vocabulary with relevant values
+            rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
+            rset = form._cw.execute(
+                'Any V, VN ORDERBY version_sort_value(VN) '
+                'WHERE V version_of P, P eid %(p)s, V num VN, '
+                'V in_state ST, NOT ST name "published"', {'p': peid}, 'p')
+            vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
+                      if rschema.has_perm(form._cw, 'add', toeid=v.eid)
+                      and v.eid != veid]
+        return vocab
+
+The first thing we have to do is fetch potential values from the ``__linkto`` url
+parameter that is often found in entity creation contexts (the creation action
+provides such a parameter with a predetermined value; for instance in this case,
+ticket creation could occur in the context of a `Version` entity). The
+:class:`~cubicweb.web.formfields.RelationField` field class provides a
+:meth:`~cubicweb.web.formfields.RelationField.relvoc_linkedto` method that gets a
+list suitably filled with vocabulary values.
+
+.. sourcecode:: python
+
+        linkedto = field.relvoc_linkedto(form)
+        if linkedto:
+            return linkedto
+
+Then, if no ``__linkto`` argument was given, we must prepare the vocabulary with
+an initial empty value (because `done_in` is not mandatory, we must allow the
+user to not select a verson) and already linked values. This is done with the
+:meth:`~cubicweb.web.formfields.RelationField.relvoc_init` method.
+
+.. sourcecode:: python
+
+        vocab = field.relvoc_init(form)
+
+But then, we have to give more: if the ticket is related to a project,
+we should provide all the non published versions of this project
+(`Version` and `Project` can be related through the `version_of`
+relation). Conversely, if we do not know yet the project, it would not
+make sense to propose all existing versions as it could potentially
+lead to incoherences. Even if these will be caught by some
+RQLConstraint, it is wise not to tempt the user with error-inducing
+candidate values.
+
+The "ticket is related to a project" part must be decomposed as:
+
+* this is a new ticket which is created is the context of a project
+
+* this is an already existing ticket, linked to a project (through the
+  `concerns` relation)
+
+* there is no related project (quite unlikely given the cardinality of
+  the `concerns` relation, so it can only mean that we are creating a
+  new ticket, and a project is about to be selected but there is no
+  ``__linkto`` argument)
+
+.. note::
+
+   the last situation could happen in several ways, but of course in a
+   polished application, the paths to ticket creation should be
+   controlled so as to avoid a suboptimal end-user experience
+
+Hence, we try to fetch the related project.
+
+.. sourcecode:: python
+
+        veid = None
+        if not entity.has_eid():
+            peids = form.linked_to[('concerns', 'subject')]
+            peid = peids and peids[0]
+        else:
+            peid = entity.project.eid
+            veid = entity.done_in and entity.done_in[0].eid
+
+We distinguish between entity creation and entity modification using
+the ``Entity.has_eid()`` method, which returns `False` on creation. At
+creation time the only way to get a project is through the
+``__linkto`` parameter. Notice that we fetch the version in which the
+ticket is `done_in` if any, for later.
+
+.. note::
+
+  the implementation above assumes that if there is a ``__linkto``
+  parameter, it is only about a project. While it makes sense most of
+  the time, it is not an absolute. Depending on how an entity creation
+  action action url is built, several outcomes could be possible
+  there
+
+If the ticket is already linked to a project, fetching it is
+trivial. Then we add the relevant version to the initial vocabulary.
+
+.. sourcecode:: python
+
+        if peid:
+            rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
+            rset = form._cw.execute(
+                'Any V, VN ORDERBY version_sort_value(VN) '
+                'WHERE V version_of P, P eid %(p)s, V num VN, '
+                'V in_state ST, NOT ST name "published"', {'p': peid})
+            vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
+                      if rschema.has_perm(form._cw, 'add', toeid=v.eid)
+                      and v.eid != veid]
+
+.. warning::
+
+   we have to defend ourselves against lack of a project eid. Given
+   the cardinality of the `concerns` relation, there *must* be a
+   project, but this rule can only be enforced at validation time,
+   which will happen of course only after form subsmission
+
+Here, given a project eid, we complete the vocabulary with all
+unpublished versions defined in the project (sorted by number) for
+which the current user is allowed to establish the relation.
+
+
+Building self-posted form with custom fields/widgets
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes you want a form that is not related to entity edition. For those,
+you'll have to handle form posting by yourself. Here is a complete example on how
+to achieve this (and more).
+
+Imagine you want a form that selects a month period. There are no proper
+field/widget to handle this in CubicWeb, so let's start by defining them:
+
+.. sourcecode:: python
+
+    # let's have the whole import list at the beginning, even those necessary for
+    # subsequent snippets
+    from logilab.common import date
+    from logilab.mtconverter import xml_escape
+    from cubicweb.view import View
+    from cubicweb.predicates import match_kwargs
+    from cubicweb.web import RequestError, ProcessFormError
+    from cubicweb.web import formfields as fields, formwidgets as wdgs
+    from cubicweb.web.views import forms, calendar
+
+    class MonthSelect(wdgs.Select):
+        """Custom widget to display month and year. Expect value to be given as a
+        date instance.
+        """
+
+        def format_value(self, form, field, value):
+            return u'%s/%s' % (value.year, value.month)
+
+        def process_field_data(self, form, field):
+            val = super(MonthSelect, self).process_field_data(form, field)
+            try:
+                year, month = val.split('/')
+                year = int(year)
+                month = int(month)
+                return date.date(year, month, 1)
+            except ValueError:
+                raise ProcessFormError(
+                    form._cw._('badly formated date string %s') % val)
+
+
+    class MonthPeriodField(fields.CompoundField):
+        """custom field composed of two subfields, 'begin_month' and 'end_month'.
+
+        It expects to be used on form that has 'mindate' and 'maxdate' in its
+        extra arguments, telling the range of month to display.
+        """
+
+        def __init__(self, *args, **kwargs):
+            kwargs.setdefault('widget', wdgs.IntervalWidget())
+            super(MonthPeriodField, self).__init__(
+                [fields.StringField(name='begin_month',
+                                    choices=self.get_range, sort=False,
+                                    value=self.get_mindate,
+                                    widget=MonthSelect()),
+                 fields.StringField(name='end_month',
+                                    choices=self.get_range, sort=False,
+                                    value=self.get_maxdate,
+                                    widget=MonthSelect())], *args, **kwargs)
+
+        @staticmethod
+        def get_range(form, field):
+            mindate = date.todate(form.cw_extra_kwargs['mindate'])
+            maxdate = date.todate(form.cw_extra_kwargs['maxdate'])
+            assert mindate <= maxdate
+            _ = form._cw._
+            months = []
+            while mindate <= maxdate:
+                label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]),
+                                   mindate.year)
+                value = field.widget.format_value(form, field, mindate)
+                months.append( (label, value) )
+                mindate = date.next_month(mindate)
+            return months
+
+        @staticmethod
+        def get_mindate(form, field):
+            return form.cw_extra_kwargs['mindate']
+
+        @staticmethod
+        def get_maxdate(form, field):
+            return form.cw_extra_kwargs['maxdate']
+
+        def process_posted(self, form):
+            for field, value in super(MonthPeriodField, self).process_posted(form):
+                if field.name == 'end_month':
+                    value = date.last_day(value)
+                yield field, value
+
+
+Here we first define a widget that will be used to select the beginning and the
+end of the period, displaying months like '<month> YYYY' but using 'YYYY/mm' as
+actual value.
+
+We then define a field that will actually hold two fields, one for the beginning
+and another for the end of the period. Each subfield uses the widget we defined
+earlier, and the outer field itself uses the standard
+:class:`IntervalWidget`. The field adds some logic:
+
+* a vocabulary generation function `get_range`, used to populate each sub-field
+
+* two 'value' functions `get_mindate` and `get_maxdate`, used to tell to
+  subfields which value they should consider on form initialization
+
+* overriding of `process_posted`, called when the form is being posted, so that
+  the end of the period is properly set to the last day of the month.
+
+Now, we can define a very simple form:
+
+.. sourcecode:: python
+
+    class MonthPeriodSelectorForm(forms.FieldsForm):
+        __regid__ = 'myform'
+        __select__ = match_kwargs('mindate', 'maxdate')
+
+        form_buttons = [wdgs.SubmitButton()]
+        form_renderer_id = 'onerowtable'
+        period = MonthPeriodField()
+
+
+where we simply add our field, set a submit button and use a very simple renderer
+(try others!). Also we specify a selector that ensures form will have arguments
+necessary to our field.
+
+Now, we need a view that will wrap the form and handle post when it occurs,
+simply displaying posted values in the page:
+
+.. sourcecode:: python
+
+    class SelfPostingForm(View):
+        __regid__ = 'myformview'
+
+        def call(self):
+            mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1)
+            form = self._cw.vreg['forms'].select(
+                'myform', self._cw, mindate=mindate, maxdate=maxdate, action='')
+            try:
+                posted = form.process_posted()
+                self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted)))
+            except RequestError: # no specified period asked
+                pass
+            form.render(w=self.w, formvalues=self._cw.form)
+
+
+Notice usage of the :meth:`process_posted` method, that will return a dictionary
+of typed values (because they have been processed by the field). In our case, when
+the form is posted you should see a dictionary with 'begin_month' and 'end_month'
+as keys with the selected dates as value (as a python `date` object).
+
+
+APIs
+~~~~
+
+.. automodule:: cubicweb.web.formfields
+.. automodule:: cubicweb.web.formwidgets
+.. automodule:: cubicweb.web.views.forms
+.. automodule:: cubicweb.web.views.formrenderers
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,15 @@
+Edition control
+===============
+
+This chapter covers the editing capabilities of |cubicweb|. It
+explains html Form construction, the Edit Controller and their
+interactions.
+
+
+.. toctree::
+   :maxdepth: 2
+
+   form
+   dissection
+   editcontroller
+   examples
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/facets.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,23 @@
+The facets system
+-----------------
+
+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 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 that may be
+filtered according to some known criteria.
+
+Base classes for facets
+~~~~~~~~~~~~~~~~~~~~~~~
+.. automodule:: cubicweb.web.facet
+
+
+.. _facet: http://en.wikipedia.org/wiki/Faceted_browser
+.. _filters: http://www.cubicweb.org/blogentry/154152
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/httpcaching.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,21 @@
+HTTP cache management
+=====================
+
+.. automodule:: cubicweb.web.httpcache
+
+Cache policies
+--------------
+.. autoclass:: cubicweb.web.httpcache.NoHTTPCacheManager
+.. autoclass:: cubicweb.web.httpcache.MaxAgeHTTPCacheManager
+.. autoclass:: cubicweb.web.httpcache.EtagHTTPCacheManager
+.. autoclass:: cubicweb.web.httpcache.EntityHTTPCacheManager
+
+Exception
+---------
+.. autoexception:: cubicweb.web.httpcache.NoEtag
+
+Helper functions
+----------------
+.. autofunction:: cubicweb.web.httpcache.set_http_cache_headers
+
+.. NOT YET AVAILABLE IN STABLE autofunction:: cubicweb.web.httpcache.lastmodified
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,24 @@
+Web side development
+====================
+
+In this chapter, we will describe the core APIs for web development in
+the *CubicWeb* framework.
+
+.. toctree::
+   :maxdepth: 2
+
+   publisher
+   controllers
+   request
+   searchbar
+   views/index
+   rtags
+   ajax
+   js
+   css
+   edition/index
+   facets
+   internationalization
+   property
+   httpcaching
+   resource
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/internationalization.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,229 @@
+.. -*- coding: utf-8 -*-
+
+.. _internationalization:
+
+Internationalization
+---------------------
+
+Cubicweb fully supports the internalization of its content and interface.
+
+Cubicweb's interface internationalization is based on the translation project `GNU gettext`_.
+
+.. _`GNU gettext`: http://www.gnu.org/software/gettext/
+
+Cubicweb' internalization involves two steps:
+
+* in your Python code and cubicweb-tal templates : mark translatable strings
+
+* in your instance : handle the translation catalog, edit translations
+
+String internationalization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+User defined string
+```````````````````
+
+In the Python code and cubicweb-tal templates translatable strings can be
+marked in one of the following ways :
+
+ * by using the *built-in* function `_`:
+
+   .. sourcecode:: python
+
+     class PrimaryView(EntityView):
+         """the full view of an non final entity"""
+         __regid__ = 'primary'
+         title = _('primary')
+
+  OR
+
+ * by using the equivalent request's method:
+
+   .. sourcecode:: python
+
+     class NoResultView(View):
+         """default view when no result has been found"""
+         __regid__ = 'noresult'
+
+         def call(self, **kwargs):
+             self.w(u'<div class="searchMessage"><strong>%s</strong></div>\n'
+                 % self._cw._('No result matching query'))
+
+The goal of the *built-in* function `_` is only **to mark the
+translatable strings**, it will only return the string to translate
+itself, but not its translation (it's actually another name for the
+`unicode` builtin).
+
+In the other hand the request's method `self._cw._` is also meant to
+retrieve the proper translation of translation strings in the
+requested language.
+
+Finally you can also use the `__` attribute of request object to get a
+translation for a string *which should not itself added to the catalog*,
+usually in case where the actual msgid is created by string interpolation ::
+
+  self._cw.__('This %s' % etype)
+
+In this example ._cw.__` is used instead of ._cw._` so we don't have 'This %s' in
+messages catalogs.
+
+Translations in cubicweb-tal template can also be done with TAL tags
+`i18n:content` and `i18n:replace`.
+
+If you need to add messages on top of those that can be found in the source,
+you can create a file named `i18n/static-messages.pot`.
+
+You could put there messages not found in the python sources or
+overrides for some messages of used cubes.
+
+Generated string
+````````````````
+
+We do not need to mark the translation strings of entities/relations used by a
+particular instance's schema as they are generated automatically. String for
+various actions are also generated.
+
+For exemple the following schema:
+
+.. sourcecode:: python
+
+
+  class EntityA(EntityType):
+      relation_a2b = SubjectRelation('EntityB')
+
+  class EntityB(EntityType):
+      pass
+
+May generate the following message ::
+
+  add EntityA relation_a2b EntityB subject
+
+This message will be used in views of ``EntityA`` for creation of a new
+``EntityB`` with a preset relation ``relation_a2b`` between the current
+``EntityA`` and the new ``EntityB``. The opposite message ::
+
+  add EntityA relation_a2b EntityB object
+
+Is used for similar creation of an ``EntityA`` from a view of ``EntityB``. The
+title of they respective creation form will be ::
+
+  creating EntityB (EntityA %(linkto)s relation_a2b EntityB)
+
+  creating EntityA (EntityA relation_a2b %(linkto)s EntityA)
+
+In the translated string you can use ``%(linkto)s`` for reference to the source
+``entity``.
+
+Handling the translation catalog
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Once the internationalization is done in your code, you need to populate and
+update the translation catalog. Cubicweb provides the following commands for this
+purpose:
+
+
+* `i18ncubicweb` updates Cubicweb framework's translation
+  catalogs. Unless you actually work on the framework itself, you
+  don't need to use this command.
+
+* `i18ncube` updates the translation catalogs of *one particular cube*
+  (or of all cubes). After this command is executed you must update
+  the translation files *.po* in the "i18n" directory of your
+  cube. This command will of course not remove existing translations
+  still in use. It will mark unused translation but not remove them.
+
+* `i18ninstance` recompiles the translation catalogs of *one particular
+  instance* (or of all instances) after the translation catalogs of
+  its cubes have been updated. This command is automatically
+  called every time you create or update your instance. The compiled
+  catalogs (*.mo*) are stored in the i18n/<lang>/LC_MESSAGES of
+  instance where `lang` is the language identifier ('en' or 'fr'
+  for exemple).
+
+
+Example
+```````
+
+You have added and/or modified some translation strings in your cube
+(after creating a new view or modifying the cube's schema for exemple).
+To update the translation catalogs you need to do:
+
+1. `cubicweb-ctl i18ncube <cube>`
+2. Edit the <cube>/i18n/xxx.po  files and add missing translations (empty `msgstr`)
+3. `hg ci -m "updated i18n catalogs"`
+4. `cubicweb-ctl i18ninstance <myinstance>`
+
+Editing po files
+~~~~~~~~~~~~~~~~
+
+Using a PO aware editor
+````````````````````````
+
+Many tools exist to help maintain .po (PO) files. Common editors or
+development environment provides modes for these. One can also find
+dedicated PO files editor, such as `poedit`_.
+
+.. _`poedit`:  http://www.poedit.net/
+
+While usage of such a tool is commendable, PO files are perfectly
+editable with a (unicode aware) plain text editor. It is also useful
+to know their structure for troubleshooting purposes.
+
+Structure of a PO file
+``````````````````````
+
+In this section, we selectively quote passages of the `GNU gettext`_
+manual chapter on PO files, available there::
+
+ http://www.gnu.org/software/hello/manual/gettext/PO-Files.html
+
+One PO file entry has the following schematic structure::
+
+     white-space
+     #  translator-comments
+     #. extracted-comments
+     #: reference...
+     #, flag...
+     #| msgid previous-untranslated-string
+     msgid untranslated-string
+     msgstr translated-string
+
+
+A simple entry can look like this::
+
+     #: lib/error.c:116
+     msgid "Unknown system error"
+     msgstr "Error desconegut del sistema"
+
+It is also possible to have entries with a context specifier. They
+look like this::
+
+     white-space
+     #  translator-comments
+     #. extracted-comments
+     #: reference...
+     #, flag...
+     #| msgctxt previous-context
+     #| msgid previous-untranslated-string
+     msgctxt context
+     msgid untranslated-string
+     msgstr translated-string
+
+
+The context serves to disambiguate messages with the same
+untranslated-string. It is possible to have several entries with the
+same untranslated-string in a PO file, provided that they each have a
+different context. Note that an empty context string and an absent
+msgctxt line do not mean the same thing.
+
+Contexts and CubicWeb
+`````````````````````
+
+CubicWeb PO files have both non-contextual and contextual msgids.
+
+Contextual entries are automatically used in some cases. For instance,
+entity.dc_type(), eschema.display_name(req) or display_name(etype,
+req, form, context) methods/function calls will use them.
+
+It is also possible to explicitly use the with _cw.pgettext(context,
+msgid).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/js.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,394 @@
+.. -*- coding: utf-8 -*-
+
+Javascript
+----------
+
+*CubicWeb* uses quite a bit of javascript in its user interface and
+ships with jquery (1.3.x) and parts of the jquery UI library, plus a
+number of homegrown files and also other third party libraries.
+
+All javascript files are stored in cubicweb/web/data/. There are
+around thirty js files there. In a cube it goes to data/.
+
+Obviously one does not want javascript pieces to be loaded all at
+once, hence the framework provides a number of mechanisms and
+conventions to deal with javascript resources.
+
+Conventions
+~~~~~~~~~~~
+
+It is good practice to name cube specific js files after the name of
+the cube, like this : 'cube.mycube.js', so as to avoid name clashes.
+
+.. XXX external_resources variable (which needs love)
+
+Server-side Javascript API
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Javascript resources are typically loaded on demand, from views. The
+request object (available as self._cw from most application objects,
+for instance views and entities objects) has a few methods to do that:
+
+* `add_js(self, jsfiles, localfile=True)` which takes a sequence of
+  javascript files and writes proper entries into the HTML header
+  section. The localfile parameter allows to declare resources which
+  are not from web/data (for instance, residing on a content delivery
+  network).
+
+* `add_onload(self, jscode)` which adds one raw javascript code
+  snippet inline in the html headers. This is quite useful for setting
+  up early jQuery(document).ready(...) initialisations.
+
+Javascript events
+~~~~~~~~~~~~~~~~~
+
+* ``server-response``: this event is triggered on HTTP responses (both
+  standard and ajax). The two following extra parameters are passed
+  to callbacks :
+
+  - ``ajax``: a boolean that says if the reponse was issued by an
+    ajax request
+
+  - ``node``: the DOM node returned by the server in case of an
+    ajax request, otherwise the document itself for standard HTTP
+    requests.
+
+Important javascript AJAX APIS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* `asyncRemoteExec` and `remoteExec` are the base building blocks for
+  doing arbitrary async (resp. sync) communications with the server
+
+* `reloadComponent` is a convenience function to replace a DOM node
+  with server supplied content coming from a specific registry (this
+  is quite handy to refresh the content of some boxes for instances)
+
+* `jQuery.fn.loadxhtml` is an important extension to jQuery which
+  allows proper loading and in-place DOM update of xhtml views. It is
+  suitably augmented to trigger necessary events, and process CubicWeb
+  specific elements such as the facet system, fckeditor, etc.
+
+
+A simple example with asyncRemoteExec
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On the python side, we have to define an
+:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
+simplest way to do that is to use the
+:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
+details on this, refer to :ref:`ajax`).
+
+.. sourcecode: python
+
+    from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+    # serialize output to json to get it back easily on the javascript side
+    @ajaxfunc(output_type='json')
+    def js_say_hello(self, name):
+        return u'hello %s' % name
+
+On the javascript side, we do the asynchronous call. Notice how it
+creates a `deferred` object. Proper treatment of the return value or
+error handling has to be done through the addCallback and addErrback
+methods.
+
+.. sourcecode: javascript
+
+    function asyncHello(name) {
+        var deferred = asyncRemoteExec('say_hello', name);
+        deferred.addCallback(function (response) {
+            alert(response);
+        });
+        deferred.addErrback(function (error) {
+            alert('something fishy happened');
+        });
+     }
+
+     function syncHello(name) {
+         alert( remoteExec('say_hello', name) );
+     }
+
+Anatomy of a reloadComponent call
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`reloadComponent` allows to dynamically replace some DOM node with new
+elements. It has the following signature:
+
+* `compid` (mandatory) is the name of the component to be reloaded
+
+* `rql` (optional) will be used to generate a result set given as
+  argument to the selected component
+
+* `registry` (optional) defaults to 'components' but can be any other
+  valid registry name
+
+* `nodeid` (optional) defaults to compid + 'Component' but can be any
+  explicitly specified DOM node id
+
+* `extraargs` (optional) should be a dictionary of values that will be
+  given to the cell_call method of the component
+
+A simple reloadComponent example
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The server side implementation of `reloadComponent` is the
+:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.
+
+The following function implements a two-steps method to delete a
+standard bookmark and refresh the UI, while keeping the UI responsive.
+
+.. sourcecode:: javascript
+
+    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"));
+         });
+    }
+
+`reloadComponent` is called with the id of the bookmark box as
+argument, no rql expression (because the bookmarks display is actually
+independant of any dataset context), a reference to the 'boxes'
+registry (which hosts all left, right and contextual boxes) and
+finally an explicit 'bookmarks_box' nodeid argument that stipulates
+the target DOM node.
+
+Anatomy of a loadxhtml call
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`jQuery.fn.loadxhtml` is an important extension to jQuery which allows
+proper loading and in-place DOM update of xhtml views. The existing
+`jQuery.load`_ function does not handle xhtml, hence the addition. The
+API of loadxhtml is roughly similar to that of `jQuery.load`_.
+
+.. _`jQuery.load`: http://api.jquery.com/load/
+
+
+* `url` (mandatory) should be a complete url (typically referencing
+  the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
+  but this is not strictly mandatory)
+
+* `data` (optional) is a dictionary of values given to the
+  controller specified through an `url` argument; some keys may have a
+  special meaning depending on the choosen controller (such as `fname`
+  for the JSonController); the `callback` key, if present, must refer
+  to a function to be called at the end of loadxhtml (more on this
+  below)
+
+* `reqtype` (optional) specifies the request method to be used (get or
+  post); if the argument is 'post', then the post method is used,
+  otherwise the get method is used
+
+* `mode` (optional) is one of `replace` (the default) which means the
+  loaded node will replace the current node content, `swap` to replace
+  the current node with the loaded node, and `append` which will
+  append the loaded node to the current node content
+
+About the `callback` option:
+
+* it is called with two parameters: the current node, and a list
+  containing the loaded (and post-processed node)
+
+* whenever it returns another function, this function is called in
+  turn with the same parameters as above
+
+This mechanism allows callback chaining.
+
+
+A simple example with loadxhtml
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here we are concerned with the retrieval of a specific view to be
+injected in the live DOM. The view will be of course selected
+server-side using an entity eid provided by the client side.
+
+.. sourcecode:: python
+
+    from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+    @ajaxfunc(output_type='xhtml')
+    def frob_status(self, eid, frobname):
+        entity = self._cw.entity_from_eid(eid)
+        return entity.view('frob', name=frobname)
+
+.. sourcecode:: javascript
+
+    function updateSomeDiv(divid, eid, frobname) {
+        var params = {fname:'frob_status', eid: eid, frobname:frobname};
+        jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
+     }
+
+In this example, the url argument is the base json url of a cube
+instance (it should contain something like
+`http://myinstance/ajax?`). The actual AjaxController method name is
+encoded in the `params` dictionary using the `fname` key.
+
+A more real-life example
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+A frequent need of Web 2 applications is the delayed (or demand
+driven) loading of pieces of the DOM. This is typically achieved using
+some preparation of the initial DOM nodes, jQuery event handling and
+proper use of loadxhtml.
+
+We present here a skeletal version of the mecanism used in CubicWeb
+and available in web/views/tabs.py, in the `LazyViewMixin` class.
+
+.. sourcecode:: python
+
+    def lazyview(self, vid, rql=None):
+        """ a lazy version of wview """
+        w = self.w
+        self._cw.add_js('cubicweb.lazy.js')
+        urlparams = {'vid' : vid, 'fname' : 'view'}
+        if rql is not None:
+            urlparams['rql'] = rql
+        w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
+            vid, xml_escape(self._cw.build_url('json', **urlparams))))
+        w(u'</div>')
+        self._cw.add_onload(u"""
+            jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
+                   loadNow('#lazy-%(vid)s');});"""
+            % {'event': 'load_%s' % vid, 'vid': vid})
+
+This creates a `div` with a specific event associated to it.
+
+The full version deals with:
+
+* optional parameters such as an entity eid, an rset
+
+* the ability to further reload the fragment
+
+* the ability to display a spinning wheel while the fragment is still
+  not loaded
+
+* handling of browsers that do not support ajax (search engines,
+  text-based browsers such as lynx, etc.)
+
+The javascript side is quite simple, due to loadxhtml awesomeness.
+
+.. sourcecode:: javascript
+
+    function loadNow(eltsel) {
+        var lazydiv = jQuery(eltsel);
+        lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
+    }
+
+This is all significantly different of the previous `simple example`
+(albeit this example actually comes from real-life code).
+
+Notice how the `cubicweb:loadurl` is used to convey the url
+information. The base of this url is similar to the global javascript
+JSON_BASE_URL. According to the pattern described earlier,
+the `fname` parameter refers to the standard `js_view` method of the
+JSonController. This method renders an arbitrary view provided a view
+id (or `vid`) is provided, and most likely an rql expression yielding
+a result set against which a proper view instance will be selected.
+
+The `cubicweb:loadurl` is one of the 29 attributes extensions to XHTML
+in a specific cubicweb namespace. It is a means to pass information
+without breaking HTML nor XHTML compliance and without resorting to
+ungodly hacks.
+
+Given all this, it is easy to add a small nevertheless useful feature
+to force the loading of a lazy view (for instance, a very
+computation-intensive web page could be scinded into one fast-loading
+part and a delayed part).
+
+On the server side, a simple call to a javascript function is
+sufficient.
+
+.. sourcecode:: python
+
+    def forceview(self, vid):
+        """trigger an event that will force immediate loading of the view
+        on dom readyness
+        """
+        self._cw.add_onload("triggerLoad('%s');" % vid)
+
+The browser-side definition follows.
+
+.. sourcecode:: javascript
+
+    function triggerLoad(divid) {
+        jQuery('#lazy-' + divd).trigger('load_' + divid);
+    }
+
+
+Javascript library: overview
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* jquery.* : jquery and jquery UI library
+
+* cubicweb.ajax.js : concentrates all ajax related facilities (it
+  extends jQuery with the loahxhtml function, provides a handfull of
+  high-level ajaxy operations like asyncRemoteExec, reloadComponent,
+  replacePageChunk, getDomFromResponse)
+
+* cubicweb.python.js : adds a number of practical extension to stdanrd
+  javascript objects (on Date, Array, String, some list and dictionary
+  operations), and a pythonesque way to build classes. Defines a
+  CubicWeb namespace.
+
+* cubicweb.htmlhelpers.js : a small bag of convenience functions used
+  in various other cubicweb javascript resources (baseuri, progress
+  cursor handling, popup login box, html2dom function, etc.)
+
+* cubicweb.widgets.js : provides a widget namespace and constructors
+  and helpers for various widgets (mainly facets and timeline)
+
+* cubicweb.edition.js : used by edition forms
+
+* cubicweb.preferences.js : used by the preference form
+
+* cubicweb.facets.js : used by the facets mechanism
+
+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")
+                ),
+            )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/property.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,15 @@
+.. _cwprops:
+
+The property mecanism
+---------------------
+
+.. XXX CWProperty and co
+
+
+Property API
+~~~~~~~~~~~~
+.. XXX feed me
+
+Registering and using your own property
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. XXX feed me
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/publisher.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,65 @@
+.. _publisher:
+
+Publisher
+---------
+
+What happens when an HTTP request is issued ?
+
+The story begins with the ``CubicWebPublisher.main_publish``
+method. We do not get upper in the bootstrap process because it is
+dependant on the used HTTP library. With `twisted`_ however,
+``cubicweb.etwist.server.CubicWebRootResource.render_request`` is the
+real entry point.
+
+.. _`twisted`: http://twistedmatrix.com/trac/
+
+What main_publish does:
+
+* get a controller id and a result set from the path (this is actually
+  delegated to the `urlpublisher` component)
+
+* the controller is then selected (if not, this is considered an
+  authorization failure and signaled as such) and called
+
+* then either a proper result is returned, in which case the
+  request/connection object issues a ``commit`` and returns the result
+
+* or error handling must happen:
+
+  * ``ValidationErrors`` pop up there and may lead to a redirect to a
+    previously arranged url or standard error handling applies
+  * an HTTP 500 error (`Internal Server Error`) is issued
+
+
+Now, let's turn to the controller. There are many of them in
+:mod:`cubicweb.web.views.basecontrollers`. We can just follow the
+default `view` controller that is selected on a `view` path. See the
+:ref:`controllers` chapter for more information on controllers.
+
+The `View` controller's entry point is the `publish` method. It does
+the following:
+
+* compute the `main` view to be applied, using either the given result
+  set or building one from a user provided rql string (`rql` and `vid`
+  can be forced from the url GET parameters), that is:
+
+    * compute the `vid` using the result set and the schema (see
+      `cubicweb.web.views.vid_from_rset`)
+    * handle all error cases that could happen in this phase
+
+* do some cache management chores
+
+* select a main template (typically `TheMainTemplate`, see chapter
+  :ref:`templates`)
+
+* call it with the result set and the computed view.
+
+What happens next actually depends on the template and the view, but
+in general this is the rendering phase.
+
+
+CubicWebPublisher API
+`````````````````````
+
+.. autoclass:: cubicweb.web.application.CubicWebPublisher
+   :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/request.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,131 @@
+The `Request` class (`cubicweb.web.request`)
+--------------------------------------------
+
+Overview
+````````
+
+A request instance is created when an HTTP request is sent to the web
+server.  It contains informations such as form parameters,
+authenticated user, etc. It is a very prevalent object and is used
+throughout all of the framework and applications, as you'll access to
+almost every resources through it.
+
+**A request represents a user query, either through HTTP or not (we
+also talk about RQL queries on the server side for example).**
+
+Here is a non-exhaustive list of attributes and methods available on
+request objects (grouped by category):
+
+* `Browser control`:
+
+  * `ie_browser`: tells if the browser belong to the Internet Explorer
+    family
+
+* `User and identification`:
+
+  * `user`, instance of `cubicweb.entities.authobjs.CWUser` corresponding to the
+    authenticated user
+
+* `Session data handling`
+
+  * `session.data` is the dictionary of the session data; it can be
+    manipulated like an ordinary Python dictionary
+
+* `Edition` (utilities for edition control):
+
+  * `cancel_edition`: resets error url and cleans up pending operations
+  * `create_entity`: utility to create an entity (from an etype,
+    attributes and relation values)
+  * `datadir_url`: returns the url to the merged external resources
+    (|cubicweb|'s `web/data` directory plus all `data` directories of
+    used cubes)
+  * `edited_eids`: returns the list of eids of entities that are
+    edited under the current http request
+  * `eid_rset(eid)`: utility which returns a result set from an eid
+  * `entity_from_eid(eid)`: returns an entity instance from the given eid
+  * `encoding`: returns the encoding of the current HTTP request
+  * `ensure_ro_rql(rql)`: ensure some rql query is a data request
+  * etype_rset
+  * `form`, dictionary containing the values of a web form
+  * `encoding`, character encoding to use in the response
+  * `next_tabindex()`: returns a monotonically growing integer used to
+    build the html tab index of forms
+
+* `HTTP`
+
+  * `authmode`: returns a string describing the authentication mode
+    (http, cookie, ...)
+  * `lang`: returns the user agents/browser's language as carried by
+    the http request
+  * `demote_to_html()`: in the context of an XHTML compliant browser,
+    this will force emission of the response as an HTML document
+    (using the http content negociation)
+
+*  `Cookies handling`
+
+  * `get_cookie()`, returns a dictionary containing the value of the header
+    HTTP 'Cookie'
+  * `set_cookie(cookie, key, maxage=300)`, adds a header HTTP `Set-Cookie`,
+    with a minimal 5 minutes length of duration by default (`maxage` = None
+    returns a *session* cookie which will expire when the user closes the browser
+    window)
+  * `remove_cookie(cookie, key)`, forces a value to expire
+
+* `URL handling`
+
+  * `build_url(__vid, *args, **kwargs)`: 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.
+  * `build_url_params(**kwargs)`: returns a properly prepared (quoted,
+    separators, ...) string from the given parameters
+  * `url()`, returns the full URL of the HTTP request
+  * `base_url()`, returns the root URL of the web application
+  * `relative_path()`, returns the relative path of the request
+
+* `Web resource (.css, .js files, etc.) handling`:
+
+  * `add_css(cssfiles)`: adds the given list of css resources to the current
+    html headers
+  * `add_js(jsfiles)`: adds the given list of javascript resources to the
+    current html headers
+  * `add_onload(jscode)`: inject the given jscode fragment (a unicode
+    string) into the current html headers, wrapped inside a
+    document.ready(...) or another ajax-friendly one-time trigger event
+  * `add_header(header, values)`: adds the header/value pair to the
+    current html headers
+  * `status_out`: control the HTTP status of the response
+
+* `And more...`
+
+  * `set_content_type(content_type, filename=None)`, adds the header HTTP
+    'Content-Type'
+  * `get_header(header)`, returns the value associated to an arbitrary header
+    of the HTTP request
+  * `set_header(header, value)`, adds an arbitrary header in the response
+  * `execute(*args, **kwargs)`, executes an RQL query and return the result set
+  * `property_value(key)`, properties management (`CWProperty`)
+  * dictionary `data` to store data to share informations between components
+    *while a request is executed*
+
+Please note that this class is abstract and that a concrete implementation
+will be provided by the *frontend* web used (in particular *twisted* as of
+today). For the views or others that are executed on the server side,
+most of the interface of `Request` is defined in the session associated
+to the client.
+
+API
+```
+
+The elements we gave in overview for above are built in three layers,
+from ``cubicweb.req.RequestSessionBase``, ``cubicweb.repoapi.Connection`` and
+``cubicweb.web.ConnectionCubicWebRequestBase``.
+
+.. autoclass:: cubicweb.req.RequestSessionBase
+   :members:
+
+.. autoclass:: cubicweb.repoapi.Connection
+   :members:
+
+.. autoclass:: cubicweb.web.request.ConnectionCubicWebRequestBase
+   :members:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/resource.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,18 @@
+.. _resources:
+
+Locate resources
+----------------
+
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_resource
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_doc_file
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_all_files
+
+Static files handling
+---------------------
+
+.. autoattribute:: cubicweb.web.webconfig.WebConfiguration.static_directory
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_exists
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_open
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_add
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_del
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/rtags.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,27 @@
+Configuring the user interface
+------------------------------
+
+.. _relation_tags:
+
+Relation tags
+~~~~~~~~~~~~~
+.. automodule:: cubicweb.rtags
+
+.. _uicfg:
+
+The uicfg module
+~~~~~~~~~~~~~~~~
+
+.. note::
+
+ The part of uicfg that deals with primary views is in the
+ :ref:`primary_view_configuration` chapter.
+
+.. automodule:: cubicweb.web.views.uicfg
+
+
+The uihelper module
+~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: cubicweb.web.uihelper
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/searchbar.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,41 @@
+.. _searchbar:
+
+RQL search bar
+--------------
+
+The RQL search bar is a visual component, hidden by default, the tiny *search*
+input being enough for common use cases.
+
+An autocompletion helper is provided to help you type valid queries, both
+in terms of syntax and in terms of schema validity.
+
+.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder
+
+
+How search is performed
++++++++++++++++++++++++
+
+You can use the *rql search bar* to either type RQL queries, plain text queries
+or standard shortcuts such as *<EntityType>* or *<EntityType> <attrname> <value>*.
+
+Ultimately, all queries are translated to rql since it's the only
+language understood on the server (data) side. To transform the user
+query into RQL, CubicWeb uses the so-called *magicsearch component*,
+defined in :mod:`cubicweb.web.views.magicsearch`, which in turn
+delegates to a number of query preprocessor that are responsible of
+interpreting the user query and generating corresponding RQL.
+
+The code of the main processor loop is easy to understand:
+
+.. sourcecode:: python
+
+  for proc in self.processors:
+      try:
+          return proc.process_query(uquery, req)
+      except (RQLSyntaxError, BadRQLQuery):
+          pass
+
+The idea is simple: for each query processor, try to translate the
+query. If it fails, try with the next processor, if it succeeds,
+we're done and the RQL query will be executed.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/basetemplates.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,161 @@
+.. -*- coding: utf-8 -*-
+
+.. _templates:
+
+Templates
+=========
+
+Templates are the entry point for the |cubicweb| view system. As seen
+in :ref:`views_base_class`, there are two kinds of views: the
+templatable and non-templatable.
+
+
+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:
+
+* set up the proper document header and content type
+* define the general layout of a document
+* 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 :class:`~cubicweb.web.views.basetemplates.TheMainTemplate`.
+
+.. _the_main_template_layout:
+
+TheMainTemplate
+~~~~~~~~~~~~~~~
+
+.. _the_main_template_sections:
+
+Layout and sections
+```````````````````
+
+A page is composed as indicated on the schema below :
+
+.. image:: ../../../images/main_template.png
+
+The sections dispatches specific views:
+
+* `header`: the rendering of the header is delegated to the
+  `htmlheader` view, whose default implementation can be found in
+  ``basetemplates.py`` and which does the following things:
+
+    * inject the favicon if there is one
+    * inject the global style sheets and javascript resources
+    * call and display a link to an rss component if there is one available
+
+  it also sets up the page title, and fills the actual
+  `header` section with top-level components, using the `header` view, which:
+
+    * tries to display a logo, the name of the application and the `breadcrumbs`
+    * provides a login status area
+    * provides a login box (hiden by default)
+
+* `left column`: this is filled with all selectable boxes matching the
+  `left` context (there is also a right column but nowadays it is
+  seldom used due to bad usability)
+
+* `contentcol`: this is the central column; it is filled with:
+
+    * the `rqlinput` view (hidden by default)
+    * the `applmessages` component
+    * the `contentheader` view which in turns dispatches all available
+      content navigation components having the `navtop` context (this
+      is used to navigate through entities implementing the IPrevNext
+      interface)
+    * the view that was given as input to the template's `call`
+      method, also dealing with pagination concerns
+    * the `contentfooter`
+
+* `footer`: adds all footer actions
+
+.. note::
+
+  How and why a view object is given to the main template is explained
+  in the :ref:`publisher` chapter.
+
+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 a unicode string**, encoded string otherwise.
+
+* `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 a 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 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)
+
+* `vtitle`, a title to be set as <h1> of the content
+
+Other templates
+~~~~~~~~~~~~~~~
+
+There are also the following other standard templates:
+
+* :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).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/baseviews.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,21 @@
+Base views
+----------
+
+|cubicweb| provides a lot of standard views, that can be found in
+: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. As other appobjects, their identifier is what distinguish them
+from each others. The most generic ones, found in
+:mod:`cubicweb.web.views.baseviews`, are described below.
+
+You'll probably want to customize one or more of the described views which are
+default, generic, implementations.
+
+
+.. automodule:: cubicweb.web.views.baseviews
+
+You will also find modules providing some specific services:
+
+.. automodule:: cubicweb.web.views.navigation
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/boxes.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,36 @@
+Boxes
+-----
+
+(:mod:`cubicweb.web.views.boxes`)
+
+*sidebox*
+  This view displays usually a side box of some related entities
+  in a primary view.
+
+The action box
+~~~~~~~~~~~~~~~
+
+The ``add_related`` is an automatic menu in the action box that allows to create
+an entity automatically related to the initial entity (context in
+which the box is displayed). By default, the links generated in this
+box are computed from the schema properties of the displayed entity,
+but it is possible to explicitly specify them thanks to the
+`cubicweb.web.views.uicfg.rmode` *relation tag*:
+
+* `link`, indicates that a relation is in general created pointing
+  to an existing entity and that we should not to display a link
+  for this relation
+
+* `create`, indicates that a relation is in general created pointing
+  to new entities and that we should display a link to create a new
+  entity and link to it automatically
+
+
+If necessary, it is possible to overwrite the method
+`relation_mode(rtype, targettype, x='subject')` to dynamically
+compute a relation creation category.
+
+Please note that if at least one action belongs to the `addrelated` category,
+the automatic behavior is desactivated in favor of an explicit behavior
+(e.g. display of `addrelated` category actions only).
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/breadcrumbs.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,60 @@
+Breadcrumbs
+-----------
+
+Breadcrumbs are a navigation component to help the user locate himself
+along a path of entities.
+
+Display
+~~~~~~~
+
+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 they begin with a
+separator.
+
+Here is the header section of the CubicWeb's forge:
+
+.. image:: ../../../images/breadcrumbs_header.png
+
+There are three breadcrumbs components defined in
+:mod:`cubicweb.web.views.ibreadcrumbs`:
+
+- `BreadCrumbEntityVComponent`: displayed for a result set with one line
+  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 can adapt to
+  ``IBreadCrumbsAdapter``.
+- `BreadCrumbAnyRSetVComponent`: displayed for any other result set.
+
+Building breadcrumbs
+~~~~~~~~~~~~~~~~~~~~
+
+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 an `ITreeAdapter` adapter on an entity type. If
+   available, it will be used to compute breadcrumbs.
+
+Here is the API of the ``IBreadCrumbsAdapter`` class:
+
+.. 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
+the elements.
+
+By default, for any entity, if recurs=True, breadcrumbs method returns
+a list of entities, else a list of a simple string.
+
+In order to see a hierarchical breadcrumbs, entities must have a
+``parent`` method which returns the parent entity. By default this
+method doesn't exist on entity, given that it can not be guessed.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/idownloadable.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,23 @@
+The 'download' views
+====================
+
+.. automodule:: cubicweb.web.views.idownloadable
+
+Components
+----------
+
+.. autoclass:: cubicweb.web.views.idownloadable.DownloadBox
+
+Download views
+--------------
+
+.. autoclass:: cubicweb.web.views.idownloadable.DownloadView
+.. autoclass:: cubicweb.web.views.idownloadable.DownloadLinkView
+.. autoclass:: cubicweb.web.views.idownloadable.IDownloadablePrimaryView
+.. autoclass:: cubicweb.web.views.idownloadable.IDownloadableOneLineView
+
+Embedded views
+--------------
+
+.. autoclass:: cubicweb.web.views.idownloadable.ImageView
+.. autoclass:: cubicweb.web.views.idownloadable.EHTMLView
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,28 @@
+The View system
+===============
+
+This chapter aims to describe the concept of a `view` used all along
+the development of a web application and how it has been implemented
+in |cubicweb|.
+
+
+.. toctree::
+   :maxdepth: 3
+
+   views
+   basetemplates
+   primary
+   reledit
+   baseviews
+   startup
+   boxes
+   table
+   xmlrss
+   urlpublish
+   breadcrumbs
+   idownloadable
+   wdoc
+
+..   editforms
+..   embedding
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/primary.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,276 @@
+.. _primary_view:
+
+The Primary View
+-----------------
+
+By default, *CubicWeb* provides a view that fits every available
+entity type. This is the first view you might be interested in
+modifying. It is also one of the richest and most complex.
+
+It is automatically selected on a one line result set containing an
+entity.
+
+It lives in the :mod:`cubicweb.web.views.primary` module.
+
+The *primary* view is supposed to render a maximum of informations about the
+entity.
+
+.. _primary_view_layout:
+
+Layout
+``````
+
+The primary view has the following layout.
+
+.. image:: ../../../images/primaryview_template.png
+
+.. _primary_view_configuration:
+
+Primary view configuration
+``````````````````````````
+
+If you want to customize the primary view of an entity, overriding the primary
+view class may not be necessary. For simple adjustments (attributes or relations
+display locations and styles), a much simpler way is to use uicfg.
+
+Attributes/relations display location
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+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** can only be displayed in the attributes section (default
+  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:
+
+.. sourcecode:: python
+
+   from cubicweb.web.views import uicfg
+   uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
+
+**Relations** can be either displayed in one of the three sections or hidden.
+
+For relations, there are two methods:
+
+* ``tag_object_of`` for modifying the primary view of the object
+* ``tag_subject_of`` for modifying the primary view of the subject
+
+These two methods take two arguments:
+
+* a triplet ``(subject, relation_name, object)``, where subject or object can be replaced with ``'*'``
+* the section name or ``hidden``
+
+.. sourcecode:: python
+
+   pv_section = uicfg.primaryview_section
+   # hide every relation `entry_of` in the `Blog` primary view
+   pv_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
+
+   # display `entry_of` relations in the `relations`
+   # section in the `BlogEntry` primary view
+   pv_section.tag_subject_of(('BlogEntry', 'entry_of', '*'), 'relations')
+
+
+Display content
+^^^^^^^^^^^^^^^
+
+You can use ``primaryview_display_ctrl`` to customize the display of attributes
+or relations. Values of ``primaryview_display_ctrl`` are dictionaries.
+
+
+Common keys for attributes and relations are:
+
+* ``vid``: specifies the regid of the view for displaying the attribute or the relation.
+
+  If ``vid`` is not specified, the default value depends on the section:
+    * ``attributes`` section: 'reledit' view
+    * ``relations`` section: 'autolimited' view
+    * ``sideboxes`` section: 'sidebox' view
+
+* ``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
+   class BlogEntry(EntityType):
+       title = String(required=True, fulltextindexed=True, maxsize=256)
+       publish_date = Date(default='TODAY')
+       content = String(required=True, fulltextindexed=True)
+       entry_of = SubjectRelation('Blog', cardinality='?*')
+
+   # now, we want to show attributes
+   # with an order different from that in the schema definition
+   view_ctrl = uicfg.primaryview_display_ctrl
+   for index, attr in enumerate('title', 'content', 'publish_date'):
+       view_ctrl.tag_attribute(('BlogEntry', attr), {'order': index})
+
+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).
+
+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.
+
+* `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)
+
+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.
+
+
+.. sourcecode:: python
+
+   pv_section = uicfg.primaryview_section
+   # in `CWUser` primary view, display `created_by`
+   # relations in relations section
+   pv_section.tag_object_of(('*', 'created_by', 'CWUser'), 'relations')
+
+   # display this relation as a list, sets the label,
+   # limit the number of results and filters on comments
+   def filter_comment(rset):
+       return rset.filtered_rset(lambda x: x.e_schema == 'Comment')
+   pv_ctrl = uicfg.primaryview_display_ctrl
+   pv_ctrl.tag_object_of(('*', 'created_by', 'CWUser'),
+                         {'vid': 'list', 'label': _('latest comment(s):'),
+                          'limit': True,
+                          'filter': filter_comment})
+
+.. warning:: with the ``primaryview_display_ctrl`` rtag, the subject or the
+   object of the relation is ignored for respectively ``tag_object_of`` or
+   ``tag_subject_of``. To avoid warnings during execution, they should be set to
+   ``'*'``.
+
+
+.. automodule:: cubicweb.web.views.primary
+
+
+Example of customization and creation
+`````````````````````````````````````
+
+We'll show you now an example of a ``primary`` view and how to customize it.
+
+If you want to change the way a ``BlogEntry`` is displayed, just
+override the method ``cell_call()`` of the view ``primary`` in
+``BlogDemo/views.py``.
+
+.. sourcecode:: python
+
+   from cubicweb.predicates import is_instance
+   from cubicweb.web.views.primary import Primaryview
+
+   class BlogEntryPrimaryView(PrimaryView):
+       __select__ = PrimaryView.__select__ & is_instance('BlogEntry')
+
+       def render_entity_attributes(self, entity):
+           self.w(u'<p>published on %s</p>' %
+                  entity.publish_date.strftime('%Y-%m-%d'))
+           super(BlogEntryPrimaryView, self).render_entity_attributes(entity)
+
+
+The above source code defines a new primary view for
+``BlogEntry``. The `__reid__` class attribute is not repeated there since it
+is inherited through the `primary.PrimaryView` class.
+
+The selector for this view chains the selector of the inherited class
+with its own specific criterion.
+
+The view method ``self.w()`` is used to output data. Here `lines
+08-09` output HTML for the publication date of the entry.
+
+.. image:: ../../../images/lax-book_09-new-view-blogentry_en.png
+   :alt: blog entries now look much nicer
+
+Let us now improve the primary view of a blog
+
+.. sourcecode:: python
+
+ from logilab.mtconverter import xml_escape
+ from cubicweb.predicates import is_instance, one_line_rset
+ from cubicweb.web.views.primary import Primaryview
+
+ class BlogPrimaryView(PrimaryView):
+     __regid__ = 'primary'
+     __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):
+         rset = self._cw.execute(self.rql, {'b' : entity.eid})
+         for entry in rset.entities():
+             self.w(u'<p>%s</p>' % entry.view('inblogcontext'))
+
+ class BlogEntryInBlogView(EntityView):
+     __regid__ = 'inblogcontext'
+     __select__ = is_instance('BlogEntry')
+
+     def cell_call(self, row, col):
+         entity = self.cw_rset.get_entity(row, col)
+         self.w(u'<a href="%s" title="%s">%s</a>' %
+                entity.absolute_url(),
+                xml_escape(entity.content[:50]),
+                xml_escape(entity.description))
+
+This happens in two places. First we override the
+render_entity_relations method of a Blog's primary view. Here we want
+to display our blog entries in a custom way.
+
+At `line 10`, a simple request is made to build a result set with all
+the entities linked to the current ``Blog`` entity by the relationship
+``entry_of``. The part of the framework handling the request knows
+about the schema and infers that such entities have to be of the
+``BlogEntry`` kind and retrieves them (in the prescribed publish_date
+order).
+
+The request returns a selection of data called a result set. Result
+set objects have an .entities() method returning a generator on
+requested entities (going transparently through the `ORM` layer).
+
+At `line 13` the view 'inblogcontext' is applied to each blog entry to
+output HTML. (Note that the 'inblogcontext' view is not defined
+whatsoever in *CubicWeb*. You are absolutely free to define whole view
+families.) We juste arrange to wrap each blogentry output in a 'p'
+html element.
+
+Next, we define the 'inblogcontext' view. This is NOT a primary view,
+with its well-defined sections (title, metadata, attribtues,
+relations/boxes). All a basic view has to define is cell_call.
+
+Since views are applied to result sets which can be tables of data, we
+have to recover the entity from its (row,col)-coordinates (`line
+20`). Then we can spit some HTML.
+
+.. warning::
+
+  Be careful: all strings manipulated in *CubicWeb* are actually
+  unicode strings. While web browsers are usually tolerant to
+  incoherent encodings they are being served, we should not abuse
+  it. Hence we have to properly escape our data. The xml_escape()
+  function has to be used to safely fill (X)HTML elements from Python
+  unicode strings.
+
+Assuming we added entries to the blog titled `MyLife`, displaying it
+now allows to read its description and all its entries.
+
+.. image:: ../../../images/lax-book_10-blog-with-two-entries_en.png
+   :alt: a blog and all its entries
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/reledit.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,149 @@
+.. _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.views.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.views.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 a
+unicode string representing a url. If an eid is provided, it will be
+internally transformed into a 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.
+
+
+Disable `reledit`
+*****************
+
+By default, `reledit` is available on attributes and relations displayed in
+the 'attribute' section of the default primary view.  If you want to disable
+it for some attribute or relation, you have use `uicfg`:
+
+.. sourcecode:: python
+
+    from cubicweb.web.views.uicfg import primaryview_display_ctrl as _pvdc
+    _pvdc.tag_attribute(('Company', 'name'), {'vid': 'incontext'})
+
+To deactivate it everywhere it's used automatically, you may use the code snippet
+below somewhere in your cube's views:
+
+.. sourcecode:: python
+
+    from cubicweb.web.views import reledit
+
+    class DeactivatedAutoClickAndEditFormView(reledit.AutoClickAndEditFormView):
+	def _should_edit_attribute(self, rschema):
+	    return False
+
+	def _should_edit_attribute(self, rschema, role):
+	    return False
+
+    def registration_callback(vreg):
+	vreg.register_and_replace(DeactivatedAutoClickAndEditFormView,
+				  reledit.AutoClickAndEditFormView)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/startup.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,18 @@
+Startup views
+-------------
+
+Startup views are views requiring no context, from which you usually start
+browsing (for instance the index page). The usual selectors are
+:class:`~cubicweb.predicates.none_rset` or :class:`~logilab.common.registry.yes`.
+
+You'll find here a description of startup views provided by the framework.
+
+.. automodule:: cubicweb.web.views.startup
+
+
+Other startup views:
+
+*schema*
+    A view dedicated to the display of the schema of the instance
+
+.. XXX to be continued
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/table.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,145 @@
+Table views
+-----------
+
+.. automodule:: cubicweb.web.views.tableview
+
+Example
+```````
+
+Let us take an example from the timesheet cube:
+
+.. sourcecode:: python
+
+    class ActivityResourcesTable(EntityView):
+        __regid__ = 'activity.resources.table'
+        __select__ = is_instance('Activity')
+
+        def call(self, showresource=True):
+            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)
+            rset = self._cw.execute(rql)
+            self.wview('resource.table', rset, 'null')
+
+    class ResourcesTable(RsetTableView):
+        __regid__ = 'resource.table'
+        # notice you may wish a stricter selector to check rql's shape
+        __select__ = is_instance('Resource')
+        # my table headers
+        headers  = ['Resource', 'diem', 'duration', 'workpackage', 'description', 'state']
+        # I want a table where attributes are editable (reledit inside)
+        finalvid = 'editable-final'
+
+        cellvids = {3: 'editable-final'}
+        # display facets and actions with a menu
+        layout_args = {'display_filter': 'top',
+                       'add_view_actions': None}
+
+To obtain an editable table, you may specify the 'editable-table' view identifier
+using some of `cellvids`, `finalvid` or `nonfinalvid`.
+
+The previous example results in:
+
+.. image:: ../../../images/views-table-shadow.png
+
+In order to activate table filter mechanism, the `display_filter` option is given
+as a layout argument. 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 additional actions for the selected entities
+by setting `add_view_actions` layout option to `True`. This will add actions
+returned by the view's :meth:`~cubicweb.web.views.tableview.TableMixIn.table_actions`.
+
+You can notice that all columns of the result set are not displayed. This is
+because of given `headers`, implying to display only columns from 0 to
+len(headers).
+
+Also Notice that the `ResourcesTable` view relies on a particular rql shape
+(which is not ensured by the way, the only checked thing is that the result set
+contains instance of the `Resource` type). That usually implies that you can't
+use this view for user specific queries (e.g. generated by facets or typed
+manually).
+
+
+So another option would be to write this view using
+:class:`~cubicweb.web.views.tableview.EntityTableView`, as below.
+
+.. sourcecode:: python
+
+    class ResourcesTable(EntityTableView):
+        __regid__ = 'resource.table'
+        __select__ = is_instance('Resource')
+        # table columns definition
+        columns  = ['resource', 'diem', 'duration', 'workpackage', 'description', 'in_state']
+        # I want a table where attributes are editable (reledit inside)
+        finalvid = 'editable-final'
+        # display facets and actions with a menu
+        layout_args = {'display_filter': 'top',
+                       'add_view_actions': None}
+
+        def workpackage_cell(entity):
+            activity = entity.reverse_done_in[0]
+            activity.view('reledit', rtype='done_for', role='subject', w=w)
+        def workpackage_sortvalue(entity):
+            activity = entity.reverse_done_in[0]
+            return activity.done_for[0].sortvalue()
+
+        column_renderers = {
+            'resource': MainEntityColRenderer(),
+            'workpackage': EntityTableColRenderer(
+               header='Workpackage',
+               renderfunc=worpackage_cell,
+               sortfunc=worpackage_sortvalue,),
+            'in_state': EntityTableColRenderer(
+               renderfunc=lambda w,x: w(x.cw_adapt_to('IWorkflowable').printable_state),
+               sortfunc=lambda x: x.cw_adapt_to('IWorkflowable').printable_state),
+         }
+
+Notice the following point:
+
+* `cell_<column>(w, entity)` will be searched for rendering the content of a
+  cell. If not found, `column` is expected to be an attribute of `entity`.
+
+* `cell_sortvalue_<column>(entity)` should return a typed value to use for
+  javascript sorting or None for not sortable columns (the default).
+
+* The :func:`etable_entity_sortvalue` decorator will set a 'sortvalue' function
+  for the column containing the main entity (the one given as argument to all
+  methods), which will call `entity.sortvalue()`.
+
+* You can set a column header using the :func:`etable_header_title` decorator.
+  This header will be translated. If it's not an already existing msgid, think
+  to mark it using `_()` (the example supposes headers are schema defined msgid).
+
+
+Pro/cons of each approach
+`````````````````````````
+:class:`EntityTableView` and :class:`RsetableView` provides basically the same
+set of features, though they don't share the same properties. Let's try to sum
+up pro and cons of each class.
+
+* `EntityTableView` view is:
+
+  - more verbose, but usually easier to understand
+
+  - easily extended (easy to add/remove columns for instance)
+
+  - doesn't rely on a particular rset shape. Simply give it a title and will be
+    listed in the 'possible views' box if any.
+
+* `RsetTableView` view is:
+
+  - hard to beat to display barely a result set, or for cases where some of
+    `headers`, `displaycols` or `cellvids` could be defined to enhance the table
+    while you don't care about e.g. pagination or facets.
+
+  - hardly extensible, as you usually have to change places where the view is
+    called to modify the RQL (hence the view's result set shape).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/urlpublish.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,137 @@
+.. -*- coding: utf-8 -*-
+
+URL publishing
+--------------
+
+(:mod:`cubicweb.web.views.urlpublishing`)
+
+.. automodule:: cubicweb.web.views.urlpublishing
+
+.. autoclass:: cubicweb.web.views.urlpublishing.URLPublisherComponent
+   :members:
+
+
+You can write your own *URLPathEvaluator* class to handle custom paths.
+For instance, if you want */my-card-id* to redirect to the corresponding
+card's primary view, you would write:
+
+.. sourcecode:: python
+
+    class CardWikiidEvaluator(URLPathEvaluator):
+        priority = 3 # make it be evaluated *before* RestPathEvaluator
+
+        def evaluate_path(self, req, segments):
+            if len(segments) != 1:
+                raise PathDontMatch()
+            rset = req.execute('Any C WHERE C wikiid %(w)s',
+                               {'w': segments[0]})
+            if len(rset) == 0:
+                # Raise NotFound if no card is found
+                raise PathDontMatch()
+            return None, rset
+
+On the other hand, you can also deactivate some of the standard
+evaluators in your final application. The only thing you have to
+do is to unregister them, for instance in a *registration_callback*
+in your cube:
+
+.. sourcecode:: python
+
+    def registration_callback(vreg):
+        vreg.unregister(RestPathEvaluator)
+
+You can even replace the :class:`cubicweb.web.views.urlpublishing.URLPublisherComponent`
+class if you want to customize the whole toolchain process or if you want
+to plug into an early enough extension point to control your request
+parameters:
+
+.. sourcecode:: python
+
+    class SanitizerPublisherComponent(URLPublisherComponent):
+        """override default publisher component to explicitly ignore
+        unauthorized request parameters in anonymous mode.
+        """
+        unauthorized_form_params = ('rql', 'vid', '__login', '__password')
+
+        def process(self, req, path):
+            if req.session.anonymous_session:
+                self._remove_unauthorized_params(req)
+            return super(SanitizerPublisherComponent, self).process(req, path)
+
+        def _remove_unauthorized_params(self, req):
+            for param in req.form.keys():
+                if param in self.unauthorized_form_params:
+                     req.form.pop(param)
+
+
+    def registration_callback(vreg):
+        vreg.register_and_replace(SanitizerPublisherComponent, URLPublisherComponent)
+
+
+.. autoclass:: cubicweb.web.views.urlpublishing.RawPathEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.EidPathEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.URLRewriteEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.RestPathEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.ActionPathEvaluator
+
+URL rewriting
+-------------
+
+(:mod:`cubicweb.web.views.urlrewrite`)
+
+.. autoclass:: cubicweb.web.views.urlrewrite.URLRewriter
+   :members:
+
+.. autoclass:: cubicweb.web.views.urlrewrite.SimpleReqRewriter
+   :members:
+
+.. autoclass:: cubicweb.web.views.urlrewrite.SchemaBasedRewriter
+   :members:
+
+
+``SimpleReqRewriter`` is enough for a certain number of simple cases. If it is not sufficient, ``SchemaBasedRewriter`` allows to do more elaborate things.
+
+Here is an example of ``SimpleReqRewriter`` usage with plain string:
+
+.. sourcecode:: python
+
+   from cubicweb.web.views.urlrewrite import SimpleReqRewriter
+   class TrackerSimpleReqRewriter(SimpleReqRewriter):
+       rules = [
+        ('/versions', dict(vid='versionsinfo')),
+        ]
+
+When the url is `<base_url>/versions`, the view with the __regid__ `versionsinfo` is displayed.
+
+Here is an example of ``SimpleReqRewriter`` usage with regular expressions:
+
+.. sourcecode:: python
+
+    from cubicweb.web.views.urlrewrite import (
+        SimpleReqRewriter, rgx)
+
+    class BlogReqRewriter(SimpleReqRewriter):
+        rules = [
+            (rgx('/blogentry/([a-z_]+)\.rss'),
+             dict(rql=('Any X ORDERBY CD DESC LIMIT 20 WHERE X is BlogEntry,'
+                       'X creation_date CD, X created_by U, '
+                       'U login "%(user)s"'
+                       % {'user': r'\1'}), vid='rss'))
+            ]
+
+When a url matches the regular expression, the view with the __regid__
+`rss` which match the result set is displayed.
+
+Here is an example of ``SchemaBasedRewriter`` usage:
+
+.. sourcecode:: python
+
+    from cubicweb.web.views.urlrewrite import (
+        SchemaBasedRewriter, rgx, build_rset)
+
+    class TrackerURLRewriter(SchemaBasedRewriter):
+        rules = [
+            (rgx('/project/([^/]+)/([^/]+)/tests'),
+             build_rset(rql='Version X WHERE X version_of P, P name %(project)s, X num %(num)s',
+                        rgxgroups=[('project', 1), ('num', 2)], vid='versiontests')),
+            ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/views.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,120 @@
+
+.. _Views:
+
+Principles
+----------
+
+We'll start with a description of the interface providing a basic
+understanding of the available classes and methods, then detail the
+view selection principle.
+
+A `View` is an object responsible for the rendering of data from the
+model into an end-user consummable form. They typically churn out an
+XHTML stream, but there are views concerned with email other non-html
+outputs.
+
+.. _views_base_class:
+
+Discovering possible views
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible to configure the web user interface to have a left box
+showing all the views than can be applied to the current result set.
+
+To enable this, click on your login at the top right corner. Chose
+"user preferences", then "boxes", then "possible views box" and check
+"visible = yes" before validating your changes.
+
+The views listed there we either not selected because of a lower
+score, or they were deliberately excluded by the main template logic.
+
+
+Basic class for views
+~~~~~~~~~~~~~~~~~~~~~
+
+Class :class:`~cubicweb.view.View`
+``````````````````````````````````
+
+.. autoclass:: cubicweb.view.View
+
+The basic interface for views is as follows (remember that the result
+set has a tabular structure with rows and columns, hence cells):
+
+* `render(**context)`, render the view by calling `call` or
+  `cell_call` depending on the context
+
+* `call(**kwargs)`, call the view for a complete result set or null
+  (the default implementation calls `cell_call()` on each cell of the
+  result set)
+
+* `cell_call(row, col, **kwargs)`, call the view for a given cell of a
+  result set (`row` and `col` being integers used to access the cell)
+
+* `url()`, returns the URL enabling us to get the view with the current
+  result set
+
+* `wview(__vid, rset, __fallback_vid=None, **kwargs)`, call the view of
+  identifier `__vid` on the given result set. It is possible to give a
+  fallback view identifier that will be used if the requested view is
+  not applicable to the result set.
+
+* `html_headers()`, returns a list of HTML headers to be set by the
+  main template
+
+* `page_title()`, returns the title to use in the HTML header `title`
+
+Other basic view classes
+````````````````````````
+Here are some of the subclasses of :class:`~cubicweb.view.View` defined in :mod:`cubicweb.view`
+that are more concrete as they relate to data rendering within the application:
+
+.. autoclass:: cubicweb.view.EntityView
+.. autoclass:: cubicweb.view.StartupView
+.. autoclass:: cubicweb.view.EntityStartupView
+.. autoclass:: cubicweb.view.AnyRsetView
+
+Examples of views class
+```````````````````````
+
+- Using `templatable`, `content_type` and HTTP cache configuration
+
+.. sourcecode:: python
+
+    class RSSView(XMLView):
+        __regid__ = 'rss'
+        title = _('rss')
+        templatable = False
+        content_type = 'text/xml'
+        http_cache_manager = MaxAgeHTTPCacheManager
+        cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
+
+
+- Using a custom selector
+
+.. sourcecode:: python
+
+    class SearchForAssociationView(EntityView):
+        """view called by the edition view when the user asks
+        to search for something to link to the edited eid
+        """
+        __regid__ = 'search-associate'
+        title = _('search for association')
+        __select__ = one_line_rset() & match_search_state('linksearch') & is_instance('Any')
+
+
+XML views, binaries views...
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For views generating other formats than HTML (an image generated dynamically
+for example), and which can not simply be included in the HTML page generated
+by the main template (see above), you have to:
+
+* set the attribute `templatable` of the class to `False`
+* set, through the attribute `content_type` of the class, the MIME
+  type generated by the view to `application/octet-stream` or any
+  relevant and more specialised mime type
+
+For views dedicated to binary content creation (like dynamically generated
+images), we have to set the attribute `binary` of the class to `True` (which
+implies that `templatable == False`, so that the attribute `w` of the view could be
+replaced by a binary flow instead of unicode).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/wdoc.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,15 @@
+.. -*- coding: utf-8 -*-
+
+Online documentation system
+===========================
+
+.. automodule:: cubicweb.web.views.wdoc
+
+Help views
+----------
+.. autoclass:: cubicweb.web.views.wdoc.InlineHelpView
+
+Actions
+-------
+.. autoclass:: cubicweb.web.views.wdoc.HelpAction
+.. autoclass:: cubicweb.web.views.wdoc.AboutAction
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/xmlrss.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,66 @@
+.. _XmlAndRss:
+
+XML and RSS views
+-----------------
+
+(:mod:`cubicweb.web.views.xmlrss`)
+
+Overview
++++++++++
+
+*rss*
+    Creates a RSS/XML view and call the view `rssitem` for each entity of
+    the result set.
+
+*rssitem*
+    Create a RSS/XML view for each entity based on the results of the dublin core
+    methods of the entity (`dc_*`)
+
+RSS Channel Example
+++++++++++++++++++++
+
+Assuming you have several blog entries, click on the title of the
+search box in the left column. A larger search box should appear. Enter:
+
+.. sourcecode:: sql
+
+   Any X ORDERBY D WHERE X is BlogEntry, X creation_date D
+
+and you get a list of blog entries.
+
+Click on your login at the top right corner. Chose "user preferences",
+then "boxes", then "possible views box" and check "visible = yes"
+before validating your changes.
+
+Enter the same query in the search box and you will see the same list,
+plus a box titled "possible views" in the left column. Click on
+"entityview", then "RSS".
+
+You just applied the "RSS" view to the RQL selection you requested.
+
+That's it, you have a RSS channel for your blog.
+
+Try again with:
+
+.. sourcecode:: sql
+
+    Any X ORDERBY D WHERE X is BlogEntry, X creation_date D,
+    X entry_of B, B title "MyLife"
+
+Another RSS channel, but a bit more focused.
+
+A last one for the road:
+
+.. sourcecode:: sql
+
+    Any C ORDERBY D WHERE C is Comment, C creation_date D LIMIT 15
+
+displayed with the RSS view, that's a channel for the last fifteen
+comments posted.
+
+[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
Binary file doc/book/en/.static/cubicweb.png has changed
Binary file doc/book/en/.static/logilab.png has changed
--- a/doc/book/en/.static/sphinx-default.css	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,861 +0,0 @@
-/**
- * Sphinx Doc Design
- */
-
-html, body {
-    background: white;
-}
-
-body {
-    font-family: Verdana, sans-serif;
-    font-size: 100%;
-    background-color: white;
-    color: black;
-    margin: 0;
-    padding: 0;
-}
-
-/* :::: LAYOUT :::: */
-
-div.logilablogo {
-    padding: 10px 10px 10px 10px;
-    height:75;
-}
-
-
-div.document {
-    background-color: white;
-}
-
-div.documentwrapper {
-    float: left;
-    width: 100%;
-}
-
-div.bodywrapper {
-    margin: 0 0 0 230px;
-}
-
-div.body {
-    background-color: white;
-    padding: 0 20px 30px 20px;
-    border-left:solid;
-    border-left-color:#e2e2e2;
-    border-left-width:thin;
-}
-
-div.sphinxsidebarwrapper {
-    padding: 10px 5px 0 10px;
-}
-
-div.sphinxsidebar {
-    float: left;
-    width: 230px;
-    margin-left: -100%;
-    font-size: 90%;
-}
-
-div.clearer {
-    clear: both;
-}
-
-div.footer {
-    color: #ff4500;
-    width: 100%;
-    padding: 9px 0 9px 0;
-    text-align: center;
-    font-size: 75%;
-}
-
-div.footer a {
-    color: #ff4500;
-    text-decoration: underline;
-}
-
-div.related {
-    background-color: #ff7700;
-    color: white;
-    width: 100%;
-    height: 30px;
-    line-height: 30px;
-    font-size: 90%;
-}
-
-div.related h3 {
-    display: none;
-}
-
-div.related ul {
-    margin: 0;
-    padding: 0 0 0 10px;
-    list-style: none;
-}
-
-div.related li {
-    display: inline;
-}
-
-div.related li.right {
-    float: right;
-    margin-right: 5px;
-}
-
-div.related a {
-    color: white;
-    font-weight:bold;
-}
-
-/* ::: TOC :::: */
-
-div.sphinxsidebar {
-    border-style:solid;
-    border-color: white;
-/*    background-color:#e2e2e2;*/
-    padding-bottom:5px;
-}
-
-div.sphinxsidebar h3 {
-    font-family: Verdana, sans-serif;
-    color: black;
-    font-size: 1.2em;
-    font-weight: normal;
-    margin: 0;
-    padding: 0;
-    font-weight:bold;
-    font-style:italic;
-}
-
-div.sphinxsidebar h4 {
-    font-family: Verdana, sans-serif;
-    color: black;
-    font-size: 1.1em;
-    font-weight: normal;
-    margin: 5px 0 0 0;
-    padding: 0;
-    font-weight:bold;
-    font-style:italic;
-}
-
-div.sphinxsidebar p {
-    color: black;
-}
-
-div.sphinxsidebar p.topless {
-    margin: 5px 10px 10px 10px;
-}
-
-div.sphinxsidebar ul {
-    margin: 10px;
-    padding: 0;
-    list-style: none;
-    color: black;
-}
-
-div.sphinxsidebar ul ul,
-div.sphinxsidebar ul.want-points {
-    margin-left: 20px;
-    list-style: square;
-}
-
-div.sphinxsidebar ul ul {
-    margin-top: 0;
-    margin-bottom: 0;
-}
-
-div.sphinxsidebar a {
-    color: black;
-    text-decoration: none;
-}
-
-div.sphinxsidebar form {
-    margin-top: 10px;
-}
-
-div.sphinxsidebar input {
-    border: 1px solid #e2e2e2;
-    font-family: sans-serif;
-    font-size: 1em;
-    padding-bottom: 5px;
-}
-
-/* :::: MODULE CLOUD :::: */
-div.modulecloud {
-    margin: -5px 10px 5px 10px;
-    padding: 10px;
-    line-height: 160%;
-    border: 1px solid #cbe7e5;
-    background-color: #f2fbfd;
-}
-
-div.modulecloud a {
-    padding: 0 5px 0 5px;
-}
-
-/* :::: SEARCH :::: */
-ul.search {
-    margin: 10px 0 0 20px;
-    padding: 0;
-}
-
-ul.search li {
-    padding: 5px 0 5px 20px;
-    background-image: url(file.png);
-    background-repeat: no-repeat;
-    background-position: 0 7px;
-}
-
-ul.search li a {
-    font-weight: bold;
-}
-
-ul.search li div.context {
-    color: #888;
-    margin: 2px 0 0 30px;
-    text-align: left;
-}
-
-ul.keywordmatches li.goodmatch a {
-    font-weight: bold;
-}
-
-/* :::: COMMON FORM STYLES :::: */
-
-div.actions {
-    padding: 5px 10px 5px 10px;
-    border-top: 1px solid #cbe7e5;
-    border-bottom: 1px solid #cbe7e5;
-    background-color: #e0f6f4;
-}
-
-form dl {
-    color: #333;
-}
-
-form dt {
-    clear: both;
-    float: left;
-    min-width: 110px;
-    margin-right: 10px;
-    padding-top: 2px;
-}
-
-input#homepage {
-    display: none;
-}
-
-div.error {
-    margin: 5px 20px 0 0;
-    padding: 5px;
-    border: 1px solid #d00;
-    font-weight: bold;
-}
-
-/* :::: INLINE COMMENTS :::: */
-
-div.inlinecomments {
-    position: absolute;
-    right: 20px;
-}
-
-div.inlinecomments a.bubble {
-    display: block;
-    float: right;
-    background-image: url(style/comment.png);
-    background-repeat: no-repeat;
-    width: 25px;
-    height: 25px;
-    text-align: center;
-    padding-top: 3px;
-    font-size: 0.9em;
-    line-height: 14px;
-    font-weight: bold;
-    color: black;
-}
-
-div.inlinecomments a.bubble span {
-    display: none;
-}
-
-div.inlinecomments a.emptybubble {
-    background-image: url(style/nocomment.png);
-}
-
-div.inlinecomments a.bubble:hover {
-    background-image: url(style/hovercomment.png);
-    text-decoration: none;
-    color: #3ca0a4;
-}
-
-div.inlinecomments div.comments {
-    float: right;
-    margin: 25px 5px 0 0;
-    max-width: 50em;
-    min-width: 30em;
-    border: 1px solid #2eabb0;
-    background-color: #f2fbfd;
-    z-index: 150;
-}
-
-div#comments {
-    border: 1px solid #2eabb0;
-    margin-top: 20px;
-}
-
-div#comments div.nocomments {
-    padding: 10px;
-    font-weight: bold;
-}
-
-div.inlinecomments div.comments h3,
-div#comments h3 {
-    margin: 0;
-    padding: 0;
-    background-color: #2eabb0;
-    color: white;
-    border: none;
-    padding: 3px;
-}
-
-div.inlinecomments div.comments div.actions {
-    padding: 4px;
-    margin: 0;
-    border-top: none;
-}
-
-div#comments div.comment {
-    margin: 10px;
-    border: 1px solid #2eabb0;
-}
-
-div.inlinecomments div.comment h4,
-div.commentwindow div.comment h4,
-div#comments div.comment h4 {
-    margin: 10px 0 0 0;
-    background-color: #2eabb0;
-    color: white;
-    border: none;
-    padding: 1px 4px 1px 4px;
-}
-
-div#comments div.comment h4 {
-    margin: 0;
-}
-
-div#comments div.comment h4 a {
-    color: #d5f4f4;
-}
-
-div.inlinecomments div.comment div.text,
-div.commentwindow div.comment div.text,
-div#comments div.comment div.text {
-    margin: -5px 0 -5px 0;
-    padding: 0 10px 0 10px;
-}
-
-div.inlinecomments div.comment div.meta,
-div.commentwindow div.comment div.meta,
-div#comments div.comment div.meta {
-    text-align: right;
-    padding: 2px 10px 2px 0;
-    font-size: 95%;
-    color: #538893;
-    border-top: 1px solid #cbe7e5;
-    background-color: #e0f6f4;
-}
-
-div.commentwindow {
-    position: absolute;
-    width: 500px;
-    border: 1px solid #cbe7e5;
-    background-color: #f2fbfd;
-    display: none;
-    z-index: 130;
-}
-
-div.commentwindow h3 {
-    margin: 0;
-    background-color: #2eabb0;
-    color: white;
-    border: none;
-    padding: 5px;
-    font-size: 1.5em;
-    cursor: pointer;
-}
-
-div.commentwindow div.actions {
-    margin: 10px -10px 0 -10px;
-    padding: 4px 10px 4px 10px;
-    color: #538893;
-}
-
-div.commentwindow div.actions input {
-    border: 1px solid #2eabb0;
-    background-color: white;
-    color: #135355;
-    cursor: pointer;
-}
-
-div.commentwindow div.form {
-    padding: 0 10px 0 10px;
-}
-
-div.commentwindow div.form input,
-div.commentwindow div.form textarea {
-    border: 1px solid #3c9ea2;
-    background-color: white;
-    color: black;
-}
-
-div.commentwindow div.error {
-    margin: 10px 5px 10px 5px;
-    background-color: #fbe5dc;
-    display: none;
-}
-
-div.commentwindow div.form textarea {
-    width: 99%;
-}
-
-div.commentwindow div.preview {
-    margin: 10px 0 10px 0;
-    background-color: #70d0d4;
-    padding: 0 1px 1px 25px;
-}
-
-div.commentwindow div.preview h4 {
-    margin: 0 0 -5px -20px;
-    padding: 4px 0 0 4px;
-    color: white;
-    font-size: 1.3em;
-}
-
-div.commentwindow div.preview div.comment {
-    background-color: #f2fbfd;
-}
-
-div.commentwindow div.preview div.comment h4 {
-    margin: 10px 0 0 0!important;
-    padding: 1px 4px 1px 4px!important;
-    font-size: 1.2em;
-}
-
-/* :::: SUGGEST CHANGES :::: */
-div#suggest-changes-box input, div#suggest-changes-box textarea {
-    border: 1px solid #ccc;
-    background-color: white;
-    color: black;
-}
-
-div#suggest-changes-box textarea {
-    width: 99%;
-    height: 400px;
-}
-
-
-/* :::: PREVIEW :::: */
-div.preview {
-    background-image: url(style/preview.png);
-    padding: 0 20px 20px 20px;
-    margin-bottom: 30px;
-}
-
-
-/* :::: INDEX PAGE :::: */
-
-table.contentstable {
-    width: 90%;
-}
-
-table.contentstable p.biglink {
-    line-height: 150%;
-}
-
-a.biglink {
-    font-size: 1.3em;
-}
-
-span.linkdescr {
-    font-style: italic;
-    padding-top: 5px;
-    font-size: 90%;
-}
-
-/* :::: INDEX STYLES :::: */
-
-table.indextable td {
-    text-align: left;
-    vertical-align: top;
-}
-
-table.indextable dl, table.indextable dd {
-    margin-top: 0;
-    margin-bottom: 0;
-}
-
-table.indextable tr.pcap {
-    height: 10px;
-}
-
-table.indextable tr.cap {
-    margin-top: 10px;
-    background-color: #f2f2f2;
-}
-
-img.toggler {
-    margin-right: 3px;
-    margin-top: 3px;
-    cursor: pointer;
-}
-
-form.pfform {
-    margin: 10px 0 20px 0;
-}
-
-/* :::: GLOBAL STYLES :::: */
-
-.docwarning {
-    background-color: #ffe4e4;
-    padding: 10px;
-    margin: 0 -20px 0 -20px;
-    border-bottom: 1px solid #f66;
-}
-
-p.subhead {
-    font-weight: bold;
-    margin-top: 20px;
-}
-
-a {
-    color: orangered;
-    text-decoration: none;
-}
-
-a:hover {
-    text-decoration: underline;
-}
-
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
-    font-family: 'Verdana', sans-serif;
-    background-color: white;
-    font-weight: bold;
-    color: black;
-    border-bottom: 1px solid #ccc;
-    margin: 20px -20px 10px -20px;
-    padding: 3px 0 3px 10px;
-}
-
-div.body h1 { margin-top: 10pt; font-size: 150%; }
-div.body h2 { font-size: 120%; }
-div.body h3 { font-size: 100%; }
-div.body h4 { font-size: 80%; }
-div.body h5 { font-size: 600%; }
-div.body h6 { font-size: 40%; }
-
-a.headerlink {
-    color: #c60f0f;
-    font-size: 0.8em;
-    padding: 0 4px 0 4px;
-    text-decoration: none;
-    visibility: hidden;
-}
-
-h1:hover > a.headerlink,
-h2:hover > a.headerlink,
-h3:hover > a.headerlink,
-h4:hover > a.headerlink,
-h5:hover > a.headerlink,
-h6:hover > a.headerlink,
-dt:hover > a.headerlink {
-    visibility: visible;
-}
-
-a.headerlink:hover {
-    background-color: #c60f0f;
-    color: white;
-}
-
-div.body p, div.body dd, div.body li {
-    text-align: justify;
-    line-height: 130%;
-}
-
-div.body p.caption {
-    text-align: inherit;
-}
-
-div.body td {
-    text-align: left;
-}
-
-ul.fakelist {
-    list-style: none;
-    margin: 10px 0 10px 20px;
-    padding: 0;
-}
-
-.field-list ul {
-    padding-left: 1em;
-}
-
-.first {
-    margin-top: 0 !important;
-}
-
-/* "Footnotes" heading */
-p.rubric {
-    margin-top: 30px;
-    font-weight: bold;
-}
-
-/* "Topics" */
-
-div.topic {
-    background-color: #eee;
-    border: 1px solid #ccc;
-    padding: 0 7px 0 7px;
-    margin: 10px 0 10px 0;
-}
-
-p.topic-title {
-    font-size: 1.1em;
-    font-weight: bold;
-    margin-top: 10px;
-}
-
-/* Admonitions */
-
-div.admonition {
-    margin-top: 10px;
-    margin-bottom: 10px;
-    padding: 7px;
-}
-
-div.admonition dt {
-    font-weight: bold;
-}
-
-div.admonition dl {
-    margin-bottom: 0;
-}
-
-div.admonition p {
-    display: inline;
-}
-
-div.seealso {
-    background-color: #ffc;
-    border: 1px solid #ff6;
-}
-
-div.warning {
-    background-color: #ffe4e4;
-    border: 1px solid #f66;
-}
-
-div.note {
-    background-color: #eee;
-    border: 1px solid #ccc;
-}
-
-p.admonition-title {
-    margin: 0px 10px 5px 0px;
-    font-weight: bold;
-    display: inline;
-}
-
-p.admonition-title:after {
-    content: ":";
-}
-
-div.body p.centered {
-    text-align: center;
-    margin-top: 25px;
-}
-
-table.docutils {
-    border: 0;
-}
-
-table.docutils td, table.docutils th {
-    padding: 1px 8px 1px 0;
-    border-top: 0;
-    border-left: 0;
-    border-right: 0;
-    border-bottom: 1px solid #aaa;
-}
-
-table.field-list td, table.field-list th {
-    border: 0 !important;
-}
-
-table.footnote td, table.footnote th {
-    border: 0 !important;
-}
-
-.field-list ul {
-    margin: 0;
-    padding-left: 1em;
-}
-
-.field-list p {
-    margin: 0;
-}
-
-dl {
-    margin-bottom: 15px;
-    clear: both;
-}
-
-dd p {
-    margin-top: 0px;
-}
-
-dd ul, dd table {
-    margin-bottom: 10px;
-}
-
-dd {
-    margin-top: 3px;
-    margin-bottom: 10px;
-    margin-left: 30px;
-}
-
-.refcount {
-    color: #060;
-}
-
-dt:target,
-.highlight {
-    background-color: #fbe54e;
-}
-
-dl.glossary dt {
-    font-weight: bold;
-    font-size: 1.1em;
-}
-
-th {
-    text-align: left;
-    padding-right: 5px;
-}
-
-pre {
-    padding: 5px;
-    background-color: #efc;
-    color: #333;
-    border: 1px solid #ac9;
-    border-left: none;
-    border-right: none;
-    overflow: auto;
-}
-
-td.linenos pre {
-    padding: 5px 0px;
-    border: 0;
-    background-color: transparent;
-    color: #aaa;
-}
-
-table.highlighttable {
-    margin-left: 0.5em;
-}
-
-table.highlighttable td {
-    padding: 0 0.5em 0 0.5em;
-}
-
-tt {
-    background-color: #ecf0f3;
-    padding: 0 1px 0 1px;
-    font-size: 0.95em;
-}
-
-tt.descname {
-    background-color: transparent;
-    font-weight: bold;
-    font-size: 1.2em;
-}
-
-tt.descclassname {
-    background-color: transparent;
-}
-
-tt.xref, a tt {
-    background-color: transparent;
-    font-weight: bold;
-}
-
-.footnote:target  { background-color: #ffa }
-
-h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
-    background-color: transparent;
-}
-
-.optional {
-    font-size: 1.3em;
-}
-
-.versionmodified {
-    font-style: italic;
-}
-
-form.comment {
-    margin: 0;
-    padding: 10px 30px 10px 30px;
-    background-color: #eee;
-}
-
-form.comment h3 {
-    background-color: #326591;
-    color: white;
-    margin: -10px -30px 10px -30px;
-    padding: 5px;
-    font-size: 1.4em;
-}
-
-form.comment input,
-form.comment textarea {
-    border: 1px solid #ccc;
-    padding: 2px;
-    font-family: sans-serif;
-    font-size: 100%;
-}
-
-form.comment input[type="text"] {
-    width: 240px;
-}
-
-form.comment textarea {
-    width: 100%;
-    height: 200px;
-    margin-bottom: 10px;
-}
-
-.system-message {
-    background-color: #fda;
-    padding: 5px;
-    border: 3px solid red;
-}
-
-/* :::: PRINT :::: */
-@media print {
-    div.document,
-    div.documentwrapper,
-    div.bodywrapper {
-        margin: 0;
-        width : 100%;
-    }
-
-    div.sphinxsidebar,
-    div.related,
-    div.footer,
-    div#comments div.new-comment-box,
-    #top-link {
-        display: none;
-    }
-}
--- a/doc/book/en/.templates/layout.html	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-{%- block doctype -%}
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-{%- endblock %}
-{%- set reldelim1 = reldelim1 is not defined and ' &raquo;' or reldelim1 %}
-{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
-{%- macro relbar() %}
-    <div class="related">
-      <h3>Navigation</h3>
-      <ul>
-        {%- for rellink in rellinks %}
-        <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
-          <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags }}"
-             accesskey="{{ rellink[2] }}">{{ rellink[3] }}</a>
-          {%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
-        {%- endfor %}
-        {%- block rootrellink %}
-        <li><a href="{{ pathto('index') }}">{{ shorttitle }}</a>{{ reldelim1 }}</li>
-        {%- endblock %}
-        {%- for parent in parents %}
-          <li><a href="{{ parent.link|e }}" accesskey="U">{{ parent.title }}</a>{{ reldelim1 }}</li>
-        {%- endfor %}
-        {%- block relbaritems %}{% endblock %}
-      </ul>
-    </div>
-{%- endmacro %}
-{%- macro sidebar() %}
-      {%- if builder != 'htmlhelp' %}
-      <div class="sphinxsidebar">
-        <div class="sphinxsidebarwrapper">
-          {%- block sidebarlogo %}
-          {%- if logo %}
-            <p class="logo"><img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/></p>
-          {%- endif %}
-          {%- endblock %}
-          {%- block sidebartoc %}
-          {%- if display_toc %}
-            <h3>Table Of Contents</h3>
-            {{ toc }}
-          {%- endif %}
-          {%- endblock %}
-          {%- block sidebarrel %}
-          {%- if prev %}
-            <h4>Previous topic</h4>
-            <p class="topless"><a href="{{ prev.link|e }}" title="previous chapter">{{ prev.title }}</a></p>
-          {%- endif %}
-          {%- if next %}
-            <h4>Next topic</h4>
-            <p class="topless"><a href="{{ next.link|e }}" title="next chapter">{{ next.title }}</a></p>
-          {%- endif %}
-          {%- endblock %}
-          {%- if sourcename %}
-            <!--<h3>This Page</h3>
-            <ul class="this-page-menu">
-            {%- if builder == 'web' %}
-              <li><a href="#comments">Comments ({{ comments|length }} so far)</a></li>
-              <li><a href="{{ pathto('@edit/' + sourcename)|e }}">Suggest Change</a></li>
-              <li><a href="{{ pathto('@source/' + sourcename)|e }}">Show Source</a></li>
-            {%- elif builder == 'html' %}
-              <li><a href="{{ pathto('_sources/' + sourcename, true)|e }}">Show Source</a></li>
-            {%- endif %}
-            </ul>-->
-          {%- endif %}
-          {%- if customsidebar %}
-          {{ rendertemplate(customsidebar) }}
-          {%- endif %}
-          {%- block sidebarsearch %}
-          {%- if pagename != "search" %}
-            <h3>{{ builder == 'web' and 'Keyword' or 'Quick' }} search</h3>
-            <form class="search" action="{{ pathto('search') }}" method="get">
-              <input type="text" name="q" size="18" /> <input type="submit" value="Go" />
-              <input type="hidden" name="check_keywords" value="yes" />
-              <input type="hidden" name="area" value="default" />
-            </form>
-            {%- if builder == 'web' %}
-            <p style="font-size: 90%">Enter a module, class or function name.</p>
-            {%- endif %}
-          {%- endif %}
-          {%- endblock %}
-        </div>
-      </div>
-      {%- endif %}
-{%- endmacro -%}
-
-<html xmlns="http://www.w3.org/1999/xhtml">
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-    {%- if builder != 'htmlhelp' %}
-      {%- set titlesuffix = " &mdash; " + docstitle %}
-    {%- endif %}
-    <title>{{ title|striptags }}{{ titlesuffix }}</title>
-    {%- if builder == 'web' %}
-    <link rel="stylesheet" href="{{ pathto('index') }}?do=stylesheet{%
-      if in_admin_panel %}&admin=yes{% endif %}" type="text/css" />
-    {%- for link, type, title in page_links %}
-    <link rel="alternate" type="{{ type|e(true) }}" title="{{ title|e(true) }}" href="{{ link|e(true) }}" />
-    {%- endfor %}
-    {%- else %}
-    <link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
-    <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
-    {%- endif %}
-    {%- if builder != 'htmlhelp' %}
-    <script type="text/javascript">
-      var DOCUMENTATION_OPTIONS = {
-          URL_ROOT:    '{{ pathto("", 1) }}',
-          VERSION:     '{{ release }}',
-          COLLAPSE_MODINDEX: false,
-          FILE_SUFFIX: '{{ file_suffix }}'
-      };
-    </script>
-    <script type="text/javascript" src="{{ pathto('_static/jquery.js', 1) }}"></script>
-    <script type="text/javascript" src="{{ pathto('_static/interface.js', 1) }}"></script>
-    <script type="text/javascript" src="{{ pathto('_static/doctools.js', 1) }}"></script>
-    <script type="text/javascript" src="{{ pathto('_static/searchtools.js', 1) }}"></script>
-    {%- if use_opensearch %}
-    <link rel="search" type="application/opensearchdescription+xml"
-          title="Search within {{ docstitle }}"
-          href="{{ pathto('_static/opensearch.xml', 1) }}"/>
-    {%- endif %}
-    {%- if favicon %}
-    <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
-    {%- endif %}
-    {%- endif %}
-{%- block rellinks %}
-    {%- if hasdoc('about') %}
-    <link rel="author" title="About these documents" href="{{ pathto('about') }}" />
-    {%- endif %}
-    <link rel="contents" title="Global table of contents" href="{{ pathto('contents') }}" />
-    <link rel="index" title="Global index" href="{{ pathto('genindex') }}" />
-    <link rel="search" title="Search" href="{{ pathto('search') }}" />
-    {%- if hasdoc('copyright') %}
-    <link rel="copyright" title="Copyright" href="{{ pathto('copyright') }}" />
-    {%- endif %}
-    <link rel="top" title="{{ docstitle }}" href="{{ pathto('index') }}" />
-    {%- if parents %}
-    <link rel="up" title="{{ parents[-1].title|striptags }}" href="{{ parents[-1].link|e }}" />
-    {%- endif %}
-    {%- if next %}
-    <link rel="next" title="{{ next.title|striptags }}" href="{{ next.link|e }}" />
-    {%- endif %}
-    {%- if prev %}
-    <link rel="prev" title="{{ prev.title|striptags }}" href="{{ prev.link|e }}" />
-    {%- endif %}
-{%- endblock %}
-{%- block extrahead %}{% endblock %}
-  </head>
-  <body>
-
-{% block logilablogo %}
-<div class="logilablogo">
-	<a class="logogo" href="http://www.cubicweb.org"><img border="0" src="{{ pathto('_static/cubicweb.png', 1) }}"/></a>
-  </div>
-{% endblock %}
-
-{%- block relbar1 %}{{ relbar() }}{% endblock %}
-
-{%- block sidebar1 %}{# possible location for sidebar #}{% endblock %}
-
-{%- block document %}
-    <div class="document">
-      <div class="documentwrapper">
-      {%- if builder != 'htmlhelp' %}
-        <div class="bodywrapper">
-      {%- endif %}
-          <div class="body">
-            {% block body %}{% endblock %}
-          </div>
-      {%- if builder != 'htmlhelp' %}
-        </div>
-      {%- endif %}
-      </div>
-{%- endblock %}
-
-{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
-      <div class="clearer"></div>
-    </div>
-
-{%- block relbar2 %}{{ relbar() }}{% endblock %}
-
-{%- block footer %}
-    <div class="footer">
-    {%- if hasdoc('copyright') %}
-      &copy; <a href="{{ pathto('copyright') }}">Copyright</a> {{ copyright }}.
-    {%- else %}
-      &copy; Copyright {{ copyright }}.
-    {%- endif %}
-    {%- if last_updated %}
-      Last updated on {{ last_updated }}.
-    {%- endif %}
-    {%- if show_sphinx %}
-      Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
-    {%- endif %}
-    </div>
-{%- endblock %}
-  </body>
-</html>
--- a/doc/book/en/MERGE_ME-tut-create-app.en.txt	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,386 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-
-Tutoriel : créer votre première application web pour Google AppEngine
-=====================================================================
-
-[TRANSLATE ME TO FRENCH]
-
-This tutorial will guide you step by step to build a blog application 
-and discover the unique features of `LAX`. It assumes that you followed
-the :ref:`installation` guidelines and that both the `AppEngine SDK` and the
-`LAX` framework are setup on your computer.
-
-Creating a new application
---------------------------
-
-We choosed in this tutorial to develop a blog as an example of web application
-and will go through each required steps/actions to have it running with `LAX`.
-When you installed `LAX`, you saw a directory named ``skel``. Make a copy of
-this directory and call it ``BlogDemo``.
-
-The location of this directory does not matter. But once decided, make sure your ``PYTHONPATH`` is properly set (:ref:`installation`).
-
-
-Defining a schema
------------------
-
-With `LAX`, the schema/datamodel is the core of the application. This is where
-you will define the type of content you have to hanlde in your application.
-
-Let us start with something simple and improve on it iteratively. 
-
-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 by the class EntityType 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`.
-
-Running the application
------------------------
-
-Defining this simple schema is enough to get us started. Make sure you
-followed the setup steps described in detail in the installation
-chapter (especially visiting http://localhost:8080/_load as an
-administrator), then launch the application with the command::
-
-   python dev_appserver.py BlogDemo
-
-and point your browser at http://localhost:8080/ (if it is easier for
-you, use the on-line demo at http://lax.appspot.com/).
-
-.. image:: images/lax-book.00-login.en.png
-   :alt: login screen
-
-After you log in, you will see the home page of your application. It
-lists the entity types: Blog and BlogEntry. If these links read
-``blog_plural`` and ``blogentry_plural`` it is because
-internationalization (i18n) is not working for you yet. Please ignore
-this for now.
-
-.. image:: images/lax-book.01-start.en.png
-   :alt: home page
-
-Creating system entities
-------------------------
-You can only create new users if you decided not to use google authentication.
-
-
-[WRITE ME : create users manages permissions etc]
-
-
-
-Creating application entites
-----------------------------
-
-Create a Blog
-~~~~~~~~~~~~~
-
-Let us create a few of these entities. 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 ``Validate``.
-
-.. image:: images/lax-book.02-create-blog.en.png
-   :alt: from to create blog
-
-Click on the logo at top left to get back to the home page, then
-follow the Blog link that will list for you all the existing Blog.
-You should be seeing a list with a single item ``Tech-blog`` you
-just created.
-
-.. image:: images/lax-book.03-list-one-blog.en.png
-   :alt: displaying a list of a single 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``.
-
-.. image:: images/lax-book.04-detail-one-blog.en.png
-  :alt: displaying the detailed view of a blog
-
-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.
-
-.. image:: images/lax-book.05-list-two-blog.en.png
-   :alt: displaying a list of two blogs
-
-
-Create a BlogEntry
-~~~~~~~~~~~~~~~~~~
-
-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 ``Validate``. 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``. 
-
-You could also have, at the time you started to fill the form for a
-new entity BlogEntry, hit ``Apply`` instead of ``Validate`` and the 
-combobox titled ``add relation`` would have showed up.
-
-.. image:: images/lax-book.06-add-relation-entryof.en.png
-   :alt: editing a blog entry to add a relation to a blog
-
-Validate the changes by clicking ``Validate``. The entity BlogEntry
-that is displayed now includes a link to the entity Blog named
-``MyLife``.
-
-.. image:: images/lax-book.07-detail-one-blogentry.en.png
-   :alt: displaying the detailed view of a blogentry
-
-Remember that all of this was handled by the framework and that the
-only input that was provided so far is the schema. To get a graphical
-view of the schema, run the ``laxctl genschema BlogDemo`` command as
-explained in the installation section and point your browser to the
-URL http://localhost:8080/schema
-
-.. image:: images/lax-book.08-schema.en.png
-   :alt: graphical view of the schema (aka data-model)
-
-Site configuration
-------------------
-
-.. image:: images/lax-book.03-site-config-panel.en.png
-
-This panel allows you to configure the appearance of your application site.
-Six menus are available and we will go through each of them to explain how
-to use them.
-
-Navigation
-~~~~~~~~~~
-This menu provides you a way to adjust some navigation options depending on
-your needs, such as the number of entities to display by page of results.
-Follows the detailled list of available options:
-  
-* navigation.combobox-limit: maximum number of entities to display in related
-  combo box (sample format: 23)
-* navigation.page-size: maximum number of objects displayed by page of results 
-  (sample format: 23)
-* navigation.related-limit: maximum number of related entities to display in 
-  the primary view (sample format: 23)
-* navigation.short-line-size: maximum number of characters in short description
-  (sample format: 23)
-
-UI
-~~
-This menu provides you a way to customize the user interface settings such as
-date format or encoding in the produced html.
-Follows the detailled list of available options:
-
-* ui.date-format : how to format date in the ui ("man strftime" for format description)
-* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
-  description)
-* ui.default-text-format : default text format for rich text fields.
-* ui.encoding : user interface encoding
-* ui.fckeditor : 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.
-* ui.float-format : how to format float numbers in the ui
-* ui.language : language of the user interface
-* ui.main-template : id of main template used to render pages
-* ui.site-title	: site title, which is displayed right next to the logo in the header
-* ui.time-format : how to format time in the ui ("man strftime" for format description)
-
-
-Actions
-~~~~~~~
-This menu provides a way to configure the context in which you expect the actions
-to be displayed to the user and if you want the action to be visible or not. 
-You must have notice that when you view a list of entities, an action box is 
-available on the left column which display some actions as well as a drop-down 
-menu for more actions. 
-
-The context available are:
-
-* mainactions : actions listed in the left box
-* moreactions : actions listed in the `more` menu of the left box
-* addrelated : add actions listed in the left box
-* useractions : actions listed in the first section of drop-down menu 
-  accessible from the right corner user login link
-* siteactions : actions listed in the second section of drop-down menu
-  accessible from the right corner user login link
-* hidden : select this to hide the specific action
-
-Boxes
-~~~~~
-The application has already a pre-defined set of boxes you can use right away. 
-This configuration section allows you to place those boxes where you want in the
-application interface to customize it. 
-
-The available boxes are:
-
-* actions box : box listing the applicable actions on the displayed data
-
-* boxes_blog_archives_box : box listing the blog archives 
-
-* possible views box : box listing the possible views for the displayed data
-
-* rss box : RSS icon to get displayed data as a RSS thread
-
-* search box : search box
-
-* startup views box : box listing the configuration options available for 
-  the application site, such as `Preferences` and `Site Configuration`
-
-Components
-~~~~~~~~~~
-[WRITE ME]
-
-Contextual components
-~~~~~~~~~~~~~~~~~~~~~
-[WRITE ME]
-
-Set-up a workflow
------------------
-
-Before starting, make sure you refresh your mind by reading [link to
-definition_workflow chapter].
-
-We want to create a workflow to control the quality of the BlogEntry 
-submitted on your application. When a BlogEntry is created by a user
-its state should be `submitted`. To be visible to all, it needs to
-be in the state `published`. To move from `submitted` to `published`
-we need a transition that we can name `approve_blogentry`.
-
-We do not want every user to be allowed to change the state of a 
-BlogEntry. We need to define a group of user, `moderators`, and 
-this group will have appropriate permissions to approve BlogEntry
-to be published and visible to all.
-
-There are two ways to create a workflow, form the user interface,
-and also by defining it in ``migration/postcreate.py``. This script
-is executed each time a new ``./bin/laxctl db-init`` is done. 
-If you create the states and transitions through the user interface
-this means that next time you will need to initialize the database
-you will have to re-create all the entities. 
-We strongly recommand you create the workflow in ``migration\postcreate.py``
-and we will now show you how.
-The user interface would only be a reference for you to view the states 
-and transitions but is not the appropriate interface to define your
-application workflow.
-
-Update the schema
-~~~~~~~~~~~~~~~~~
-To enable a BlogEntry to have a State, we have to define a relation
-``in_state`` in the schema of BlogEntry. Please do as follows, add
-the line ``in_state (...)``::
-
-  class BlogEntry(EntityType):
-      title = String(maxsize=100, required=True)
-      publish_date = Date(default='TODAY')
-      text_format = String(meta=True, internationalizable=True, maxsize=50,
-                           default='text/rest', constraints=[format_constraint])
-      text = String(fulltextindexed=True)
-      category = String(vocabulary=('important','business'))
-      entry_of = SubjectRelation('Blog', cardinality='?*')
-      in_state = SubjectRelation('State', cardinality='1*')
-
-As you updated the schema, you will have re-execute ``./bin/laxctl db-init``
-to initialize the database and migrate your existing entities.
-[WRITE ABOUT MIGRATION]
-
-Create states, transitions and group permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-At the time the ``postcreate.py`` script is executed, several methods
-can be used. They are all defined in the ``class ServerMigrationHelper``.
-We will only discuss the method we use to create a wrokflow here.
-
-To define our workflow for BlogDemo, please add the following lines
-to ``migration/postcreate.py``::
-  
-  _ = unicode
-
-  moderators      = add_entity('CWGroup', name=u"moderators")
-
-  submitted = add_state(_('submitted'), 'BlogEntry', initial=True)
-  published = add_state(_('published'), 'BlogEntry')
-
-  add_transition(_('approve_blogentry'), 'BlogEntry', (submitted,), published, ('moderators', 'managers'),)
-
-  checkpoint()
-
-``add_entity`` is used here to define the new group of users that we
-need to define the transitions, `moderators`.
-If this group required by the transition is not defined before the
-transition is created, it will not create the relation `transition 
-require the group moderator`.
-
-``add_state`` expects as the first argument the name of the state you are
-willing to create, then the entity type on which the state can be applied, 
-and an optionnal argument to set if the state is the initial state
-of the entity type or not.
-
-``add_transition`` expects as the first argument the name of the 
-transition, then the entity type on which we can apply the transition,
-then the list of possible initial states from which the transition
-can be applied, the target state of the transition, and the permissions
-(e.g. list of the groups of users who can apply the transition).
-
-.. image:: images/lax-book.03-transitions-view.en.png
-
-You can now notice that in the actions box of a BlogEntry, the state
-is now listed as well as the possible transitions from this state
-defined by the workflow. This transition, as defined in the workflow,
-will only being displayed for the users belonging to the group
-moderators of managers.
-
-Change view permission
-~~~~~~~~~~~~~~~~~~~~~~
-
-
-
-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 ?
-~~~~~~~~~~~~~~
-
-Although the application is fully functionnal, its look is very
-basic. In the following section we will learn to create views to
-customize how data is displayed.
-
-
--- a/doc/book/en/MERGE_ME-tut-create-gae-app.en.txt	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _tutorielGAE:
-
-Tutoriel : créer votre première application web pour Google AppEngine
-=====================================================================
-
-Ce tutoriel va vous guider pas à pas a construire une apllication web 
-de gestion de Blog afin de vous faire découvrir les fonctionnalités de
-*CubicWeb*.
-
-Nous supposons que vous avec déjà suivi le guide :ref:`installationGAE`.
-
-
-Créez une nouvelle application
-------------------------------
-
-Nous choisissons dans ce tutoriel de développer un blog comme un exemple
-d'application web et nous allons expliciter toutes les étapes nécessaires
-à sa réalisation.  
-
-::
-  
-  cubicweb-ctl newgapp blogdemo
-
-`newgapp` est la commande permettant de créer une instance *CubicWeb* pour
-le datastore.
-
-Assurez-vous que votre variable d'environnement ``PYTHONPATH`` est correctement
-initialisée (:ref:`installationGAE`)
-
-Définissez un schéma
---------------------
-
-Le modèle de données ou schéma est au coeur d'une application *CubicWeb*.
-C'est là où vous allez devoir définir le type de contenu que votre application
-devra gérer.
-
-Commençons par un schéma simple que nous améliorerons progressivemment.
-
-Une fois votre instance ``blogdemo`` crée, vous trouverez un fichier ``schema.py``
-contenant la définition des entités suivantes : ``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='?*')
-
-
-Un ``Blog`` a un titre et une description. Le titre est une chaîne 
-de caractères requise par la classe parente EntityType and ne doit
-pas excéder 50 caractères. La description est une chaîne de 
-caractères sans contraintes.
-
-Une ``BlogEntry`` a un titre, une date de publication et du texte
-étant son contenu. Le titre est une chaîne de caractères qui ne 
-doit pas excéder 100 caractères. La date de publication est de type Date et a
-pour valeur par défaut TODAY, ce qui signifie que lorsqu'une 
-``BlogEntry`` sera créée, sa date de publication sera la date
-courante a moins de modifier ce champ. Le texte est une chaîne de
-caractères qui sera indexée en plein texte et sans contraintes.
-
-Une ``BlogEntry`` a aussi une relation nommée ``entry_of`` qui la
-relie à un ``Blog``. La cardinalité ``?*`` signifie que BlogEntry
-peut faire partie de zero a un Blog (``?`` signifie `zero ou un`) et
-qu'un Blog peut avoir une infinité de BlogEntry (``*`` signifie
-`n'importe quel nombre incluant zero`). 
-Par soucis de complétude, nous rappellerons que ``+`` signifie
-`un ou plus`.
-
-Lancez l'application
---------------------
-
-Définir ce simple schéma est suffisant pour commencer. Assurez-vous 
-que vous avez suivi les étapes décrites dans la section installation
-(en particulier visitez http://localhost:8080/_load en tant qu'administrateur
-afin d'initialiser le datastore), puis lancez votre application avec la commande ::
-   
-   python dev_appserver.py BlogDemo
-
-puis dirigez vous vers http://localhost:8080/ (ou si c'est plus facile
-vous pouvez utiliser la démo en ligne http://lax.appspot.com/).
-[FIXME] -- changer la demo en ligne en quelque chose qui marche (!)
-
-.. image:: images/lax-book.00-login.en.png
-   :alt: login screen
-
-Après vous être authentifié, vous arrivez sur la page d'accueil de votre 
-application. Cette page liste les types d'entités accessibles dans votre
-application, en l'occurrence : Blog et Articles. Si vous lisez ``blog_plural``
-et ``blogentry_plural`` cela signifie que l'internationalisation (i18n)
-n'a pas encore fonctionné. Ignorez cela pour le moment.
-
-.. image:: images/lax-book.01-start.en.png
-   :alt: home page
-
-Créez des entités système
--------------------------
-
-Vous ne pourrez créer de nouveaux utilisateurs que dans le cas où vous
-avez choisi de ne pas utiliser l'authentification Google.
-
-
-[WRITE ME : create users manages permissions etc]
-
-
-
-Créez des entités applicatives
-------------------------------
-
-Créez un Blog
-~~~~~~~~~~~~~
-
-Créons à présent quelques entités. Cliquez sur `[+]` sur la
-droite du lien Blog. Appelez cette nouvelle entité Blog ``Tech-Blog``
-et tapez pour la description ``everything about technology``,
-puis validez le formulaire d'édition en cliquant sur le bouton
-``Validate``.
-
-
-.. image:: images/lax-book.02-create-blog.en.png
-   :alt: from to create blog
-
-En cliquant sur le logo situé dans le coin gauche de la fenêtre,
-vous allez être redirigé vers la page d'accueil. Ensuite, si vous allez 
-sur le lien Blog, vous devriez voir la liste des entités Blog, en particulier
-celui que vous venez juste de créer ``Tech-Blog``.
-
-.. image:: images/lax-book.03-list-one-blog.en.png
-   :alt: displaying a list of a single blog
-
-Si vous cliquez sur ``Tech-Blog`` vous devriez obtenir une description
-détaillée, ce qui dans notre cas, n'est rien de plus que le titre
-et la phrase ``everything about technology``
-
-
-.. image:: images/lax-book.04-detail-one-blog.en.png
-   :alt: displaying the detailed view of a blog
-
-Maintenant retournons sur la page d'accueil et créons un nouveau
-Blog ``MyLife`` et retournons sur la page d'accueil, puis suivons
-le lien Blog et nous constatons qu'à présent deux blogs sont listés.
-
-.. image:: images/lax-book.05-list-two-blog.en.png
-   :alt: displaying a list of two blogs
-
-Créons un article
-~~~~~~~~~~~~~~~~~
-
-Revenons sur la page d'accueil et cliquons sur `[+]` à droite du lien
-`articles`. Appellons cette nouvelle entité ``Hello World`` et introduisons
-un peut de texte avant de ``Valider``. Vous venez d'ajouter un article
-sans avoir précisé à quel Blog il appartenait. Dans la colonne de gauche
-se trouve une boite intitulé ``actions``, cliquez sur le menu ``modifier``.
-Vous êtes de retour sur le formulaire d'édition de l'article que vous 
-venez de créer, à ceci près que ce formulaire a maintenant une nouvelle
-section intitulée ``ajouter relation``. Choisissez ``entry_of`` dans ce menu,
-cela va faire apparaitre une deuxième menu déroulant dans lequel vous
-allez pouvoir séléctionner le Blog ``MyLife``.
-
-Vous auriez pu aussi, au moment où vous avez crée votre article, sélectionner
-``appliquer`` au lieu de ``valider`` et le menu ``ajouter relation`` serait apparu.
-
-.. image:: images/lax-book.06-add-relation-entryof.en.png
-   :alt: editing a blog entry to add a relation to a blog
-
-Validez vos modifications en cliquant sur ``Valider``. L'entité article
-qui est listée contient maintenant un lien vers le Blog auquel il 
-appartient, ``MyLife``.
-
-.. image:: images/lax-book.07-detail-one-blogentry.en.png
-   :alt: displaying the detailed view of a blogentry
-
-Rappelez-vous que pour le moment, tout a été géré par la plate-forme
-*CubicWeb* et que la seule chose qui a été fournie est le schéma de
-données. D'ailleurs pour obtenir une vue graphique du schéma, exécutez
-la commande ``laxctl genschema blogdemo`` et vous pourrez visualiser
-votre schéma a l'URL suivante : http://localhost:8080/schema
-
-.. image:: images/lax-book.08-schema.en.png
-   :alt: graphical view of the schema (aka data-model)
-
-
-Change view permission
-~~~~~~~~~~~~~~~~~~~~~~
-
-
-
-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 ?
-~~~~~~~~~~~~~~
-
-Although the application is fully functionnal, its look is very
-basic. In the following section we will learn to create views to
-customize how data is displayed.
-
-
--- a/doc/book/en/_themes/cubicweb/layout.html	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-{% extends "basic/layout.html" %}
-
-{%- block extrahead %}
-<!--[if lte IE 6]>
-<link rel="stylesheet" href="{{ pathto('_static/ie6.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
-<![endif]-->
-{%- if theme_favicon %}
-<link rel="shortcut icon" href="{{ pathto('_static/'+theme_favicon, 1) }}"/>
-{%- endif %}
-
-{%- if theme_canonical_url %}
-<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
-{%- endif %}
-{% endblock %}
-
-{% block header %}
-
-{% if theme_in_progress|tobool %}
-    <img style="position: fixed; display: block; width: 165px; height: 165px; bottom: 60px; right: 0; border: 0;" src="{{ pathto('_static/in_progress.png', 1) }}" alt="Documentation in progress" />
-{% endif %}
-
-{% if theme_outdated|tobool %}
-    <div style="bottom: 60px; right: 20px;position: fixed;"><a href="{{ latest_url }}" class="btn btn-large btn-danger"><strong>&gt;</strong> Read the latest version of this page</a></div>
-{% endif %}
-
-<div class="header-small">
-	{%- if theme_logo %}
-	{% set img, ext = theme_logo.split('.', -1) %}
-	<div class="logo-small">
-		<a href="{{ pathto(master_doc) }}">
-      		<img class="logo" src="{{ pathto('_static/%s-small.%s' % (img, ext), 1)}}" alt="Logo"/>
-		</a>
-  	</div>
-  	{%- endif %}
-</div>
-{% endblock %}
-
-{%- macro relbar() %}
-<div class="related">
-	<h3>{{ _('Navigation') }}</h3>
-	<ul>
-		{%- for rellink in rellinks %}
-		<li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
-			<a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags|e }}"
-			{{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a>
-			{%- if not loop.first %}{{ reldelim2 }}{% endif %}
-		</li>
-		{%- endfor %}
-    	{%- block rootrellink %}
-    	<li><a href="{{ pathto(master_doc) }}">{{ docstitle|e }}</a>{{ reldelim1 }}</li>
-    	{%- endblock %}
-    	{%- for parent in parents %}
-          <li><a href="{{ parent.link|e }}" {% if loop.last %}{{ accesskey("U") }}{% endif %}>{{ parent.title }}</a>{{ reldelim1 }}</li>
-        {%- endfor %}
-        {%- block relbaritems %} {% endblock %}
-  	</ul>
-</div>
-{%- endmacro %}
-
-{%- block sidebarlogo %}{%- endblock %}
-{%- block sidebarsourcelink %}{%- endblock %}
--- a/doc/book/en/_themes/cubicweb/static/cubicweb.css_t	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-/*
- * cubicweb.css_t
- * ~~~~~~~~~~~~~~
- *
- * Sphinx stylesheet -- cubicweb theme.
- *
- * :copyright: Copyright 2014 by the Cubicweb team, see AUTHORS.
- * :license: LGPL, see LICENSE for details.
- *
- */
- 
-@import url("pyramid.css");
-
-div.header-small {
-  background-image: linear-gradient(white, #e2e2e2);
-  border-bottom: 1px solid #bbb;
-}
-
-div.logo-small {
-  padding: 10px;
-}
-
-img.logo {
-  width: 150px;
-}
-
-div.related a {
-  color: #e6820e;
-}
-
-a, a .pre {
-  color: #e6820e;
-}
--- a/doc/book/en/_themes/cubicweb/static/cubicweb.ico	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-../../../../../../web/data/favicon.ico
\ No newline at end of file
--- a/doc/book/en/_themes/cubicweb/static/logo-cubicweb-small.svg	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-logo-cubicweb.svg
\ No newline at end of file
--- a/doc/book/en/_themes/cubicweb/static/logo-cubicweb.svg	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-../../../../../../web/data/logo-cubicweb.svg
\ No newline at end of file
--- a/doc/book/en/_themes/cubicweb/theme.conf	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-[theme]
-inherit = pyramid
-pygments_style = sphinx.pygments_styles.PyramidStyle
-stylesheet = cubicweb.css
-
-
-[options]
-logo = logo-cubicweb.svg
-favicon = cubicweb.ico
-in_progress = false
-outdated = false
-canonical_url = 
--- a/doc/book/en/additionnal_services/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-Additional services
-===================
-
-In this chapter, we introduce services crossing the *web -
-repository - administration* organisation of the first parts of the
-CubicWeb book. Those services can be either proper services (like the
-undo functionality) or mere *topical cross-sections* across CubicWeb.
-
-.. toctree::
-   :maxdepth: 2
-
-   undo
-
-
--- a/doc/book/en/additionnal_services/undo.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,337 +0,0 @@
-Undoing changes in CubicWeb
----------------------------
-
-Many desktop applications offer the possibility for the user to
-undo its last changes : this *undo feature* has now been
-integrated into the CubicWeb framework. This document will
-introduce you to the *undo feature* both from the end-user and the
-application developer point of view.
-
-But because a semantic web application and a common desktop
-application are not the same thing at all, especially as far as
-undoing is concerned, we will first introduce *what* is the *undo
-feature* for now.
-
-What's *undoing* in a CubicWeb application
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-What is an *undo feature* is quite intuitive in the context of a
-desktop application. But it is a bit subtler in the context of a
-Semantic Web application. This section introduces some of the main
-differences between a classical desktop and a Semantic Web
-applications to keep in mind in order to state precisely *what we
-want*.
-
-The notion transactions
-```````````````````````
-
-A CubicWeb application acts upon an *Entity-Relationship* model,
-described by a schema. This allows to ensure some data integrity
-properties. It also implies that changes are made by all-or-none
-groups called *transactions*, such that the data integrity is
-preserved whether the transaction is completely applied *or* none
-of it is applied.
-
-A transaction can thus include more actions than just those
-directly required by the main purpose of the user.  For example,
-when a user *just* writes a new blog entry, the underlying
-*transaction* holds several *actions* as illustrated below :
-
-* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
-
-  #. Created Blog entry : Torototo
-  #. Added relation : Torototo owned by admin
-  #. Added relation : Torototo blog entry of Undo Blog
-  #. Added relation : Torototo in state draft (draft)
-  #. Added relation : Torototo created by admin
-
-Because of the very nature (all-or-none) of the transactions, the
-"undoable stuff" are the transactions and not the actions !
-
-Public and private actions within a transaction
-```````````````````````````````````````````````
-
-Actually, within the *transaction* "Created Blog entry :
-Torototo", two of those *actions* are said to be *public* and
-the others are said to be *private*. *Public* here means that the
-public actions (1 and 3) were directly requested by the end user ;
-whereas *private* means that the other actions (2, 4, 5) were
-triggered "under the hood" to fulfill various requirements for the
-user operation (ensuring integrity, security, ... ).
-
-And because quite a lot of actions can be triggered by a "simple"
-end-user request, most of which the end-user is not (and does not
-need or wish to be) aware, only the so-called public actions will
-appear [1]_ in the description of the an undoable transaction.
-
-* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
-
-  #. Created Blog entry : Torototo
-  #. Added relation : Torototo blog entry of Undo Blog
-
-But note that both public and private actions will be undone
-together when the transaction is undone.
-
-(In)dependent transactions : the simple case
-````````````````````````````````````````````
-
-A CubicWeb application can be used *simultaneously* by different users
-(whereas a single user works on an given office document at a
-given time), so that there is not always a single history
-time-line in the CubicWeb case. Moreover CubicWeb provides
-security through the mechanism of *permissions* granted to each
-user. This can lead to some transactions *not* being undoable in
-some contexts.
-
-In the simple case two (unprivileged) users Alice and Bob make
-relatively independent changes : then both Alice and Bob can undo
-their changes. But in some case there is a clean dependency
-between Alice's and Bob's actions or between actions of one of
-them. For example let's suppose that :
-
-- Alice has created a blog,
-- then has published a first post inside,
-- then Bob has published a second post in the same blog,
-- and finally Alice has updated its post contents.
-
-Then it is clear that Alice can undo her contents changes and Bob
-can undo his post creation independently. But Alice can not undo
-her post creation while she has not first undone her changes.
-It is also clear that Bob should *not* have the
-permissions to undo any of Alice's transactions.
-
-
-More complex dependencies between transactions
-``````````````````````````````````````````````
-
-But more surprising things can quickly happen. Going back to the
-previous example, Alice *can* undo the creation of the blog after
-Bob has published its post in it ! But this is possible only
-because the schema does not *require* for a post to be in a
-blog. Would the *blog entry of* relation have been mandatory, then
-Alice could not have undone the blog creation because it would
-have broken integrity constraint for Bob's post.
-
-When a user attempts to undo a transaction the system will check
-whether a later transaction has explicit dependency on the
-would-be-undone transaction. In this case the system will not even
-attempt the undo operation and inform the user.
-
-If no such dependency is detected the system will attempt the undo
-operation but it can fail, typically because of integrity
-constraint violations. In such a case the undo operation is
-completely [3]_ rollbacked.
-
-
-The *undo feature* for CubicWeb end-users
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The exposition of the undo feature to the end-user through a Web
-interface is still quite basic and will be improved toward a
-greater usability. But it is already fully functional.  For now
-there are two ways to access the *undo feature* as long as the it
-has been activated in the instance configuration file with the
-option *undo-support=yes*.
-
-Immediately after having done the change to be canceled through
-the **undo** link in the message. This allows to undo an
-hastily action immediately. For example, just after having
-validated the creation of the blog entry *A second blog entry* we
-get the following message, allowing to undo the creation.
-
-.. image:: /images/undo_mesage_w600.png
-   :width: 600px
-   :alt: Screenshot of the undo link in the message
-   :align: center
-
-At any time we can access the **undo-history view** accessible from the
-start-up page.
-
-.. image:: /images/undo_startup-link_w600.png
-   :width: 600px
-   :alt: Screenshot of the startup menu with access to the history view
-   :align: center
-
-This view will provide inspection of the transaction and their (public)
-actions. Each transaction provides its own **undo** link. Only the
-transactions the user has permissions to see and undo will be shown.
-
-.. image:: /images/undo_history-view_w600.png
-   :width: 600px
-   :alt: Screenshot of the undo history main view
-   :align: center
-
-If the user attempts to undo a transaction which can't be undone or
-whose undoing fails, then a message will explain the situation and
-no partial undoing will be left behind.
-
-This is all for the end-user side of the undo mechanism : this is
-quite simple indeed ! Now, in the following section, we are going
-to introduce the developer side of the undo mechanism.
-
-The *undo feature* for CubicWeb application developers
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A word of warning : this section is intended for developers,
-already having some knowledge of what's under CubicWeb's hood. If
-it is not *yet* the case, please refer to CubicWeb documentation
-http://docs.cubicweb.org/ .
-
-Overview
-````````
-
-The core of the undo mechanisms is at work in the *native source*,
-beyond the RQL. This does mean that *transactions* and *actions*
-are *no entities*. Instead they are represented at the SQL level
-and exposed through the *DB-API* supported by the repository
-*Connection* objects.
-
-Once the *undo feature* has been activated in the instance
-configuration file with the option *undo-support=yes*, each
-mutating operation (cf. [2]_) will be recorded in some special SQL
-table along with its associated transaction. Transaction are
-identified by a *txuuid* through which the functions of the
-*DB-API* handle them.
-
-On the web side the last commited transaction *txuuid* is
-remembered in the request's data to allow for imediate undoing
-whereas the *undo-history view* relies upon the *DB-API* to list
-the accessible transactions. The actual undoing is performed by
-the *UndoController* accessible at URL of the form
-`www.my.host/my/instance/undo?txuuid=...`
-
-The repository side
-```````````````````
-
-Please refer to the file `cubicweb/server/sources/native.py` and
-`cubicweb/transaction.py` for the details.
-
-The undoing information is mainly stored in three SQL tables:
-
-`transactions`
-    Stores the txuuid, the user eid and the date-and-time of
-    the transaction. This table is referenced by the two others.
-
-`tx_entity_actions`
-    Stores the undo information for actions on entities.
-
-`tx_relation_actions`
-    Stores the undo information for the actions on relations.
-
-When the undo support is activated, entries are added to those
-tables for each mutating operation on the data repository, and are
-deleted on each transaction undoing.
-
-Those table are accessible through the following methods of the
-repository `Connection` object :
-
-`undoable_transactions`
-    Returns a list of `Transaction` objects accessible to the user
-    and according to the specified filter(s) if any.
-
-`tx_info`
-    Returns a `Transaction` object from a `txuuid`
-
-`undo_transaction`
-    Returns the list of `Action` object for the given `txuuid`.
-
-    NB:  By default it only return *public* actions.
-
-The web side
-````````````
-
-The exposure of the *undo feature* to the end-user through the Web
-interface relies on the *DB-API* introduced above. This implies
-that the *transactions* and *actions* are not *entities* linked by
-*relations* on which the usual views can be applied directly.
-
-That's why the file `cubicweb/web/views/undohistory.py` defines
-some dedicated views to access the undo information :
-
-`UndoHistoryView`
-    This is a *StartupView*, the one accessible from the home
-    page of the instance which list all transactions.
-
-`UndoableTransactionView`
-    This view handles the display of a single `Transaction` object.
-
-`UndoableActionBaseView`
-    This (abstract) base class provides private methods to build
-    the display of actions whatever their nature.
-
-`Undoable[Add|Remove|Create|Delete|Update]ActionView`
-    Those views all inherit from `UndoableActionBaseView` and
-    each handles a specific kind of action.
-
-`UndoableActionPredicate`
-    This predicate is used as a *selector* to pick the appropriate
-    view for actions.
-
-Apart from this main *undo-history view* a `txuuid` is stored in
-the request's data `last_undoable_transaction` in order to allow
-immediate undoing of a hastily validated operation. This is
-handled in `cubicweb/web/application.py` in the `main_publish` and
-`add_undo_link_to_msg` methods for the storing and displaying
-respectively.
-
-Once the undo information is accessible, typically through a
-`txuuid` in an *undo* URL, the actual undo operation can be
-performed by the `UndoController` defined in
-`cubicweb/web/views/basecontrollers.py`. This controller basically
-extracts the `txuuid` and performs a call to `undo_transaction` and
-in case of an undo-specific error, lets the top level publisher
-handle it as a validation error.
-
-
-Conclusion
-~~~~~~~~~~
-
-The undo mechanism relies upon a low level recording of the
-mutating operation on the repository. Those records are accessible
-through some method added to the *DB-API* and exposed to the
-end-user either through a whole history view of through an
-immediate undoing link in the message box.
-
-The undo feature is functional but the interface and configuration
-options are still quite reduced. One major improvement would be to
-be able to filter with a finer grain which transactions or actions
-one wants to see in the *undo-history view*. Another critical
-improvement would be to enable the undo feature on a part only of
-the entity-relationship schema to avoid storing too much useless
-data and reduce the underlying overhead.
-
-But both functionality are related to the strong design choice not
-to represent transactions and actions as entities and
-relations. This has huge benefits in terms of safety and conceptual
-simplicity but prevents from using lots of convenient CubicWeb
-features such as *facets* to access undo information.
-
-Before developing further the undo feature or eventually revising
-this design choice, it appears that some return of experience is
-strongly needed. So don't hesitate to try the undo feature in your
-application and send us some feedback.
-
-
-Notes
-~~~~~
-
-.. [1] The end-user Web interface could be improved to enable
-       user to choose whether he wishes to see private actions.
-
-.. [2] There is only five kind of elementary actions (beyond
-       merely accessing data for reading):
-
-       * **C** : creating an entity
-       * **D** : deleting an entity
-       * **U** : updating an entity attributes
-       * **A** : adding a relation
-       * **R** : removing a relation
-
-.. [3] Meaning none of the actions in the transaction is
-       undone. Depending upon the application, it might make sense
-       to enable *partial* undo. That is to say undo in which some
-       actions could not be undo without preventing to undo the
-       others actions in the transaction (as long as it does not
-       break schema integrity). This is not forbidden by the
-       back-end but is deliberately not supported by the front-end
-       (for now at least).
--- a/doc/book/en/admin/additional-tips.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-
-.. _Additional Tips:
-
-Backups (mostly with postgresql)
---------------------------------
-
-It is always a good idea to backup. If your system does not do that,
-you should set it up. Note that whenever you do an upgrade,
-`cubicweb-ctl` offers you to backup your database.  There are a number
-of ways for doing backups.
-
-Using postgresql (and only that)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Before you
-go ahead, make sure the following permissions are correct ::
-
-   # chgrp postgres /var/lib/cubicweb/backup
-   # chmod g+ws /var/lib/cubicweb/backup
-   # chgrp postgres /etc/cubicweb.d/*<instance>*/sources
-   # chmod g+r /etc/cubicweb.d/*<instance>*/sources
-
-Simply use the pg_dump in a cron installed for `postgres` user on the database server::
-
-    # m h  dom mon dow   command
-    0 2 * * * pg_dump -Fc --username=cubicweb --no-owner <instance> > /var/backups/<instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
-
-Using :command:`cubicweb-ctl db-dump`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The CubicWeb way is to use the :command:`db-dump` command. For that,
-you have to put your passwords in a user-only-readable file at the
-home directory of root user.  The file is `.pgpass` (`chmod 0600`), in
-this case for a socket run connection to PostgreSQL ::
-
-    /var/run/postgresql:5432:<instance>:<database user>:<database password>
-
-The postgres documentation for the `.pgpass` format can be found `here`_
-
-Then add the following command to the crontab of the user (`crontab -e`)::
-
-    # m h  dom mon dow   command
-    0 2 * * * cubicweb-ctl db-dump <instance>
-
-
-Backup ninja
-~~~~~~~~~~~~
-
-You can use a combination `backup-ninja`_ (which has a postgres script in the
-example directory), `backuppc`)_ (for versionning).
-
-Please note that in the *CubicWeb way* it adds a second location for your
-password which is error-prone.
-
-.. _`here` : http://www.postgresql.org/docs/current/static/libpq-pgpass.html
-.. _`backup-ninja` : https://labs.riseup.net/code/projects/show/backupninja/
-.. _`backuppc` : http://backuppc.sourceforge.net/
-
-.. warning::
-
-  Remember that these indications will fail you whenever you use
-  another database backend than postgres. Also it does properly handle
-  externally managed data such as files (using the Bytes File System
-  Storage).
--- a/doc/book/en/admin/config.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,255 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _ConfigEnv:
-
-Set-up of a *CubicWeb* environment
-==================================
-
-You can `configure the database`_ system of your choice:
-
-  - `PostgreSQL configuration`_
-  - `MySql configuration`_
-  - `SQLServer configuration`_
-  - `SQLite configuration`_
-
-For advanced features, have a look to:
-
-  - `Pyro configuration`_
-  - `Cubicweb resources configuration`_
-
-.. _`configure the database`: DatabaseInstallation_
-.. _`PostgreSQL configuration`: PostgresqlConfiguration_
-.. _`MySql configuration`: MySqlConfiguration_
-.. _`SQLServer configuration`: SQLServerConfiguration_
-.. _`SQLite configuration`: SQLiteConfiguration_
-.. _`Pyro configuration`: PyroConfiguration_
-.. _`Cubicweb resources configuration`: RessourcesConfiguration_
-
-
-
-.. _RessourcesConfiguration:
-
-Cubicweb resources configuration
---------------------------------
-
-.. autodocstring:: cubicweb.cwconfig
-
-
-.. _DatabaseInstallation:
-
-Databases configuration
------------------------
-
-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. CubicWeb is known to run with
-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:
-
-PostgreSQL
-~~~~~~~~~~
-
-Many Linux distributions ship with the appropriate PostgreSQL packages.
-Basically, you need to install the following packages:
-
-* `postgresql` and `postgresql-client`, which will pull the respective
-  versioned packages (e.g. `postgresql-9.1` and `postgresql-client-9.1`) and,
-  optionally,
-* a `postgresql-plpython-X.Y` package with a version corresponding to that of
-  the aforementioned packages (e.g. `postgresql-plpython-9.1`).
-
-If you run postgres version prior to 8.3, you'll also need the
-`postgresql-contrib-8.X` package for full-text search extension.
-
-If you run postgres on another host than the |cubicweb| repository, you should
-install the `postgresql-client` package on the |cubicweb| host, and others on the
-database host.
-
-For extra details concerning installation, please refer to the `PostgreSQL
-project online documentation`_.
-
-.. _`PostgreSQL project online documentation`: http://www.postgresql.org/docs
-
-
-Database cluster
-++++++++++++++++
-
-If you already have an existing cluster and PostgreSQL server running, you do
-not need to execute the initilization step of your PostgreSQL database unless
-you want a specific cluster for |cubicweb| databases or if your existing
-cluster doesn't use the UTF8 encoding (see note below).
-
-To initialize a PostgreSQL cluster, use the command ``initdb``::
-
-    $ initdb -E UTF8 -D /path/to/pgsql
-
-Notice the encoding specification. This is necessary since |cubicweb| usually
-want UTF8 encoded database. If you use a cluster with the wrong encoding, you'll
-get error like::
-
-  new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
-  HINT:  Use the same encoding as in the template database, or use template0 as template.
-
-Once initialized, start the database server PostgreSQL with the command::
-
-  $ postgres -D /path/to/psql
-
-If you cannot execute this command due to permission issues, please make sure
-that your username has write access on the database.  ::
-
-  $ chown username /path/to/pgsql
-
-Database authentication
-+++++++++++++++++++++++
-
-The database authentication is configured in `pg_hba.conf`. It can be either set
-to `ident sameuser` or `md5`.  If set to `md5`, make sure to use an existing
-user of your database.  If set to `ident sameuser`, make sure that your client's
-operating system user name has a matching user in the database. If not, please
-do as follow to create a user::
-
-  $ su
-  $ su - postgres
-  $ createuser -s -P username
-
-The option `-P` (for password prompt), will encrypt the password with the
-method set in the configuration file :file:`pg_hba.conf`.  If you do not use this
-option `-P`, then the default value will be null and you will need to set it
-with::
-
-  $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
-
-The above login/password will be requested when you will create an instance with
-`cubicweb-ctl create` to initialize the database of your instance.
-
-Notice that the `cubicweb-ctl db-create` does database initialization that
-may requires a postgres superuser. That's why a login/password is explicitly asked
-at this step, so you can use there a superuser without using this user when running
-the instance. Things that require special privileges at this step:
-
-* database creation, require the 'create database' permission
-* install the plpython extension language (require superuser)
-* install the tsearch extension for postgres version prior to 8.3 (require superuser)
-
-To avoid using a super user each time you create an install, a nice trick is to
-install plpython (and tsearch when needed) on the special `template1` database,
-so they will be installed automatically when cubicweb databases are created
-without even with needs for special access rights. To do so, run ::
-
-  # Installation of plpythonu language by default ::
-  $ createlang -U pgadmin plpythonu template1
-  $ psql -U pgadmin template1
-  template1=# update pg_language set lanpltrusted=TRUE where lanname='plpythonu';
-
-Where `pgadmin` is a postgres superuser. The last command is necessary since by
-default plpython is an 'untrusted' language and as such can't be used by non
-superuser. This update fix that problem by making it trusted.
-
-To install the tsearch plain-text index extension on postgres prior to 8.3, run::
-
-    cat /usr/share/postgresql/8.X/contrib/tsearch2.sql | psql -U username template1
-
-
-.. _MySqlConfiguration:
-
-MySql
-~~~~~
-
-You must add the following lines in ``/etc/mysql/my.cnf`` file::
-
-    transaction-isolation=READ-COMMITTED
-    default-storage-engine=INNODB
-    default-character-set=utf8
-    max_allowed_packet = 128M
-
-.. Note::
-    It is unclear whether mysql supports indexed string of arbitrary length or
-    not.
-
-
-.. _SQLServerConfiguration:
-
-SQLServer
-~~~~~~~~~
-
-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)::
-
-  [system]
-  db-driver=sqlserver2005
-  db-user=someuser
-  # database password not needed
-  #db-password=toto123
-  #db-create/init may ask for a pwd: just say anything
-  db-extra-arguments=Trusted_Connection
-  db-encoding=utf8
-
-
-You need to change the default settings on the database by running::
-
- ALTER DATABASE <databasename> SET READ_COMMITTED_SNAPSHOT ON;
-
-The ALTER DATABASE command above requires some permissions that your
-user may not have. In that case you will have to ask your local DBA to
-run the query for you.
-
-You can check that the setting is correct by running the following
-query which must return '1'::
-
-   SELECT is_read_committed_snapshot_on
-     FROM sys.databases WHERE name='<databasename>';
-
-
-
-.. _SQLiteConfiguration:
-
-SQLite
-~~~~~~
-
-SQLite has the great advantage of requiring almost no configuration. Simply
-use 'sqlite' as db-driver, and set path to the dabase as db-name. Don't specify
-anything for db-user and db-password, they will be ignore anyway.
-
-.. Note::
-  SQLite is great for testing and to play with cubicweb but is not suited for
-  production environments.
-
-
-.. _PyroConfiguration:
-
-Pyro configuration
-------------------
-
-Pyro name server
-~~~~~~~~~~~~~~~~
-
-If you want to use Pyro to access your instance remotely, or to have multi-source
-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 :
-
-* be sure to have installed it (see :ref:`InstallDependencies`)
-
-* 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
-
-Note that you can use the pyro server without a running pyro nameserver.
-Refer to `pyro-ns-host` server configuration option for details.
-
--- a/doc/book/en/admin/create-instance.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Creation of your first instance
-===============================
-
-Instance creation
------------------
-
-Now that we created a cube, we can create an instance and access it via a web
-browser. We will use a `all-in-one` configuration to simplify things ::
-
-  cubicweb-ctl create -c all-in-one mycube myinstance
-
-.. note::
-  Please note that we created a new cube for a demo purposes but
-  you could have used an existing cube available in our standard library
-  such as blog or person for example.
-
-A series of questions will be prompted to you, the default answer is usually
-sufficient. You can anyway modify the configuration later on by editing
-configuration files. When a login/password are requested to access the database
-please use the credentials you created at the time you configured the database
-(:ref:`PostgresqlConfiguration`).
-
-It is important to distinguish here the user used to access the database and the
-user used to login to the cubicweb instance. When an instance starts, it uses
-the login/password for the database to get the schema and handle low level
-transaction. But, when :command:`cubicweb-ctl create` asks for a manager
-login/psswd of *CubicWeb*, it refers to the user you will use during the
-development to administrate your web instance. It will be possible, later on,
-to use this user to create other users for your final web instance.
-
-
-Instance administration
------------------------
-
-start / stop
-~~~~~~~~~~~~
-
-When this command is completed, the definition of your instance is
-located in :file:`~/etc/cubicweb.d/myinstance/*`. To launch it, you
-just type ::
-
-  cubicweb-ctl start -D myinstance
-
-The option `-D` specifies the *debug mode* : the instance is not
-running in server mode and does not disconnect from the terminal,
-which simplifies debugging in case the instance is not properly
-launched. You can see how it looks by visiting the URL
-`http://localhost:8080` (the port number depends of your
-configuration). To login, please use the cubicweb administrator
-login/password you defined when you created the instance.
-
-To shutdown the instance, Crtl-C in the terminal window is enough.
-If you did not use the option `-D`, then type ::
-
-  cubicweb-ctl stop myinstance
-
-This is it! All is settled down to start developping your data model...
-
-.. note::
-
-  The output of `cubicweb-ctl start -D myinstance` can be
-  overwhelming. It is possible to reduce the log level with the
-  `--loglevel` parameter as in `cubicweb-ctl start -D myinstance -l
-  info` to filter out all logs under `info` gravity.
-
-upgrade
-~~~~~~~
-
-A manual upgrade step is necessary whenever a new version of CubicWeb or
-a cube is installed, in order to synchronise the instance's
-configuration and schema with the new code.  The command is::
-
-  cubicweb-ctl upgrade myinstance
-
-A series of questions will be asked. It always starts with a proposal
-to make a backup of your sources (where it applies). Unless you know
-exactly what you are doing (i.e. typically fiddling in debug mode, but
-definitely NOT migrating a production instance), you should answer YES
-to that.
-
-The remaining questions concern the migration steps of |cubicweb|,
-then of the cubes that form the whole application, in reverse
-dependency order.
-
-In principle, if the migration scripts have been properly written and
-tested, you should answer YES to all questions.
-
-Somtimes, typically while debugging a migration script, something goes
-wrong and the migration fails. Unfortunately the databse may be in an
-incoherent state. You have two options here:
-
-* fix the bug, restore the database and restart the migration process
-  from scratch (quite recommended in a production environement)
-
-* try to replay the migration up to the last successful commit, that
-  is answering NO to all questions up to the step that failed, and
-  finish by answering YES to the remaining questions.
-
--- a/doc/book/en/admin/cubicweb-ctl.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _cubicweb-ctl:
-
-``cubicweb-ctl`` tool
-=====================
-
-`cubicweb-ctl` is the swiss knife to manage *CubicWeb* instances.
-The general syntax is ::
-
-  cubicweb-ctl <command> [options command] <arguments commands>
-
-To view available commands ::
-
-  cubicweb-ctl
-  cubicweb-ctl --help
-
-Please note that the commands available depends on the *CubicWeb* packages
-and cubes that have been installed.
-
-To view the help menu on specific command ::
-
-  cubicweb-ctl <command> --help
-
-Listing available cubes and instance
--------------------------------------
-
-* ``list``, provides a list of the available configuration, cubes
-  and instances.
-
-
-Creation of a new cube
------------------------
-
-Create your new cube cube ::
-
-   cubicweb-ctl newcube
-
-This will create a new cube in
-``/path/to/grshell-cubicweb/cubes/<mycube>`` for a Mercurial
-installation, or in ``/usr/share/cubicweb/cubes`` for a debian
-packages installation.
-
-Create an instance
--------------------
-
-You must ensure `~/etc/cubicweb.d/` exists prior to this. On windows, the
-'~' part will probably expand to 'Documents and Settings/user'.
-
-To create an instance from an existing cube, execute the following
-command ::
-
-   cubicweb-ctl create <cube_name> <instance_name>
-
-This command will create the configuration files of an instance in
-``~/etc/cubicweb.d/<instance_name>``.
-
-The tool ``cubicweb-ctl`` executes the command ``db-create`` and
-``db-init`` when you run ``create`` so that you can complete an
-instance creation in a single command. But of course it is possible
-to issue these separate commands separately, at a later stage.
-
-Command to create/initialize an instance database
--------------------------------------------------
-
-* ``db-create``, creates the system database of an instance (tables and
-  extensions only)
-* ``db-init``, initializes the system database of an instance
-  (schema, groups, users, workflows...)
-
-Commands to control instances
------------------------------
-
-* ``start``, starts one or more or all instances
-
-of special interest::
-
-  start -D
-
-will start in debug mode (under windows, starting without -D will not
-work; you need instead to setup your instance as a service).
-
-* ``stop``, stops one or more or all instances
-* ``restart``, restarts one or more or all instances
-* ``status``, returns the status of the instance(s)
-
-Commands to maintain instances
-------------------------------
-
-* ``upgrade``, launches the existing instances migration when a new version
-  of *CubicWeb* or the cubes installed is available
-* ``shell``, opens a (Python based) migration shell for manual maintenance of the instance
-* ``db-dump``, creates a dump of the system database
-* ``db-restore``, restores a dump of the system database
-* ``db-check``, checks data integrity of an instance. If the automatic correction
-  is activated, it is recommanded to create a dump before this operation.
-* ``schema-sync``, synchronizes the persistent schema of an instance with
-  the instance schema. It is recommanded to create a dump before this operation.
-
-Commands to maintain i18n catalogs
-----------------------------------
-* ``i18ncubicweb``, regenerates messages catalogs of the *CubicWeb* library
-* ``i18ncube``, regenerates the messages catalogs of a cube
-* ``i18ninstance``, recompiles the messages catalogs of an instance.
-  This is automatically done while upgrading.
-
-See also chapter :ref:`internationalization`.
-
-Other commands
---------------
-* ``delete``, deletes an instance (configuration files and database)
--- a/doc/book/en/admin/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Part3:
-
---------------
-Administration
---------------
-
-This part is for installation and administration of the *CubicWeb* framework and
-instances based on that framework.
-
-.. toctree::
-   :maxdepth: 1
-   :numbered:
-
-   setup
-   setup-windows
-   config
-   cubicweb-ctl
-   create-instance
-   instance-config
-   site-config
-   multisources
-   ldap
-   pyro
-   migration
-   additional-tips
-   rql-logs
-
--- a/doc/book/en/admin/instance-config.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,226 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-
-Configure an instance
-=====================
-
-While creating an instance, a configuration file is generated in::
-
-    $ (CW_INSTANCES_DIR) / <instance> / <configuration name>.conf
-
-For example::
-
-    /etc/cubicweb.d/myblog/all-in-one.conf
-
-It is a simple text file in the INI format
-(http://en.wikipedia.org/wiki/INI_file). In the following description,
-each option name is prefixed with its own section and followed by its
-default value if necessary, e.g. "`<section>.<option>` [value]."
-
-.. _`WebServerConfig`:
-
-Configuring the Web server
---------------------------
-:`web.auth-model` [cookie]:
-    authentication mode, cookie or http
-:`web.realm`:
-    realm of the instance in http authentication mode
-:`web.http-session-time` [0]:
-    period of inactivity of an HTTP session before it closes automatically.
-    Duration in seconds, 0 meaning no expiration (or more exactly at the
-    closing of the browser client)
-
-:`main.anonymous-user`, `main.anonymous-password`:
-    login and password to use to connect to the RQL server with
-    HTTP anonymous connection. CWUser account should exist.
-
-:`main.base-url`:
-    url base site to be used to generate the urls of web pages
-
-Https configuration
-```````````````````
-It is possible to make a site accessible for anonymous http connections
-and https for authenticated users. This requires to
-use apache (for example) for redirection and the variable `main.https-url`
-of configuration file.
-
-For this to work you have to activate the following apache modules :
-
-* rewrite
-* proxy
-* http_proxy
-
-The command on Debian based systems for that is ::
-
-  a2enmod rewrite http_proxy proxy
-  /etc/init.d/apache2 restart
-
-:Example:
-
-   For an apache redirection of a site accessible via `http://localhost/demo`
-   and `https://localhost/demo` and actually running on port 8080, it
-   takes to the http:::
-
-     ProxyPreserveHost On
-     RewriteEngine On
-     RewriteCond %{REQUEST_URI} ^/demo
-     RewriteRule ^/demo$ /demo/
-     RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P]
-
-   and for the https:::
-
-     ProxyPreserveHost On
-     RewriteEngine On
-     RewriteCond %{REQUEST_URI} ^/ demo
-     RewriteRule ^/demo$/demo/
-     RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]
-
-
-   and we will file in the all-in-one.conf of the instance:::
-
-     base-url = http://localhost/demo
-     https-url = https://localhost/demo
-
-Notice that if you simply want a site accessible through https, not *both* http
-and https, simply set `base-url` to the https url and the first section into your
-apache configuration (as you would have to do for an http configuration with an
-apache front-end).
-
-Setting up the web client
--------------------------
-:`web.embed-allowed`:
-    regular expression matching sites which could be "embedded" in
-    the site (controllers 'embed')
-:`web.submit-url`:
-    url where the bugs encountered in the instance can be mailed to
-
-
-RQL server configuration
-------------------------
-:`main.host`:
-    host name if it can not be detected correctly
-:`main.pid-file`:
-    file where will be written the server pid
-:`main.uid`:
-    user account to use for launching the server when it is
-    root launched by init
-:`main.session-time [30*60]`:
-    timeout of a RQL session
-:`main.query-log-file`:
-    file where all requests RQL executed by the server are written
-
-
-Pyro configuration for the instance
------------------------------------
-Web server side:
-
-:`pyro.pyro-instance-id`:
-    pyro identifier of RQL server (e.g. the instance name)
-
-RQL server side:
-
-:`main.pyro-server`:
-    boolean to switch on/off pyro server-side
-
-:`pyro.pyro-host`:
-    pyro host:port number. If no port is specified, it is assigned
-    automatically.
-
-RQL and web servers side:
-
-:`pyro.pyro-ns-host`:
-    hostname hosting pyro server name. If no value is
-    specified, it is located by a request from broadcast
-
-:`pyro.pyro-ns-group`:
-    pyro group in which to save the instance (will default to 'cubicweb')
-
-
-Configuring e-mail
-------------------
-RQL and web server side:
-
-:`email.mangle-mails [no]`:
-    indicates whether the email addresses must be displayed as is or
-    transformed
-
-RQL server side:
-
-:`email.smtp-host [mail]`:
-    hostname hosting the SMTP server to use for outgoing mail
-:`email.smtp-port [25]`:
-    SMTP server port to use for outgoing mail
-:`email.sender-name`:
-    name to use for outgoing mail of the instance
-:`email.sender-addr`:
-    address for outgoing mail of the instance
-:`email.default dest-addrs`:
-    destination addresses by default, if used by the configuration of the
-    dissemination of the model (separated by commas)
-:`email.supervising-addrs`:
-    destination addresses of e-mails of supervision (separated by
-    commas)
-
-
-Configuring logging
--------------------
-:`main.log-threshold`:
-    level of filtering messages (DEBUG, INFO, WARNING, ERROR)
-:`main.log-file`:
-    file to write messages
-
-
-.. _PersistentProperties:
-
-Configuring persistent properties
----------------------------------
-Other configuration settings are in the form of entities `CWProperty`
-in the database. It must be edited via the web interface or by
-RQL queries.
-
-:`ui.encoding`:
-    Character encoding to use for the web
-:`navigation.short-line-size`:
-    number of characters for "short" display
-:`navigation.page-size`:
-    maximum number of entities to show per results page
-:`navigation.related-limit`:
-    number of related entities to show up on primary entity view
-:`navigation.combobox-limit`:
-    number of entities unrelated to show up on the drop-down lists of
-    the sight on an editing entity view
-
-Cross-Origin Resource Sharing
------------------------------
-
-CubicWeb provides some support for the CORS_ protocol. For now, the
-provided implementation only deals with access to a CubicWeb instance
-as a whole. Support for a finer granularity may be considered in the
-future.
-
-Specificities of the provided implementation:
-
-- ``Access-Control-Allow-Credentials`` is always true
-- ``Access-Control-Allow-Origin`` header in response will never be
-  ``*``
-- ``Access-Control-Expose-Headers`` can be configured globally (see below)
-- ``Access-Control-Max-Age`` can be configured globally (see below)
-- ``Access-Control-Allow-Methods`` can be configured globally (see below)
-- ``Access-Control-Allow-Headers`` can be configured globally (see below)
-
-
-A few parameters can be set to configure the CORS_ capabilities of CubicWeb.
-
-.. _CORS: http://www.w3.org/TR/cors/
-
-:`access-control-allow-origin`:
-   comma-separated list of allowed origin domains or "*" for any domain
-:`access-control-allow-methods`:
-   comma-separated list of allowed HTTP methods
-:`access-control-max-age`:
-   maximum age of cross-origin resource sharing (in seconds)
-:`access-control-allow-headers`:
-   comma-separated list of allowed HTTP custom headers (used in simple requests)
-:`access-control-expose-headers`:
-   comma-separated list of allowed HTTP custom headers (used in preflight requests)
-
--- a/doc/book/en/admin/ldap.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,134 +0,0 @@
-.. _LDAP:
-
-LDAP integration
-================
-
-Overview
---------
-
-Using LDAP as a source for user credentials and information is quite
-easy. The most difficult part lies in building an LDAP schema or
-using an existing one.
-
-At cube creation time, one is asked if more sources are wanted. LDAP
-is one possible option at this time. Of course, it is always possible
-to set it up later using the `CWSource` entity type, which we discuss
-there.
-
-It is possible to add as many LDAP sources as wanted, which translates
-in as many `CWSource` entities as needed.
-
-The general principle of the LDAP source is, given a proper
-configuration, to create local users matching the users available in
-the directory and deriving local user attributes from directory users
-attributes. Then a periodic task ensures local user information
-synchronization with the directory.
-
-Users handled by such a source should not be edited directly from
-within the application instance itself. Rather, updates should happen
-at the LDAP server level.
-
-Credential checks are _always_ done against the LDAP server.
-
-.. Note::
-
-  There are currently two ldap source types: the older `ldapuser` and
-  the newer `ldapfeed`. The older will be deprecated anytime soon, as
-  the newer has now gained all the features of the old and does not
-  suffer from some of its illnesses.
-
-  The ldapfeed creates real `CWUser` entities, and then
-  activate/deactivate them depending on their presence/absence in the
-  corresponding LDAP source. Their attribute and state
-  (activated/deactivated) are hence managed by the source mechanism;
-  they should not be altered by other means (as such alterations may
-  be overridden in some subsequent source synchronisation).
-
-
-Configuration of an LDAPfeed source
------------------------------------
-
-Additional sources are created at cube creation time or later through the
-user interface.
-
-Configure an `ldapfeed` source from the user interface under `Manage` then
-`data sources`:
-
-* At this point `type` has been set to `ldapfeed`.
-
-* The `parser` attribute shall be set to `ldapfeed`.
-
-* The `url` attribute shall be set to an URL such as ldap://ldapserver.domain/.
-
-* The `configuration` attribute contains many options. They are described in
-  detail in the next paragraph.
-
-
-Options of an LDAPfeed source
------------------------------
-
-Let us enumerate the options by categories (LDAP server connection,
-LDAP schema mapping information).
-
-LDAP server connection options:
-
-* `auth-mode`, (choices are simple, cram_md5, digest_md5, gssapi, support
-  for the later being partial as of now)
-
-* `auth-realm`, realm to use when using gssapi/kerberos authentication
-
-* `data-cnx-dn`, user dn to use to open data connection to the ldap (eg
-  used to respond to rql queries)
-
-* `data-cnx-password`, password to use to open data connection to the
-  ldap (eg used to respond to rql queries)
-
-If the LDAP server accepts anonymous binds, then it is possible to
-leave data-cnx-dn and data-cnx-password empty. This is, however, quite
-unlikely in practice. Beware that the LDAP server might hide attributes
-such as "userPassword" while the rest of the attributes remain visible
-through an anonymous binding.
-
-LDAP schema mapping options:
-
-* `user-base-dn`, base DN to lookup for users
-
-* `user-scope`, user search scope (valid values: "BASE", "ONELEVEL",
-  "SUBTREE")
-
-* `user-classes`, classes of user (with Active Directory, you want to
-  say "user" here)
-
-* `user-filter`, additional filters to be set in the ldap query to
-  find valid users
-
-* `user-login-attr`, attribute used as login on authentication (with
-  Active Directory, you want to use "sAMAccountName" here)
-
-* `user-default-group`, name of a group in which ldap users will be by
-  default. You can set multiple groups by separating them by a comma
-
-* `user-attrs-map`, map from ldap user attributes to cubicweb
-  attributes (with Active Directory, you want to use
-  sAMAccountName:login,mail:email,givenName:firstname,sn:surname)
-
-
-Other notes
------------
-
-* Cubicweb is able to start if ldap cannot be reached, even on
-  cubicweb-ctl start ... If some source ldap server cannot be used
-  while an instance is running, the corresponding users won't be
-  authenticated but their status will not change (e.g. they will not
-  be deactivated)
-
-* The user-base-dn is a key that helps cubicweb map CWUsers to LDAP
-  users: beware updating it
-
-* When a user is removed from an LDAP source, it is deactivated in the
-  CubicWeb instance; when a deactivated user comes back in the LDAP
-  source, it (automatically) is activated again
-
-* You can use the :class:`CWSourceHostConfig` to have variants for a source
-  configuration according to the host the instance is running on. To do so
-  go on the source's view from the sources management view.
--- a/doc/book/en/admin/migration.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Migrating cubicweb instances - benefits from a distributed architecture
-=======================================================================
-
-Migrate apache & cubicweb
--------------------------
-
-**Aim** : do the migration for N cubicweb instances hosted on a server to another with no downtime.
-
-**Prerequisites** : have an explicit definition of the database host (not default or localhost). In our case, the database is hosted on another host. You are not migrating your pyro server. You are not using multisource (more documentation on that soon).
-
-**Steps** :
-
-1. *on new machine* : install your environment (*pseudocode*) ::
-
-     apt-get install cubicweb cubicweb-applications apache2
-
-2. *on old machine* : copy your cubicweb and apache configuration to the new machine ::
-
-    scp /etc/cubicweb.d/ newmachine:/etc/cubicweb.d/
-    scp /etc/apache2/sites-available/ newmachine:/etc/apache2/sites-available/
-
-3. *on new machine* : give new ids to pyro registration so the new instances can register ::
-
-     cd /etc/cubicweb.d/ ; sed -i.bck 's/^pyro-instance-id=.*$/\02/' */all-in-one.conf
-
-4. *on new machine* : start your instances ::
-
-     cubicweb start
-
-5. *on new machine* : enable sites and modules for apache and start it, test it using by modifying your /etc/host file.
-
-6. change dns entry from your oldmachine to newmachine
-
-7. shutdown your *old machine* (if it doesn't host other services or your database)
-
-8. That's it.
-
-**Possible enhancements** : use right from the start a pound server behind your apache, that way you can add backends and smoothily migrate by shuting down backends that pound will take into account.
-
-Migrate apache & cubicweb with pyro
------------------------------------
-
-FIXME TODO
-
--- a/doc/book/en/admin/multisources.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-Multiple sources of data
-========================
-
-Data sources include SQL, LDAP, RQL, mercurial and subversion.
-
-.. XXX feed me
--- a/doc/book/en/admin/pyro.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-.. _UsingPyro:
-
-Working with a distributed client (using Pyro)
-==============================================
-
-In some circumstances, it is practical to split the repository and
-web-client parts of the application for load-balancing reasons. Or
-one wants to access the repository from independant scripts to consult
-or update the database.
-
-Prerequisites
--------------
-
-For this to work, several steps have to be taken in order.
-
-You must first ensure that the appropriate software is installed and
-running (see :ref:`ConfigEnv`)::
-
-  pyro-nsd -x -p 6969
-
-Then you have to set appropriate options in your configuration. For
-instance::
-
-  pyro-server=yes
-  pyro-ns-host=localhost:6969
-
-  pyro-instance-id=myinstancename
-
-Connect to the CubicWeb repository from a python script
--------------------------------------------------------
-
-Assuming pyro-nsd is running and your instance is configured with ``pyro-server=yes``,
-you will be able to use :mod:`cubicweb.dbapi` api to initiate the connection.
-
-.. note::
-    Regardless of whether your instance is pyro activated or not, you can still
-    achieve this by using cubicweb-ctl shell scripts in a simpler way, as by default
-    it creates a repository 'in-memory' instead of connecting through pyro. That
-    also means you've to be on the host where the instance is running.
-
-Finally, the client (for instance a python script) must connect specifically
-as in the following example code:
-
-.. sourcecode:: python
-
-    from cubicweb import dbapi
-
-    cnx = dbapi.connect(database='instance-id', user='admin', password='admin')
-    cnx.load_appobjects()
-    cur = cnx.cursor()
-    for name in (u'Personal', u'Professional', u'Computers'):
-        cur.execute('INSERT Tag T: T name %(n)s', {'n': name})
-    cnx.commit()
-
-Calling :meth:`cubicweb.dbapi.load_appobjects`, will populate the
-cubicweb registries (see :ref:`VRegistryIntro`) with the application
-objects installed on the host where the script runs. You'll then be
-allowed to use the ORM goodies and custom entity methods and views. Of
-course this is optional, without it you can still get the repository
-data through the connection but in a roughly way: only RQL cursors
-will be available, e.g. you can't even build entity objects from the
-result set.
--- a/doc/book/en/admin/rql-logs.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-RQL logs
-========
-
-You can configure the *CubicWeb* instance to keep a log
-of the queries executed against your database. To do so,
-edit the configuration file of your instance
-``.../etc/cubicweb.d/myapp/all-in-one.conf`` and uncomment the
-variable ``query-log-file``::
-
-  # web instance query log file
-  query-log-file=/tmp/rql-myapp.log
-
--- a/doc/book/en/admin/setup-windows.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _SetUpWindowsEnv:
-
-Installing a development environement on Windows
-================================================
-
-Setting up a Windows development environment is not too complicated
-but it requires a series of small steps.
-
-We propose an example of a typical |cubicweb| installation on Windows
-from sources. We assume everything goes into ``C:\\`` and for any
-package, without version specification, "the latest is
-the greatest".
-
-Mind that adjusting the installation drive should be straightforward.
-
-
-
-Install the required elements
------------------------------
-
-|cubicweb| requires some base elements that must be installed to run
-correctly. So, first of all, you must install them :
-
-* python >= 2.6 and < 3
-  (`Download Python <http://www.python.org/download/>`_).
-  You can also consider the Python(x,y) distribution
-  (`Download Python(x,y) <http://code.google.com/p/pythonxy/wiki/Downloads>`_)
-  as it makes things easier for Windows user by wrapping in a single installer
-  python 2.7 plus numerous useful third-party modules and
-  applications (including Eclipse + pydev, which is an arguably good
-  IDE for Python under Windows).
-
-* `Twisted <http://twistedmatrix.com/trac/>`_ is an event-driven
-  networking engine
-  (`Download Twisted <http://twistedmatrix.com/trac/>`_)
-
-* `lxml <http://codespeak.net/lxml/>`_ library
-  (version >=2.2.1) allows working with XML and HTML
-  (`Download lxml <http://pypi.python.org/pypi/lxml/2.2.1>`_)
-
-* `Postgresql <http://www.postgresql.org/>`_,
-  an object-relational database system
-  (`Download Postgresql <http://www.enterprisedb.com/products/pgdownload.do#windows>`_)
-  and its python drivers
-  (`Download psycopg <http://www.stickpeople.com/projects/python/win-psycopg/#Version2>`_)
-
-* A recent version of `gettext`
-  (`Download gettext <http://download.logilab.org/pub/gettext/gettext-0.17-win32-setup.exe>`_).
-
-* `rql <http://www.logilab.org/project/rql>`_,
-  the recent version of the Relationship Query Language parser.
-
-Install optional elements
--------------------------
-
-We recommend you to install the following elements. They are not
-mandatory but they activate very interesting features in |cubicweb|:
-
-* `python-ldap <http://pypi.python.org/pypi/python-ldap>`_
-  provides access to LDAP/Active directory directories
-  (`Download python-ldap <http://www.osuch.org/python-ldap>`_).
-
-* `graphviz <http://www.graphviz.org/>`_
-  which allow schema drawings.
-  (`Download graphviz <http://www.graphviz.org/Download_windows.php>`_).
-  It is quite recommended (albeit not mandatory).
-
-Other elements will activate more features once installed. Take a look
-at :ref:`InstallDependencies`.
-
-Useful tools
-------------
-
-Some additional tools could be useful to develop :ref:`cubes <AvailableCubes>`
-with the framework.
-
-* `mercurial <http://mercurial.selenic.com/>`_ and its standard windows GUI
-  (`TortoiseHG <http://tortoisehg.bitbucket.org/>`_) allow you to get the source
-  code of |cubicweb| from control version repositories. So you will be able to
-  get the latest development version and pre-release bugfixes in an easy way
-  (`Download mercurial <http://bitbucket.org/tortoisehg/stable/wiki/download>`_).
-
-* You can also consider the ssh client `Putty` in order to peruse
-  mercurial over ssh (`Download <http://www.putty.org/>`_).
-
-* If you are an Eclipse user, mercurial can be integrated using the
-  `MercurialEclipse` plugin
-  (`Home page <http://www.vectrace.com/mercurialeclipse/>`_).
-
-Getting the sources
--------------------
-
-There are two ways to get the sources of |cubicweb| and its
-:ref:`cubes <AvailableCubes>`:
-
-* download the latest release (:ref:`SourceInstallation`)
-* get the development version using Mercurial
-  (:ref:`MercurialInstallation`)
-
-Environment variables
----------------------
-
-You will need some convenience environment variables once all is set up. These
-variables are settable through the GUI by getting at the `System properties`
-window (by righ-clicking on `My Computer` -> `properties`).
-
-In the `advanced` tab, there is an `Environment variables` button. Click on
-it. That opens a small window allowing edition of user-related and system-wide
-variables.
-
-We will consider only user variables. First, the ``PATH`` variable. Assuming
-you are logged as user *Jane*, add the following paths, separated by
-semi-colons::
-
-  C:\Documents and Settings\Jane\My Documents\Python\cubicweb\cubicweb\bin
-  C:\Program Files\Graphviz2.24\bin
-
-The ``PYTHONPATH`` variable should also contain::
-
-  C:\Documents and Settings\Jane\My Documents\Python\cubicweb\
-
-From now, on a fresh `cmd` shell, you should be able to type::
-
-  cubicweb-ctl list
-
-... and get a meaningful output.
-
-Running an instance as a service
---------------------------------
-
-This currently assumes that the instances configurations is located at
-``C:\\etc\\cubicweb.d``. For a cube 'my_instance', you will find
-``C:\\etc\\cubicweb.d\\my_instance\\win32svc.py``.
-
-Now, register your instance as a windows service with::
-
-  win32svc install
-
-Then start the service with::
-
-  net start cubicweb-my_instance
-
-In case this does not work, you should be able to see error reports in
-the application log, using the windows event log viewer.
--- a/doc/book/en/admin/setup.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,269 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _SetUpEnv:
-
-Installation of a *CubicWeb* environment
-========================================
-
-Official releases are available from the `CubicWeb.org forge`_ and from
-`PyPI`_. Since CubicWeb is developed using `Agile software development
-<http://en.wikipedia.org/wiki/Agile_software_development>`_ techniques, releases
-happen frequently. In a version numbered X.Y.Z, X changes after a few years when
-the API breaks, Y changes after a few weeks when features are added and Z
-changes after a few days when bugs are fixed.
-
-Depending on your needs, you will chose a different way to install CubicWeb on
-your system:
-
-- `Installation on Debian/Ubuntu`_
-- `Installation on Windows`_
-- `Installation in a virtualenv`_
-- `Installation with pip`_
-- `Installation with easy_install`_
-- `Installation from tarball`_
-
-If you are a power-user and need the very latest features, you will
-
-- `Install from version control`_
-
-Once the software is installed, move on to :ref:`ConfigEnv` for better control
-and advanced features of |cubicweb|.
-
-.. _`Installation on Debian/Ubuntu`: DebianInstallation_
-.. _`Installation on Windows`: WindowsInstallation_
-.. _`Installation in a virtualenv`: VirtualenvInstallation_
-.. _`Installation with pip`: PipInstallation_
-.. _`Installation with easy_install`: EasyInstallInstallation_
-.. _`Installation from tarball`: TarballInstallation_
-.. _`Install from version control`: MercurialInstallation_
-
-
-.. _DebianInstallation:
-
-Debian/Ubuntu install
----------------------
-
-|cubicweb| is packaged for Debian/Ubuntu (and derived
-distributions). Their integrated package-management system make
-installation and upgrade much easier for users since
-dependencies (like databases) are automatically installed.
-
-Depending on the distribution you are using, add the appropriate line to your
-`list of sources` (for example by editing ``/etc/apt/sources.list``).
-
-For Debian 7.0 Wheezy (stable)::
-
-  deb http://download.logilab.org/production/ wheezy/
-
-For Debian Sid (unstable)::
-
-  deb http://download.logilab.org/production/ sid/
-
-For Ubuntu 12.04 Precise Pangolin (Long Term Support) and newer::
-
-  deb http://download.logilab.org/production/ precise/
-
-The repositories are signed with the `Logilab's gnupg key`_. You can download
-and register the key to avoid warnings::
-
-  wget -q http://download.logilab.org/logilab-dists-key.asc -O- | sudo apt-key add -
-
-Update your list of packages and perform the installation::
-
-  apt-get update
-  apt-get install cubicweb cubicweb-dev
-
-``cubicweb`` installs the framework itself, allowing you to create new
-instances. ``cubicweb-dev`` installs the development environment
-allowing you to develop new cubes.
-
-There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
-list of available cubes using ``apt-cache search cubicweb`` or at the
-`CubicWeb.org forge`_.
-
-.. note::
-
-  `cubicweb-dev` will install basic sqlite support. You can easily setup
-  :ref:`cubicweb with other database <DatabaseInstallation>` using the following
-  virtual packages :
-
-  * `cubicweb-postgresql-support` contains the necessary dependencies for
-    using :ref:`cubicweb with postgresql datatabase <PostgresqlConfiguration>`
-
-  * `cubicweb-mysql-support` contains the necessary dependencies for using
-    :ref:`cubicweb with mysql database <MySqlConfiguration>`.
-
-.. _`list of sources`: http://wiki.debian.org/SourcesList
-.. _`Logilab's gnupg key`: http://download.logilab.org/logilab-dists-key.asc
-.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
-
-.. _WindowsInstallation:
-
-Windows Install
----------------
-
-You need to have `python`_ version >= 2.5 and < 3 installed.
-
-If you want an automated install, your best option is probably the
-:ref:`EasyInstallInstallation`. EasyInstall is a tool that helps users to
-install python packages along with their dependencies, searching for suitable
-pre-compiled binaries on the `The Python Package Index`_.
-
-If you want better control over the process as well as a suitable development
-environment or if you are having problems with `easy_install`, read on to
-:ref:`SetUpWindowsEnv`.
-
-.. _python:  http://www.python.org/
-.. _`The Python Package Index`: http://pypi.python.org
-
-.. _VirtualenvInstallation:
-
-`Virtualenv` install
---------------------
-
-|cubicweb| can be safely installed, used and contained inside a
-`virtualenv`_. You can use either :ref:`pip <PipInstallation>` or
-:ref:`easy_install <EasyInstallInstallation>` to install |cubicweb|
-inside an activated virtual environment.
-
-.. _PipInstallation:
-
-`pip` install
--------------
-
-`pip <http://pip.openplans.org/>`_ is a python tool that helps downloading,
-building, installing, and managing Python packages and their dependencies. It
-is fully compatible with `virtualenv`_ and installs the packages from sources
-published on the `The Python Package Index`_.
-
-.. _`virtualenv`: http://virtualenv.openplans.org/
-
-A working compilation chain is needed to build the modules that include C
-extensions. If you really do not want to compile anything, installing `lxml <http://lxml.de/>`_,
-`Twisted Web <http://twistedmatrix.com/trac/wiki/Downloads/>`_ and `libgecode
-<http://www.gecode.org/>`_ will help.
-
-For Debian, these minimal dependencies can be obtained by doing::
-
-  apt-get install gcc python-pip python-dev python-lxml
-
-or, if you prefer to get as much as possible from pip::
-
-  apt-get install gcc python-pip python-dev libxslt1-dev libxml2-dev
-
-For Windows, you can install pre-built packages (possible `source
-<http://www.lfd.uci.edu/~gohlke/pythonlibs/>`_). For a minimal setup, install:
-
-- pip http://www.lfd.uci.edu/~gohlke/pythonlibs/#pip
-- setuptools http://www.lfd.uci.edu/~gohlke/pythonlibs/#setuptools
-- libxml-python http://www.lfd.uci.edu/~gohlke/pythonlibs/#libxml-python>
-- lxml http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml and
-- twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
-
-Make sure to choose the correct architecture and version of Python.
-
-Finally, install |cubicweb| and its dependencies, by running::
-
-  pip install cubicweb
-
-Many other :ref:`cubes <AvailableCubes>` are available. A list is available at
-`PyPI <http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
-or at the `CubicWeb.org forge`_.
-
-For example, installing the *blog cube* is achieved by::
-
-  pip install cubicweb-blog
-
-.. _EasyInstallInstallation:
-
-`easy_install` install
-----------------------
-
-.. note::
-
-   If you are not a Windows user and you have a compilation environment, we
-   recommend you to use the PipInstallation_.
-
-`easy_install`_ is a python utility that helps downloading, installing, and
-managing python packages and their dependencies.
-
-Install |cubicweb| and its dependencies, run::
-
-  easy_install cubicweb
-
-There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
-list of available cubes on `PyPI
-<http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
-or at the `CubicWeb.org Forge`_.
-
-For example, installing the *blog cube* is achieved by::
-
-  easy_install cubicweb-blog
-
-.. note::
-
-  If you encounter problem with :ref:`cubes <AvailableCubes>` installation,
-  consider using :ref:`PipInstallation` which is more stable
-  but can not installed pre-compiled binaries.
-
-.. _`easy_install`: http://packages.python.org/distribute/easy_install.html
-
-
-.. _SourceInstallation:
-
-Install from source
--------------------
-
-.. _TarballInstallation:
-
-You can download the archive containing the sources from
-`http://download.logilab.org/pub/cubicweb/ <http://download.logilab.org/pub/cubicweb/>`_.
-
-Make sure you also have all the :ref:`InstallDependencies`.
-
-Once uncompressed, you can install the framework from inside the uncompressed
-folder with::
-
-  python setup.py install
-
-Or you can run |cubicweb| directly from the source directory by
-setting the :ref:`resource mode <RessourcesConfiguration>` to `user`. This will
-ease the development with the framework.
-
-There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
-list of availble cubes at the `CubicWeb.org Forge`_.
-
-
-.. _MercurialInstallation:
-
-Install from version control system
------------------------------------
-
-To keep-up with on-going development, clone the :ref:`Mercurial
-<MercurialPresentation>` repository::
-
-  hg clone -u stable http://hg.logilab.org/cubicweb # stable branch
-  hg clone http://hg.logilab.org/cubicweb # development branch
-
-To get many of CubicWeb's dependencies and a nice set of base cubes, 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 the
-directory containing the ``cubicweb`` repository, and update them to the
-latest published version tag (if any).
-
-.. note::
-
-  In every cloned repositories, a `hg tags` will display a list of
-  tags in reverse chronological order. One reasonnable option is to go to a
-  tagged version: the latest published version or example, as done by
-  the `clone_deps` script)::
-
-   hg update cubicweb-version-3.12.2
-
-Make sure you also have all the :ref:`InstallDependencies`.
-
--- a/doc/book/en/admin/site-config.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-User interface for web site configuration
-=========================================
-
-.. image:: ../images/lax-book_03-site-config-panel_en.png
-
-This panel allows you to configure the appearance of your instance site.
-Six menus are available and we will go through each of them to explain how
-to use them.
-
-Navigation
-~~~~~~~~~~
-This menu provides you a way to adjust some navigation options depending on
-your needs, such as the number of entities to display by page of results.
-Follows the detailled list of available options :
-
-* navigation.combobox-limit : maximum number of entities to display in related
-  combo box (sample format: 23)
-* navigation.page-size : maximum number of objects displayed by page of results
-  (sample format: 23)
-* navigation.related-limit : maximum number of related entities to display in
-  the primary view (sample format: 23)
-* navigation.short-line-size : maximum number of characters in short description
-  (sample format: 23)
-
-UI
-~~
-This menu provides you a way to customize the user interface settings such as
-date format or encoding in the produced html.
-Follows the detailled list of available options :
-
-* ui.date-format : how to format date in the ui ("man strftime" for format description)
-* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
-  description)
-* ui.default-text-format : default text format for rich text fields.
-* ui.encoding : user interface encoding
-* ui.fckeditor :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.
-* ui.float-format : how to format float numbers in the ui
-* ui.language : language of the user interface
-* ui.main-template : id of main template used to render pages
-* ui.site-title	: site title, which is displayed right next to the logo in the header
-* ui.time-format : how to format time in the ui ("man strftime" for format description)
-
-
-Actions
-~~~~~~~
-This menu provides a way to configure the context in which you expect the actions
-to be displayed to the user and if you want the action to be visible or not.
-You must have notice that when you view a list of entities, an action box is
-available on the left column which display some actions as well as a drop-down
-menu for more actions.
-
-The context available are :
-
-* mainactions : actions listed in the left box
-* moreactions : actions listed in the `more` menu of the left box
-* addrelated : add actions listed in the left box
-* useractions : actions listed in the first section of drop-down menu
-  accessible from the right corner user login link
-* siteactions : actions listed in the second section of drop-down menu
-  accessible from the right corner user login link
-* hidden : select this to hide the specific action
-
-Boxes
-~~~~~
-The instance has already a pre-defined set of boxes you can use right away.
-This configuration section allows you to place those boxes where you want in the
-instance interface to customize it.
-
-The available boxes are :
-
-* actions box : box listing the applicable actions on the displayed data
-
-* boxes_blog_archives_box : box listing the blog archives
-
-* possible views box : box listing the possible views for the displayed data
-
-* rss box : RSS icon to get displayed data as a RSS thread
-
-* search box : search box
-
-* startup views box : box listing the configuration options available for
-  the instance site, such as `Preferences` and `Site Configuration`
-
-Components
-~~~~~~~~~~
-[WRITE ME]
-
-Contextual components
-~~~~~~~~~~~~~~~~~~~~~
-[WRITE ME]
-
--- a/doc/book/en/annexes/depends.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _InstallDependencies:
-
-Installation dependencies
-=========================
-
-When you run CubicWeb from source, either by downloading the tarball or
-cloning the mercurial tree, here is the list of tools and libraries you need
-to have installed in order for CubicWeb to work:
-
-* yapps - http://theory.stanford.edu/~amitp/yapps/ -
-  http://pypi.python.org/pypi/Yapps2
-
-* pygraphviz - http://networkx.lanl.gov/pygraphviz/ -
-  http://pypi.python.org/pypi/pygraphviz
-
-* docutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
-
-* lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml
-
-* twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted
-
-* logilab-common - http://www.logilab.org/project/logilab-common -
-  http://pypi.python.org/pypi/logilab-common/
-
-* logilab-database - http://www.logilab.org/project/logilab-database -
-  http://pypi.python.org/pypi/logilab-database/
-
-* logilab-constraint - http://www.logilab.org/project/logilab-constraint -
-  http://pypi.python.org/pypi/constraint/
-
-* logilab-mtconverter - http://www.logilab.org/project/logilab-mtconverter -
-  http://pypi.python.org/pypi/logilab-mtconverter
-
-* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql
-
-* yams - http://www.logilab.org/project/yams - http://pypi.python.org/pypi/yams
-
-* indexer - http://www.logilab.org/project/indexer -
-  http://pypi.python.org/pypi/indexer
-
-* passlib - https://code.google.com/p/passlib/ -
-  http://pypi.python.org/pypi/passlib
-
-If you're using a Postgresql database (recommended):
-
-* psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2
-* plpythonu extension
-
-Other optional packages:
-
-* fyzz - http://www.logilab.org/project/fyzz -
-  http://pypi.python.org/pypi/fyzz *to activate Sparql querying*
-
-
-Any help with the packaging of CubicWeb for more than Debian/Ubuntu (including
-eggs, buildouts, etc) will be greatly appreciated.
--- a/doc/book/en/annexes/docstrings-conventions.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-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	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,439 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Frequently Asked Questions (FAQ)
-================================
-
-
-Generalities
-````````````
-
-Why do you use the LGPL license to prevent me from doing X ?
-------------------------------------------------------------
-
-LGPL means that *if* you redistribute your application, you need to
-redistribute the changes you made to CubicWeb under the LGPL licence.
-
-Publishing a web site has nothing to do with redistributing source
-code according to the terms of the LGPL. A fair amount of companies
-use modified LGPL code for internal use. And someone could publish a
-*CubicWeb* component under a BSD licence for others to plug into a
-LGPL framework without any problem. The only thing we are trying to
-prevent here is someone taking the framework and packaging it as
-closed source to his own clients.
-
-Why does not CubicWeb have a template language ?
-------------------------------------------------
-
-There are enough template languages out there. You can use your
-preferred template language if you want. [explain how to use a
-template language]
-
-*CubicWeb* does not define its own templating language as this was
-not our goal. Based on our experience, we realized that
-we could gain productivity by letting designers use design tools
-and developpers develop without the use of the templating language
-as an intermediary that could not be anyway efficient for both parties.
-Python is the templating language that we use in *CubicWeb*, but again,
-it does not prevent you from using a templating language.
-
-Moreover, CubicWeb currently supports `simpletal`_ out of the box and
-it is also possible to use the `cwtags`_ library to build html trees
-using the `with statement`_ with more comfort than raw strings.
-
-.. _`simpletal`: http://www.owlfish.com/software/simpleTAL/
-.. _`cwtags`: http://www.cubicweb.org/project/cwtags
-.. _`with statement`: http://www.python.org/dev/peps/pep-0343/
-
-Why do you think using pure python is better than using a template language ?
------------------------------------------------------------------------------
-
-Python is an Object Oriented Programming language and as such it
-already provides a consistent and strong architecture and syntax
-a templating language would not reach.
-
-Using Python instead of a template langage for describing the user interface
-makes it to maintain with real functions/classes/contexts without the need of
-learning a new dialect. By using Python, we use standard OOP techniques and
-this is a key factor in a robust application.
-
-CubicWeb looks pretty recent. Is it stable ?
---------------------------------------------
-
-It is constantly evolving, piece by piece.  The framework has evolved since
-2001 and data has been migrated from one schema to the other ever since. There
-is a well-defined way to handle data and schema migration.
-
-You can see the roadmap there:
-http://www.cubicweb.org/project/cubicweb?tab=projectroadmap_tab.
-
-
-Why is the RQL query language looking similar to X ?
-----------------------------------------------------
-
-It may remind you of SQL but it is higher level than SQL, more like
-SPARQL. Except that SPARQL did not exist when we started the project.
-With version 3.4, CubicWeb has support for SPARQL.
-
-The RQL language is what is going to make a difference with django-
-like frameworks for several reasons.
-
-1. accessing data is *much* easier with it. One can write complex
-   queries with RQL that would be tedious to define and hard to maintain
-   using an object/filter suite of method calls.
-
-2. it offers an abstraction layer allowing your applications to run
-   on multiple back-ends. That means not only various SQL backends
-   (postgresql, sqlite, mysql), but also multiple databases at the
-   same time, and also non-SQL data stores like LDAP directories and
-   subversion/mercurial repositories (see the `vcsfile`
-   component). Google App Engine is yet another supported target for
-   RQL.
-
-Which ajax library is CubicWeb using ?
---------------------------------------
-
-CubicWeb uses jQuery_ and provides a few helpers on top of that. Additionally,
-some jQuery plugins are provided (some are provided in specific cubes).
-
-.. _jQuery: http://jquery.com
-
-
-Development
-```````````
-
-How to change the instance logo ?
----------------------------------
-
-The logo is managed by css. You must provide a custom css that will contain
-the code below: 
-
-::
-   
-     #logo {
-        background-image: url("logo.jpg");
-     }
-
-
-``logo.jpg`` is in ``mycube/data`` directory.
-
-How to create an anonymous user ?
----------------------------------
-
-This allows to browse the site without being authenticated. In the
-``all-in-one.conf`` file of your instance, define the anonymous user
-as follows ::
-
-  # login of the CubicWeb user account to use for anonymous user (if you want to
-  # allow anonymous)
-  anonymous-user=anon
-
-  # password of the CubicWeb user account matching login
-  anonymous-password=anon
-
-You also must ensure that this `anon` user is a registered user of
-the DB backend. If not, you can create through the administation
-interface of your instance by adding a user with in the group `guests`.
-
-.. note::
-    While creating a new instance, you can decide to allow access
-    to anonymous user, which will automatically execute what is
-    decribed above.
-
-How to load data from a python script ?
----------------------------------------
-Please, refer to :ref:`UsingPyro`.
-
-
-How to format an entity date attribute ?
-----------------------------------------
-
-If your schema has an attribute of type `Date` or `Datetime`, you usually want to
-format it when displaying it. First, you should define your preferred format
-using the site configuration panel
-``http://appurl/view?vid=systempropertiesform`` and then set ``ui.date`` and/or
-``ui.datetime``.  Then in the view code, use:
-
-.. sourcecode:: python
-
-    entity.printable_value(date_attribute)
-
-which will always return a string whatever the attribute's type (so it's
-recommended also for other attribute types). By default it expects to generate
-HTML, so it deals with rich text formating, xml escaping...
-
-How to update a database after a schema modification ?
-------------------------------------------------------
-
-It depends on what has been modified in the schema.
-
-* update the permissions and properties of an entity or a relation:
-  ``sync_schema_props_perms('MyEntityOrRelation')``.
-
-* add an attribute: ``add_attribute('MyEntityType', 'myattr')``.
-
-* add a relation: ``add_relation_definition('SubjRelation', 'MyRelation', 'ObjRelation')``.
-
-I get `NoSelectableObject` exceptions, how do I debug selectors ?
------------------------------------------------------------------
-
-You just need to put the appropriate context manager around view/component
-selection. One standard place for components is in cubicweb/vregistry.py: 
-
-.. sourcecode:: python
-
-    def possible_objects(self, *args, **kwargs):
-        """return an iterator on possible objects in this registry for the given
-        context
-        """
-        from logilab.common.registry import traced_selection
-        with traced_selection():
-            for appobjects in self.itervalues():
-                try:
-                    yield self._select_best(appobjects, *args, **kwargs)
-                except NoSelectableObject:
-                    continue
-
-This will yield additional WARNINGs, like this::
-
-    2009-01-09 16:43:52 - (cubicweb.selectors) WARNING: selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
-
-For views, you can put this context in `cubicweb/web/views/basecontrollers.py` in
-the `ViewController`:
-
-.. sourcecode:: python
-
-    def _select_view_and_rset(self, rset):
-        ...
-        try:
-            from logilab.common.registry import traced_selection
-            with traced_selection():
-                view = self._cw.vreg['views'].select(vid, req, rset=rset)
-        except ObjectNotFound:
-            self.warning("the view %s could not be found", vid)
-            req.set_message(req._("The view %s could not be found") % vid)
-            vid = vid_from_rset(req, rset, self._cw.vreg.schema)
-            view = self._cw.vreg['views'].select(vid, req, rset=rset)
-        ...
-
-I get "database is locked" when executing tests
------------------------------------------------
-
-If you have "database is locked" as error when you are executing security tests,
-it is usually because commit or rollback are missing before login() calls.
-
-You can also use a context manager, to avoid such errors, as described
-here: :ref:`securitytest`.
-
-
-What are hooks used for ?
--------------------------
-
-Hooks are executed around (actually before or after) events.  The most common
-events are data creation, update and deletion.  They permit additional constraint
-checking (those not expressible at the schema level), pre and post computations
-depending on data movements.
-
-As such, they are a vital part of the framework.
-
-Other kinds of hooks, called Operations, are available
-for execution just before commit.
-
-For more information, read :ref:`hooks` section.
-
-
-Configuration
-`````````````
-
-How to configure a LDAP source ?
---------------------------------
-
-See :ref:`LDAP`.
-
-How to import LDAP users in |cubicweb| ?
-----------------------------------------
-
-  Here is a useful script which enables you to import LDAP users
-  into your *CubicWeb* instance by running the following:
-
-.. sourcecode:: python
-
-    import os
-    import pwd
-    import sys
-
-    from logilab.database import get_connection
-
-    def getlogin():
-        """avoid using os.getlogin() because of strange tty/stdin problems
-        (man 3 getlogin)
-        Another solution would be to use $LOGNAME, $USER or $USERNAME
-        """
-        return pwd.getpwuid(os.getuid())[0]
-
-
-    try:
-        database = sys.argv[1]
-    except IndexError:
-        print 'USAGE: python ldap2system.py <database>'
-        sys.exit(1)
-
-    if raw_input('update %s db ? [y/n]: ' % database).strip().lower().startswith('y'):
-        cnx = get_connection(user=getlogin(), database=database)
-        cursor = cnx.cursor()
-
-        insert = ('INSERT INTO euser (creation_date, eid, modification_date, login, '
-                  ' firstname, surname, last_login_time, upassword) '
-                  "VALUES (%(mtime)s, %(eid)s, %(mtime)s, %(login)s, %(firstname)s, "
-                  "%(surname)s, %(mtime)s, './fqEz5LeZnT6');")
-        update = "UPDATE entities SET source='system' WHERE eid=%(eid)s;"
-        cursor.execute("SELECT eid,type,source,extid,mtime FROM entities WHERE source!='system'")
-        for eid, type, source, extid, mtime in cursor.fetchall():
-            if type != 'CWUser':
-                print "don't know what to do with entity type", type
-                continue
-            if source != 'ldapuser':
-                print "don't know what to do with source type", source
-                continue
-            ldapinfos = dict(x.strip().split('=') for x in extid.split(','))
-            login = ldapinfos['uid']
-            firstname = ldapinfos['uid'][0].upper()
-            surname = ldapinfos['uid'][1:].capitalize()
-            if login != 'jcuissinat':
-                args = dict(eid=eid, type=type, source=source, login=login,
-                            firstname=firstname, surname=surname, mtime=mtime)
-                print args
-                cursor.execute(insert, args)
-                cursor.execute(update, args)
-
-        cnx.commit()
-        cnx.close()
-
-
-Security
-````````
-
-How to reset the password for user joe ?
-----------------------------------------
-
-If you want to reset the admin password for ``myinstance``, do::
-
-    $ cubicweb-ctl reset-admin-pwd myinstance
-
-You need to generate a new encrypted password::
-
-    $ python
-    >>> from cubicweb.server.utils import crypt_password
-    >>> crypt_password('joepass')
-    'qHO8282QN5Utg'
-    >>>
-
-and paste it in the database::
-
-    $ psql mydb
-    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 import Binary
-    >>> 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.cw_set(upassword=Binary(crypted))
-
-Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file.
-
-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 !
----------------------------------------------------------
-
-You are probably getting errors such as ::
-
-  remove {'PR': 'Project', 'C': 'CWUser'} from solutions since your_user has no read access to cost
-
-This is because you have to put your user in the "users" group. The user has to
-be in both groups.
-
-How is security implemented ?
-------------------------------
-
-The basis for security is a mapping from operations to groups or
-arbitrary RQL expressions. These mappings are scoped to entities and
-relations.
-
-This is an example for an Entity Type definition:
-
-.. sourcecode:: python
-
-    class Version(EntityType):
-        """a version is defining the content of a particular project's
-        release"""
-        # definition of attributes is voluntarily missing
-        __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'),)}
-
-The above means that permission to read a Version is granted to any
-user that is part of one of the groups 'managers', 'users', 'guests'.
-The 'add' permission is granted to users in group 'managers' or
-'logilab' or to users in group G, if G is linked by a permission
-entity named "add_version" to the version's project.
-
-An example for a Relation Definition (RelationType both defines a
-relation type and implicitly one relation definition, on which the
-permissions actually apply):
-
-.. sourcecode:: python
-
-    class version_of(RelationType):
-        """link a version to its project. A version is necessarily linked
-        to one and only one project. """
-        # some lines voluntarily missing
-        __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'),) }
-
-The main difference lies in the basic available operations (there is
-no 'update' operation) and the usage of an RRQLExpression (rql
-expression for a relation) instead of an ERQLExpression (rql
-expression for an entity).
-
-You can find additional information in the section :ref:`securitymodel`.
-
-Is it possible to bypass security from the UI (web front) part ?
-----------------------------------------------------------------
-
-No. Only Hooks/Operations can do that.
-
-Can PostgreSQL and CubicWeb authentication work with kerberos ?
-----------------------------------------------------------------
-
-If you have PostgreSQL set up to accept kerberos authentication, you can set
-the db-host, db-name and db-user parameters in the `sources` configuration
-file while leaving the password blank. It should be enough for your
-instance to connect to postgresql with a kerberos ticket.
-
-
--- a/doc/book/en/annexes/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Part4:
-
-----------
-Appendixes
-----------
-
-The following chapters are reference material.
-
-.. toctree::
-   :maxdepth: 1
-   :numbered:
-
-   faq
-   rql/index
-   mercurial
-   depends
-   docstrings-conventions
--- a/doc/book/en/annexes/mercurial.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _MercurialPresentation:
-
-Introducing Mercurial
-=====================
-
-Introduction
-````````````
-Mercurial_ manages a distributed repository containing revisions
-trees (each revision indicates the changes required to obtain the
-next, and so on). Locally, we have a repository containing revisions
-tree, and a working directory. It is possible
-to put in its working directory, one of the versions of its local repository,
-modify and then push it in its repository.
-It is also possible to get revisions from another repository or to export
-its own revisions from the local repository to another repository.
-
-.. _Mercurial: http://www.selenic.com/mercurial/
-
-In contrast to CVS/Subversion, we usually create a repository per
-project to manage.
-
-In a collaborative development, we usually create a central repository
-accessible to all developers of the project. These central repository is used
-as a reference. According to their needs, everyone can have a local repository,
-that they will have to synchronize with the central repository from time to time.
-
-
-Major commands
-``````````````
-* Create a local repository::
-
-     hg clone ssh://myhost//home/src/repo
-
-* See the contents of the local repository (graphical tool in Qt)::
-
-     hgview
-
-* Add a sub-directory or file in the current directory::
-
-     hg add subdir
-
-* Move to the working directory a specific revision (or last
-  revision) from the local repository::
-
-     hg update [identifier-revision]
-     hg up [identifier-revision]
-
-* Get in its local repository, the tree of revisions contained in a
-  remote repository (this does not change the local directory)::
-
-     hg pull ssh://myhost//home/src/repo
-     hg pull -u ssh://myhost//home/src/repo # equivalent to pull + update
-
-* See what are the heads of branches of the local repository if a `pull`
-  returned a new branch::
-
-     hg heads
-
-* Submit the working directory in the local repository (and create a new
-  revision)::
-
-     hg commit
-     hg ci
-
-* Merge with the mother revision of local directory, another revision from
-  the local respository (the new revision will be then two mothers
-  revisions)::
-
-     hg merge identifier-revision
-
-* Export to a remote repository, the tree of revisions in its content
-  local respository (this does not change the local directory)::
-
-     hg push ssh://myhost//home/src/repo
-
-* See what local revisions are not in another repository::
-
-     hg outgoing ssh://myhost//home/src/repo
-
-* See what are the revisions of a repository not found locally::
-
-     hg incoming ssh://myhost//home/src/repo
-
-* See what is the revision of the local repository which has been taken out
-  from the working directory and amended::
-
-     hg parent
-
-* See the differences between the working directory and the mother revision
-  of the local repository, possibly to submit them in the local repository::
-
-     hg diff
-     hg commit-tool
-     hg ct
-
-
-Best Practices
-``````````````
-* Remember to `hg pull -u` regularly, and particularly before
-   a `hg commit`.
-
-* Remember to `hg push` when your repository contains a version
-  relatively stable of your changes.
-
-* If a `hg pull -u` created a new branch head:
-
-   1. find its identifier with `hg head`
-   2. merge with `hg merge`
-   3. `hg ci`
-   4. `hg push`
-
-Installation of the guestrepo extension
-```````````````````````````````````````
-
-Set up the guestrepo extension by getting a copy of the sources
-from https://bitbucket.org/selinc/guestrepo and adding the following
-lines to your ``~/.hgrc``: ::
-
-   [extensions]
-   guestrepo=/path/to/guestrepo/guestrepo
-
-
-More information
-````````````````
-
-For more information about Mercurial, please refer to the Mercurial project online documentation_.
-
-.. _documentation: http://www.selenic.com/mercurial/wiki/
-
Binary file doc/book/en/annexes/rql/Graph-ex.gif has changed
--- a/doc/book/en/annexes/rql/debugging.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _DEBUGGING:
-
-Debugging RQL
--------------
-
-Available levels
-~~~~~~~~~~~~~~~~
-
-Server debugging flags. They may be combined using binary operators.
-
-.. autodata:: cubicweb.server.DBG_NONE
-.. autodata:: cubicweb.server.DBG_RQL
-.. autodata:: cubicweb.server.DBG_SQL
-.. autodata:: cubicweb.server.DBG_REPO
-.. autodata:: cubicweb.server.DBG_MS
-.. autodata:: cubicweb.server.DBG_HOOKS
-.. autodata:: cubicweb.server.DBG_OPS
-.. autodata:: cubicweb.server.DBG_MORE
-.. autodata:: cubicweb.server.DBG_ALL
-
-
-Enable verbose output
-~~~~~~~~~~~~~~~~~~~~~
-
-To debug your RQL statements, it can be useful to enable a verbose output:
-
-.. sourcecode:: python
-
-    from cubicweb import server
-    server.set_debug(server.DBG_RQL|server.DBG_SQL|server.DBG_ALL)
-
-.. autofunction:: cubicweb.server.set_debug
-
-Another example showing how to debug hooks at a specific code site:
-
-.. sourcecode:: python
-
-    from cubicweb.server import debugged, DBG_HOOKS
-    with debugged(DBG_HOOKS):
-        person.cw_set(works_for=company)
-
-
-Detect largest RQL queries
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-See `Profiling and performance` chapter (see :ref:`PROFILING`).
-
-
-API
-~~~
-
-.. autoclass:: cubicweb.server.debugged
-
--- a/doc/book/en/annexes/rql/implementation.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-
-
-Implementation
---------------
-
-BNF grammar
-~~~~~~~~~~~
-
-The terminal elements are in capital letters, non-terminal in lowercase.
-The value of the terminal elements (between quotes) is a Python regular
-expression.
-::
-
-     statement ::= (select | delete | insert | update) ';'
-
-
-     # select specific rules
-     select      ::= 'DISTINCT'? E_TYPE selected_terms restriction? group? sort?
-
-     selected_terms ::= expression ( ',' expression)*
-
-     group       ::= 'GROUPBY' VARIABLE ( ',' VARIABLE)*
-
-     sort        ::= 'ORDERBY' sort_term ( ',' sort_term)*
-
-     sort_term   ::=  VARIABLE sort_method =?
-
-     sort_method ::= 'ASC' | 'DESC'
-
-
-     # delete specific rules
-     delete ::= 'DELETE' (variables_declaration | relations_declaration) restriction?
-
-
-     # insert specific rules
-     insert ::= 'INSERT' variables_declaration ( ':' relations_declaration)? restriction?
-
-
-     # update specific rules
-     update ::= 'SET' relations_declaration restriction
-
-
-     # common rules
-     variables_declaration ::= E_TYPE VARIABLE (',' E_TYPE VARIABLE)*
-
-     relations_declaration ::= simple_relation (',' simple_relation)*
-
-     simple_relation ::= VARIABLE R_TYPE expression
-
-     restriction ::= 'WHERE' relations
-
-     relations   ::= relation (LOGIC_OP relation)*
-                   | '(' relations')'
-
-     relation    ::= 'NOT'? VARIABLE R_TYPE COMP_OP? expression
-                   | 'NOT'? R_TYPE VARIABLE 'IN' '(' expression (',' expression)* ')'
-
-     expression  ::= var_or_func_or_const (MATH_OP var_or_func_or_const) *
-                   | '(' expression ')'
-
-     var_or_func_or_const ::= VARIABLE | function | constant
-
-     function    ::= FUNCTION '(' expression ( ',' expression) * ')'
-
-     constant    ::= KEYWORD | STRING | FLOAT | INT
-
-     # tokens
-     LOGIC_OP ::= ',' | 'OR' | 'AND'
-     MATH_OP  ::= '+' | '-' | '/' | '*'
-     COMP_OP  ::= '>' | '>=' | '=' | '<=' | '<' | '~=' | 'LIKE'
-
-     FUNCTION ::= 'MIN' | 'MAX' | 'SUM' | 'AVG' | 'COUNT' | 'UPPER' | 'LOWER'
-
-     VARIABLE ::= '[A-Z][A-Z0-9]*'
-     E_TYPE   ::= '[A-Z]\w*'
-     R_TYPE   ::= '[a-z_]+'
-
-     KEYWORD  ::= 'TRUE' | 'FALSE' | 'NULL' | 'TODAY' | 'NOW'
-     STRING   ::= "'([^'\]|\\.)*'" |'"([^\"]|\\.)*\"'
-     FLOAT    ::= '\d+\.\d*'
-     INT      ::= '\d+'
-
-
-Internal representation (syntactic tree)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The tree research does not contain the selected variables
-(e.g. there is only what follows "WHERE").
-
-The insertion tree does not contain the variables inserted or relations
-defined on these variables (e.g. there is only what follows "WHERE").
-
-The removal tree does not contain the deleted variables and relations
-(e.g. there is only what follows the "WHERE").
-
-The update tree does not contain the variables and relations updated
-(e.g. there is only what follows the "WHERE").
-
-::
-
-     Select         ((Relationship | And | Or)?, Group?, Sort?)
-     Insert         (Relations | And | Or)?
-     Delete         (Relationship | And | Or)?
-     Update         (Relations | And | Or)?
-
-     And            ((Relationship | And | Or), (Relationship | And | Or))
-     Or             ((Relationship | And | Or), (Relationship | And | Or))
-
-     Relationship   ((VariableRef, Comparison))
-
-     Comparison     ((Function | MathExpression | Keyword | Constant | VariableRef) +)
-
-     Function       (())
-     MathExpression ((MathExpression | Keyword | Constant | VariableRef), (MathExpression | Keyword | Constant | VariableRef))
-
-     Group          (VariableRef +)
-     Sort           (SortTerm +)
-     SortTerm       (VariableRef +)
-
-     VariableRef    ()
-     Variable       ()
-     Keyword        ()
-     Constant       ()
-
-
-Known limitations
-~~~~~~~~~~~~~~~~~
-
-- The current implementation does not support linking two relations of type 'is'
-  with an OR. I do not think that the negation is supported on this type of
-  relation (XXX to be confirmed).
-
-- missing COALESCE and certainly other things...
-
-- writing an rql query requires knowledge of the used schema (with real relation
-  names and entities, not those viewed in the user interface). On the other
-  hand, we cannot really bypass that, and it is the job of a user interface to
-  hide the RQL.
-
-
-Topics
-~~~~~~
-
-It would be convenient to express the schema matching
-relations (non-recursive rules)::
-
-     Document class Type <-> Document occurence_of Fiche class Type
-     Sheet class Type    <-> Form collection Collection class Type
-
-Therefore 1. becomes::
-
-     Document X where
-     X class C, C name 'Cartoon'
-     X owned_by U, U login 'syt'
-     X available true
-
-I'm not sure that we should handle this at RQL level ...
-
-There should also be a special relation 'anonymous'.
--- a/doc/book/en/annexes/rql/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-.. _RQLChapter:
-
-Relation Query Language (RQL)
-=============================
-
-This chapter describes the Relation Query Language syntax and its implementation in CubicWeb.
-
-.. toctree::
-   :maxdepth: 2
-
-   intro
-   language
-   debugging
-   implementation
--- a/doc/book/en/annexes/rql/intro.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,162 +0,0 @@
-
-.. _rql_intro:
-
-Introduction
-------------
-
-Goals of RQL
-~~~~~~~~~~~~
-
-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).
-
-Comparison with existing languages
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-SQL
-```
-
-RQL may remind of SQL but works at a higher abstraction level (the *CubicWeb*
-framework generates SQL from RQL to fetch data from relation databases). RQL is
-focused on browsing relations. The user needs only to know about the *CubicWeb*
-data model he is querying, but not about the underlying SQL model.
-
-Sparql
-``````
-
-The query language most similar to RQL is SPARQL_, defined by the W3C to serve
-for the semantic web.
-
-Versa
-`````
-
-We should look in more detail, but here are already some ideas for the moment
-... Versa_ is the language most similar to what we wanted to do, but the model
-underlying data being RDF, there are some things such as namespaces or
-handling of the RDF types which does not interest us. On the functionality
-level, Versa_ is very comprehensive including through many functions of
-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
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Search (`Any`)
-   Extract entities and attributes of entities.
-
-Insert entities (`INSERT`)
-   Insert new entities or relations in the database.
-   It can also directly create relationships for the newly created entities.
-
-Update entities, create relations (`SET`)
-   Update existing entities in the database,
-   or create relations between existing entities.
-
-Delete entities or relationship (`DELETE`)
-   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`_
-
-
-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	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,804 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _RQL:
-
-RQL syntax
-----------
-
-.. _RQLKeywords:
-
-Reserved keywords
-~~~~~~~~~~~~~~~~~
-
-::
-
-  AND, ASC, BEING, DELETE, DESC, DISTINCT, EXISTS, FALSE, GROUPBY,
-  HAVING, ILIKE, INSERT, LIKE, LIMIT, NOT, NOW, NULL, OFFSET,
-  OR, ORDERBY, SET, TODAY, TRUE, UNION, WHERE, WITH
-
-The keywords are not case sensitive. You should not use them when defining your
-schema, or as RQL variable names.
-
-
-.. _RQLCase:
-
-Case
-~~~~
-
-* Variables should be all upper-cased.
-
-* Relation should be all lower-cased and match exactly names of relations defined
-  in the schema.
-
-* Entity types should start with an upper cased letter and be followed by at least
-  a lower cased latter.
-
-
-.. _RQLVariables:
-
-Variables and typing
-~~~~~~~~~~~~~~~~~~~~
-
-Entities and values to browse and/or select are represented in the query by
-*variables* that must be written in capital letters.
-
-With RQL, we do not distinguish between entities and attributes. The value of an
-attribute is considered as an entity of a particular type (see below), linked to
-one (real) entity by a relation called the name of the attribute, where the
-entity is the subject and the attribute the object.
-
-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.
-
-We can restrict the possible types for a variable using the special relation
-**is** in the restrictions.
-
-
-.. _VirtualRelations:
-
-Virtual relations
-~~~~~~~~~~~~~~~~~
-
-Those relations may only be used in RQL query but are not actual attributes of
-your entities.
-
-* `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 is the same as another
-  when you've to use two different variables for querying purpose. On the
-  opposite it's also useful together with the ``NOT`` operator to tell that two
-  variables should not identify the same entity
-
-
-.. _RQLLiterals:
-
-Literal expressions
-~~~~~~~~~~~~~~~~~~~
-
-Bases types supported by RQL are those supported by yams schema. Literal values
-are expressed as explained below:
-
-* string should be between double or single quotes. If the value contains a
-  quote, it should be preceded by a backslash '\\'
-
-* floats separator is dot '.'
-
-* boolean values are ``TRUE`` and ``FALSE`` keywords
-
-* date and time should be expressed as a string with ISO notation : YYYY/MM/DD
-  [hh:mm], or using keywords ``TODAY`` and ``NOW``
-
-You may also use the ``NULL`` keyword, meaning 'unspecified'.
-
-
-.. _RQLOperators:
-
-Operators
-~~~~~~~~~
-
-.. _RQLLogicalOperators:
-
-Logical operators
-`````````````````
-::
-
-     AND, OR, NOT, ','
-
-',' is equivalent to 'AND' but with the smallest among the priority of logical
-operators (see :ref:`RQLOperatorsPriority`).
-
-.. _RQLMathematicalOperators:
-
-Mathematical operators
-``````````````````````
-
-+----------+---------------------+-----------+--------+
-| Operator |    Description      | Example   | Result |
-+==========+=====================+===========+========+
-|  `+`     | addition            | 2 + 3     | 5      |
-+----------+---------------------+-----------+--------+
-|  `-`     | subtraction         | 2 - 3     | -1     |
-+----------+---------------------+-----------+--------+
-|  `*`     | multiplication      | 2 * 3     | 6      |
-+----------+---------------------+-----------+--------+
-|  /       | division            | 4 / 2     | 2      |
-+----------+---------------------+-----------+--------+
-|  %       | modulo (remainder)  | 5 % 4     | 1      |
-+----------+---------------------+-----------+--------+
-|  ^       | exponentiation      | 2.0 ^ 3.0 | 8      |
-+----------+---------------------+-----------+--------+
-|  &       | bitwise AND         | 91 & 15   | 11     |
-+----------+---------------------+-----------+--------+
-|  `|`     | bitwise OR          | 32 | 3    | 35     |
-+----------+---------------------+-----------+--------+
-|  #       | bitwise XOR         | 17 # 5    | 20     |
-+----------+---------------------+-----------+--------+
-|  ~       | bitwise NOT         | ~1        | -2     |
-+----------+---------------------+-----------+--------+
-|  <<      | bitwise shift left  | 1 << 4    | 16     |
-+----------+---------------------+-----------+--------+
-|  >>      | bitwise shift right | 8 >> 2    | 2      |
-+----------+---------------------+-----------+--------+
-
-
-Notice integer division truncates results depending on the backend behaviour. For
-instance, postgresql does.
-
-
-.. _RQLComparisonOperators:
-
-Comparison operators
-````````````````````
- ::
-
-     =, !=, <, <=, >=, >, IN
-
-
-The syntax to use comparison operators is:
-
-    `VARIABLE attribute <operator> VALUE`
-
-The `=` operator is the default operator and can be omitted, i.e. :
-
-    `VARIABLE attribute = VALUE`
-
-is equivalent to
-
-    `VARIABLE attribute VALUE`
-
-
-The operator `IN` provides a list of possible values:
-
-.. sourcecode:: sql
-
-    Any X WHERE X name IN ('chauvat', 'fayolle', 'di mascio', 'thenault')
-
-
-.. _RQLStringOperators:
-
-String operators
-````````````````
-::
-
-  LIKE, ILIKE, ~=, REGEXP
-
-The ``LIKE`` string operator can be used with the special character `%` in
-a string as wild-card:
-
-.. sourcecode:: sql
-
-     -- match every entity whose name starts with 'Th'
-     Any X WHERE X name ~= 'Th%'
-     -- match every entity whose name endswith 'lt'
-     Any X WHERE X name LIKE '%lt'
-     -- match every entity whose name contains a 'l' and a 't'
-     Any X WHERE X name LIKE '%l%t%'
-
-``ILIKE`` is the case insensitive version of ``LIKE``. It's not
-available on all backend (e.g. sqlite doesn't support it). If not available for
-your backend, ``ILIKE`` will behave like ``LIKE``.
-
-`~=` is a shortcut version of ``ILIKE``, or of ``LIKE`` when the
-former is not available on the back-end.
-
-
-The ``REGEXP`` is an alternative to ``LIKE`` that supports POSIX
-regular expressions:
-
-.. sourcecode:: sql
-
-   -- match entities whose title starts with a digit
-   Any X WHERE X title REGEXP "^[0-9].*"
-
-
-The underlying SQL operator used is back-end-dependent :
-
-- the ``~`` operator is used for postgresql,
-- the ``REGEXP`` operator for mysql and sqlite.
-
-Other back-ends are not supported yet.
-
-
-.. _RQLOperatorsPriority:
-
-Operators priority
-``````````````````
-
-#. `(`, `)`
-#. `^`, `<<`, `>>`
-#. `*`, `/`, `%`, `&`
-#. `+`, `-`, `|`, `#`
-#. `NOT`
-#. `AND`
-#. `OR`
-#. `,`
-
-
-.. _RQLSearchQuery:
-
-Search Query
-~~~~~~~~~~~~
-
-Simplified grammar of search query: ::
-
-   [ `DISTINCT`] `Any` V1 (, V2) \*
-   [ `GROUPBY` V1 (, V2) \*] [ `ORDERBY` <orderterms>]
-   [ `LIMIT` <value>] [ `OFFSET` <value>]
-   [ `WHERE` <triplet restrictions>]
-   [ `WITH` V1 (, V2)\* BEING (<query>)]
-   [ `HAVING` <other restrictions>]
-   [ `UNION` <query>]
-
-Selection
-`````````
-
-The fist occuring clause is the selection of terms that should be in the result
-set.  Terms may be variable, literals, function calls, arithmetic, etc. and each
-term is separated by a comma.
-
-There will be as much column in the result set as term in this clause, respecting
-order.
-
-Syntax for function call is somewhat intuitive, for instance:
-
-.. sourcecode:: sql
-
-    Any UPPER(N) WHERE P firstname N
-
-
-Grouping and aggregating
-````````````````````````
-
-The ``GROUPBY`` keyword is followed by a list of terms on which results
-should be grouped. They are usually used with aggregate functions, responsible to
-aggregate values for each group (see :ref:`RQLAggregateFunctions`).
-
-For grouped queries, all selected variables must be either aggregated (i.e. used
-by an aggregate function) or grouped (i.e. listed in the ``GROUPBY``
-clause).
-
-
-Sorting
-```````
-
-The ``ORDERBY`` keyword if followed by the definition of the selection
-order: variable or column number followed by sorting method (``ASC``,
-``DESC``), ``ASC`` being the default. If the sorting method is not
-specified, then the sorting is ascendant (`ASC`).
-
-
-Pagination
-``````````
-
-The ``LIMIT`` and ``OFFSET`` keywords may be respectively used to
-limit the number of results and to tell from which result line to start (for
-instance, use `LIMIT 20` to get the first 20 results, then `LIMIT 20 OFFSET 20`
-to get the next 20.
-
-
-Restrictions
-````````````
-
-The ``WHERE`` keyword introduce one of the "main" part of the query, where
-you "define" variables and add some restrictions telling what you're interested
-in.
-
-It's a list of triplets "subject relation object", e.g. `V1 relation
-(V2 | <static value>)`. Triplets are separated using :ref:`RQLLogicalOperators`.
-
-.. note::
-
-  About the negation operator (``NOT``):
-
-  * ``NOT X relation Y`` is equivalent to ``NOT EXISTS(X relation Y)``
-
-  * ``Any X WHERE NOT X owned_by U`` means "entities that have no relation
-    ``owned_by``".
-
-  * ``Any X WHERE NOT X owned_by U, U login "syt"`` means "the entity have no
-     relation ``owned_by`` with the user syt". They may have a relation "owned_by"
-     with another user.
-
-In this clause, you can also use ``EXISTS`` when you want to know if some
-expression is true and do not need the complete set of elements that make it
-true. Testing for existence is much faster than fetching the complete set of
-results, especially when you think about using ``OR`` against several expressions. For instance
-if you want to retrieve versions which are in state "ready" or tagged by
-"priority", you should write :
-
-.. sourcecode:: sql
-
-    Any X ORDERBY PN,N
-    WHERE X num N, X version_of P, P name PN,
-          EXISTS(X in_state S, S name "ready")
-          OR EXISTS(T tags X, T name "priority")
-
-not
-
-.. sourcecode:: sql
-
-    Any X ORDERBY PN,N
-    WHERE X num N, X version_of P, P name PN,
-          (X in_state S, S name "ready")
-          OR (T tags X, T name "priority")
-
-Both queries aren't at all equivalent :
-
-* the former will retrieve all versions, then check for each one which are in the
-  matching state of or tagged by the expected tag,
-
-* the later will retrieve all versions, state and tags (cartesian product!),
-  compute join and then exclude each row which are in the matching state or
-  tagged by the expected tag. This implies that you won't get any result if the
-  in_state or tag tables are empty (ie there is no such relation in the
-  application). This is usually NOT what you want.
-
-Another common case where you may want to use ``EXISTS`` is when you
-find yourself using ``DISTINCT`` at the beginning of your query to
-remove duplicate results. The typical case is when you have a
-multivalued relation such as Version version_of Project and you want
-to retrieve projects which have a version:
-
-.. sourcecode:: sql
-
-  Any P WHERE V version_of P
-
-will return each project number of versions times. So you may be
-tempted to use:
-
-.. sourcecode:: sql
-
-  DISTINCT Any P WHERE V version_of P
-
-This will work, but is not efficient, as it will use the ``SELECT
-DISTINCT`` SQL predicate, which needs to retrieve all projects, then
-sort them and discard duplicates, which can have a very high cost for
-large result sets. So the best way to write this is:
-
-.. sourcecode:: sql
-
-  Any P WHERE EXISTS(V version_of P)
-
-
-You can also use the question mark (`?`) to mark optional relations. This allows
-you to select entities related **or not** to another. It is a similar concept
-to `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).
-
-You must use the `?` behind a variable to specify that the relation to
-that variable is optional. For instance:
-
-- Bugs of a project attached or not to a version
-
-   .. sourcecode:: sql
-
-       Any X, V WHERE X concerns P, P eid 42, X corrected_in V?
-
-  You will get a result set containing all the project's tickets, with either the
-  version in which it's fixed or None for tickets not related to a version.
-
-
-- All cards and the project they document if any
-
-  .. sourcecode:: sql
-
-       Any C, P WHERE C is Card, P? documented_by C
-
-Notice you may also use outer join:
-
-- on the RHS of attribute relation, e.g.
-
-  .. sourcecode:: sql
-
-       Any X WHERE X ref XR, Y name XR?
-
-  so that Y is outer joined on X by ref/name attributes comparison
-
-
-- on any side of an ``HAVING`` expression, e.g.
-
-  .. sourcecode:: sql
-
-       Any X WHERE X creation_date XC, Y creation_date YC
-       HAVING YEAR(XC)=YEAR(YC)?
-
-  so that Y is outer joined on X by comparison of the year extracted from their
-  creation date.
-
-  .. sourcecode:: sql
-
-       Any X WHERE X creation_date XC, Y creation_date YC
-       HAVING YEAR(XC)?=YEAR(YC)
-
-  would outer join X on Y instead.
-
-
-Having restrictions
-```````````````````
-
-The ``HAVING`` clause, as in SQL, may be used to restrict a query
-according to value returned by an aggregate function, e.g.
-
-.. sourcecode:: sql
-
-    Any X GROUPBY X WHERE X relation Y HAVING COUNT(Y) > 10
-
-It may however be used for something else: In the ``WHERE`` clause, we are
-limited to triplet expressions, so some things may not be expressed there. Let's
-take an example : if you want to get people whose upper-cased first name equals to
-another person upper-cased first name. There is no proper way to express this
-using triplet, so you should use something like:
-
-.. sourcecode:: sql
-
-    Any X WHERE X firstname XFN, Y firstname YFN, NOT X identity Y HAVING UPPER(XFN) = UPPER(YFN)
-
-Another example: imagine you want person born in 2000:
-
-.. sourcecode:: sql
-
-    Any X WHERE X birthday XB HAVING YEAR(XB) = 2000
-
-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.
-
-
-Sub-queries
-```````````
-
-The ``WITH`` keyword introduce sub-queries clause. Each sub-query has the
-form:
-
-  V1(,V2) BEING (rql query)
-
-Variables at the left of the ``BEING`` keyword defines into which
-variables results from the sub-query will be mapped to into the outer query.
-Sub-queries are separated from each other using a comma.
-
-Let's say we want to retrieve for each project its number of versions and its
-number of tickets. Due to the nature of relational algebra behind the scene, this
-can't be achieved using a single query. You have to write something along the
-line of:
-
-.. sourcecode:: sql
-
-  Any X, VC, TC WHERE X identity XX
-  WITH X, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
-       XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
-
-Notice that we can't reuse a same variable name as alias for two different
-sub-queries, hence the usage of 'X' and 'XX' in this example, which are then
-unified using the special `identity` relation (see :ref:`VirtualRelations`).
-
-.. warning::
-
-  Sub-queries define a new variable scope, so even if a variable has the same name
-  in the outer query and in the sub-query, they technically **aren't** the same
-  variable. So:
-
-  .. sourcecode:: sql
-
-     Any W, REF WITH W, REF BEING
-         (Any W, REF WHERE W is Workcase, W ref REF,
-                           W concerned_by D, D name "Logilab")
-
-  could be written:
-
-  .. sourcecode:: sql
-
-     Any W, REF WITH W, REF BEING
-        (Any W1, REF1 WHERE W1 is Workcase, W1 ref REF1,
-                            W1 concerned_by D, D name "Logilab")
-
-  Also, when a variable is coming from a sub-query, you currently can't reference
-  its attribute or inlined relations in the outer query, you've to fetch them in
-  the sub-query. For instance, let's say we want to sort by project name in our
-  first example, we would have to write:
-
-  .. sourcecode:: sql
-
-
-    Any X, VC, TC ORDERBY XN WHERE X identity XX
-    WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X,XN WHERE V version_of X, X name XN),
-         XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
-
-  instead of:
-
-  .. sourcecode:: sql
-
-    Any X, VC, TC ORDERBY XN WHERE X identity XX, X name XN,
-    WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
-         XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
-
-  which would result in a SQL execution error.
-
-
-Union
-`````
-
-You may get a result set containing the concatenation of several queries using
-the ``UNION``. The selection of each query should have the same number of
-columns.
-
-.. sourcecode:: sql
-
-    (Any X, XN WHERE X is Person, X surname XN) UNION (Any X,XN WHERE X is Company, X name XN)
-
-
-.. _RQLFunctions:
-
-Available functions
-~~~~~~~~~~~~~~~~~~~
-
-Below is the list of aggregate and transformation functions that are supported
-nativly by the framework. Notice that cubes may define additional functions.
-
-.. _RQLAggregateFunctions:
-
-Aggregate functions
-```````````````````
-+------------------------+----------------------------------------------------------+
-| ``COUNT(Any)``         | return the number of rows                                |
-+------------------------+----------------------------------------------------------+
-| ``MIN(Any)``           | return the minimum value                                 |
-+------------------------+----------------------------------------------------------+
-| ``MAX(Any)``           | return the maximum value                                 |
-+------------------------+----------------------------------------------------------+
-| ``AVG(Any)``           | return the average value                                 |
-+------------------------+----------------------------------------------------------+
-| ``SUM(Any)``           | return the sum of values                                 |
-+------------------------+----------------------------------------------------------+
-| ``COMMA_JOIN(String)`` | return each value separated by a comma (for string only) |
-+------------------------+----------------------------------------------------------+
-
-All aggregate functions above take a single argument. Take care some aggregate
-functions (e.g. ``MAX``, ``MIN``) may return `None` if there is no
-result row.
-
-.. _RQLStringFunctions:
-
-String transformation functions
-```````````````````````````````
-
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``UPPER(String)``                             | upper case the string                                           |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``LOWER(String)``                             | lower case the string                                           |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``LENGTH(String)``                            | return the length of the string                                 |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``SUBSTRING(String, start, length)``          | extract from the string a string starting at given index and of |
-|                                               | given length                                                    |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``LIMIT_SIZE(String, max size)``              | if the length of the string is greater than given max size,     |
-|                                               | strip it and add ellipsis ("..."). The resulting string will    |
-|                                               | hence have max size + 3 characters                              |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``TEXT_LIMIT_SIZE(String, format, max size)`` | similar to the above, but allow to specify the MIME type of the |
-|                                               | text contained by the string. Supported formats are text/html,  |
-|                                               | text/xhtml and text/xml. All others will be considered as plain |
-|                                               | text. For non plain text format, sgml tags will be first removed|
-|                                               | before limiting the string.                                     |
-+-----------------------------------------------+-----------------------------------------------------------------+
-
-.. _RQLDateFunctions:
-
-Date extraction functions
-`````````````````````````
-
-+----------------------+----------------------------------------+
-| ``YEAR(Date)``       | return the year of a date or datetime  |
-+----------------------+----------------------------------------+
-| ``MONTH(Date)``      | return the month of a date or datetime |
-+----------------------+----------------------------------------+
-| ``DAY(Date)``        | return the day of a date or datetime   |
-+----------------------+----------------------------------------+
-| ``HOUR(Datetime)``   | return the hours of a datetime         |
-+----------------------+----------------------------------------+
-| ``MINUTE(Datetime)`` | return the minutes of a datetime       |
-+----------------------+----------------------------------------+
-| ``SECOND(Datetime)`` | return the seconds of a datetime       |
-+----------------------+----------------------------------------+
-| ``WEEKDAY(Date)``    | return the day of week of a date or    |
-|                      | datetime.  Sunday == 1, Saturday == 7. |
-+----------------------+----------------------------------------+
-
-.. _RQLOtherFunctions:
-
-Other functions
-```````````````
-+-------------------+--------------------------------------------------------------------+
-| ``ABS(num)``      |  return the absolute value of a number                             |
-+-------------------+--------------------------------------------------------------------+
-| ``RANDOM()``      | return a pseudo-random value from 0.0 to 1.0                       |
-+-------------------+--------------------------------------------------------------------+
-| ``FSPATH(X)``     | expect X to be an attribute whose value is stored in a             |
-|                   | :class:`BFSStorage` and return its path on the file system         |
-+-------------------+--------------------------------------------------------------------+
-| ``FTIRANK(X)``    | expect X to be an entity used in a has_text relation, and return a |
-|                   | number corresponding to the rank order of each resulting entity    |
-+-------------------+--------------------------------------------------------------------+
-| ``CAST(Type, X)`` | expect X to be an attribute and return it casted into the given    |
-|                   | final type                                                         |
-+-------------------+--------------------------------------------------------------------+
-
-
-.. _RQLExamples:
-
-Examples
-~~~~~~~~
-
-- *Search for the object of identifier 53*
-
-  .. sourcecode:: sql
-
-        Any X WHERE X eid 53
-
-- *Search material such as comics, owned by syt and available*
-
-  .. sourcecode:: sql
-
-        Any X WHERE X is Document,
-                    X occurence_of F, F class C, C name 'Comics',
-                    X owned_by U, U login 'syt',
-                    X available TRUE
-
-- *Looking for people working for eurocopter interested in training*
-
-  .. sourcecode:: sql
-
-        Any P WHERE P is Person, P work_for S, S name 'Eurocopter',
-                    P interested_by T, T name 'training'
-
-- *Search note less than 10 days old written by jphc or ocy*
-
-  .. sourcecode:: sql
-
-        Any N WHERE N is Note, N written_on D, D day> (today -10),
-                    N written_by P, P name 'jphc' or P name 'ocy'
-
-- *Looking for people interested in training or living in Paris*
-
-  .. sourcecode:: sql
-
-        Any P WHERE P is Person, EXISTS(P interested_by T, T name 'training') OR
-                    (P city 'Paris')
-
-- *The surname and firstname of all people*
-
-  .. sourcecode:: sql
-
-        Any N, P WHERE 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
-  to all the selected variables. We could write here
-
-  .. sourcecode:: sql
-
-        String N, P WHERE X is Person, X name N, X first_name P
-
-
-  Note: You can not specify several types with * ... where X is FirstType or X is SecondType*.
-  To specify several types explicitly, you have to do
-
-
-  .. sourcecode:: sql
-
-        Any X WHERE X is IN (FirstType, SecondType)
-
-
-.. _RQLInsertQuery:
-
-Insertion query
-~~~~~~~~~~~~~~~
-
-    `INSERT` <entity type> V1 (, <entity type> V2) \ * `:` <assignments>
-    [ `WHERE` <restriction>]
-
-:assignments:
-   list of relations to assign in the form `V1 relationship V2 | <static value>`
-
-The restriction can define variables used in assignments.
-
-Caution, if a restriction is specified, the insertion is done for
-*each line result returned by the restriction*.
-
-- *Insert a new person named 'foo'*
-
-  .. sourcecode:: sql
-
-        INSERT Person X: X name 'foo'
-
-- *Insert a new person named 'foo', another called 'nice' and a 'friend' relation
-  between them*
-
-  .. sourcecode:: sql
-
-        INSERT Person X, Person Y: X name 'foo', Y name 'nice', X friend Y
-
-- *Insert a new person named 'foo' and a 'friend' relation with an existing
-  person called 'nice'*
-
-  .. sourcecode:: sql
-
-        INSERT Person X: X name 'foo', X friend  Y WHERE Y name 'nice'
-
-.. _RQLSetQuery:
-
-Update and relation creation queries
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-    `SET` <assignements>
-    [ `WHERE` <restriction>]
-
-Caution, if a restriction is specified, the update is done *for
-each result line returned by the restriction*.
-
-- *Renaming of the person named 'foo' to 'bar' with the first name changed*
-
-  .. sourcecode:: sql
-
-        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'*
-
-  .. sourcecode:: sql
-
-        SET X know Y  WHERE X friend Y
-
-
-.. _RQLDeleteQuery:
-
-Deletion query
-~~~~~~~~~~~~~~
-
-    `DELETE` (<entity type> V) | (V1 relation v2 ),...
-    [ `WHERE` <restriction>]
-
-Caution, if a restriction is specified, the deletion is made *for
-each line result returned by the restriction*.
-
-- *Deletion of the person named 'foo'*
-
-  .. sourcecode:: sql
-
-        DELETE Person X WHERE X name 'foo'
-
-- *Removal of all relations of type 'friend' from the person named 'foo'*
-
-  .. sourcecode:: sql
-
-        DELETE X friend Y WHERE X is Person, X name 'foo'
-
-
-.. _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	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,225 +0,0 @@
-# -*- coding: utf-8 -*-
-# copyright 2003-2014 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 documentation build configuration file, created by
-# sphinx-quickstart on Fri Oct 31 09:10:36 2008.
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# The contents of this file are pickled, so don't put values in the namespace
-# that aren't pickleable (module imports are okay, they're removed automatically).
-#
-# All configuration values have a default value; values that are commented out
-# serve to show the default value.
-
-from os import path as osp
-
-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
-# absolute, like shown here.
-#sys.path.append(os.path.abspath('some/directory'))
-
-# General configuration
-# ---------------------
-
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = [
-  'sphinx.ext.autodoc', 
-  'sphinx.ext.viewcode',
-  'logilab.common.sphinx_ext',
-  ]
-
-autoclass_content = 'both'
-
-# Add any paths that contain templates here, relative to this directory.
-#templates_path = []
-
-# The suffix of source filenames.
-source_suffix = '.rst'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General substitutions.
-project = 'CubicWeb'
-copyright = '2001-2014, Logilab'
-
-# The default replacements for |version| and |release|, also used in various
-# other places throughout the built documents.
-#
-# The short X.Y version.
-version = '.'.join(str(n) for n in cw['numversion'][:2])
-# The full version, including alpha/beta/rc tags.
-release = cw['version']
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-#today = ''
-# Else, today_fmt is used as the format for a strftime call.
-today_fmt = '%B %d, %Y'
-
-# List of documents that shouldn't be included in the build.
-unused_docs = []
-
-# List of directories, relative to source directories, that shouldn't be searched
-# for source files.
-#exclude_dirs = []
-
-# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-#add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-#show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-
-# Options for HTML output
-# -----------------------
-
-# The style sheet to use for HTML and HTML Help pages. A file of that name
-# must exist either in Sphinx' static/ path, or in one of the custom paths
-# given in html_static_path.
-#html_style = 'sphinx-default.css'
-
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
-html_title = '%s %s' % (project, release)
-
-html_theme_path = ['_themes']
-html_theme = 'cubicweb'
-
-# A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (within the static path) to place at the top of
-# the sidebar.
-#html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['.static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-#html_additional_pages = {}
-
-# If false, no module index is generated.
-html_use_modindex = True
-
-# If false, no index is generated.
-#html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-#html_split_index = False
-
-# If true, the reST sources are included in the HTML build as _sources/<name>.
-#html_copy_source = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it.  The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-html_file_suffix = '.html'
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'Cubicwebdoc'
-
-
-# Options for LaTeX output
-# ------------------------
-
-# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
-
-# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, document class [howto/manual]).
-latex_documents = [
-  ('index', 'Cubicweb.tex', 'Cubicweb Documentation',
-   'Logilab', 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_use_modindex = True
-
-#aafig_format = dict(latex='pdf', html='svg', text=None)
-
-rst_epilog = """
-.. |cubicweb| replace:: *CubicWeb*
-.. |yams| replace:: *Yams*
-.. |rql| replace:: *RQL*
-"""
--- a/doc/book/en/devrepo/cubes/available-cubes.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-.. _AvailableCubes:
-
-Available cubes
----------------
-
-An instance is made of several basic cubes. In the set of available
-basic cubes we can find for example:
-
-Base entity types
-~~~~~~~~~~~~~~~~~
-* addressbook_: PhoneNumber and PostalAddress
-* card_: Card, generic documenting card
-* event_: Event (define events, display them in calendars)
-* file_: File (to allow users to upload and store binary or text files)
-* link_: Link (to collect links to web resources)
-* mailinglist_: MailingList (to reference a mailing-list and the URLs
-  for its archives and its admin interface)
-* person_: Person (easily mixed with addressbook)
-* task_: Task (something to be done between start and stop date)
-* zone_: Zone (to define places within larger places, for example a
-  city in a state in a country)
-
-
-Classification
-~~~~~~~~~~~~~~
-* folder_: Folder (to organize things by grouping them in folders)
-* keyword_: Keyword (to define classification schemes)
-* tag_: Tag (to tag anything)
-
-Other features
-~~~~~~~~~~~~~~
-* basket_: Basket (like a shopping cart)
-* blog_: a blogging system using Blog and BlogEntry entity types
-* comment_: system to attach comment threads to entities)
-* email_: archiving management for emails (`Email`, `Emailpart`,
-  `Emailthread`), trigger action in cubicweb through email
-
-
-
-
-
-.. _addressbook: http://www.cubicweb.org/project/cubicweb-addressbook
-.. _basket: http://www.cubicweb.org/project/cubicweb-basket
-.. _card: http://www.cubicweb.org/project/cubicweb-card
-.. _blog: http://www.cubicweb.org/project/cubicweb-blog
-.. _comment: http://www.cubicweb.org/project/cubicweb-comment
-.. _email: http://www.cubicweb.org/project/cubicweb-email
-.. _event: http://www.cubicweb.org/project/cubicweb-event
-.. _file: http://www.cubicweb.org/project/cubicweb-file
-.. _folder: http://www.cubicweb.org/project/cubicweb-folder
-.. _keyword: http://www.cubicweb.org/project/cubicweb-keyword
-.. _link: http://www.cubicweb.org/project/cubicweb-link
-.. _mailinglist: http://www.cubicweb.org/project/cubicweb-mailinglist
-.. _person: http://www.cubicweb.org/project/cubicweb-person
-.. _tag: http://www.cubicweb.org/project/cubicweb-tag
-.. _task: http://www.cubicweb.org/project/cubicweb-task
-.. _zone: http://www.cubicweb.org/project/cubicweb-zone
-
-To declare the use of a cube, once installed, add the name of the cube
-and its dependency relation in the `__depends_cubes__` dictionary
-defined in the file `__pkginfo__.py` of your own component.
-
-.. note::
-  The listed cubes above are available as debian-packages on `CubicWeb's forge`_.
-
-.. _`CubicWeb's forge`: http://www.cubicweb.org/project?vtitle=All%20cubicweb%20projects
--- a/doc/book/en/devrepo/cubes/cc-newcube.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-Creating a new cube from scratch
---------------------------------
-
-Let's start by creating the cube environment in which we will develop ::
-
-  cd ~/cubes
-  # use cubicweb-ctl to generate a template for the cube
-  # will ask some questions, most with nice default
-  cubicweb-ctl newcube mycube
-  # makes the cube source code managed by mercurial
-  cd mycube
-  hg init
-  hg add .
-  hg ci
-
-If all went well, you should see the cube you just created in the list
-returned by ``cubicweb-ctl list`` in the  *Available cubes* section.
-If not, please refer to :ref:`ConfigurationEnv`.
-
-To reuse an existing cube, add it to the list named
-``__depends_cubes__`` which is defined in :file:`__pkginfo__.py`.
-This variable is used for the instance packaging (dependencies handled
-by system utility tools such as APT) and to find used cubes when the
-database for the instance is created (import_erschema('MyCube') will
-not properly work otherwise).
-
-On a Unix system, the available cubes are usually stored in the
-directory :file:`/usr/share/cubicweb/cubes`. If you are using the
-cubicweb mercurial repository (:ref:`SourceInstallation`), the cubes
-are searched in the directory
-:file:`/path/to/cubicweb_toplevel/cubes`. In this configuration
-cubicweb itself ought to be located at
-:file:`/path/to/cubicweb_toplevel/cubicweb`.
-
-.. note::
-
-    Please note that if you do not wish to use default directory for your cubes
-    library, you should set the :envvar:`CW_CUBES_PATH` environment variable to
-    add extra directories where cubes will be search, and you'll then have to use
-    the option `--directory` to specify where you would like to place the source
-    code of your cube:
-
-    ``cubicweb-ctl newcube --directory=/path/to/cubes/library mycube``
-
-
-.. XXX resurrect once live-server is back
-.. Usage of :command:`cubicweb-ctl liveserver`
-.. -------------------------------------------
-
-.. To quickly test a new cube, you can also use the `liveserver` command for cubicweb-ctl
-.. which allows to create an instance in memory (using an SQLite database by
-.. default) and make it accessible through a web server ::
-
-..   cubicweb-ctl live-server mycube
-
-.. or by using an existing database (SQLite or Postgres)::
-
-..   cubicweb-ctl live-server -s myfile_sources mycube
--- a/doc/book/en/devrepo/cubes/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-Cubes
-=====
-
-This chapter describes how to define your own cubes and reuse already available cubes.
-
-.. toctree::
-   :maxdepth: 1
-
-   layout.rst
-   cc-newcube.rst
-   available-cubes.rst
--- a/doc/book/en/devrepo/cubes/layout.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-
-.. _foundationsCube:
-
-.. _cubelayout:
-
-Standard structure for a cube
------------------------------
-
-A cube is structured as follows:
-
-::
-
-  mycube/
-  |
-  |-- data/
-  |   |-- cubes.mycube.css
-  |   |-- cubes.mycube.js
-  |   `-- external_resources
-  |
-  |-- debian/
-  |   |-- changelog
-  |   |-- compat
-  |   |-- control
-  |   |-- copyright
-  |   |-- cubicweb-mycube.prerm
-  |   `-- rules
-  |
-  |-- entities.py
-  |
-  |-- i18n/
-  |   |-- en.po
-  |   |-- es.po
-  |   `-- fr.po
-  |
-  |-- __init__.py
-  |
-  |-- MANIFEST.in
-  |
-  |-- migration/
-  |   |-- postcreate.py
-  |   `-- precreate.py
-  |
-  |-- __pkginfo__.py
-  |
-  |-- schema.py
-  |
-  |-- setup.py
-  |
-  |-- site_cubicweb.py
-  |
-  |-- hooks.py
-  |
-  |-- test/
-  |   |-- data/
-  |   |   `-- bootstrap_cubes
-  |   |-- pytestconf.py
-  |   |-- realdb_test_mycube.py
-  |   `-- test_mycube.py
-  |
-  `-- views.py
-
-
-We can use subpackages instead of python modules for ``views.py``, ``entities.py``,
-``schema.py`` or ``hooks.py``. For example, we could have:
-
-::
-
-  mycube/
-  |
-  |-- entities.py
-  |-- hooks.py
-  `-- views/
-      |-- __init__.py
-      |-- forms.py
-      |-- primary.py
-      `-- widgets.py
-
-
-where :
-
-* ``schema`` contains the schema definition (server side only)
-* ``entities`` contains the entity definitions (server side and web interface)
-* ``hooks`` contains hooks and/or views notifications (server side only)
-* ``views`` contains the web interface components (web interface only)
-* ``test`` contains tests related to the cube (not installed)
-* ``i18n`` contains message catalogs for supported languages (server side and
-  web interface)
-* ``data`` contains data files for static content (images, css,
-  javascript code)...(web interface only)
-* ``migration`` contains initialization files for new instances (``postcreate.py``)
-  and a file containing dependencies of the component depending on the version
-  (``depends.map``)
-* ``debian`` contains all the files managing debian packaging (you will find
-  the usual files ``control``, ``rules``, ``changelog``... not installed)
-* file ``__pkginfo__.py`` provides component meta-data, especially the distribution
-  and the current version (server side and web interface) or sub-cubes used by
-  the cube.
-
-
-At least you should have the file ``__pkginfo__.py``.
-
-
-The :file:`__init__.py` and :file:`site_cubicweb.py` files
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX WRITEME
-
-The :file:`__pkginfo__.py` file
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-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`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX detail steps of instance creation
-
-
-External resources such as image, javascript and css files
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX naming convention external_resources file
-
-
-Out-of the box testing
-~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
-
-
-Packaging and distribution
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
-
--- a/doc/book/en/devrepo/dataimport.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-. -*- coding: utf-8 -*-
-
-.. _dataimport:
-
-Dataimport
-==========
-
-*CubicWeb* is designed to manipulate huge of amount of data, and provides helper functions to do so.
-These functions insert data within different levels of the *CubicWeb* API,
-allowing different speed/security tradeoffs. Those keeping all the *CubicWeb* hooks
-and security will be slower but the possible errors in insertion
-(bad data types, integrity error, ...) will be raised.
-
-These dataimport function are provided in the file `dataimport.py`.
-
-All the stores have the following API::
-
-    >>> store = ObjectStore()
-    >>> user = store.create_entity('CWUser', login=u'johndoe')
-    >>> group = store.create_entity('CWUser', name=u'unknown')
-    >>> store.relate(user.eid, 'in_group', group.eid)
-
-
-ObjectStore
------------
-
-This store keeps objects in memory for *faster* validation. It may be useful
-in development mode. However, as it will not enforce the constraints of the schema,
-it may miss some problems.
-
-
-
-RQLObjectStore
---------------
-
-This store works with an actual RQL repository, and it may be used in production mode.
-
-
-NoHookRQLObjectStore
---------------------
-
-This store works similarly to the *RQLObjectStore* but bypasses some *CubicWeb* hooks to be faster.
-
-
-SQLGenObjectStore
------------------
-
-This store relies on *COPY FROM*/execute many sql commands to directly push data using SQL commands
-rather than using the whole *CubicWeb* API. For now, **it only works with PostgresSQL** as it requires
-the *COPY FROM* command.
-
-The API is similar to the other stores, but **it requires a flush** after some imports to copy data
-in the database (these flushes may be multiples through the processes, or be done only once at the
-end if there is no memory issue)::
-
-    >>> store = SQLGenObjectStore(session)
-    >>> store.create_entity('Person', ...)
-    >>> store.flush()
--- a/doc/book/en/devrepo/datamodel/baseschema.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-.. _pre_defined_entity_types:
-
-Pre-defined entities in the library
------------------------------------
-
-The library defines a set of entity schemas that are required by the system
-or commonly used in *CubicWeb* instances.
-
-
-Entity types used to store the schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* _`CWEType`, entity type
-* _`CWRType`, relation type
-* _`CWRelation`, relation definition
-* _`CWAttribute`, attribute relation definition
-* _`CWConstraint`,  `CWConstraintType`, `RQLExpression`
-
-Entity types used to manage users and permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* _`CWUser`, system users
-* _`CWGroup`, users groups
-
-Entity types used to manage workflows
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* :ref:`Workflow <Workflow>`, workflow entity, linked to some entity types which may use this workflow
-* _`State`, workflow state
-* _`Transition`, workflow transition
-* _`TrInfo`, record of a transition trafic for an entity
-
-Other entity types
-~~~~~~~~~~~~~~~~~~
-* _`CWCache`, cache entities used to improve performances
-* _`CWProperty`, used to configure the instance
-
-* _`EmailAddress`, email address, used by the system to send notifications
-  to the users and also used by others optionnals schemas
-
-* _`Bookmark`, an entity type used to allow a user to customize his links within
-  the instance
-
-* _`ExternalUri`, used for semantic web site to indicate that an entity is the
-  same as another from an external site
--- a/doc/book/en/devrepo/datamodel/define-workflows.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Workflow:
-
-Defining a Workflow
-===================
-
-General
--------
-
-A workflow describes how certain entities have to evolve between different
-states. Hence we have a set of states, and a "transition graph", i.e. a set of
-possible transitions from one state to another state.
-
-We will define a simple workflow for a blog, with only the following two states:
-`submitted` and `published`. You may want to take a look at :ref:`TutosBase` if
-you want to quickly setup an instance running a blog.
-
-Setting up a workflow
----------------------
-
-We want to create a workflow to control the quality of the BlogEntry
-submitted on the instance. When a BlogEntry is created by a user
-its state should be `submitted`. To be visible to all, it has to
-be in the state `published`. To move it from `submitted` to `published`,
-we need a transition that we can call `approve_blogentry`.
-
-A BlogEntry state should not be modifiable by every user.
-So we have to define a group of users, `moderators`, and
-this group will have appropriate permissions to publish a BlogEntry.
-
-There are two ways to create a workflow: from the user interface, or
-by defining it in ``migration/postcreate.py``. This script is executed
-each time a new ``cubicweb-ctl db-init`` is done.  We strongly
-recommend to create the workflow in ``migration/postcreate.py`` and we
-will now show you how. Read `Two bits of warning`_ to understand why.
-
-The state of an entity is managed by the `in_state` attribute which
-can be added to your entity schema by inheriting from
-`cubicweb.schema.WorkflowableEntityType`.
-
-
-About our example of BlogEntry, we must have:
-
-.. sourcecode:: python
-
-  from cubicweb.schema import WorkflowableEntityType
-
-  class BlogEntry(WorkflowableEntityType):
-      ...
-
-
-Creating states, transitions and group permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The :mod:`postcreate` script is executed in a special environment,
-adding several |cubicweb| primitives that can be used.
-
-They are all defined in the :class:`ServerMigrationHelper` class.
-We will only discuss the methods we use to create a workflow in this example.
-
-A workflow is a collection of entities of type ``State`` and of type
-``Transition`` which are standard *CubicWeb* entity types.
-
-To define a workflow for BlogDemo, please add the following lines
-to ``migration/postcreate.py``:
-
-.. sourcecode:: python
-
-  _ = unicode
-
-  moderators = add_entity('CWGroup', name=u"moderators")
-
-This adds the `moderators` user group.
-
-.. sourcecode:: python
-
-  wf = add_workflow(u'blog publication workflow', 'BlogEntry')
-
-At first, instanciate a new workflow object with a gentle description
-and the concerned entity types (this one can be a tuple for multiple
-value).
-
-.. sourcecode:: python
-
-  submitted = wf.add_state(_('submitted'), initial=True)
-  published = wf.add_state(_('published'))
-
-This will create two entities of type ``State``, one with name
-'submitted', and the other with name 'published'.
-
-``add_state`` expects as first argument the name of the state you want
-to create and an optional argument to say if it is supposed to be the
-initial state of the entity type.
-
-.. sourcecode:: python
-
-  wf.add_transition(_('approve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
-
-This will create an entity of type ``Transition`` with name
-`approve_blogentry` which will be linked to the ``State`` entities
-created before.
-
-``add_transition`` expects
-
-  * as the first argument: the name of the transition
-  * then the list of states on which the transition can be triggered,
-  * the target state of the transition,
-  * and the permissions
-    (e.g. a list of user groups who can apply the transition; the user
-    has to belong to at least one of the listed group to perform the action).
-
-.. sourcecode:: python
-
-  checkpoint()
-
-.. note::
-  Do not forget to add the `_()` in front of all states and
-  transitions names while creating a workflow so that they will be
-  identified by the i18n catalog scripts.
-
-In addition to the user groups (one of which the user needs to belong
-to), we could have added a RQL condition.  In this case, the user can
-only perform the action if the two conditions are satisfied.
-
-If we use an RQL condition on a transition, we can use the following variables:
-
-* `X`, the entity on which we may pass the transition
-* `U`, the user executing that may pass the transition
-
-
-.. image:: ../../images/03-transitions-view_en.png
-
-You can notice that in the action box of a BlogEntry, the state is now
-listed as well as the possible transitions for the current state
-defined by the workflow.
-
-The transitions will only be displayed for users having the right permissions.
-In our example, the transition `approve_blogentry` will only be displayed
-for the users belonging to the group `moderators` or `managers`.
-
-
-Two bits of warning
-~~~~~~~~~~~~~~~~~~~
-
-We could perfectly use the administration interface to do these
-operations. It is a convenient thing to do at times (when doing
-development, to quick-check things). But it is not recommended beyond
-that because it is a bit complicated to do it right and it will be
-only local to your instance (or, said a bit differently, such a
-workflow only exists in an instance database). Furthermore, you cannot
-write unit tests against deployed instances, and experience shows it
-is mandatory to have tests for any mildly complicated workflow
-setup.
-
-Indeed, if you create the states and transitions through the user
-interface, next time you initialize the database you will have to
-re-create all the workflow entities. The user interface should only be
-a reference for you to view the states and transitions, but is not the
-appropriate interface to define your application workflow.
--- a/doc/book/en/devrepo/datamodel/definition.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,912 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _datamodel_definition:
-
-Yams *schema*
--------------
-
-The **schema** is the core piece of a *CubicWeb* instance as it
-defines and handles the data model. It is based on entity types that
-are either already defined in `Yams`_ and the *CubicWeb* standard
-library; or more specific types defined in cubes. The schema for a
-cube is defined in a `schema` python module or package.
-
-.. _`Yams`: http://www.logilab.org/project/yams
-
-.. _datamodel_overview:
-
-Overview
-~~~~~~~~
-
-The core idea of the yams schema is not far from the classical
-`Entity-relationship`_ model. But while an E/R model (or `logical
-model`) traditionally has to be manually translated to a lower-level
-data description language (such as the SQL `create table`
-sublanguage), also often described as the `physical model`, no such
-step is required with |yams| and |cubicweb|.
-
-.. _`Entity-relationship`: http://en.wikipedia.org/wiki/Entity-relationship_model
-
-This is because in addition to high-level, logical |yams| models, one
-uses the |rql| data manipulation language to query, insert, update and
-delete data. |rql| abstracts as much of the underlying SQL database as
-a |yams| schema abstracts from the physical layout. The vagaries of
-SQL are avoided.
-
-As a bonus point, such abstraction make it quite comfortable to build
-or use different backends to which |rql| queries apply.
-
-So, as in the E/R formalism, the building blocks are ``entities``
-(:ref:`EntityType`), ``relationships`` (:ref:`RelationType`,
-:ref:`RelationDefinition`) and ``attributes`` (handled like relation
-with |yams|).
-
-Let us detail a little the divergences between E/R and |yams|:
-
-* all relationship are binary which means that to represent a
-  non-binary relationship, one has to use an entity,
-* relationships do not support attributes (yet, see:
-  http://www.cubicweb.org/ticket/341318), hence the need to reify it
-  as an entity if need arises,
-* all entities have an `eid` attribute (an integer) that is its
-  primary key (but it is possible to declare uniqueness on other
-  attributes)
-
-Also |yams| supports the notions of:
-
-* entity inheritance (quite experimental yet, and completely
-  undocumented),
-* relation type: that is, relationships can be established over a set
-  of couple of entity types (henre the distinction made between
-  `RelationType` and `RelationDefinition` below)
-
-Finally |yams| has a few concepts of its own:
-
-* relationships being oriented and binary, we call the left hand
-  entity type the `subject` and the right hand entity type the
-  `object`
-
-.. note::
-
-   The |yams| schema is available at run time through the .schema
-   attribute of the `vregistry`.  It's an instance of
-   :class:`cubicweb.schema.Schema`, which extends
-   :class:`yams.schema.Schema`.
-
-.. _EntityType:
-
-Entity type
-~~~~~~~~~~~
-
-An entity type is an instance of :class:`yams.schema.EntitySchema`. Each entity type has
-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``, ``BigInt``, ``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
-
-The ``__unique_together__`` class attribute is a list of tuples of names of
-attributes or inlined relations.  For each tuple, CubicWeb ensures the unicity
-of the combination.  For example:
-
-.. sourcecode:: python
-
-  class State(EntityType):
-      __unique_together__ = [('name', 'state_of')]
-
-      name = String(required=True)
-      state_of = SubjectRelation('Workflow', cardinality='1*',
-                                 composite='object', inlined=True)
-
-
-You can find more base entity types in
-:ref:`pre_defined_entity_types`.
-
-.. XXX yams inheritance
-
-.. _RelationType:
-
-Relation type
-~~~~~~~~~~~~~
-
-A relation type is an instance of
-:class:`yams.schema.RelationSchema`. A relation type is simply a
-semantic definition of a kind of relationship that may occur in an
-application.
-
-It may be referenced by zero, one or more relation definitions.
-
-It is important to choose a good name, at least to avoid conflicts
-with some semantically different relation defined in other cubes
-(since there's only a shared name space for these names).
-
-A relation type holds the following properties (which are hence shared
-between all relation definitions of that type):
-
-* `inlined`: boolean handling the physical optimization for archiving
-  the relation in the subject entity table, instead of creating a specific
-  table for the relation. This applies to relations where cardinality
-  of subject->relation->object is 0..1 (`?`) or 1..1 (`1`) for *all* its relation
-  definitions.
-
-* `symmetric`: boolean indicating that the relation is symmetrical, which
-  means that `X relation Y` implies `Y relation X`.
-
-.. _RelationDefinition:
-
-Relation definition
-~~~~~~~~~~~~~~~~~~~
-
-A relation definition is an instance of
-:class:`yams.schema.RelationDefinition`. It is a complete triplet
-"<subject entity type> <relation type> <object entity type>".
-
-When creating a new instance of that class, the corresponding
-:class:`RelationType` instance is created on the fly if necessary.
-
-Properties
-``````````
-
-The available properties for relation definitions are enumerated
-here. There are several kind of properties, as some relation
-definitions are actually attribute definitions, and other are not.
-
-Some properties may be completely optional, other may have a default
-value.
-
-Common properties for attributes and relations:
-
-* `description`: an unicode string describing an attribute or a
-  relation. By default this string will be used in the editing form of
-  the entity, which means that it is supposed to help the end-user and
-  should be flagged by the function `_` to be properly
-  internationalized.
-
-* `constraints`: a list of conditions/constraints that the relation has to
-  satisfy (c.f. `Constraints`_)
-
-* `cardinality`: a two character string specifying the cardinality of
-  the relation. The first character defines the cardinality of the
-  relation on the subject, and the second on the object. When a
-  relation can have multiple subjects or objects, the cardinality
-  applies to all, not on a one-to-one basis (so it must be
-  consistent...). Default value is '**'. The possible values are
-  inspired from regular expression syntax:
-
-    * `1`: 1..1
-    * `?`: 0..1
-    * `+`: 1..n
-    * `*`: 0..n
-
-Attributes properties:
-
-* `unique`: boolean indicating if the value of the attribute has to be
-  unique or not within all entities of the same type (false by
-  default)
-
-* `indexed`: boolean indicating if an index needs to be created for
-  this attribute in the database (false by default). This is useful
-  only if you know that you will have to run numerous searches on the
-  value of this attribute.
-
-* `default`: default value of the attribute. In case of date types, the values
-  which could be used correspond to the RQL keywords `TODAY` and `NOW`.
-
-* `metadata`: Is also accepted as an argument of the attribute contructor. It is
-  not really an attribute property. see `Metadata`_ for details.
-
-Properties for `String` attributes:
-
-* `fulltextindexed`: boolean indicating if the attribute is part of
-  the full text index (false by default) (*applicable on the type
-  `Byte` as well*)
-
-* `internationalizable`: boolean indicating if the value of the
-  attribute is internationalizable (false by default)
-
-Relation properties:
-
-* `composite`: string indicating that the subject (composite ==
-  'subject') is composed of the objects of the relations. For the
-  opposite case (when the object is composed of the subjects of the
-  relation), we just set 'object' as value. The composition implies
-  that when the relation is deleted (so when the composite is deleted,
-  at least), the composed are also deleted.
-
-* `fulltext_container`: string indicating if the value if the full
-  text indexation of the entity on one end of the relation should be
-  used to find the entity on the other end. The possible values are
-  'subject' or 'object'. For instance the use_email relation has that
-  property set to 'subject', since when performing a full text search
-  people want to find the entity using an email address, and not the
-  entity representing the email address.
-
-Constraints
-```````````
-
-By default, the available constraint types are:
-
-General Constraints
-......................
-
-* `SizeConstraint`: allows to specify a minimum and/or maximum size on
-  string (generic case of `maxsize`)
-
-* `BoundaryConstraint`: allows to specify a minimum and/or maximum value
-  on numeric types and date
-
-.. sourcecode:: python
-
-   from yams.constraints import BoundaryConstraint, TODAY, NOW, Attribute
-
-   class DatedEntity(EntityType):
-      start = Date(constraints=[BoundaryConstraint('>=', TODAY())])
-      end = Date(constraints=[BoundaryConstraint('>=', Attribute('start'))])
-
-   class Before(EntityType);
-      last_time = DateTime(constraints=[BoundaryConstraint('<=', NOW())])
-
-* `IntervalBoundConstraint`: allows to specify an interval with
-  included values
-
-.. sourcecode:: python
-
-     class Node(EntityType):
-         latitude = Float(constraints=[IntervalBoundConstraint(-90, +90)])
-
-* `UniqueConstraint`: identical to "unique=True"
-
-* `StaticVocabularyConstraint`: identical to "vocabulary=(...)"
-
-Constraints can be dependent on a fixed value (90, Date(2015,3,23)) or a variable.
-In this second case, yams can handle :
-
-* `Attribute`: compare to the value of another attribute.
-* `TODAY`: compare to the current Date.
-* `NOW`: compare to the current Datetime.
-
-RQL Based Constraints
-......................
-
-RQL based constraints may take three arguments. The first one is the ``WHERE``
-clause of a RQL query used by the constraint. The second argument ``mainvars``
-is the ``Any`` clause of the query. By default this include `S` reserved for the
-subject of the relation and `O` for the object. Additional variables could be
-specified using ``mainvars``. The argument expects a single string with all
-variable's name separated by spaces. The last one, ``msg``, is the error message
-displayed when the constraint fails. As RQLVocabularyConstraint never fails the
-third argument is not available.
-
-* `RQLConstraint`: allows to specify a RQL query that has to be satisfied
-  by the subject and/or the object of relation. In this query the variables
-  `S` and `O` are reserved for the relation subject and object entities.
-
-* `RQLVocabularyConstraint`: similar to the previous type of constraint except
-  that it does not express a "strong" constraint, which means it is only used to
-  restrict the values listed in the drop-down menu of editing form, but it does
-  not prevent another entity to be selected.
-
-* `RQLUniqueConstraint`: allows to the specify a RQL query that ensure that an
-  attribute is unique in a specific context. The Query must **never** return more
-  than a single result to be satisfied. In this query the variables `S` is
-  reserved for the relation subject entity. The other variables should be
-  specified with the second constructor argument (mainvars). This constraint type
-  should be used when __unique_together__ doesn't fit.
-
-.. XXX note about how to add new constraint
-
-.. _securitymodel:
-
-The security model
-~~~~~~~~~~~~~~~~~~
-
-The security model of `CubicWeb` is based on `Access Control List`.
-The main principles are:
-
-* users and groups of users
-* a user belongs to at least one group of user
-* permissions (`read`, `update`, `create`, `delete`)
-* permissions are assigned to groups (and not to users)
-
-For *CubicWeb* in particular:
-
-* we associate rights at the entities/relations schema level
-
-* the default groups are: `managers`, `users` and `guests`
-
-* 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
-
-Setting permissions is done with the class attribute `__permissions__`
-of entity types and relation definitions. The value of this attribute
-is a dictionary where the keys are the access types (action), and the
-values are the authorized groups or rql expressions.
-
-For an entity type, the possible actions are `read`, `add`, `update` and
-`delete`.
-
-For a relation, the possible actions are `read`, `add`, and `delete`.
-
-For an attribute, the possible actions are `read`, `add` and `update`,
-and they are a refinement of an entity type permission.
-
-.. note::
-
-   By default, the permissions of an entity type attributes are
-   equivalent to the permissions of the entity type itself.
-
-   It is possible to provide custom attribute permissions which are
-   stronger than, or are more lenient than the entity type
-   permissions.
-
-   In a situation where all attributes were given custom permissions,
-   the entity type permissions would not be checked, except for the
-   `delete` action.
-
-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.
-
-Default permissions
-```````````````````
-
-The default permissions for ``EntityType`` are:
-
-.. sourcecode:: python
-
-   __permissions__ = {
-        'read': ('managers', 'users', 'guests',),
-        'update': ('managers', 'owners',),
-        'delete': ('managers', 'owners'),
-        'add': ('managers', 'users',)
-        }
-
-The default permissions for relations are:
-
-.. sourcecode:: python
-
-   __permissions__ = {'read': ('managers', 'users', 'guests',),
-                    'delete': ('managers', 'users'),
-                    'add': ('managers', 'users',)}
-
-The default permissions for attributes are:
-
-.. sourcecode:: python
-
-   __permissions__ = {'read': ('managers', 'users', 'guests',),
-                      'add': ('managers', ERQLExpression('U has_add_permission X'),
-                      'update': ('managers', ERQLExpression('U has_update_permission X')),}
-
-.. note::
-
-   The default permissions for attributes are not syntactically
-   equivalent to the default permissions of the entity types, but the
-   rql expressions work by delegating to the entity type permissions.
-
-
-The standard user groups
-````````````````````````
-
-* `guests`
-
-* `users`
-
-* `managers`
-
-* `owners`: virtual group corresponding to the entity's owner.
-  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).
-
-
-Use of RQL expression for write permissions
-```````````````````````````````````````````
-
-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.
-
-To use RQL expression in entity type permission:
-
-* 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
-
-For RQL expressions on a relation type, the principles are the same except for
-the following:
-
-* you have to use the class :class:`~cubicweb.schema.RRQLExpression` instead of
-  :class:`~cubicweb.schema.ERQLExpression`
-
-* 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.
-
-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.
-
-
-.. sourcecode:: python
-
-  class my_relation(RelationDefinition):
-    __permissions__ = {'read': ('managers', 'users'),
-                       'add': ('managers', RRQLExpression('U has_update_permission S')),
-                       'delete': ('managers', RRQLExpression('U has_update_permission S'))
-		       }
-
-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, 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
-````````````````````````````````````````
-
-The principles are the same but with the following restrictions:
-
-* you can not use rql expression for the `read` permission of relations and
-  attributes,
-
-* 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
-
-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:
-
-
-Derived attributes and relations
---------------------------------
-
-.. note:: **TODO** Check organisation of the whole chapter of the documentation
-
-Cubicweb offers the possibility to *query* data using so called
-*computed* relations and attributes. Those are *seen* by RQL requests
-as normal attributes and relations but are actually derived from other
-attributes and relations. In a first section we'll informally review
-two typical use cases. Then we see how to use computed attributes and
-relations in your schema. Last we will consider various significant
-aspects of their implementation and the impact on their usage.
-
-Motivating use cases
-~~~~~~~~~~~~~~~~~~~~
-
-Computed (or reified) relations
-```````````````````````````````
-
-It often arises that one must represent a ternary relation, or a
-family of relations. For example, in the context of an exhibition
-catalog you might want to link all *contributors* to the *work* they
-contributed to, but this contribution can be as *illustrator*,
-*author*, *performer*, ...
-
-The classical way to describe this kind of information within an
-entity-relationship schema is to *reify* the relation, that is turn
-the relation into a entity. In our example the schema will have a
-*Contribution* entity type used to represent the family of the
-contribution relations.
-
-
-.. sourcecode:: python
-
-    class ArtWork(EntityType):
-        name = String()
-        ...
-
-    class Person(EntityType):
-        name = String()
-        ...
-
-    class Contribution(EntityType):
-        contributor = SubjectRelation('Person', cardinality='1*', inlined=True)
-        manifestation = SubjectRelation('ArtWork')
-        role = SubjectRelation('Role')
-
-    class Role(EntityType):
-        name = String()
-
-But then, in order to query the illustrator(s) ``I`` of a work ``W``,
-one has to write::
-
-    Any I, W WHERE C is Contribution, C contributor I, C manifestation W,
-                   C role R, R name 'illustrator'
-
-whereas we would like to be able to simply write::
-
-    Any I, W WHERE I illustrator_of W
-
-This is precisely what the computed relations allow.
-
-
-Computed (or synthesized) attribute
-```````````````````````````````````
-
-Assuming a trivial schema for describing employees in companies, one
-can be interested in the total of salaries payed by a company for
-all its employees. One has to write::
-
-    Any C, SUM(SA) GROUPBY S WHERE E works_for C, E salary SA
-
-whereas it would be most convenient to simply write::
-
-    Any C, TS WHERE C total_salary TS
-
-And this is again what computed attributes provide.
-
-
-Using computed attributes and relations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Computed (or reified) relations
-```````````````````````````````
-
-In the above case we would define the *computed relation*
-``illustrator_of`` in the schema by:
-
-.. sourcecode:: python
-
-    class illustrator_of(ComputedRelation):
-        rule  = ('C is Contribution, C contributor S, C manifestation O,'
-                 'C role R, R name "illustrator"')
-
-You will note that:
-
-* the ``S`` and ``O`` RQL variables implicitly identify the subject and
-  object of the defined computed relation, akin to what happens in
-  RRQLExpression
-* the possible subject and object entity types are inferred from the rule;
-* computed relation definitions always have empty *add* and *delete* permissions
-* *read* permissions can be defined, permissions from the relations used in the
-  rewrite rule **are not considered** ;
-* nothing else may be defined on the `ComputedRelation` subclass beside
-  description, permissions and rule (e.g. no cardinality, composite, etc.,).
-  `BadSchemaDefinition` is raised on attempt to specify other attributes;
-* computed relations can not be used in 'SET' and 'DELETE' rql queries
-  (`BadQuery` exception raised).
-
-
-NB: The fact that the *add* and *delete* permissions are *empty* even
-for managers is expected to make the automatic UI not attempt to edit
-them.
-
-Computed (or synthesized) attributes
-````````````````````````````````````
-
-In the above case we would define the *computed attribute*
-``total_salary`` on the ``Company`` entity type in the schema by:
-
-.. sourcecode:: python
-
-    class Company(EntityType):
-        name = String()
-        total_salary = Int(formula='Any SUM(SA) GROUPBY E WHERE P works_for X, E salary SA')
-
-* the ``X`` RQL variable implicitly identifies the entity holding the
-  computed attribute, akin to what happens in ERQLExpression;
-* the type inferred from the formula is checked against the declared type, and
-  `BadSchemaDefinition` is raised if they don't match;
-* the computed attributes always have empty *update* permissions
-* `BadSchemaDefinition` is raised on attempt to set 'update' permissions;
-* 'read' permissions can be defined, permissions regarding the formula
-  **are not considered**;
-* other attribute's property (inlined, ...) can be defined as for normal attributes;
-* Similarly to computed relation, computed attribute can't be used in 'SET' and
-  'DELETE' rql queries (`BadQuery` exception raised).
-
-
-API and implementation
-~~~~~~~~~~~~~~~~~~~~~~
-
-Representation in the data backend
-``````````````````````````````````
-
-Computed relations have no direct representation at the SQL table
-level.  Instead, each time a query is issued the query is rewritten to
-replace the computed relation by its equivalent definition and the
-resulting rewritten query is performed in the usual way.
-
-On the contrary, computed attributes are represented as a column in the
-table for their host entity type, just like normal attributes. Their
-value is kept up-to-date with respect to their defintion by a system
-of hooks (also called triggers in most RDBMS) which recomputes them
-when the relations and attributes they depend on are modified.
-
-Yams API
-````````
-
-When accessing the schema through the *yams API* (not when defining a
-schema in a ``schema.py`` file) the computed attributes and relations
-are represented as follows:
-
-relations
-    The ``yams.RelationSchema`` class has a new ``rule`` attribute
-    holding the rule as a string. If this attribute is set all others
-    must not be set.
-attributes
-    A new property ``formula`` is added on class
-    ``yams.RelationDefinitionSchema`` alomng with a new keyword
-    argument ``formula`` on the initializer.
-
-Migration
-`````````
-
-The migrations are to be handled as summarized in the array below.
-
-+------------+---------------------------------------------------+---------------------------------------+
-|            | Computed rtype                                    | Computed attribute                    |
-+============+===================================================+=======================================+
-| add        | * add_relation_type                               | * add_attribute                       |
-|            | * add_relation_definition should trigger an error | * add_relation_definition             |
-+------------+---------------------------------------------------+---------------------------------------+
-| modify     | * sync_schema_prop_perms:                         | * sync_schema_prop_perms:             |
-|            |   checks the rule is                              |                                       |
-| (rule or   |   synchronized with the database                  |   - empty the cache,                  |
-| formula)   |                                                   |   - check formula,                    |
-|            |                                                   |   - make sure all the values get      |
-|            |                                                   |     updated                           |
-+------------+---------------------------------------------------+---------------------------------------+
-| del        | * drop_relation_type                              | * drop_attribute                      |
-|            | * drop_relation_definition should trigger an error| * drop_relation_definition            |
-+------------+---------------------------------------------------+---------------------------------------+
-
-
-Defining your schema using yams
--------------------------------
-
-Entity type definition
-~~~~~~~~~~~~~~~~~~~~~~
-
-An entity type is defined by a Python class which inherits from
-:class:`yams.buildobjs.EntityType`.  The class definition contains the
-description of attributes and relations for the defined entity type.
-The class name corresponds to the entity type name. It is expected to
-be defined in the module ``mycube.schema``.
-
-:Note on schema definition:
-
- The code in ``mycube.schema`` is not meant to be executed. The class
- EntityType mentioned above is different from the EntitySchema class
- described in the previous chapter. EntityType is a helper class to
- make Entity definition easier. Yams will process EntityType classes
- and create EntitySchema instances from these class definitions. Similar
- manipulation happen for relations.
-
-When defining a schema using python files, you may use the following shortcuts:
-
-- `required`: boolean indicating if the attribute is required, ed subject cardinality is '1'
-
-- `vocabulary`: specify static possible values of an attribute
-
-- `maxsize`: integer providing the maximum size of a string (no limit by default)
-
-For example:
-
-.. sourcecode:: python
-
-  class Person(EntityType):
-    """A person with the properties and the relations necessary for my
-    application"""
-
-    last_name = String(required=True, fulltextindexed=True)
-    first_name = String(required=True, fulltextindexed=True)
-    title = String(vocabulary=('Mr', 'Mrs', 'Miss'))
-    date_of_birth = Date()
-    works_for = SubjectRelation('Company', cardinality='?*')
-
-
-The entity described above defines three attributes of type String,
-last_name, first_name and title, an attribute of type Date for the date of
-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
- usage is to use ``CamelCase`` names.
-
- Attribute and relation names must start with a lowercase letter. The
- common usage is to use ``underscore_separated_words``. Attribute and
- relation names starting with a single underscore are permitted, to
- denote a somewhat "protected" or "private" attribute.
-
- In any case, identifiers starting with "CW" or "cw" are reserved for
- internal use by the framework.
-
- .. _Metadata:
-
- Some attribute using the name of another attribute as prefix are considered
- metadata.  For example, if an EntityType have both a ``data`` and
- ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata
- of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch
- metadata related to an attribute. There are only three valid metadata names:
- ``format``, ``encoding`` and ``name``.
-
-
-The name of the Python attribute corresponds to the name of the attribute
-or the relation in *CubicWeb* application.
-
-An attribute is defined in the schema as follows::
-
-    attr_name = AttrType(*properties, metadata={})
-
-where
-
-* `AttrType`: is one of the type listed in EntityType_,
-
-* `properties`: is a list of the attribute needs to satisfy (see `Properties`_
-  for more details),
-
-* `metadata`: is a dictionary of meta attributes related to ``attr_name``.
-  Dictionary keys are the name of the meta attribute. Dictionary values
-  attributes objects (like the content of ``AttrType``). For each entry of the
-  metadata dictionary a ``<attr_name>_<key> = <value>`` attribute is
-  automaticaly added to the EntityType.  see `Metadata`_ section for details
-  about valid key.
-
-
- ---
-
-While building your schema
-
-* it is possible to use the attribute `meta` to flag an entity type as a `meta`
-  (e.g. used to describe/categorize other entities)
-
-.. XXX the paragraph below needs clarification and / or moving out in
-.. another place
-
-*Note*: if you end up with an `if` in the definition of your entity, this probably
-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
-
-
-Definition of relations
-~~~~~~~~~~~~~~~~~~~~~~~
-
-.. 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
-a description of the properties of this type of relation, and could as well
-contain a string for the subject and a string for the object. This allows to create
-new definition of associated relations, (so that the class can have the
-definition properties from the relation) for example ::
-
-  class locked_by(RelationType):
-    """relation on all entities indicating that they are locked"""
-    inlined = True
-    cardinality = '?*'
-    subject = '*'
-    object = 'CWUser'
-
-If provided, the `subject` and `object` attributes denote the subject
-and object of the various relation definitions related to the relation
-type. Allowed values for these attributes are:
-
-* a string corresponding to an entity type
-* a tuple of string corresponding to multiple entity types
-* 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`
-attribute in the EntityType class. The first argument of `SubjectRelation` gives
-the entity type for the object of the relation.
-
-:Naming convention:
-
- Although this way of defining relations uses a Python class, the
- naming convention defined earlier prevails over the PEP8 conventions
- used in the framework: relation type class names use
- ``underscore_separated_words``.
-
-:Historical note:
-
-   It has been historically possible to use `ObjectRelation` which
-   defines a relation in the opposite direction. This feature is
-   deprecated and therefore should not be used in newly written code.
-
-:Future deprecation note:
-
-  In an even more remote future, it is quite possible that the
-  SubjectRelation shortcut will become deprecated, in favor of the
-  RelationType declaration which offers some advantages in the context
-  of reusable cubes.
-
-
-
-
-Handling schema changes
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Also, it should be clear that to properly handle data migration, an
-instance's schema is stored in the database, so the python schema file
-used to defined it is only read when the instance is created or
-upgraded.
-
-.. XXX complete me
--- a/doc/book/en/devrepo/datamodel/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-Data model
-==========
-
-This chapter describes how you define a schema and how to make it evolves as the time goes.
-
-.. toctree::
-   :maxdepth: 1
-
-   definition
-   metadata
-   baseschema
-   define-workflows
--- a/doc/book/en/devrepo/datamodel/metadata.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-
-Metadata
---------
-
-.. index::
-   schema: meta-data;
-   schema: eid; creation_date; modification_data; cwuri
-   schema: created_by; owned_by; is; is_instance;
-
-Each entity type in |cubicweb| has at least the following meta-data attributes and relations:
-
-`eid`
-  entity's identifier which is unique in an instance. We usually call this identifier `eid` for historical reason.
-
-`creation_date`
-  Date and time of the creation of the entity.
-
-`modification_date`
-  Date and time of the latest modification of an entity.
-
-`cwuri`
-  Reference URL of the entity, which is not expected to change.
-
-`created_by`
-  Relation to the :ref:`users <CWUser>` who has created the entity
-
-`owned_by`
-  Relation to :ref:`users <CWUser>` whom the entity belongs; usually the creator but not
-  necessary, and it could have multiple owners notably for permission control
-
-`is`
-  Relation to the :ref:`entity type <CWEType>` of which type the entity is.
-
-`is_instance`
-  Relation to the :ref:`entity types <CWEType>` of which type the
-  entity is an instance of.
-
--- a/doc/book/en/devrepo/devcore/dbapi.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,133 +0,0 @@
-.. _dbapi:
-
-Python/RQL API
-~~~~~~~~~~~~~~
-
-The Python API developped to interface with RQL is inspired from the standard db-api,
-with a Connection object having the methods cursor, rollback and commit essentially.
-The most important method is the `execute` method of a cursor.
-
-.. sourcecode:: python
-
-   execute(rqlstring, args=None, build_descr=True)
-
-:rqlstring: the RQL query to execute (unicode)
-:args: if the query contains substitutions, a dictionary containing the values to use
-
-The `Connection` object owns the methods `commit` and `rollback`. You
-*should never need to use them* during the development of the web
-interface based on the *CubicWeb* framework as it determines the end
-of the transaction depending on the query execution success. They are
-however useful in other contexts such as tests or custom controllers.
-
-.. note::
-
-  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.
-
-.. note::
-
-   A :exc:`ValidationError` has a `entity` attribute. In CubicWeb,
-   this atttribute is set to the entity's eid (not a reference to the
-   entity itself).
-
-Executing RQL queries from a view or a hook
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-When you're within code of the web interface, the db-api like connexion is
-handled by the request object. You should not have to access it directly, but
-use the `execute` method directly available on the request, eg:
-
-.. sourcecode:: python
-
-   rset = self._cw.execute(rqlstring, kwargs)
-
-Similarly, on the server side (eg in hooks), there is no db-api connexion (since
-you're directly inside the data-server), so you'll have to use the execute method
-of the session object.
-
-
-Proper usage of `.execute`
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Let's say you want to get T which is in configuration C, this translates to:
-
-.. sourcecode:: python
-
-   self._cw.execute('Any T WHERE T in_conf C, C eid %s' % entity.eid)
-
-But it must be written in a syntax that will benefit from the use
-of a cache on the RQL server side:
-
-.. sourcecode:: python
-
-   self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid})
-
-The syntax tree is built once for the "generic" RQL and can be re-used
-with a number of different eids. There rql IN operator is an exception
-to this rule.
-
-.. sourcecode:: python
-
-   self._cw.execute('Any T WHERE T in_conf C, C name IN (%s)'
-                    % ','.join(['foo', 'bar']))
-
-Alternativelly, some of the common data related to an entity can be
-obtained from the `entity.related()` method (which is used under the
-hood by the orm when you use attribute access notation on an entity to
-get a relation. The initial request would then be translated to:
-
-.. sourcecode:: python
-
-   entity.related('in_conf', 'object')
-
-Additionnaly this benefits from the fetch_attrs policy (see
-:ref:`FetchAttrs`) eventually defined on the class element, which says
-which attributes must be also loaded when the entity is loaded through
-the orm.
-
-
-.. _resultset:
-
-The `ResultSet` API
-~~~~~~~~~~~~~~~~~~~
-
-ResultSet instances are a very commonly manipulated object. They have
-a rich API as seen below, but we would like to highlight a bunch of
-methods that are quite useful in day-to-day practice:
-
-* `__str__()` (applied by `print`) gives a very useful overview of both
-  the underlying RQL expression and the data inside; unavoidable for
-  debugging purposes
-
-* `printable_rql()` produces back a well formed RQL expression as a
-  string; it is very useful to build views
-
-* `entities()` returns a generator on all entities of the result set
-
-* `get_entity(row, col)` gets the entity at row, col coordinates; one
-  of the most used result set method
-
-.. autoclass:: cubicweb.rset.ResultSet
-   :members:
-
-
-The `Cursor` and `Connection` API
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The whole cursor API is developped below.
-
-.. note::
-
-  In practice you'll usually use the `.execute` method on the _cw object of
-  appobjects. Usage of other methods is quite rare.
-
-.. autoclass:: cubicweb.dbapi.Cursor
-   :members:
-
-.. autoclass:: cubicweb.dbapi.Connection
-   :members:
--- a/doc/book/en/devrepo/devcore/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-Core APIs
-=========
-
-.. toctree::
-   :maxdepth: 1
-
-   dbapi.rst
-   reqbase.rst
-
--- a/doc/book/en/devrepo/devcore/reqbase.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-Request and ResultSet methods
------------------------------
-
-Those are methods you'll find on both request objects and on
-repository session.
-
-Request methods
-~~~~~~~~~~~~~~~
-
-`URL handling`:
-
-* `build_url(*args, **kwargs)`, returns an absolute URL based on the
-  given arguments. The *controller* supposed to handle the response,
-  can be specified through the first positional parameter (the
-  connection is theoretically done automatically :).
-
-`Data formatting`:
-
-* `format_date(date, date_format=None, time=False)` returns a string for a
-  date time according to instance's configuration
-
-* `format_time(time)` returns a string for a date time according to
-  instance's configuration
-
-`And more...`:
-
-* `tal_render(template, variables)`, renders a precompiled page template with
-  variables in the given dictionary as context
-
-
-Result set methods
-~~~~~~~~~~~~~~~~~~
-
-* `get_entity(row, col)`, returns the entity corresponding to the data position
-  in the *result set*
-
-* `complete_entity(row, col, skip_bytes=True)`, is equivalent to `get_entity` but
-  also call the method `complete()` on the entity before returning it
-
-
--- a/doc/book/en/devrepo/entityclasses/adapters.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-.. _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.
-
-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.predicates.implements` selector. You should instead use
-   :class:`cubicweb.predicates.is_instance` when you want to select on an entity
-   type, or :class:`cubicweb.predicates.adaptable` when you want to select on a
-   service.
-
-
-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.predicates 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.predicates 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	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,177 +0,0 @@
-How to use entities objects and adapters
-----------------------------------------
-
-The previous chapters detailed the classes and methods available to
-the developer at the so-called `ORM`_ level. However they say little
-about the common patterns of usage of these objects.
-
-.. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
-
-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
-same role as database triggers, but in a way that is independent of
-the actual data sources).
-
-So a lot of an application's business rules will be written in Hooks
-(or Operations).
-
-On the web side, views also typically operate using entity
-objects. Obvious entity methods for use in views are the Dublin Core
-methods like ``dc_title``. For separation of concerns reasons, one
-should ensure no ui logic pervades the entities level, and also no
-business logic should creep into the views.
-
-In the duration of a transaction, entities objects can be instantiated
-many times, in views and hooks, even for the same database entity. For
-instance, in a classic CubicWeb deployment setup, the repository and
-the web front-end are separated process communicating over the
-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``,
-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.cw_set(x=42)`` notation or a plain
-RQL ``SET`` expression.
-
-In views, it would be preferable to encapsulate the necessary logic in
-a method of an adapter for the concerned entity class(es). But of
-course, this advice is also reasonable 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 objects adapters: it's where an
-important part of the application logic lies (the other part being
-located in the Hook/Operations).
-
-Anatomy of an entity class
---------------------------
-
-We can look now at a real life example coming from the `tracker`_
-cube. Let us begin to study the ``entities/project.py`` content.
-
-.. sourcecode:: python
-
-    from cubicweb.entities.adapters import ITreeAdapter
-
-    class ProjectAdapter(ITreeAdapter):
-        __select__ = is_instance('Project')
-        tree_relation = 'subproject_of'
-
-    class Project(AnyEntity):
-        __regid__ = 'Project'
-        fetch_attrs, cw_fetch_order = fetch_config(('name', 'description',
-                                                    'description_format', 'summary'))
-
-        TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
-
-        def dc_title(self):
-            return self.name
-
-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
-tree-like relations, but also because it is perfectly fine and useful
-to use such an interface in Hooks.
-
-In fact, Tree nature is a property of the data model that cannot be
-fully and portably expressed at the level of database entities (think
-about the transitive closure of the child relation). This is a further
-argument to implement it at entity class level.
-
-``fetch_attrs`` configures which attributes should be pre-fetched when using ORM
-methods retrieving entity of this type. In a same manner, the ``cw_fetch_order`` is
-a class method allowing to control sort order. More on this in :ref:`FetchAttrs`.
-
-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
-consumed 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
-responsible of ensuring XHTML compliance), or a console or file
-oriented output (which would have the necessary context about the
-needed byte stream encoding).
-
-.. note::
-
-  The Dublin Core `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, continuing the
-Project class.
-
-.. sourcecode:: python
-
-    def latest_version(self, states=('published',), reverse=None):
-        """returns the latest version(s) for the project in one of the given
-        states.
-
-        when no states specified, returns the latest published version.
-        """
-        order = 'DESC'
-        if reverse is not None:
-            warn('reverse argument is deprecated',
-                 DeprecationWarning, stacklevel=1)
-            if reverse:
-                order = 'ASC'
-        rset = self.versions_in_state(states, order, True)
-        if rset:
-            return rset.get_entity(0, 0)
-        return None
-
-    def versions_in_state(self, states, order='ASC', limit=False):
-        """returns version(s) for the project in one of the given states, sorted
-        by version number.
-
-        If limit is true, limit result to one version.
-        If reverse, versions are returned from the smallest to the greatest.
-        """
-        if limit:
-            order += ' LIMIT 1'
-        rql = 'Any V,N ORDERBY version_sort_value(N) %s ' \
-              'WHERE V num N, V in_state S, S name IN (%s), ' \
-              'V version_of P, P eid %%(p)s' % (order, ','.join(repr(s) for s in states))
-        return self._cw.execute(rql, {'p': self.eid})
-
-.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker/
-
-These few lines exhibit the important properties we want to outline:
-
-* entity code is concerned with the application domain
-
-* it is NOT concerned with database consistency (this is the realm of
-  Hooks/Operations); in other words, it assumes a consistent world
-
-* it is NOT (directly) concerned with end-user interfaces
-
-* however it can be used in both contexts
-
-* it does not create or manipulate the internal object's state
-
-* it plays freely with RQL expression as needed
-
-* it is not concerned with internationalization
-
-* it does not raise exceptions
-
-
--- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-Access to persistent data
---------------------------
-
-Python-level access to persistent data is provided by the
-:class:`Entity <cubicweb.entity>` class.
-
-.. 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:
-
-* the attributes defined in the schema appear as attributes of these classes
-
-* the relations defined in the schema appear as attributes of these classes,
-  but are lists of instances
-
-`Formatting and output generation`:
-
-* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
-  (and returns an unicode string)
-
-* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
-
-* :meth:`rest_path()`, returns a relative REST URL to get the entity
-
-* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
-  returns a string enabling the display of an attribute value in a given format
-  (the value is automatically recovered if necessary)
-
-`Data handling`:
-
-* :meth:`as_rset()`, converts the entity into an equivalent result set simulating the
-  request `Any X WHERE X eid _eid_`
-
-* :meth:`complete(skip_bytes=True)`, executes a request that recovers at
-  once all the missing attributes of an entity
-
-* :meth:`get_value(name)`, returns the value associated to the attribute name given
-  in parameter
-
-* :meth:`related(rtype, role='subject', limit=None, entities=False)`,
-  returns a list of entities related to the current entity by the
-  relation given in parameter
-
-* :meth:`unrelated(rtype, targettype, role='subject', limit=None)`,
-  returns a result set corresponding to the entities not (yet)
-  related to the current entity by the relation given in parameter
-  and satisfying its constraints
-
-* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the
-  corresponding values given named parameters. 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).
-
-* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid
-  given in the parameters on the current entity
-
-* :meth:`cw_delete()` allows to delete the entity
-
-
-The :class:`AnyEntity` 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.
-
-The class `AnyEntity` is a sub-class of Entity that add methods to it,
-and helps specializing (by further subclassing) the handling of a
-given entity type.
-
-Most methods defined for `AnyEntity`, in addition to `Entity`, add
-support for the `Dublin Core`_ metadata.
-
-.. _`Dublin Core`: http://dublincore.org/
-
-`Standard meta-data (Dublin Core)`:
-
-* :meth:`dc_title()`, returns a unicode string corresponding to the
-  meta-data `Title` (used by default is the first non-meta attribute
-  of the entity schema)
-
-* :meth:`dc_long_title()`, same as dc_title but can return a more
-  detailed title
-
-* :meth:`dc_description(format='text/plain')`, returns a unicode string
-  corresponding to the meta-data `Description` (looks for a
-  description attribute by default)
-
-* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data
-  `Authors` (owners by default)
-
-* :meth:`dc_creator()`, returns a unicode string corresponding to the
-  creator of the entity
-
-* :meth:`dc_date(date_format=None)`, returns a unicode string corresponding to
-  the meta-data `Date` (update date by default)
-
-* :meth:`dc_type(form='')`, returns a string to display the entity type by
-  specifying the preferred form (`plural` for a plural form)
-
-* :meth:`dc_language()`, returns the language used by the entity
-
-Inheritance
------------
-
-When describing a data model, entities can inherit from other entities as is
-common in object-oriented programming.
-
-You have the possibility to redefine whatever pleases you, as follow:
-
-.. sourcecode:: python
-
-    from cubes.OTHER_CUBE import entities
-
-    class EntityExample(entities.EntityExample):
-
-        def dc_long_title(self):
-            return '%s (%s)' % (self.name, self.description)
-
-The most specific entity definition will always the one used by the
-ORM. For instance, the new EntityExample above in mycube replaces the
-one in OTHER_CUBE. These types are stored in the `etype` section of
-the `vregistry`.
-
-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	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-Data as objects
-===============
-
-In this chapter, we will introduce the objects that are used to handle
-the logic associated to the data stored in the database.
-
-.. toctree::
-   :maxdepth: 1
-
-   data-as-objects
-   load-sort
-   adapters
-   application-logic
--- a/doc/book/en/devrepo/entityclasses/load-sort.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-
-.. _FetchAttrs:
-
-Loaded attributes and default sorting management
-````````````````````````````````````````````````
-
-* The class attribute `fetch_attrs` allows to define in an entity class a list of
-  names of attributes that should be automatically loaded when entities of this
-  type are fetched from the database using ORM methods retrieving entity of this
-  type (such as :meth:`related` and :meth:`unrelated`). You can also put relation
-  names in there, but we are limited to *subject relations of cardinality `?` or
-  `1`*.
-
-* The :meth:`cw_fetch_order` and :meth:`cw_fetch_unrelated_order` class methods
-  are respectively responsible to control how entities will be sorted when:
-
-  - retrieving all entities of a given type, or entities related to another
-
-  - retrieving a list of entities for use in drop-down lists enabling relations
-    creation in the editing view of an entity
-
-By default entities will be listed on their modification date descending,
-i.e. you'll get entities recently modified first. While this is usually a good
-default in drop-down list, you'll probably want to change `cw_fetch_order`.
-
-This may easily be done using the :func:`~cubicweb.entities.fetch_config`
-function, which simplifies the definition of attributes to load and sorting by
-returning a list of attributes to pre-load (considering automatically the
-attributes of `AnyEntity`) and a sorting function as described below:
-
-.. autofunction:: cubicweb.entities.fetch_config
-
-In you want something else (such as sorting on the result of a registered
-procedure), here is the prototype of those methods:
-
-
-.. automethod:: cubicweb.entity.Entity.cw_fetch_order
-
-.. automethod:: cubicweb.entity.Entity.cw_fetch_unrelated_order
-
--- a/doc/book/en/devrepo/fti.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-.. _fti:
-
-Full Text Indexing in CubicWeb
-------------------------------
-
-When an attribute is tagged as *fulltext-indexable* in the datamodel,
-CubicWeb will automatically trigger hooks to update the internal
-fulltext index (i.e the ``appears`` SQL table) each time this attribute
-is modified.
-
-CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
-fulltext on demand:
-
-.. sourcecode:: bash
-
-   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
-
-You can also rebuild the fulltext index for a given set of entity types:
-
-.. sourcecode:: bash
-
-   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
-
-In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
-will be rebuilt.
-
-
-Standard FTI process
-~~~~~~~~~~~~~~~~~~~~
-
-Considering an entity type ``ET``, the default *fti* process is to :
-
-1. fetch all entities of type ``ET``
-
-2. for each entity, adapt it to ``IFTIndexable`` (see
-   :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
-
-3. call
-   :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
-   the adapter which is supposed to return a dictionary *weight* ->
-   *list of words* as expected by
-   :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
-   tokenization of each attribute value is done by
-   :meth:`~logilab.database.fti.tokenize`.
-
-
-See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
-
-
-Yams and ``fulltext_container``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-It is possible in the datamodel to indicate that fulltext-indexed
-attributes defined for an entity type will be used to index not the
-entity itself but a related entity. This is especially useful for
-composite entities. Let's take a look at (a simplified version of)
-the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
-
-.. sourcecode:: python
-
-  class CWUser(WorkflowableEntityType):
-      login     = String(required=True, unique=True, maxsize=64)
-      upassword = Password(required=True)
-
-  class EmailAddress(EntityType):
-      address = String(required=True,  fulltextindexed=True,
-                       indexed=True, unique=True, maxsize=128)
-
-
-  class use_email_relation(RelationDefinition):
-      name = 'use_email'
-      subject = 'CWUser'
-      object = 'EmailAddress'
-      cardinality = '*?'
-      composite = 'subject'
-
-
-The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
-and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
-in your application, if you use fulltext search to look for an email address, CubicWeb
-will return the ``EmailAddress`` itself. But the objects we'd like to index
-are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
-
-The simplest way to achieve that is to tag the ``use_email`` relation in
-the datamodel:
-
-.. sourcecode:: python
-
-  class use_email(RelationType):
-      fulltext_container = 'subject'
-
-
-Customizing how entities are fetched during ``db-rebuild-fti``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-``db-rebuild-fti`` will call the
-:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
-method on your entity type.
-
-.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
-
-Now, suppose you've got a _huge_ table to index, you probably don't want to
-get all entities at once. So here's a simple customized example that will
-process block of 10000 entities:
-
-.. sourcecode:: python
-
-
-    class MyEntityClass(AnyEntity):
-        __regid__ = 'MyEntityClass'
-
-    @classmethod
-    def cw_fti_index_rql_queries(cls, req):
-        # get the default RQL method and insert LIMIT / OFFSET instructions
-        base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
-        selected, restrictions = base_rql.split(' WHERE ')
-        rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
-            selected, restrictions)
-        # count how many entities you'll have to index
-        count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
-        # iterate by blocks of 10000 entities
-        chunksize = 10000
-        for offset in xrange(0, count, chunksize):
-            print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
-            yield rql_template % {'limit': chunksize, 'offset': offset}
-
-Since you have access to ``req``, you can more or less fetch whatever you want.
-
-
-Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can also customize the FTI process by providing your own ``get_words()``
-implementation:
-
-.. sourcecode:: python
-
-    from cubicweb.entities.adapters import IFTIndexableAdapter
-
-    class SearchIndexAdapter(IFTIndexableAdapter):
-        __regid__ = 'IFTIndexable'
-        __select__ = is_instance('MyEntityClass')
-
-        def fti_containers(self, _done=None):
-            """this should yield any entity that must be considered to
-            fulltext-index self.entity
-
-            CubicWeb's default implementation will look for yams'
-            ``fulltex_container`` property.
-            """
-            yield self.entity
-            yield self.entity.some_related_entity
-
-
-        def get_words(self):
-            # implement any logic here
-            # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
-            # for the actual signification of 'C'
-            return {'C': ['any', 'word', 'I', 'want']}
--- a/doc/book/en/devrepo/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-.. _Part2:
-
-----------------------
-Repository development
-----------------------
-
-This part is about developing applications with the *CubicWeb*
-framework. It is not concerned with the web system, which is a
-separate layer and has its own whole chapter.
-
-.. toctree::
-   :maxdepth: 2
-   :numbered:
-
-   cubes/index
-   vreg.rst
-   datamodel/index
-   entityclasses/index
-   devcore/index
-   repo/index
-   testing.rst
-   migration.rst
-   profiling.rst
-   fti.rst
-   dataimport
--- a/doc/book/en/devrepo/migration.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,250 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _migration:
-
-Migration
-=========
-
-One of the main design goals of *CubicWeb* was to support iterative and agile
-development. For this purpose, multiple actions are provided to facilitate the
-improvement of an instance, and in particular to handle the changes to be
-applied to the data model, without loosing existing data.
-
-The current version of a cube (and of cubicweb itself) is provided in the file
-`__pkginfo__.py` as a tuple of 3 integers.
-
-Migration scripts management
-----------------------------
-
-Migration scripts has to be located in the directory `migration` of your
-cube and named accordingly:
-
-::
-
-  <version n° X.Y.Z>[_<description>]_<mode>.py
-
-in which :
-
-* X.Y.Z is the model version number to which the script enables to migrate.
-
-* *mode* (between the last "_" and the extension ".py") is used for
-  distributed installation. It indicates to which part
-  of the application (RQL server, web server) the script applies.
-  Its value could be :
-
-  * `common`, applies to the RQL server as well as the web server and updates
-    files on the hard drive (configuration files migration for example).
-
-  * `web`, applies only to the web server and updates files on the hard drive.
-
-  * `repository`, applies only to the RQL server and updates files on the
-    hard drive.
-
-  * `Any`, applies only to the RQL server and updates data in the database
-    (schema and data migration for example).
-
-Again in the directory `migration`, the file `depends.map` allows to indicate
-that for the migration to a particular model version, you always have to first
-migrate to a particular *CubicWeb* version. This file can contain comments (lines
-starting with `#`) and a dependency is listed as follows: ::
-
-  <model version n° X.Y.Z> : <cubicweb version n° X.Y.Z>
-
-For example: ::
-
-  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
-
-Base context
-------------
-
-The following identifiers are pre-defined in migration scripts:
-
-* `config`, instance configuration
-
-* `interactive_mode`, boolean indicating that the script is executed in
-  an interactive mode or not
-
-* `versions_map`, dictionary of migrated versions  (key are cubes
-  names, including 'cubicweb', values are (from version, to version)
-
-* `confirm(question)`, function asking the user and returning true
-  if the user answers yes, false otherwise (always returns true in
-  non-interactive mode)
-
-* `_()` is equivalent to `unicode` allowing to flag the strings to
-  internationalize in the migration scripts.
-
-In the `repository` scripts, the following identifiers are also defined:
-
-* `commit(ask_confirm=True)`, request confirming and executing a "commit"
-
-* `schema`, instance schema (readen from the database)
-
-* `fsschema`, installed schema on the file system (e.g. schema of
-  the updated model and cubicweb)
-
-* `repo`, repository object
-
-* `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`
-scripts:
-
-* `add_attribute(etype, attrname, attrtype=None, commit=True)`, adds a new
-  attribute to an existing entity type. If the attribute type is not specified,
-  then it is extracted from the updated schema.
-
-* `drop_attribute(etype, attrname, commit=True)`, removes an attribute from an
-  existing entity type.
-
-* `rename_attribute(etype, oldname, newname, commit=True)`, renames an attribute
-
-* `add_entity_type(etype, auto=True, commit=True)`, adds a new entity type.
-  If `auto` is True, all the relations using this entity type and having a known
-  entity type on the other hand will automatically be added.
-
-* `drop_entity_type(etype, commit=True)`, removes an entity type and all the
-  relations using it.
-
-* `rename_entity_type(oldname, newname, commit=True)`, renames an entity type
-
-* `add_relation_type(rtype, addrdef=True, commit=True)`, adds a new relation
-  type. If `addrdef` is True, all the relations definitions of this type will
-  be added.
-
-* `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
-  definitions of this type.
-
-* `rename_relation_type(oldname, newname, commit=True)`, renames a relation type.
-
-* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
-  relation definition.
-
-* `drop_relation_definition(subjtype, rtype, objtype, commit=True)`, removes
-  a relation definition.
-
-* `sync_schema_props_perms(ertype=None, syncperms=True, syncprops=True, syncrdefs=True, commit=True)`,
-  synchronizes properties and/or permissions on:
-  - the whole schema if ertype is None
-  - an entity or relation type schema if ertype is a string
-  - a relation definition  if ertype is a 3-uple (subject, relation, object)
-
-* `change_relation_props(subjtype, rtype, objtype, commit=True, **kwargs)`, changes
-  properties of a relation definition by using the named parameters of the properties
-  to change.
-
-* `set_widget(etype, rtype, widget, commit=True)`, changes the widget used for the
-  relation <rtype> of entity type <etype>.
-
-* `set_size_constraint(etype, rtype, size, commit=True)`, changes the size constraints
-  for the relation <rtype> of entity type <etype>.
-
-Data migration
---------------
-The following functions for data migration are available in `repository` scripts:
-
-* `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL
-  query, either to interrogate or update. A result set object is returned.
-
-* `add_entity(etype, *args, **kwargs)`, adds a new entity of the given type.
-  The attribute and relation values are specified as named positional
-  arguments.
-
-Workflow creation
------------------
-
-The following functions for workflow creation are available in `repository`
-scripts:
-
-* `add_workflow(label, workflowof, initial=False, commit=False, **kwargs)`, adds a new workflow
-  for a given type(s)
-
-You can find more details about workflows in the chapter :ref:`Workflow` .
-
-Configuration migration
------------------------
-
-The following functions for configuration migration are available in all
-scripts:
-
-* `option_renamed(oldname, newname)`, indicates that an option has been renamed
-
-* `option_group_change(option, oldgroup, newgroup)`, indicates that an option does not
-  belong anymore to the same group.
-
-* `option_added(oldname, newname)`, indicates that an option has been added.
-
-* `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
---------------------------
-Those functions are only used for low level operations that could not be
-accomplished otherwise or to repair damaged databases during interactive
-session. They are available in `repository` scripts:
-
-* `sql(sql, args=None, ask_confirm=True)`, executes an arbitrary SQL query on the system source
-* `add_entity_type_table(etype, commit=True)`
-* `add_relation_type_table(rtype, commit=True)`
-* `uninline_relation(rtype, commit=True)`
-
-
-[FIXME] Add explanation on how to use cubicweb-ctl shell
--- a/doc/book/en/devrepo/profiling.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-.. _PROFILING:
-
-Profiling and performance
-=========================
-
-If you feel that one of your pages takes more time than it should to be
-generated, chances are that you're making too many RQL queries.  Obviously,
-there are other reasons but experience tends to show this is the first thing to
-track down. Luckily, CubicWeb provides a configuration option to log RQL
-queries. In your ``all-in-one.conf`` file, set the **query-log-file** option::
-
-    # web application query log file
-    query-log-file=/home/user/myapp-rql.log
-
-Then restart your application, reload your page and stop your application.
-The file ``myapp-rql.log`` now contains the list of RQL queries that were
-executed during your test. It's a simple text file containing lines such as::
-
-    Any A WHERE X eid %(x)s, X lastname A {'x': 448} -- (0.002 sec, 0.010 CPU sec)
-    Any A WHERE X eid %(x)s, X firstname A {'x': 447} -- (0.002 sec, 0.000 CPU sec)
-
-The structure of each line is::
-
-    <RQL QUERY> <QUERY ARGS IF ANY> -- <TIME SPENT>
-
-CubicWeb also provides the **exlog** command to examine and summarize data found
-in such a file:
-
-.. sourcecode:: sh
-
-    $ cubicweb-ctl exlog /home/user/myapp-rql.log
-    0.07 50 Any A WHERE X eid %(x)s, X firstname A {}
-    0.05 50 Any A WHERE X eid %(x)s, X lastname A {}
-    0.01 1 Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E employees X, X modification_date AA {}
-    0.01 1 Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s {, }
-    0.01 1 Any B,T,P ORDERBY lower(T) WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, U eid %(x)s {}
-    0.01 1 Any A,B,C,D WHERE A eid %(x)s,A name B,A creation_date C,A modification_date D {}
-
-This command sorts and uniquifies queries so that it's easy to see where
-is the hot spot that needs optimization.
-
-Do not neglect to set the **fetch_attrs** attribute you can define in your
-entity classes because it can greatly reduce the number of queries executed (see
-:ref:`FetchAttrs`).
-
-You should also know about the **profile** option in the ``all-in-on.conf``. If
-set, this option will make your application run in an `hotshot`_ session and
-store the results in the specified file.
-
-.. _hotshot: http://docs.python.org/library/hotshot.html#module-hotshot
-
-Last but no least, if you're using the PostgreSQL database backend, VACUUMing
-your database can significantly improve the performance of the queries (by
-updating the statistics used by the query optimizer). Nowadays, this is done
-automatically from time to time, but if you've just imported a large amount of
-data in your db, you will want to vacuum it (with the analyse option on). Read
-the documentation of your database for more information.
--- a/doc/book/en/devrepo/repo/hooks.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,279 +0,0 @@
-.. -*- coding: utf-8 -*-
-.. _hooks:
-
-Hooks and Operations
-====================
-
-.. autodocstring:: cubicweb.server.hook
-
-
-Example using dataflow hooks
-----------------------------
-
-We will use a very simple example to show hooks usage. Let us start with the
-following schema.
-
-.. sourcecode:: python
-
-   class Person(EntityType):
-       age = Int(required=True)
-
-We would like to add a range constraint over a person's age. Let's write an hook
-(supposing yams can not handle this nativly, which is wrong). It shall be placed
-into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
-`mycube/hooks/... package` containing hooks in various modules.
-
-.. sourcecode:: python
-
-   from cubicweb import ValidationError
-   from cubicweb.predicates import is_instance
-   from cubicweb.server.hook import Hook
-
-   class PersonAgeRange(Hook):
-        __regid__ = 'person_age_range'
-        __select__ = Hook.__select__ & is_instance('Person')
-        events = ('before_add_entity', 'before_update_entity')
-
-        def __call__(self):
-	    if 'age' in self.entity.cw_edited:
-                if 0 <= self.entity.age <= 120:
-                   return
-		msg = self._cw._('age must be between 0 and 120')
-		raise ValidationError(self.entity.eid, {'age': msg})
-
-In our example the base `__select__` is augmented with an `is_instance` selector
-matching the desired entity type.
-
-The `events` tuple is used specify that our hook should be called before the
-entity is added or updated.
-
-Then in the hook's `__call__` method, we:
-
-* check if the 'age' attribute is edited
-* if so, check the value is in the range
-* if not, raise a validation error properly
-
-Now Let's augment our schema with new `Company` entity type with some relation to
-`Person` (in 'mycube/schema.py').
-
-.. sourcecode:: python
-
-   class Company(EntityType):
-        name = String(required=True)
-        boss = SubjectRelation('Person', cardinality='1*')
-        subsidiary_of = SubjectRelation('Company', cardinality='*?')
-
-
-We would like to constrain the company's bosses to have a minimum (legal)
-age. Let's write an hook for this, which will be fired when the `boss` relation
-is established (still supposing we could not specify that kind of thing in the
-schema).
-
-.. sourcecode:: python
-
-   class CompanyBossLegalAge(Hook):
-        __regid__ = 'company_boss_legal_age'
-        __select__ = Hook.__select__ & match_rtype('boss')
-        events = ('before_add_relation',)
-
-        def __call__(self):
-            boss = self._cw.entity_from_eid(self.eidto)
-            if boss.age < 18:
-                msg = self._cw._('the minimum age for a boss is 18')
-                raise ValidationError(self.eidfrom, {'boss': msg})
-
-.. Note::
-
-    We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
-    proper relation type.
-
-    The essential difference with respect to an entity hook is that there is no
-    self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
-    represent the subject and object **eid** of the relation.
-
-Suppose we want to check that there is no cycle by the `subsidiary_of`
-relation. This is best achieved in an operation since all relations are likely to
-be set at commit time.
-
-.. sourcecode:: python
-
-    from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
-
-    def check_cycle(self, session, eid, rtype, role='subject'):
-        parents = set([eid])
-        parent = session.entity_from_eid(eid)
-        while parent.related(rtype, role):
-            parent = parent.related(rtype, role)[0]
-            if parent.eid in parents:
-                msg = session._('detected %s cycle' % rtype)
-                raise ValidationError(eid, {rtype: msg})
-            parents.add(parent.eid)
-
-
-    class CheckSubsidiaryCycleOp(Operation):
-
-        def precommit_event(self):
-            check_cycle(self.session, self.eidto, 'subsidiary_of')
-
-
-    class CheckSubsidiaryCycleHook(Hook):
-        __regid__ = 'check_no_subsidiary_cycle'
-        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
-        events = ('after_add_relation',)
-
-        def __call__(self):
-            CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
-
-
-Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
-exceptions are usually programming errors.
-
-In the above example, our hook will instantiate an operation each time the hook
-is called, i.e. each time the `subsidiary_of` relation is set. There is an
-alternative method to schedule an operation from a hook, using the
-:func:`get_instance` class method.
-
-.. sourcecode:: python
-
-   from cubicweb.server.hook import set_operation
-
-   class CheckSubsidiaryCycleHook(Hook):
-       __regid__ = 'check_no_subsidiary_cycle'
-       events = ('after_add_relation',)
-       __select__ = Hook.__select__ & match_rtype('subsidiary_of')
-
-       def __call__(self):
-           CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)
-
-   class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):
-
-       def precommit_event(self):
-           for eid in self.get_data():
-               check_cycle(self.session, eid, self.rtype)
-
-
-Here, we call :func:`set_operation` so that we will simply accumulate eids of
-entities to check at the end in a single `CheckSubsidiaryCycleOp`
-operation. Value are stored in a set associated to the
-'subsidiary_cycle_detection' transaction data key. The set initialization and
-operation creation are handled nicely by :func:`set_operation`.
-
-A more realistic example can be found in the advanced tutorial chapter
-:ref:`adv_tuto_security_propagation`.
-
-
-Inter-instance communication
-----------------------------
-
-If your application consists of several instances, you may need some means to
-communicate between them.  Cubicweb provides a publish/subscribe mechanism
-using ØMQ_.  In order to use it, use
-:meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
-`repo.app_instances_bus` object.  The `callback` will get the message (as a
-list).  A message can be sent by calling
-:meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
-The first element of the message is the topic which is used for filtering and
-dispatching messages.
-
-.. _ØMQ: http://www.zeromq.org/
-
-.. sourcecode:: python
-
-  class FooHook(hook.Hook):
-      events = ('server_startup',)
-      __regid__ = 'foo_startup'
-
-      def __call__(self):
-          def callback(msg):
-              self.info('received message: %s', ' '.join(msg))
-          self.repo.app_instances_bus.add_subscription('hello', callback)
-
-.. sourcecode:: python
-
-  def do_foo(self):
-      actually_do_foo()
-      self._cw.repo.app_instances_bus.publish(['hello', 'world'])
-
-The `zmq-address-pub` configuration variable contains the address used
-by the instance for sending messages, e.g. `tcp://*:1234`.  The
-`zmq-address-sub` variable contains a comma-separated list of addresses
-to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
-
-
-Hooks writing tips
-------------------
-
-Reminder
-~~~~~~~~
-
-You should never use the `entity.foo = 42` notation to update an entity. It will
-not do what you expect (updating the database). Instead, use the
-:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
-:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
-'before_update_entity' event.
-
-
-How to choose between a before and an after event ?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-`before_*` hooks give you access to the old attribute (or relation)
-values. You can also intercept and update edited values in the case of
-entity modification before they reach the database.
-
-Else the question is: should I need to do things before or after the actual
-modification ? If the answer is "it doesn't matter", use an 'after' event.
-
-
-Validation Errors
-~~~~~~~~~~~~~~~~~
-
-When a hook which is responsible to maintain the consistency of the
-data model detects an error, it must use a specific exception named
-:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
-:exc:`~cubicweb.ValidationError` is a programming error. Raising it
-entails aborting the current transaction.
-
-This exception is used to convey enough information up to the user
-interface. Hence its constructor is different from the default Exception
-constructor. It accepts, positionally:
-
-* an entity eid (**not the entity itself**),
-
-* a dict whose keys represent attribute (or relation) names and values
-  an end-user facing message (hence properly translated) relating the
-  problem.
-
-.. sourcecode:: python
-
-  raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
-                                    'temperature': self._cw._('too hot')})
-
-
-Checking for object created/deleted in the current transaction
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In hooks, you can use the
-:meth:`~cubicweb.server.session.Session.added_in_transaction` or
-:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
-object to check if an eid has been created or deleted during the hook's
-transaction.
-
-This is useful to enable or disable some stuff if some entity is being added or
-deleted.
-
-.. sourcecode:: python
-
-   if self._cw.deleted_in_transaction(self.eidto):
-      return
-
-
-Peculiarities of inlined relations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
-for details) are inserted in the database at the same time as entity attributes.
-
-This may have some side effect, for instance when creating an entity
-and setting an inlined relation in the same rql query, then at
-`before_add_relation` time, the relation will already exist in the
-database (it is otherwise not the case).
--- a/doc/book/en/devrepo/repo/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Repository customization
-++++++++++++++++++++++++
-.. toctree::
-   :maxdepth: 1
-
-   sessions
-   hooks
-   notifications
-   tasks
-
-
--- a/doc/book/en/devrepo/repo/notifications.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Notifications management
-========================
-
-CubicWeb provides a machinery to ease notifications handling. To use it for a
-notification:
-
-* write a view inheriting from
-  :class:`~cubicweb.sobjects.notification.NotificationView`.  The usual view api
-  is used to generated the email (plain text) content, and additional
-  :meth:`~cubicweb.sobjects.notification.NotificationView.subject` and
-  :meth:`~cubicweb.sobjects.notification.NotificationView.recipients` methods
-  are used to build the email's subject and
-  recipients. :class:`NotificationView` provides default implementation for both
-  methods.
-
-* write a hook for event that should trigger this notification, select the view
-  (without rendering it), and give it to
-  :func:`cubicweb.hooks.notification.notify_on_commit` so that the notification
-  will be sent if the transaction succeed.
-
-
-.. XXX explain recipient finder and provide example
-
-API details
-~~~~~~~~~~~
-.. autoclass:: cubicweb.sobjects.notification.NotificationView
-.. autofunction:: cubicweb.hooks.notification.notify_on_commit
--- a/doc/book/en/devrepo/repo/sessions.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Sessions
-========
-
-Sessions are objects linked to an authenticated user.  The `Session.new_cnx`
-method returns a new Connection linked to that session.
-
-Connections
-===========
-
-Connections provide the `.execute` method to query the data sources.
-
-Kinds of connections
---------------------
-
-There are two kinds of connections.
-
-* `normal connections` are the most common: they are related to users and
-  carry security checks coming with user credentials
-
-* `internal connections` have all the powers; they are also used in only a
-  few situations where you don't already have an adequate session at
-  hand, like: user authentication, data synchronisation in
-  multi-source contexts
-
-Normal connections are typically named `_cw` in most appobjects or
-sometimes just `session`.
-
-Internal connections are available from the `Repository` object and are
-to be used like this:
-
-.. sourcecode:: python
-
-   with self.repo.internal_cnx() as cnx:
-       do_stuff_with(cnx)
-       cnx.commit()
-
-Connections should always be used as context managers, to avoid leaks.
-
-Authentication and management of sessions
------------------------------------------
-
-The authentication process is a ballet involving a few dancers:
-
-* through its `get_session` 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 (named `LoginPasswordRetriever`)
-        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 delegate work to an authentifier
-          class that defines 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 plethora 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 `trustedauth`_ cube.
-
-.. _`trustedauth`: http://www.cubicweb.org/project/cubicweb-trustedauth
-
-Repository authentication plugins
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-On the repository side, it is possible to register a source
-authentifier using the following kind of code:
-
-.. 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
-
- class XFooUserRetriever(authentication.LoginPasswordRetriever):
-     """ 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
-
-Full Session and Connection API
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. autoclass:: cubicweb.server.session.Session
-.. autoclass:: cubicweb.server.session.Connection
--- a/doc/book/en/devrepo/repo/tasks.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Tasks
-=========
-
-[WRITE ME]
-
-* repository tasks
-
--- a/doc/book/en/devrepo/testing.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,559 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Tests
-=====
-
-Unit tests
-----------
-
-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,
-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, automatic tests check that views are valid
-XHTML. See :ref:`automatic_views_tests` for details.
-
-Most unit tests need a live database to work against. This is achieved
-by CubicWeb using automatically sqlite (bundled with Python, see
-http://docs.python.org/library/sqlite3.html) as a backend.
-
-The database is stored in the mycube/test/tmpdb,
-mycube/test/tmpdb-template files. If it does not (yet) exist, it will
-be built automatically when the test suite starts.
-
-.. warning::
-
-  Whenever the schema changes (new entities, attributes, relations)
-  one must delete these two files. Changes concerned only with entity
-  or relation type properties (constraints, cardinalities,
-  permissions) and generally dealt with using the
-  `sync_schema_props_perms()` function of the migration environment do
-  not need a database regeneration step.
-
-.. _hook_test:
-
-Unit test by example
-````````````````````
-
-We start with an example extracted from the keyword cube (available
-from http://www.cubicweb.org/project/cubicweb-keyword).
-
-.. sourcecode:: python
-
-    from cubicweb.devtools.testlib import CubicWebTC
-    from cubicweb import ValidationError
-
-    class ClassificationHooksTC(CubicWebTC):
-
-        def setup_database(self):
-            with self.admin_access.repo_cnx() as cnx:
-                group_etype = cnx.find('CWEType', name='CWGroup').one()
-                c1 = cnx.create_entity('Classification', name=u'classif1',
-                                       classifies=group_etype)
-                user_etype = cnx.find('CWEType', name='CWUser').one()
-                c2 = cnx.create_entity('Classification', name=u'classif2',
-                                       classifies=user_etype)
-                self.kw1eid = cnx.create_entity('Keyword', name=u'kwgroup', included_in=c1).eid
-                cnx.commit()
-
-        def test_cannot_create_cycles(self):
-            with self.admin_access.repo_cnx() as cnx:
-                kw1 = cnx.entity_from_eid(self.kw1eid)
-                # direct obvious cycle
-                with self.assertRaises(ValidationError):
-                    kw1.cw_set(subkeyword_of=kw1)
-                cnx.rollback()
-                # testing indirect cycles
-                kw3 = cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
-                                  'SK subkeyword_of K WHERE C name "classif1", K eid %(k)s'
-                                  {'k': kw1}).get_entity(0,0)
-                kw3.cw_set(reverse_subkeyword_of=kw1)
-                self.assertRaises(ValidationError, cnx.commit)
-
-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.
-
-The test case itself checks that an Operation does its job of
-preventing cycles amongst Keyword entities.
-
-The `create_entity` method of connection (or request) objects allows
-to create an entity. You can link this entity to other 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::
-
-   the :meth:`commit` method is not called automatically. You have to
-   call it explicitly if needed (notably to test operations). It is a
-   good practice to regenerate entities with :meth:`entity_from_eid`
-   after a commit to avoid request cache effects.
-
-You can see an example of security tests in the
-:ref:`adv_tuto_security`.
-
-It is possible to have these tests run continuously using `apycot`_.
-
-.. _apycot: http://www.cubicweb.org/project/apycot
-
-.. _securitytest:
-
-Managing connections or users
-+++++++++++++++++++++++++++++
-
-Since unit tests are done with the SQLITE backend and this does not
-support multiple connections at a time, you must be careful when
-simulating security, changing users.
-
-By default, tests run with a user with admin privileges. Connections
-using these credentials are accessible through the `admin_access` object
-of the test classes.
-
-The `repo_cnx()` method returns a connection object that can be used as a
-context manager:
-
-.. sourcecode:: python
-
-   # admin_access is a pre-cooked session wrapping object
-   # it is built with:
-   # self.admin_access = self.new_access('admin')
-   with self.admin_access.repo_cnx() as cnx:
-       cnx.execute(...)
-       self.create_user(cnx, login='user1')
-       cnx.commit()
-
-   user1access = self.new_access('user1')
-   with user1access.web_request() as req:
-       req.execute(...)
-       req.cnx.commit()
-
-On exit of the context manager, a rollback is issued, which releases
-the connection. Don't forget to issue the `cnx.commit()` calls!
-
-.. warning::
-
-   Do not use references kept to the entities created with a
-   connection from another one!
-
-Email notifications tests
-`````````````````````````
-
-When running tests, potentially generated e-mails are not really sent
-but are found in the list `MAILBOX` of module
-:mod:`cubicweb.devtools.testlib`.
-
-You can test your notifications by analyzing the contents of this list, which
-contains objects with two attributes:
-
-* `recipients`, the list of recipients
-* `msg`, email.Message object
-
-Let us look at a simple example from the ``blog`` cube.
-
-.. sourcecode:: python
-
-    from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
-
-    class BlogTestsCubicWebTC(CubicWebTC):
-        """test blog specific behaviours"""
-
-        def test_notifications(self):
-            with self.admin_access.web_request() as req:
-                cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
-                                    description=u'cubicweb is beautiful')
-                blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
-                                                 content=u'cubicweb hop')
-                blog_entry_1.cw_set(entry_of=cubicweb_blog)
-                blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
-                                                 content=u'cubicweb yes')
-                blog_entry_2.cw_set(entry_of=cubicweb_blog)
-                self.assertEqual(len(MAILBOX), 0)
-                req.cnx.commit()
-                self.assertEqual(len(MAILBOX), 2)
-                mail = MAILBOX[0]
-                self.assertEqual(mail.subject, '[data] hop')
-                mail = MAILBOX[1]
-                self.assertEqual(mail.subject, '[data] yes')
-
-Visible actions tests
-`````````````````````
-
-It is easy to write unit tests to test actions which are visible to
-a 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):
-            with self.admin_access.repo_cnx() as cnx:
-                self.confeid = cnx.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).eid
-
-        def test_admin(self):
-            with self.admin_access.web_request() as req:
-                rset = req.find('Conference').one()
-                self.assertListEqual(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.assertListEqual(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.confeid}),
-                                       ])
-
-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 a 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 :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
-invoked and tested.
-
-Depending on the schema, hooks and operations constraints, it is not
-always possible for the automatic auto_populate to proceed.
-
-It is possible of course to completely redefine auto_populate. A
-lighter solution is to give hints (fill some class attributes) about
-what entities and relations have to be skipped by the auto_populate
-mechanism. These are:
-
-* `no_auto_populate`, may contain a list of entity types to skip
-* `ignored_relations`, may contain a list of relation types to skip
-* `application_rql`, may contain a list of rql expressions that
-  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.
-
-Cache heavy database setup
--------------------------------
-
-Some test suites require a complex setup of the database that takes
-seconds (or even minutes) to complete. Doing the whole setup for each
-individual test makes the whole run very slow. The ``CubicWebTC``
-class offer a simple way to prepare a specific database once for
-multiple tests. The `test_db_id` class attribute of your
-``CubicWebTC`` subclass must be set to a unique identifier and the
-:meth:`pre_setup_database` class method must build the cached content. As
-the :meth:`pre_setup_database` method is not garanteed to be called
-every time a test method is run, you must not set any class attribute
-to be used during test *there*. Databases for each `test_db_id` are
-automatically created if not already in cache. Clearing the cache is
-up to the user. Cache files are found in the :file:`data/database`
-subdirectory of your test directory.
-
-.. warning::
-
-  Take care to always have the same :meth:`pre_setup_database`
-  function for all classes with a given `test_db_id` otherwise your
-  tests will have unpredictable results depending on the first
-  encountered one.
-
-
-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):
-            with self.admin_access.web_request() as req:
-            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, req=req)
-
-
-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 comma-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. 
-
-Literate programming
---------------------
-
-CubicWeb provides some literate programming capabilities. The :ref:`cubicweb-ctl`
-`shell` command accepts different file formats. If your file ends with `.txt`
-or `.rst`, the file will be parsed by :mod:`doctest.testfile` with CubicWeb's
-:ref:`migration` API enabled in it.
-
-Create a `scenario.txt` file in the `test/` directory and fill with some content.
-Refer to the :mod:`doctest.testfile` `documentation`_.
-
-.. _documentation: http://docs.python.org/library/doctest.html
-
-Then, you can run it directly by::
-
-    $ cubicweb-ctl shell <cube_instance> test/scenario.txt
-
-When your scenario file is ready, put it in a new test case to be able to run
-it automatically.
-
-.. sourcecode:: python
-
-      from os.path import dirname, join
-      from logilab.common.testlib import unittest_main
-      from cubicweb.devtools.testlib import CubicWebTC
-
-      class AcceptanceTC(CubicWebTC):
-
-              def test_scenario(self):
-                      self.assertDocTestFile(join(dirname(__file__), 'scenario.txt'))
-
-      if __name__ == '__main__':
-              unittest_main()
-
-Skipping a scenario
-```````````````````
-
-If you want to set up initial conditions that you can't put in your unit test
-case, you have to use a :exc:`KeyboardInterrupt` exception only because of the
-way :mod:`doctest` module will catch all the exceptions internally.
-
-    >>> if condition_not_met:
-    ...     raise KeyboardInterrupt('please, check your fixture.')
-
-Passing paramaters
-``````````````````
-Using extra arguments to parametrize your scenario is possible by prepending them
-by double dashes.
-
-Please refer to the `cubicweb-ctl shell --help` usage.
-
-.. important::
-    Your scenario file must be utf-8 encoded.
-
-Test APIS
----------
-
-Using Pytest
-````````````
-
-The `pytest` utility (shipping with `logilab-common`_, which is a
-mandatory dependency of CubicWeb) extends the Python unittest
-functionality and is the preferred way to run the CubicWeb test
-suites. Bare unittests also work the usual way.
-
-.. _logilab-common: http://www.logilab.org/project/logilab-common
-
-To use it, you may:
-
-* just launch `pytest` in your cube to execute all tests (it will
-  discover them automatically)
-* launch `pytest unittest_foo.py` to execute one test file
-* launch `pytest unittest_foo.py bar` to execute all test methods and
-  all test cases whose name contains `bar`
-
-Additionally, the `-x` option tells pytest to exit at the first error
-or failure. The `-i` option tells pytest to drop into pdb whenever an
-exception occurs in a test.
-
-When the `-x` option has been used and the run stopped on a test, it
-is possible, after having fixed the test, to relaunch pytest with the
-`-R` option to tell it to start testing again from where it previously
-failed.
-
-Using the `TestCase` base class
-```````````````````````````````
-
-The base class of CubicWebTC is logilab.common.testlib.TestCase, which
-provides a lot of convenient assertion methods.
-
-.. autoclass:: logilab.common.testlib.TestCase
-   :members:
-
-CubicWebTC API
-``````````````
-.. autoclass:: cubicweb.devtools.testlib.CubicWebTC
-   :members:
-
-
-What you need to know about request and session
------------------------------------------------
-
-.. image:: ../images/request_session.png
-
-First, remember to think that some code run on a client side, some
-other on the repository side. More precisely:
-
-* client side: web interface, raw repoapi connection (cubicweb-ctl shell for
-  instance);
-
-* repository side: RQL query execution, that may trigger hooks and operation.
-
-The client interacts with the repository through a repoapi connection.
-
-
-.. note::
-
-   These distinctions are going to disappear in cubicweb 3.21 (if not
-   before).
-
-A repoapi connection is tied to a session in the repository. The connection and
-request objects are inaccessible from repository code / the session object is
-inaccessible from client code (theoretically at least).
-
-The web interface provides a request class.  That `request` object provides
-access to all cubicweb resources, eg:
-
-* the registry (which itself provides access to the schema and the
-  configuration);
-
-* an underlying repoapi connection (when using req.execute, you actually call the
-  repoapi);
-
-* other specific resources depending on the client type (url generation according
-  to base url, form parameters, etc.).
-
-
-A `session` provides an api similar to a request regarding RQL execution and
-access to global resources (registry and all), but also has the following
-responsibilities:
-
-* handle transaction data, that will live during the time of a single
-  transaction. This includes the database connections that will be used to
-  execute RQL queries.
-
-* handle persistent data that may be used across different (web) requests
-
-* security and hooks control (not possible through a request)
-
-
-The `_cw` attribute
-```````````````````
-The `_cw` attribute available on every application object provides access to all
-cubicweb resources, i.e.:
-
-- For code running on the client side (eg web interface view), `_cw` is a request
-  instance.
-
-- For code running on the repository side (hooks and operation), `_cw` is a
-  Connection or Session instance.
-
-
-Beware some views may be called with a session (e.g. notifications) or with a
-request.
-
-
-Request, session and transaction
-````````````````````````````````
-
-In the web interface, an HTTP request is handled by a single request, which will
-be thrown away once the response is sent.
-
-The web publisher handles the transaction:
-
-* commit / rollback is done automatically
-
-* you should not commit / rollback explicitly, except if you really
-  need it
-
-Let's detail the process:
-
-1. an incoming RQL query comes from a client to the web stack
-
-2. the web stack opens an authenticated database connection for the
-   request, which is associated to a user session
-
-3. the query is executed (through the repository connection)
-
-4. this query may trigger hooks. Hooks and operations may execute some rql queries
-   through `cnx.execute`.
-
-5. the repository gets the result of the query in 1. If it was a RQL read query,
-   the database connection is released. If it was a write query, the connection
-   is then tied to the session until the transaction is commited or rolled back.
-
-6. results are sent back to the client
-
-This implies several things:
-
-* when using a request, or code executed in hooks, this database
-  connection handling is totally transparent
-
-* however, take care when writing tests: you are usually faking /
-  testing both the server and the client side, so you have to decide
-  when to use RepoAccess.client_cnx or RepoAccess.repo_cnx. Ask
-  yourself "where will the code I want to test be running, client or
-  repository side?". The response is usually: use a repo (since the
-  "client connection" concept is going away in a couple of releases).
--- a/doc/book/en/devrepo/vreg.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-The Registry, selectors and application objects
-===============================================
-
-This chapter deals with some of the  core concepts of the |cubicweb| framework
-which make it different from other frameworks (and maybe not easy to
-grasp at a first glance). To be able to do advanced development with
-|cubicweb| you need a good understanding of what is explained below.
-
-This chapter goes deep into details. You don't have to remember them
-all but keep it in mind so you can go back there later.
-
-An overview of AppObjects, the VRegistry and Selectors is given in the
-:ref:`VRegistryIntro` chapter.
-
-.. autodocstring:: cubicweb.cwvreg
-.. autodocstring:: cubicweb.predicates
-.. automodule:: cubicweb.appobject
-
-Base predicates
----------------
-
-Predicates are scoring functions that are called by the registry to tell whenever
-an appobject can be selected in a given context. Predicates may be chained
-together using operators to build a selector. A selector is the glue that tie
-views to the data model or whatever input context. Using them appropriately is an
-essential part of the construction of well behaved cubes.
-
-Of course you may have to write your own set of predicates as your needs grows
-and you get familiar with the framework (see :ref:`CustomPredicates`).
-
-Here is a description of generic predicates provided by CubicWeb that should suit
-most of your needs.
-
-Bare predicates
-~~~~~~~~~~~~~~~
-Those predicates are somewhat dumb, which doesn't mean they're not (very) useful.
-
-.. autoclass:: cubicweb.appobject.yes
-.. autoclass:: cubicweb.predicates.match_kwargs
-.. autoclass:: cubicweb.predicates.appobject_selectable
-.. autoclass:: cubicweb.predicates.adaptable
-.. autoclass:: cubicweb.predicates.configuration_values
-
-
-Result set predicates
-~~~~~~~~~~~~~~~~~~~~~
-Those predicates are looking for a result set in the context ('rset' argument or
-the input context) and match or not according to its shape. Some of these
-predicates have different behaviour if a particular cell of the result set is
-specified using 'row' and 'col' arguments of the input context or not.
-
-.. autoclass:: cubicweb.predicates.none_rset
-.. autoclass:: cubicweb.predicates.any_rset
-.. autoclass:: cubicweb.predicates.nonempty_rset
-.. autoclass:: cubicweb.predicates.empty_rset
-.. autoclass:: cubicweb.predicates.one_line_rset
-.. autoclass:: cubicweb.predicates.multi_lines_rset
-.. autoclass:: cubicweb.predicates.multi_columns_rset
-.. autoclass:: cubicweb.predicates.paginated_rset
-.. autoclass:: cubicweb.predicates.sorted_rset
-.. autoclass:: cubicweb.predicates.one_etype_rset
-.. autoclass:: cubicweb.predicates.multi_etypes_rset
-
-
-Entity predicates
-~~~~~~~~~~~~~~~~~
-Those predicates are looking for either an `entity` argument in the input context,
-or entity found in the result set ('rset' argument or the input context) and
-match or not according to entity's (instance or class) properties.
-
-.. autoclass:: cubicweb.predicates.non_final_entity
-.. autoclass:: cubicweb.predicates.is_instance
-.. autoclass:: cubicweb.predicates.score_entity
-.. autoclass:: cubicweb.predicates.rql_condition
-.. autoclass:: cubicweb.predicates.relation_possible
-.. autoclass:: cubicweb.predicates.partial_relation_possible
-.. autoclass:: cubicweb.predicates.has_related_entities
-.. autoclass:: cubicweb.predicates.partial_has_related_entities
-.. autoclass:: cubicweb.predicates.has_permission
-.. autoclass:: cubicweb.predicates.has_add_permission
-.. autoclass:: cubicweb.predicates.has_mimetype
-.. autoclass:: cubicweb.predicates.is_in_state
-.. autofunction:: cubicweb.predicates.on_fire_transition
-
-
-Logged user predicates
-~~~~~~~~~~~~~~~~~~~~~~
-Those predicates are looking for properties of the user issuing the request.
-
-.. autoclass:: cubicweb.predicates.match_user_groups
-
-
-Web request predicates
-~~~~~~~~~~~~~~~~~~~~~~
-Those predicates are looking for properties of *web* request, they can not be
-used on the data repository side.
-
-.. autoclass:: cubicweb.predicates.no_cnx
-.. autoclass:: cubicweb.predicates.anonymous_user
-.. autoclass:: cubicweb.predicates.authenticated_user
-.. autoclass:: cubicweb.predicates.match_form_params
-.. autoclass:: cubicweb.predicates.match_search_state
-.. autoclass:: cubicweb.predicates.match_context_prop
-.. autoclass:: cubicweb.predicates.match_context
-.. autoclass:: cubicweb.predicates.match_view
-.. autoclass:: cubicweb.predicates.primary_view
-.. autoclass:: cubicweb.predicates.contextual
-.. autoclass:: cubicweb.predicates.specified_etype_implements
-.. autoclass:: cubicweb.predicates.attribute_edited
-.. autoclass:: cubicweb.predicates.match_transition
-
-
-Other predicates
-~~~~~~~~~~~~~~~~
-.. autoclass:: cubicweb.predicates.match_exception
-.. autoclass:: cubicweb.predicates.debug_mode
-
-You'll also find some other (very) specific predicates hidden in other modules
-than :mod:`cubicweb.predicates`.
--- a/doc/book/en/devweb/ajax.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-.. _ajax:
-
-Ajax
-----
-
-CubicWeb provides a few helpers to facilitate *javascript <-> python* communications.
-
-You can, for instance, register some python functions that will become
-callable from javascript through ajax calls. All the ajax URLs are handled
-by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
-
-.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/controllers.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-.. _controllers:
-
-Controllers
------------
-
-Overview
-++++++++
-
-Controllers are responsible for taking action upon user requests
-(loosely following the terminology of the MVC meta pattern).
-
-The following controllers are provided out-of-the box in CubicWeb. We
-list them by category. They are all defined in
-(:mod:`cubicweb.web.views.basecontrollers`).
-
-`Browsing`:
-
-* the View controller is associated with most browsing actions within a
-  CubicWeb application: it always instantiates a
-  :ref:`the_main_template_layout` 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 JSonpController is a wrapper around the ``ViewController`` that
-  provides jsonp_ services. Padding can be specified with the
-  ``callback`` request parameter. Only *jsonexport* / *ejsonexport*
-  views can be used. If another ``vid`` is specified, it will be
-  ignored and replaced by *jsonexport*. Request is anonymized
-  to avoid returning sensitive data and reduce the risks of CSRF attacks;
-
-* the Login/Logout controllers make effective user login or logout
-  requests
-
-
-.. _jsonp: http://en.wikipedia.org/wiki/JSONP
-
-`Edition`:
-
-* the Edit controller (see :ref:`edit_controller`) handles CRUD
-  operations in response to a form being submitted; it works in close
-  association with the Forms, to which it delegates some of the work
-
-* the ``Form validator controller`` provides form validation from Ajax
-  context, using the Edit controller, to implement the classic form
-  handling loop (user edits, hits `submit/apply`, validation occurs
-  server-side by way of the Form validator controller, and the UI is
-  decorated with failure information, either global or per-field ,
-  until it is valid)
-
-`Other`:
-
-* the ``SendMail controller`` (web/views/basecontrollers.py) is reponsible
-  for outgoing email notifications
-
-* the MailBugReport controller (web/views/basecontrollers.py) allows
-  to quickly have a `reportbug` feature in one's application
-
-* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`
-  (:mod:`cubicweb.web.views.ajaxcontroller`) provides
-  services for Ajax calls, typically using JSON as a serialization format
-  for input, and sometimes using either JSON or XML for output. See
-  :ref:`ajax` chapter for more information.
-
-
-Registration
-++++++++++++
-
-All controllers (should) live in the 'controllers' namespace within
-the global registry.
-
-Concrete controllers
-++++++++++++++++++++
-
-Most API details should be resolved by source code inspection, as the
-various controllers have differing goals. See for instance the
-:ref:`edit_controller` chapter.
-
-:mod:`cubicweb.web.controller` contains the top-level abstract
-Controller class and its unimplemented entry point
-`publish(rset=None)` method.
-
-A handful of helpers are also provided there:
-
-* process_rql builds a result set from an rql query typically issued
-  from the browser (and available through _cw.form['rql'])
-
-* validate_cache will force cache validation handling with respect to
-  the HTTP Cache directives (that were typically originally issued
-  from a previous server -> client response); concrete Controller
-  implementations dealing with HTTP (thus, for instance, not the
-  SendMail controller) may very well call this in their publication
-  process.
--- a/doc/book/en/devweb/css.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-CSS Stylesheet
----------------
-Conventions
-~~~~~~~~~~~
-
-.. XXX external_resources variable
-..    naming convention
-..    request.add_css
-
-
-Extending / overriding existing styles
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We cannot modify the order in which the application is reading the CSS. In
-the case we want to create new CSS style, the best is to define it a in a new
-CSS located under ``myapp/data/`` and use those new styles while writing
-customized views and templates.
-
-If you want to modify an existing CSS styling property, you will have to use
-``!important`` declaration to override the existing property. The application
-apply a higher priority on the default CSS and you can not change that.
-Customized CSS will not be read first.
-
-
-CubicWeb stylesheets
-~~~~~~~~~~~~~~~~~~~~
-
-.. XXX explain diffenrent files and main classes
--- a/doc/book/en/devweb/edition/dissection.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,338 +0,0 @@
-
-.. _form_dissection:
-
-Dissection of an entity form
-----------------------------
-
-This is done (again) with a vanilla instance of the `tracker`_
-cube. We will populate the database with a bunch of entities and see
-what kind of job the automatic entity form does.
-
-.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
-
-Populating the database
-~~~~~~~~~~~~~~~~~~~~~~~
-
-We should start by setting up a bit of context: a project with two
-unpublished versions, and a ticket linked to the project and the first
-version.
-
-.. sourcecode:: python
-
- >>> p = rql('INSERT Project P: P name "cubicweb"')
- >>> for num in ('0.1.0', '0.2.0'):
- ...  rql('INSERT Version V: V num "%s", V version_of P WHERE P eid %%(p)s' % num, {'p': p[0][0]})
- ...
- <resultset 'INSERT Version V: V num "0.1.0", V version_of P WHERE P eid %(p)s' (1 rows): [765L] (('Version',))>
- <resultset 'INSERT Version V: V num "0.2.0", V version_of P WHERE P eid %(p)s' (1 rows): [766L] (('Version',))>
- >>> t = rql('INSERT Ticket T: T title "let us write more doc", T done_in V, '
-             'T concerns P WHERE V num "0.1.0"', P eid %(p)s', {'p': p[0][0]})
- >>> commit()
-
-Now let's see what the edition form builds for us.
-
-.. sourcecode:: python
-
- >>> cnx.use_web_compatible_requests('http://fakeurl.com')
- >>> req = cnx.request()
- >>> form = req.vreg['forms'].select('edition', req, rset=rql('Ticket T'))
- >>> html = form.render()
-
-.. note::
-
-  In order to play interactively with web side application objects, we have to
-  cheat a bit to have request object that will looks like HTTP request object, by
-  calling :meth:`use_web_compatible_requests()` on the connection.
-
-This creates an automatic entity form. The ``.render()`` call yields
-an html (unicode) string. The html output is shown below (with
-internal fieldset omitted).
-
-Looking at the html output
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The form enveloppe
-''''''''''''''''''
-
-.. sourcecode:: html
-
- <div class="iformTitle"><span>main informations</span></div>
- <div class="formBody">
-  <form action="http://crater:9999/validateform" method="post" enctype="application/x-www-form-urlencoded"
-        id="entityForm" onsubmit="return freezeFormButtons(&#39;entityForm&#39;);"
-        class="entityForm" cubicweb:target="eformframe">
-    <div id="progress">validating...</div>
-    <fieldset>
-      <input name="__form_id" type="hidden" value="edition" />
-      <input name="__errorurl" type="hidden" value="http://perdu.com#entityForm" />
-      <input name="__domid" type="hidden" value="entityForm" />
-      <input name="__type:763" type="hidden" value="Ticket" />
-      <input name="eid" type="hidden" value="763" />
-      <input name="__maineid" type="hidden" value="763" />
-      <input name="_cw_edited_fields:763" type="hidden"
-             value="concerns-subject,done_in-subject,priority-subject,type-subject,title-subject,description-subject,__type,_cw_generic_field" />
-      ...
-    </fieldset>
-   </form>
- </div>
-
-The main fieldset encloses a set of hidden fields containing various
-metadata, that will be used by the `edit controller` to process it
-back correctly.
-
-The `freezeFormButtons(...)` javascript callback defined on the
-``onlick`` event of the form element prevents accidental multiple
-clicks in a row.
-
-The ``action`` of the form is mapped to the ``validateform`` controller
-(situated in :mod:`cubicweb.web.views.basecontrollers`).
-
-A full explanation of the validation loop is given in
-:ref:`validation_process`.
-
-.. _attributes_section:
-
-The attributes section
-''''''''''''''''''''''
-
-We can have a look at some of the inner nodes of the form. Some fields
-are omitted as they are redundant for our purposes.
-
-.. sourcecode:: html
-
-      <fieldset class="default">
-        <table class="attributeForm">
-          <tr class="title_subject_row">
-            <th class="labelCol"><label class="required" for="title-subject:763">title</label></th>
-            <td>
-              <input id="title-subject:763" maxlength="128" name="title-subject:763" size="45"
-                     tabindex="1" type="text" value="let us write more doc" />
-            </td>
-          </tr>
-          ... (description field omitted) ...
-          <tr class="priority_subject_row">
-            <th class="labelCol"><label class="required" for="priority-subject:763">priority</label></th>
-            <td>
-              <select id="priority-subject:763" name="priority-subject:763" size="1" tabindex="4">
-                <option value="important">important</option>
-                <option selected="selected" value="normal">normal</option>
-                <option value="minor">minor</option>
-              </select>
-              <div class="helper">importance</div>
-            </td>
-          </tr>
-          ... (type field omitted) ...
-          <tr class="concerns_subject_row">
-            <th class="labelCol"><label class="required" for="concerns-subject:763">concerns</label></th>
-            <td>
-              <select id="concerns-subject:763" name="concerns-subject:763" size="1" tabindex="6">
-                <option selected="selected" value="760">Foo</option>
-              </select>
-            </td>
-          </tr>
-          <tr class="done_in_subject_row">
-            <th class="labelCol"><label for="done_in-subject:763">done in</label></th>
-            <td>
-              <select id="done_in-subject:763" name="done_in-subject:763" size="1" tabindex="7">
-                <option value="__cubicweb_internal_field__"></option>
-                <option selected="selected" value="761">Foo 0.1.0</option>
-                <option value="762">Foo 0.2.0</option>
-              </select>
-              <div class="helper">version in which this ticket will be / has been  done</div>
-            </td>
-          </tr>
-        </table>
-      </fieldset>
-
-
-Note that the whole form layout has been computed by the form
-renderer. It is the renderer which produces the table
-structure. Otherwise, the fields html structure is emitted by their
-associated widget.
-
-While it is called the `attributes` section of the form, it actually
-contains attributes and *mandatory relations*. For each field, we
-observe:
-
-* a dedicated row with a specific class, such as ``title_subject_row``
-  (responsability of the form renderer)
-
-* an html widget (input, select, ...) with:
-
-  * an id built from the ``rtype-role:eid`` pattern
-
-  * a name built from the same pattern
-
-  * possible values or preselected options
-
-The relations section
-'''''''''''''''''''''
-
-.. sourcecode:: html
-
-      <fieldset class="This ticket :">
-        <legend>This ticket :</legend>
-        <table class="attributeForm">
-          <tr class="_cw_generic_field_None_row">
-            <td colspan="2">
-              <table id="relatedEntities">
-                <tr><th>&#160;</th><td>&#160;</td></tr>
-                <tr id="relationSelectorRow_763" class="separator">
-                  <th class="labelCol">
-                    <select id="relationSelector_763" tabindex="8"
-                            onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,763);">
-                      <option value="">select a relation</option>
-                      <option value="appeared_in_subject">appeared in</option>
-                      <option value="custom_workflow_subject">custom workflow</option>
-                      <option value="depends_on_object">dependency of</option>
-                      <option value="depends_on_subject">depends on</option>
-                      <option value="identical_to_subject">identical to</option>
-                      <option value="see_also_subject">see also</option>
-                    </select>
-                  </th>
-                  <td id="unrelatedDivs_763"></td>
-                </tr>
-              </table>
-            </td>
-          </tr>
-        </table>
-      </fieldset>
-
-The optional relations are grouped into a drop-down combo
-box. Selection of an item triggers a javascript function which will:
-
-* show already related entities in the div of id `relatedentities`
-  using a two-colown layout, with an action to allow deletion of
-  individual relations (there are none in this example)
-
-* provide a relation selector in the div of id `relationSelector_EID`
-  to allow the user to set up relations and trigger dynamic action on
-  the last div
-
-* fill the div of id `unrelatedDivs_EID` with a dynamically computed
-  selection widget allowing direct selection of an unrelated (but
-  relatable) entity or a switch towards the `search mode` of
-  |cubicweb| which allows full browsing and selection of an entity
-  using a dedicated action situated in the left column boxes.
-
-
-The buttons zone
-''''''''''''''''
-
-Finally comes the buttons zone.
-
-.. sourcecode:: html
-
-      <table width="100%">
-        <tbody>
-          <tr>
-            <td align="center">
-              <button class="validateButton" tabindex="9" type="submit" value="validate">
-                <img alt="OK_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/ok.png" />
-                validate
-              </button>
-            </td>
-            <td style="align: right; width: 50%;">
-              <button class="validateButton"
-                      onclick="postForm(&#39;__action_apply&#39;, &#39;button_apply&#39;, &#39;entityForm&#39;)"
-                      tabindex="10" type="button" value="apply">
-                <img alt="APPLY_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/plus.png" />
-                apply
-              </button>
-              <button class="validateButton"
-                      onclick="postForm(&#39;__action_cancel&#39;, &#39;button_cancel&#39;, &#39;entityForm&#39;)"
-                      tabindex="11" type="button" value="cancel">
-                <img alt="CANCEL_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/cancel.png" />
-                cancel
-              </button>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-
-The most notable artifacts here are the ``postForm(...)`` calls
-defined on click events on these buttons. This function basically
-submits the form.
-
-.. _validation_process:
-
-The form validation process
----------------------------
-
-Preparation
-~~~~~~~~~~~
-
-After the (html) document is loaded, the ``setFormsTarget`` javascript
-function dynamically transforms the DOM as follows. For all forms of
-the DOM, it:
-
-* sets the ``target`` attribute where there is a ``cubicweb:target``
-  attribute (with the same value)
-
-* appends an empty `IFRAME` element at the end
-
-Let us have a look again at the form element. We have omitted some
-irrelevant attributes.
-
-.. sourcecode::html
-
-  <form action="http://crater:9999/validateform" method="post"
-        enctype="application/x-www-form-urlencoded"
-        id="entityForm" cubicweb:target="eformframe"
-        target="eformframe">
-  ...
-  </form>
-
-Validation loop
-~~~~~~~~~~~~~~~
-
-On form submission, the form.action is invoked. Basically, the
-``validateform`` controller is called and its output lands in the
-specified ``target``, the iframe that was previously prepared.
-
-Hence, the main page is not replaced, only the iframe contents. The
-``validateform`` controller only outputs a tiny javascript fragment
-which is then immediately executed.
-
-.. sourcecode:: html
-
- <iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0)">
-   <script type="text/javascript">
-     window.parent.handleFormValidationResponse('entityForm', null, null,
-                                                [false, [2164, {"name-subject": "required field"}], null],
-                                                null);
-   </script>
- </iframe>
-
-The ``window.parent`` part ensures the javascript function is called
-on the right context (that is: the form element). We will describe its
-parameters:
-
-* first comes the form id (`entityForm`)
-
-* then two optional callbacks for the success and failure case
-
-* an array containing:
-
-  * a boolean which indicates status (success or failure), and then, on error:
-
-    * an array structured as ``[eid, {'rtype-role': 'error msg'}, ...]``
-
-  * on success:
-
-    * an url (string) representing the next thing to jump to
-
-Given the array structure described above, it is quite simple to
-manipulate the DOM to show the errors at appropriate places.
-
-Explanation
-~~~~~~~~~~~
-
-This mecanism may seem a bit overcomplicated but we have to deal with
-two realities:
-
-* in the (strict) XHTML world, there are no iframes (hence the dynamic
-  inclusion, tolerated by Firefox)
-
-* no (or not all) browser(s) support file input field handling through
-  ajax.
--- a/doc/book/en/devweb/edition/editcontroller.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-.. _edit_controller:
-
-The `edit controller`
----------------------
-
-It can be found in (:mod:`cubicweb.web.views.editcontroller`). This
-controller processes data received from an html form to create or
-update entities.
-
-Edition handling
-~~~~~~~~~~~~~~~~
-
-The parameters related to entities to edit are specified as follows
-(first seen in :ref:`attributes_section`)::
-
-  <rtype-role>:<entity eid>
-
-where entity eid could be a letter in case of an entity to create. We
-name those parameters as *qualified*.
-
-* Retrieval of entities to edit is done by using the forms parameters
-  `eid` and `__type`
-
-* For all the attributes and the relations of an entity to edit
-  (attributes and relations are handled a bit differently but these
-  details are not much relevant here) :
-
-   * using the ``rtype``, ``role`` and ``__type`` information, fetch
-     an appropriate field instance
-
-   * check if the field has been modified (if not, proceed to the next
-     relation)
-
-   * build an rql expression to update the entity
-
-At the end, all rql expressions are executed.
-
-* For each entity to edit:
-
-   * if a qualified parameter `__linkto` is specified, its value has
-     to be a string (or a list of strings) such as: ::
-
-        <relation type>:<eids>:<target>
-
-     where <target> is either `subject` or `object` and each eid could
-     be separated from the others by a `_`. Target specifies if the
-     *edited entity* is subject or object of the relation and each
-     relation specified will be inserted.
-
-    * if a qualified parameter `__clone_eid` is specified for an entity, the
-      relations of the specified entity passed as value of this parameter are
-      copied on the edited entity.
-
-    * if a qualified parameter `__delete` is specified, its value must be
-      a string or a list of string such as follows: ::
-
-          <subjects eids>:<relation type>:<objects eids>
-
-      where each eid subject or object can be seperated from the other
-      by `_`. Each specified relation will be deleted.
-
-
-* If no entity is edited but the form contains the parameters `__linkto`
-  and `eid`, this one is interpreted by using the value specified for `eid`
-  to designate the entity on which to add the relations.
-
-.. note::
-
-   * if the parameter `__action_delete` is found, all the entities specified
-     as to be edited will be deleted.
-
-   * if the parameter `__action_cancel` is found, no action is completed.
-
-   * if the parameter `__action_apply` is found, the editing is
-     applied normally but the redirection is done on the form (see
-     :ref:`RedirectionControl`).
-
-   * if no entity is found to be edited and if there is no parameter
-     `__action_delete`, `__action_cancel`, `__linkto`, `__delete` or
-     `__insert`, an error is raised.
-
-   * using the parameter `__message` in the form will allow to use its value
-     as a message to provide the user once the editing is completed.
-
-
-.. _RedirectionControl:
-
-Redirection control
-~~~~~~~~~~~~~~~~~~~
-Once editing is completed, there is still an issue left: where should we go
-now? If nothing is specified, the controller will do his job but it does not
-mean we will be happy with the result. We can control that by using the
-following parameters:
-
-* `__redirectpath`: path of the URL (relative to the root URL of the site,
-  no form parameters
-
-* `__redirectparams`: forms parameters to add to the path
-
-* `__redirectrql`: redirection RQL request
-
-* `__redirectvid`: redirection view identifier
-
-* `__errorurl`: initial form URL, used for redirecting in case a validation
-  error is raised during editing. If this one is not specified, an error page
-  is displayed instead of going back to the form (which is, if necessary,
-  responsible for displaying the errors)
-
-* `__form_id`: initial view form identifier, used if `__action_apply` is
-  found
-
-In general we use either `__redirectpath` and `__redirectparams` or
-`__redirectrql` and `__redirectvid`.
--- a/doc/book/en/devweb/edition/examples.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,232 +0,0 @@
-Examples
---------
-
-(Automatic) Entity form
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Looking at some cubes available on the `cubicweb forge`_ we find some
-with form manipulation. The following example comes from the the
-`conference`_ cube. It extends the change state form for the case
-where a ``Talk`` entity is getting into ``submitted`` state. The goal
-is to select reviewers for the submitted talk.
-
-.. _`cubicweb forge`: http://www.cubicweb.org/view?rql=Any+P+ORDERBY+N+WHERE+P+name+LIKE+%22cubicweb-%25%22%2C+P+is+Project%2C+P+name+N
-.. _`conference`: http://www.cubicweb.org/project/cubicweb-conference
-
-.. sourcecode:: python
-
- from cubicweb.web import formfields as ff, formwidgets as fwdgs
- class SendToReviewerStatusChangeView(ChangeStateFormView):
-     __select__ = (ChangeStateFormView.__select__ &
-                   is_instance('Talk') &
-                   rql_condition('X in_state S, S name "submitted"'))
-
-     def get_form(self, entity, transition, **kwargs):
-         form = super(SendToReviewerStatusChangeView, self).get_form(entity, transition, **kwargs)
-         relation = ff.RelationField(name='reviews', role='object',
-                                     eidparam=True,
-                                     label=_('select reviewers'),
-                                     widget=fwdgs.Select(multiple=True))
-         form.append_field(relation)
-         return form
-
-Simple extension of a form can be done from within the `FormView`
-wrapping the form. FormView instances have a handy ``get_form`` method
-that returns the form to be rendered. Here we add a ``RelationField``
-to the base state change form.
-
-One notable point is the ``eidparam`` argument: it tells both the
-field and the ``edit controller`` that the field is linked to a
-specific entity.
-
-It is hence entirely possible to add ad-hoc fields that will be
-processed by some specialized instance of the edit controller.
-
-
-Ad-hoc fields form
-~~~~~~~~~~~~~~~~~~
-
-We want to define a form doing something else than editing an entity. The idea is
-to propose a form to send an email to entities in a resultset which implements
-:class:`IEmailable`.  Let's take a simplified version of what you'll find in
-:mod:`cubicweb.web.views.massmailing`.
-
-Here is the source code:
-
-.. sourcecode:: python
-
-    def sender_value(form, field):
-	return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
-
-    def recipient_choices(form, field):
-	return [(e.get_email(), e.eid)
-                 for e in form.cw_rset.entities()
-		 if e.get_email()]
-
-    def recipient_value(form, field):
-	return [e.eid for e in form.cw_rset.entities()
-                if e.get_email()]
-
-    class MassMailingForm(forms.FieldsForm):
-	__regid__ = 'massmailing'
-
-	needs_js = ('cubicweb.widgets.js',)
-	domid = 'sendmail'
-	action = 'sendmail'
-
-	sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
-				label=_('From:'),
-				value=sender_value)
-
-	recipient = ff.StringField(widget=CheckBox(),
-	                           label=_('Recipients:'),
-				   choices=recipient_choices,
-				   value=recipients_value)
-
-	subject = ff.StringField(label=_('Subject:'), max_length=256)
-
-	mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
-						    inputid='mailbody'))
-
-	form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
-				  _('send email'), 'SEND_EMAIL_ICON'),
-			ImgButton('cancelbutton', "javascript: history.back()",
-				  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
-
-Let's detail what's going on up there. Our form will hold four fields:
-
-* a sender field, which is disabled and will simply contains the user's name and
-  email
-
-* a recipients field, which will be displayed as a list of users in the context
-  result set with checkboxes so user can still choose who will receive his mailing
-  by checking or not the checkboxes. By default all of them will be checked since
-  field's value return a list containing same eids as those returned by the
-  vocabulary function.
-
-* a subject field, limited to 256 characters (hence we know a
-  :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
-  :class:`~cubicweb.web.formfields.StringField`)
-
-* a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
-  and whose definition won't be shown here. Notice though that we tell this form
-  need this javascript file by using `needs_js`
-
-Last but not least, we add two buttons control: one to post the form using
-javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
-set to 'sendmail', which is our form DOM id as specified by its `domid`
-attribute), another to cancel the form which will go back to the previous page
-using another javascript call. Also we specify an image to use as button icon as a
-resource identifier (see :ref:`uiprops`) given as last argument to
-:class:`cubicweb.web.formwidgets.ImgButton`.
-
-To see this form, we still have to wrap it in a view. This is pretty simple:
-
-.. sourcecode:: python
-
-    class MassMailingFormView(form.FormViewMixIn, EntityView):
-	__regid__ = 'massmailing'
-	__select__ = is_instance(IEmailable) & authenticated_user()
-
-	def call(self):
-	    form = self._cw.vreg['forms'].select('massmailing', self._cw,
-	                                         rset=self.cw_rset)
-	    form.render(w=self.w)
-
-As you see, we simply define a view with proper selector so it only apply to a
-result set containing :class:`IEmailable` entities, and so that only users in the
-managers or users group can use it. Then in the `call()` method for this view we
-simply select the above form and call its `.render()` method with our output
-stream as argument.
-
-When this form is submitted, a controller with id 'sendmail' will be called (as
-specified using `action`). This controller will be responsible to actually send
-the mail to specified recipients.
-
-Here is what it looks like:
-
-.. sourcecode:: python
-
-   class SendMailController(Controller):
-       __regid__ = 'sendmail'
-       __select__ = (authenticated_user() &
-                     match_form_params('recipient', 'mailbody', 'subject'))
-
-       def publish(self, rset=None):
-           body = self._cw.form['mailbody']
-           subject = self._cw.form['subject']
-           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)))
-           recipients = list(rset.entities())
-           msg = format_mail({'email' : self._cw.user.get_email(),
-                              'name' : self._cw.user.dc_title()},
-                             recipients, body, subject)
-           if not self._cw.vreg.config.sendmails([(msg, recipients)]):
-               msg = self._cw._('could not connect to the SMTP server')
-           else:
-               msg = self._cw._('emails successfully sent')
-           raise Redirect(self._cw.build_url(__message=msg))
-
-
-The entry point of a controller is the publish method. In that case we simply get
-back post values in request's `form` attribute, get user instances according
-to eids found in the 'recipient' form value, and send email after calling
-:func:`format_mail` to get a proper email message. If we can't send email or
-if we successfully sent email, we redirect to the index page with proper message
-to inform the user.
-
-Also notice that our controller has a selector that deny access to it
-to anonymous users (we don't want our instance to be used as a spam
-relay), but also checks if the expected parameters are specified in
-forms. That avoids later defensive programming (though it's not enough
-to handle all possible error cases).
-
-To conclude our example, suppose we wish a different form layout and that existent
-renderers are not satisfying (we would check that first of course :). We would then
-have to define our own renderer:
-
-.. sourcecode:: python
-
-    class MassMailingFormRenderer(formrenderers.FormRenderer):
-        __regid__ = 'massmailing'
-
-        def _render_fields(self, fields, w, form):
-            w(u'<table class="headersform">')
-            for field in fields:
-                if field.name == 'mailbody':
-                    w(u'</table>')
-                    w(u'<div id="toolbar">')
-                    w(u'<ul>')
-                    for button in form.form_buttons:
-                        w(u'<li>%s</li>' % button.render(form))
-                    w(u'</ul>')
-                    w(u'</div>')
-                    w(u'<div>')
-                    w(field.render(form, self))
-                    w(u'</div>')
-                else:
-                    w(u'<tr>')
-                    w(u'<td class="hlabel">%s</td>' %
-                      self.render_label(form, field))
-                    w(u'<td class="hvalue">')
-                    w(field.render(form, self))
-                    w(u'</td></tr>')
-
-        def render_buttons(self, w, form):
-            pass
-
-We simply override the `_render_fields` and `render_buttons` method of the base form renderer
-to arrange fields as we desire it: here we'll have first a two columns table with label and
-value of the sender, recipients and subject field (form order respected), then form controls,
-then a div containing the textarea for the email's content.
-
-To bind this renderer to our form, we should add to our form definition above:
-
-.. sourcecode:: python
-
-    form_renderer_id = 'massmailing'
-
--- a/doc/book/en/devweb/edition/form.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,377 +0,0 @@
-.. _webform:
-
-HTML form construction
-----------------------
-
-CubicWeb provides the somewhat usual form / field / widget / renderer abstraction
-to provide generic building blocks which will greatly help you in building forms
-properly integrated with CubicWeb (coherent display, error handling, etc...),
-while keeping things as flexible as possible.
-
-A ``form`` basically only holds a set of ``fields``, and has te be bound to a
-``renderer`` which is responsible to layout them. Each field is bound to a
-``widget`` that will be used to fill in value(s) for that field (at form
-generation time) and 'decode' (fetch and give a proper Python type to) values
-sent back by the browser.
-
-The ``field`` should be used according to the type of what you want to edit.
-E.g. if you want to edit some date, you'll have to use the
-:class:`cubicweb.web.formfields.DateField`. Then you can choose among multiple
-widgets to edit it, for instance :class:`cubicweb.web.formwidgets.TextInput` (a
-bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple
-calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery
-calendar).  You can of course also write your own widget.
-
-Exploring the available forms
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A small excursion into a |cubicweb| shell is the quickest way to
-discover available forms (or application objects in general).
-
-.. sourcecode:: python
-
- >>> from pprint import pprint
- >>> pprint( session.vreg['forms'] )
- {'base': [<class 'cubicweb.web.views.forms.FieldsForm'>,
-           <class 'cubicweb.web.views.forms.EntityFieldsForm'>],
-  'changestate': [<class 'cubicweb.web.views.workflow.ChangeStateForm'>,
-                  <class 'cubes.tracker.views.forms.VersionChangeStateForm'>],
-  'composite': [<class 'cubicweb.web.views.forms.CompositeForm'>,
-                <class 'cubicweb.web.views.forms.CompositeEntityForm'>],
-  'deleteconf': [<class 'cubicweb.web.views.editforms.DeleteConfForm'>],
-  'edition': [<class 'cubicweb.web.views.autoform.AutomaticEntityForm'>,
-              <class 'cubicweb.web.views.workflow.TransitionEditionForm'>,
-              <class 'cubicweb.web.views.workflow.StateEditionForm'>],
-  'logform': [<class 'cubicweb.web.views.basetemplates.LogForm'>],
-  'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>],
-  'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>],
-  'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]}
-
-
-The two most important form families here (for all practical purposes) are `base`
-and `edition`. Most of the time one wants alterations of the
-:class:`AutomaticEntityForm` to generate custom forms to handle edition of an
-entity.
-
-The Automatic Entity Form
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.web.views.autoform
-
-Anatomy of a choices function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Let's have a look at the `ticket_done_in_choices` function given to
-the `choices` parameter of the relation tag that is applied to the
-('Ticket', 'done_in', '*') relation definition, as it is both typical
-and sophisticated enough. This is a code snippet from the `tracker`_
-cube.
-
-.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
-
-The ``Ticket`` entity type can be related to a ``Project`` and a
-``Version``, respectively through the ``concerns`` and ``done_in``
-relations. When a user is about to edit a ticket, we want to fill the
-combo box for the ``done_in`` relation with values pertinent with
-respect to the context. The important context here is:
-
-* creation or modification (we cannot fetch values the same way in
-  either case)
-
-* ``__linkto`` url parameter given in a creation context
-
-.. sourcecode:: python
-
-    from cubicweb.web import formfields
-
-    def ticket_done_in_choices(form, field):
-        entity = form.edited_entity
-        # first see if its specified by __linkto form parameters
-        linkedto = form.linked_to[('done_in', 'subject')]
-        if linkedto:
-            return linkedto
-        # it isn't, get initial values
-        vocab = field.relvoc_init(form)
-        veid = None
-        # try to fetch the (already or pending) related version and project
-        if not entity.has_eid():
-            peids = form.linked_to[('concerns', 'subject')]
-            peid = peids and peids[0]
-        else:
-            peid = entity.project.eid
-            veid = entity.done_in and entity.done_in[0].eid
-        if peid:
-            # we can complete the vocabulary with relevant values
-            rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
-            rset = form._cw.execute(
-                'Any V, VN ORDERBY version_sort_value(VN) '
-                'WHERE V version_of P, P eid %(p)s, V num VN, '
-                'V in_state ST, NOT ST name "published"', {'p': peid}, 'p')
-            vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
-                      if rschema.has_perm(form._cw, 'add', toeid=v.eid)
-                      and v.eid != veid]
-        return vocab
-
-The first thing we have to do is fetch potential values from the ``__linkto`` url
-parameter that is often found in entity creation contexts (the creation action
-provides such a parameter with a predetermined value; for instance in this case,
-ticket creation could occur in the context of a `Version` entity). The
-:class:`~cubicweb.web.formfields.RelationField` field class provides a
-:meth:`~cubicweb.web.formfields.RelationField.relvoc_linkedto` method that gets a
-list suitably filled with vocabulary values.
-
-.. sourcecode:: python
-
-        linkedto = field.relvoc_linkedto(form)
-        if linkedto:
-            return linkedto
-
-Then, if no ``__linkto`` argument was given, we must prepare the vocabulary with
-an initial empty value (because `done_in` is not mandatory, we must allow the
-user to not select a verson) and already linked values. This is done with the
-:meth:`~cubicweb.web.formfields.RelationField.relvoc_init` method.
-
-.. sourcecode:: python
-
-        vocab = field.relvoc_init(form)
-
-But then, we have to give more: if the ticket is related to a project,
-we should provide all the non published versions of this project
-(`Version` and `Project` can be related through the `version_of`
-relation). Conversely, if we do not know yet the project, it would not
-make sense to propose all existing versions as it could potentially
-lead to incoherences. Even if these will be caught by some
-RQLConstraint, it is wise not to tempt the user with error-inducing
-candidate values.
-
-The "ticket is related to a project" part must be decomposed as:
-
-* this is a new ticket which is created is the context of a project
-
-* this is an already existing ticket, linked to a project (through the
-  `concerns` relation)
-
-* there is no related project (quite unlikely given the cardinality of
-  the `concerns` relation, so it can only mean that we are creating a
-  new ticket, and a project is about to be selected but there is no
-  ``__linkto`` argument)
-
-.. note::
-
-   the last situation could happen in several ways, but of course in a
-   polished application, the paths to ticket creation should be
-   controlled so as to avoid a suboptimal end-user experience
-
-Hence, we try to fetch the related project.
-
-.. sourcecode:: python
-
-        veid = None
-        if not entity.has_eid():
-            peids = form.linked_to[('concerns', 'subject')]
-            peid = peids and peids[0]
-        else:
-            peid = entity.project.eid
-            veid = entity.done_in and entity.done_in[0].eid
-
-We distinguish between entity creation and entity modification using
-the ``Entity.has_eid()`` method, which returns `False` on creation. At
-creation time the only way to get a project is through the
-``__linkto`` parameter. Notice that we fetch the version in which the
-ticket is `done_in` if any, for later.
-
-.. note::
-
-  the implementation above assumes that if there is a ``__linkto``
-  parameter, it is only about a project. While it makes sense most of
-  the time, it is not an absolute. Depending on how an entity creation
-  action action url is built, several outcomes could be possible
-  there
-
-If the ticket is already linked to a project, fetching it is
-trivial. Then we add the relevant version to the initial vocabulary.
-
-.. sourcecode:: python
-
-        if peid:
-            rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
-            rset = form._cw.execute(
-                'Any V, VN ORDERBY version_sort_value(VN) '
-                'WHERE V version_of P, P eid %(p)s, V num VN, '
-                'V in_state ST, NOT ST name "published"', {'p': peid})
-            vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
-                      if rschema.has_perm(form._cw, 'add', toeid=v.eid)
-                      and v.eid != veid]
-
-.. warning::
-
-   we have to defend ourselves against lack of a project eid. Given
-   the cardinality of the `concerns` relation, there *must* be a
-   project, but this rule can only be enforced at validation time,
-   which will happen of course only after form subsmission
-
-Here, given a project eid, we complete the vocabulary with all
-unpublished versions defined in the project (sorted by number) for
-which the current user is allowed to establish the relation.
-
-
-Building self-posted form with custom fields/widgets
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Sometimes you want a form that is not related to entity edition. For those,
-you'll have to handle form posting by yourself. Here is a complete example on how
-to achieve this (and more).
-
-Imagine you want a form that selects a month period. There are no proper
-field/widget to handle this in CubicWeb, so let's start by defining them:
-
-.. sourcecode:: python
-
-    # let's have the whole import list at the beginning, even those necessary for
-    # subsequent snippets
-    from logilab.common import date
-    from logilab.mtconverter import xml_escape
-    from cubicweb.view import View
-    from cubicweb.predicates import match_kwargs
-    from cubicweb.web import RequestError, ProcessFormError
-    from cubicweb.web import formfields as fields, formwidgets as wdgs
-    from cubicweb.web.views import forms, calendar
-
-    class MonthSelect(wdgs.Select):
-        """Custom widget to display month and year. Expect value to be given as a
-        date instance.
-        """
-
-        def format_value(self, form, field, value):
-            return u'%s/%s' % (value.year, value.month)
-
-        def process_field_data(self, form, field):
-            val = super(MonthSelect, self).process_field_data(form, field)
-            try:
-                year, month = val.split('/')
-                year = int(year)
-                month = int(month)
-                return date.date(year, month, 1)
-            except ValueError:
-                raise ProcessFormError(
-                    form._cw._('badly formated date string %s') % val)
-
-
-    class MonthPeriodField(fields.CompoundField):
-        """custom field composed of two subfields, 'begin_month' and 'end_month'.
-
-        It expects to be used on form that has 'mindate' and 'maxdate' in its
-        extra arguments, telling the range of month to display.
-        """
-
-        def __init__(self, *args, **kwargs):
-            kwargs.setdefault('widget', wdgs.IntervalWidget())
-            super(MonthPeriodField, self).__init__(
-                [fields.StringField(name='begin_month',
-                                    choices=self.get_range, sort=False,
-                                    value=self.get_mindate,
-                                    widget=MonthSelect()),
-                 fields.StringField(name='end_month',
-                                    choices=self.get_range, sort=False,
-                                    value=self.get_maxdate,
-                                    widget=MonthSelect())], *args, **kwargs)
-
-        @staticmethod
-        def get_range(form, field):
-            mindate = date.todate(form.cw_extra_kwargs['mindate'])
-            maxdate = date.todate(form.cw_extra_kwargs['maxdate'])
-            assert mindate <= maxdate
-            _ = form._cw._
-            months = []
-            while mindate <= maxdate:
-                label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]),
-                                   mindate.year)
-                value = field.widget.format_value(form, field, mindate)
-                months.append( (label, value) )
-                mindate = date.next_month(mindate)
-            return months
-
-        @staticmethod
-        def get_mindate(form, field):
-            return form.cw_extra_kwargs['mindate']
-
-        @staticmethod
-        def get_maxdate(form, field):
-            return form.cw_extra_kwargs['maxdate']
-
-        def process_posted(self, form):
-            for field, value in super(MonthPeriodField, self).process_posted(form):
-                if field.name == 'end_month':
-                    value = date.last_day(value)
-                yield field, value
-
-
-Here we first define a widget that will be used to select the beginning and the
-end of the period, displaying months like '<month> YYYY' but using 'YYYY/mm' as
-actual value.
-
-We then define a field that will actually hold two fields, one for the beginning
-and another for the end of the period. Each subfield uses the widget we defined
-earlier, and the outer field itself uses the standard
-:class:`IntervalWidget`. The field adds some logic:
-
-* a vocabulary generation function `get_range`, used to populate each sub-field
-
-* two 'value' functions `get_mindate` and `get_maxdate`, used to tell to
-  subfields which value they should consider on form initialization
-
-* overriding of `process_posted`, called when the form is being posted, so that
-  the end of the period is properly set to the last day of the month.
-
-Now, we can define a very simple form:
-
-.. sourcecode:: python
-
-    class MonthPeriodSelectorForm(forms.FieldsForm):
-        __regid__ = 'myform'
-        __select__ = match_kwargs('mindate', 'maxdate')
-
-        form_buttons = [wdgs.SubmitButton()]
-        form_renderer_id = 'onerowtable'
-        period = MonthPeriodField()
-
-
-where we simply add our field, set a submit button and use a very simple renderer
-(try others!). Also we specify a selector that ensures form will have arguments
-necessary to our field.
-
-Now, we need a view that will wrap the form and handle post when it occurs,
-simply displaying posted values in the page:
-
-.. sourcecode:: python
-
-    class SelfPostingForm(View):
-        __regid__ = 'myformview'
-
-        def call(self):
-            mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1)
-            form = self._cw.vreg['forms'].select(
-                'myform', self._cw, mindate=mindate, maxdate=maxdate, action='')
-            try:
-                posted = form.process_posted()
-                self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted)))
-            except RequestError: # no specified period asked
-                pass
-            form.render(w=self.w, formvalues=self._cw.form)
-
-
-Notice usage of the :meth:`process_posted` method, that will return a dictionary
-of typed values (because they have been processed by the field). In our case, when
-the form is posted you should see a dictionary with 'begin_month' and 'end_month'
-as keys with the selected dates as value (as a python `date` object).
-
-
-APIs
-~~~~
-
-.. automodule:: cubicweb.web.formfields
-.. automodule:: cubicweb.web.formwidgets
-.. automodule:: cubicweb.web.views.forms
-.. automodule:: cubicweb.web.views.formrenderers
-
-
--- a/doc/book/en/devweb/edition/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-Edition control
-===============
-
-This chapter covers the editing capabilities of |cubicweb|. It
-explains html Form construction, the Edit Controller and their
-interactions.
-
-
-.. toctree::
-   :maxdepth: 2
-
-   form
-   dissection
-   editcontroller
-   examples
--- a/doc/book/en/devweb/facets.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-The facets system
------------------
-
-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 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 that may be
-filtered according to some known criteria.
-
-Base classes for facets
-~~~~~~~~~~~~~~~~~~~~~~~
-.. automodule:: cubicweb.web.facet
-
-
-.. _facet: http://en.wikipedia.org/wiki/Faceted_browser
-.. _filters: http://www.cubicweb.org/blogentry/154152
-
--- a/doc/book/en/devweb/httpcaching.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-HTTP cache management
-=====================
-
-.. automodule:: cubicweb.web.httpcache
-
-Cache policies
---------------
-.. autoclass:: cubicweb.web.httpcache.NoHTTPCacheManager
-.. autoclass:: cubicweb.web.httpcache.MaxAgeHTTPCacheManager
-.. autoclass:: cubicweb.web.httpcache.EtagHTTPCacheManager
-.. autoclass:: cubicweb.web.httpcache.EntityHTTPCacheManager
-
-Exception
----------
-.. autoexception:: cubicweb.web.httpcache.NoEtag
-
-Helper functions
-----------------
-.. autofunction:: cubicweb.web.httpcache.set_http_cache_headers
-
-.. NOT YET AVAILABLE IN STABLE autofunction:: cubicweb.web.httpcache.lastmodified
--- a/doc/book/en/devweb/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-Web side development
-====================
-
-In this chapter, we will describe the core APIs for web development in
-the *CubicWeb* framework.
-
-.. toctree::
-   :maxdepth: 2
-
-   publisher
-   controllers
-   request
-   searchbar
-   views/index
-   rtags
-   ajax
-   js
-   css
-   edition/index
-   facets
-   internationalization
-   property
-   httpcaching
-   resource
--- a/doc/book/en/devweb/internationalization.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,229 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _internationalization:
-
-Internationalization
----------------------
-
-Cubicweb fully supports the internalization of its content and interface.
-
-Cubicweb's interface internationalization is based on the translation project `GNU gettext`_.
-
-.. _`GNU gettext`: http://www.gnu.org/software/gettext/
-
-Cubicweb' internalization involves two steps:
-
-* in your Python code and cubicweb-tal templates : mark translatable strings
-
-* in your instance : handle the translation catalog, edit translations
-
-String internationalization
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-User defined string
-```````````````````
-
-In the Python code and cubicweb-tal templates translatable strings can be
-marked in one of the following ways :
-
- * by using the *built-in* function `_`:
-
-   .. sourcecode:: python
-
-     class PrimaryView(EntityView):
-         """the full view of an non final entity"""
-         __regid__ = 'primary'
-         title = _('primary')
-
-  OR
-
- * by using the equivalent request's method:
-
-   .. sourcecode:: python
-
-     class NoResultView(View):
-         """default view when no result has been found"""
-         __regid__ = 'noresult'
-
-         def call(self, **kwargs):
-             self.w(u'<div class="searchMessage"><strong>%s</strong></div>\n'
-                 % self._cw._('No result matching query'))
-
-The goal of the *built-in* function `_` is only **to mark the
-translatable strings**, it will only return the string to translate
-itself, but not its translation (it's actually another name for the
-`unicode` builtin).
-
-In the other hand the request's method `self._cw._` is also meant to
-retrieve the proper translation of translation strings in the
-requested language.
-
-Finally you can also use the `__` attribute of request object to get a
-translation for a string *which should not itself added to the catalog*,
-usually in case where the actual msgid is created by string interpolation ::
-
-  self._cw.__('This %s' % etype)
-
-In this example ._cw.__` is used instead of ._cw._` so we don't have 'This %s' in
-messages catalogs.
-
-Translations in cubicweb-tal template can also be done with TAL tags
-`i18n:content` and `i18n:replace`.
-
-If you need to add messages on top of those that can be found in the source,
-you can create a file named `i18n/static-messages.pot`.
-
-You could put there messages not found in the python sources or
-overrides for some messages of used cubes.
-
-Generated string
-````````````````
-
-We do not need to mark the translation strings of entities/relations used by a
-particular instance's schema as they are generated automatically. String for
-various actions are also generated.
-
-For exemple the following schema:
-
-.. sourcecode:: python
-
-
-  class EntityA(EntityType):
-      relation_a2b = SubjectRelation('EntityB')
-
-  class EntityB(EntityType):
-      pass
-
-May generate the following message ::
-
-  add EntityA relation_a2b EntityB subject
-
-This message will be used in views of ``EntityA`` for creation of a new
-``EntityB`` with a preset relation ``relation_a2b`` between the current
-``EntityA`` and the new ``EntityB``. The opposite message ::
-
-  add EntityA relation_a2b EntityB object
-
-Is used for similar creation of an ``EntityA`` from a view of ``EntityB``. The
-title of they respective creation form will be ::
-
-  creating EntityB (EntityA %(linkto)s relation_a2b EntityB)
-
-  creating EntityA (EntityA relation_a2b %(linkto)s EntityA)
-
-In the translated string you can use ``%(linkto)s`` for reference to the source
-``entity``.
-
-Handling the translation catalog
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Once the internationalization is done in your code, you need to populate and
-update the translation catalog. Cubicweb provides the following commands for this
-purpose:
-
-
-* `i18ncubicweb` updates Cubicweb framework's translation
-  catalogs. Unless you actually work on the framework itself, you
-  don't need to use this command.
-
-* `i18ncube` updates the translation catalogs of *one particular cube*
-  (or of all cubes). After this command is executed you must update
-  the translation files *.po* in the "i18n" directory of your
-  cube. This command will of course not remove existing translations
-  still in use. It will mark unused translation but not remove them.
-
-* `i18ninstance` recompiles the translation catalogs of *one particular
-  instance* (or of all instances) after the translation catalogs of
-  its cubes have been updated. This command is automatically
-  called every time you create or update your instance. The compiled
-  catalogs (*.mo*) are stored in the i18n/<lang>/LC_MESSAGES of
-  instance where `lang` is the language identifier ('en' or 'fr'
-  for exemple).
-
-
-Example
-```````
-
-You have added and/or modified some translation strings in your cube
-(after creating a new view or modifying the cube's schema for exemple).
-To update the translation catalogs you need to do:
-
-1. `cubicweb-ctl i18ncube <cube>`
-2. Edit the <cube>/i18n/xxx.po  files and add missing translations (empty `msgstr`)
-3. `hg ci -m "updated i18n catalogs"`
-4. `cubicweb-ctl i18ninstance <myinstance>`
-
-Editing po files
-~~~~~~~~~~~~~~~~
-
-Using a PO aware editor
-````````````````````````
-
-Many tools exist to help maintain .po (PO) files. Common editors or
-development environment provides modes for these. One can also find
-dedicated PO files editor, such as `poedit`_.
-
-.. _`poedit`:  http://www.poedit.net/
-
-While usage of such a tool is commendable, PO files are perfectly
-editable with a (unicode aware) plain text editor. It is also useful
-to know their structure for troubleshooting purposes.
-
-Structure of a PO file
-``````````````````````
-
-In this section, we selectively quote passages of the `GNU gettext`_
-manual chapter on PO files, available there::
-
- http://www.gnu.org/software/hello/manual/gettext/PO-Files.html
-
-One PO file entry has the following schematic structure::
-
-     white-space
-     #  translator-comments
-     #. extracted-comments
-     #: reference...
-     #, flag...
-     #| msgid previous-untranslated-string
-     msgid untranslated-string
-     msgstr translated-string
-
-
-A simple entry can look like this::
-
-     #: lib/error.c:116
-     msgid "Unknown system error"
-     msgstr "Error desconegut del sistema"
-
-It is also possible to have entries with a context specifier. They
-look like this::
-
-     white-space
-     #  translator-comments
-     #. extracted-comments
-     #: reference...
-     #, flag...
-     #| msgctxt previous-context
-     #| msgid previous-untranslated-string
-     msgctxt context
-     msgid untranslated-string
-     msgstr translated-string
-
-
-The context serves to disambiguate messages with the same
-untranslated-string. It is possible to have several entries with the
-same untranslated-string in a PO file, provided that they each have a
-different context. Note that an empty context string and an absent
-msgctxt line do not mean the same thing.
-
-Contexts and CubicWeb
-`````````````````````
-
-CubicWeb PO files have both non-contextual and contextual msgids.
-
-Contextual entries are automatically used in some cases. For instance,
-entity.dc_type(), eschema.display_name(req) or display_name(etype,
-req, form, context) methods/function calls will use them.
-
-It is also possible to explicitly use the with _cw.pgettext(context,
-msgid).
--- a/doc/book/en/devweb/js.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,394 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Javascript
-----------
-
-*CubicWeb* uses quite a bit of javascript in its user interface and
-ships with jquery (1.3.x) and parts of the jquery UI library, plus a
-number of homegrown files and also other third party libraries.
-
-All javascript files are stored in cubicweb/web/data/. There are
-around thirty js files there. In a cube it goes to data/.
-
-Obviously one does not want javascript pieces to be loaded all at
-once, hence the framework provides a number of mechanisms and
-conventions to deal with javascript resources.
-
-Conventions
-~~~~~~~~~~~
-
-It is good practice to name cube specific js files after the name of
-the cube, like this : 'cube.mycube.js', so as to avoid name clashes.
-
-.. XXX external_resources variable (which needs love)
-
-Server-side Javascript API
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Javascript resources are typically loaded on demand, from views. The
-request object (available as self._cw from most application objects,
-for instance views and entities objects) has a few methods to do that:
-
-* `add_js(self, jsfiles, localfile=True)` which takes a sequence of
-  javascript files and writes proper entries into the HTML header
-  section. The localfile parameter allows to declare resources which
-  are not from web/data (for instance, residing on a content delivery
-  network).
-
-* `add_onload(self, jscode)` which adds one raw javascript code
-  snippet inline in the html headers. This is quite useful for setting
-  up early jQuery(document).ready(...) initialisations.
-
-Javascript events
-~~~~~~~~~~~~~~~~~
-
-* ``server-response``: this event is triggered on HTTP responses (both
-  standard and ajax). The two following extra parameters are passed
-  to callbacks :
-
-  - ``ajax``: a boolean that says if the reponse was issued by an
-    ajax request
-
-  - ``node``: the DOM node returned by the server in case of an
-    ajax request, otherwise the document itself for standard HTTP
-    requests.
-
-Important javascript AJAX APIS
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* `asyncRemoteExec` and `remoteExec` are the base building blocks for
-  doing arbitrary async (resp. sync) communications with the server
-
-* `reloadComponent` is a convenience function to replace a DOM node
-  with server supplied content coming from a specific registry (this
-  is quite handy to refresh the content of some boxes for instances)
-
-* `jQuery.fn.loadxhtml` is an important extension to jQuery which
-  allows proper loading and in-place DOM update of xhtml views. It is
-  suitably augmented to trigger necessary events, and process CubicWeb
-  specific elements such as the facet system, fckeditor, etc.
-
-
-A simple example with asyncRemoteExec
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-On the python side, we have to define an
-:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
-simplest way to do that is to use the
-:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
-details on this, refer to :ref:`ajax`).
-
-.. sourcecode: python
-
-    from cubicweb.web.views.ajaxcontroller import ajaxfunc
-
-    # serialize output to json to get it back easily on the javascript side
-    @ajaxfunc(output_type='json')
-    def js_say_hello(self, name):
-        return u'hello %s' % name
-
-On the javascript side, we do the asynchronous call. Notice how it
-creates a `deferred` object. Proper treatment of the return value or
-error handling has to be done through the addCallback and addErrback
-methods.
-
-.. sourcecode: javascript
-
-    function asyncHello(name) {
-        var deferred = asyncRemoteExec('say_hello', name);
-        deferred.addCallback(function (response) {
-            alert(response);
-        });
-        deferred.addErrback(function (error) {
-            alert('something fishy happened');
-        });
-     }
-
-     function syncHello(name) {
-         alert( remoteExec('say_hello', name) );
-     }
-
-Anatomy of a reloadComponent call
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-`reloadComponent` allows to dynamically replace some DOM node with new
-elements. It has the following signature:
-
-* `compid` (mandatory) is the name of the component to be reloaded
-
-* `rql` (optional) will be used to generate a result set given as
-  argument to the selected component
-
-* `registry` (optional) defaults to 'components' but can be any other
-  valid registry name
-
-* `nodeid` (optional) defaults to compid + 'Component' but can be any
-  explicitly specified DOM node id
-
-* `extraargs` (optional) should be a dictionary of values that will be
-  given to the cell_call method of the component
-
-A simple reloadComponent example
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The server side implementation of `reloadComponent` is the
-:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.
-
-The following function implements a two-steps method to delete a
-standard bookmark and refresh the UI, while keeping the UI responsive.
-
-.. sourcecode:: javascript
-
-    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"));
-         });
-    }
-
-`reloadComponent` is called with the id of the bookmark box as
-argument, no rql expression (because the bookmarks display is actually
-independant of any dataset context), a reference to the 'boxes'
-registry (which hosts all left, right and contextual boxes) and
-finally an explicit 'bookmarks_box' nodeid argument that stipulates
-the target DOM node.
-
-Anatomy of a loadxhtml call
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-`jQuery.fn.loadxhtml` is an important extension to jQuery which allows
-proper loading and in-place DOM update of xhtml views. The existing
-`jQuery.load`_ function does not handle xhtml, hence the addition. The
-API of loadxhtml is roughly similar to that of `jQuery.load`_.
-
-.. _`jQuery.load`: http://api.jquery.com/load/
-
-
-* `url` (mandatory) should be a complete url (typically referencing
-  the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
-  but this is not strictly mandatory)
-
-* `data` (optional) is a dictionary of values given to the
-  controller specified through an `url` argument; some keys may have a
-  special meaning depending on the choosen controller (such as `fname`
-  for the JSonController); the `callback` key, if present, must refer
-  to a function to be called at the end of loadxhtml (more on this
-  below)
-
-* `reqtype` (optional) specifies the request method to be used (get or
-  post); if the argument is 'post', then the post method is used,
-  otherwise the get method is used
-
-* `mode` (optional) is one of `replace` (the default) which means the
-  loaded node will replace the current node content, `swap` to replace
-  the current node with the loaded node, and `append` which will
-  append the loaded node to the current node content
-
-About the `callback` option:
-
-* it is called with two parameters: the current node, and a list
-  containing the loaded (and post-processed node)
-
-* whenever it returns another function, this function is called in
-  turn with the same parameters as above
-
-This mechanism allows callback chaining.
-
-
-A simple example with loadxhtml
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here we are concerned with the retrieval of a specific view to be
-injected in the live DOM. The view will be of course selected
-server-side using an entity eid provided by the client side.
-
-.. sourcecode:: python
-
-    from cubicweb.web.views.ajaxcontroller import ajaxfunc
-
-    @ajaxfunc(output_type='xhtml')
-    def frob_status(self, eid, frobname):
-        entity = self._cw.entity_from_eid(eid)
-        return entity.view('frob', name=frobname)
-
-.. sourcecode:: javascript
-
-    function updateSomeDiv(divid, eid, frobname) {
-        var params = {fname:'frob_status', eid: eid, frobname:frobname};
-        jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
-     }
-
-In this example, the url argument is the base json url of a cube
-instance (it should contain something like
-`http://myinstance/ajax?`). The actual AjaxController method name is
-encoded in the `params` dictionary using the `fname` key.
-
-A more real-life example
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-A frequent need of Web 2 applications is the delayed (or demand
-driven) loading of pieces of the DOM. This is typically achieved using
-some preparation of the initial DOM nodes, jQuery event handling and
-proper use of loadxhtml.
-
-We present here a skeletal version of the mecanism used in CubicWeb
-and available in web/views/tabs.py, in the `LazyViewMixin` class.
-
-.. sourcecode:: python
-
-    def lazyview(self, vid, rql=None):
-        """ a lazy version of wview """
-        w = self.w
-        self._cw.add_js('cubicweb.lazy.js')
-        urlparams = {'vid' : vid, 'fname' : 'view'}
-        if rql is not None:
-            urlparams['rql'] = rql
-        w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
-            vid, xml_escape(self._cw.build_url('json', **urlparams))))
-        w(u'</div>')
-        self._cw.add_onload(u"""
-            jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
-                   loadNow('#lazy-%(vid)s');});"""
-            % {'event': 'load_%s' % vid, 'vid': vid})
-
-This creates a `div` with a specific event associated to it.
-
-The full version deals with:
-
-* optional parameters such as an entity eid, an rset
-
-* the ability to further reload the fragment
-
-* the ability to display a spinning wheel while the fragment is still
-  not loaded
-
-* handling of browsers that do not support ajax (search engines,
-  text-based browsers such as lynx, etc.)
-
-The javascript side is quite simple, due to loadxhtml awesomeness.
-
-.. sourcecode:: javascript
-
-    function loadNow(eltsel) {
-        var lazydiv = jQuery(eltsel);
-        lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
-    }
-
-This is all significantly different of the previous `simple example`
-(albeit this example actually comes from real-life code).
-
-Notice how the `cubicweb:loadurl` is used to convey the url
-information. The base of this url is similar to the global javascript
-JSON_BASE_URL. According to the pattern described earlier,
-the `fname` parameter refers to the standard `js_view` method of the
-JSonController. This method renders an arbitrary view provided a view
-id (or `vid`) is provided, and most likely an rql expression yielding
-a result set against which a proper view instance will be selected.
-
-The `cubicweb:loadurl` is one of the 29 attributes extensions to XHTML
-in a specific cubicweb namespace. It is a means to pass information
-without breaking HTML nor XHTML compliance and without resorting to
-ungodly hacks.
-
-Given all this, it is easy to add a small nevertheless useful feature
-to force the loading of a lazy view (for instance, a very
-computation-intensive web page could be scinded into one fast-loading
-part and a delayed part).
-
-On the server side, a simple call to a javascript function is
-sufficient.
-
-.. sourcecode:: python
-
-    def forceview(self, vid):
-        """trigger an event that will force immediate loading of the view
-        on dom readyness
-        """
-        self._cw.add_onload("triggerLoad('%s');" % vid)
-
-The browser-side definition follows.
-
-.. sourcecode:: javascript
-
-    function triggerLoad(divid) {
-        jQuery('#lazy-' + divd).trigger('load_' + divid);
-    }
-
-
-Javascript library: overview
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* jquery.* : jquery and jquery UI library
-
-* cubicweb.ajax.js : concentrates all ajax related facilities (it
-  extends jQuery with the loahxhtml function, provides a handfull of
-  high-level ajaxy operations like asyncRemoteExec, reloadComponent,
-  replacePageChunk, getDomFromResponse)
-
-* cubicweb.python.js : adds a number of practical extension to stdanrd
-  javascript objects (on Date, Array, String, some list and dictionary
-  operations), and a pythonesque way to build classes. Defines a
-  CubicWeb namespace.
-
-* cubicweb.htmlhelpers.js : a small bag of convenience functions used
-  in various other cubicweb javascript resources (baseuri, progress
-  cursor handling, popup login box, html2dom function, etc.)
-
-* cubicweb.widgets.js : provides a widget namespace and constructors
-  and helpers for various widgets (mainly facets and timeline)
-
-* cubicweb.edition.js : used by edition forms
-
-* cubicweb.preferences.js : used by the preference form
-
-* cubicweb.facets.js : used by the facets mechanism
-
-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/property.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-.. _cwprops:
-
-The property mecanism
----------------------
-
-.. XXX CWProperty and co
-
-
-Property API
-~~~~~~~~~~~~
-.. XXX feed me
-
-Registering and using your own property
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. XXX feed me
--- a/doc/book/en/devweb/publisher.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-.. _publisher:
-
-Publisher
----------
-
-What happens when an HTTP request is issued ?
-
-The story begins with the ``CubicWebPublisher.main_publish``
-method. We do not get upper in the bootstrap process because it is
-dependant on the used HTTP library. With `twisted`_ however,
-``cubicweb.etwist.server.CubicWebRootResource.render_request`` is the
-real entry point.
-
-.. _`twisted`: http://twistedmatrix.com/trac/
-
-What main_publish does:
-
-* get a controller id and a result set from the path (this is actually
-  delegated to the `urlpublisher` component)
-
-* the controller is then selected (if not, this is considered an
-  authorization failure and signaled as such) and called
-
-* then either a proper result is returned, in which case the
-  request/connection object issues a ``commit`` and returns the result
-
-* or error handling must happen:
-
-  * ``ValidationErrors`` pop up there and may lead to a redirect to a
-    previously arranged url or standard error handling applies
-  * an HTTP 500 error (`Internal Server Error`) is issued
-
-
-Now, let's turn to the controller. There are many of them in
-:mod:`cubicweb.web.views.basecontrollers`. We can just follow the
-default `view` controller that is selected on a `view` path. See the
-:ref:`controllers` chapter for more information on controllers.
-
-The `View` controller's entry point is the `publish` method. It does
-the following:
-
-* compute the `main` view to be applied, using either the given result
-  set or building one from a user provided rql string (`rql` and `vid`
-  can be forced from the url GET parameters), that is:
-
-    * compute the `vid` using the result set and the schema (see
-      `cubicweb.web.views.vid_from_rset`)
-    * handle all error cases that could happen in this phase
-
-* do some cache management chores
-
-* select a main template (typically `TheMainTemplate`, see chapter
-  :ref:`templates`)
-
-* call it with the result set and the computed view.
-
-What happens next actually depends on the template and the view, but
-in general this is the rendering phase.
-
-
-CubicWebPublisher API
-`````````````````````
-
-.. autoclass:: cubicweb.web.application.CubicWebPublisher
-   :members:
--- a/doc/book/en/devweb/request.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-The `Request` class (`cubicweb.web.request`)
---------------------------------------------
-
-Overview
-````````
-
-A request instance is created when an HTTP request is sent to the web
-server.  It contains informations such as form parameters,
-authenticated user, etc. It is a very prevalent object and is used
-throughout all of the framework and applications, as you'll access to
-almost every resources through it.
-
-**A request represents a user query, either through HTTP or not (we
-also talk about RQL queries on the server side for example).**
-
-Here is a non-exhaustive list of attributes and methods available on
-request objects (grouped by category):
-
-* `Browser control`:
-
-  * `ie_browser`: tells if the browser belong to the Internet Explorer
-    family
-
-* `User and identification`:
-
-  * `user`, instance of `cubicweb.entities.authobjs.CWUser` corresponding to the
-    authenticated user
-
-* `Session data handling`
-
-  * `session.data` is the dictionary of the session data; it can be
-    manipulated like an ordinary Python dictionary
-
-* `Edition` (utilities for edition control):
-
-  * `cancel_edition`: resets error url and cleans up pending operations
-  * `create_entity`: utility to create an entity (from an etype,
-    attributes and relation values)
-  * `datadir_url`: returns the url to the merged external resources
-    (|cubicweb|'s `web/data` directory plus all `data` directories of
-    used cubes)
-  * `edited_eids`: returns the list of eids of entities that are
-    edited under the current http request
-  * `eid_rset(eid)`: utility which returns a result set from an eid
-  * `entity_from_eid(eid)`: returns an entity instance from the given eid
-  * `encoding`: returns the encoding of the current HTTP request
-  * `ensure_ro_rql(rql)`: ensure some rql query is a data request
-  * etype_rset
-  * `form`, dictionary containing the values of a web form
-  * `encoding`, character encoding to use in the response
-  * `next_tabindex()`: returns a monotonically growing integer used to
-    build the html tab index of forms
-
-* `HTTP`
-
-  * `authmode`: returns a string describing the authentication mode
-    (http, cookie, ...)
-  * `lang`: returns the user agents/browser's language as carried by
-    the http request
-  * `demote_to_html()`: in the context of an XHTML compliant browser,
-    this will force emission of the response as an HTML document
-    (using the http content negociation)
-
-*  `Cookies handling`
-
-  * `get_cookie()`, returns a dictionary containing the value of the header
-    HTTP 'Cookie'
-  * `set_cookie(cookie, key, maxage=300)`, adds a header HTTP `Set-Cookie`,
-    with a minimal 5 minutes length of duration by default (`maxage` = None
-    returns a *session* cookie which will expire when the user closes the browser
-    window)
-  * `remove_cookie(cookie, key)`, forces a value to expire
-
-* `URL handling`
-
-  * `build_url(__vid, *args, **kwargs)`: 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.
-  * `build_url_params(**kwargs)`: returns a properly prepared (quoted,
-    separators, ...) string from the given parameters
-  * `url()`, returns the full URL of the HTTP request
-  * `base_url()`, returns the root URL of the web application
-  * `relative_path()`, returns the relative path of the request
-
-* `Web resource (.css, .js files, etc.) handling`:
-
-  * `add_css(cssfiles)`: adds the given list of css resources to the current
-    html headers
-  * `add_js(jsfiles)`: adds the given list of javascript resources to the
-    current html headers
-  * `add_onload(jscode)`: inject the given jscode fragment (an unicode
-    string) into the current html headers, wrapped inside a
-    document.ready(...) or another ajax-friendly one-time trigger event
-  * `add_header(header, values)`: adds the header/value pair to the
-    current html headers
-  * `status_out`: control the HTTP status of the response
-
-* `And more...`
-
-  * `set_content_type(content_type, filename=None)`, adds the header HTTP
-    'Content-Type'
-  * `get_header(header)`, returns the value associated to an arbitrary header
-    of the HTTP request
-  * `set_header(header, value)`, adds an arbitrary header in the response
-  * `execute(*args, **kwargs)`, executes an RQL query and return the result set
-  * `property_value(key)`, properties management (`CWProperty`)
-  * dictionary `data` to store data to share informations between components
-    *while a request is executed*
-
-Please note that this class is abstract and that a concrete implementation
-will be provided by the *frontend* web used (in particular *twisted* as of
-today). For the views or others that are executed on the server side,
-most of the interface of `Request` is defined in the session associated
-to the client.
-
-API
-```
-
-The elements we gave in overview for above are built in three layers,
-from ``cubicweb.req.RequestSessionBase``, ``cubicweb.repoapi.ClientConnection`` and
-``cubicweb.web.ConnectionCubicWebRequestBase``.
-
-.. autoclass:: cubicweb.req.RequestSessionBase
-   :members:
-
-.. autoclass:: cubicweb.repoapi.ClientConnection
-   :members:
-
-.. autoclass:: cubicweb.web.request.ConnectionCubicWebRequestBase
-   :members:
--- a/doc/book/en/devweb/resource.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-.. _resources:
-
-Locate resources
-----------------
-
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_resource
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_doc_file
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_all_files
-
-Static files handling
----------------------
-
-.. autoattribute:: cubicweb.web.webconfig.WebConfiguration.static_directory
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_exists
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_open
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_add
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_del
-
--- a/doc/book/en/devweb/rtags.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-Configuring the user interface
-------------------------------
-
-.. _relation_tags:
-
-Relation tags
-~~~~~~~~~~~~~
-.. automodule:: cubicweb.rtags
-
-.. _uicfg:
-
-The uicfg module
-~~~~~~~~~~~~~~~~
-
-.. note::
-
- The part of uicfg that deals with primary views is in the
- :ref:`primary_view_configuration` chapter.
-
-.. automodule:: cubicweb.web.views.uicfg
-
-
-The uihelper module
-~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.web.uihelper
-
--- a/doc/book/en/devweb/searchbar.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-.. _searchbar:
-
-RQL search bar
---------------
-
-The RQL search bar is a visual component, hidden by default, the tiny *search*
-input being enough for common use cases.
-
-An autocompletion helper is provided to help you type valid queries, both
-in terms of syntax and in terms of schema validity.
-
-.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder
-
-
-How search is performed
-+++++++++++++++++++++++
-
-You can use the *rql search bar* to either type RQL queries, plain text queries
-or standard shortcuts such as *<EntityType>* or *<EntityType> <attrname> <value>*.
-
-Ultimately, all queries are translated to rql since it's the only
-language understood on the server (data) side. To transform the user
-query into RQL, CubicWeb uses the so-called *magicsearch component*,
-defined in :mod:`cubicweb.web.views.magicsearch`, which in turn
-delegates to a number of query preprocessor that are responsible of
-interpreting the user query and generating corresponding RQL.
-
-The code of the main processor loop is easy to understand:
-
-.. sourcecode:: python
-
-  for proc in self.processors:
-      try:
-          return proc.process_query(uquery, req)
-      except (RQLSyntaxError, BadRQLQuery):
-          pass
-
-The idea is simple: for each query processor, try to translate the
-query. If it fails, try with the next processor, if it succeeds,
-we're done and the RQL query will be executed.
-
--- a/doc/book/en/devweb/views/basetemplates.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _templates:
-
-Templates
-=========
-
-Templates are the entry point for the |cubicweb| view system. As seen
-in :ref:`views_base_class`, there are two kinds of views: the
-templatable and non-templatable.
-
-
-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:
-
-* set up the proper document header and content type
-* define the general layout of a document
-* 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 :class:`~cubicweb.web.views.basetemplates.TheMainTemplate`.
-
-.. _the_main_template_layout:
-
-TheMainTemplate
-~~~~~~~~~~~~~~~
-
-.. _the_main_template_sections:
-
-Layout and sections
-```````````````````
-
-A page is composed as indicated on the schema below :
-
-.. image:: ../../images/main_template.png
-
-The sections dispatches specific views:
-
-* `header`: the rendering of the header is delegated to the
-  `htmlheader` view, whose default implementation can be found in
-  ``basetemplates.py`` and which does the following things:
-
-    * inject the favicon if there is one
-    * inject the global style sheets and javascript resources
-    * call and display a link to an rss component if there is one available
-
-  it also sets up the page title, and fills the actual
-  `header` section with top-level components, using the `header` view, which:
-
-    * tries to display a logo, the name of the application and the `breadcrumbs`
-    * provides a login status area
-    * provides a login box (hiden by default)
-
-* `left column`: this is filled with all selectable boxes matching the
-  `left` context (there is also a right column but nowadays it is
-  seldom used due to bad usability)
-
-* `contentcol`: this is the central column; it is filled with:
-
-    * the `rqlinput` view (hidden by default)
-    * the `applmessages` component
-    * the `contentheader` view which in turns dispatches all available
-      content navigation components having the `navtop` context (this
-      is used to navigate through entities implementing the IPrevNext
-      interface)
-    * the view that was given as input to the template's `call`
-      method, also dealing with pagination concerns
-    * the `contentfooter`
-
-* `footer`: adds all footer actions
-
-.. note::
-
-  How and why a view object is given to the main template is explained
-  in the :ref:`publisher` chapter.
-
-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.
-
-* `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 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)
-
-* `vtitle`, a title to be set as <h1> of the content
-
-Other templates
-~~~~~~~~~~~~~~~
-
-There are also the following other standard templates:
-
-* :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	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-Base views
-----------
-
-|cubicweb| provides a lot of standard views, that can be found in
-: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. As other appobjects, their identifier is what distinguish them
-from each others. The most generic ones, found in
-:mod:`cubicweb.web.views.baseviews`, are described below.
-
-You'll probably want to customize one or more of the described views which are
-default, generic, implementations.
-
-
-.. automodule:: cubicweb.web.views.baseviews
-
-You will also find modules providing some specific services:
-
-.. automodule:: cubicweb.web.views.navigation
-
--- a/doc/book/en/devweb/views/boxes.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-Boxes
------
-
-(:mod:`cubicweb.web.views.boxes`)
-
-*sidebox*
-  This view displays usually a side box of some related entities
-  in a primary view.
-
-The action box
-~~~~~~~~~~~~~~~
-
-The ``add_related`` is an automatic menu in the action box that allows to create
-an entity automatically related to the initial entity (context in
-which the box is displayed). By default, the links generated in this
-box are computed from the schema properties of the displayed entity,
-but it is possible to explicitly specify them thanks to the
-`cubicweb.web.views.uicfg.rmode` *relation tag*:
-
-* `link`, indicates that a relation is in general created pointing
-  to an existing entity and that we should not to display a link
-  for this relation
-
-* `create`, indicates that a relation is in general created pointing
-  to new entities and that we should display a link to create a new
-  entity and link to it automatically
-
-
-If necessary, it is possible to overwrite the method
-`relation_mode(rtype, targettype, x='subject')` to dynamically
-compute a relation creation category.
-
-Please note that if at least one action belongs to the `addrelated` category,
-the automatic behavior is desactivated in favor of an explicit behavior
-(e.g. display of `addrelated` category actions only).
-
--- a/doc/book/en/devweb/views/breadcrumbs.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-Breadcrumbs
------------
-
-Breadcrumbs are a navigation component to help the user locate himself
-along a path of entities.
-
-Display
-~~~~~~~
-
-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 they begin with a
-separator.
-
-Here is the header section of the CubicWeb's forge:
-
-.. image:: ../../images/breadcrumbs_header.png
-
-There are three breadcrumbs components defined in
-:mod:`cubicweb.web.views.ibreadcrumbs`:
-
-- `BreadCrumbEntityVComponent`: displayed for a result set with one line
-  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 can adapt to
-  ``IBreadCrumbsAdapter``.
-- `BreadCrumbAnyRSetVComponent`: displayed for any other result set.
-
-Building breadcrumbs
-~~~~~~~~~~~~~~~~~~~~
-
-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 an `ITreeAdapter` adapter on an entity type. If
-   available, it will be used to compute breadcrumbs.
-
-Here is the API of the ``IBreadCrumbsAdapter`` class:
-
-.. 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
-the elements.
-
-By default, for any entity, if recurs=True, breadcrumbs method returns
-a list of entities, else a list of a simple string.
-
-In order to see a hierarchical breadcrumbs, entities must have a
-``parent`` method which returns the parent entity. By default this
-method doesn't exist on entity, given that it can not be guessed.
--- a/doc/book/en/devweb/views/idownloadable.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-The 'download' views
-====================
-
-.. automodule:: cubicweb.web.views.idownloadable
-
-Components
-----------
-
-.. autoclass:: cubicweb.web.views.idownloadable.DownloadBox
-
-Download views
---------------
-
-.. autoclass:: cubicweb.web.views.idownloadable.DownloadView
-.. autoclass:: cubicweb.web.views.idownloadable.DownloadLinkView
-.. autoclass:: cubicweb.web.views.idownloadable.IDownloadablePrimaryView
-
-Embedded views
---------------
-
-.. autoclass:: cubicweb.web.views.idownloadable.ImageView
-.. autoclass:: cubicweb.web.views.idownloadable.EHTMLView
--- a/doc/book/en/devweb/views/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-The View system
-===============
-
-This chapter aims to describe the concept of a `view` used all along
-the development of a web application and how it has been implemented
-in |cubicweb|.
-
-
-.. toctree::
-   :maxdepth: 3
-
-   views
-   basetemplates
-   primary
-   reledit
-   baseviews
-   startup
-   boxes
-   table
-   xmlrss
-   urlpublish
-   breadcrumbs
-   idownloadable
-   wdoc
-
-..   editforms
-..   embedding
-
--- a/doc/book/en/devweb/views/primary.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +0,0 @@
-.. _primary_view:
-
-The Primary View
------------------
-
-By default, *CubicWeb* provides a view that fits every available
-entity type. This is the first view you might be interested in
-modifying. It is also one of the richest and most complex.
-
-It is automatically selected on a one line result set containing an
-entity.
-
-It lives in the :mod:`cubicweb.web.views.primary` module.
-
-The *primary* view is supposed to render a maximum of informations about the
-entity.
-
-.. _primary_view_layout:
-
-Layout
-``````
-
-The primary view has the following layout.
-
-.. image:: ../../images/primaryview_template.png
-
-.. _primary_view_configuration:
-
-Primary view configuration
-``````````````````````````
-
-If you want to customize the primary view of an entity, overriding the primary
-view class may not be necessary. For simple adjustments (attributes or relations
-display locations and styles), a much simpler way is to use uicfg.
-
-Attributes/relations display location
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-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** can only be displayed in the attributes section (default
-  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:
-
-.. sourcecode:: python
-
-   from cubicweb.web.views import uicfg
-   uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
-
-**Relations** can be either displayed in one of the three sections or hidden.
-
-For relations, there are two methods:
-
-* ``tag_object_of`` for modifying the primary view of the object
-* ``tag_subject_of`` for modifying the primary view of the subject
-
-These two methods take two arguments:
-
-* a triplet ``(subject, relation_name, object)``, where subject or object can be replaced with ``'*'``
-* the section name or ``hidden``
-
-.. sourcecode:: python
-
-   pv_section = uicfg.primaryview_section
-   # hide every relation `entry_of` in the `Blog` primary view
-   pv_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
-
-   # display `entry_of` relations in the `relations`
-   # section in the `BlogEntry` primary view
-   pv_section.tag_subject_of(('BlogEntry', 'entry_of', '*'), 'relations')
-
-
-Display content
-^^^^^^^^^^^^^^^
-
-You can use ``primaryview_display_ctrl`` to customize the display of attributes
-or relations. Values of ``primaryview_display_ctrl`` are dictionaries.
-
-
-Common keys for attributes and relations are:
-
-* ``vid``: specifies the regid of the view for displaying the attribute or the relation.
-
-  If ``vid`` is not specified, the default value depends on the section:
-    * ``attributes`` section: 'reledit' view
-    * ``relations`` section: 'autolimited' view
-    * ``sideboxes`` section: 'sidebox' view
-
-* ``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
-   class BlogEntry(EntityType):
-       title = String(required=True, fulltextindexed=True, maxsize=256)
-       publish_date = Date(default='TODAY')
-       content = String(required=True, fulltextindexed=True)
-       entry_of = SubjectRelation('Blog', cardinality='?*')
-
-   # now, we want to show attributes
-   # with an order different from that in the schema definition
-   view_ctrl = uicfg.primaryview_display_ctrl
-   for index, attr in enumerate('title', 'content', 'publish_date'):
-       view_ctrl.tag_attribute(('BlogEntry', attr), {'order': index})
-
-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).
-
-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.
-
-* `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)
-
-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.
-
-
-.. sourcecode:: python
-
-   pv_section = uicfg.primaryview_section
-   # in `CWUser` primary view, display `created_by`
-   # relations in relations section
-   pv_section.tag_object_of(('*', 'created_by', 'CWUser'), 'relations')
-
-   # display this relation as a list, sets the label,
-   # limit the number of results and filters on comments
-   def filter_comment(rset):
-       return rset.filtered_rset(lambda x: x.e_schema == 'Comment')
-   pv_ctrl = uicfg.primaryview_display_ctrl
-   pv_ctrl.tag_object_of(('*', 'created_by', 'CWUser'),
-                         {'vid': 'list', 'label': _('latest comment(s):'),
-                          'limit': True,
-                          'filter': filter_comment})
-
-.. warning:: with the ``primaryview_display_ctrl`` rtag, the subject or the
-   object of the relation is ignored for respectively ``tag_object_of`` or
-   ``tag_subject_of``. To avoid warnings during execution, they should be set to
-   ``'*'``.
-
-
-.. automodule:: cubicweb.web.views.primary
-
-
-Example of customization and creation
-`````````````````````````````````````
-
-We'll show you now an example of a ``primary`` view and how to customize it.
-
-If you want to change the way a ``BlogEntry`` is displayed, just
-override the method ``cell_call()`` of the view ``primary`` in
-``BlogDemo/views.py``.
-
-.. sourcecode:: python
-
-   from cubicweb.predicates import is_instance
-   from cubicweb.web.views.primary import Primaryview
-
-   class BlogEntryPrimaryView(PrimaryView):
-       __select__ = PrimaryView.__select__ & is_instance('BlogEntry')
-
-       def render_entity_attributes(self, entity):
-           self.w(u'<p>published on %s</p>' %
-                  entity.publish_date.strftime('%Y-%m-%d'))
-           super(BlogEntryPrimaryView, self).render_entity_attributes(entity)
-
-
-The above source code defines a new primary view for
-``BlogEntry``. The `__reid__` class attribute is not repeated there since it
-is inherited through the `primary.PrimaryView` class.
-
-The selector for this view chains the selector of the inherited class
-with its own specific criterion.
-
-The view method ``self.w()`` is used to output data. Here `lines
-08-09` output HTML for the publication date of the entry.
-
-.. image:: ../../images/lax-book_09-new-view-blogentry_en.png
-   :alt: blog entries now look much nicer
-
-Let us now improve the primary view of a blog
-
-.. sourcecode:: python
-
- from logilab.mtconverter import xml_escape
- from cubicweb.predicates import is_instance, one_line_rset
- from cubicweb.web.views.primary import Primaryview
-
- class BlogPrimaryView(PrimaryView):
-     __regid__ = 'primary'
-     __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):
-         rset = self._cw.execute(self.rql, {'b' : entity.eid})
-         for entry in rset.entities():
-             self.w(u'<p>%s</p>' % entry.view('inblogcontext'))
-
- class BlogEntryInBlogView(EntityView):
-     __regid__ = 'inblogcontext'
-     __select__ = is_instance('BlogEntry')
-
-     def cell_call(self, row, col):
-         entity = self.cw_rset.get_entity(row, col)
-         self.w(u'<a href="%s" title="%s">%s</a>' %
-                entity.absolute_url(),
-                xml_escape(entity.content[:50]),
-                xml_escape(entity.description))
-
-This happens in two places. First we override the
-render_entity_relations method of a Blog's primary view. Here we want
-to display our blog entries in a custom way.
-
-At `line 10`, a simple request is made to build a result set with all
-the entities linked to the current ``Blog`` entity by the relationship
-``entry_of``. The part of the framework handling the request knows
-about the schema and infers that such entities have to be of the
-``BlogEntry`` kind and retrieves them (in the prescribed publish_date
-order).
-
-The request returns a selection of data called a result set. Result
-set objects have an .entities() method returning a generator on
-requested entities (going transparently through the `ORM` layer).
-
-At `line 13` the view 'inblogcontext' is applied to each blog entry to
-output HTML. (Note that the 'inblogcontext' view is not defined
-whatsoever in *CubicWeb*. You are absolutely free to define whole view
-families.) We juste arrange to wrap each blogentry output in a 'p'
-html element.
-
-Next, we define the 'inblogcontext' view. This is NOT a primary view,
-with its well-defined sections (title, metadata, attribtues,
-relations/boxes). All a basic view has to define is cell_call.
-
-Since views are applied to result sets which can be tables of data, we
-have to recover the entity from its (row,col)-coordinates (`line
-20`). Then we can spit some HTML.
-
-.. warning::
-
-  Be careful: all strings manipulated in *CubicWeb* are actually
-  unicode strings. While web browsers are usually tolerant to
-  incoherent encodings they are being served, we should not abuse
-  it. Hence we have to properly escape our data. The xml_escape()
-  function has to be used to safely fill (X)HTML elements from Python
-  unicode strings.
-
-Assuming we added entries to the blog titled `MyLife`, displaying it
-now allows to read its description and all its entries.
-
-.. image:: ../../images/lax-book_10-blog-with-two-entries_en.png
-   :alt: a blog and all its entries
-
--- a/doc/book/en/devweb/views/reledit.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-.. _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.views.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.views.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.
-
-
-Disable `reledit`
-*****************
-
-By default, `reledit` is available on attributes and relations displayed in
-the 'attribute' section of the default primary view.  If you want to disable
-it for some attribute or relation, you have use `uicfg`:
-
-.. sourcecode:: python
-
-    from cubicweb.web.views.uicfg import primaryview_display_ctrl as _pvdc
-    _pvdc.tag_attribute(('Company', 'name'), {'vid': 'incontext'})
-
-To deactivate it everywhere it's used automatically, you may use the code snippet
-below somewhere in your cube's views:
-
-.. sourcecode:: python
-
-    from cubicweb.web.views import reledit
-
-    class DeactivatedAutoClickAndEditFormView(reledit.AutoClickAndEditFormView):
-	def _should_edit_attribute(self, rschema):
-	    return False
-
-	def _should_edit_attribute(self, rschema, role):
-	    return False
-
-    def registration_callback(vreg):
-	vreg.register_and_replace(DeactivatedAutoClickAndEditFormView,
-				  reledit.AutoClickAndEditFormView)
-
-
--- a/doc/book/en/devweb/views/startup.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-Startup views
--------------
-
-Startup views are views requiring no context, from which you usually start
-browsing (for instance the index page). The usual selectors are
-:class:`~cubicweb.predicates.none_rset` or :class:`~logilab.common.registry.yes`.
-
-You'll find here a description of startup views provided by the framework.
-
-.. automodule:: cubicweb.web.views.startup
-
-
-Other startup views:
-
-*schema*
-    A view dedicated to the display of the schema of the instance
-
-.. XXX to be continued
\ No newline at end of file
--- a/doc/book/en/devweb/views/table.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-Table views
------------
-
-.. automodule:: cubicweb.web.views.tableview
-
-Example
-```````
-
-Let us take an example from the timesheet cube:
-
-.. sourcecode:: python
-
-    class ActivityResourcesTable(EntityView):
-        __regid__ = 'activity.resources.table'
-        __select__ = is_instance('Activity')
-
-        def call(self, showresource=True):
-            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)
-            rset = self._cw.execute(rql)
-            self.wview('resource.table', rset, 'null')
-
-    class ResourcesTable(RsetTableView):
-        __regid__ = 'resource.table'
-        # notice you may wish a stricter selector to check rql's shape
-        __select__ = is_instance('Resource')
-        # my table headers
-        headers  = ['Resource', 'diem', 'duration', 'workpackage', 'description', 'state']
-        # I want a table where attributes are editable (reledit inside)
-        finalvid = 'editable-final'
-
-        cellvids = {3: 'editable-final'}
-        # display facets and actions with a menu
-        layout_args = {'display_filter': 'top',
-                       'add_view_actions': None}
-
-To obtain an editable table, you may specify the 'editable-table' view identifier
-using some of `cellvids`, `finalvid` or `nonfinalvid`.
-
-The previous example results in:
-
-.. image:: ../../images/views-table-shadow.png
-
-In order to activate table filter mechanism, the `display_filter` option is given
-as a layout argument. 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 additional actions for the selected entities
-by setting `add_view_actions` layout option to `True`. This will add actions
-returned by the view's :meth:`~cubicweb.web.views.tableview.TableMixIn.table_actions`.
-
-You can notice that all columns of the result set are not displayed. This is
-because of given `headers`, implying to display only columns from 0 to
-len(headers).
-
-Also Notice that the `ResourcesTable` view relies on a particular rql shape
-(which is not ensured by the way, the only checked thing is that the result set
-contains instance of the `Resource` type). That usually implies that you can't
-use this view for user specific queries (e.g. generated by facets or typed
-manually).
-
-
-So another option would be to write this view using
-:class:`~cubicweb.web.views.tableview.EntityTableView`, as below.
-
-.. sourcecode:: python
-
-    class ResourcesTable(EntityTableView):
-        __regid__ = 'resource.table'
-        __select__ = is_instance('Resource')
-        # table columns definition
-        columns  = ['resource', 'diem', 'duration', 'workpackage', 'description', 'in_state']
-        # I want a table where attributes are editable (reledit inside)
-        finalvid = 'editable-final'
-        # display facets and actions with a menu
-        layout_args = {'display_filter': 'top',
-                       'add_view_actions': None}
-
-        def workpackage_cell(entity):
-            activity = entity.reverse_done_in[0]
-            activity.view('reledit', rtype='done_for', role='subject', w=w)
-        def workpackage_sortvalue(entity):
-            activity = entity.reverse_done_in[0]
-            return activity.done_for[0].sortvalue()
-
-        column_renderers = {
-            'resource': MainEntityColRenderer(),
-            'workpackage': EntityTableColRenderer(
-               header='Workpackage',
-               renderfunc=worpackage_cell,
-               sortfunc=worpackage_sortvalue,),
-            'in_state': EntityTableColRenderer(
-               renderfunc=lambda w,x: w(x.cw_adapt_to('IWorkflowable').printable_state),
-               sortfunc=lambda x: x.cw_adapt_to('IWorkflowable').printable_state),
-         }
-
-Notice the following point:
-
-* `cell_<column>(w, entity)` will be searched for rendering the content of a
-  cell. If not found, `column` is expected to be an attribute of `entity`.
-
-* `cell_sortvalue_<column>(entity)` should return a typed value to use for
-  javascript sorting or None for not sortable columns (the default).
-
-* The :func:`etable_entity_sortvalue` decorator will set a 'sortvalue' function
-  for the column containing the main entity (the one given as argument to all
-  methods), which will call `entity.sortvalue()`.
-
-* You can set a column header using the :func:`etable_header_title` decorator.
-  This header will be translated. If it's not an already existing msgid, think
-  to mark it using `_()` (the example supposes headers are schema defined msgid).
-
-
-Pro/cons of each approach
-`````````````````````````
-:class:`EntityTableView` and :class:`RsetableView` provides basically the same
-set of features, though they don't share the same properties. Let's try to sum
-up pro and cons of each class.
-
-* `EntityTableView` view is:
-
-  - more verbose, but usually easier to understand
-
-  - easily extended (easy to add/remove columns for instance)
-
-  - doesn't rely on a particular rset shape. Simply give it a title and will be
-    listed in the 'possible views' box if any.
-
-* `RsetTableView` view is:
-
-  - hard to beat to display barely a result set, or for cases where some of
-    `headers`, `displaycols` or `cellvids` could be defined to enhance the table
-    while you don't care about e.g. pagination or facets.
-
-  - hardly extensible, as you usually have to change places where the view is
-    called to modify the RQL (hence the view's result set shape).
--- a/doc/book/en/devweb/views/urlpublish.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,137 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-URL publishing
---------------
-
-(:mod:`cubicweb.web.views.urlpublishing`)
-
-.. automodule:: cubicweb.web.views.urlpublishing
-
-.. autoclass:: cubicweb.web.views.urlpublishing.URLPublisherComponent
-   :members:
-
-
-You can write your own *URLPathEvaluator* class to handle custom paths.
-For instance, if you want */my-card-id* to redirect to the corresponding
-card's primary view, you would write:
-
-.. sourcecode:: python
-
-    class CardWikiidEvaluator(URLPathEvaluator):
-        priority = 3 # make it be evaluated *before* RestPathEvaluator
-
-        def evaluate_path(self, req, segments):
-            if len(segments) != 1:
-                raise PathDontMatch()
-            rset = req.execute('Any C WHERE C wikiid %(w)s',
-                               {'w': segments[0]})
-            if len(rset) == 0:
-                # Raise NotFound if no card is found
-                raise PathDontMatch()
-            return None, rset
-
-On the other hand, you can also deactivate some of the standard
-evaluators in your final application. The only thing you have to
-do is to unregister them, for instance in a *registration_callback*
-in your cube:
-
-.. sourcecode:: python
-
-    def registration_callback(vreg):
-        vreg.unregister(RestPathEvaluator)
-
-You can even replace the :class:`cubicweb.web.views.urlpublishing.URLPublisherComponent`
-class if you want to customize the whole toolchain process or if you want
-to plug into an early enough extension point to control your request
-parameters:
-
-.. sourcecode:: python
-
-    class SanitizerPublisherComponent(URLPublisherComponent):
-        """override default publisher component to explicitly ignore
-        unauthorized request parameters in anonymous mode.
-        """
-        unauthorized_form_params = ('rql', 'vid', '__login', '__password')
-
-        def process(self, req, path):
-            if req.session.anonymous_session:
-                self._remove_unauthorized_params(req)
-            return super(SanitizerPublisherComponent, self).process(req, path)
-
-        def _remove_unauthorized_params(self, req):
-            for param in req.form.keys():
-                if param in self.unauthorized_form_params:
-                     req.form.pop(param)
-
-
-    def registration_callback(vreg):
-        vreg.register_and_replace(SanitizerPublisherComponent, URLPublisherComponent)
-
-
-.. autoclass:: cubicweb.web.views.urlpublishing.RawPathEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.EidPathEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.URLRewriteEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.RestPathEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.ActionPathEvaluator
-
-URL rewriting
--------------
-
-(:mod:`cubicweb.web.views.urlrewrite`)
-
-.. autoclass:: cubicweb.web.views.urlrewrite.URLRewriter
-   :members:
-
-.. autoclass:: cubicweb.web.views.urlrewrite.SimpleReqRewriter
-   :members:
-
-.. autoclass:: cubicweb.web.views.urlrewrite.SchemaBasedRewriter
-   :members:
-
-
-``SimpleReqRewriter`` is enough for a certain number of simple cases. If it is not sufficient, ``SchemaBasedRewriter`` allows to do more elaborate things.
-
-Here is an example of ``SimpleReqRewriter`` usage with plain string:
-
-.. sourcecode:: python
-
-   from cubicweb.web.views.urlrewrite import SimpleReqRewriter
-   class TrackerSimpleReqRewriter(SimpleReqRewriter):
-       rules = [
-        ('/versions', dict(vid='versionsinfo')),
-        ]
-
-When the url is `<base_url>/versions`, the view with the __regid__ `versionsinfo` is displayed.
-
-Here is an example of ``SimpleReqRewriter`` usage with regular expressions:
-
-.. sourcecode:: python
-
-    from cubicweb.web.views.urlrewrite import (
-        SimpleReqRewriter, rgx)
-
-    class BlogReqRewriter(SimpleReqRewriter):
-        rules = [
-            (rgx('/blogentry/([a-z_]+)\.rss'),
-             dict(rql=('Any X ORDERBY CD DESC LIMIT 20 WHERE X is BlogEntry,'
-                       'X creation_date CD, X created_by U, '
-                       'U login "%(user)s"'
-                       % {'user': r'\1'}), vid='rss'))
-            ]
-
-When a url matches the regular expression, the view with the __regid__
-`rss` which match the result set is displayed.
-
-Here is an example of ``SchemaBasedRewriter`` usage:
-
-.. sourcecode:: python
-
-    from cubicweb.web.views.urlrewrite import (
-        SchemaBasedRewriter, rgx, build_rset)
-
-    class TrackerURLRewriter(SchemaBasedRewriter):
-        rules = [
-            (rgx('/project/([^/]+)/([^/]+)/tests'),
-             build_rset(rql='Version X WHERE X version_of P, P name %(project)s, X num %(num)s',
-                        rgxgroups=[('project', 1), ('num', 2)], vid='versiontests')),
-            ]
--- a/doc/book/en/devweb/views/views.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-
-.. _Views:
-
-Principles
-----------
-
-We'll start with a description of the interface providing a basic
-understanding of the available classes and methods, then detail the
-view selection principle.
-
-A `View` is an object responsible for the rendering of data from the
-model into an end-user consummable form. They typically churn out an
-XHTML stream, but there are views concerned with email other non-html
-outputs.
-
-.. _views_base_class:
-
-Discovering possible views
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-It is possible to configure the web user interface to have a left box
-showing all the views than can be applied to the current result set.
-
-To enable this, click on your login at the top right corner. Chose
-"user preferences", then "boxes", then "possible views box" and check
-"visible = yes" before validating your changes.
-
-The views listed there we either not selected because of a lower
-score, or they were deliberately excluded by the main template logic.
-
-
-Basic class for views
-~~~~~~~~~~~~~~~~~~~~~
-
-Class :class:`~cubicweb.view.View`
-``````````````````````````````````
-
-.. autoclass:: cubicweb.view.View
-
-The basic interface for views is as follows (remember that the result
-set has a tabular structure with rows and columns, hence cells):
-
-* `render(**context)`, render the view by calling `call` or
-  `cell_call` depending on the context
-
-* `call(**kwargs)`, call the view for a complete result set or null
-  (the default implementation calls `cell_call()` on each cell of the
-  result set)
-
-* `cell_call(row, col, **kwargs)`, call the view for a given cell of a
-  result set (`row` and `col` being integers used to access the cell)
-
-* `url()`, returns the URL enabling us to get the view with the current
-  result set
-
-* `wview(__vid, rset, __fallback_vid=None, **kwargs)`, call the view of
-  identifier `__vid` on the given result set. It is possible to give a
-  fallback view identifier that will be used if the requested view is
-  not applicable to the result set.
-
-* `html_headers()`, returns a list of HTML headers to be set by the
-  main template
-
-* `page_title()`, returns the title to use in the HTML header `title`
-
-Other basic view classes
-````````````````````````
-Here are some of the subclasses of :class:`~cubicweb.view.View` defined in :mod:`cubicweb.view`
-that are more concrete as they relate to data rendering within the application:
-
-.. autoclass:: cubicweb.view.EntityView
-.. autoclass:: cubicweb.view.StartupView
-.. autoclass:: cubicweb.view.EntityStartupView
-.. autoclass:: cubicweb.view.AnyRsetView
-
-Examples of views class
-```````````````````````
-
-- Using `templatable`, `content_type` and HTTP cache configuration
-
-.. sourcecode:: python
-
-    class RSSView(XMLView):
-        __regid__ = 'rss'
-        title = _('rss')
-        templatable = False
-        content_type = 'text/xml'
-        http_cache_manager = MaxAgeHTTPCacheManager
-        cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
-
-
-- Using a custom selector
-
-.. sourcecode:: python
-
-    class SearchForAssociationView(EntityView):
-        """view called by the edition view when the user asks
-        to search for something to link to the edited eid
-        """
-        __regid__ = 'search-associate'
-        title = _('search for association')
-        __select__ = one_line_rset() & match_search_state('linksearch') & is_instance('Any')
-
-
-XML views, binaries views...
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-For views generating other formats than HTML (an image generated dynamically
-for example), and which can not simply be included in the HTML page generated
-by the main template (see above), you have to:
-
-* set the attribute `templatable` of the class to `False`
-* set, through the attribute `content_type` of the class, the MIME
-  type generated by the view to `application/octet-stream` or any
-  relevant and more specialised mime type
-
-For views dedicated to binary content creation (like dynamically generated
-images), we have to set the attribute `binary` of the class to `True` (which
-implies that `templatable == False`, so that the attribute `w` of the view could be
-replaced by a binary flow instead of unicode).
--- a/doc/book/en/devweb/views/wdoc.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Online documentation system
-===========================
-
-.. automodule:: cubicweb.web.views.wdoc
-
-Help views
-----------
-.. autoclass:: cubicweb.web.views.wdoc.InlineHelpView
-
-Actions
--------
-.. autoclass:: cubicweb.web.views.wdoc.HelpAction
-.. autoclass:: cubicweb.web.views.wdoc.AboutAction
--- a/doc/book/en/devweb/views/xmlrss.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-.. _XmlAndRss:
-
-XML and RSS views
------------------
-
-(:mod:`cubicweb.web.views.xmlrss`)
-
-Overview
-+++++++++
-
-*rss*
-    Creates a RSS/XML view and call the view `rssitem` for each entity of
-    the result set.
-
-*rssitem*
-    Create a RSS/XML view for each entity based on the results of the dublin core
-    methods of the entity (`dc_*`)
-
-RSS Channel Example
-++++++++++++++++++++
-
-Assuming you have several blog entries, click on the title of the
-search box in the left column. A larger search box should appear. Enter:
-
-.. sourcecode:: sql
-
-   Any X ORDERBY D WHERE X is BlogEntry, X creation_date D
-
-and you get a list of blog entries.
-
-Click on your login at the top right corner. Chose "user preferences",
-then "boxes", then "possible views box" and check "visible = yes"
-before validating your changes.
-
-Enter the same query in the search box and you will see the same list,
-plus a box titled "possible views" in the left column. Click on
-"entityview", then "RSS".
-
-You just applied the "RSS" view to the RQL selection you requested.
-
-That's it, you have a RSS channel for your blog.
-
-Try again with:
-
-.. sourcecode:: sql
-
-    Any X ORDERBY D WHERE X is BlogEntry, X creation_date D,
-    X entry_of B, B title "MyLife"
-
-Another RSS channel, but a bit more focused.
-
-A last one for the road:
-
-.. sourcecode:: sql
-
-    Any C ORDERBY D WHERE C is Comment, C creation_date D LIMIT 15
-
-displayed with the RSS view, that's a channel for the last fifteen
-comments posted.
-
-[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
Binary file doc/book/en/images/03-transitions-view_en.png has changed
Binary file doc/book/en/images/archi_globale.png has changed
Binary file doc/book/en/images/archi_globale_en.png has changed
Binary file doc/book/en/images/breadcrumbs_header.png has changed
Binary file doc/book/en/images/facet_date_range.png has changed
Binary file doc/book/en/images/facet_has_image.png has changed
Binary file doc/book/en/images/facet_overview.png has changed
Binary file doc/book/en/images/facet_range.png has changed
Binary file doc/book/en/images/lax-book_00-login_en.png has changed
Binary file doc/book/en/images/lax-book_01-start_en.png has changed
Binary file doc/book/en/images/lax-book_02-cookie-values_en.png has changed
Binary file doc/book/en/images/lax-book_02-create-blog_en.png has changed
Binary file doc/book/en/images/lax-book_03-list-one-blog_en.png has changed
Binary file doc/book/en/images/lax-book_03-site-config-panel_en.png has changed
Binary file doc/book/en/images/lax-book_03-state-submitted_en.png has changed
Binary file doc/book/en/images/lax-book_03-transitions-view_en.png has changed
Binary file doc/book/en/images/lax-book_04-detail-one-blog_en.png has changed
Binary file doc/book/en/images/lax-book_05-list-two-blog_en.png has changed
Binary file doc/book/en/images/lax-book_06-add-relation-entryof_en.png has changed
Binary file doc/book/en/images/lax-book_06-main-template-logo_en.png has changed
Binary file doc/book/en/images/lax-book_07-detail-one-blogentry_en.png has changed
Binary file doc/book/en/images/lax-book_08-schema_en.png has changed
Binary file doc/book/en/images/lax-book_09-new-view-blogentry_en.png has changed
Binary file doc/book/en/images/lax-book_10-blog-with-two-entries_en.png has changed
Binary file doc/book/en/images/main_template.png has changed
--- a/doc/book/en/images/main_template.svg	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,207 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="1036.6421"
-   height="845.07812"
-   id="svg2"
-   sodipodi:version="0.32"
-   inkscape:version="0.46"
-   sodipodi:docname="main_template.svg"
-   inkscape:output_extension="org.inkscape.output.svg.inkscape"
-   version="1.0"
-   inkscape:export-filename="/home/auc/cw/doc/book/en/images/main_template.png"
-   inkscape:export-xdpi="60.659016"
-   inkscape:export-ydpi="60.659016">
-  <defs
-     id="defs4">
-    <inkscape:perspective
-       sodipodi:type="inkscape:persp3d"
-       inkscape:vp_x="0 : 526.18109 : 1"
-       inkscape:vp_y="0 : 1000 : 0"
-       inkscape:vp_z="744.09448 : 526.18109 : 1"
-       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
-       id="perspective10" />
-  </defs>
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.80355603"
-     inkscape:cx="510.91495"
-     inkscape:cy="422.53906"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="925"
-     inkscape:window-height="1168"
-     inkscape:window-x="0"
-     inkscape:window-y="0"
-     inkscape:snap-bbox="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Calque 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(162.2968,90.697922)">
-    <rect
-       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect2439"
-       width="854.37006"
-       height="698.2019"
-       x="20.307629"
-       y="-20.575344" />
-    <rect
-       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3301"
-       width="816.3457"
-       height="508.15628"
-       x="31.751091"
-       y="96.33345" />
-    <g
-       id="g3220"
-       transform="matrix(1.0035394,0,0,1,0.5745006,0)">
-      <rect
-         y="-89.447922"
-         x="-161.0468"
-         height="55.714287"
-         width="1031.1713"
-         id="rect3240"
-         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.50000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
-      <text
-         id="text3264"
-         y="-51.771908"
-         x="757.85767"
-         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
-         xml:space="preserve"><tspan
-           id="tspan3266"
-           y="-51.771908"
-           x="757.85767"
-           sodipodi:role="line">header</tspan></text>
-    </g>
-    <rect
-       style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3270"
-       width="167.87744"
-       height="707.71222"
-       x="-160.02441"
-       y="-24.671618" />
-    <g
-       id="g2434"
-       transform="matrix(0.975467,0,0,1,0.6942419,-3.6587365)">
-      <rect
-         y="35.365849"
-         x="29.548275"
-         height="55.714287"
-         width="842.59979"
-         id="rect3279"
-         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
-      <text
-         id="text3281"
-         y="72.885193"
-         x="681.65283"
-         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
-         xml:space="preserve"><tspan
-           id="tspan3283"
-           y="72.885193"
-           x="681.65283"
-           sodipodi:role="line">contentheader</tspan></text>
-    </g>
-    <g
-       id="g3170"
-       transform="matrix(1.0023324,0,0,1,-2.0421673,-10.976211)">
-      <rect
-         y="698.6355"
-         x="-158.28485"
-         height="55.714287"
-         width="1032.5997"
-         id="rect3285"
-         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
-      <text
-         id="text3287"
-         y="736.52045"
-         x="770.28204"
-         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
-         xml:space="preserve"><tspan
-           id="tspan3289"
-           y="736.52045"
-           x="770.28204"
-           sodipodi:role="line">footer</tspan></text>
-    </g>
-    <g
-       id="g3211" />
-    <g
-       id="g3215"
-       transform="matrix(0.9712065,0,0,1,0.7659296,-17.074106)">
-      <rect
-         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-         id="rect3291"
-         width="844.62012"
-         height="55.714287"
-         x="27.850754"
-         y="629.88562" />
-      <text
-         id="text3293"
-         y="666.60339"
-         x="692.85773"
-         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
-         xml:space="preserve"><tspan
-           id="tspan3295"
-           y="666.60339"
-           x="692.85773"
-           sodipodi:role="line">contentfooter</tspan></text>
-    </g>
-    <text
-       xml:space="preserve"
-       style="font-size:23.38711166px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="-143.67273"
-       y="20.58094"
-       id="text3297"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan2432"
-         x="-143.67273"
-         y="20.58094">left column</tspan></text>
-    <text
-       transform="scale(0.9876573,1.0124969)"
-       id="text3175"
-       y="12.071429"
-       x="721.0575"
-       style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
-       xml:space="preserve"><tspan
-         id="tspan3177"
-         y="12.071429"
-         x="721.0575"
-         sodipodi:role="line">contentcol</tspan></text>
-    <text
-       transform="scale(0.9876573,1.0124969)"
-       id="text3179"
-       y="126.27104"
-       x="701.45959"
-       style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
-       xml:space="preserve"><tspan
-         id="tspan3181"
-         y="126.27104"
-         x="701.45959"
-         sodipodi:role="line">contentmain</tspan></text>
-  </g>
-</svg>
Binary file doc/book/en/images/main_template_layout.png has changed
Binary file doc/book/en/images/primaryview_template.png has changed
--- a/doc/book/en/images/primaryview_template.svg	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="1036.6421"
-   height="845.07812"
-   id="svg2"
-   sodipodi:version="0.32"
-   inkscape:version="0.46"
-   sodipodi:docname="primaryview_template.svg"
-   inkscape:output_extension="org.inkscape.output.svg.inkscape"
-   version="1.0"
-   inkscape:export-filename="/home/steph/local/fcubicweb/cubicweb/doc/book/en/images/primaryview_template.png"
-   inkscape:export-xdpi="43.451603"
-   inkscape:export-ydpi="43.451603">
-  <defs
-     id="defs4">
-    <inkscape:perspective
-       sodipodi:type="inkscape:persp3d"
-       inkscape:vp_x="0 : 526.18109 : 1"
-       inkscape:vp_y="0 : 1000 : 0"
-       inkscape:vp_z="744.09448 : 526.18109 : 1"
-       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
-       id="perspective10" />
-  </defs>
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.9357135"
-     inkscape:cx="518.32104"
-     inkscape:cy="337.0428"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1307"
-     inkscape:window-height="1168"
-     inkscape:window-x="0"
-     inkscape:window-y="0" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Calque 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(162.2968,90.697922)">
-    <g
-       id="g3869"
-       transform="matrix(1,0,0,1.0373644,0,-72.039777)"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449">
-      <rect
-         y="-15.840891"
-         x="-159.08963"
-         height="770.11017"
-         width="1033.0049"
-         id="rect3301"
-         style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.90144825;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
-      <text
-         id="text3865"
-         y="19.784882"
-         x="-150.07172"
-         style="font-size:28.67479324px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
-         xml:space="preserve"><tspan
-           id="tspan3867"
-           y="19.784882"
-           x="-150.07172"
-           sodipodi:role="line">contentmain</tspan></text>
-    </g>
-    <rect
-       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.45654476;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect2383"
-       width="772.32111"
-       height="43.888428"
-       x="-131.1837"
-       y="86.559296"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449" />
-    <text
-       xml:space="preserve"
-       style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
-       x="-122.69418"
-       y="115.50363"
-       id="text2385"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         x="-122.69418"
-         y="115.50363"
-         id="tspan3163">navcontenttop</tspan></text>
-    <rect
-       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3167"
-       width="770.26868"
-       height="203.16078"
-       x="-125.88269"
-       y="172.90417"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449" />
-    <text
-       xml:space="preserve"
-       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="348.26724"
-       y="205.34305"
-       id="text3169"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         x="348.26724"
-         y="205.34305"
-         id="tspan3171">render_entity_attributes()</tspan></text>
-    <rect
-       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3173"
-       width="769.93549"
-       height="237.84663"
-       x="-125.03326"
-       y="391.32156"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449" />
-    <text
-       xml:space="preserve"
-       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="360.99954"
-       y="428.38055"
-       id="text3175"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         x="360.99954"
-         y="428.38055"
-         id="tspan3177">render_entity_relations()</tspan></text>
-    <rect
-       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:2.15903592;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3185"
-       width="178.93939"
-       height="612.36584"
-       x="667.10443"
-       y="84.64225"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449" />
-    <text
-       xml:space="preserve"
-       style="font-size:22px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
-       x="105.32364"
-       y="-810.65997"
-       id="text3187"
-       transform="matrix(0,1,-1,0,0,0)"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         id="tspan2408">render_side_boxes()</tspan></text>
-    <rect
-       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:3.0652349;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3191"
-       width="771.97766"
-       height="55.647793"
-       x="-127.80586"
-       y="642.0293"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449" />
-    <text
-       xml:space="preserve"
-       style="font-size:16px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="-121.22153"
-       y="674.1748"
-       id="text3181"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         x="-121.22153"
-         y="674.1748"
-         id="tspan3183">navcontentbottom</tspan></text>
-    <rect
-       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3881"
-       width="986.90503"
-       height="45.800392"
-       x="-128.34428"
-       y="-31.574066"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449" />
-    <text
-       xml:space="preserve"
-       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="355.60541"
-       y="-2.7424495"
-       id="text3883"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         x="355.60541"
-         y="-2.7424495"
-         id="tspan3885">render_entity_toolbox(), render_entity_title()</tspan></text>
-    <rect
-       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="rect3890"
-       width="986.90503"
-       height="45.800392"
-       x="-128.87863"
-       y="19.723684"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449" />
-    <text
-       xml:space="preserve"
-       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="565.71027"
-       y="50.135612"
-       id="text3892"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         x="565.71027"
-         y="50.135612"
-         id="tspan3894">render_entity_summary()</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="87.154541"
-       y="114.2578"
-       id="text3899"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         id="tspan3903"
-         x="87.154541"
-         y="114.2578">content_navigation_components('navcontenttop')</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
-       x="88.46772"
-       y="675.71582"
-       id="text2410"
-       sodipodi:linespacing="125%"
-       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
-       inkscape:export-xdpi="60.912449"
-       inkscape:export-ydpi="60.912449"><tspan
-         sodipodi:role="line"
-         id="tspan2412"
-         x="88.46772"
-         y="675.71582">content_navigation_components('navcontenttop')</tspan></text>
-  </g>
-</svg>
Binary file doc/book/en/images/request_session.png has changed
--- a/doc/book/en/images/request_session.svg	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,206 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="85.960938"
-   height="12.382812"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.3.1 r9886"
-   sodipodi:docname="request_session.svg">
-  <defs
-     id="defs4">
-    <marker
-       inkscape:stockid="Arrow1Lend"
-       orient="auto"
-       refY="0.0"
-       refX="0.0"
-       id="Arrow1Lend"
-       style="overflow:visible;">
-      <path
-         id="path3822"
-         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
-         style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none;"
-         transform="scale(0.8) rotate(180) translate(12.5,0)" />
-    </marker>
-  </defs>
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.98994949"
-     inkscape:cx="25.928992"
-     inkscape:cy="-185.87004"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     fit-margin-top="0"
-     fit-margin-left="0"
-     fit-margin-right="0"
-     fit-margin-bottom="0"
-     inkscape:window-width="958"
-     inkscape:window-height="1160"
-     inkscape:window-x="0"
-     inkscape:window-y="38"
-     inkscape:window-maximized="0"
-     inkscape:snap-global="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(-263.52249,-495.73373)">
-    <rect
-       style="fill:#ffffff;stroke:#000000;stroke-width:0.92460138;stroke-opacity:1"
-       id="rect3773"
-       width="214.15233"
-       height="184.80336"
-       x="57.578697"
-       y="366.01306" />
-    <rect
-       id="rect2985"
-       width="216.86372"
-       height="183.54575"
-       x="348.50262"
-       y="367.78079"
-       style="fill:#ffffff;stroke:#000000;stroke-width:0.55298227;stroke-opacity:1" />
-    <text
-       xml:space="preserve"
-       style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="376.7869"
-       y="399.80365"
-       id="text3755"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3757"
-         x="376.7869"
-         y="399.80365">Repository</tspan></text>
-    <rect
-       style="fill:#ffffff;stroke:#000000;stroke-opacity:1"
-       id="rect3759"
-       width="144.45181"
-       height="104.04572"
-       x="237.38585"
-       y="423.03714" />
-    <text
-       xml:space="preserve"
-       style="font-size:24px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="262.63968"
-       y="470.51431"
-       id="text3761"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3763"
-         x="262.63968"
-         y="470.51431">REPOAPI</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="262.63968"
-       y="507.88998"
-       id="text3765"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3767"
-         x="262.63968"
-         y="507.88998">connection</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="419.21332"
-       y="509.91025"
-       id="text3769"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3771"
-         x="419.21332"
-         y="509.91025">session</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="102.02541"
-       y="397.78333"
-       id="text3775"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3777"
-         x="102.02541"
-         y="397.78333">Client</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="116.16754"
-       y="507.88995"
-       id="text3779"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3781"
-         x="116.16754"
-         y="507.88995">request</tspan></text>
-    <text
-       xml:space="preserve"
-       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="361.50729"
-       y="585.89832"
-       id="text3802"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3804"
-         x="361.50729"
-         y="585.89832">database </tspan><tspan
-         sodipodi:role="line"
-         x="361.50729"
-         y="605.89832"
-         id="tspan3806">connection</tspan></text>
-    <rect
-       style="fill:#ffffff;stroke:#000000;stroke-width:1.48014534;stroke-opacity:1"
-       id="rect3808"
-       width="192.09367"
-       height="58.095726"
-       x="365.79443"
-       y="621.50018" />
-    <text
-       xml:space="preserve"
-       style="font-size:36px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
-       x="369.5885"
-       y="662.66992"
-       id="text3810"
-       sodipodi:linespacing="125%"><tspan
-         sodipodi:role="line"
-         id="tspan3812"
-         x="369.5885"
-         y="662.66992">Database</tspan></text>
-    <path
-       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:none"
-       d="M 197.57252,125.76645 195.76971,55.592808"
-       id="path4260"
-       inkscape:connector-type="polyline"
-       inkscape:connector-curvature="3"
-       inkscape:connection-start="#rect3808"
-       inkscape:connection-start-point="d4"
-       inkscape:connection-end="#rect2985"
-       inkscape:connection-end-point="d4"
-       transform="translate(263.52249,495.73373)" />
-  </g>
-</svg>
Binary file doc/book/en/images/server-class-diagram.png has changed
Binary file doc/book/en/images/tutos-base_blog-form_en.png has changed
Binary file doc/book/en/images/tutos-base_blog-primary-after-post-creation_en.png has changed
Binary file doc/book/en/images/tutos-base_blog-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_blogs-list_en.png has changed
Binary file doc/book/en/images/tutos-base_form-generic-relations_en.png has changed
Binary file doc/book/en/images/tutos-base_index_en.png has changed
Binary file doc/book/en/images/tutos-base_login-form_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-community-custom-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-community-default-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-community-taggable-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-custom-footer_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-schema_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-siteinfo_en.png has changed
Binary file doc/book/en/images/tutos-base_schema_en.png has changed
Binary file doc/book/en/images/tutos-base_siteconfig_en.png has changed
Binary file doc/book/en/images/tutos-base_user-menu_en.png has changed
Binary file doc/book/en/images/tutos-photowebsite_background-image.png has changed
Binary file doc/book/en/images/tutos-photowebsite_boxes.png has changed
Binary file doc/book/en/images/tutos-photowebsite_breadcrumbs.png has changed
Binary file doc/book/en/images/tutos-photowebsite_facets.png has changed
Binary file doc/book/en/images/tutos-photowebsite_grey-box.png has changed
Binary file doc/book/en/images/tutos-photowebsite_index-after.png has changed
Binary file doc/book/en/images/tutos-photowebsite_index-before.png has changed
Binary file doc/book/en/images/tutos-photowebsite_login-box.png has changed
Binary file doc/book/en/images/tutos-photowebsite_prevnext.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui1.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui2.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui3.png has changed
Binary file doc/book/en/images/undo_history-view_w600.png has changed
Binary file doc/book/en/images/undo_mesage_w600.png has changed
Binary file doc/book/en/images/undo_startup-link_w600.png has changed
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	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _contents:
-
-=====================================================
-|cubicweb| - The Semantic Web is a construction game!
-=====================================================
-
-|cubicweb| is a semantic web application framework, licensed under the LGPL,
-that empowers developers to efficiently build web applications by reusing
-components (called `cubes`) and following the well known object-oriented design
-principles.
-
-Its main features are:
-
-* an engine driven by the explicit :ref:`data model
-  <TutosBaseCustomizingTheApplicationDataModel>` of the application,
-
-* a query language named :ref:`RQL <RQL>` similar to W3C's SPARQL,
-
-* a :ref:`selection+view <TutosBaseCustomizingTheApplicationCustomViews>`
-  mechanism for semi-automatic XHTML/XML/JSON/text generation,
-
-* a library of reusable :ref:`components <Cube>` (data model and views) that
-  fulfill common needs,
-
-* the power and flexibility of the Python_ programming language,
-
-* the reliability of SQL databases, LDAP directories, Subversion and Mercurial
-  for storage backends.
-
-Built since 2000 from an R&D effort still continued, supporting 100,000s of
-daily visits at some production sites, |cubicweb| is a proven end to end solution
-for semantic web application development that promotes quality, reusability and
-efficiency.
-
-The unbeliever will read the :ref:`Tutorials`.
-
-The hacker will join development at the forge_.
-
-The impatient developer will move right away to :ref:`SetUpEnv` then to :ref:`ConfigEnv`.
-
-The chatter lover will join the `jabber forum`_, the `mailing-list`_ and the blog_.
-
-.. _Logilab: http://www.logilab.fr/
-.. _forge: http://www.cubicweb.org/project/
-.. _Python: http://www.python.org/
-.. _`jabber forum`: http://www.logilab.org/blogentry/6718
-.. _`mailing-list`: http://lists.cubicweb.org/mailman/listinfo/cubicweb
-.. _blog: http://www.cubicweb.org/blog/1238
-
-.. toctree::
-   :maxdepth: 2
-
-   intro/index
-   tutorials/index
-
-.. toctree::
-   :maxdepth: 3
-
-   devrepo/index
-   devweb/index
-
-.. toctree::
-   :maxdepth: 2
-
-   admin/index
-   additionnal_services/index
-   annexes/index
-
-See also:
-
-* the :ref:`genindex`,
-* the :ref:`modindex`,
--- a/doc/book/en/intro/concepts.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,306 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Concepts:
-
-The Core Concepts of |cubicweb|
-===============================
-
-This section defines some terms and core concepts of the |cubicweb| framework. To
-avoid confusion while reading this book, take time to go through the following
-definitions and use this section as a reference during your reading.
-
-
-.. _Cube:
-
-Cubes
------
-
-A cube is a software component made of three parts: its data model
-(:mod:`schema`), its logic (:mod:`entities`) and its user interface
-(:mod:`views`).
-
-A cube can use other cubes as building blocks and assemble them to provide a
-whole with richer functionnalities than its parts. The cubes `cubicweb-blog`_ and
-`cubicweb-comment`_ could be used to make a cube named *myblog* with commentable
-blog entries.
-
-The `CubicWeb.org Forge`_ offers a large number of cubes developed by the community
-and available under a free software license.
-
-.. note::
-
- The command :command:`cubicweb-ctl list` displays the list of available cubes.
-
-.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
-.. _`cubicweb-blog`: http://www.cubicweb.org/project/cubicweb-blog
-.. _`cubicweb-comment`: http://www.cubicweb.org/project/cubicweb-comment
-
-
-.. _Instance:
-
-Instances
----------
-
-An instance is a runnable application installed on a computer and based on a
-cube.
-
-The instance directory contains the configuration files. Several instances can be
-created and based on the same cube. For exemple, several software forges can be
-set up on one computer system based on the `cubicweb-forge`_ cube.
-
-.. _`cubicweb-forge`: http://www.cubicweb.org/project/cubicweb-forge
-
-Instances can be of three different types: all-in-one, web engine or data
-repository. For applications that support high traffic, several web (front-end)
-and data (back-end) instances can be set-up to share the load.
-
-.. image:: ../images/archi_globale_en.png
-
-The command :command:`cubicweb-ctl list` also displays the list of instances
-installed on your system.
-
-.. note::
-
-  The term application is used to refer to "something that should do something as
-  a whole", eg more like a project and so can refer to an instance or to a cube,
-  depending on the context. This book will try to use *application*, *cube* and
-  *instance* as appropriate.
-
-
-.. _RepositoryIntro:
-
-Data Repository
----------------
-
-The data repository [1]_ encapsulates and groups an access to one or
-more data sources (including SQL databases, LDAP repositories, other
-|cubicweb| instance repositories, filesystems, Google AppEngine's
-DataStore, etc).
-
-All interactions with the repository are done using the `Relation Query Language`
-(:ref:`RQL`). The repository federates the data sources and hides them from the
-querier, which does not realize when a query spans several data sources
-and requires running sub-queries and merges to complete.
-
-Application logic can be mapped to data events happenning within the
-repository, like creation of entities, deletion of relations,
-etc. This is used for example to send email notifications when the
-state of an object changes. See :ref:`HookIntro` below.
-
-.. [1] not to be confused with a Mercurial repository or a Debian repository.
-.. _`Python Remote Objects`: http://pythonhosted.org/Pyro4/
-
-.. _WebEngineIntro:
-
-Web Engine
-----------
-
-The web engine replies to http requests and runs the user interface.
-
-By default the web engine provides a `CRUD`_ user interface based on
-the data model of the instance. Entities can be created, displayed,
-updated and deleted. As the default user interface is not very fancy,
-it is usually necessary to develop your own.
-
-It is common to run the web engine and the repository in the same
-process (see instances of type all-in-one above), but this is not a
-requirement. A repository can be set up to be accessed remotely using
-Pyro (`Python Remote Objects`_) and act as a standalone server, which
-can be directly accessed or also through a standalone web engine.
-
-.. _`CRUD`: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
-
-.. _SchemaIntro:
-
-Schema (Data Model)
--------------------
-
-The data model of a cube is described as an entity-relationship schema using a
-comprehensive language made of Python classes imported from the yams_ library.
-
-.. _yams: http://www.logilab.org/project/yams/
-
-An `entity type` defines a sequence of attributes. Attributes may be
-of the following types: `String`, `Int`, `Float`, `Boolean`, `Date`,
-`Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`.
-
-A `relation type` is used to define an oriented binary relation
-between entity types.  The left-hand part of a relation is named the
-`subject` and the right-hand part is named the `object`.
-
-A `relation definition` is a triple (*subject entity type*, *relation type*, *object
-entity type*) associated with a set of properties such as cardinality,
-constraints, etc.
-
-Permissions can be set on entity types or relation definition to control who
-will be able to create, read, update or delete entities and relations. Permissions
-are granted to groups (to which users may belong) or using rql expressions (if the
-rql expression returns some results, the permission is granted).
-
-Some meta-data necessary to the system are added to the data model. That includes
-entities like users and groups, the entities used to store the data model
-itself and attributes like unique identifier, creation date, creator, etc.
-
-When you create a new |cubicweb| instance, the schema is stored in the database.
-When the cubes the instance is based on evolve, they may change their data model
-and provide migration scripts that will be executed when the administrator will
-run the upgrade process for the instance.
-
-
-.. _VRegistryIntro:
-
-Registries and application objects
-----------------------------------
-
-Application objects
-~~~~~~~~~~~~~~~~~~~
-
-Besides a few core functionalities, almost every feature of the framework is
-achieved by dynamic objects (`application objects` or `appobjects`) stored in a
-two-levels registry. Each object is affected to a registry with
-an identifier in this registry. You may have more than one object sharing an
-identifier in the same registry:
-
-  object's `__registry__` : object's `__regid__` : [list of app objects]
-
-In other words, the `registry` contains several (sub-)registries which hold a
-list of appobjects associated to an identifier.
-
-The base class of appobjects is :class:`cubicweb.appobject.AppObject`.
-
-Selectors
-~~~~~~~~~
-
-At runtime, appobjects can be selected in a registry according to some
-contextual information. Selection is done by comparing the *score*
-returned by each appobject's *selector*.
-
-The better the object fits the context, the higher the score. Scores
-are the glue that ties appobjects to the data model. Using them
-appropriately is an essential part of the construction of well behaved
-cubes.
-
-|cubicweb| provides a set of basic selectors that may be parametrized.  Also,
-selectors can be combined with the `~` unary operator (negation) and the binary
-operators `&` and `|` (respectivly 'and' and 'or') to build more complex
-selectors. Of course complex selectors may be combined too. Last but not least, you
-can write your own selectors.
-
-The `registry`
-~~~~~~~~~~~~~~~
-
-At startup, the `registry` inspects a number of directories looking
-for compatible class definitions. After a recording process, the
-objects are assigned to registries and become available through the
-selection process.
-
-In a cube, application object classes are looked in the following modules or
-packages:
-
-- `entities`
-- `views`
-- `hooks`
-- `sobjects`
-
-There are three common ways to look up some application object from a
-registry:
-
-* get the most appropriate object by specifying an identifier and
-  context objects. The object with the greatest score is
-  selected. There should always be a single appobject with a greater
-  score than others for a particular context.
-
-* get all objects applying to a context by specifying a registry. A
-  list of objects will be returned containing the object with the
-  highest score (> 0) for each identifier in that registry.
-
-* get the object within a particular registry/identifier. No selection
-  process is involved: the registry will expect to find a single
-  object in that cell.
-
-
-.. _RQLIntro:
-
-The RQL query language
-----------------------
-
-No need for a complicated ORM when you have a powerful data
-manipulation language.
-
-All the persistent data in a |cubicweb| instance is retrieved and
-modified using RQL (see :ref:`rql_intro`).
-
-This query language is inspired by SQL but is on a higher level in order to
-emphasize browsing relations.
-
-
-Result set
-~~~~~~~~~~
-
-Every request made (using RQL) to the data repository returns an object we call a
-Result Set. It enables easy use of the retrieved data, providing a translation
-layer between the backend's native datatypes and |cubicweb| schema's EntityTypes.
-
-Result sets provide access to the raw data, yielding either basic Python data
-types, or schema-defined high-level entities, in a straightforward way.
-
-
-.. _ViewIntro:
-
-Views
------
-
-**CubicWeb is data driven**
-
-The view system is loosely coupled to data through the selection system explained
-above. Views are application objects with a dedicated interface to 'render'
-something, eg producing some html, text, xml, pdf, or whatsover that can be
-displayed to a user.
-
-Views actually are partitioned into different kind of objects such as
-`templates`, `boxes`, `components` and proper `views`, which are more
-high-level abstraction useful to build the user interface in an object
-oriented way.
-
-
-.. _HookIntro:
-
-Hooks and operations
---------------------
-
-**CubicWeb provides an extensible data repository**
-
-The data model defined using Yams types allows to express the data
-model in a comfortable way. However several aspects of the data model
-can not be expressed there. For instance:
-
-* managing computed attributes
-
-* enforcing complicated business rules
-
-* real-world side-effects linked to data events (email notification
-  being a prime example)
-
-The hook system is much like the triggers of an SQL database engine,
-except that:
-
-* it is not limited to one specific SQL backend (every one of them
-  having an idiomatic way to encode triggers), nor to SQL backends at
-  all (think about LDAP or a Subversion repository)
-
-* it is well-coupled to the rest of the framework
-
-Hooks are also application objects (in the `hooks` registry) and
-selected on events such as after/before add/update/delete on
-entities/relations, server startup or shutdown, etc.
-
-`Operations` may be instantiated by hooks to do further processing at different
-steps of the transaction's commit / rollback, which usually can not be done
-safely at the hook execution time.
-
-Hooks and operation are an essential building block of any moderately complicated
-cubicweb application.
-
-.. note::
-   RQL queries executed in hooks and operations are *unsafe* by default, i.e. the
-   read and write security is deactivated unless explicitly asked.
--- a/doc/book/en/intro/history.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-A little history...
-===================
-
-*CubicWeb* is a semantic web application framework that Logilab_ started
-developing in 2001 as an offspring of its Narval_ research project. *CubicWeb*
-is written in Python and includes a data server and a web engine.
-
-Its data server publishes data federated from different sources like
-SQL databases, LDAP directories, `VCS`_ repositories or even from other
-CubicWeb data servers.
-
-.. _`VCS`: http://en.wikipedia.org/wiki/Revision_control
-
-Its web engine was designed to let the final user control what content to select
-and how to display it. It allows one to browse the federated data sources and
-display the results with the rendering that best fits the context. This
-flexibility of the user interface gives back to the user some capabilities
-usually only accessible to application developers.
-
-*CubicWeb* has been developed by Logilab_ and used in-house for many years
-before it was first installed for its clients in 2006 as version 2.
-
-In 2008, *CubicWeb* version 3 became downloadable for free under the
-terms of the LGPL license. Its community is now steadily growing
-without hampering the fast-paced stream of changes thanks to the time
-and energy originally put in the design of the framework.
-
-
-.. _Narval: http://www.logilab.org/project/narval-moved
-.. _Logilab: http://www.logilab.fr/
--- a/doc/book/en/intro/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Part1:
-
---------------------------
-Introduction to *CubicWeb*
---------------------------
-
-This first part of the book offers different reading path to
-discover the *CubicWeb* framework, provides a tutorial to get a quick
-overview of its features and lists its key concepts.
-
-
-.. toctree::
-   :maxdepth: 2
-   :numbered:
-
-   history
-   concepts.rst
--- a/doc/book/en/makefile	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-SRC=.
-
-# You can set these sphinx variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
-#BUILDDIR      = build
-BUILDDIR      = ../..
-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
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d ${BUILDDIR}/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-
-
-.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
-
-help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  all       to make standalone HTML files, developer manual and API doc"
-	@echo "  html      to make standalone HTML files"
-	@echo "---  "
-	@echo "  pickle    to make pickle files (usable by e.g. sphinx-web)"
-	@echo "  htmlhelp  to make HTML files and a HTML help project"
-	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  changes   to make an overview over all changed/added/deprecated items"
-	@echo "  linkcheck to check all external links for integrity"
-
-clean:
-	rm -f *.html
-	-rm -rf ${BUILDDIR}/html ${BUILDDIR}/doctrees
-	-rm -rf ${BUILDJS}
-
-all: html
-
-# run sphinx ###
-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
-	@echo
-	@echo "Build finished; now you can process the pickle files or run"
-	@echo "  sphinx-web ${BUILDDIR}/pickle"
-	@echo "to start the sphinx-web server."
-
-web: pickle
-
-htmlhelp:
-	mkdir -p ${BUILDDIR}/htmlhelp ${BUILDDIR}/doctrees
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) ${BUILDDIR}/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in ${BUILDDIR}/htmlhelp."
-
-latex:
-	mkdir -p ${BUILDDIR}/latex ${BUILDDIR}/doctrees
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) ${BUILDDIR}/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in ${BUILDDIR}/latex."
-	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
-	      "run these through (pdf)latex."
-
-changes:
-	mkdir -p ${BUILDDIR}/changes ${BUILDDIR}/doctrees
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) ${BUILDDIR}/changes
-	@echo
-	@echo "The overview file is in ${BUILDDIR}/changes."
-
-linkcheck:
-	mkdir -p ${BUILDDIR}/linkcheck ${BUILDDIR}/doctrees
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) ${BUILDDIR}/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in ${BUILDDIR}/linkcheck/output.txt."
--- a/doc/book/en/tutorials/advanced/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-
-.. _TutosPhotoWebSite:
-
-Building a photo gallery with |cubicweb|
-========================================
-
-Desired features
-----------------
-
-* basically a photo gallery
-
-* photo stored on the file system and displayed dynamically through a web interface
-
-* navigation through folder (album), tags, geographical zone, people on the
-  picture... using facets
-
-* advanced security (not everyone can see everything). More on this later.
-
-
-.. toctree::
-   :maxdepth: 2
-
-   part01_create-cube
-   part02_security
-   part03_bfss
-   part04_ui-base
-   part05_ui-advanced
-
-
--- a/doc/book/en/tutorials/advanced/part01_create-cube.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-.. _TutosPhotoWebSiteCubeCreation:
-
-Cube creation and schema definition
------------------------------------
-
-.. _adv_tuto_create_new_cube:
-
-Step 1: creating a new cube for my web site
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-One note about my development environment: I wanted to use the packaged
-version of CubicWeb and cubes while keeping my cube in my user
-directory, let's say `~src/cubes`.  I achieve this by setting the
-following environment variables::
-
-  CW_CUBES_PATH=~/src/cubes
-  CW_MODE=user
-
-I can now create the cube which will hold custom code for this web
-site using::
-
-  cubicweb-ctl newcube --directory=~/src/cubes sytweb
-
-
-.. _adv_tuto_assemble_cubes:
-
-Step 2: pick building blocks into existing cubes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Almost everything I want to handle in my web-site is somehow already modelized in
-existing cubes that I'll extend for my need. So I'll pick the following cubes:
-
-* `folder`, containing the `Folder` entity type, which will be used as
-  both 'album' and a way to map file system folders. Entities are
-  added to a given folder using the `filed_under` relation.
-
-* `file`, containing `File` entity type, gallery view, and a file system import
-  utility.
-
-* `zone`, containing the `Zone` entity type for hierarchical geographical
-  zones. Entities (including sub-zones) are added to a given zone using the
-  `situated_in` relation.
-
-* `person`, containing the `Person` entity type plus some basic views.
-
-* `comment`, providing a full commenting system allowing one to comment entity types
-  supporting the `comments` relation by adding a `Comment` entity.
-
-* `tag`, providing a full tagging system as an easy and powerful way to classify
-  entities supporting the `tags` relation by linking the to `Tag` entities. This
-  will allows navigation into a large number of picture.
-
-Ok, now I'll tell my cube requires all this by editing :file:`cubes/sytweb/__pkginfo__.py`:
-
-  .. sourcecode:: python
-
-    __depends__ = {'cubicweb': '>= 3.10.0',
-                   'cubicweb-file': '>= 1.9.0',
-		   'cubicweb-folder': '>= 1.1.0',
-		   'cubicweb-person': '>= 1.2.0',
-		   'cubicweb-comment': '>= 1.2.0',
-		   'cubicweb-tag': '>= 1.2.0',
-		   'cubicweb-zone': None}
-
-Notice that you can express minimal version of the cube that should be used,
-`None` meaning whatever version available. All packages starting with 'cubicweb-'
-will be recognized as being cube, not bare python packages. You can still specify
-this explicitly using instead the `__depends_cubes__` dictionary which should
-contains cube's name without the prefix. So the example below would be written
-as:
-
-  .. sourcecode:: python
-
-    __depends__ = {'cubicweb': '>= 3.10.0'}
-    __depends_cubes__ = {'file': '>= 1.9.0',
-		         'folder': '>= 1.1.0',
-		   	 'person': '>= 1.2.0',
-		   	 'comment': '>= 1.2.0',
-		   	 'tag': '>= 1.2.0',
-		   	 'zone': None}
-
-If your cube is packaged for debian, it's a good idea to update the
-`debian/control` file at the same time, so you won't forget it.
-
-
-Step 3: glue everything together in my cube's schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. sourcecode:: python
-
-    from yams.buildobjs import RelationDefinition
-
-    class comments(RelationDefinition):
-	subject = 'Comment'
-	object = 'File'
-	cardinality = '1*'
-	composite = 'object'
-
-    class tags(RelationDefinition):
-	subject = 'Tag'
-	object = 'File'
-
-    class filed_under(RelationDefinition):
-	subject = 'File'
-	object = 'Folder'
-
-    class situated_in(RelationDefinition):
-	subject = 'File'
-	object = 'Zone'
-
-    class displayed_on(RelationDefinition):
-	subject = 'Person'
-	object = 'File'
-
-
-This schema:
-
-* allows to comment and tag on `File` entity type by adding the `comments` and
-  `tags` relations. This should be all we've to do for this feature since the
-  related cubes provide 'pluggable section' which are automatically displayed on
-  the primary view of entity types supporting the relation.
-
-* adds a `situated_in` relation definition so that image entities can be
-  geolocalized.
-
-* add a new relation `displayed_on` relation telling who can be seen on a
-  picture.
-
-This schema will probably have to evolve as time goes (for security handling at
-least), but since the possibility to let a schema evolve is one of CubicWeb's
-features (and goals), we won't worry about it for now and see that later when needed.
-
-
-Step 4: creating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Now that I have a schema, I want to create an instance. To
-do so using this new 'sytweb' cube, I run::
-
-  cubicweb-ctl create sytweb sytweb_instance
-
-Hint: if you get an error while the database is initialized, you can
-avoid having to answer the questions again by running::
-
-   cubicweb-ctl db-create sytweb_instance
-
-This will use your already configured instance and start directly from the create
-database step, thus skipping questions asked by the 'create' command.
-
-Once the instance and database are fully initialized, run ::
-
-  cubicweb-ctl start sytweb_instance
-
-to start the instance, check you can connect on it, etc...
-
--- a/doc/book/en/tutorials/advanced/part02_security.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,440 +0,0 @@
-.. _TutosPhotoWebSiteSecurity:
-
-Security, testing and migration
--------------------------------
-
-This part will cover various topics:
-
-* configuring security
-* migrating existing instance
-* writing some unit tests
-
-Here is the ``read`` security model I want:
-
-* folders, files, images and comments should have one of the following visibility:
-
-  - ``public``, everyone can see it
-  - ``authenticated``, only authenticated users can see it
-  - ``restricted``, only a subset of authenticated users can see it
-
-* managers (e.g. me) can see everything
-* only authenticated users can see people
-* everyone can see classifier entities, such as tag and zone
-
-Also, unless explicitly specified, the visibility of an image should be the same as
-its parent folder, as well as visibility of a comment should be the same as the
-commented entity. If there is no parent entity, the default visibility is
-``authenticated``.
-
-Regarding write security, that's much easier:
-* anonymous can't write anything
-* authenticated users can only add comment
-* managers will add the remaining stuff
-
-Now, let's implement that!
-
-Proper security in CubicWeb is done at the schema level, so you don't have to
-bother with it in views: users will only see what they can see automatically.
-
-.. _adv_tuto_security:
-
-Step 1: configuring security into the schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In schema, you can grant access according to groups, or to some RQL expressions:
-users get access if the expression returns some results. To implement the read
-security defined earlier, groups are not enough, we'll need some RQL expression. Here
-is the idea:
-
-* add a `visibility` attribute on Folder, File and Comment, which may be one of
-  the value explained above
-
-* add a `may_be_read_by` relation from Folder, File and Comment to users,
-  which will define who can see the entity
-
-* security propagation will be done in hook.
-
-So the first thing to do is to modify my cube's schema.py to define those
-relations:
-
-.. sourcecode:: python
-
-    from yams.constraints import StaticVocabularyConstraint
-
-    class visibility(RelationDefinition):
-	subject = ('Folder', 'File', 'Comment')
-	object = 'String'
-	constraints = [StaticVocabularyConstraint(('public', 'authenticated',
-						   'restricted', 'parent'))]
-	default = 'parent'
-	cardinality = '11' # required
-
-    class may_be_read_by(RelationDefinition):
-        __permissions__ = {
-	    'read':   ('managers', 'users'),
-	    'add':    ('managers',),
-	    'delete': ('managers',),
-	    }
-
-	subject = ('Folder', 'File', 'Comment',)
-	object = 'CWUser'
-
-We can note the following points:
-
-* we've added a new `visibility` attribute to folder, file, image and comment
-  using a `RelationDefinition`
-
-* `cardinality = '11'` means this attribute is required. This is usually hidden
-  under the `required` argument given to the `String` constructor, but we can
-  rely on this here (same thing for StaticVocabularyConstraint, which is usually
-  hidden by the `vocabulary` argument)
-
-* the `parent` possible value will be used for visibility propagation
-
-* think to secure the `may_be_read_by` permissions, else any user can add/delete it
-  by default, which somewhat breaks our security model...
-
-Now, we should be able to define security rules in the schema, based on these new
-attribute and relation. Here is the code to add to *schema.py*:
-
-.. sourcecode:: python
-
-    from cubicweb.schema import ERQLExpression
-
-    VISIBILITY_PERMISSIONS = {
-	'read':   ('managers',
-		   ERQLExpression('X visibility "public"'),
-		   ERQLExpression('X may_be_read_by U')),
-	'add':    ('managers',),
-	'update': ('managers', 'owners',),
-	'delete': ('managers', 'owners'),
-	}
-    AUTH_ONLY_PERMISSIONS = {
-	    'read':   ('managers', 'users'),
-	    'add':    ('managers',),
-	    'update': ('managers', 'owners',),
-	    'delete': ('managers', 'owners'),
-	    }
-    CLASSIFIERS_PERMISSIONS = {
-	    'read':   ('managers', 'users', 'guests'),
-	    'add':    ('managers',),
-	    'update': ('managers', 'owners',),
-	    'delete': ('managers', 'owners'),
-	    }
-
-    from cubes.folder.schema import Folder
-    from cubes.file.schema import File
-    from cubes.comment.schema import Comment
-    from cubes.person.schema import Person
-    from cubes.zone.schema import Zone
-    from cubes.tag.schema import Tag
-
-    Folder.__permissions__ = VISIBILITY_PERMISSIONS
-    File.__permissions__ = VISIBILITY_PERMISSIONS
-    Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
-    Comment.__permissions__['add'] = ('managers', 'users',)
-    Person.__permissions__ = AUTH_ONLY_PERMISSIONS
-    Zone.__permissions__ = CLASSIFIERS_PERMISSIONS
-    Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
-
-What's important in there:
-
-* `VISIBILITY_PERMISSIONS` provides read access to managers group, if
-  `visibility` attribute's value is 'public', or if user (designed by the 'U'
-  variable in the expression) is linked to the entity (the 'X' variable) through
-  the `may_be_read_by` permission
-
-* we modify permissions of the entity types we use by importing them and
-  modifying their `__permissions__` attribute
-
-* notice the `.copy()`: we only want to modify 'add' permission for `Comment`,
-  not for all entity types using `VISIBILITY_PERMISSIONS`!
-
-* the remaining part of the security model is done using regular groups:
-
-  - `users` is the group to which all authenticated users will belong
-  - `guests` is the group of anonymous users
-
-
-.. _adv_tuto_security_propagation:
-
-Step 2: security propagation in hooks
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To fullfill the requirements, we have to implement::
-
-  Also, unless explicity specified, visibility of an image should be the same as
-  its parent folder, as well as visibility of a comment should be the same as the
-  commented entity.
-
-This kind of `active` rule will be done using CubicWeb's hook
-system. Hooks are triggered on database events such as addition of a new
-entity or relation.
-
-The tricky part of the requirement is in *unless explicitly specified*, notably
-because when the entity is added, we don't know yet its 'parent'
-entity (e.g. Folder of an File, File commented by a Comment). To handle such things,
-CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
-
-In our case we will:
-
-* on entity creation, schedule an operation that will set default visibility
-
-* when a "parent" relation is added, propagate parent's visibility unless the
-  child already has a visibility set
-
-Here is the code in cube's *hooks.py*:
-
-.. sourcecode:: python
-
-    from cubicweb.predicates import is_instance
-    from cubicweb.server import hook
-
-    class SetVisibilityOp(hook.DataOperationMixIn, hook.Operation):
-
-	def precommit_event(self):
-	    for eid in self.get_data():
-		entity = self.session.entity_from_eid(eid)
-		if entity.visibility == 'parent':
-		    entity.cw_set(visibility=u'authenticated')
-
-    class SetVisibilityHook(hook.Hook):
-	__regid__ = 'sytweb.setvisibility'
-	__select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Comment')
-	events = ('after_add_entity',)
-
-	def __call__(self):
-	    SetVisibilityOp.get_instance(self._cw).add_data(self.entity.eid)
-
-    class SetParentVisibilityHook(hook.Hook):
-	__regid__ = 'sytweb.setparentvisibility'
-	__select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
-	events = ('after_add_relation',)
-
-	def __call__(self):
-	    parent = self._cw.entity_from_eid(self.eidto)
-	    child = self._cw.entity_from_eid(self.eidfrom)
-	    if child.visibility == 'parent':
-		child.cw_set(visibility=parent.visibility)
-
-Notice:
-
-* hooks are application objects, hence have selectors that should match entity or
-  relation types to which the hook applies. To match a relation type, we use the
-  hook specific `match_rtype` selector.
-
-* usage of `DataOperationMixIn`: instead of adding an operation for each added entity,
-  DataOperationMixIn allows to create a single one and to store entity's eids to be
-  processed in the transaction data. This is a good pratice to avoid heavy
-  operations manipulation cost when creating a lot of entities in the same
-  transaction.
-
-* the `precommit_event` method of the operation will be called at transaction's
-  commit time.
-
-* in a hook, `self._cw` is the repository session, not a web request as usually
-  in views
-
-* according to hook's event, you have access to different attributes on the hook
-  instance. Here:
-
-  - `self.entity` is the newly added entity on 'after_add_entity' events
-
-  - `self.eidfrom` / `self.eidto` are the eid of the subject / object entity on
-    'after_add_relation' events (you may also get the relation type using
-    `self.rtype`)
-
-The `parent` visibility value is used to tell "propagate using parent security"
-because we want that attribute to be required, so we can't use None value else
-we'll get an error before we get any chance to propagate...
-
-Now, we also want to propagate the `may_be_read_by` relation. Fortunately,
-CubicWeb provides some base hook classes for such things, so we only have to add
-the following code to *hooks.py*:
-
-.. sourcecode:: python
-
-    # relations where the "parent" entity is the subject
-    S_RELS = set()
-    # relations where the "parent" entity is the object
-    O_RELS = set(('filed_under', 'comments',))
-
-    class AddEntitySecurityPropagationHook(hook.PropagateRelationHook):
-	"""propagate permissions when new entity are added"""
-	__regid__ = 'sytweb.addentity_security_propagation'
-	__select__ = (hook.PropagateRelationHook.__select__
-		      & hook.match_rtype_sets(S_RELS, O_RELS))
-	main_rtype = 'may_be_read_by'
-	subject_relations = S_RELS
-	object_relations = O_RELS
-
-    class AddPermissionSecurityPropagationHook(hook.PropagateRelationAddHook):
-	"""propagate permissions when new entity are added"""
-	__regid__ = 'sytweb.addperm_security_propagation'
-	__select__ = (hook.PropagateRelationAddHook.__select__
-		      & hook.match_rtype('may_be_read_by',))
-	subject_relations = S_RELS
-	object_relations = O_RELS
-
-    class DelPermissionSecurityPropagationHook(hook.PropagateRelationDelHook):
-	__regid__ = 'sytweb.delperm_security_propagation'
-	__select__ = (hook.PropagateRelationDelHook.__select__
-		      & hook.match_rtype('may_be_read_by',))
-	subject_relations = S_RELS
-	object_relations = O_RELS
-
-* the `AddEntitySecurityPropagationHook` will propagate the relation
-  when `filed_under` or `comments` relations are added
-
-  - the `S_RELS` and `O_RELS` set as well as the `match_rtype_sets` selector are
-    used here so that if my cube is used by another one, it'll be able to
-    configure security propagation by simply adding relation to one of the two
-    sets.
-
-* the two others will propagate permissions changes on parent entities to
-  children entities
-
-
-.. _adv_tuto_tesing_security:
-
-Step 3: testing our security
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Security is tricky. Writing some tests for it is a very good idea. You should
-even write them first, as Test Driven Development recommends!
-
-Here is a small test case that will check the basis of our security
-model, in *test/unittest_sytweb.py*:
-
-.. sourcecode:: python
-
-    from cubicweb.devtools.testlib import CubicWebTC
-    from cubicweb import Binary
-
-    class SecurityTC(CubicWebTC):
-
-        def test_visibility_propagation(self):
-            with self.admin_access.repo_cnx() as cnx:
-                # create a user for later security checks
-                toto = self.create_user(cnx, 'toto')
-                cnx.commit()
-                # init some data using the default manager connection
-                folder = cnx.create_entity('Folder',
-                                           name=u'restricted',
-                                           visibility=u'restricted')
-                photo1 = cnx.create_entity('File',
-                                           data_name=u'photo1.jpg',
-                                           data=Binary('xxx'),
-                                           filed_under=folder)
-                cnx.commit()
-                # visibility propagation
-                self.assertEquals(photo1.visibility, 'restricted')
-                # unless explicitly specified
-                photo2 = cnx.create_entity('File',
-                                           data_name=u'photo2.jpg',
-                                           data=Binary('xxx'),
-                                           visibility=u'public',
-                                           filed_under=folder)
-                cnx.commit()
-                self.assertEquals(photo2.visibility, 'public')
-            with self.new_access('toto').repo_cnx() as cnx:
-                # test security
-                self.assertEqual(1, len(cnx.execute('File X'))) # only the public one
-                self.assertEqual(0, len(cnx.execute('Folder X'))) # restricted...
-            with self.admin_access.repo_cnx() as cnx:
-                # may_be_read_by propagation
-                folder = cnx.entity_from_eid(folder.eid)
-                folder.cw_set(may_be_read_by=toto)
-                cnx.commit()
-            with self.new_access('toto').repo_cnx() as cnx:
-                photo1 = cnx.entity_from_eid(photo1.eid)
-                self.failUnless(photo1.may_be_read_by)
-                # test security with permissions
-                self.assertEquals(2, len(cnx.execute('File X'))) # now toto has access to photo2
-                self.assertEquals(1, len(cnx.execute('Folder X'))) # and to restricted folder
-
-    if __name__ == '__main__':
-        from logilab.common.testlib import unittest_main
-        unittest_main()
-
-It's not complete, but shows most things you'll want to do in tests: adding some
-content, creating users and connecting as them in the test, etc...
-
-To run it type:
-
-.. sourcecode:: bash
-
-    $ pytest unittest_sytweb.py
-    ========================  unittest_sytweb.py  ========================
-    -> creating tables [....................]
-    -> inserting default user and default groups.
-    -> storing the schema in the database [....................]
-    -> database for instance data initialized.
-    .
-    ----------------------------------------------------------------------
-    Ran 1 test in 22.547s
-
-    OK
-
-
-The first execution is taking time, since it creates a sqlite database for the
-test instance. The second one will be much quicker:
-
-.. sourcecode:: bash
-
-    $ pytest unittest_sytweb.py
-    ========================  unittest_sytweb.py  ========================
-    .
-    ----------------------------------------------------------------------
-    Ran 1 test in 2.662s
-
-    OK
-
-If you do some changes in your schema, you'll have to force regeneration of that
-database. You do that by removing the tmpdb files before running the test: ::
-
-    $ rm data/database/tmpdb*
-
-
-.. Note::
-  pytest is a very convenient utility used to control test execution. It is available from the `logilab-common`_ package.
-
-.. _`logilab-common`: http://www.logilab.org/project/logilab-common
-
-.. _adv_tuto_migration_script:
-
-Step 4: writing the migration script and migrating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Prior to those changes, I created an instance, fed it with some data, so I
-don't want to create a new one, but to migrate the existing one. Let's see how to
-do that.
-
-Migration commands should be put in the cube's *migration* directory, in a
-file named file:`<X.Y.Z>_Any.py` ('Any' being there mostly for historical reasons).
-
-Here I'll create a *migration/0.2.0_Any.py* file containing the following
-instructions:
-
-.. sourcecode:: python
-
-  add_relation_type('may_be_read_by')
-  add_relation_type('visibility')
-  sync_schema_props_perms()
-
-Then I update the version number in the cube's *__pkginfo__.py* to 0.2.0. And
-that's it! Those instructions will:
-
-* update the instance's schema by adding our two new relations and update the
-  underlying database tables accordingly (the first two instructions)
-
-* update schema's permissions definition (the last instruction)
-
-
-To migrate my instance I simply type::
-
-   cubicweb-ctl upgrade sytweb_instance
-
-You'll then be asked some questions to do the migration step by step. You should say
-YES when it asks if a backup of your database should be done, so you can get back
-to initial state if anything goes wrong...
--- a/doc/book/en/tutorials/advanced/part03_bfss.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-Storing images on the file-system
----------------------------------
-
-Step 1: configuring the BytesFileSystem storage
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To avoid cluttering my database, and to ease file manipulation, I don't want them
-to be stored in the database. I want to be able create File entities for some
-files on the server file system, where those file will be accessed to get
-entities data. To do so I've to set a custom :class:`BytesFileSystemStorage`
-storage for the File 'data' attribute, which hold the actual file's content.
-
-Since the function to register a custom storage needs to have a repository
-instance as first argument, we've to call it in a server startup hook. So I added
-in `cubes/sytweb/hooks.py` :
-
-.. sourcecode:: python
-
-    from os import makedirs
-    from os.path import join, exists
-
-    from cubicweb.server import hook
-    from cubicweb.server.sources import storages
-
-    class ServerStartupHook(hook.Hook):
-        __regid__ = 'sytweb.serverstartup'
-        events = ('server_startup', 'server_maintenance')
-
-        def __call__(self):
-            bfssdir = join(self.repo.config.appdatahome, 'bfss')
-            if not exists(bfssdir):
-                makedirs(bfssdir)
-                print 'created', bfssdir
-            storage = storages.BytesFileSystemStorage(bfssdir)
-            storages.set_attribute_storage(self.repo, 'File', 'data', storage)
-
-.. Note::
-
-  * how we built the hook's registry identifier (`__regid__`): you can introduce
-    'namespaces' by using there python module like naming identifiers. This is
-    especially important for hooks where you usually want a new custom hook, not
-    overriding / specializing an existant one, but the concept may be applied to
-    any application objects
-
-  * we catch two events here: "server_startup" and "server_maintenance". The first
-    is called on regular repository startup (eg, as a server), the other for
-    maintenance task such as shell or upgrade. In both cases, we need to have
-    the storage set, else we'll be in trouble...
-
-  * the path given to the storage is the place where file added through the ui
-    (or in the database before migration) will be located
-
-  * beware that by doing this, you can't anymore write queries that will try to
-    restrict on File `data` attribute. Hopefuly we don't do that usually
-    on file's content or more generally on attributes for the Bytes type
-
-Now, if you've already added some photos through the web ui, you'll have to
-migrate existing data so file's content will be stored on the file-system instead
-of the database. There is a migration command to do so, let's run it in the
-cubicweb shell (in real life, you would have to put it in a migration script as we
-have seen last time):
-
-::
-
-   $ cubicweb-ctl shell sytweb_instance
-   entering the migration python shell
-   just type migration commands or arbitrary python code and type ENTER to execute it
-   type "exit" or Ctrl-D to quit the shell and resume operation
-   >>> storage_changed('File', 'data')
-   [........................]
-
-
-That's it. Now, files added through the web ui will have their content stored on
-the file-system, and you'll also be able to import files from the file-system as
-explained in the next part.
-
-Step 2: importing some data into the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Hey, we start to have some nice features, let us give a try to this new web
-site. For instance if I have a 'photos/201005WePyrenees' containing pictures for
-a particular event, I can import it to my web site by typing ::
-
-  $ cubicweb-ctl fsimport -F sytweb_instance photos/201005WePyrenees/
-  ** importing directory /home/syt/photos/201005WePyrenees
-  importing IMG_8314.JPG
-  importing IMG_8274.JPG
-  importing IMG_8286.JPG
-  importing IMG_8308.JPG
-  importing IMG_8304.JPG
-
-.. Note::
-  The -F option means that folders should be mapped, hence my photos will be
-  linked to a Folder entity corresponding to the file-system folder.
-
-Let's take a look at the web ui:
-
-.. image:: ../../images/tutos-photowebsite_ui1.png
-
-Nothing different, I can't see the new folder... But remember our security model!
-By default, files are only accessible to authenticated users, and I'm looking at
-the site as anonymous, e.g. not authenticated. If I login, I can now see:
-
-.. image:: ../../images/tutos-photowebsite_ui2.png
-
-Yeah, it's there! You will notice that I can see some entities as well as
-folders and images the anonymous user can't. It just works **everywhere in the
-ui** since it's handled at the repository level, thanks to our security model.
-
-Now if I click on the recently inserted folder, I can see
-
-.. image:: ../../images/tutos-photowebsite_ui3.png
-
-Great! There is even my pictures in the folder. I can know give to this folder a
-nicer name (provided I don't intend to import from it anymore, else already
-imported photos will be reimported), change permissions, title for some pictures,
-etc... Having a good content is much more difficult than having a good web site
-;)
-
-
-Conclusion
-~~~~~~~~~~
-
-We started to see here an advanced feature of our repository: the ability
-to store some parts of our data-model into a custom storage, outside the
-database. There is currently only the :class:`BytesFileSystemStorage` available,
-but you can expect to see more coming in a near future (or write your own!).
-
-Also, we can know start to feed our web-site with some nice pictures!
-The site isn't perfect (far from it actually) but it's usable, and we can
-now start using it and improve it on the way. The Incremental Cubic Way :)
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,361 +0,0 @@
-Let's make it more user friendly
-================================
-
-
-Step 1: let's improve site's usability for our visitors
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The first thing I've noticed is that people to whom I send links to photos with
-some login/password authentication get lost, because they don't grasp they have
-to login by clicking on the 'authenticate' link. That's much probably because
-they only get a 404 when trying to access an unauthorized folder, and the site
-doesn't make clear that 1. you're not authenticated, 2. you could get more
-content by authenticating yourself.
-
-So, to improve this situation, I decided that I should:
-
-* make a login box appears for anonymous, so they see at a first glance a place
-  to put the login / password information I provided
-
-* customize the 404 page, proposing to login to anonymous.
-
-Here is the code, samples from my cube's `views.py` file:
-
-.. sourcecode:: python
-
-    from cubicweb.predicates import is_instance
-    from cubicweb.web import component
-    from cubicweb.web.views import error
-    from cubicweb.predicates import anonymous_user
-
-    class FourOhFour(error.FourOhFour):
-	__select__ = error.FourOhFour.__select__ & anonymous_user()
-
-	def call(self):
-	    self.w(u"<h1>%s</h1>" % self._cw._('this resource does not exist'))
-	    self.w(u"<p>%s</p>" % self._cw._('have you tried to login?'))
-
-
-    class LoginBox(component.CtxComponent):
-	"""display a box containing links to all startup views"""
-	__regid__ = 'sytweb.loginbox'
-	__select__ = component.CtxComponent.__select__ & anonymous_user()
-
-	title = _('Authenticate yourself')
-	order = 70
-
-	def render_body(self, w):
-	    cw = self._cw
-	    form = cw.vreg['forms'].select('logform', cw)
-	    form.render(w=w, table_class='', display_progress_div=False)
-
-The first class provides a new specific implementation of the default page you
-get on 404 error, to display an adapted message to anonymous user.
-
-.. Note::
-
-  Thanks to the selection mecanism, it will be selected for anoymous user,
-  since the additional `anonymous_user()` selector gives it a higher score than
-  the default, and not for authenticated since this selector will return 0 in
-  such case (hence the object won't be selectable)
-
-The second class defines a simple box, that will be displayed by default with
-boxes in the left column, thanks to default :class:`component.CtxComponent`
-selector. The HTML is written to match default CubicWeb boxes style. The code
-fetch the actual login form and render it.
-
-
-.. figure:: ../../images/tutos-photowebsite_login-box.png
-   :alt: login box / 404 screenshot
-
-   The login box and the custom 404 page for an anonymous visitor (translated in french)
-
-
-Step 2: providing a custom index page
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Another thing we can easily do to improve the site is... A nicer index page
-(e.g. the first page you get when accessing the web site)! The default one is
-quite intimidating (that should change in a near future). I will provide a much
-simpler index page that simply list available folders (e.g. photo albums in that
-site).
-
-.. sourcecode:: python
-
-    from cubicweb.web.views import startup
-
-    class IndexView(startup.IndexView):
-	def call(self, **kwargs):
-	    self.w(u'<div>\n')
-	    if self._cw.cnx.anonymous_connection:
-		self.w(u'<h4>%s</h4>\n' % self._cw._('Public Albums'))
-	    else:
-		self.w(u'<h4>%s</h4>\n' % self._cw._('Albums for %s') % self._cw.user.login)
-	    self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
-	    self.w(u'</div>\n')
-
-    def registration_callback(vreg):
-	vreg.register_all(globals().values(), __name__, (IndexView,))
-	vreg.register_and_replace(IndexView, startup.IndexView)
-
-As you can see, we override the default index view found in
-`cubicweb.web.views.startup`, geting back nothing but its identifier and selector
-since we override the top level view's `call` method.
-
-.. Note::
-
-  in that case, we want our index view to **replace** the existing one. To do so
-  we've to implements the `registration_callback` function, in which we tell to
-  register everything in the module *but* our IndexView, then we register it
-  instead of the former index view.
-
-Also, we added a title that tries to make it more evident that the visitor is
-authenticated, or not. Hopefuly people will get it now!
-
-
-.. figure:: ../../images/tutos-photowebsite_index-before.png
-   :alt: default index page screenshot
-
-   The default index page
-
-.. figure:: ../../images/tutos-photowebsite_index-after.png
-   :alt: new index page screenshot
-
-   Our simpler, less intimidating, index page (still translated in french)
-
-
-Step 3: more navigation improvments
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-There are still a few problems I want to solve...
-
-* Images in a folder are displayed in a somewhat random order. I would like to
-  have them ordered by file's name (which will usually, inside a given folder,
-  also result ordering photo by their date and time)
-
-* When clicking a photo from an album view, you've to get back to the gallery
-  view to go to the next photo. This is pretty annoying...
-
-* Also, when viewing an image, there is no clue about the folder to which this
-  image belongs to.
-
-I will first try to explain the ordering problem. By default, when accessing
-related entities by using the ORM's API, you should get them ordered according to
-the target's class `cw_fetch_order`. If we take a look at the file cube'schema,
-we can see:
-
-.. sourcecode:: python
-
-    class File(AnyEntity):
-	"""customized class for File entities"""
-	__regid__ = 'File'
-	fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title'])
-
-
-By default, `fetch_config` will return a `cw_fetch_order` method that will order
-on the first attribute in the list. So, we could expect to get files ordered by
-their name. But we don't.  What's up doc ?
-
-The problem is that files are related to folder using the `filed_under` relation.
-And that relation is ambiguous, eg it can lead to `File` entities, but also to
-`Folder` entities. In such case, since both entity types doesn't share the
-attribute on which we want to sort, we'll get linked entities sorted on a common
-attribute (usually `modification_date`).
-
-To fix this, we've to help the ORM. We'll do this in the method from the `ITree`
-folder's adapter, used in the folder's primary view to display the folder's
-content. Here's the code, that I've put in our cube's `entities.py` file, since
-it's more logical stuff than view stuff:
-
-.. sourcecode:: python
-
-    from cubes.folder import entities as folder
-
-    class FolderITreeAdapter(folder.FolderITreeAdapter):
-
-	def different_type_children(self, entities=True):
-	    rql = self.entity.cw_related_rql(self.tree_relation,
-					     self.parent_role, ('File',))
-	    rset = self._cw.execute(rql, {'x': self.entity.eid})
-	    if entities:
-		return list(rset.entities())
-	    return rset
-
-    def registration_callback(vreg):
-	vreg.register_and_replace(FolderITreeAdapter, folder.FolderITreeAdapter)
-
-As you can see, we simple inherit from the adapter defined in the `folder` cube,
-then we override the `different_type_children` method to give a clue to the ORM's
-`cw_related_rql` method, that is responsible to generate the rql to get entities
-related to the folder by the `filed_under` relation (the value of the
-`tree_relation` attribute).  The clue is that we only want to consider the `File`
-target entity type. By doing this, we remove the ambiguity and get back a RQL
-query that correctly order files by their `data_name` attribute.
-
-
-.. Note::
-
-    * As seen earlier, we want to **replace** the folder's `ITree` adapter by our
-      implementation, hence the custom `registration_callback` method.
-
-
-Ouf. That one was tricky...
-
-Now the easier parts. Let's start by adding some links on the file's primary view
-to see the previous / next image in the same folder. CubicWeb's provide a
-component that do exactly that. To make it appears, one have to be adaptable to
-the `IPrevNext` interface. Here is the related code sample, extracted from our
-cube's `views.py` file:
-
-.. sourcecode:: python
-
-    from cubicweb.predicates import is_instance
-    from cubicweb.web.views import navigation
-
-
-    class FileIPrevNextAdapter(navigation.IPrevNextAdapter):
-	__select__ = is_instance('File')
-
-	def previous_entity(self):
-	    rset = self._cw.execute('File F ORDERBY FDN DESC LIMIT 1 WHERE '
-				    'X filed_under FOLDER, F filed_under FOLDER, '
-				    'F data_name FDN, X data_name > FDN, X eid %(x)s',
-				    {'x': self.entity.eid})
-	    if rset:
-		return rset.get_entity(0, 0)
-
-	def next_entity(self):
-	    rset = self._cw.execute('File F ORDERBY FDN ASC LIMIT 1 WHERE '
-				    'X filed_under FOLDER, F filed_under FOLDER, '
-				    'F data_name FDN, X data_name < FDN, X eid %(x)s',
-				    {'x': self.entity.eid})
-	    if rset:
-		return rset.get_entity(0, 0)
-
-
-The `IPrevNext` interface implemented by the adapter simply consist in the
-`previous_entity` / `next_entity` methods, that should respectivly return the
-previous / next entity or `None`. We make an RQL query to get files in the same
-folder, ordered similarly (eg by their `data_name` attribute). We set
-ascendant/descendant ordering and a strict comparison with current file's name
-(the "X" variable representing the current file).
-
-Notice that this query supposes we wont have two files of the same name in the
-same folder, else things may go wrong. Fixing this is out of the scope of this
-blog. And as I would like to have at some point a smarter, context sensitive
-previous/next entity, I'll probably never fix this query (though if I had to, I
-would probably choosing to add a constraint in the schema so that we can't add
-two files of the same name in a folder).
-
-One more thing: by default, the component will be displayed below the content
-zone (the one with the white background). You can change this in the site's
-properties through the ui, but you can also change the default value in the code
-by modifying the `context` attribute of the component:
-
-.. sourcecode:: python
-
-    navigation.NextPrevNavigationComponent.context = 'navcontentbottom'
-
-.. Note::
-
-   `context` may be one of 'navtop', 'navbottom', 'navcontenttop' or
-   'navcontentbottom'; the first two being outside the main content zone, the two
-   others inside it.
-
-.. figure:: ../../images/tutos-photowebsite_prevnext.png
-   :alt: screenshot of the previous/next entity component
-
-   The previous/next entity component, at the bottom of the main content zone.
-
-Now, the only remaining stuff in my todo list is to see the file's folder. I'll use
-the standard breadcrumb component to do so. Similarly as what we've seen before, this
-component is controled by the :class:`IBreadCrumbs` interface, so we'll have to provide a custom
-adapter for `File` entity, telling the a file's parent entity is its folder:
-
-.. sourcecode:: python
-
-    from cubicweb.web.views import ibreadcrumbs
-
-    class FileIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
-	__select__ = is_instance('File')
-
-	def parent_entity(self):
-	    if self.entity.filed_under:
-		return self.entity.filed_under[0]
-
-In that case, we simply use attribute notation provided by the ORM to get the
-folder in which the current file (e.g. `self.entity`) is located.
-
-.. Note::
-
-   The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
-   :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
-   at the value returned by its `parent_entity` method. It also provides a
-   default implementation for this method for entities adapting to the `ITree`
-   interface, but as our `File` doesn't, we've to provide a custom adapter.
-
-.. figure:: ../../images/tutos-photowebsite_breadcrumbs.png
-   :alt: screenshot of the breadcrumb component
-
-   The breadcrumb component when on a file entity, now displaying parent folder.
-
-
-Step 4: preparing the release and migrating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Now that greatly enhanced our cube, it's time to release it to upgrade production site.
-I'll probably detail that process later, but I currently simply transfer the new code
-to the server running the web site.
-
-However, I've still today some step to respect to get things done properly...
-
-First, as I've added some translatable string, I've to run: ::
-
-  $ cubicweb-ctl i18ncube sytweb
-
-To update the cube's gettext catalogs (the '.po' files under the cube's `i18n`
-directory). Once the above command is executed, I'll then update translations.
-
-To see if everything is ok on my test instance, I do: ::
-
-  $ cubicweb-ctl i18ninstance sytweb
-  $ cubicweb-ctl start -D sytweb
-
-The first command compile i18n catalogs (e.g. generates '.mo' files) for my test
-instance. The second command start it in debug mode, so I can open my browser and
-navigate through the web site to see if everything is ok...
-
-.. Note::
-
-   In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
-   in the two other, it refers to the **instance** (if you can't see the
-   difference, reread CubicWeb's concept chapter !).
-
-
-Once I've checked it's ok, I simply have to bump the version number in the
-`__pkginfo__` module to trigger a migration once I'll have updated the code on
-the production site. I can check then check the migration is also going fine, by
-first restoring a dump from the production site, then upgrading my test instance.
-
-To generate a dump from the production site: ::
-
-  $ cubicweb-ctl db-dump sytweb
-  pg_dump -Fc --username=syt --no-owner --file /home/syt/etc/cubicweb.d/sytweb/backup/tmpYIN0YI/system sytweb
-  -> backup file /home/syt/etc/cubicweb.d/sytweb/backup/sytweb-2010-07-13_10-22-40.tar.gz
-
-I can now get back the dump file ('sytweb-2010-07-13_10-22-40.tar.gz') to my test
-machine (using `scp` for instance) to restore it and start migration: ::
-
-  $ cubicweb-ctl db-restore sytweb sytweb-2010-07-13_10-22-40.tar.gz
-  $ cubicweb-ctl upgrade sytweb
-
-You'll have to answer some questions, as we've seen in `an earlier post`_.
-
-Now that everything is tested, I can transfer the new code to the production
-server, `apt-get upgrade` cubicweb and its dependencies, and eventually
-upgrade the production instance.
-
-
-.. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
-.. _`3.8`: http://www.cubicweb.org/blogentry/917107
-.. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
-.. _`an earlier post`: http://www.cubicweb.org/867464
--- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,374 +0,0 @@
-Building my photos web site with |cubicweb| part V: let's make it even more user friendly
-=========================================================================================
-
-.. _uiprops:
-
-Step 1: tired of the default look?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-OK... Now our site has its most desired features. But... I would like to make it look
-somewhat like *my* website. It is not www.cubicweb.org after all. Let's tackle this
-first!
-
-The first thing we can to is to change the logo. There are various way to achieve
-this. The easiest way is to put a :file:`logo.png` file into the cube's :file:`data`
-directory. As data files are looked at according to cubes order (CubicWeb
-resources coming last), that file will be selected instead of CubicWeb's one.
-
-.. Note::
-   As the location for static resources are cached, you'll have to restart
-   your instance for this to be taken into account.
-
-Though there are some cases where you don't want to use a :file:`logo.png` file.
-For instance if it's a JPEG file. You can still change the logo by defining in
-the cube's :file:`uiprops.py` file:
-
-.. sourcecode:: python
-
-   LOGO = data('logo.jpg')
-
-The uiprops machinery is used to define some static file resources,
-such as the logo, default Javascript / CSS files, as well as CSS
-properties (we'll see that later).
-
-.. Note::
-   This file is imported specifically by |cubicweb|, with a predefined name space,
-   containing for instance the `data` function, telling the file is somewhere
-   in a cube or CubicWeb's data directory.
-
-   One side effect of this is that it can't be imported as a regular python
-   module.
-
-The nice thing is that in debug mode, change to a :file:`uiprops.py` file are detected
-and then automatically reloaded.
-
-Now, as it's a photos web-site, I would like to have a photo of mine as background...
-After some trials I won't detail here, I've found a working recipe explained `here`_.
-All I've to do is to override some stuff of the default CubicWeb user interface to
-apply it as explained.
-
-The first thing to to get the ``<img/>`` tag as first element after the
-``<body>`` tag.  If you know a way to avoid this by simply specifying the image
-in the CSS, tell me!  The easiest way to do so is to override the
-:class:`HTMLPageHeader` view, since that's the one that is directly called once
-the ``<body>`` has been written. How did I find this?  By looking in the
-:mod:`cubiweb.web.views.basetemplates` module, since I know that global page
-layouts sits there. I could also have grep the "body" tag in
-:mod:`cubicweb.web.views`... Finding this was the hardest part. Now all I need is
-to customize it to write that ``img`` tag, as below:
-
-.. sourcecode:: python
-
-    class HTMLPageHeader(basetemplates.HTMLPageHeader):
-	# override this since it's the easier way to have our bg image
-	# as the first element following <body>
-	def call(self, **kwargs):
-            self.w(u'<img id="bg-image" src="%sbackground.jpg" alt="background image"/>'
-                   % self._cw.datadir_url)
-	    super(HTMLPageHeader, self).call(**kwargs)
-
-
-    def registration_callback(vreg):
-	vreg.register_all(globals().values(), __name__, (HTMLPageHeader))
-	vreg.register_and_replace(HTMLPageHeader, basetemplates.HTMLPageHeader)
-
-
-As you may have guessed, my background image is in a :file:`background.jpg` file
-in the cube's :file:`data` directory, but there are still some things to explain
-to newcomers here:
-
-* The :meth:`call` method is there the main access point of the view. It's called by
-  the view's :meth:`render` method. It is not the only access point for a view, but
-  this will be detailed later.
-
-* Calling `self.w` writes something to the output stream. Except for binary views
-  (which do not generate text), it *must* be passed an Unicode string.
-
-* The proper way to get a file in :file:`data` directory is to use the `datadir_url`
-  attribute of the incoming request (e.g. `self._cw`).
-
-I won't explain again the :func:`registration_callback` stuff, you should understand it
-now!  If not, go back to previous posts in the series :)
-
-Fine. Now all I've to do is to add a bit of CSS to get it to behave nicely (which
-is not the case at all for now). I'll put all this in a :file:`cubes.sytweb.css`
-file, stored as usual in our :file:`data` directory:
-
-.. sourcecode:: css
-
-
-    /* fixed full screen background image
-     * as explained on http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
-     *
-     * syt update: set z-index=0 on the img instead of z-index=1 on div#page & co to
-     * avoid pb with the user actions menu
-     */
-    img#bg-image {
-	position: fixed;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-	z-index: 0;
-    }
-
-    div#page, table#header, div#footer {
-	background: transparent;
-	position: relative;
-    }
-
-    /* add some space around the logo
-     */
-    img#logo {
-	padding: 5px 15px 0px 15px;
-    }
-
-    /* more dark font for metadata to have a chance to see them with the background
-     *  image
-     */
-    div.metadata {
-	color: black;
-    }
-
-You can see here stuff explained in the cited page, with only a slight modification
-explained in the comments, plus some additional rules to make things somewhat cleaner:
-
-* a bit of padding around the logo
-
-* darker metadata which appears by default below the content (the white frame in the page)
-
-To get this CSS file used everywhere in the site, I have to modify the :file:`uiprops.py` file
-introduced above:
-
-.. sourcecode:: python
-
-   STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.sytweb.css')]
-
-.. Note::
-   `sheet` is another predefined variable containing values defined by
-   already process `:file:`uiprops.py`` file, notably the CubicWeb's one.
-
-Here we simply want our CSS in addition to CubicWeb's base CSS files, so we
-redefine the `STYLESHEETS` variable to existing CSS (accessed through the `sheet`
-variable) with our one added. I could also have done:
-
-.. sourcecode:: python
-
-   sheet['STYLESHEETS'].append(data('cubes.sytweb.css'))
-
-But this is less interesting since we don't see the overriding mechanism...
-
-At this point, the site should start looking good, the background image being
-resized to fit the screen.
-
-.. image:: ../../images/tutos-photowebsite_background-image.png
-
-The final touch: let's customize CubicWeb's CSS to get less orange... By simply adding
-
-.. sourcecode:: python
-
-  contextualBoxTitleBg = incontextBoxTitleBg = '#AAAAAA'
-
-and reloading the page we've just seen, we know have a nice greyed box instead of
-the orange one:
-
-.. image:: ../../images/tutos-photowebsite_grey-box.png
-
-This is because CubicWeb's CSS include some variables which are
-expanded by values defined in uiprops file. In our case we controlled the
-properties of the CSS `background` property of boxes with CSS class
-`contextualBoxTitleBg` and `incontextBoxTitleBg`.
-
-
-Step 2: configuring boxes
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Boxes present to the user some ways to use the application. Let's first do a few
-user interface tweaks in our :file:`views.py` file:
-
-.. sourcecode:: python
-
-  from cubicweb.predicates import none_rset
-  from cubicweb.web.views import bookmark
-  from cubes.zone import views as zone
-  from cubes.tag import views as tag
-
-  # change bookmarks box selector so it's only displayed on startup views
-  bookmark.BookmarksBox.__select__ = bookmark.BookmarksBox.__select__ & none_rset()
-  # move zone box to the left instead of in the context frame and tweak its order
-  zone.ZoneBox.context = 'left'
-  zone.ZoneBox.order = 100
-  # move tags box to the left instead of in the context frame and tweak its order
-  tag.TagsBox.context = 'left'
-  tag.TagsBox.order = 102
-  # hide similarity box, not interested
-  tag.SimilarityBox.visible = False
-
-The idea is to move all boxes in the left column, so we get more space for the
-photos.  Now, serious things: I want a box similar to the tags box but to handle
-the `Person displayed_on File` relation. We can do this simply by adding a
-:class:`AjaxEditRelationCtxComponent` subclass to our views, as below:
-
-.. sourcecode:: python
-
-    from logilab.common.decorators import monkeypatch
-    from cubicweb import ValidationError
-    from cubicweb.web.views import uicfg, component
-    from cubicweb.web.views import basecontrollers
-
-    # hide displayed_on relation using uicfg since it will be displayed by the box below
-    uicfg.primaryview_section.tag_object_of(('*', 'displayed_on', '*'), 'hidden')
-
-    class PersonBox(component.AjaxEditRelationCtxComponent):
-	__regid__ = 'sytweb.displayed-on-box'
-	# box position
-	order = 101
-	context = 'left'
-	# define relation to be handled
-	rtype = 'displayed_on'
-	role = 'object'
-	target_etype = 'Person'
-	# messages
-	added_msg = _('person has been added')
-	removed_msg = _('person has been removed')
-	# bind to js_* methods of the json controller
-	fname_vocabulary = 'unrelated_persons'
-	fname_validate = 'link_to_person'
-	fname_remove = 'unlink_person'
-
-
-    @monkeypatch(basecontrollers.JSonController)
-    @basecontrollers.jsonize
-    def js_unrelated_persons(self, eid):
-	"""return tag unrelated to an entity"""
-	rql = "Any F + ' ' + S WHERE P surname S, P firstname F, X eid %(x)s, NOT P displayed_on X"
-	return [name for (name,) in self._cw.execute(rql, {'x' : eid})]
-
-
-    @monkeypatch(basecontrollers.JSonController)
-    def js_link_to_person(self, eid, people):
-	req = self._cw
-	for name in people:
-	    name = name.strip().title()
-	    if not name:
-		continue
-	    try:
-		firstname, surname = name.split(None, 1)
-	    except:
-		raise ValidationError(eid, {('displayed_on', 'object'): 'provide <first name> <surname>'})
-	    rset = req.execute('Person P WHERE '
-			       'P firstname %(firstname)s, P surname %(surname)s',
-			       locals())
-	    if rset:
-		person = rset.get_entity(0, 0)
-	    else:
-		person = req.create_entity('Person', firstname=firstname,
-						surname=surname)
-	    req.execute('SET P displayed_on X WHERE '
-			'P eid %(p)s, X eid %(x)s, NOT P displayed_on X',
-			{'p': person.eid, 'x' : eid})
-
-    @monkeypatch(basecontrollers.JSonController)
-    def js_unlink_person(self, eid, personeid):
-	self._cw.execute('DELETE P displayed_on X WHERE P eid %(p)s, X eid %(x)s',
-			 {'p': personeid, 'x': eid})
-
-
-You basically subclass to configure with some class attributes. The `fname_*`
-attributes give the name of methods that should be defined on the json control to
-make the AJAX part of the widget work: one to get the vocabulary, one to add a
-relation and another to delete a relation. These methods must start by a `js_`
-prefix and are added to the controller using the `@monkeypatch` decorator. In my
-case, the most complicated method is the one which adds a relation, since it
-tries to see if the person already exists, and else automatically create it,
-assuming the user entered "firstname surname".
-
-Let's see how it looks like on a file primary view:
-
-.. image:: ../../images/tutos-photowebsite_boxes.png
-
-Great, it's now as easy for me to link my pictures to people than to tag them.
-Also, visitors get a consistent display of these two pieces of information.
-
-.. Note::
-  The ui component system has been refactored in `CubicWeb 3.10`_, which also
-  introduced the :class:`AjaxEditRelationCtxComponent` class.
-
-
-Step 3: configuring facets
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The last feature we'll add today is facet configuration. If you access to the
-'/file' url, you'll see a set of 'facets' appearing in the left column. Facets
-provide an intuitive way to build a query incrementally, by proposing to the user
-various way to restrict the result set. For instance CubicWeb proposes a facet to
-restrict based on who created an entity; the tag cube proposes a facet to
-restrict based on tags; the zoe cube a facet to restrict based on geographical
-location, and so on. In that gist, I want to propose a facet to restrict based on
-the people displayed on the picture. To do so, there are various classes in the
-:mod:`cubicweb.web.facet` module which simply have to be configured using class
-attributes as we've done for the box. In our case, we'll define a subclass of
-:class:`RelationFacet`.
-
-.. Note::
-
-   Since that's ui stuff, we'll continue to add code below to our
-   :file:`views.py` file. Though we begin to have a lot of various code their, so
-   it's may be a good time to split our views module into submodules of a `view`
-   package. In our case of a simple application (glue) cube, we could start using
-   for instance the layout below: ::
-
-     views/__init__.py   # uicfg configuration, facets
-     views/layout.py     # header/footer/background stuff
-     views/components.py # boxes, adapters
-     views/pages.py      # index view, 404 view
-
-.. sourcecode:: python
-
-    from cubicweb.web import facet
-
-    class DisplayedOnFacet(facet.RelationFacet):
-	__regid__ = 'displayed_on-facet'
-	# relation to be displayed
-	rtype = 'displayed_on'
-	role = 'object'
-	# view to use to display persons
-	label_vid = 'combobox'
-
-Let's say we also want to filter according to the `visibility` attribute. This is
-even simpler as we just have to derive from the :class:`AttributeFacet` class:
-
-.. sourcecode:: python
-
-    class VisibilityFacet(facet.AttributeFacet):
-	__regid__ = 'visibility-facet'
-	rtype = 'visibility'
-
-Now if I search for some pictures on my site, I get the following facets available:
-
-.. image:: ../../images/tutos-photowebsite_facets.png
-
-.. Note::
-
-  By default a facet must be applyable to every entity in the result set and
-  provide at leat two elements of vocabulary to be displayed (for instance you
-  won't see the `created_by` facet if the same user has created all
-  entities). This may explain why you don't see yours...
-
-
-Conclusion
-~~~~~~~~~~
-
-We started to see the power behind the infrastructure provided by the
-framework, both on the pure ui (CSS, Javascript) side and on the Python side
-(high level generic classes for components, including boxes and facets). We now
-have, with a few lines of code, a full-featured web site with a personalized look.
-
-Of course we'll probably want more as time goes, but we can now
-concentrate on making good pictures, publishing albums and sharing them with
-friends...
-
-
-
-.. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518
-.. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
--- a/doc/book/en/tutorials/base/blog-in-five-minutes.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _TutosBaseBlogFiveMinutes:
-
-Get a blog running in five minutes!
------------------------------------
-
-For Debian or Ubuntu users, first install the following packages
-(:ref:`DebianInstallation`)::
-
-    cubicweb, cubicweb-dev, cubicweb-blog
-
-Windows or Mac OS X users must install |cubicweb| from source (see
-:ref:`SourceInstallation` and :ref:`WindowsInstallation`).
-
-Then create and initialize your instance::
-
-    cubicweb-ctl create blog myblog
-
-You'll be asked a few questions, and you can keep the default answer for most of
-them. The one question you'll have to think about is the database you'll want to
-use for that instance. For a quick test, if you don't have `postgresql` installed
-and configured (see :ref:`PostgresqlConfiguration`), it's highly recommended to
-choose `sqlite` when asked for which database driver to use, since it has a much
-simple setup (no database server needed).
-
-One the process is completed (including database initialisation), you can start
-your instance by using: ::
-
-    cubicweb-ctl start -D myblog
-
-The `-D` option activates the debugging mode. Removing it will launch the instance
-as a daemon in the background, and ``cubicweb-ctl stop myblog`` will stop
-it in that case. 
-
-
-About file system permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Unless you installed from sources, the above commands assume that you have root
-access to the :file:`/etc/` directory. In order to initialize your instance as a
-regular user, within your home directory, you can use the :envvar:`CW_MODE`
-environment variable: ::
-
-  export CW_MODE=user
-
-then create a :file:`~/etc/cubicweb.d` directory that will hold your instances.
-
-More information about how to configure your own environment is
-available in :ref:`ResourceMode`.
-
-
-Instance parameters
-~~~~~~~~~~~~~~~~~~~
-
-If you would like to change database parameters such as the database host or the
-user name used to connect to the database, edit the `sources` file located in the
-:file:`/etc/cubicweb.d/myblog` directory.
-
-Then relaunch the database creation::
-
-     cubicweb-ctl db-create myblog
-
-Other parameters, like web server or emails parameters, can be modified in the
-:file:`/etc/cubicweb.d/myblog/all-in-one.conf` file.
-
-You'll have to restart the instance after modification in one of those files.
-
-This is it. Your blog is functional and running. Visit http://localhost:8080 and enjoy it!
-
--- a/doc/book/en/tutorials/base/conclusion.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-What's next?
-------------
-
-In this tutorial, we have seen that you can, right after the installation of
-|cubicweb|, build a web application in a few minutes by defining a data model as
-assembling cubes. You get a working application that you can then customize there
-and there while keeping something that works. This is important in agile
-development practices, you can right from the start of the project show things
-to customer and so take the right decision early in the process.
-
-The next steps will be to discover hooks, security, data sources, digging deeper
-into view writing and interface customisation... Yet a lot of fun stuff to
-discover! You will find more `tutorials and howtos`_ in the blog published on the
-CubicWeb.org website.
-
-.. _`tutorials and howtos`: http://www.cubicweb.org/view?rql=Any+X+ORDERBY+D+DESC+WHERE+X+is+BlogEntry%2C+T+tags+X%2C+T+name+IN+%28%22tutorial%22%2C+%22howto%22%29%2C+X+creation_date+D
--- a/doc/book/en/tutorials/base/customizing-the-application.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,539 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _TutosBaseCustomizingTheApplication:
-
-Customizing your application
-----------------------------
-
-So far so good. The point is that usually, you won't get enough by assembling
-cubes out-of-the-box. You will want to customize them, have a personal look and
-feel, add your own data model and so on. Or maybe start from scratch?
-
-So let's get a bit deeper and start coding our own cube. In our case, we want
-to customize the blog we created to add more features to it.
-
-
-Create your own cube
-~~~~~~~~~~~~~~~~~~~~
-
-First, notice that if you've installed |cubicweb| using Debian packages, you will
-need the additional ``cubicweb-dev`` package to get the commands necessary to
-|cubicweb| development. All `cubicweb-ctl` commands are described in details in
-:ref:`cubicweb-ctl`.
-
-Once your |cubicweb| development environment is set up, you can create a new
-cube::
-
-  cubicweb-ctl newcube myblog
-
-This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source
-installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation)
-a directory named :file:`blog` reflecting the structure described in
-:ref:`cubelayout`.
-
-For packages installation, you can still create new cubes in your home directory
-using the following configuration. Let's say you want to develop your new cubes
-in `~src/cubes`, then set the following environment variables: ::
-
-  CW_CUBES_PATH=~/src/cubes
-
-and then create your new cube using: ::
-
-  cubicweb-ctl newcube --directory=~/src/cubes myblog
-
-.. Note::
-
-   We previously used `myblog` as the name of our *instance*. We're now creating
-   a *cube* with the same name. Both are different things. We'll now try to
-   specify when we talk about one or another, but keep in mind this difference.
-
-
-Cube metadata
-~~~~~~~~~~~~~
-
-A simple set of metadata about your cube are stored in the :file:`__pkginfo__.py`
-file. In our case, we want to extend the blog cube, so we have to tell that our
-cube depends on this cube, by modifying the ``__depends__`` dictionary in that
-file:
-
-.. sourcecode:: python
-
-   __depends__ =  {'cubicweb': '>= 3.10.7',
-                   'cubicweb-blog': None}
-
-where the ``None`` means we do not depends on a particular version of the cube.
-
-.. _TutosBaseCustomizingTheApplicationDataModel:
-
-Extending the data model
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-The data model or schema is the core of your |cubicweb| application.  It defines
-the type of content your application will handle. It is defined in the file
-:file:`schema.py` of the cube.
-
-
-Defining our model
-******************
-
-For the sake of example, let's say we want a new entity type named `Community`
-with a name, a description. A `Community` will hold several blogs.
-
-.. sourcecode:: python
-
-  from yams.buildobjs import EntityType, RelationDefinition, String, RichString
-
-  class Community(EntityType):
-      name = String(maxsize=50, required=True)
-      description = RichString()
-
-  class community_blog(RelationDefinition):
-      subject = 'Community'
-      object = 'Blog'
-      cardinality = '*?'
-      composite = 'subject'
-
-The first step is the import from the :mod:`yams` package necessary classes to build
-the schema.
-
-This file defines the following:
-
-* a `Community` has a title and a description as attributes
-
-  - the name is a string that is required and can't be longer than 50 characters
-
-  - the description is a string that is not constrained and may contains rich
-    content such as HTML or Restructured text.
-
-* a `Community` may be linked to a `Blog` using the `community_blog` relation
-
-  - ``*`` means a community may be linked to 0 to N blog, ``?`` means a blog may
-    be linked to 0 to 1 community. For completeness, remember that you can also
-    use ``+`` for 1 to N, and ``1`` for single, mandatory relation (e.g. one to one);
-
-  - this is a composite relation where `Community` (e.g. the subject of the
-    relation) is the composite. That means that if you delete a community, its
-    blog will be deleted as well.
-
-Of course, there are a lot of other data types and things such as constraints,
-permissions, etc, that may be defined in the schema, but those won't be covered
-in this tutorial.
-
-Notice that our schema refers to the `Blog` entity type which is not defined
-here.  But we know this type is available since we depend on the `blog` cube
-which is defining it.
-
-
-Applying changes to the model into our instance
-***********************************************
-
-Now the problem is that we created an instance using the `blog` cube, not our
-`myblog` cube, so if we don't do anything there is no way that we'll see anything
-changing in the instance.
-
-One easy way, as we've no really valuable data in the instance would be to trash and recreated it::
-
-  cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
-  cubicweb-ctl delete myblog
-  cubicweb-ctl create myblog
-  cubicweb-ctl start -D myblog
-
-Another way is to add our cube to the instance using the cubicweb-ctl shell
-facility. It's a python shell connected to the instance with some special
-commands available to manipulate it (the same as you'll have in migration
-scripts, which are not covered in this tutorial). In that case, we're interested
-in the `add_cube` command: ::
-
-  $ cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
-  $ cubicweb-ctl shell myblog
-  entering the migration python shell
-  just type migration commands or arbitrary python code and type ENTER to execute it
-  type "exit" or Ctrl-D to quit the shell and resume operation
-  >>> add_cube('myblog')
-  >>>
-  $ cubicweb-ctl start -D myblog
-
-The `add_cube` command is enough since it automatically updates our
-application to the cube's schema. There are plenty of other migration
-commands of a more finer grain. They are described in :ref:`migration`
-
-As explained, leave the shell by typing Ctrl-D. If you restart the instance and
-take another look at the schema, you'll see that changes to the data model have
-actually been applied (meaning database schema updates and all necessary stuff
-has been done).
-
-.. image:: ../../images/tutos-base_myblog-schema_en.png
-   :alt: the instance schema after adding our cube
-
-If you follow the 'info' link in the user pop-up menu, you'll also see that the
-instance is using blog and myblog cubes.
-
-.. image:: ../../images/tutos-base_myblog-siteinfo_en.png
-   :alt: the instance schema after adding our cube
-
-You can now add some communities, link them to blog, etc... You'll see that the
-framework provides default views for this entity type (we have not yet defined any
-view for it!), and also that the blog primary view will show the community it's
-linked to if any. All this thanks to the model driven interface provided by the
-framework.
-
-You'll then be able to redefine each of them according to your needs
-and preferences. We'll now see how to do such thing.
-
-.. _TutosBaseCustomizingTheApplicationCustomViews:
-
-Defining your views
-~~~~~~~~~~~~~~~~~~~
-
-|cubicweb| provides a lot of standard views in directory
-:file:`cubicweb/web/views/`. We already talked about 'primary' and 'list' views,
-which are views which apply to one ore more entities.
-
-A view is defined by a python class which includes:
-
-  - an identifier: all objects used to build the user interface in |cubicweb| are
-    recorded in a registry and this identifier will be used as a key in that
-    registry. There may be multiple views for the same identifier.
-
-  - a *selector*, which is a kind of filter telling how well a view suit to a
-    particular context. When looking for a particular view (e.g. given an
-    identifier), |cubicweb| computes for each available view with that identifier
-    a score which is returned by the selector. Then the view with the highest
-    score is used. The standard library of predicates is in
-    :mod:`cubicweb.predicates`.
-
-A view has a set of methods inherited from the :class:`cubicweb.view.View` class,
-though you usually don't derive directly from this class but from one of its more
-specific child class.
-
-Last but not least, |cubicweb| provides a set of default views accepting any kind
-of entities.
-
-Want a proof? Create a community as you've already done for other entity types
-through the index page, you'll then see something like that:
-
-.. image:: ../../images/tutos-base_myblog-community-default-primary_en.png
-   :alt: the default primary view for our community entity type
-
-
-If you notice the weird messages that appear in the page: those are messages
-generated for the new data model, which have no translation yet. To fix that,
-we'll have to use dedicated `cubicweb-ctl` commands:
-
-.. sourcecode: bash
-
-  cubicweb-ctl i18ncube myblog # build/update cube's message catalogs
-  # then add translation into .po file into the cube's i18n directory
-  cubicweb-ctl i18ninstance myblog # recompile instance's message catalogs
-  cubicweb-ctl restart -D myblog # instance has to be restarted to consider new catalogs
-
-You'll then be able to redefine each of them according to your needs and
-preferences. So let's see how to do such thing.
-
-Changing the layout of the application
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The layout is the general organization of the pages in the site. Views that generate
-the layout are sometimes referred to as 'templates'. They are implemented in the
-framework in the module :mod:`cubicweb.web.views.basetemplates`. By overriding
-classes in this module, you can customize whatever part you wish of the default
-layout.
-
-But notice that |cubicweb| provides many other ways to customize the
-interface, thanks to actions and components (which you can individually
-(de)activate, control their location, customize their look...) as well as
-"simple" CSS customization. You should first try to achieve your goal using such
-fine grained parametrization rather then overriding a whole template, which usually
-embeds customisation access points that you may loose in the process.
-
-But for the sake of example, let's say we want to change the generic page
-footer...  We can simply add to the module ``views`` of our cube,
-e.g. :file:`cubes/myblog/views.py`, the code below:
-
-.. sourcecode:: python
-
-  from cubicweb.web.views import basetemplates
-
-  class MyHTMLPageFooter(basetemplates.HTMLPageFooter):
-
-      def footer_content(self):
-	  self.w(u'This website has been created with <a href="http://cubicweb.org">CubicWeb</a>.')
-
-  def registration_callback(vreg):
-      vreg.register_all(globals().values(), __name__, (MyHTMLPageFooter,))
-      vreg.register_and_replace(MyHTMLPageFooter, basetemplates.HTMLPageFooter)
-
-
-* Our class inherits from the default page footer to ease getting things right,
-  but this is not mandatory.
-
-* When we want to write something to the output stream, we simply call `self.w`,
-  with *must be passed an unicode string*.
-
-* The latest function is the most exotic stuff. The point is that without it, you
-  would get an error at display time because the framework wouldn't be able to
-  choose which footer to use between :class:`HTMLPageFooter` and
-  :class:`MyHTMLPageFooter`, since both have the same selector, hence the same
-  score...  In this case, we want our footer to replace the default one, so we have
-  to define a :func:`registration_callback` function to control object
-  registration: the first instruction tells to register everything in the module
-  but the :class:`MyHTMLPageFooter` class, then the second to register it instead
-  of :class:`HTMLPageFooter`. Without this function, everything in the module is
-  registered blindly.
-
-.. Note::
-
-  When a view is modified while running in debug mode, it is not required to
-  restart the instance server. Save the Python file and reload the page in your
-  web browser to view the changes.
-
-We will now have this simple footer on every page of the site.
-
-
-Primary view customization
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The 'primary' view (i.e. any view with the identifier set to 'primary') is the one used to
-display all the information about a single entity. The standard primary view is one
-of the most sophisticated views of all. It has several customisation points, but
-its power comes with `uicfg`, allowing you to control it without having to
-subclass it.
-
-However this is a bit off-topic for this first tutorial. Let's say we simply want a
-custom primary view for my `Community` entity type, using directly the view
-interface without trying to benefit from the default implementation (you should
-do that though if you're rewriting reusable cubes; everything is described in more
-details in :ref:`primary_view`).
-
-
-So... Some code! That we'll put again in the module ``views`` of our cube.
-
-.. sourcecode:: python
-
-  from cubicweb.predicates import is_instance
-  from cubicweb.web.views import primary
-
-  class CommunityPrimaryView(primary.PrimaryView):
-      __select__ = is_instance('Community')
-
-      def cell_call(self, row, col):
-          entity = self.cw_rset.get_entity(row, col)
-          self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
-          if entity.description:
-              self.w(u'<p>%s</p>' % entity.printable_value('description'))
-
-What's going on here?
-
-* Our class inherits from the default primary view, here mainly to get the correct
-  view identifier, since we don't use any of its features.
-
-* We set on it a selector telling that it only applies when trying to display
-  some entity of the `Community` type. This is enough to get an higher score than
-  the default view for entities of this type.
-
-* View applying to entities usually have to define `cell_call` as entry point,
-  and are given `row` and `col` arguments tell to which entity in the result set
-  the view is applied. We can then get this entity from the result set
-  (`self.cw_rset`) by using the `get_entity` method.
-
-* To ease thing, we access our entity's attribute for display using its
-  printable_value method, which will handle formatting and escaping when
-  necessary. As you can see, you can also access attributes by their name on the
-  entity to get the raw value.
-
-
-You can now reload the page of the community we just created and see the changes.
-
-.. image:: ../../images/tutos-base_myblog-community-custom-primary_en.png
-   :alt: the custom primary view for our community entity type
-
-We've seen here a lot of thing you'll have to deal with to write views in
-|cubicweb|. The good news is that this is almost everything that is used to
-build higher level layers.
-
-.. Note::
-
-  As things get complicated and the volume of code in your cube increases, you can
-  of course still split your views module into a python package with subpackages.
-
-You can find more details about views and selectors in :ref:`Views`.
-
-
-Write entities to add logic in your data
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-|cubicweb| provides an ORM to easily programmaticaly manipulate
-entities (just like the one we have fetched earlier by calling
-`get_entity` on a result set). By default, entity
-types are instances of the :class:`AnyEntity` class, which holds a set of
-predefined methods as well as property automatically generated for
-attributes/relations of the type it represents.
-
-You can redefine each entity to provide additional methods or whatever you want
-to help you write your application. Customizing an entity requires that your
-entity:
-
-- inherits from :class:`cubicweb.entities.AnyEntity` or any subclass
-
-- defines a :attr:`__regid__` linked to the corresponding data type of your schema
-
-You may then want to add your own methods, override default implementation of some
-method, etc...
-
-.. sourcecode:: python
-
-    from cubicweb.entities import AnyEntity, fetch_config
-
-
-    class Community(AnyEntity):
-        """customized class for Community entities"""
-        __regid__ = 'Community'
-
-        fetch_attrs, cw_fetch_order = fetch_config(['name'])
-
-        def dc_title(self):
-            return self.name
-
-        def display_cw_logo(self):
-            return 'CubicWeb' in self.description
-
-In this example:
-
-* we used convenience :func:`fetch_config` function to tell which attributes
-  should be prefetched by the ORM when looking for some related entities of this
-  type, and how they should be ordered
-
-* we overrode the standard `dc_title` method, used in various place in the interface
-  to display the entity (though in this case the default implementation would
-  have had the same result)
-
-* we implemented here a method :meth:`display_cw_logo` which tests if the blog
-  entry title contains 'CW'.  It can then be used when you're writing code
-  involving 'Community' entities in your views, hooks, etc. For instance, you can
-  modify your previous views as follows:
-
-.. sourcecode:: python
-
-
-  class CommunityPrimaryView(primary.PrimaryView):
-      __select__ = is_instance('Community')
-
-      def cell_call(self, row, col):
-          entity = self.cw_rset.get_entity(row, col)
-          self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
-          if entity.display_cw_logo():
-              self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
-          if entity.description:
-              self.w(u'<p>%s</p>' % entity.printable_value('description'))
-
-Then each community whose description contains 'CW' is shown with the |cubicweb|
-logo in front of it.
-
-.. Note::
-
-  As for view, you don't have to restart your instance when modifying some entity
-  classes while your server is running in debug mode, the code will be
-  automatically reloaded.
-
-
-Extending the application by using more cubes!
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-One of the goal of the |cubicweb| framework was to have truly reusable
-components. To do so, they must both behave nicely when plugged into the
-application and be easily customisable, from the data model to the user
-interface. And I think the result is pretty successful, thanks to system such as
-the selection mechanism and the choice to write views as python code which allows
-to build our page using true object oriented programming techniques, that no
-template language provides.
-
-
-A library of standard cubes is available from `CubicWeb Forge`_, to address a
-lot of common concerns such has manipulating people, files, things to do, etc. In
-our community blog case, we could be interested for instance in functionalities
-provided by the `comment` and `tag` cubes. The former provides threaded
-discussion functionalities, the latter a simple tag mechanism to classify content.
-Let's say we want to try those. We will first modify our cube's :file:`__pkginfo__.py`
-file:
-
-.. sourcecode:: python
-
-   __depends__ =  {'cubicweb': '>= 3.10.7',
-                   'cubicweb-blog': None,
-                   'cubicweb-comment': None,
-                   'cubicweb-tag': None}
-
-Now, we'll simply tell on which entity types we want to activate the 'comment'
-and 'tag' facilities by adding respectively the 'comments' and 'tags' relations on
-them in our schema (:file:`schema.py`).
-
-.. sourcecode:: python
-
-  class comments(RelationDefinition):
-      subject = 'Comment'
-      object = 'BlogEntry'
-      cardinality = '1*'
-      composite = 'object'
-
-  class tags(RelationDefinition):
-      subject = 'Tag'
-      object = ('Community', 'BlogEntry')
-
-
-So in the case above we activated comments on `BlogEntry` entities and tags on
-both `Community` and `BlogEntry`. Various views from both `comment` and `tag`
-cubes will then be automatically displayed when one of those relations is
-supported.
-
-Let's synchronize the data model as we've done earlier: ::
-
-
-  $ cubicweb-ctl stop myblog
-  $ cubicweb-ctl shell myblog
-  entering the migration python shell
-  just type migration commands or arbitrary python code and type ENTER to execute it
-  type "exit" or Ctrl-D to quit the shell and resume operation
-  >>> add_cubes(('comment', 'tag'))
-  >>>
-
-Then restart the instance. Let's look at a blog entry:
-
-.. image:: ../../images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png
-   :alt: the primary view for a blog entry with comments and tags activated
-
-As you can see, we now have a box displaying tags and a section proposing to add
-a comment and displaying existing one below the post. All this without changing
-anything in our views, thanks to the design of generic views provided by the
-framework. Though if we take a look at a community, we won't see the tags box!
-That's because by default this box try to locate itself in the left column within
-the white frame, and this column is handled by the primary view we
-hijacked. Let's change our view to make it more extensible, by keeping both our
-custom rendering but also extension points provided by the default
-implementation.
-
-
-.. sourcecode:: python
-
-  class CommunityPrimaryView(primary.PrimaryView):
-      __select__ = is_instance('Community')
-
-      def render_entity_title(self, entity):
-	  self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
-
-      def render_entity_attributes(self, entity):
-	  if entity.display_cw_logo():
-	      self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
-	  if entity.description:
-	      self.w(u'<p>%s</p>' % entity.printable_value('description'))
-
-It appears now properly:
-
-.. image:: ../../images/tutos-base_myblog-community-taggable-primary_en.png
-   :alt: the custom primary view for a community entry with tags activated
-
-You can control part of the interface independently from each others, piece by
-piece. Really.
-
-
-
-.. _`CubicWeb Forge`: http://www.cubicweb.org/project
--- a/doc/book/en/tutorials/base/discovering-the-ui.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-
-.. _TutosBaseDiscoveringTheUI:
-
-Discovering the web interface
------------------------------
-
-You can now access your web instance to create blogs and post messages
-by visiting the URL http://localhost:8080/.
-
-By default, anonymous access is disabled, so a login form will appear. If you
-asked to allow anonymous access when initializing the instance, click on the
-'login' link in the top right hand corner. To login, you need then use the admin
-account you specified at the time you initialized the database with
-``cubicweb-ctl create``.
-
-.. image:: ../../images/tutos-base_login-form_en.png
-   :alt: the login form
-
-
-Once authenticated, you can start playing with your instance. The default index
-page looks like the following:
-
-.. image:: ../../images/tutos-base_index_en.png
-   :alt: the index page
-
-
-Minimal configuration
-~~~~~~~~~~~~~~~~~~~~~
-
-Before creating entities, let's change that 'unset title' thing that appears
-here and there. This comes from a |cubicweb| system properties. To set it,
-click on the 'site configuration link' in the pop-up menu behind your login name
-in the upper left-hand corner
-
-.. image:: ../../images/tutos-base_user-menu_en.png
-   :alt: the user pop-up menu
-
-The site title is in the 'Ui' section. Simply set it to the desired value and
-click the 'validate' button.
-
-.. image:: ../../images/tutos-base_siteconfig_en.png
-   :alt: the site configuration form
-
-You should see a 'changes applied' message. You can now go back to the
-index page by clicking on the |cubicweb| logo in the upper left-hand corner.
-
-You will much likely still see 'unset title' at this point. This is because by
-default the index page is cached. Force a refresh of the page (by typing Ctrl-R
-in Firefox for instance) and you should now see the title you entered.
-
-
-Adding entities
-~~~~~~~~~~~~~~~
-
-The ``blog`` cube defines several entity types, among them ``Blog`` which is a
-container for ``BlogEntry`` (i.e. posts) on a particular topic. We can get a
-graphical view of the schema by clicking on the 'site schema' link in the user
-pop-up menu we've already seen:
-
-.. image:: ../../images/tutos-base_schema_en.png
-   :alt: graphical view of the schema (aka data-model)
-
-Nice isn't it? Notice that this, as most other stuff we'll see in this tutorial,
-is generated by the framework according to the model of the application. In our
-case, the model defined by the ``blog`` cube.
-
-Now let us create a few of these entities.
-
-
-Add a blog
-**********
-
-Clicking on the `[+]` at the left of the 'Blog' link on the index page will lead
-you to an HTML form to create a blog.
-
-.. image:: ../../images/tutos-base_blog-form_en.png
-   :alt: the blog creation form
-
-For instance, call this new blog 'Tech-blog' and type in 'everything about
-technology' as the description , then validate the form by clicking on
-'Validate'. You will be redirected to the `primary` view of the newly created blog.
-
-.. image:: ../../images/tutos-base_blog-primary_en.png
-   :alt: the blog primary view
-
-
-Add a blog post
-***************
-
-There are several ways to add a blog entry. The simplest is to click on the 'add
-blog entry' link in the actions box on viewing the blog you have just created.
-You will then see a form to create a post, with a 'blog entry of' field preset
-to the blog we're coming from. Enter a title, some content, click the 'validate'
-button and you're done. You will be redirected to the blog primary view, though you
-now see that it contains the blog post you've just created.
-
-.. image:: ../../images/tutos-base_blog-primary-after-post-creation_en.png
-   :alt: the blog primary view after creation of a post
-
-Notice there are some new boxes that appears in the left column.
-
-You can achieve the same thing by following the same path as we did for the blog
-creation, e.g. by clicking on the `[+]` at the left of the 'Blog entry' link on
-the index page. The diffidence being that since there is no context information,
-the 'blog entry of' selector won't be preset to the blog.
-
-
-If you click on the 'modify' link of the action box, you are back to
-the form to edit the entity you just created, except that the form now
-has another section with a combo-box entitled 'add relation'. It
-provisos a generic way to edit relations which don't appears in the
-above form. Choose the relation you want to add and a second combo box
-appears where you can pick existing entities.  If there are too many
-of them, you will be offered to navigate to the target entity, that is
-go away from the form and go back to it later, once you've selected
-the entity you want to link with.
-
-.. image:: ../../images/tutos-base_form-generic-relations_en.png
-   :alt: the generic relations combo box
-
-This combo box can't appear until the entity is actually created. That's why you
-haven't seen it at creation time. You could also have hit 'Apply' instead of
-'validate' and it would have showed up.
-
-
-About ui auto-adaptation
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-One of the things that make |cubicweb| different of other frameworks is
-its automatic user interface that adapts itself according to the data being
-displayed. Let's see an example.
-
-If you go back to the home page an click on the 'Blog' link, you will be redirected
-to the primary view of the blog, the same we've seen earlier. Now, add another
-blog, go back to the index page, and click again on this link. You will see
-a very different view (namely the 'list' view).
-
-.. image:: ../../images/tutos-base_blogs-list_en.png
-   :alt: the list view when there are more than one blog to display
-
-This is because in the first case, the framework chose to use the 'primary'
-view since there was only one entity in the data to be displayed. Now that there
-are two entities, the 'list' view is more appropriate and hence is being used.
-
-There are various other places where |cubicweb| adapts to display data in the best
-way, the main being provided by the view *selection* mechanism that will be detailed
-later.
-
-
-Digging deeper
-~~~~~~~~~~~~~~
-
-By following principles explained below, you should now be able to
-create new users for your application, to configure with a finer
-grain, etc... You will notice that the index page lists a lot of types
-you don't know about. Most are built-in types provided by the framework
-to make the whole system work. You may ignore them in a first time and
-discover them as time goes.
-
-One thing that is worth playing with is the search box. It may be used in various
-way, from simple full text search to advanced queries using the :ref:`RQL` .
--- a/doc/book/en/tutorials/base/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _TutosBase:
-
-Building a simple blog with |cubicweb|
-======================================
-
-|cubicweb| is a semantic web application framework that favors reuse and
-object-oriented design.
-
-
-This tutorial is designed to help in your very first steps to start with
-|cubicweb|. We will tour through basic concepts such as:
-
-* getting an application running by using existing components
-* discovering the default user interface
-* basic extending and customizing the look and feel of that application
-
-More advanced concepts are covered in :ref:`TutosPhotoWebSite`.
-
-
-.. _TutosBaseVocab:
-
-Some vocabulary
----------------
-
-|cubicweb| comes with a few words of vocabulary that you should know to
-understand what we're talking about. To follow this tutorial, you should at least
-know that:
-
-* a `cube` is a component that usually includes a model defining some data types
-  and a set of views to display them. A cube can be built by assembling other
-  cubes;
-
-* an `instance` is a specific installation of one or more cubes and includes
-  configuration files, a web server and a database.
-
-Reading :ref:`Concepts` for more vocabulary will be required at some point.
-
-Now, let's start the hot stuff!
-
-.. toctree::
-   :maxdepth: 2
-
-   blog-in-five-minutes
-   discovering-the-ui
-   customizing-the-application
-   conclusion
--- a/doc/book/en/tutorials/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-.. _Tutorials:
-
----------
-Tutorials
----------
-
-We present two tutorials of different levels. The blog building
-tutorial introduces one smoothly to the basic concepts.
-
-Then there is a photo gallery construction tutorial which highlights
-more advanced concepts such as unit tests, security settings,
-migration scripts.
-
-.. toctree::
-   :maxdepth: 1
-   :numbered:
-
-   base/index
-   advanced/index
-   tools/windmill.rst
-   textreports/index
--- a/doc/book/en/tutorials/textreports/index.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Writing text reports with RestructuredText
-==========================================
-
-|cubicweb| offers several text formats for the RichString type used in schemas,
-including restructuredtext.
-
-Three additional restructuredtext roles are defined by |cubicweb|:
-
-.. autofunction:: cubicweb.ext.rest.eid_reference_role
-.. autofunction:: cubicweb.ext.rest.rql_role
-.. autofunction:: cubicweb.ext.rest.bookmark_role
--- a/doc/book/en/tutorials/tools/windmill.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,229 +0,0 @@
-==========================
-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::
-
-    $ 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>
-
-And enter python commands:
-
-.. 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:
-
-.. sourcecode:: python
-
-    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
-:attr:`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/book/intro/concepts.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,297 @@
+.. -*- coding: utf-8 -*-
+
+.. _Concepts:
+
+The Core Concepts of |cubicweb|
+===============================
+
+This section defines some terms and core concepts of the |cubicweb| framework. To
+avoid confusion while reading this book, take time to go through the following
+definitions and use this section as a reference during your reading.
+
+
+.. _Cube:
+
+Cubes
+-----
+
+A cube is a software component made of three parts:
+
+- its data model (:mod:`schema`),
+- its logic (:mod:`entities`) and
+- its user interface (:mod:`views`).
+
+A cube can use other cubes as building blocks and assemble them to provide a
+whole with richer functionnalities than its parts. The cubes `cubicweb-blog`_ and
+`cubicweb-comment`_ could be used to make a cube named *myblog* with commentable
+blog entries.
+
+The `CubicWeb.org Forge`_ offers a large number of cubes developed by the community
+and available under a free software license.
+
+.. note::
+
+   The command :command:`cubicweb-ctl list` displays the list of available cubes.
+
+.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
+.. _`cubicweb-blog`: http://www.cubicweb.org/project/cubicweb-blog
+.. _`cubicweb-comment`: http://www.cubicweb.org/project/cubicweb-comment
+
+
+.. _Instance:
+
+Instances
+---------
+
+An instance is a runnable application installed on a computer and
+based on one or more cubes.
+
+The instance directory contains the configuration files. Several
+instances can be created and based on the same cube. For example,
+several software forges can be set up on one computer system based on
+the `cubicweb-forge`_ cube.
+
+.. _`cubicweb-forge`: http://www.cubicweb.org/project/cubicweb-forge
+
+The command :command:`cubicweb-ctl list` also displays the list of instances
+installed on your system.
+
+.. note::
+
+  The term application is used to refer to "something that should do something as
+  a whole", eg more like a project and so can refer to an instance or to a cube,
+  depending on the context. This book will try to use *application*, *cube* and
+  *instance* as appropriate.
+
+
+.. _RepositoryIntro:
+
+Data Repository
+---------------
+
+The data repository [1]_ encapsulates and groups an access to one or
+more data sources (including SQL databases, LDAP repositories, other
+|cubicweb| instance repositories, filesystems, Google AppEngine's
+DataStore, etc).
+
+All interactions with the repository are done using the `Relation Query Language`
+(:ref:`RQL`). The repository federates the data sources and hides them from the
+querier, which does not realize when a query spans several data sources
+and requires running sub-queries and merges to complete.
+
+Application logic can be mapped to data events happenning within the
+repository, like creation of entities, deletion of relations,
+etc. This is used for example to send email notifications when the
+state of an object changes. See :ref:`HookIntro` below.
+
+.. [1] not to be confused with a Mercurial repository or a Debian repository.
+.. _`Python Remote Objects`: http://pythonhosted.org/Pyro4/
+
+.. _WebEngineIntro:
+
+Web Engine
+----------
+
+The web engine replies to http requests and runs the user interface.
+
+By default the web engine provides a `CRUD`_ user interface based on
+the data model of the instance. Entities can be created, displayed,
+updated and deleted. As the default user interface is not very fancy,
+it is usually necessary to develop your own.
+
+.. _`CRUD`: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
+
+.. _SchemaIntro:
+
+Schema (Data Model)
+-------------------
+
+The data model of a cube is described as an entity-relationship schema using a
+comprehensive language made of Python classes imported from the yams_ library.
+
+.. _yams: http://www.logilab.org/project/yams/
+
+An `entity type` defines a sequence of attributes. Attributes may be
+of the following types: `String`, `Int`, `Float`, `Boolean`, `Date`,
+`Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`.
+
+A `relation type` is used to define an oriented binary relation
+between entity types.  The left-hand part of a relation is named the
+`subject` and the right-hand part is named the `object`.
+
+A `relation definition` is a triple (*subject entity type*, *relation type*, *object
+entity type*) associated with a set of properties such as cardinality,
+constraints, etc.
+
+Permissions can be set on entity types or relation definition to control who
+will be able to create, read, update or delete entities and relations. Permissions
+are granted to groups (to which users may belong) or using rql expressions (if the
+rql expression returns some results, the permission is granted).
+
+Some meta-data necessary to the system are added to the data model. That includes
+entities like users and groups, the entities used to store the data model
+itself and attributes like unique identifier, creation date, creator, etc.
+
+When you create a new |cubicweb| instance, the schema is stored in the database.
+When the cubes the instance is based on evolve, they may change their data model
+and provide migration scripts that will be executed when the administrator will
+run the upgrade process for the instance.
+
+
+.. _VRegistryIntro:
+
+Registries and application objects
+----------------------------------
+
+Application objects
+~~~~~~~~~~~~~~~~~~~
+
+Besides a few core functionalities, almost every feature of the framework is
+achieved by dynamic objects (`application objects` or `appobjects`) stored in a
+two-levels registry. Each object is affected to a registry with
+an identifier in this registry. You may have more than one object sharing an
+identifier in the same registry:
+
+  object's `__registry__` : object's `__regid__` : [list of app objects]
+
+In other words, the `registry` contains several (sub-)registries which hold a
+list of appobjects associated to an identifier.
+
+The base class of appobjects is :class:`cubicweb.appobject.AppObject`.
+
+Selectors
+~~~~~~~~~
+
+At runtime, appobjects can be selected in a registry according to some
+contextual information. Selection is done by comparing the *score*
+returned by each appobject's *selector*.
+
+The better the object fits the context, the higher the score. Scores
+are the glue that ties appobjects to the data model. Using them
+appropriately is an essential part of the construction of well behaved
+cubes.
+
+|cubicweb| provides a set of basic selectors that may be parametrized.  Also,
+selectors can be combined with the `~` unary operator (negation) and the binary
+operators `&` and `|` (respectivly 'and' and 'or') to build more complex
+selectors. Of course complex selectors may be combined too. Last but not least, you
+can write your own selectors.
+
+The `registry`
+~~~~~~~~~~~~~~~
+
+At startup, the `registry` inspects a number of directories looking
+for compatible class definitions. After a recording process, the
+objects are assigned to registries and become available through the
+selection process.
+
+In a cube, application object classes are looked in the following modules or
+packages:
+
+- `entities`
+- `views`
+- `hooks`
+- `sobjects`
+
+There are three common ways to look up some application object from a
+registry:
+
+* get the most appropriate object by specifying an identifier and
+  context objects. The object with the greatest score is
+  selected. There should always be a single appobject with a greater
+  score than others for a particular context.
+
+* get all objects applying to a context by specifying a registry. A
+  list of objects will be returned containing the object with the
+  highest score (> 0) for each identifier in that registry.
+
+* get the object within a particular registry/identifier. No selection
+  process is involved: the registry will expect to find a single
+  object in that cell.
+
+
+.. _RQLIntro:
+
+The RQL query language
+----------------------
+
+No need for a complicated ORM when you have a powerful data
+manipulation language.
+
+All the persistent data in a |cubicweb| instance is retrieved and
+modified using RQL (see :ref:`rql_intro`).
+
+This query language is inspired by SQL but is on a higher level in order to
+emphasize browsing relations.
+
+
+Result set
+~~~~~~~~~~
+
+Every request made (using RQL) to the data repository returns an object we call a
+Result Set. It enables easy use of the retrieved data, providing a translation
+layer between the backend's native datatypes and |cubicweb| schema's EntityTypes.
+
+Result sets provide access to the raw data, yielding either basic Python data
+types, or schema-defined high-level entities, in a straightforward way.
+
+
+.. _ViewIntro:
+
+Views
+-----
+
+**CubicWeb is data driven**
+
+The view system is loosely coupled to data through the selection system explained
+above. Views are application objects with a dedicated interface to 'render'
+something, eg producing some html, text, xml, pdf, or whatsover that can be
+displayed to a user.
+
+Views actually are partitioned into different kind of objects such as
+`templates`, `boxes`, `components` and proper `views`, which are more
+high-level abstraction useful to build the user interface in an object
+oriented way.
+
+
+.. _HookIntro:
+
+Hooks and operations
+--------------------
+
+**CubicWeb provides an extensible data repository**
+
+The data model defined using Yams types allows to express the data
+model in a comfortable way. However several aspects of the data model
+can not be expressed there. For instance:
+
+* managing computed attributes
+
+* enforcing complicated business rules
+
+* real-world side-effects linked to data events (email notification
+  being a prime example)
+
+The hook system is much like the triggers of an SQL database engine,
+except that:
+
+* it is not limited to one specific SQL backend (every one of them
+  having an idiomatic way to encode triggers), nor to SQL backends at
+  all (think about LDAP or a Subversion repository)
+
+* it is well-coupled to the rest of the framework
+
+Hooks are also application objects (in the `hooks` registry) and
+selected on events such as after/before add/update/delete on
+entities/relations, server startup or shutdown, etc.
+
+`Operations` may be instantiated by hooks to do further processing at different
+steps of the transaction's commit / rollback, which usually can not be done
+safely at the hook execution time.
+
+Hooks and operation are an essential building block of any moderately complicated
+cubicweb application.
+
+.. note::
+   RQL queries executed in hooks and operations are *unsafe* by default, i.e. the
+   read and write security is deactivated unless explicitly asked.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/intro/history.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,32 @@
+.. -*- coding: utf-8 -*-
+
+A little history...
+===================
+
+*CubicWeb* is a semantic web application framework that Logilab_ started
+developing in 2001 as an offspring of its Narval_ research project. *CubicWeb*
+is written in Python and includes a data server and a web engine.
+
+Its data server publishes data federated from different sources like
+SQL databases, LDAP directories, `VCS`_ repositories or even from other
+CubicWeb data servers.
+
+.. _`VCS`: http://en.wikipedia.org/wiki/Revision_control
+
+Its web engine was designed to let the final user control what content to select
+and how to display it. It allows one to browse the federated data sources and
+display the results with the rendering that best fits the context. This
+flexibility of the user interface gives back to the user some capabilities
+usually only accessible to application developers.
+
+*CubicWeb* has been developed by Logilab_ and used in-house for many years
+before it was first installed for its clients in 2006 as version 2.
+
+In 2008, *CubicWeb* version 3 became downloadable for free under the
+terms of the LGPL license. Its community is now steadily growing
+without hampering the fast-paced stream of changes thanks to the time
+and energy originally put in the design of the framework.
+
+
+.. _Narval: http://www.logilab.org/project/narval-moved
+.. _Logilab: http://www.logilab.fr/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/intro/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,19 @@
+.. -*- coding: utf-8 -*-
+
+.. _Part1:
+
+--------------------------
+Introduction to *CubicWeb*
+--------------------------
+
+This first part of the book offers different reading path to
+discover the *CubicWeb* framework, provides a tutorial to get a quick
+overview of its features and lists its key concepts.
+
+
+.. toctree::
+   :maxdepth: 2
+   :numbered:
+
+   history
+   concepts.rst
--- a/doc/book/mode_plan.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +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 mode_plan import *
->>> ls()
-<list of directory content>
->>> ren('A01','A03')
-rename A010-joe.en.txt to A030-joe.en.txt
-accept [y/N]?
-"""
-
-def ren(a,b):
-    names = glob.glob('%s*'%a)
-    for name in names :
-        print 'rename %s to %s' % (name, name.replace(a,b))
-    if raw_input('accept [y/N]?').lower() =='y':
-        for name in names:
-            os.system('hg mv %s %s' % (name, name.replace(a,b)))
-
-
-def ls(): print '\n'.join(sorted(os.listdir('.')))
-
-def move():
-    filenames = []
-    for name in sorted(os.listdir('.')):
-        num = name[:2]
-        if num.isdigit():
-            filenames.append( (int(num), name) )
-
-
-    #print filenames
-
-    for num, name in filenames:
-        if num >= start:
-            print 'hg mv %s %2i%s' %(name,num+1,name[2:])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.14.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,165 @@
+3.14 (09/11/2011)
+=================
+
+First notice CW 3.14 depends on yams 0.34 (which is incompatible with prior
+cubicweb releases regarding instance re-creation).
+
+
+API changes
+-----------
+
+* `Entity.fetch_rql` `restriction` argument has been deprecated and should be
+  replaced with a call to the new `Entity.fetch_rqlst` method, get the returned
+  value (a rql `Select` node) and use the RQL syntax tree API to include the
+  above-mentionned restrictions.
+
+  Backward compat is kept with proper warning.
+
+* `Entity.fetch_order` and `Entity.fetch_unrelated_order` class methods have been
+  replaced by `Entity.cw_fetch_order` and `Entity.cw_fetch_unrelated_order` with
+  a different prototype:
+
+  - instead of taking (attr, var) as two string argument, they now take (select,
+    attr, var) where select is the rql syntax tree beinx constructed and var the
+    variable *node*.
+
+  - instead of returning some string to be inserted in the ORDERBY clause, it has
+    to modify the syntax tree
+
+  Backward compat is kept with proper warning, BESIDE cases below:
+
+  - custom order method return **something else the a variable name with or
+    without the sorting order** (e.g. cases where you sort on the value of a
+    registered procedure as it was done in the tracker for instance). In such
+    case, an error is logged telling that this sorting is ignored until API
+    upgrade.
+
+  - client code use direct access to one of those methods on an entity (no code
+    known to do that).
+
+* `Entity._rest_attr_info` class method has been renamed to
+  `Entity.cw_rest_attr_info`
+
+  No backward compat yet since this is a protected method an no code is known to
+  use it outside cubicweb itself.
+
+* `AnyEntity.linked_to` has been removed as part of a refactoring of this
+  functionality (link a entity to another one at creation step). It was replaced
+  by a `EntityFieldsForm.linked_to` property.
+
+  In the same refactoring, `cubicweb.web.formfield.relvoc_linkedto`,
+  `cubicweb.web.formfield.relvoc_init` and
+  `cubicweb.web.formfield.relvoc_unrelated` were removed and replaced by
+  RelationField methods with the same names, that take a form as a parameter.
+
+  **No backward compatibility yet**. It's still time to cry for it.
+  Cubes known to be affected: tracker, vcsfile, vcreview.
+
+* `CWPermission` entity type and its associated require_permission relation type
+  (abstract) and require_group relation definitions have been moved to a new
+  `localperms` cube. With this have gone some functions from the
+  `cubicweb.schemas` package as well as some views. This makes cubicweb itself
+  smaller while you get all the local permissions stuff into a single,
+  documented, place.
+
+  Backward compat is kept for existing instances, **though you should have
+  installed the localperms cubes**. A proper error should be displayed when
+  trying to migrate to 3.14 an instance the use `CWPermission` without the new
+  cube installed. For new instances / test, you should add a dependancy on the
+  new cube in cubes using this feature, along with a dependancy on cubicweb >=
+  3.14.
+
+* jQuery has been updated to 1.6.4 and jquery-tablesorter to 2.0.5. No backward
+  compat issue known.
+
+* Table views refactoring : new `RsetTableView` and `EntityTableView`, as well as
+  rewritten an enhanced version of `PyValTableView` on the same bases, with logic
+  moved to some column renderers and a layout. Those should be well documented
+  and deprecates former `TableView`, `EntityAttributesTableView` and `CellView`,
+  which are however kept for backward compat, with some warnings that may not be
+  very clear unfortunatly (you may see your own table view subclass name here,
+  which doesn't make the problem that clear). Notice that `_cw.view('table',
+  rset, *kwargs)` will be routed to the new `RsetTableView` or to the old
+  `TableView` depending on given extra arguments. See #1986413.
+
+* `display_name` don't call .lower() anymore. This may leads to changes in your
+  user interface. Different msgid for upper/lower cases version of entity type
+  names, as this is the only proper way to handle this with some languages.
+
+* `IEditControlAdapter` has been deprecated in favor of `EditController`
+  overloading, which was made easier by adding dedicated selectors called
+  `match_edited_type` and `match_form_id`.
+
+* Pre 3.6 API backward compat has been dropped, though *data* migration
+  compatibility has been kept. You may have to fix errors due to old API usage
+  for your instance before to be able to run migration, but then you should be
+  able to upgrade even a pre 3.6 database.
+
+* Deprecated `cubicweb.web.views.iprogress` in favor of new `iprogress` cube.
+
+* Deprecated `cubicweb.web.views.flot` in favor of new `jqplot` cube.
+
+
+Unintrusive API changes
+-----------------------
+
+* Refactored properties forms (eg user preferences and site wide properties) as
+  well as pagination components to ease overridding.
+
+* New `cubicweb.web.uihelper` module with high-level helpers for uicfg.
+
+* New `anonymized_request` decorator to temporary run stuff as an anonymous
+  user, whatever the currently logged in user.
+
+* New 'verbatimattr' attribute view.
+
+* New facet and form widget for Integer used to store binary mask.
+
+* New `js_href` function to generated proper javascript href.
+
+* `match_kwargs` and `match_form_params` selectors both accept a new
+  `once_is_enough` argument.
+
+* `printable_value` is now a method of request, and may be given dict of
+   formatters to use.
+
+* `[Rset]TableView` allows to set None in 'headers', meaning the label should be
+  fetched from the result set as done by default.
+
+* Field vocabulary computation on entity creation now takes `__linkto`
+  information into accounet.
+
+* Started a `cubicweb.pylintext` pylint plugin to help pylint analyzing cubes.
+
+
+RQL
+---
+
+* Support for HAVING in 'SET' and 'DELETE' queries.
+
+* new `AT_TZ` function to get back a timestamp at a given time-zone.
+
+* new `WEEKDAY` date extraction function
+
+
+User interface changes
+----------------------
+
+* Datafeed source now present an history of the latest import's log, including
+  global status and debug/info/warning/error messages issued during
+  imports. Import logs older than a configurable amount of time are automatically
+  deleted.
+
+* Breadcrumbs component is properly kept when creating an entity with '__linkto'.
+
+* users and groups management now really lead to that (i.e. includes *groups*
+  management).
+
+* New 'jsonp' controller with 'jsonexport' and 'ejsonexport' views.
+
+
+Configuration
+-------------
+
+* Added option 'resources-concat' to make javascript/css files concatenation
+  optional.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.15.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,98 @@
+3.15 (12/04/2012)
+=================
+
+New functionnalities
+--------------------
+
+* Add Zmq server, based on the cutting edge ZMQ (http://www.zeromq.org/) socket
+  library.  This allows to access distant instance, in a similar way as Pyro.
+
+* Publish/subscribe mechanism using ZMQ for communication among cubicweb
+  instances.  The new zmq-address-sub and zmq-address-pub configuration variables
+  define where this communication occurs.  As of this release this mechanism is
+  used for entity cache invalidation.
+
+* Improved WSGI support. While there is still some caveats, most of the code
+  which was twisted only is now generic and allows related functionalities to work
+  with a WSGI front-end.
+
+* Full undo/transaction support : undo of modification has eventually been
+  implemented, and the configuration simplified (basically you activate it or not
+  on an instance basis).
+
+* Controlling HTTP status code used is not much more easier :
+
+  - `WebRequest` now has a `status_out` attribut to control the response status ;
+
+  - most web-side exceptions take an optional ``status`` argument.
+
+
+API changes
+-----------
+
+* The base registry implementation has been moved to a new
+  `logilab.common.registry` module (see #1916014). This includes code from :
+
+  * `cubicweb.vreg` (the whole things that was in there)
+  * `cw.appobject` (base selectors and all).
+
+  In the process, some renaming was done:
+
+  * the top level registry is now `RegistryStore` (was `VRegistry`), but that
+    should not impact cubicweb client code ;
+
+  * former selectors functions are now known as "predicate", though you still use
+    predicates to build an object'selector ;
+
+  * for consistency, the `objectify_selector` decoraror has hence be renamed to
+    `objectify_predicate` ;
+
+  * on the CubicWeb side, the `selectors` module has been renamed to
+    `predicates`.
+
+  Debugging refactoring dropped the more need for the `lltrace` decorator.  There
+  should be full backward compat with proper deprecation warnings.  Notice the
+  `yes` predicate and `objectify_predicate` decorator, as well as the
+  `traced_selection` function should now be imported from the
+  `logilab.common.registry` module.
+
+* All login forms are now submitted to <app_root>/login. Redirection to requested
+  page is now handled by the login controller (it was previously handle by the
+  session manager).
+
+* `Publisher.publish` has been renamed to `Publisher.handle_request`. This
+  method now contains generic version of logic previously handled by
+  Twisted. `Controller.publish` is **not** affected.
+
+
+Unintrusive API changes
+-----------------------
+
+* New 'ldapfeed' source type, designed to replace 'ldapuser' source with
+  data-feed (i.e. copy based) source ideas.
+
+* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
+
+* A new registry called `services` has appeared, where you can register
+  server-side `cubicweb.server.Service` child classes. Their `call` method can be
+  invoked from a web-side AppObject instance using new `self._cw.call_service`
+  method or a server-side one using `self.session.call_service`. This is a new
+  way to call server-side methods, much cleaner than monkey patching the
+  Repository class, which becomes a deprecated way to perform similar tasks.
+
+* a new `ajax-func` registry now hosts all remote functions (i.e. functions
+  callable through the `asyncRemoteExec` JS api). A convenience `ajaxfunc`
+  decorator will let you expose your python function easily without all the
+  appobject standard boilerplate. Backward compatibility is preserved.
+
+* the 'json' controller is now deprecated in favor of the 'ajax' one.
+
+* `WebRequest.build_url` can now take a __secure__ argument. When True cubicweb
+  try to generate an https url.
+
+
+User interface changes
+----------------------
+
+A new 'undohistory' view expose the undoable transactions and give access to undo
+some of them.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.16.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,97 @@
+3.16 (25/01/2013)
+=================
+
+New functionalities
+-------------------
+
+* Add a new dataimport store (`SQLGenObjectStore`). This store enables a fast
+  import of data (entity creation, link creation) in CubicWeb, by directly
+  flushing information in SQL.  This may only be used with PostgreSQL, as it
+  requires the 'COPY FROM' command.
+
+
+API changes
+-----------
+
+* Orm: `set_attributes` and `set_relations` are unified (and
+  deprecated) in favor of `cw_set` that works in all cases.
+
+* db-api/configuration: all the external repository connection information is
+  now in an URL (see `#2521848 <http://www.cubicweb.org/2521848>`_),
+  allowing to drop specific options of pyro nameserver host, group, etc and fix
+  broken `ZMQ <http://www.zeromq.org/>`_ source. Configuration related changes:
+
+  * Dropped 'pyro-ns-host', 'pyro-instance-id', 'pyro-ns-group' from the client side
+    configuration, in favor of 'repository-uri'. **NO MIGRATION IS DONE**,
+    supposing there is no web-only configuration in the wild.
+
+  * Stop discovering the connection method through `repo_method` class attribute
+    of the configuration, varying according to the configuration class. This is
+    a first step on the way to a simpler configuration handling.
+
+  DB-API related changes:
+
+  * Stop indicating the connection method using `ConnectionProperties`.
+
+  * Drop `_cnxtype` attribute from `Connection` and `cnxtype` from
+    `Session`. The former is replaced by a `is_repo_in_memory` property
+    and the later is totaly useless.
+
+  * Turn `repo_connect` into `_repo_connect` to mark it as a private function.
+
+  * Deprecate `in_memory_cnx` which becomes useless, use `_repo_connect` instead
+    if necessary.
+
+* the "tcp://" uri scheme used for `ZMQ <http://www.zeromq.org/>`_
+  communications (in a way reminiscent of Pyro) is now named
+  "zmqpickle-tcp://", so as to make room for future zmq-based lightweight
+  communications (without python objects pickling).
+
+* Request.base_url gets a `secure=True` optional parameter that yields
+  an https url if possible, allowing hook-generated content to send
+  secure urls (e.g. when sending mail notifications)
+
+* Dataimport ucsvreader gets a new boolean `ignore_errors`
+  parameter.
+
+
+Unintrusive API changes
+-----------------------
+
+* Drop of `cubicweb.web.uicfg.AutoformSectionRelationTags.bw_tag_map`,
+  deprecated since 3.6.
+
+
+User interface changes
+----------------------
+
+* The RQL search bar has now some auto-completion support. It means
+  relation types or entity types can be suggested while typing. It is
+  an awesome improvement over the current behaviour !
+
+* The `action box` associated with `table` views (from `tableview.py`)
+  has been transformed into a nice-looking series of small tabs; it
+  means that the possible actions are immediately visible and need not
+  be discovered by clicking on an almost invisible icon on the upper
+  right.
+
+* The `uicfg` module has moved to web/views/ and ui configuration
+  objects are now selectable. This will reduce the amount of
+  subclassing and whole methods replacement usually needed to
+  customize the ui behaviour in many cases.
+
+* Remove changelog view, as neither cubicweb nor known
+  cubes/applications were properly feeding related files.
+
+
+Other changes
+-------------
+
+* 'pyrorql' sources will be automatically updated to use an URL to locate the source
+  rather than configuration option. 'zmqrql' sources were broken before this change,
+  so no upgrade is needed...
+
+* Debugging filters for Hooks and Operations have been added.
+
+* Some cubicweb-ctl commands used to show the output of `msgcat` and
+  `msgfmt`; they don't anymore.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.17.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,61 @@
+3.17 (02/05/2013)
+=================
+
+New functionalities
+-------------------
+
+* add a command to compare db schema and file system schema
+  (see `#464991 <http://www.cubicweb.org/464991>`_)
+
+* Add CubicWebRequestBase.content with the content of the HTTP request (see #2742453)
+  (see `#2742453 <http://www.cubicweb.org/2742453>`_)
+
+* Add directive bookmark to ReST rendering
+  (see `#2545595 <http://www.cubicweb.org/ticket/2545595>`_)
+
+* Allow user defined final type
+  (see `#124342 <https://www.logilab.org/ticket/124342>`_)
+
+
+API changes
+-----------
+
+* drop typed_eid() in favour of int() (see `#2742462 <http://www.cubicweb.org/2742462>`_)
+
+* The SIOC views and adapters have been removed from CubicWeb and moved to the
+  `sioc` cube.
+
+* The web page embedding views and adapters have been removed from CubicWeb and
+  moved to the `embed` cube.
+
+* The email sending views and controllers have been removed from CubicWeb and
+  moved to the `massmailing` cube.
+
+* ``RenderAndSendNotificationView`` is deprecated in favor of
+  ``ActualNotificationOp`` the new operation use the more efficient *data*
+  idiom.
+
+* Looping task can now have a interval <= ``0``. Negative interval disable the
+  looping task entirely.
+
+* We now serve html instead of xhtml.
+  (see `#2065651 <http://www.cubicweb.org/ticket/2065651>`_)
+
+
+Deprecation
+-----------
+
+* ``ldapuser`` have been deprecated. It'll be fully dropped in the next
+  version. If you are still using ldapuser switch to ``ldapfeed`` **NOW**!
+
+* ``hijack_user`` have been deprecated. It will be dropped soon.
+
+
+Deprecated Code Drops
+---------------------
+
+* The progress views and adapters have been removed from CubicWeb. These
+  classes were deprecated since 3.14.0. They are still available in the
+  `iprogress` cube.
+
+* API deprecated since 3.7 have been dropped.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.18.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,101 @@
+3.18 (10/01/2014)
+=================
+
+The migration script does not handle sqlite nor mysql instances.
+
+
+New functionalities
+-------------------
+
+* add a security debugging tool
+  (see `#2920304 <http://www.cubicweb.org/2920304>`_)
+
+* introduce an `add` permission on attributes, to be interpreted at
+  entity creation time only and allow the implementation of complex
+  `update` rules that don't block entity creation (before that the
+  `update` attribute permission was interpreted at entity creation and
+  update time)
+
+* the primary view display controller (uicfg) now has a
+  `set_fields_order` method similar to the one available for forms
+
+* new method `ResultSet.one(col=0)` to retrive a single entity and enforce the
+  result has only one row (see `#3352314 <https://www.cubicweb.org/ticket/3352314>`_)
+
+* new method `RequestSessionBase.find` to look for entities
+  (see `#3361290 <https://www.cubicweb.org/ticket/3361290>`_)
+
+* the embedded jQuery copy has been updated to version 1.10.2, and jQuery UI to
+  version 1.10.3.
+
+* initial support for wsgi for the debug mode, available through the new
+  ``wsgi`` cubicweb-ctl command, which can use either python's builtin
+  wsgi server or the werkzeug module if present.
+
+* a ``rql-table`` directive is now available in ReST fields
+
+* cubicweb-ctl upgrade can now generate the static data resource directory
+  directly, without a manual call to gen-static-datadir.
+
+API changes
+-----------
+
+* not really an API change, but the entity permission checks are now
+  systematically deferred to an operation, instead of a) trying in a
+  hook and b) if it failed, retrying later in an operation
+
+* The default value storage for attributes is no longer String, but
+  Bytes.  This opens the road to storing arbitrary python objects, e.g.
+  numpy arrays, and fixes a bug where default values whose truth value
+  was False were not properly migrated.
+
+* `symmetric` relations are no more handled by an rql rewrite but are
+  now handled with hooks (from the `activeintegrity` category); this
+  may have some consequences for applications that do low-level database
+  manipulations or at times disable (some) hooks.
+
+* `unique together` constraints (multi-columns unicity constraints)
+  get a `name` attribute that maps the CubicWeb contraint entities to
+  corresponding backend index.
+
+* BreadCrumbEntityVComponent's open_breadcrumbs method now includes
+  the first breadcrumbs separator
+
+* entities can be compared for equality and hashed
+
+* the ``on_fire_transition`` predicate accepts a sequence of possible
+  transition names
+
+* the GROUP_CONCAT rql aggregate function no longer repeats duplicate
+  values, on the sqlite and postgresql backends
+
+Deprecation
+-----------
+
+* ``pyrorql`` sources have been deprecated. Multisource will be fully dropped
+  in the next version. If you are still using pyrorql, switch to ``datafeed``
+  **NOW**!
+
+* the old multi-source system
+
+* `find_one_entity` and `find_entities` in favor of `find`
+  (see `#3361290 <https://www.cubicweb.org/ticket/3361290>`_)
+
+* the `TmpFileViewMixin` and `TmpPngView` classes (see
+  `#3400448 <https://www.cubicweb.org/ticket/3400448>`_)
+
+Deprecated Code Drops
+---------------------
+
+* ``ldapuser`` have been dropped; use ``ldapfeed`` now
+  (see `#2936496 <http://www.cubicweb.org/2936496>`_)
+
+* action ``GotRhythm`` was removed, make sure you do not
+  import it in your cubes (even to unregister it)
+  (see `#3093362 <http://www.cubicweb.org/3093362>`_)
+
+* all 3.8 backward compat is gone
+
+* all 3.9 backward compat (including the javascript side) is gone
+
+* the ``twisted`` (web-only) instance type has been removed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.19.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,180 @@
+3.19 (28/04/2015)
+=================
+
+New functionalities
+-------------------
+
+* implement Cross Origin Resource Sharing (CORS)
+  (see `#2491768 <http://www.cubicweb.org/2491768>`_)
+
+* system_source.create_eid can get a range of IDs, to reduce overhead of batch
+  entity creation
+
+Behaviour Changes
+-----------------
+
+* The anonymous property of Session and Connection are now computed from the
+  related user login. If it matches the ``anonymous-user`` in the config the
+  connection is anonymous. Beware that the ``anonymous-user`` config is web
+  specific. Therefore, no session may be anonymous in a repository only setup.
+
+
+New Repository Access API
+-------------------------
+
+Connection replaces Session
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A new explicit Connection object replaces Session as the main repository entry
+point. Connection holds all the necessary methods to be used server-side
+(``execute``, ``commit``, ``rollback``, ``call_service``, ``entity_from_eid``,
+etc...). One obtains a new Connection object using ``session.new_cnx()``.
+Connection objects need to have an explicit begin and end. Use them as a context
+manager to never miss an end::
+
+    with session.new_cnx() as cnx:
+        cnx.execute('INSERT Elephant E, E name "Babar"')
+        cnx.commit()
+        cnx.execute('INSERT Elephant E, E name "Celeste"')
+        cnx.commit()
+    # Once you get out of the "with" clause, the connection is closed.
+
+Using the same Connection object in multiple threads will give you access to the
+same Transaction. However, Connection objects are not thread safe (hence at your
+own risks).
+
+``repository.internal_session`` is deprecated in favor of
+``repository.internal_cnx``. Note that internal connections are now `safe` by default,
+i.e. the integrity hooks are enabled.
+
+Backward compatibility is preserved on Session.
+
+
+dbapi vs repoapi
+~~~~~~~~~~~~~~~~
+
+A new API has been introduced to replace the dbapi. It is called `repoapi`.
+
+There are three relevant functions for now:
+
+* ``repoapi.get_repository`` returns a Repository object either from an
+  URI when used as ``repoapi.get_repository(uri)`` or from a config
+  when used as ``repoapi.get_repository(config=config)``.
+
+* ``repoapi.connect(repo, login, **credentials)`` returns a ClientConnection
+  associated with the user identified by the credentials. The
+  ClientConnection is associated with its own Session that is closed
+  when the ClientConnection is closed. A ClientConnection is a
+  Connection-like object to be used client side.
+
+* ``repoapi.anonymous_cnx(repo)`` returns a ClientConnection associated
+  with the anonymous user if described in the config.
+
+
+repoapi.ClientConnection replace dbapi.Connection and company
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On the client/web side, the Request is now using a ``repoapi.ClientConnection``
+instead of a ``dbapi.connection``. The ``ClientConnection`` has multiple backward
+compatible methods to make it look like a ``dbapi.Cursor`` and ``dbapi.Connection``.
+
+Session used on the Web side are now the same than the one used Server side.
+Some backward compatibility methods have been installed on the server side Session
+to ease the transition.
+
+The authentication stack has been altered to use the ``repoapi`` instead of
+the ``dbapi``. Cubes adding new element to this stack are likely to break.
+
+Session data can be accessed using the cnx.data dictionary, while
+transaction data is available through cnx.transaction_data.  These
+replace the [gs]et_shared_data methods with optional txid kwarg.
+
+New API in tests
+~~~~~~~~~~~~~~~~
+
+All current methods and attributes used to access the repo on ``CubicWebTC`` are
+deprecated. You may now use a ``RepoAccess`` object. A ``RepoAccess`` object is
+linked to a new ``Session`` for a specified user. It is able to create
+``Connection``, ``ClientConnection`` and web side requests linked to this
+session::
+
+    access = self.new_access('babar') # create a new RepoAccess for user babar
+    with access.repo_cnx() as cnx:
+        # some work with server side cnx
+        cnx.execute(...)
+        cnx.commit()
+        cnx.execute(...)
+        cnx.commit()
+
+    with access.client_cnx() as cnx:
+        # some work with client side cnx
+        cnx.execute(...)
+        cnx.commit()
+
+    with access.web_request(elephant='babar') as req:
+        # some work with client side cnx
+        elephant_name = req.form['elephant']
+        req.execute(...)
+        req.cnx.commit()
+
+By default ``testcase.admin_access`` contains a ``RepoAccess`` object for the
+default admin session.
+
+
+API changes
+-----------
+
+* ``RepositorySessionManager.postlogin`` is now called with two arguments,
+  request and session. And this now happens before the session is linked to the
+  request.
+
+* ``SessionManager`` and ``AuthenticationManager`` now take a repo object at
+  initialization time instead of a vreg.
+
+* The ``async`` argument of ``_cw.call_service`` has been dropped. All calls are
+  now  synchronous. The zmq notification bus looks like a good replacement for
+  most async use cases.
+
+* ``repo.stats()`` is now deprecated. The same information is available through
+  a service (``_cw.call_service('repo_stats')``).
+
+* ``repo.gc_stats()`` is now deprecated. The same information is available through
+  a service (``_cw.call_service('repo_gc_stats')``).
+
+* ``repo.register_user()`` is now deprecated.  The functionality is now
+  available through a service (``_cw.call_service('register_user')``).
+
+* ``request.set_session`` no longer takes an optional ``user`` argument.
+
+* CubicwebTC does not have repo and cnx as class attributes anymore. They are
+  standard instance attributes. ``set_cnx`` and ``_init_repo`` class methods
+  become instance methods.
+
+* ``set_cnxset`` and ``free_cnxset`` are deprecated. cnxset are now
+  automatically managed.
+
+* The implementation of cascading deletion when deleting `composite`
+  entities has changed. There comes a semantic change: merely deleting
+  a composite relation does not entail any more the deletion of the
+  component side of the relation.
+
+* ``_cw.user_callback`` and ``_cw.user_rql_callback`` are deprecated.  Users
+  are encouraged to write an actual controller (e.g. using ``ajaxfunc``)
+  instead of storing a closure in the session data.
+
+* A new ``entity.cw_linkable_rql`` method provides the rql to fetch all entities
+  that are already or may be related to the current entity using the given
+  relation.
+
+
+Deprecated Code Drops
+---------------------
+
+* session.hijack_user mechanism has been dropped.
+
+* EtypeRestrictionComponent has been removed, its functionality has been
+  replaced by facets a while ago.
+
+* the old multi-source support has been removed.  Only copy-based sources
+  remain, such as datafeed or ldapfeed.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.20.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,100 @@
+3.20 (06/01/2015)
+=================
+
+New features
+------------
+
+* virtual relations: a new ComputedRelation class can be used in
+  schema.py; its `rule` attribute is an RQL snippet that defines the new
+  relation.
+
+* computed attributes: an attribute can now be defined with a `formula`
+  argument (also an RQL snippet); it will be read-only, and updated
+  automatically.
+
+  Both of these features are described in `CWEP-002`_, and the updated
+  "Data model" chapter of the CubicWeb book.
+
+* cubicweb-ctl plugins can use the ``cubicweb.utils.admincnx`` function
+  to get a Connection object from an instance name.
+
+* new 'tornado' wsgi backend
+
+* session cookies have the HttpOnly flag, so they're no longer exposed to
+  javascript
+
+* rich text fields can be formatted as markdown
+
+* the edit controller detects concurrent editions, and raises a ValidationError
+  if an entity was modified between form generation and submission
+
+* cubicweb can use a postgresql "schema" (namespace) for its tables
+
+* "cubicweb-ctl configure" can be used to set values of the admin user
+  credentials in the sources configuration file
+
+* in debug mode, setting the _cwtracehtml parameter on a request allows tracing
+  where each bit of output is produced
+
+.. _CWEP-002: http://hg.logilab.org/review/cwep/file/tip/CWEP-002.rst
+
+
+API Changes
+-----------
+
+* ``ucsvreader()`` and ``ucsvreader_pb()`` from the ``dataimport`` module have
+  2 new keyword arguments ``delimiter`` and ``quotechar`` to replace the
+  ``separator`` and ``quote`` arguments respectively. This makes the API match
+  that of Python's ``csv.reader()``.  The old arguments are still supported
+  though deprecated.
+
+* the migration environment's ``remove_cube`` function is now called ``drop_cube``.
+
+* cubicweb.old.css is now cubicweb.css.  The previous "new"
+  cubicweb.css, along with its cubicweb.reset.css companion, have been
+  removed.
+
+* the jquery-treeview plugin was updated to its latest version
+
+
+Deprecated Code Drops
+----------------------
+
+* most of 3.10 and 3.11 backward compat is gone; this includes:
+
+  - CtxComponent.box_action() and CtxComponent.build_link()
+  
+  - cubicweb.devtools.htmlparser.XMLDemotingValidator
+  
+  - various methods and properties on Entities, replaced by cw_edited
+    and cw_attr_cache
+  
+  - 'commit_event' method on hooks, replaced by 'postcommit_event'
+  
+  - server.hook.set_operation(), replaced by
+    Operation.get_instance(...).add_data()
+  
+  - View.div_id(), View.div_class() and View.create_url()
+  
+  - `*VComponent` classes
+  
+  - in forms, Field.value() and Field.help() must take the form and
+    the field itself as arguments
+  
+  - form.render() must get `w` as a named argument, and
+    renderer.render() must take `w` as first argument
+  
+  - in breadcrumbs, the optional `recurs` argument must be a set, not
+    False
+  
+  - cubicweb.web.views.idownloadable.{download_box,IDownloadableLineView}
+  
+  - primary views no longer have `render_entity_summary` and `summary`
+    methods
+  
+  - WFHistoryVComponent's `cell_call` method is replaced by
+    `render_body`
+  
+  - cubicweb.dataimport.ObjectStore.add(), replaced by create_entity
+  
+  - ManageView.{folders,display_folders}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.21.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,77 @@
+3.21
+====
+
+New features
+------------
+
+* the datadir-url configuration option lets one choose where static data files
+  are served (instead of the default ${base-url}/data/)
+
+* some integrity checking that was previously implemented in Python was
+  moved to the SQL backend.  This includes some constraints, and
+  referential integrity.  Some consequences are that:
+
+  - disabling integrity hooks no longer disables those checks
+  - upgrades that modify constraints will fail when running on sqlite
+    (but upgrades aren't supported on sqlite anyway)
+
+  Note: as of 3.21.0, the upgrade script only works on PostgreSQL.  The
+  migration for SQLServer will be added in a future bugfix release.
+
+* for easier instance monitoring, cubicweb can regularly dump some statistics
+  (basically those exposed by the 'info' and 'gc' views) in json format to a file
+
+User-visible changes
+--------------------
+
+* the use of fckeditor for text form fields is disabled by default
+
+* the 'https-deny-anonymous' configuration setting no longer exists
+
+Code movement
+-------------
+
+The cubicweb.web.views.timeline module (providing the timeline-json, timeline
+and static-timeline views) has moved to a standalone cube_
+
+.. _cube: https://www.cubicweb.org/project/cubicweb-timeline
+
+API changes
+-----------
+
+* req.set_cookie's "expires" argument, if not None, is expected to be a
+  date or a datetime in UTC.  It was previously interpreted as localtime
+  with the UTC offset the server started in, which was inconsistent (we
+  are not aware of any users of that API).
+
+* the way to run tests on a postgresql backend has changed slightly, use
+  cubicweb.devtools.{start,stop}pgcluster in setUpModule and tearDownModule
+
+* the Connection and ClientConnection objects introduced in CubicWeb 3.19 have
+  been unified.  To connect to a repository, use::
+
+    session = repo.new_session(login, password=...)
+    with session.new_cnx() as cnx:
+        cnx.execute(...)
+
+  In tests, the 'repo_cnx' and 'client_cnx' methods of RepoAccess are now
+  aliases to 'cnx'.
+
+Deprecated code drops
+---------------------
+
+* the user_callback api has been removed; people should use plain
+  ajax functions instead
+
+* the `Pyro` and `Zmq-pickle` remote repository access methods have
+  been entirely removed (emerging alternatives such as rqlcontroller
+  and cwclientlib should be used instead).  Note that as a side effect,
+  "repository-only" instances (i.e. without a http component) are no
+  longer possible.  If you have any such instances, you will need to
+  rename the configuration file from repository.conf to all-in-one.conf
+  and run ``cubicweb-ctl upgrade`` to update it.  Likewise, remote cubicweb-ctl
+  shell is no longer available.
+
+* the old (deprecated since 3.19) `DBAPI` api is completely removed
+
+* cubicweb.toolsutils.config_connect() has been removed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/changelog.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,12 @@
+===================
+ Changelog history
+===================
+
+.. include:: 3.21.rst
+.. include:: 3.20.rst
+.. include:: 3.19.rst
+.. include:: 3.18.rst
+.. include:: 3.17.rst
+.. include:: 3.16.rst
+.. include:: 3.15.rst
+.. include:: 3.14.rst
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,14 @@
+Release notes
+-------------
+
+.. toctree::
+    :maxdepth: 1
+
+    3.21
+    3.20
+    3.19
+    3.18
+    3.17
+    3.16
+    3.15
+    3.14
--- a/doc/coding_standards_css.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-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
-
-
--- a/doc/coding_standards_js.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-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/conf.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,225 @@
+# -*- coding: utf-8 -*-
+# copyright 2003-2015 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 documentation build configuration file, created by
+# sphinx-quickstart on Fri Oct 31 09:10:36 2008.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# The contents of this file are pickled, so don't put values in the namespace
+# that aren't pickleable (module imports are okay, they're removed automatically).
+#
+# All configuration values have a default value; values that are commented out
+# serve to show the default value.
+
+from os import path as osp
+
+path = __file__
+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
+# absolute, like shown here.
+#sys.path.append(os.path.abspath('some/directory'))
+
+# General configuration
+# ---------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+  'sphinx.ext.autodoc', 
+  'sphinx.ext.viewcode',
+  'logilab.common.sphinx_ext',
+  ]
+
+autoclass_content = 'both'
+
+# Add any paths that contain templates here, relative to this directory.
+#templates_path = []
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General substitutions.
+project = 'CubicWeb'
+copyright = '2001-2015, Logilab'
+
+# The default replacements for |version| and |release|, also used in various
+# other places throughout the built documents.
+#
+# The short X.Y version.
+version = '.'.join(str(n) for n in cw['numversion'][:2])
+# The full version, including alpha/beta/rc tags.
+release = cw['version']
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+unused_docs = []
+
+# A list of glob-style patterns that should be excluded when looking
+# for source files. [1] They are matched against the source file names
+# relative to the source directory, using slashes as directory
+# separators on all platforms.
+exclude_patterns = ['book/_maybe_to_integrate']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+
+# Options for HTML output
+# -----------------------
+
+# The style sheet to use for HTML and HTML Help pages. A file of that name
+# must exist either in Sphinx' static/ path, or in one of the custom paths
+# given in html_static_path.
+#html_style = 'sphinx-default.css'
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+html_title = '%s %s' % (project, release)
+
+html_theme_path = ['_themes']
+html_theme = 'cubicweb'
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (within the static path) to place at the top of
+# the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, the reST sources are included in the HTML build as _sources/<name>.
+#html_copy_source = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+html_file_suffix = '.html'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Cubicwebdoc'
+
+
+# Options for LaTeX output
+# ------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, document class [howto/manual]).
+latex_documents = [
+  ('index', 'Cubicweb.tex', 'Cubicweb Documentation',
+   'Logilab', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+#aafig_format = dict(latex='pdf', html='svg', text=None)
+
+rst_epilog = """
+.. |cubicweb| replace:: *CubicWeb*
+.. |yams| replace:: *Yams*
+.. |rql| replace:: *RQL*
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/coding_standards_css.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -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/dev/coding_standards_js.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -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/dev/documenting.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,85 @@
+====
+Book
+====
+
+----
+Part
+----
+
+Chapter
+=======
+
+.. _Level1AnchorForLaterReference:
+
+Level 1 section
+---------------
+
+Level 2 section
+~~~~~~~~~~~~~~~
+
+Level 3 section
+```````````````
+
+
+
+*CubicWeb*
+
+
+inline directives:
+  :file:`directory/file`
+  :envvar:`AN_ENV_VARIABLE`
+  :command:`command --option arguments`
+
+  :ref:, :mod:
+
+
+.. sourcecode:: python
+
+   class SomePythonCode:
+     ...
+
+.. XXX a comment, wont be rendered
+
+
+a [foot note]_
+
+.. [foot note] the foot note content
+
+
+Boxes
+=====
+
+- warning box: 
+    .. warning::
+
+       Warning content
+- note box:
+    .. note::
+
+       Note content
+
+
+
+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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/features_list.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,223 @@
+=================
+CubicWeb features
+=================
+
+This page  tries to resume features found in the bare cubicweb framework,
+how mature and documented they are.
+
+:code maturity (CM):
+
+  - 0: experimental, not ready at all for production, may be killed
+
+  - 1: draft / unsatisfying, api may change in a near future, much probably in long
+       term
+
+  - 2: good enough, api sounds good but will probably evolve a bit with more
+    hindsight
+
+  - 3: mature, backward incompatible changes unexpected (may still evolve though,
+    of course)
+
+
+:documentation level (DL):
+
+  - 0: no documentation
+
+  - 1: poor documentation
+
+  - 2: some valuable documentation but some parts keep uncovered
+
+  - 3: good / complete documentation
+
+
+Instance configuration and maintainance
+=======================================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| setup - installation                                               | 2  | 3  |
+| setup - environment variables                                      | 3  | 2  |
+| setup - running modes                                              | 2  | 2  |
+| setup - administration tasks                                       | 2  | 2  |
+| setup - configuration file                                         | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| configuration - user / groups handling                             | 3  | 1  |
+| configuration - site configuration                                 | 3  | 1  |
+| configuration - distributed configuration                          | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| multi-sources - capabilities                                       | NA | 0  |
+| multi-sources - configuration                                      | 2  | 0  |
+| multi-sources - ldap integration                                   | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| usage - custom ReST markup                                         | 2  | 0  |
+| usage - personal preferences                                       | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+
+
+Core development
+================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| base - concepts                                                    | NA | 3  |
+| base - security model                                              | NA | 2  |
+| base - database initialization                                     | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| rql - base                                                         | 2  | 2  |
+| rql - write                                                        | 2  | 2  |
+| rql - function                                                     | 2  | 0  |
+| rql - outer joins                                                  | 2  | 1  |
+| rql - aggregates                                                   | 2  | 1  |
+| rql - subqueries                                                   | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| schema - base                                                      | 2  | 3  |
+| schema - constraints                                               | 3  | 2  |
+| schema - security                                                  | 2  | 2  |
+| schema - inheritance                                               | 1  | 1  |
+| schema - customization                                             | 1  | 1  |
+| schema - introspection                                             | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| vregistry - appobject                                              | 2  | 2  |
+| vregistry - registration                                           | 2  | 2  |
+| vregistry - selection                                              | 3  | 2  |
+| vregistry - core selectors                                         | 3  | 3  |
+| vregistry - custom selectors                                       | 2  | 1  |
+| vregistry - debugging selection                                    | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| entities - interfaces                                              | 2  | ?  |
+| entities - customization (`dc_`, ...)                              | 2  | ?  |
+| entities - app logic                                               | 2  | 2  |
+| entities - orm configuration                                       | 2  | 1  |
+| entities - pluggable mixins                                        | 1  | 0  |
+| entities - workflow                                                | 3  | 2  |
++--------------------------------------------------------------------+----+----+
+| dbapi - connection                                                 | 3  | 1  |
+| dbapi - data management                                            | 1  | 1  |
+| dbapi - result set                                                 | 3  | 1  |
+| dbapi - transaction, undo                                          | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| cube - layout                                                      | 2  | 3  |
+| cube - new cube                                                    | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| migration - context                                                | 2  | 1  |
+| migration - commands                                               | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| testlib - CubicWebTC                                               | 2  | 1  |
+| testlib - automatic tests                                          | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| i18n - mark string                                                 | 3  | 2  |
+| i18n - customize strings from other cubes / cubicweb               | 3  | 1  |
+| i18n - update catalog                                              | 3  | 2  |
++--------------------------------------------------------------------+----+----+
+| more - reloading tips                                              | NA | 0  |
+| more - site_cubicweb                                               | 2  | ?  |
+| more - adding options in configuration file                        | 3  | 0  |
+| more - adding options in site configuration / preferences          | 3  | ?  |
+| more - optimizing / profiling                                      | 2  | 1  |
+| more - c-c plugins                                                 | 3  | 0  |
+| more - crypto services                                             | 0  | 0  |
+| more - massive import                                              | 2  | 0  |
+| more - mime type based conversion                                  | 2  | 0  |
+| more - CWCache                                                     | 1  | 0  |
++--------------------------------------------------------------------+----+----+
+
+
+Web UI development
+==================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| base - web request                                                 | 2  | 2  |
+| base - exceptions                                                  | 2  | 0  |
+| base - session, authentication                                     | 1  | 0  |
+| base - http caching                                                | 2  | 1  |
+| base - external resources                                          | 2  | 2  |
+| base - static files                                                | 2  | ?  |
+| base - data sharing                                                | 2  | 2  |
+| base - graphical chart customization                               | 1  | 1  |
++--------------------------------------------------------------------+----+----+
+| publishing - cycle                                                 | 2  | 2  |
+| publishing - error handling                                        | 2  | 1  |
+| publishing - transactions                                          | NA | ?  |
++--------------------------------------------------------------------+----+----+
+| controller - base                                                  | 2  | 2  |
+| controller - view                                                  | 2  | 1  |
+| controller - edit                                                  | 2  | 1  |
+| controller - json                                                  | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| views - base                                                       | 2  | 2  |
+| views - templates                                                  | 2  | 2  |
+| views - boxes                                                      | 2  | 1  |
+| views - components                                                 | 2  | 1  |
+| views - primary                                                    | 2  | 1  |
+| views - tabs                                                       | 2  | 1  |
+| views - xml                                                        | 2  | 0  |
+| views - text                                                       | 2  | 1  |
+| views - table                                                      | 2  | 1  |
+| views - plot                                                       | 2  | 0  |
+| views - navigation                                                 | 2  | 0  |
+| views - calendar, timeline                                         | 2  | 0  |
+| views - index                                                      | 2  | 2  |
+| views - breadcrumbs                                                | 2  | 1  |
+| views - actions                                                    | 2  | 1  |
+| views - debugging                                                  | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| form - base                                                        | 2  | 1  |
+| form - fields                                                      | 2  | 1  |
+| form - widgets                                                     | 2  | 1  |
+| form - captcha                                                     | 2  | 0  |
+| form - renderers                                                   | 2  | 0  |
+| form - validation error handling                                   | 2  | 0  |
+| form - autoform                                                    | 2  | 2  |
+| form - reledit                                                     | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| facets - base                                                      | 2  | ?  |
+| facets - configuration                                             | 2  | 1  |
+| facets - custom facets                                             | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| css - base                                                         | 1  | 1  |
+| css - customization                                                | 1  | 1  |
++--------------------------------------------------------------------+----+----+
+| js - base                                                          | 1  | 1  |
+| js - jquery                                                        | 1  | 1  |
+| js - base functions                                                | 1  | 0  |
+| js - ajax                                                          | 1  | 0  |
+| js - widgets                                                       | 1  | 1  |
++--------------------------------------------------------------------+----+----+
+| other - page template                                              | 0  | 0  |
+| other - inline doc (wdoc)                                          | 2  | 0  |
+| other - magic search                                               | 2  | 0  |
+| other - url mapping                                                | 1  | 1  |
+| other - apache style url rewrite                                   | 1  | 1  |
+| other - sparql                                                     | 1  | 0  |
+| other - bookmarks                                                  | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+
+
+Repository development
+======================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| base - session                                                     | 2  | 2  |
+| base - more security control                                       | 2  | 0  |
+| base - debugging                                                   | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| hooks - development                                                | 2  | 2  |
+| hooks - abstract hooks                                             | 2  | 0  |
+| hooks - core hooks                                                 | 2  | 0  |
+| hooks - control                                                    | 2  | 0  |
+| hooks - operation                                                  | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| notification - sending email                                       | 2  | ?  |
+| notification - base views                                          | 1  | ?  |
+| notification - supervisions                                        | 1  | 0  |
++--------------------------------------------------------------------+----+----+
+| source - storages                                                  | 2  | 0  |
+| source - authentication plugins                                    | 2  | 0  |
+| source - custom sources                                            | 2  | 0  |
++--------------------------------------------------------------------+----+----+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/refactoring-the-css-with-uiprops.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,42 @@
+=========================================
+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.
--- a/doc/features_list.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,224 +0,0 @@
-=================
-CubicWeb features
-=================
-
-This page  tries to resume features found in the bare cubicweb framework,
-how mature and documented they are.
-
-:code maturity (CM):
-
-  - 0: experimental, not ready at all for production, may be killed
-
-  - 1: draft / unsatisfying, api may change in a near future, much probably in long
-       term
-
-  - 2: good enough, api sounds good but will probably evolve a bit with more
-    hindsight
-
-  - 3: mature, backward incompatible changes unexpected (may still evolve though,
-    of course)
-
-
-:documentation level (DL):
-
-  - 0: no documentation
-
-  - 1: poor documentation
-
-  - 2: some valuable documentation but some parts keep uncovered
-
-  - 3: good / complete documentation
-
-
-Instance configuration and maintainance
-=======================================
-
-+====================================================================+====+====+
-|  FEATURE                                                           | CM | DL |
-+====================================================================+====+====+
-| setup - installation                                               | 2  | 3  |
-| setup - environment variables                                      | 3  | 2  |
-| setup - running modes                                              | 2  | 2  |
-| setup - administration tasks                                       | 2  | 2  |
-| setup - configuration file                                         | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-| configuration - user / groups handling                             | 3  | 1  |
-| configuration - site configuration                                 | 3  | 1  |
-| configuration - distributed configuration                          | 2  | 1  |
-| configuration - pyro                                               | 2  | 2  |
-+--------------------------------------------------------------------+----+----+
-| multi-sources - capabilities                                       | NA | 0  |
-| multi-sources - configuration                                      | 2  | 0  |
-| multi-sources - ldap integration                                   | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-| usage - custom ReST markup                                         | 2  | 0  |
-| usage - personal preferences                                       | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-
-
-Core development
-================
-
-+====================================================================+====+====+
-|  FEATURE                                                           | CM | DL |
-+====================================================================+====+====+
-| base - concepts                                                    | NA | 3  |
-| base - security model                                              | NA | 2  |
-| base - database initialization                                     | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-| rql - base                                                         | 2  | 2  |
-| rql - write                                                        | 2  | 2  |
-| rql - function                                                     | 2  | 0  |
-| rql - outer joins                                                  | 2  | 1  |
-| rql - aggregates                                                   | 2  | 1  |
-| rql - subqueries                                                   | 2  | 0  |
-+--------------------------------------------------------------------+----+----+
-| schema - base                                                      | 2  | 3  |
-| schema - constraints                                               | 3  | 2  |
-| schema - security                                                  | 2  | 2  |
-| schema - inheritance                                               | 1  | 1  |
-| schema - customization                                             | 1  | 1  |
-| schema - introspection                                             | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-| vregistry - appobject                                              | 2  | 2  |
-| vregistry - registration                                           | 2  | 2  |
-| vregistry - selection                                              | 3  | 2  |
-| vregistry - core selectors                                         | 3  | 3  |
-| vregistry - custom selectors                                       | 2  | 1  |
-| vregistry - debugging selection                                    | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-| entities - interfaces                                              | 2  | ?  |
-| entities - customization (dc_,...)                                 | 2  | ?  |
-| entities - app logic                                               | 2  | 2  |
-| entities - orm configuration                                       | 2  | 1  |
-| entities - pluggable mixins                                        | 1  | 0  |
-| entities - workflow                                                | 3  | 2  |
-+--------------------------------------------------------------------+----+----+
-| dbapi - connection                                                 | 3  | 1  |
-| dbapi - data management                                            | 1  | 1  |
-| dbapi - result set                                                 | 3  | 1  |
-| dbapi - transaction, undo                                          | 2  | 0  |
-+--------------------------------------------------------------------+----+----+
-| cube - layout                                                      | 2  | 3  |
-| cube - new cube                                                    | 2  | 2  |
-+--------------------------------------------------------------------+----+----+
-| migration - context                                                | 2  | 1  |
-| migration - commands                                               | 2  | 2  |
-+--------------------------------------------------------------------+----+----+
-| testlib - CubicWebTC                                               | 2  | 1  |
-| testlib - automatic tests                                          | 2  | 2  |
-+--------------------------------------------------------------------+----+----+
-| i18n - mark string                                                 | 3  | 2  |
-| i18n - customize strings from other cubes / cubicweb               | 3  | 1  |
-| i18n - update catalog                                              | 3  | 2  |
-+--------------------------------------------------------------------+----+----+
-| more - reloading tips                                              | NA | 0  |
-| more - site_cubicweb                                               | 2  | ?  |
-| more - adding options in configuration file                        | 3  | 0  |
-| more - adding options in site configuration / preferences          | 3  | ?  |
-| more - optimizing / profiling                                      | 2  | 1  |
-| more - c-c plugins                                                 | 3  | 0  |
-| more - crypto services                                             | 0  | 0  |
-| more - massive import                                              | 2  | 0  |
-| more - mime type based conversion                                  | 2  | 0  |
-| more - CWCache                                                     | 1  | 0  |
-+--------------------------------------------------------------------+----+----+
-
-
-Web UI development
-==================
-
-+====================================================================+====+====+
-|  FEATURE                                                           | CM | DL |
-+====================================================================+====+====+
-| base - web request                                                 | 2  | 2  |
-| base - exceptions                                                  | 2  | 0  |
-| base - session, authentication                                     | 1  | 0  |
-| base - http caching                                                | 2  | 1  |
-| base - external resources                                          | 2  | 2  |
-| base - static files                                                | 2  | ?  |
-| base - data sharing                                                | 2  | 2  |
-| base - graphical chart customization                               | 1  | 1  |
-+--------------------------------------------------------------------+----+----+
-| publishing - cycle                                                 | 2  | 2  |
-| publishing - error handling                                        | 2  | 1  |
-| publishing - transactions                                          | NA | ?  |
-+--------------------------------------------------------------------+----+----+
-| controller - base                                                  | 2  | 2  |
-| controller - view                                                  | 2  | 1  |
-| controller - edit                                                  | 2  | 1  |
-| controller - json                                                  | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-| views - base                                                       | 2  | 2  |
-| views - templates                                                  | 2  | 2  |
-| views - boxes                                                      | 2  | 1  |
-| views - components                                                 | 2  | 1  |
-| views - primary                                                    | 2  | 1  |
-| views - tabs                                                       | 2  | 1  |
-| views - xml                                                        | 2  | 0  |
-| views - text                                                       | 2  | 1  |
-| views - table                                                      | 2  | 1  |
-| views - plot                                                       | 2  | 0  |
-| views - navigation                                                 | 2  | 0  |
-| views - calendar, timeline                                         | 2  | 0  |
-| views - index                                                      | 2  | 2  |
-| views - breadcrumbs                                                | 2  | 1  |
-| views - actions                                                    | 2  | 1  |
-| views - debugging                                                  | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-| form - base                                                        | 2  | 1  |
-| form - fields                                                      | 2  | 1  |
-| form - widgets                                                     | 2  | 1  |
-| form - captcha                                                     | 2  | 0  |
-| form - renderers                                                   | 2  | 0  |
-| form - validation error handling                                   | 2  | 0  |
-| form - autoform                                                    | 2  | 2  |
-| form - reledit                                                     | 2  | 0  |
-+--------------------------------------------------------------------+----+----+
-| facets - base                                                      | 2  | ?  |
-| facets - configuration                                             | 2  | 1  |
-| facets - custom facets                                             | 2  | 0  |
-+--------------------------------------------------------------------+----+----+
-| css - base                                                         | 1  | 1  |
-| css - customization                                                | 1  | 1  |
-+--------------------------------------------------------------------+----+----+
-| js - base                                                          | 1  | 1  |
-| js - jquery                                                        | 1  | 1  |
-| js - base functions                                                | 1  | 0  |
-| js - ajax                                                          | 1  | 0  |
-| js - widgets                                                       | 1  | 1  |
-+--------------------------------------------------------------------+----+----+
-| other - page template                                              | 0  | 0  |
-| other - inline doc (wdoc)                                          | 2  | 0  |
-| other - magic search                                               | 2  | 0  |
-| other - url mapping                                                | 1  | 1  |
-| other - apache style url rewrite                                   | 1  | 1  |
-| other - sparql                                                     | 1  | 0  |
-| other - bookmarks                                                  | 2  | 1  |
-+--------------------------------------------------------------------+----+----+
-
-
-Repository development
-======================
-
-+====================================================================+====+====+
-|  FEATURE                                                           | CM | DL |
-+====================================================================+====+====+
-| base - session                                                     | 2  | 2  |
-| base - more security control                                       | 2  | 0  |
-| base - debugging                                                   | 2  | 0  |
-+--------------------------------------------------------------------+----+----+
-| hooks - development                                                | 2  | 2  |
-| hooks - abstract hooks                                             | 2  | 0  |
-| hooks - core hooks                                                 | 2  | 0  |
-| hooks - control                                                    | 2  | 0  |
-| hooks - operation                                                  | 2  | 2  |
-+--------------------------------------------------------------------+----+----+
-| notification - sending email                                       | 2  | ?  |
-| notification - base views                                          | 1  | ?  |
-| notification - supervisions                                        | 1  | 0  |
-+--------------------------------------------------------------------+----+----+
-| source - storages                                                  | 2  | 0  |
-| source - authentication plugins                                    | 2  | 0  |
-| source - custom sources                                            | 2  | 0  |
-+--------------------------------------------------------------------+----+----+
Binary file doc/images/03-transitions-view_en.png has changed
Binary file doc/images/breadcrumbs_header.png has changed
Binary file doc/images/facet_date_range.png has changed
Binary file doc/images/facet_has_image.png has changed
Binary file doc/images/facet_overview.png has changed
Binary file doc/images/facet_range.png has changed
Binary file doc/images/lax-book_00-login_en.png has changed
Binary file doc/images/lax-book_01-start_en.png has changed
Binary file doc/images/lax-book_02-cookie-values_en.png has changed
Binary file doc/images/lax-book_02-create-blog_en.png has changed
Binary file doc/images/lax-book_03-list-one-blog_en.png has changed
Binary file doc/images/lax-book_03-site-config-panel_en.png has changed
Binary file doc/images/lax-book_03-state-submitted_en.png has changed
Binary file doc/images/lax-book_03-transitions-view_en.png has changed
Binary file doc/images/lax-book_04-detail-one-blog_en.png has changed
Binary file doc/images/lax-book_05-list-two-blog_en.png has changed
Binary file doc/images/lax-book_06-add-relation-entryof_en.png has changed
Binary file doc/images/lax-book_06-main-template-logo_en.png has changed
Binary file doc/images/lax-book_07-detail-one-blogentry_en.png has changed
Binary file doc/images/lax-book_08-schema_en.png has changed
Binary file doc/images/lax-book_09-new-view-blogentry_en.png has changed
Binary file doc/images/lax-book_10-blog-with-two-entries_en.png has changed
Binary file doc/images/main_template.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/images/main_template.svg	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="1036.6421"
+   height="845.07812"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   sodipodi:docname="main_template.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   version="1.0"
+   inkscape:export-filename="/home/auc/cw/doc/book/en/images/main_template.png"
+   inkscape:export-xdpi="60.659016"
+   inkscape:export-ydpi="60.659016">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.80355603"
+     inkscape:cx="510.91495"
+     inkscape:cy="422.53906"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="925"
+     inkscape:window-height="1168"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:snap-bbox="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Calque 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(162.2968,90.697922)">
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect2439"
+       width="854.37006"
+       height="698.2019"
+       x="20.307629"
+       y="-20.575344" />
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3301"
+       width="816.3457"
+       height="508.15628"
+       x="31.751091"
+       y="96.33345" />
+    <g
+       id="g3220"
+       transform="matrix(1.0035394,0,0,1,0.5745006,0)">
+      <rect
+         y="-89.447922"
+         x="-161.0468"
+         height="55.714287"
+         width="1031.1713"
+         id="rect3240"
+         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.50000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text3264"
+         y="-51.771908"
+         x="757.85767"
+         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           id="tspan3266"
+           y="-51.771908"
+           x="757.85767"
+           sodipodi:role="line">header</tspan></text>
+    </g>
+    <rect
+       style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3270"
+       width="167.87744"
+       height="707.71222"
+       x="-160.02441"
+       y="-24.671618" />
+    <g
+       id="g2434"
+       transform="matrix(0.975467,0,0,1,0.6942419,-3.6587365)">
+      <rect
+         y="35.365849"
+         x="29.548275"
+         height="55.714287"
+         width="842.59979"
+         id="rect3279"
+         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text3281"
+         y="72.885193"
+         x="681.65283"
+         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           id="tspan3283"
+           y="72.885193"
+           x="681.65283"
+           sodipodi:role="line">contentheader</tspan></text>
+    </g>
+    <g
+       id="g3170"
+       transform="matrix(1.0023324,0,0,1,-2.0421673,-10.976211)">
+      <rect
+         y="698.6355"
+         x="-158.28485"
+         height="55.714287"
+         width="1032.5997"
+         id="rect3285"
+         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text3287"
+         y="736.52045"
+         x="770.28204"
+         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           id="tspan3289"
+           y="736.52045"
+           x="770.28204"
+           sodipodi:role="line">footer</tspan></text>
+    </g>
+    <g
+       id="g3211" />
+    <g
+       id="g3215"
+       transform="matrix(0.9712065,0,0,1,0.7659296,-17.074106)">
+      <rect
+         style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect3291"
+         width="844.62012"
+         height="55.714287"
+         x="27.850754"
+         y="629.88562" />
+      <text
+         id="text3293"
+         y="666.60339"
+         x="692.85773"
+         style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           id="tspan3295"
+           y="666.60339"
+           x="692.85773"
+           sodipodi:role="line">contentfooter</tspan></text>
+    </g>
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="-143.67273"
+       y="20.58094"
+       id="text3297"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2432"
+         x="-143.67273"
+         y="20.58094">left column</tspan></text>
+    <text
+       transform="scale(0.9876573,1.0124969)"
+       id="text3175"
+       y="12.071429"
+       x="721.0575"
+       style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       xml:space="preserve"><tspan
+         id="tspan3177"
+         y="12.071429"
+         x="721.0575"
+         sodipodi:role="line">contentcol</tspan></text>
+    <text
+       transform="scale(0.9876573,1.0124969)"
+       id="text3179"
+       y="126.27104"
+       x="701.45959"
+       style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       xml:space="preserve"><tspan
+         id="tspan3181"
+         y="126.27104"
+         x="701.45959"
+         sodipodi:role="line">contentmain</tspan></text>
+  </g>
+</svg>
Binary file doc/images/main_template_layout.png has changed
Binary file doc/images/primaryview_template.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/images/primaryview_template.svg	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="1036.6421"
+   height="845.07812"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   sodipodi:docname="primaryview_template.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   version="1.0"
+   inkscape:export-filename="/home/steph/local/fcubicweb/cubicweb/doc/book/en/images/primaryview_template.png"
+   inkscape:export-xdpi="43.451603"
+   inkscape:export-ydpi="43.451603">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.9357135"
+     inkscape:cx="518.32104"
+     inkscape:cy="337.0428"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1307"
+     inkscape:window-height="1168"
+     inkscape:window-x="0"
+     inkscape:window-y="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Calque 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(162.2968,90.697922)">
+    <g
+       id="g3869"
+       transform="matrix(1,0,0,1.0373644,0,-72.039777)"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449">
+      <rect
+         y="-15.840891"
+         x="-159.08963"
+         height="770.11017"
+         width="1033.0049"
+         id="rect3301"
+         style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.90144825;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text3865"
+         y="19.784882"
+         x="-150.07172"
+         style="font-size:28.67479324px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           id="tspan3867"
+           y="19.784882"
+           x="-150.07172"
+           sodipodi:role="line">contentmain</tspan></text>
+    </g>
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.45654476;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect2383"
+       width="772.32111"
+       height="43.888428"
+       x="-131.1837"
+       y="86.559296"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449" />
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
+       x="-122.69418"
+       y="115.50363"
+       id="text2385"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         x="-122.69418"
+         y="115.50363"
+         id="tspan3163">navcontenttop</tspan></text>
+    <rect
+       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3167"
+       width="770.26868"
+       height="203.16078"
+       x="-125.88269"
+       y="172.90417"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449" />
+    <text
+       xml:space="preserve"
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="348.26724"
+       y="205.34305"
+       id="text3169"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         x="348.26724"
+         y="205.34305"
+         id="tspan3171">render_entity_attributes()</tspan></text>
+    <rect
+       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3173"
+       width="769.93549"
+       height="237.84663"
+       x="-125.03326"
+       y="391.32156"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449" />
+    <text
+       xml:space="preserve"
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="360.99954"
+       y="428.38055"
+       id="text3175"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         x="360.99954"
+         y="428.38055"
+         id="tspan3177">render_entity_relations()</tspan></text>
+    <rect
+       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:2.15903592;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3185"
+       width="178.93939"
+       height="612.36584"
+       x="667.10443"
+       y="84.64225"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449" />
+    <text
+       xml:space="preserve"
+       style="font-size:22px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
+       x="105.32364"
+       y="-810.65997"
+       id="text3187"
+       transform="matrix(0,1,-1,0,0,0)"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         id="tspan2408">render_side_boxes()</tspan></text>
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:3.0652349;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3191"
+       width="771.97766"
+       height="55.647793"
+       x="-127.80586"
+       y="642.0293"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449" />
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="-121.22153"
+       y="674.1748"
+       id="text3181"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         x="-121.22153"
+         y="674.1748"
+         id="tspan3183">navcontentbottom</tspan></text>
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3881"
+       width="986.90503"
+       height="45.800392"
+       x="-128.34428"
+       y="-31.574066"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449" />
+    <text
+       xml:space="preserve"
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="355.60541"
+       y="-2.7424495"
+       id="text3883"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         x="355.60541"
+         y="-2.7424495"
+         id="tspan3885">render_entity_toolbox(), render_entity_title()</tspan></text>
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3890"
+       width="986.90503"
+       height="45.800392"
+       x="-128.87863"
+       y="19.723684"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449" />
+    <text
+       xml:space="preserve"
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="565.71027"
+       y="50.135612"
+       id="text3892"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         x="565.71027"
+         y="50.135612"
+         id="tspan3894">render_entity_summary()</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="87.154541"
+       y="114.2578"
+       id="text3899"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         id="tspan3903"
+         x="87.154541"
+         y="114.2578">content_navigation_components('navcontenttop')</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+       x="88.46772"
+       y="675.71582"
+       id="text2410"
+       sodipodi:linespacing="125%"
+       inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+       inkscape:export-xdpi="60.912449"
+       inkscape:export-ydpi="60.912449"><tspan
+         sodipodi:role="line"
+         id="tspan2412"
+         x="88.46772"
+         y="675.71582">content_navigation_components('navcontenttop')</tspan></text>
+  </g>
+</svg>
Binary file doc/images/request_session.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/images/request_session.svg	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="85.960938"
+   height="12.382812"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="request_session.svg">
+  <defs
+     id="defs4">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0.0"
+       refX="0.0"
+       id="Arrow1Lend"
+       style="overflow:visible;">
+      <path
+         id="path3822"
+         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none;"
+         transform="scale(0.8) rotate(180) translate(12.5,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="25.928992"
+     inkscape:cy="-185.87004"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="958"
+     inkscape:window-height="1160"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="0"
+     inkscape:snap-global="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-263.52249,-495.73373)">
+    <rect
+       style="fill:#ffffff;stroke:#000000;stroke-width:0.92460138;stroke-opacity:1"
+       id="rect3773"
+       width="214.15233"
+       height="184.80336"
+       x="57.578697"
+       y="366.01306" />
+    <rect
+       id="rect2985"
+       width="216.86372"
+       height="183.54575"
+       x="348.50262"
+       y="367.78079"
+       style="fill:#ffffff;stroke:#000000;stroke-width:0.55298227;stroke-opacity:1" />
+    <text
+       xml:space="preserve"
+       style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="376.7869"
+       y="399.80365"
+       id="text3755"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3757"
+         x="376.7869"
+         y="399.80365">Repository</tspan></text>
+    <rect
+       style="fill:#ffffff;stroke:#000000;stroke-opacity:1"
+       id="rect3759"
+       width="144.45181"
+       height="104.04572"
+       x="237.38585"
+       y="423.03714" />
+    <text
+       xml:space="preserve"
+       style="font-size:24px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="262.63968"
+       y="470.51431"
+       id="text3761"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3763"
+         x="262.63968"
+         y="470.51431">REPOAPI</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="262.63968"
+       y="507.88998"
+       id="text3765"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3767"
+         x="262.63968"
+         y="507.88998">connection</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="419.21332"
+       y="509.91025"
+       id="text3769"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3771"
+         x="419.21332"
+         y="509.91025">session</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="102.02541"
+       y="397.78333"
+       id="text3775"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3777"
+         x="102.02541"
+         y="397.78333">Client</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="116.16754"
+       y="507.88995"
+       id="text3779"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3781"
+         x="116.16754"
+         y="507.88995">request</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="361.50729"
+       y="585.89832"
+       id="text3802"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3804"
+         x="361.50729"
+         y="585.89832">database </tspan><tspan
+         sodipodi:role="line"
+         x="361.50729"
+         y="605.89832"
+         id="tspan3806">connection</tspan></text>
+    <rect
+       style="fill:#ffffff;stroke:#000000;stroke-width:1.48014534;stroke-opacity:1"
+       id="rect3808"
+       width="192.09367"
+       height="58.095726"
+       x="365.79443"
+       y="621.50018" />
+    <text
+       xml:space="preserve"
+       style="font-size:36px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="369.5885"
+       y="662.66992"
+       id="text3810"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3812"
+         x="369.5885"
+         y="662.66992">Database</tspan></text>
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:none"
+       d="M 197.57252,125.76645 195.76971,55.592808"
+       id="path4260"
+       inkscape:connector-type="polyline"
+       inkscape:connector-curvature="3"
+       inkscape:connection-start="#rect3808"
+       inkscape:connection-start-point="d4"
+       inkscape:connection-end="#rect2985"
+       inkscape:connection-end-point="d4"
+       transform="translate(263.52249,495.73373)" />
+  </g>
+</svg>
Binary file doc/images/server-class-diagram.png has changed
Binary file doc/images/tutos-base_blog-form_en.png has changed
Binary file doc/images/tutos-base_blog-primary-after-post-creation_en.png has changed
Binary file doc/images/tutos-base_blog-primary_en.png has changed
Binary file doc/images/tutos-base_blogs-list_en.png has changed
Binary file doc/images/tutos-base_form-generic-relations_en.png has changed
Binary file doc/images/tutos-base_index_en.png has changed
Binary file doc/images/tutos-base_login-form_en.png has changed
Binary file doc/images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-community-custom-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-community-default-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-community-taggable-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-custom-footer_en.png has changed
Binary file doc/images/tutos-base_myblog-schema_en.png has changed
Binary file doc/images/tutos-base_myblog-siteinfo_en.png has changed
Binary file doc/images/tutos-base_schema_en.png has changed
Binary file doc/images/tutos-base_siteconfig_en.png has changed
Binary file doc/images/tutos-base_user-menu_en.png has changed
Binary file doc/images/tutos-photowebsite_background-image.png has changed
Binary file doc/images/tutos-photowebsite_boxes.png has changed
Binary file doc/images/tutos-photowebsite_breadcrumbs.png has changed
Binary file doc/images/tutos-photowebsite_facets.png has changed
Binary file doc/images/tutos-photowebsite_grey-box.png has changed
Binary file doc/images/tutos-photowebsite_index-after.png has changed
Binary file doc/images/tutos-photowebsite_index-before.png has changed
Binary file doc/images/tutos-photowebsite_login-box.png has changed
Binary file doc/images/tutos-photowebsite_prevnext.png has changed
Binary file doc/images/tutos-photowebsite_ui1.png has changed
Binary file doc/images/tutos-photowebsite_ui2.png has changed
Binary file doc/images/tutos-photowebsite_ui3.png has changed
Binary file doc/images/undo_history-view_w600.png has changed
Binary file doc/images/undo_mesage_w600.png has changed
Binary file doc/images/undo_startup-link_w600.png has changed
Binary file doc/images/views-table-filter-shadow.png has changed
Binary file doc/images/views-table-filter.png has changed
Binary file doc/images/views-table-shadow.png has changed
Binary file doc/images/views-table.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,125 @@
+=====================================================
+|cubicweb| - The Semantic Web is a construction game!
+=====================================================
+
+|cubicweb| is a semantic web application framework, licensed under the LGPL,
+that empowers developers to efficiently build web applications by reusing
+components (called `cubes`) and following the well known object-oriented design
+principles.
+
+Main Features
+~~~~~~~~~~~~~
+
+* an engine driven by the explicit :ref:`data model
+  <TutosBaseCustomizingTheApplicationDataModel>` of the application,
+
+* a query language named :ref:`RQL <RQL>` similar to W3C's SPARQL,
+
+* a :ref:`selection+view <TutosBaseCustomizingTheApplicationCustomViews>`
+  mechanism for semi-automatic XHTML/XML/JSON/text generation,
+
+* a library of reusable :ref:`components <Cube>` (data model and views) that
+  fulfill common needs,
+
+* the power and flexibility of the Python_ programming language,
+
+* the reliability of SQL databases, LDAP directories, Subversion and Mercurial
+  for storage backends.
+
+Built since 2000 from an R&D effort still continued, supporting 100,000s of
+daily visits at some production sites, |cubicweb| is a proven end to end solution
+for semantic web application development that promotes quality, reusability and
+efficiency.
+
+QuickStart
+~~~~~~~~~~
+
+The impatient developer will move right away to :ref:`SetUpEnv` then to :ref:`ConfigEnv`.
+
+Social
+~~~~~~
+
+*   Chat on the `jabber forum`_
+*   Discuss on the `mailing-list`_
+*   Discover on the `blog`_
+*   Contribute on the forge_
+
+
+.. _Logilab: http://www.logilab.fr/
+.. _forge: http://www.cubicweb.org/project/
+.. _Python: http://www.python.org/
+.. _`jabber forum`: http://www.logilab.org/blogentry/6718
+.. _`mailing-list`: http://lists.cubicweb.org/mailman/listinfo/cubicweb
+.. _blog: http://www.cubicweb.org/blog/1238
+
+
+Narrative Documentation
+~~~~~~~~~~~~~~~~~~~~~~~
+
+A.k.a. "The Book"
+
+.. toctree::
+   :maxdepth: 2
+
+   book/intro/index
+
+.. toctree::
+   :maxdepth: 2
+
+   tutorials/index
+   
+.. toctree::
+   :maxdepth: 3
+
+   book/devrepo/index
+   book/devweb/index
+
+.. toctree::
+   :maxdepth: 2
+
+   book/admin/index
+   book/additionnal_services/index
+   book/annexes/index
+
+
+
+Changes
+~~~~~~~
+
+.. toctree::
+   :maxdepth: 2
+
+   changes/changelog
+
+
+Reference documentation
+~~~~~~~~~~~~~~~~~~~~~~~
+
+API
+'''
+
+.. toctree::
+    :maxdepth: 1
+    :glob:
+
+    api/*
+
+.. toctree::
+    :maxdepth: 1
+
+    js_api/index
+
+Developpers
+~~~~~~~~~~~
+
+.. toctree::
+    :maxdepth: 1
+    :glob:
+
+    dev/*
+
+Indexes
+~~~~~~~
+
+* the :ref:`genindex`,
+* the :ref:`modindex`,
--- a/doc/refactoring-the-css-with-uiprops.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-=========================================
-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.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tools/mode_plan.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,50 @@
+# 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 mode_plan import *
+>>> ls()
+<list of directory content>
+>>> ren('A01','A03')
+rename A010-joe.en.txt to A030-joe.en.txt
+accept [y/N]?
+"""
+
+def ren(a,b):
+    names = glob.glob('%s*'%a)
+    for name in names :
+        print 'rename %s to %s' % (name, name.replace(a,b))
+    if raw_input('accept [y/N]?').lower() =='y':
+        for name in names:
+            os.system('hg mv %s %s' % (name, name.replace(a,b)))
+
+
+def ls(): print '\n'.join(sorted(os.listdir('.')))
+
+def move():
+    filenames = []
+    for name in sorted(os.listdir('.')):
+        num = name[:2]
+        if num.isdigit():
+            filenames.append( (int(num), name) )
+
+
+    #print filenames
+
+    for num, name in filenames:
+        if num >= start:
+            print 'hg mv %s %2i%s' %(name,num+1,name[2:])
--- a/doc/tools/pyjsrest.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/doc/tools/pyjsrest.py	Fri Oct 09 17:52:14 2015 +0200
@@ -92,6 +92,9 @@
                 f_rst.write(rst_content)
     stream = open(osp.join(rst_dir, 'index.rst'), 'w')
     stream.write('''
+Javascript API
+==============
+
 .. toctree::
     :maxdepth: 1
 
@@ -134,7 +137,6 @@
     'cubicweb.preferences',
     'cubicweb.edition',
     'cubicweb.reledit',
-    'cubicweb.timeline-ext',
 ]
 
 FILES_TO_IGNORE = set([
@@ -152,7 +154,6 @@
     'cubicweb.fckcwconfig-full.js',
     'cubicweb.goa.js',
     'cubicweb.compat.js',
-    'cubicweb.timeline-bundle.js',
     ])
 
 if __name__ == '__main__':
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,29 @@
+
+.. _TutosPhotoWebSite:
+
+Building a photo gallery with |cubicweb|
+========================================
+
+Desired features
+----------------
+
+* basically a photo gallery
+
+* photo stored on the file system and displayed dynamically through a web interface
+
+* navigation through folder (album), tags, geographical zone, people on the
+  picture... using facets
+
+* advanced security (not everyone can see everything). More on this later.
+
+
+.. toctree::
+   :maxdepth: 2
+
+   part01_create-cube
+   part02_security
+   part03_bfss
+   part04_ui-base
+   part05_ui-advanced
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part01_create-cube.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,155 @@
+.. _TutosPhotoWebSiteCubeCreation:
+
+Cube creation and schema definition
+-----------------------------------
+
+.. _adv_tuto_create_new_cube:
+
+Step 1: creating a new cube for my web site
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One note about my development environment: I wanted to use the packaged
+version of CubicWeb and cubes while keeping my cube in my user
+directory, let's say `~src/cubes`.  I achieve this by setting the
+following environment variables::
+
+  CW_CUBES_PATH=~/src/cubes
+  CW_MODE=user
+
+I can now create the cube which will hold custom code for this web
+site using::
+
+  cubicweb-ctl newcube --directory=~/src/cubes sytweb
+
+
+.. _adv_tuto_assemble_cubes:
+
+Step 2: pick building blocks into existing cubes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Almost everything I want to handle in my web-site is somehow already modelized in
+existing cubes that I'll extend for my need. So I'll pick the following cubes:
+
+* `folder`, containing the `Folder` entity type, which will be used as
+  both 'album' and a way to map file system folders. Entities are
+  added to a given folder using the `filed_under` relation.
+
+* `file`, containing `File` entity type, gallery view, and a file system import
+  utility.
+
+* `zone`, containing the `Zone` entity type for hierarchical geographical
+  zones. Entities (including sub-zones) are added to a given zone using the
+  `situated_in` relation.
+
+* `person`, containing the `Person` entity type plus some basic views.
+
+* `comment`, providing a full commenting system allowing one to comment entity types
+  supporting the `comments` relation by adding a `Comment` entity.
+
+* `tag`, providing a full tagging system as an easy and powerful way to classify
+  entities supporting the `tags` relation by linking the to `Tag` entities. This
+  will allows navigation into a large number of picture.
+
+Ok, now I'll tell my cube requires all this by editing :file:`cubes/sytweb/__pkginfo__.py`:
+
+  .. sourcecode:: python
+
+    __depends__ = {'cubicweb': '>= 3.10.0',
+                   'cubicweb-file': '>= 1.9.0',
+		   'cubicweb-folder': '>= 1.1.0',
+		   'cubicweb-person': '>= 1.2.0',
+		   'cubicweb-comment': '>= 1.2.0',
+		   'cubicweb-tag': '>= 1.2.0',
+		   'cubicweb-zone': None}
+
+Notice that you can express minimal version of the cube that should be used,
+`None` meaning whatever version available. All packages starting with 'cubicweb-'
+will be recognized as being cube, not bare python packages. You can still specify
+this explicitly using instead the `__depends_cubes__` dictionary which should
+contains cube's name without the prefix. So the example below would be written
+as:
+
+  .. sourcecode:: python
+
+    __depends__ = {'cubicweb': '>= 3.10.0'}
+    __depends_cubes__ = {'file': '>= 1.9.0',
+		         'folder': '>= 1.1.0',
+		   	 'person': '>= 1.2.0',
+		   	 'comment': '>= 1.2.0',
+		   	 'tag': '>= 1.2.0',
+		   	 'zone': None}
+
+If your cube is packaged for debian, it's a good idea to update the
+`debian/control` file at the same time, so you won't forget it.
+
+
+Step 3: glue everything together in my cube's schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
+
+    from yams.buildobjs import RelationDefinition
+
+    class comments(RelationDefinition):
+	subject = 'Comment'
+	object = 'File'
+	cardinality = '1*'
+	composite = 'object'
+
+    class tags(RelationDefinition):
+	subject = 'Tag'
+	object = 'File'
+
+    class filed_under(RelationDefinition):
+	subject = 'File'
+	object = 'Folder'
+
+    class situated_in(RelationDefinition):
+	subject = 'File'
+	object = 'Zone'
+
+    class displayed_on(RelationDefinition):
+	subject = 'Person'
+	object = 'File'
+
+
+This schema:
+
+* allows to comment and tag on `File` entity type by adding the `comments` and
+  `tags` relations. This should be all we've to do for this feature since the
+  related cubes provide 'pluggable section' which are automatically displayed on
+  the primary view of entity types supporting the relation.
+
+* adds a `situated_in` relation definition so that image entities can be
+  geolocalized.
+
+* add a new relation `displayed_on` relation telling who can be seen on a
+  picture.
+
+This schema will probably have to evolve as time goes (for security handling at
+least), but since the possibility to let a schema evolve is one of CubicWeb's
+features (and goals), we won't worry about it for now and see that later when needed.
+
+
+Step 4: creating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that I have a schema, I want to create an instance. To
+do so using this new 'sytweb' cube, I run::
+
+  cubicweb-ctl create sytweb sytweb_instance
+
+Hint: if you get an error while the database is initialized, you can
+avoid having to answer the questions again by running::
+
+   cubicweb-ctl db-create sytweb_instance
+
+This will use your already configured instance and start directly from the create
+database step, thus skipping questions asked by the 'create' command.
+
+Once the instance and database are fully initialized, run ::
+
+  cubicweb-ctl start sytweb_instance
+
+to start the instance, check you can connect on it, etc...
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part02_security.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,440 @@
+.. _TutosPhotoWebSiteSecurity:
+
+Security, testing and migration
+-------------------------------
+
+This part will cover various topics:
+
+* configuring security
+* migrating existing instance
+* writing some unit tests
+
+Here is the ``read`` security model I want:
+
+* folders, files, images and comments should have one of the following visibility:
+
+  - ``public``, everyone can see it
+  - ``authenticated``, only authenticated users can see it
+  - ``restricted``, only a subset of authenticated users can see it
+
+* managers (e.g. me) can see everything
+* only authenticated users can see people
+* everyone can see classifier entities, such as tag and zone
+
+Also, unless explicitly specified, the visibility of an image should be the same as
+its parent folder, as well as visibility of a comment should be the same as the
+commented entity. If there is no parent entity, the default visibility is
+``authenticated``.
+
+Regarding write security, that's much easier:
+* anonymous can't write anything
+* authenticated users can only add comment
+* managers will add the remaining stuff
+
+Now, let's implement that!
+
+Proper security in CubicWeb is done at the schema level, so you don't have to
+bother with it in views: users will only see what they can see automatically.
+
+.. _adv_tuto_security:
+
+Step 1: configuring security into the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In schema, you can grant access according to groups, or to some RQL expressions:
+users get access if the expression returns some results. To implement the read
+security defined earlier, groups are not enough, we'll need some RQL expression. Here
+is the idea:
+
+* add a `visibility` attribute on Folder, File and Comment, which may be one of
+  the value explained above
+
+* add a `may_be_read_by` relation from Folder, File and Comment to users,
+  which will define who can see the entity
+
+* security propagation will be done in hook.
+
+So the first thing to do is to modify my cube's schema.py to define those
+relations:
+
+.. sourcecode:: python
+
+    from yams.constraints import StaticVocabularyConstraint
+
+    class visibility(RelationDefinition):
+	subject = ('Folder', 'File', 'Comment')
+	object = 'String'
+	constraints = [StaticVocabularyConstraint(('public', 'authenticated',
+						   'restricted', 'parent'))]
+	default = 'parent'
+	cardinality = '11' # required
+
+    class may_be_read_by(RelationDefinition):
+        __permissions__ = {
+	    'read':   ('managers', 'users'),
+	    'add':    ('managers',),
+	    'delete': ('managers',),
+	    }
+
+	subject = ('Folder', 'File', 'Comment',)
+	object = 'CWUser'
+
+We can note the following points:
+
+* we've added a new `visibility` attribute to folder, file, image and comment
+  using a `RelationDefinition`
+
+* `cardinality = '11'` means this attribute is required. This is usually hidden
+  under the `required` argument given to the `String` constructor, but we can
+  rely on this here (same thing for StaticVocabularyConstraint, which is usually
+  hidden by the `vocabulary` argument)
+
+* the `parent` possible value will be used for visibility propagation
+
+* think to secure the `may_be_read_by` permissions, else any user can add/delete it
+  by default, which somewhat breaks our security model...
+
+Now, we should be able to define security rules in the schema, based on these new
+attribute and relation. Here is the code to add to *schema.py*:
+
+.. sourcecode:: python
+
+    from cubicweb.schema import ERQLExpression
+
+    VISIBILITY_PERMISSIONS = {
+	'read':   ('managers',
+		   ERQLExpression('X visibility "public"'),
+		   ERQLExpression('X may_be_read_by U')),
+	'add':    ('managers',),
+	'update': ('managers', 'owners',),
+	'delete': ('managers', 'owners'),
+	}
+    AUTH_ONLY_PERMISSIONS = {
+	    'read':   ('managers', 'users'),
+	    'add':    ('managers',),
+	    'update': ('managers', 'owners',),
+	    'delete': ('managers', 'owners'),
+	    }
+    CLASSIFIERS_PERMISSIONS = {
+	    'read':   ('managers', 'users', 'guests'),
+	    'add':    ('managers',),
+	    'update': ('managers', 'owners',),
+	    'delete': ('managers', 'owners'),
+	    }
+
+    from cubes.folder.schema import Folder
+    from cubes.file.schema import File
+    from cubes.comment.schema import Comment
+    from cubes.person.schema import Person
+    from cubes.zone.schema import Zone
+    from cubes.tag.schema import Tag
+
+    Folder.__permissions__ = VISIBILITY_PERMISSIONS
+    File.__permissions__ = VISIBILITY_PERMISSIONS
+    Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
+    Comment.__permissions__['add'] = ('managers', 'users',)
+    Person.__permissions__ = AUTH_ONLY_PERMISSIONS
+    Zone.__permissions__ = CLASSIFIERS_PERMISSIONS
+    Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
+
+What's important in there:
+
+* `VISIBILITY_PERMISSIONS` provides read access to managers group, if
+  `visibility` attribute's value is 'public', or if user (designed by the 'U'
+  variable in the expression) is linked to the entity (the 'X' variable) through
+  the `may_be_read_by` permission
+
+* we modify permissions of the entity types we use by importing them and
+  modifying their `__permissions__` attribute
+
+* notice the `.copy()`: we only want to modify 'add' permission for `Comment`,
+  not for all entity types using `VISIBILITY_PERMISSIONS`!
+
+* the remaining part of the security model is done using regular groups:
+
+  - `users` is the group to which all authenticated users will belong
+  - `guests` is the group of anonymous users
+
+
+.. _adv_tuto_security_propagation:
+
+Step 2: security propagation in hooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To fullfill the requirements, we have to implement::
+
+  Also, unless explicity specified, visibility of an image should be the same as
+  its parent folder, as well as visibility of a comment should be the same as the
+  commented entity.
+
+This kind of `active` rule will be done using CubicWeb's hook
+system. Hooks are triggered on database events such as addition of a new
+entity or relation.
+
+The tricky part of the requirement is in *unless explicitly specified*, notably
+because when the entity is added, we don't know yet its 'parent'
+entity (e.g. Folder of an File, File commented by a Comment). To handle such things,
+CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
+
+In our case we will:
+
+* on entity creation, schedule an operation that will set default visibility
+
+* when a "parent" relation is added, propagate parent's visibility unless the
+  child already has a visibility set
+
+Here is the code in cube's *hooks.py*:
+
+.. sourcecode:: python
+
+    from cubicweb.predicates import is_instance
+    from cubicweb.server import hook
+
+    class SetVisibilityOp(hook.DataOperationMixIn, hook.Operation):
+
+	def precommit_event(self):
+	    for eid in self.get_data():
+		entity = self.session.entity_from_eid(eid)
+		if entity.visibility == 'parent':
+		    entity.cw_set(visibility=u'authenticated')
+
+    class SetVisibilityHook(hook.Hook):
+	__regid__ = 'sytweb.setvisibility'
+	__select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Comment')
+	events = ('after_add_entity',)
+
+	def __call__(self):
+	    SetVisibilityOp.get_instance(self._cw).add_data(self.entity.eid)
+
+    class SetParentVisibilityHook(hook.Hook):
+	__regid__ = 'sytweb.setparentvisibility'
+	__select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
+	events = ('after_add_relation',)
+
+	def __call__(self):
+	    parent = self._cw.entity_from_eid(self.eidto)
+	    child = self._cw.entity_from_eid(self.eidfrom)
+	    if child.visibility == 'parent':
+		child.cw_set(visibility=parent.visibility)
+
+Notice:
+
+* hooks are application objects, hence have selectors that should match entity or
+  relation types to which the hook applies. To match a relation type, we use the
+  hook specific `match_rtype` selector.
+
+* usage of `DataOperationMixIn`: instead of adding an operation for each added entity,
+  DataOperationMixIn allows to create a single one and to store entity's eids to be
+  processed in the transaction data. This is a good pratice to avoid heavy
+  operations manipulation cost when creating a lot of entities in the same
+  transaction.
+
+* the `precommit_event` method of the operation will be called at transaction's
+  commit time.
+
+* in a hook, `self._cw` is the repository session, not a web request as usually
+  in views
+
+* according to hook's event, you have access to different attributes on the hook
+  instance. Here:
+
+  - `self.entity` is the newly added entity on 'after_add_entity' events
+
+  - `self.eidfrom` / `self.eidto` are the eid of the subject / object entity on
+    'after_add_relation' events (you may also get the relation type using
+    `self.rtype`)
+
+The `parent` visibility value is used to tell "propagate using parent security"
+because we want that attribute to be required, so we can't use None value else
+we'll get an error before we get any chance to propagate...
+
+Now, we also want to propagate the `may_be_read_by` relation. Fortunately,
+CubicWeb provides some base hook classes for such things, so we only have to add
+the following code to *hooks.py*:
+
+.. sourcecode:: python
+
+    # relations where the "parent" entity is the subject
+    S_RELS = set()
+    # relations where the "parent" entity is the object
+    O_RELS = set(('filed_under', 'comments',))
+
+    class AddEntitySecurityPropagationHook(hook.PropagateRelationHook):
+	"""propagate permissions when new entity are added"""
+	__regid__ = 'sytweb.addentity_security_propagation'
+	__select__ = (hook.PropagateRelationHook.__select__
+		      & hook.match_rtype_sets(S_RELS, O_RELS))
+	main_rtype = 'may_be_read_by'
+	subject_relations = S_RELS
+	object_relations = O_RELS
+
+    class AddPermissionSecurityPropagationHook(hook.PropagateRelationAddHook):
+	"""propagate permissions when new entity are added"""
+	__regid__ = 'sytweb.addperm_security_propagation'
+	__select__ = (hook.PropagateRelationAddHook.__select__
+		      & hook.match_rtype('may_be_read_by',))
+	subject_relations = S_RELS
+	object_relations = O_RELS
+
+    class DelPermissionSecurityPropagationHook(hook.PropagateRelationDelHook):
+	__regid__ = 'sytweb.delperm_security_propagation'
+	__select__ = (hook.PropagateRelationDelHook.__select__
+		      & hook.match_rtype('may_be_read_by',))
+	subject_relations = S_RELS
+	object_relations = O_RELS
+
+* the `AddEntitySecurityPropagationHook` will propagate the relation
+  when `filed_under` or `comments` relations are added
+
+  - the `S_RELS` and `O_RELS` set as well as the `match_rtype_sets` selector are
+    used here so that if my cube is used by another one, it'll be able to
+    configure security propagation by simply adding relation to one of the two
+    sets.
+
+* the two others will propagate permissions changes on parent entities to
+  children entities
+
+
+.. _adv_tuto_tesing_security:
+
+Step 3: testing our security
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Security is tricky. Writing some tests for it is a very good idea. You should
+even write them first, as Test Driven Development recommends!
+
+Here is a small test case that will check the basis of our security
+model, in *test/unittest_sytweb.py*:
+
+.. sourcecode:: python
+
+    from cubicweb.devtools.testlib import CubicWebTC
+    from cubicweb import Binary
+
+    class SecurityTC(CubicWebTC):
+
+        def test_visibility_propagation(self):
+            with self.admin_access.repo_cnx() as cnx:
+                # create a user for later security checks
+                toto = self.create_user(cnx, 'toto')
+                cnx.commit()
+                # init some data using the default manager connection
+                folder = cnx.create_entity('Folder',
+                                           name=u'restricted',
+                                           visibility=u'restricted')
+                photo1 = cnx.create_entity('File',
+                                           data_name=u'photo1.jpg',
+                                           data=Binary('xxx'),
+                                           filed_under=folder)
+                cnx.commit()
+                # visibility propagation
+                self.assertEquals(photo1.visibility, 'restricted')
+                # unless explicitly specified
+                photo2 = cnx.create_entity('File',
+                                           data_name=u'photo2.jpg',
+                                           data=Binary('xxx'),
+                                           visibility=u'public',
+                                           filed_under=folder)
+                cnx.commit()
+                self.assertEquals(photo2.visibility, 'public')
+            with self.new_access('toto').repo_cnx() as cnx:
+                # test security
+                self.assertEqual(1, len(cnx.execute('File X'))) # only the public one
+                self.assertEqual(0, len(cnx.execute('Folder X'))) # restricted...
+            with self.admin_access.repo_cnx() as cnx:
+                # may_be_read_by propagation
+                folder = cnx.entity_from_eid(folder.eid)
+                folder.cw_set(may_be_read_by=toto)
+                cnx.commit()
+            with self.new_access('toto').repo_cnx() as cnx:
+                photo1 = cnx.entity_from_eid(photo1.eid)
+                self.failUnless(photo1.may_be_read_by)
+                # test security with permissions
+                self.assertEquals(2, len(cnx.execute('File X'))) # now toto has access to photo2
+                self.assertEquals(1, len(cnx.execute('Folder X'))) # and to restricted folder
+
+    if __name__ == '__main__':
+        from logilab.common.testlib import unittest_main
+        unittest_main()
+
+It's not complete, but shows most things you'll want to do in tests: adding some
+content, creating users and connecting as them in the test, etc...
+
+To run it type:
+
+.. sourcecode:: bash
+
+    $ pytest unittest_sytweb.py
+    ========================  unittest_sytweb.py  ========================
+    -> creating tables [....................]
+    -> inserting default user and default groups.
+    -> storing the schema in the database [....................]
+    -> database for instance data initialized.
+    .
+    ----------------------------------------------------------------------
+    Ran 1 test in 22.547s
+
+    OK
+
+
+The first execution is taking time, since it creates a sqlite database for the
+test instance. The second one will be much quicker:
+
+.. sourcecode:: bash
+
+    $ pytest unittest_sytweb.py
+    ========================  unittest_sytweb.py  ========================
+    .
+    ----------------------------------------------------------------------
+    Ran 1 test in 2.662s
+
+    OK
+
+If you do some changes in your schema, you'll have to force regeneration of that
+database. You do that by removing the tmpdb files before running the test: ::
+
+    $ rm data/database/tmpdb*
+
+
+.. Note::
+  pytest is a very convenient utility used to control test execution. It is available from the `logilab-common`_ package.
+
+.. _`logilab-common`: http://www.logilab.org/project/logilab-common
+
+.. _adv_tuto_migration_script:
+
+Step 4: writing the migration script and migrating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Prior to those changes, I created an instance, fed it with some data, so I
+don't want to create a new one, but to migrate the existing one. Let's see how to
+do that.
+
+Migration commands should be put in the cube's *migration* directory, in a
+file named file:`<X.Y.Z>_Any.py` ('Any' being there mostly for historical reasons).
+
+Here I'll create a *migration/0.2.0_Any.py* file containing the following
+instructions:
+
+.. sourcecode:: python
+
+  add_relation_type('may_be_read_by')
+  add_relation_type('visibility')
+  sync_schema_props_perms()
+
+Then I update the version number in the cube's *__pkginfo__.py* to 0.2.0. And
+that's it! Those instructions will:
+
+* update the instance's schema by adding our two new relations and update the
+  underlying database tables accordingly (the first two instructions)
+
+* update schema's permissions definition (the last instruction)
+
+
+To migrate my instance I simply type::
+
+   cubicweb-ctl upgrade sytweb_instance
+
+You'll then be asked some questions to do the migration step by step. You should say
+YES when it asks if a backup of your database should be done, so you can get back
+to initial state if anything goes wrong...
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part03_bfss.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,131 @@
+Storing images on the file-system
+---------------------------------
+
+Step 1: configuring the BytesFileSystem storage
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To avoid cluttering my database, and to ease file manipulation, I don't want them
+to be stored in the database. I want to be able create File entities for some
+files on the server file system, where those file will be accessed to get
+entities data. To do so I've to set a custom :class:`BytesFileSystemStorage`
+storage for the File 'data' attribute, which hold the actual file's content.
+
+Since the function to register a custom storage needs to have a repository
+instance as first argument, we've to call it in a server startup hook. So I added
+in `cubes/sytweb/hooks.py` :
+
+.. sourcecode:: python
+
+    from os import makedirs
+    from os.path import join, exists
+
+    from cubicweb.server import hook
+    from cubicweb.server.sources import storages
+
+    class ServerStartupHook(hook.Hook):
+        __regid__ = 'sytweb.serverstartup'
+        events = ('server_startup', 'server_maintenance')
+
+        def __call__(self):
+            bfssdir = join(self.repo.config.appdatahome, 'bfss')
+            if not exists(bfssdir):
+                makedirs(bfssdir)
+                print 'created', bfssdir
+            storage = storages.BytesFileSystemStorage(bfssdir)
+            storages.set_attribute_storage(self.repo, 'File', 'data', storage)
+
+.. Note::
+
+  * how we built the hook's registry identifier (`__regid__`): you can introduce
+    'namespaces' by using there python module like naming identifiers. This is
+    especially important for hooks where you usually want a new custom hook, not
+    overriding / specializing an existant one, but the concept may be applied to
+    any application objects
+
+  * we catch two events here: "server_startup" and "server_maintenance". The first
+    is called on regular repository startup (eg, as a server), the other for
+    maintenance task such as shell or upgrade. In both cases, we need to have
+    the storage set, else we'll be in trouble...
+
+  * the path given to the storage is the place where file added through the ui
+    (or in the database before migration) will be located
+
+  * beware that by doing this, you can't anymore write queries that will try to
+    restrict on File `data` attribute. Hopefuly we don't do that usually
+    on file's content or more generally on attributes for the Bytes type
+
+Now, if you've already added some photos through the web ui, you'll have to
+migrate existing data so file's content will be stored on the file-system instead
+of the database. There is a migration command to do so, let's run it in the
+cubicweb shell (in real life, you would have to put it in a migration script as we
+have seen last time):
+
+::
+
+   $ cubicweb-ctl shell sytweb_instance
+   entering the migration python shell
+   just type migration commands or arbitrary python code and type ENTER to execute it
+   type "exit" or Ctrl-D to quit the shell and resume operation
+   >>> storage_changed('File', 'data')
+   [........................]
+
+
+That's it. Now, files added through the web ui will have their content stored on
+the file-system, and you'll also be able to import files from the file-system as
+explained in the next part.
+
+Step 2: importing some data into the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Hey, we start to have some nice features, let us give a try to this new web
+site. For instance if I have a 'photos/201005WePyrenees' containing pictures for
+a particular event, I can import it to my web site by typing ::
+
+  $ cubicweb-ctl fsimport -F sytweb_instance photos/201005WePyrenees/
+  ** importing directory /home/syt/photos/201005WePyrenees
+  importing IMG_8314.JPG
+  importing IMG_8274.JPG
+  importing IMG_8286.JPG
+  importing IMG_8308.JPG
+  importing IMG_8304.JPG
+
+.. Note::
+  The -F option means that folders should be mapped, hence my photos will be
+  linked to a Folder entity corresponding to the file-system folder.
+
+Let's take a look at the web ui:
+
+.. image:: ../../images/tutos-photowebsite_ui1.png
+
+Nothing different, I can't see the new folder... But remember our security model!
+By default, files are only accessible to authenticated users, and I'm looking at
+the site as anonymous, e.g. not authenticated. If I login, I can now see:
+
+.. image:: ../../images/tutos-photowebsite_ui2.png
+
+Yeah, it's there! You will notice that I can see some entities as well as
+folders and images the anonymous user can't. It just works **everywhere in the
+ui** since it's handled at the repository level, thanks to our security model.
+
+Now if I click on the recently inserted folder, I can see
+
+.. image:: ../../images/tutos-photowebsite_ui3.png
+
+Great! There is even my pictures in the folder. I can know give to this folder a
+nicer name (provided I don't intend to import from it anymore, else already
+imported photos will be reimported), change permissions, title for some pictures,
+etc... Having a good content is much more difficult than having a good web site
+;)
+
+
+Conclusion
+~~~~~~~~~~
+
+We started to see here an advanced feature of our repository: the ability
+to store some parts of our data-model into a custom storage, outside the
+database. There is currently only the :class:`BytesFileSystemStorage` available,
+but you can expect to see more coming in a near future (or write your own!).
+
+Also, we can know start to feed our web-site with some nice pictures!
+The site isn't perfect (far from it actually) but it's usable, and we can
+now start using it and improve it on the way. The Incremental Cubic Way :)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part04_ui-base.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,361 @@
+Let's make it more user friendly
+================================
+
+
+Step 1: let's improve site's usability for our visitors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first thing I've noticed is that people to whom I send links to photos with
+some login/password authentication get lost, because they don't grasp they have
+to login by clicking on the 'authenticate' link. That's much probably because
+they only get a 404 when trying to access an unauthorized folder, and the site
+doesn't make clear that 1. you're not authenticated, 2. you could get more
+content by authenticating yourself.
+
+So, to improve this situation, I decided that I should:
+
+* make a login box appears for anonymous, so they see at a first glance a place
+  to put the login / password information I provided
+
+* customize the 404 page, proposing to login to anonymous.
+
+Here is the code, samples from my cube's `views.py` file:
+
+.. sourcecode:: python
+
+    from cubicweb.predicates import is_instance
+    from cubicweb.web import component
+    from cubicweb.web.views import error
+    from cubicweb.predicates import anonymous_user
+
+    class FourOhFour(error.FourOhFour):
+	__select__ = error.FourOhFour.__select__ & anonymous_user()
+
+	def call(self):
+	    self.w(u"<h1>%s</h1>" % self._cw._('this resource does not exist'))
+	    self.w(u"<p>%s</p>" % self._cw._('have you tried to login?'))
+
+
+    class LoginBox(component.CtxComponent):
+	"""display a box containing links to all startup views"""
+	__regid__ = 'sytweb.loginbox'
+	__select__ = component.CtxComponent.__select__ & anonymous_user()
+
+	title = _('Authenticate yourself')
+	order = 70
+
+	def render_body(self, w):
+	    cw = self._cw
+	    form = cw.vreg['forms'].select('logform', cw)
+	    form.render(w=w, table_class='', display_progress_div=False)
+
+The first class provides a new specific implementation of the default page you
+get on 404 error, to display an adapted message to anonymous user.
+
+.. Note::
+
+  Thanks to the selection mecanism, it will be selected for anoymous user,
+  since the additional `anonymous_user()` selector gives it a higher score than
+  the default, and not for authenticated since this selector will return 0 in
+  such case (hence the object won't be selectable)
+
+The second class defines a simple box, that will be displayed by default with
+boxes in the left column, thanks to default :class:`component.CtxComponent`
+selector. The HTML is written to match default CubicWeb boxes style. The code
+fetch the actual login form and render it.
+
+
+.. figure:: ../../images/tutos-photowebsite_login-box.png
+   :alt: login box / 404 screenshot
+
+   The login box and the custom 404 page for an anonymous visitor (translated in french)
+
+
+Step 2: providing a custom index page
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Another thing we can easily do to improve the site is... A nicer index page
+(e.g. the first page you get when accessing the web site)! The default one is
+quite intimidating (that should change in a near future). I will provide a much
+simpler index page that simply list available folders (e.g. photo albums in that
+site).
+
+.. sourcecode:: python
+
+    from cubicweb.web.views import startup
+
+    class IndexView(startup.IndexView):
+	def call(self, **kwargs):
+	    self.w(u'<div>\n')
+	    if self._cw.cnx.anonymous_connection:
+		self.w(u'<h4>%s</h4>\n' % self._cw._('Public Albums'))
+	    else:
+		self.w(u'<h4>%s</h4>\n' % self._cw._('Albums for %s') % self._cw.user.login)
+	    self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
+	    self.w(u'</div>\n')
+
+    def registration_callback(vreg):
+	vreg.register_all(globals().values(), __name__, (IndexView,))
+	vreg.register_and_replace(IndexView, startup.IndexView)
+
+As you can see, we override the default index view found in
+`cubicweb.web.views.startup`, geting back nothing but its identifier and selector
+since we override the top level view's `call` method.
+
+.. Note::
+
+  in that case, we want our index view to **replace** the existing one. To do so
+  we've to implements the `registration_callback` function, in which we tell to
+  register everything in the module *but* our IndexView, then we register it
+  instead of the former index view.
+
+Also, we added a title that tries to make it more evident that the visitor is
+authenticated, or not. Hopefuly people will get it now!
+
+
+.. figure:: ../../images/tutos-photowebsite_index-before.png
+   :alt: default index page screenshot
+
+   The default index page
+
+.. figure:: ../../images/tutos-photowebsite_index-after.png
+   :alt: new index page screenshot
+
+   Our simpler, less intimidating, index page (still translated in french)
+
+
+Step 3: more navigation improvments
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are still a few problems I want to solve...
+
+* Images in a folder are displayed in a somewhat random order. I would like to
+  have them ordered by file's name (which will usually, inside a given folder,
+  also result ordering photo by their date and time)
+
+* When clicking a photo from an album view, you've to get back to the gallery
+  view to go to the next photo. This is pretty annoying...
+
+* Also, when viewing an image, there is no clue about the folder to which this
+  image belongs to.
+
+I will first try to explain the ordering problem. By default, when accessing
+related entities by using the ORM's API, you should get them ordered according to
+the target's class `cw_fetch_order`. If we take a look at the file cube'schema,
+we can see:
+
+.. sourcecode:: python
+
+    class File(AnyEntity):
+	"""customized class for File entities"""
+	__regid__ = 'File'
+	fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title'])
+
+
+By default, `fetch_config` will return a `cw_fetch_order` method that will order
+on the first attribute in the list. So, we could expect to get files ordered by
+their name. But we don't.  What's up doc ?
+
+The problem is that files are related to folder using the `filed_under` relation.
+And that relation is ambiguous, eg it can lead to `File` entities, but also to
+`Folder` entities. In such case, since both entity types doesn't share the
+attribute on which we want to sort, we'll get linked entities sorted on a common
+attribute (usually `modification_date`).
+
+To fix this, we've to help the ORM. We'll do this in the method from the `ITree`
+folder's adapter, used in the folder's primary view to display the folder's
+content. Here's the code, that I've put in our cube's `entities.py` file, since
+it's more logical stuff than view stuff:
+
+.. sourcecode:: python
+
+    from cubes.folder import entities as folder
+
+    class FolderITreeAdapter(folder.FolderITreeAdapter):
+
+	def different_type_children(self, entities=True):
+	    rql = self.entity.cw_related_rql(self.tree_relation,
+					     self.parent_role, ('File',))
+	    rset = self._cw.execute(rql, {'x': self.entity.eid})
+	    if entities:
+		return list(rset.entities())
+	    return rset
+
+    def registration_callback(vreg):
+	vreg.register_and_replace(FolderITreeAdapter, folder.FolderITreeAdapter)
+
+As you can see, we simple inherit from the adapter defined in the `folder` cube,
+then we override the `different_type_children` method to give a clue to the ORM's
+`cw_related_rql` method, that is responsible to generate the rql to get entities
+related to the folder by the `filed_under` relation (the value of the
+`tree_relation` attribute).  The clue is that we only want to consider the `File`
+target entity type. By doing this, we remove the ambiguity and get back a RQL
+query that correctly order files by their `data_name` attribute.
+
+
+.. Note::
+
+    * As seen earlier, we want to **replace** the folder's `ITree` adapter by our
+      implementation, hence the custom `registration_callback` method.
+
+
+Ouf. That one was tricky...
+
+Now the easier parts. Let's start by adding some links on the file's primary view
+to see the previous / next image in the same folder. CubicWeb's provide a
+component that do exactly that. To make it appears, one have to be adaptable to
+the `IPrevNext` interface. Here is the related code sample, extracted from our
+cube's `views.py` file:
+
+.. sourcecode:: python
+
+    from cubicweb.predicates import is_instance
+    from cubicweb.web.views import navigation
+
+
+    class FileIPrevNextAdapter(navigation.IPrevNextAdapter):
+	__select__ = is_instance('File')
+
+	def previous_entity(self):
+	    rset = self._cw.execute('File F ORDERBY FDN DESC LIMIT 1 WHERE '
+				    'X filed_under FOLDER, F filed_under FOLDER, '
+				    'F data_name FDN, X data_name > FDN, X eid %(x)s',
+				    {'x': self.entity.eid})
+	    if rset:
+		return rset.get_entity(0, 0)
+
+	def next_entity(self):
+	    rset = self._cw.execute('File F ORDERBY FDN ASC LIMIT 1 WHERE '
+				    'X filed_under FOLDER, F filed_under FOLDER, '
+				    'F data_name FDN, X data_name < FDN, X eid %(x)s',
+				    {'x': self.entity.eid})
+	    if rset:
+		return rset.get_entity(0, 0)
+
+
+The `IPrevNext` interface implemented by the adapter simply consist in the
+`previous_entity` / `next_entity` methods, that should respectivly return the
+previous / next entity or `None`. We make an RQL query to get files in the same
+folder, ordered similarly (eg by their `data_name` attribute). We set
+ascendant/descendant ordering and a strict comparison with current file's name
+(the "X" variable representing the current file).
+
+Notice that this query supposes we wont have two files of the same name in the
+same folder, else things may go wrong. Fixing this is out of the scope of this
+blog. And as I would like to have at some point a smarter, context sensitive
+previous/next entity, I'll probably never fix this query (though if I had to, I
+would probably choosing to add a constraint in the schema so that we can't add
+two files of the same name in a folder).
+
+One more thing: by default, the component will be displayed below the content
+zone (the one with the white background). You can change this in the site's
+properties through the ui, but you can also change the default value in the code
+by modifying the `context` attribute of the component:
+
+.. sourcecode:: python
+
+    navigation.NextPrevNavigationComponent.context = 'navcontentbottom'
+
+.. Note::
+
+   `context` may be one of 'navtop', 'navbottom', 'navcontenttop' or
+   'navcontentbottom'; the first two being outside the main content zone, the two
+   others inside it.
+
+.. figure:: ../../images/tutos-photowebsite_prevnext.png
+   :alt: screenshot of the previous/next entity component
+
+   The previous/next entity component, at the bottom of the main content zone.
+
+Now, the only remaining stuff in my todo list is to see the file's folder. I'll use
+the standard breadcrumb component to do so. Similarly as what we've seen before, this
+component is controled by the :class:`IBreadCrumbs` interface, so we'll have to provide a custom
+adapter for `File` entity, telling the a file's parent entity is its folder:
+
+.. sourcecode:: python
+
+    from cubicweb.web.views import ibreadcrumbs
+
+    class FileIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+	__select__ = is_instance('File')
+
+	def parent_entity(self):
+	    if self.entity.filed_under:
+		return self.entity.filed_under[0]
+
+In that case, we simply use attribute notation provided by the ORM to get the
+folder in which the current file (e.g. `self.entity`) is located.
+
+.. Note::
+
+   The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
+   :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
+   at the value returned by its `parent_entity` method. It also provides a
+   default implementation for this method for entities adapting to the `ITree`
+   interface, but as our `File` doesn't, we've to provide a custom adapter.
+
+.. figure:: ../../images/tutos-photowebsite_breadcrumbs.png
+   :alt: screenshot of the breadcrumb component
+
+   The breadcrumb component when on a file entity, now displaying parent folder.
+
+
+Step 4: preparing the release and migrating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Now that greatly enhanced our cube, it's time to release it to upgrade production site.
+I'll probably detail that process later, but I currently simply transfer the new code
+to the server running the web site.
+
+However, I've still today some step to respect to get things done properly...
+
+First, as I've added some translatable string, I've to run: ::
+
+  $ cubicweb-ctl i18ncube sytweb
+
+To update the cube's gettext catalogs (the '.po' files under the cube's `i18n`
+directory). Once the above command is executed, I'll then update translations.
+
+To see if everything is ok on my test instance, I do: ::
+
+  $ cubicweb-ctl i18ninstance sytweb
+  $ cubicweb-ctl start -D sytweb
+
+The first command compile i18n catalogs (e.g. generates '.mo' files) for my test
+instance. The second command start it in debug mode, so I can open my browser and
+navigate through the web site to see if everything is ok...
+
+.. Note::
+
+   In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
+   in the two other, it refers to the **instance** (if you can't see the
+   difference, reread CubicWeb's concept chapter !).
+
+
+Once I've checked it's ok, I simply have to bump the version number in the
+`__pkginfo__` module to trigger a migration once I'll have updated the code on
+the production site. I can check then check the migration is also going fine, by
+first restoring a dump from the production site, then upgrading my test instance.
+
+To generate a dump from the production site: ::
+
+  $ cubicweb-ctl db-dump sytweb
+  pg_dump -Fc --username=syt --no-owner --file /home/syt/etc/cubicweb.d/sytweb/backup/tmpYIN0YI/system sytweb
+  -> backup file /home/syt/etc/cubicweb.d/sytweb/backup/sytweb-2010-07-13_10-22-40.tar.gz
+
+I can now get back the dump file ('sytweb-2010-07-13_10-22-40.tar.gz') to my test
+machine (using `scp` for instance) to restore it and start migration: ::
+
+  $ cubicweb-ctl db-restore sytweb sytweb-2010-07-13_10-22-40.tar.gz
+  $ cubicweb-ctl upgrade sytweb
+
+You'll have to answer some questions, as we've seen in `an earlier post`_.
+
+Now that everything is tested, I can transfer the new code to the production
+server, `apt-get upgrade` cubicweb and its dependencies, and eventually
+upgrade the production instance.
+
+
+.. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
+.. _`3.8`: http://www.cubicweb.org/blogentry/917107
+.. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
+.. _`an earlier post`: http://www.cubicweb.org/867464
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part05_ui-advanced.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,374 @@
+Building my photos web site with |cubicweb| part V: let's make it even more user friendly
+=========================================================================================
+
+.. _uiprops:
+
+Step 1: tired of the default look?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+OK... Now our site has its most desired features. But... I would like to make it look
+somewhat like *my* website. It is not www.cubicweb.org after all. Let's tackle this
+first!
+
+The first thing we can to is to change the logo. There are various way to achieve
+this. The easiest way is to put a :file:`logo.png` file into the cube's :file:`data`
+directory. As data files are looked at according to cubes order (CubicWeb
+resources coming last), that file will be selected instead of CubicWeb's one.
+
+.. Note::
+   As the location for static resources are cached, you'll have to restart
+   your instance for this to be taken into account.
+
+Though there are some cases where you don't want to use a :file:`logo.png` file.
+For instance if it's a JPEG file. You can still change the logo by defining in
+the cube's :file:`uiprops.py` file:
+
+.. sourcecode:: python
+
+   LOGO = data('logo.jpg')
+
+The uiprops machinery is used to define some static file resources,
+such as the logo, default Javascript / CSS files, as well as CSS
+properties (we'll see that later).
+
+.. Note::
+   This file is imported specifically by |cubicweb|, with a predefined name space,
+   containing for instance the `data` function, telling the file is somewhere
+   in a cube or CubicWeb's data directory.
+
+   One side effect of this is that it can't be imported as a regular python
+   module.
+
+The nice thing is that in debug mode, change to a :file:`uiprops.py` file are detected
+and then automatically reloaded.
+
+Now, as it's a photos web-site, I would like to have a photo of mine as background...
+After some trials I won't detail here, I've found a working recipe explained `here`_.
+All I've to do is to override some stuff of the default CubicWeb user interface to
+apply it as explained.
+
+The first thing to to get the ``<img/>`` tag as first element after the
+``<body>`` tag.  If you know a way to avoid this by simply specifying the image
+in the CSS, tell me!  The easiest way to do so is to override the
+:class:`HTMLPageHeader` view, since that's the one that is directly called once
+the ``<body>`` has been written. How did I find this?  By looking in the
+:mod:`cubiweb.web.views.basetemplates` module, since I know that global page
+layouts sits there. I could also have grep the "body" tag in
+:mod:`cubicweb.web.views`... Finding this was the hardest part. Now all I need is
+to customize it to write that ``img`` tag, as below:
+
+.. sourcecode:: python
+
+    class HTMLPageHeader(basetemplates.HTMLPageHeader):
+	# override this since it's the easier way to have our bg image
+	# as the first element following <body>
+	def call(self, **kwargs):
+            self.w(u'<img id="bg-image" src="%sbackground.jpg" alt="background image"/>'
+                   % self._cw.datadir_url)
+	    super(HTMLPageHeader, self).call(**kwargs)
+
+
+    def registration_callback(vreg):
+	vreg.register_all(globals().values(), __name__, (HTMLPageHeader))
+	vreg.register_and_replace(HTMLPageHeader, basetemplates.HTMLPageHeader)
+
+
+As you may have guessed, my background image is in a :file:`background.jpg` file
+in the cube's :file:`data` directory, but there are still some things to explain
+to newcomers here:
+
+* The :meth:`call` method is there the main access point of the view. It's called by
+  the view's :meth:`render` method. It is not the only access point for a view, but
+  this will be detailed later.
+
+* Calling `self.w` writes something to the output stream. Except for binary views
+  (which do not generate text), it *must* be passed an Unicode string.
+
+* The proper way to get a file in :file:`data` directory is to use the `datadir_url`
+  attribute of the incoming request (e.g. `self._cw`).
+
+I won't explain again the :func:`registration_callback` stuff, you should understand it
+now!  If not, go back to previous posts in the series :)
+
+Fine. Now all I've to do is to add a bit of CSS to get it to behave nicely (which
+is not the case at all for now). I'll put all this in a :file:`cubes.sytweb.css`
+file, stored as usual in our :file:`data` directory:
+
+.. sourcecode:: css
+
+
+    /* fixed full screen background image
+     * as explained on http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
+     *
+     * syt update: set z-index=0 on the img instead of z-index=1 on div#page & co to
+     * avoid pb with the user actions menu
+     */
+    img#bg-image {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 0;
+    }
+
+    div#page, table#header, div#footer {
+	background: transparent;
+	position: relative;
+    }
+
+    /* add some space around the logo
+     */
+    img#logo {
+	padding: 5px 15px 0px 15px;
+    }
+
+    /* more dark font for metadata to have a chance to see them with the background
+     *  image
+     */
+    div.metadata {
+	color: black;
+    }
+
+You can see here stuff explained in the cited page, with only a slight modification
+explained in the comments, plus some additional rules to make things somewhat cleaner:
+
+* a bit of padding around the logo
+
+* darker metadata which appears by default below the content (the white frame in the page)
+
+To get this CSS file used everywhere in the site, I have to modify the :file:`uiprops.py` file
+introduced above:
+
+.. sourcecode:: python
+
+   STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.sytweb.css')]
+
+.. Note::
+   `sheet` is another predefined variable containing values defined by
+   already process `:file:`uiprops.py`` file, notably the CubicWeb's one.
+
+Here we simply want our CSS in addition to CubicWeb's base CSS files, so we
+redefine the `STYLESHEETS` variable to existing CSS (accessed through the `sheet`
+variable) with our one added. I could also have done:
+
+.. sourcecode:: python
+
+   sheet['STYLESHEETS'].append(data('cubes.sytweb.css'))
+
+But this is less interesting since we don't see the overriding mechanism...
+
+At this point, the site should start looking good, the background image being
+resized to fit the screen.
+
+.. image:: ../../images/tutos-photowebsite_background-image.png
+
+The final touch: let's customize CubicWeb's CSS to get less orange... By simply adding
+
+.. sourcecode:: python
+
+  contextualBoxTitleBg = incontextBoxTitleBg = '#AAAAAA'
+
+and reloading the page we've just seen, we know have a nice greyed box instead of
+the orange one:
+
+.. image:: ../../images/tutos-photowebsite_grey-box.png
+
+This is because CubicWeb's CSS include some variables which are
+expanded by values defined in uiprops file. In our case we controlled the
+properties of the CSS `background` property of boxes with CSS class
+`contextualBoxTitleBg` and `incontextBoxTitleBg`.
+
+
+Step 2: configuring boxes
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Boxes present to the user some ways to use the application. Let's first do a few
+user interface tweaks in our :file:`views.py` file:
+
+.. sourcecode:: python
+
+  from cubicweb.predicates import none_rset
+  from cubicweb.web.views import bookmark
+  from cubes.zone import views as zone
+  from cubes.tag import views as tag
+
+  # change bookmarks box selector so it's only displayed on startup views
+  bookmark.BookmarksBox.__select__ = bookmark.BookmarksBox.__select__ & none_rset()
+  # move zone box to the left instead of in the context frame and tweak its order
+  zone.ZoneBox.context = 'left'
+  zone.ZoneBox.order = 100
+  # move tags box to the left instead of in the context frame and tweak its order
+  tag.TagsBox.context = 'left'
+  tag.TagsBox.order = 102
+  # hide similarity box, not interested
+  tag.SimilarityBox.visible = False
+
+The idea is to move all boxes in the left column, so we get more space for the
+photos.  Now, serious things: I want a box similar to the tags box but to handle
+the `Person displayed_on File` relation. We can do this simply by adding a
+:class:`AjaxEditRelationCtxComponent` subclass to our views, as below:
+
+.. sourcecode:: python
+
+    from logilab.common.decorators import monkeypatch
+    from cubicweb import ValidationError
+    from cubicweb.web.views import uicfg, component
+    from cubicweb.web.views import basecontrollers
+
+    # hide displayed_on relation using uicfg since it will be displayed by the box below
+    uicfg.primaryview_section.tag_object_of(('*', 'displayed_on', '*'), 'hidden')
+
+    class PersonBox(component.AjaxEditRelationCtxComponent):
+	__regid__ = 'sytweb.displayed-on-box'
+	# box position
+	order = 101
+	context = 'left'
+	# define relation to be handled
+	rtype = 'displayed_on'
+	role = 'object'
+	target_etype = 'Person'
+	# messages
+	added_msg = _('person has been added')
+	removed_msg = _('person has been removed')
+	# bind to js_* methods of the json controller
+	fname_vocabulary = 'unrelated_persons'
+	fname_validate = 'link_to_person'
+	fname_remove = 'unlink_person'
+
+
+    @monkeypatch(basecontrollers.JSonController)
+    @basecontrollers.jsonize
+    def js_unrelated_persons(self, eid):
+	"""return tag unrelated to an entity"""
+	rql = "Any F + ' ' + S WHERE P surname S, P firstname F, X eid %(x)s, NOT P displayed_on X"
+	return [name for (name,) in self._cw.execute(rql, {'x' : eid})]
+
+
+    @monkeypatch(basecontrollers.JSonController)
+    def js_link_to_person(self, eid, people):
+	req = self._cw
+	for name in people:
+	    name = name.strip().title()
+	    if not name:
+		continue
+	    try:
+		firstname, surname = name.split(None, 1)
+	    except:
+		raise ValidationError(eid, {('displayed_on', 'object'): 'provide <first name> <surname>'})
+	    rset = req.execute('Person P WHERE '
+			       'P firstname %(firstname)s, P surname %(surname)s',
+			       locals())
+	    if rset:
+		person = rset.get_entity(0, 0)
+	    else:
+		person = req.create_entity('Person', firstname=firstname,
+						surname=surname)
+	    req.execute('SET P displayed_on X WHERE '
+			'P eid %(p)s, X eid %(x)s, NOT P displayed_on X',
+			{'p': person.eid, 'x' : eid})
+
+    @monkeypatch(basecontrollers.JSonController)
+    def js_unlink_person(self, eid, personeid):
+	self._cw.execute('DELETE P displayed_on X WHERE P eid %(p)s, X eid %(x)s',
+			 {'p': personeid, 'x': eid})
+
+
+You basically subclass to configure with some class attributes. The `fname_*`
+attributes give the name of methods that should be defined on the json control to
+make the AJAX part of the widget work: one to get the vocabulary, one to add a
+relation and another to delete a relation. These methods must start by a `js_`
+prefix and are added to the controller using the `@monkeypatch` decorator. In my
+case, the most complicated method is the one which adds a relation, since it
+tries to see if the person already exists, and else automatically create it,
+assuming the user entered "firstname surname".
+
+Let's see how it looks like on a file primary view:
+
+.. image:: ../../images/tutos-photowebsite_boxes.png
+
+Great, it's now as easy for me to link my pictures to people than to tag them.
+Also, visitors get a consistent display of these two pieces of information.
+
+.. Note::
+  The ui component system has been refactored in `CubicWeb 3.10`_, which also
+  introduced the :class:`AjaxEditRelationCtxComponent` class.
+
+
+Step 3: configuring facets
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The last feature we'll add today is facet configuration. If you access to the
+'/file' url, you'll see a set of 'facets' appearing in the left column. Facets
+provide an intuitive way to build a query incrementally, by proposing to the user
+various way to restrict the result set. For instance CubicWeb proposes a facet to
+restrict based on who created an entity; the tag cube proposes a facet to
+restrict based on tags; the zoe cube a facet to restrict based on geographical
+location, and so on. In that gist, I want to propose a facet to restrict based on
+the people displayed on the picture. To do so, there are various classes in the
+:mod:`cubicweb.web.facet` module which simply have to be configured using class
+attributes as we've done for the box. In our case, we'll define a subclass of
+:class:`RelationFacet`.
+
+.. Note::
+
+   Since that's ui stuff, we'll continue to add code below to our
+   :file:`views.py` file. Though we begin to have a lot of various code their, so
+   it's may be a good time to split our views module into submodules of a `view`
+   package. In our case of a simple application (glue) cube, we could start using
+   for instance the layout below: ::
+
+     views/__init__.py   # uicfg configuration, facets
+     views/layout.py     # header/footer/background stuff
+     views/components.py # boxes, adapters
+     views/pages.py      # index view, 404 view
+
+.. sourcecode:: python
+
+    from cubicweb.web import facet
+
+    class DisplayedOnFacet(facet.RelationFacet):
+	__regid__ = 'displayed_on-facet'
+	# relation to be displayed
+	rtype = 'displayed_on'
+	role = 'object'
+	# view to use to display persons
+	label_vid = 'combobox'
+
+Let's say we also want to filter according to the `visibility` attribute. This is
+even simpler as we just have to derive from the :class:`AttributeFacet` class:
+
+.. sourcecode:: python
+
+    class VisibilityFacet(facet.AttributeFacet):
+	__regid__ = 'visibility-facet'
+	rtype = 'visibility'
+
+Now if I search for some pictures on my site, I get the following facets available:
+
+.. image:: ../../images/tutos-photowebsite_facets.png
+
+.. Note::
+
+  By default a facet must be applyable to every entity in the result set and
+  provide at leat two elements of vocabulary to be displayed (for instance you
+  won't see the `created_by` facet if the same user has created all
+  entities). This may explain why you don't see yours...
+
+
+Conclusion
+~~~~~~~~~~
+
+We started to see the power behind the infrastructure provided by the
+framework, both on the pure ui (CSS, Javascript) side and on the Python side
+(high level generic classes for components, including boxes and facets). We now
+have, with a few lines of code, a full-featured web site with a personalized look.
+
+Of course we'll probably want more as time goes, but we can now
+concentrate on making good pictures, publishing albums and sharing them with
+friends...
+
+
+
+.. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518
+.. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/blog-in-five-minutes.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,70 @@
+.. -*- coding: utf-8 -*-
+
+.. _TutosBaseBlogFiveMinutes:
+
+Get a blog running in five minutes!
+-----------------------------------
+
+For Debian or Ubuntu users, first install the following packages
+(:ref:`DebianInstallation`)::
+
+    cubicweb, cubicweb-dev, cubicweb-blog
+
+Windows or Mac OS X users must install |cubicweb| from source (see
+:ref:`SourceInstallation` and :ref:`WindowsInstallation`).
+
+Then create and initialize your instance::
+
+    cubicweb-ctl create blog myblog
+
+You'll be asked a few questions, and you can keep the default answer for most of
+them. The one question you'll have to think about is the database you'll want to
+use for that instance. For a quick test, if you don't have `postgresql` installed
+and configured (see :ref:`PostgresqlConfiguration`), it's highly recommended to
+choose `sqlite` when asked for which database driver to use, since it has a much
+simple setup (no database server needed).
+
+One the process is completed (including database initialisation), you can start
+your instance by using: ::
+
+    cubicweb-ctl start -D myblog
+
+The `-D` option activates the debugging mode. Removing it will launch the instance
+as a daemon in the background, and ``cubicweb-ctl stop myblog`` will stop
+it in that case. 
+
+
+About file system permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Unless you installed from sources, the above commands assume that you have root
+access to the :file:`/etc/` directory. In order to initialize your instance as a
+regular user, within your home directory, you can use the :envvar:`CW_MODE`
+environment variable: ::
+
+  export CW_MODE=user
+
+then create a :file:`~/etc/cubicweb.d` directory that will hold your instances.
+
+More information about how to configure your own environment is
+available in :ref:`ResourceMode`.
+
+
+Instance parameters
+~~~~~~~~~~~~~~~~~~~
+
+If you would like to change database parameters such as the database host or the
+user name used to connect to the database, edit the `sources` file located in the
+:file:`/etc/cubicweb.d/myblog` directory.
+
+Then relaunch the database creation::
+
+     cubicweb-ctl db-create myblog
+
+Other parameters, like web server or emails parameters, can be modified in the
+:file:`/etc/cubicweb.d/myblog/all-in-one.conf` file.
+
+You'll have to restart the instance after modification in one of those files.
+
+This is it. Your blog is functional and running. Visit http://localhost:8080 and enjoy it!
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/conclusion.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,18 @@
+.. -*- coding: utf-8 -*-
+
+What's next?
+------------
+
+In this tutorial, we have seen that you can, right after the installation of
+|cubicweb|, build a web application in a few minutes by defining a data model as
+assembling cubes. You get a working application that you can then customize there
+and there while keeping something that works. This is important in agile
+development practices, you can right from the start of the project show things
+to customer and so take the right decision early in the process.
+
+The next steps will be to discover hooks, security, data sources, digging deeper
+into view writing and interface customisation... Yet a lot of fun stuff to
+discover! You will find more `tutorials and howtos`_ in the blog published on the
+CubicWeb.org website.
+
+.. _`tutorials and howtos`: http://www.cubicweb.org/view?rql=Any+X+ORDERBY+D+DESC+WHERE+X+is+BlogEntry%2C+T+tags+X%2C+T+name+IN+%28%22tutorial%22%2C+%22howto%22%29%2C+X+creation_date+D
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/customizing-the-application.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,539 @@
+.. -*- coding: utf-8 -*-
+
+.. _TutosBaseCustomizingTheApplication:
+
+Customizing your application
+----------------------------
+
+So far so good. The point is that usually, you won't get enough by assembling
+cubes out-of-the-box. You will want to customize them, have a personal look and
+feel, add your own data model and so on. Or maybe start from scratch?
+
+So let's get a bit deeper and start coding our own cube. In our case, we want
+to customize the blog we created to add more features to it.
+
+
+Create your own cube
+~~~~~~~~~~~~~~~~~~~~
+
+First, notice that if you've installed |cubicweb| using Debian packages, you will
+need the additional ``cubicweb-dev`` package to get the commands necessary to
+|cubicweb| development. All `cubicweb-ctl` commands are described in details in
+:ref:`cubicweb-ctl`.
+
+Once your |cubicweb| development environment is set up, you can create a new
+cube::
+
+  cubicweb-ctl newcube myblog
+
+This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source
+installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation)
+a directory named :file:`blog` reflecting the structure described in
+:ref:`cubelayout`.
+
+For packages installation, you can still create new cubes in your home directory
+using the following configuration. Let's say you want to develop your new cubes
+in `~src/cubes`, then set the following environment variables: ::
+
+  CW_CUBES_PATH=~/src/cubes
+
+and then create your new cube using: ::
+
+  cubicweb-ctl newcube --directory=~/src/cubes myblog
+
+.. Note::
+
+   We previously used `myblog` as the name of our *instance*. We're now creating
+   a *cube* with the same name. Both are different things. We'll now try to
+   specify when we talk about one or another, but keep in mind this difference.
+
+
+Cube metadata
+~~~~~~~~~~~~~
+
+A simple set of metadata about your cube are stored in the :file:`__pkginfo__.py`
+file. In our case, we want to extend the blog cube, so we have to tell that our
+cube depends on this cube, by modifying the ``__depends__`` dictionary in that
+file:
+
+.. sourcecode:: python
+
+   __depends__ =  {'cubicweb': '>= 3.10.7',
+                   'cubicweb-blog': None}
+
+where the ``None`` means we do not depends on a particular version of the cube.
+
+.. _TutosBaseCustomizingTheApplicationDataModel:
+
+Extending the data model
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The data model or schema is the core of your |cubicweb| application.  It defines
+the type of content your application will handle. It is defined in the file
+:file:`schema.py` of the cube.
+
+
+Defining our model
+******************
+
+For the sake of example, let's say we want a new entity type named `Community`
+with a name, a description. A `Community` will hold several blogs.
+
+.. sourcecode:: python
+
+  from yams.buildobjs import EntityType, RelationDefinition, String, RichString
+
+  class Community(EntityType):
+      name = String(maxsize=50, required=True)
+      description = RichString()
+
+  class community_blog(RelationDefinition):
+      subject = 'Community'
+      object = 'Blog'
+      cardinality = '*?'
+      composite = 'subject'
+
+The first step is the import from the :mod:`yams` package necessary classes to build
+the schema.
+
+This file defines the following:
+
+* a `Community` has a title and a description as attributes
+
+  - the name is a string that is required and can't be longer than 50 characters
+
+  - the description is a string that is not constrained and may contains rich
+    content such as HTML or Restructured text.
+
+* a `Community` may be linked to a `Blog` using the `community_blog` relation
+
+  - ``*`` means a community may be linked to 0 to N blog, ``?`` means a blog may
+    be linked to 0 to 1 community. For completeness, remember that you can also
+    use ``+`` for 1 to N, and ``1`` for single, mandatory relation (e.g. one to one);
+
+  - this is a composite relation where `Community` (e.g. the subject of the
+    relation) is the composite. That means that if you delete a community, its
+    blog will be deleted as well.
+
+Of course, there are a lot of other data types and things such as constraints,
+permissions, etc, that may be defined in the schema, but those won't be covered
+in this tutorial.
+
+Notice that our schema refers to the `Blog` entity type which is not defined
+here.  But we know this type is available since we depend on the `blog` cube
+which is defining it.
+
+
+Applying changes to the model into our instance
+***********************************************
+
+Now the problem is that we created an instance using the `blog` cube, not our
+`myblog` cube, so if we don't do anything there is no way that we'll see anything
+changing in the instance.
+
+One easy way, as we've no really valuable data in the instance would be to trash and recreated it::
+
+  cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
+  cubicweb-ctl delete myblog
+  cubicweb-ctl create myblog
+  cubicweb-ctl start -D myblog
+
+Another way is to add our cube to the instance using the cubicweb-ctl shell
+facility. It's a python shell connected to the instance with some special
+commands available to manipulate it (the same as you'll have in migration
+scripts, which are not covered in this tutorial). In that case, we're interested
+in the `add_cube` command: ::
+
+  $ cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
+  $ cubicweb-ctl shell myblog
+  entering the migration python shell
+  just type migration commands or arbitrary python code and type ENTER to execute it
+  type "exit" or Ctrl-D to quit the shell and resume operation
+  >>> add_cube('myblog')
+  >>>
+  $ cubicweb-ctl start -D myblog
+
+The `add_cube` command is enough since it automatically updates our
+application to the cube's schema. There are plenty of other migration
+commands of a more finer grain. They are described in :ref:`migration`
+
+As explained, leave the shell by typing Ctrl-D. If you restart the instance and
+take another look at the schema, you'll see that changes to the data model have
+actually been applied (meaning database schema updates and all necessary stuff
+has been done).
+
+.. image:: ../../images/tutos-base_myblog-schema_en.png
+   :alt: the instance schema after adding our cube
+
+If you follow the 'info' link in the user pop-up menu, you'll also see that the
+instance is using blog and myblog cubes.
+
+.. image:: ../../images/tutos-base_myblog-siteinfo_en.png
+   :alt: the instance schema after adding our cube
+
+You can now add some communities, link them to blog, etc... You'll see that the
+framework provides default views for this entity type (we have not yet defined any
+view for it!), and also that the blog primary view will show the community it's
+linked to if any. All this thanks to the model driven interface provided by the
+framework.
+
+You'll then be able to redefine each of them according to your needs
+and preferences. We'll now see how to do such thing.
+
+.. _TutosBaseCustomizingTheApplicationCustomViews:
+
+Defining your views
+~~~~~~~~~~~~~~~~~~~
+
+|cubicweb| provides a lot of standard views in directory
+:file:`cubicweb/web/views/`. We already talked about 'primary' and 'list' views,
+which are views which apply to one ore more entities.
+
+A view is defined by a python class which includes:
+
+  - an identifier: all objects used to build the user interface in |cubicweb| are
+    recorded in a registry and this identifier will be used as a key in that
+    registry. There may be multiple views for the same identifier.
+
+  - a *selector*, which is a kind of filter telling how well a view suit to a
+    particular context. When looking for a particular view (e.g. given an
+    identifier), |cubicweb| computes for each available view with that identifier
+    a score which is returned by the selector. Then the view with the highest
+    score is used. The standard library of predicates is in
+    :mod:`cubicweb.predicates`.
+
+A view has a set of methods inherited from the :class:`cubicweb.view.View` class,
+though you usually don't derive directly from this class but from one of its more
+specific child class.
+
+Last but not least, |cubicweb| provides a set of default views accepting any kind
+of entities.
+
+Want a proof? Create a community as you've already done for other entity types
+through the index page, you'll then see something like that:
+
+.. image:: ../../images/tutos-base_myblog-community-default-primary_en.png
+   :alt: the default primary view for our community entity type
+
+
+If you notice the weird messages that appear in the page: those are messages
+generated for the new data model, which have no translation yet. To fix that,
+we'll have to use dedicated `cubicweb-ctl` commands:
+
+.. sourcecode: bash
+
+  cubicweb-ctl i18ncube myblog # build/update cube's message catalogs
+  # then add translation into .po file into the cube's i18n directory
+  cubicweb-ctl i18ninstance myblog # recompile instance's message catalogs
+  cubicweb-ctl restart -D myblog # instance has to be restarted to consider new catalogs
+
+You'll then be able to redefine each of them according to your needs and
+preferences. So let's see how to do such thing.
+
+Changing the layout of the application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The layout is the general organization of the pages in the site. Views that generate
+the layout are sometimes referred to as 'templates'. They are implemented in the
+framework in the module :mod:`cubicweb.web.views.basetemplates`. By overriding
+classes in this module, you can customize whatever part you wish of the default
+layout.
+
+But notice that |cubicweb| provides many other ways to customize the
+interface, thanks to actions and components (which you can individually
+(de)activate, control their location, customize their look...) as well as
+"simple" CSS customization. You should first try to achieve your goal using such
+fine grained parametrization rather then overriding a whole template, which usually
+embeds customisation access points that you may loose in the process.
+
+But for the sake of example, let's say we want to change the generic page
+footer...  We can simply add to the module ``views`` of our cube,
+e.g. :file:`cubes/myblog/views.py`, the code below:
+
+.. sourcecode:: python
+
+  from cubicweb.web.views import basetemplates
+
+  class MyHTMLPageFooter(basetemplates.HTMLPageFooter):
+
+      def footer_content(self):
+	  self.w(u'This website has been created with <a href="http://cubicweb.org">CubicWeb</a>.')
+
+  def registration_callback(vreg):
+      vreg.register_all(globals().values(), __name__, (MyHTMLPageFooter,))
+      vreg.register_and_replace(MyHTMLPageFooter, basetemplates.HTMLPageFooter)
+
+
+* Our class inherits from the default page footer to ease getting things right,
+  but this is not mandatory.
+
+* When we want to write something to the output stream, we simply call `self.w`,
+  which *must be passed a unicode string*.
+
+* The latest function is the most exotic stuff. The point is that without it, you
+  would get an error at display time because the framework wouldn't be able to
+  choose which footer to use between :class:`HTMLPageFooter` and
+  :class:`MyHTMLPageFooter`, since both have the same selector, hence the same
+  score...  In this case, we want our footer to replace the default one, so we have
+  to define a :func:`registration_callback` function to control object
+  registration: the first instruction tells to register everything in the module
+  but the :class:`MyHTMLPageFooter` class, then the second to register it instead
+  of :class:`HTMLPageFooter`. Without this function, everything in the module is
+  registered blindly.
+
+.. Note::
+
+  When a view is modified while running in debug mode, it is not required to
+  restart the instance server. Save the Python file and reload the page in your
+  web browser to view the changes.
+
+We will now have this simple footer on every page of the site.
+
+
+Primary view customization
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The 'primary' view (i.e. any view with the identifier set to 'primary') is the one used to
+display all the information about a single entity. The standard primary view is one
+of the most sophisticated views of all. It has several customisation points, but
+its power comes with `uicfg`, allowing you to control it without having to
+subclass it.
+
+However this is a bit off-topic for this first tutorial. Let's say we simply want a
+custom primary view for my `Community` entity type, using directly the view
+interface without trying to benefit from the default implementation (you should
+do that though if you're rewriting reusable cubes; everything is described in more
+details in :ref:`primary_view`).
+
+
+So... Some code! That we'll put again in the module ``views`` of our cube.
+
+.. sourcecode:: python
+
+  from cubicweb.predicates import is_instance
+  from cubicweb.web.views import primary
+
+  class CommunityPrimaryView(primary.PrimaryView):
+      __select__ = is_instance('Community')
+
+      def cell_call(self, row, col):
+          entity = self.cw_rset.get_entity(row, col)
+          self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
+          if entity.description:
+              self.w(u'<p>%s</p>' % entity.printable_value('description'))
+
+What's going on here?
+
+* Our class inherits from the default primary view, here mainly to get the correct
+  view identifier, since we don't use any of its features.
+
+* We set on it a selector telling that it only applies when trying to display
+  some entity of the `Community` type. This is enough to get an higher score than
+  the default view for entities of this type.
+
+* View applying to entities usually have to define `cell_call` as entry point,
+  and are given `row` and `col` arguments tell to which entity in the result set
+  the view is applied. We can then get this entity from the result set
+  (`self.cw_rset`) by using the `get_entity` method.
+
+* To ease thing, we access our entity's attribute for display using its
+  printable_value method, which will handle formatting and escaping when
+  necessary. As you can see, you can also access attributes by their name on the
+  entity to get the raw value.
+
+
+You can now reload the page of the community we just created and see the changes.
+
+.. image:: ../../images/tutos-base_myblog-community-custom-primary_en.png
+   :alt: the custom primary view for our community entity type
+
+We've seen here a lot of thing you'll have to deal with to write views in
+|cubicweb|. The good news is that this is almost everything that is used to
+build higher level layers.
+
+.. Note::
+
+  As things get complicated and the volume of code in your cube increases, you can
+  of course still split your views module into a python package with subpackages.
+
+You can find more details about views and selectors in :ref:`Views`.
+
+
+Write entities to add logic in your data
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+|cubicweb| provides an ORM to easily programmaticaly manipulate
+entities (just like the one we have fetched earlier by calling
+`get_entity` on a result set). By default, entity
+types are instances of the :class:`AnyEntity` class, which holds a set of
+predefined methods as well as property automatically generated for
+attributes/relations of the type it represents.
+
+You can redefine each entity to provide additional methods or whatever you want
+to help you write your application. Customizing an entity requires that your
+entity:
+
+- inherits from :class:`cubicweb.entities.AnyEntity` or any subclass
+
+- defines a :attr:`__regid__` linked to the corresponding data type of your schema
+
+You may then want to add your own methods, override default implementation of some
+method, etc...
+
+.. sourcecode:: python
+
+    from cubicweb.entities import AnyEntity, fetch_config
+
+
+    class Community(AnyEntity):
+        """customized class for Community entities"""
+        __regid__ = 'Community'
+
+        fetch_attrs, cw_fetch_order = fetch_config(['name'])
+
+        def dc_title(self):
+            return self.name
+
+        def display_cw_logo(self):
+            return 'CubicWeb' in self.description
+
+In this example:
+
+* we used convenience :func:`fetch_config` function to tell which attributes
+  should be prefetched by the ORM when looking for some related entities of this
+  type, and how they should be ordered
+
+* we overrode the standard `dc_title` method, used in various place in the interface
+  to display the entity (though in this case the default implementation would
+  have had the same result)
+
+* we implemented here a method :meth:`display_cw_logo` which tests if the blog
+  entry title contains 'CW'.  It can then be used when you're writing code
+  involving 'Community' entities in your views, hooks, etc. For instance, you can
+  modify your previous views as follows:
+
+.. sourcecode:: python
+
+
+  class CommunityPrimaryView(primary.PrimaryView):
+      __select__ = is_instance('Community')
+
+      def cell_call(self, row, col):
+          entity = self.cw_rset.get_entity(row, col)
+          self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
+          if entity.display_cw_logo():
+              self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
+          if entity.description:
+              self.w(u'<p>%s</p>' % entity.printable_value('description'))
+
+Then each community whose description contains 'CW' is shown with the |cubicweb|
+logo in front of it.
+
+.. Note::
+
+  As for view, you don't have to restart your instance when modifying some entity
+  classes while your server is running in debug mode, the code will be
+  automatically reloaded.
+
+
+Extending the application by using more cubes!
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One of the goal of the |cubicweb| framework was to have truly reusable
+components. To do so, they must both behave nicely when plugged into the
+application and be easily customisable, from the data model to the user
+interface. And I think the result is pretty successful, thanks to system such as
+the selection mechanism and the choice to write views as python code which allows
+to build our page using true object oriented programming techniques, that no
+template language provides.
+
+
+A library of standard cubes is available from `CubicWeb Forge`_, to address a
+lot of common concerns such has manipulating people, files, things to do, etc. In
+our community blog case, we could be interested for instance in functionalities
+provided by the `comment` and `tag` cubes. The former provides threaded
+discussion functionalities, the latter a simple tag mechanism to classify content.
+Let's say we want to try those. We will first modify our cube's :file:`__pkginfo__.py`
+file:
+
+.. sourcecode:: python
+
+   __depends__ =  {'cubicweb': '>= 3.10.7',
+                   'cubicweb-blog': None,
+                   'cubicweb-comment': None,
+                   'cubicweb-tag': None}
+
+Now, we'll simply tell on which entity types we want to activate the 'comment'
+and 'tag' facilities by adding respectively the 'comments' and 'tags' relations on
+them in our schema (:file:`schema.py`).
+
+.. sourcecode:: python
+
+  class comments(RelationDefinition):
+      subject = 'Comment'
+      object = 'BlogEntry'
+      cardinality = '1*'
+      composite = 'object'
+
+  class tags(RelationDefinition):
+      subject = 'Tag'
+      object = ('Community', 'BlogEntry')
+
+
+So in the case above we activated comments on `BlogEntry` entities and tags on
+both `Community` and `BlogEntry`. Various views from both `comment` and `tag`
+cubes will then be automatically displayed when one of those relations is
+supported.
+
+Let's synchronize the data model as we've done earlier: ::
+
+
+  $ cubicweb-ctl stop myblog
+  $ cubicweb-ctl shell myblog
+  entering the migration python shell
+  just type migration commands or arbitrary python code and type ENTER to execute it
+  type "exit" or Ctrl-D to quit the shell and resume operation
+  >>> add_cubes(('comment', 'tag'))
+  >>>
+
+Then restart the instance. Let's look at a blog entry:
+
+.. image:: ../../images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png
+   :alt: the primary view for a blog entry with comments and tags activated
+
+As you can see, we now have a box displaying tags and a section proposing to add
+a comment and displaying existing one below the post. All this without changing
+anything in our views, thanks to the design of generic views provided by the
+framework. Though if we take a look at a community, we won't see the tags box!
+That's because by default this box try to locate itself in the left column within
+the white frame, and this column is handled by the primary view we
+hijacked. Let's change our view to make it more extensible, by keeping both our
+custom rendering but also extension points provided by the default
+implementation.
+
+
+.. sourcecode:: python
+
+  class CommunityPrimaryView(primary.PrimaryView):
+      __select__ = is_instance('Community')
+
+      def render_entity_title(self, entity):
+	  self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
+
+      def render_entity_attributes(self, entity):
+	  if entity.display_cw_logo():
+	      self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
+	  if entity.description:
+	      self.w(u'<p>%s</p>' % entity.printable_value('description'))
+
+It appears now properly:
+
+.. image:: ../../images/tutos-base_myblog-community-taggable-primary_en.png
+   :alt: the custom primary view for a community entry with tags activated
+
+You can control part of the interface independently from each others, piece by
+piece. Really.
+
+
+
+.. _`CubicWeb Forge`: http://www.cubicweb.org/project
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/discovering-the-ui.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,161 @@
+
+.. _TutosBaseDiscoveringTheUI:
+
+Discovering the web interface
+-----------------------------
+
+You can now access your web instance to create blogs and post messages
+by visiting the URL http://localhost:8080/.
+
+By default, anonymous access is disabled, so a login form will appear. If you
+asked to allow anonymous access when initializing the instance, click on the
+'login' link in the top right hand corner. To login, you need then use the admin
+account you specified at the time you initialized the database with
+``cubicweb-ctl create``.
+
+.. image:: ../../images/tutos-base_login-form_en.png
+   :alt: the login form
+
+
+Once authenticated, you can start playing with your instance. The default index
+page looks like the following:
+
+.. image:: ../../images/tutos-base_index_en.png
+   :alt: the index page
+
+
+Minimal configuration
+~~~~~~~~~~~~~~~~~~~~~
+
+Before creating entities, let's change that 'unset title' thing that appears
+here and there. This comes from a |cubicweb| system properties. To set it,
+click on the 'site configuration link' in the pop-up menu behind your login name
+in the upper left-hand corner
+
+.. image:: ../../images/tutos-base_user-menu_en.png
+   :alt: the user pop-up menu
+
+The site title is in the 'Ui' section. Simply set it to the desired value and
+click the 'validate' button.
+
+.. image:: ../../images/tutos-base_siteconfig_en.png
+   :alt: the site configuration form
+
+You should see a 'changes applied' message. You can now go back to the
+index page by clicking on the |cubicweb| logo in the upper left-hand corner.
+
+You will much likely still see 'unset title' at this point. This is because by
+default the index page is cached. Force a refresh of the page (by typing Ctrl-R
+in Firefox for instance) and you should now see the title you entered.
+
+
+Adding entities
+~~~~~~~~~~~~~~~
+
+The ``blog`` cube defines several entity types, among them ``Blog`` which is a
+container for ``BlogEntry`` (i.e. posts) on a particular topic. We can get a
+graphical view of the schema by clicking on the 'site schema' link in the user
+pop-up menu we've already seen:
+
+.. image:: ../../images/tutos-base_schema_en.png
+   :alt: graphical view of the schema (aka data-model)
+
+Nice isn't it? Notice that this, as most other stuff we'll see in this tutorial,
+is generated by the framework according to the model of the application. In our
+case, the model defined by the ``blog`` cube.
+
+Now let us create a few of these entities.
+
+
+Add a blog
+**********
+
+Clicking on the `[+]` at the left of the 'Blog' link on the index page will lead
+you to an HTML form to create a blog.
+
+.. image:: ../../images/tutos-base_blog-form_en.png
+   :alt: the blog creation form
+
+For instance, call this new blog 'Tech-blog' and type in 'everything about
+technology' as the description , then validate the form by clicking on
+'Validate'. You will be redirected to the `primary` view of the newly created blog.
+
+.. image:: ../../images/tutos-base_blog-primary_en.png
+   :alt: the blog primary view
+
+
+Add a blog post
+***************
+
+There are several ways to add a blog entry. The simplest is to click on the 'add
+blog entry' link in the actions box on viewing the blog you have just created.
+You will then see a form to create a post, with a 'blog entry of' field preset
+to the blog we're coming from. Enter a title, some content, click the 'validate'
+button and you're done. You will be redirected to the blog primary view, though you
+now see that it contains the blog post you've just created.
+
+.. image:: ../../images/tutos-base_blog-primary-after-post-creation_en.png
+   :alt: the blog primary view after creation of a post
+
+Notice there are some new boxes that appears in the left column.
+
+You can achieve the same thing by following the same path as we did for the blog
+creation, e.g. by clicking on the `[+]` at the left of the 'Blog entry' link on
+the index page. The diffidence being that since there is no context information,
+the 'blog entry of' selector won't be preset to the blog.
+
+
+If you click on the 'modify' link of the action box, you are back to
+the form to edit the entity you just created, except that the form now
+has another section with a combo-box entitled 'add relation'. It
+provisos a generic way to edit relations which don't appears in the
+above form. Choose the relation you want to add and a second combo box
+appears where you can pick existing entities.  If there are too many
+of them, you will be offered to navigate to the target entity, that is
+go away from the form and go back to it later, once you've selected
+the entity you want to link with.
+
+.. image:: ../../images/tutos-base_form-generic-relations_en.png
+   :alt: the generic relations combo box
+
+This combo box can't appear until the entity is actually created. That's why you
+haven't seen it at creation time. You could also have hit 'Apply' instead of
+'validate' and it would have showed up.
+
+
+About ui auto-adaptation
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+One of the things that make |cubicweb| different of other frameworks is
+its automatic user interface that adapts itself according to the data being
+displayed. Let's see an example.
+
+If you go back to the home page an click on the 'Blog' link, you will be redirected
+to the primary view of the blog, the same we've seen earlier. Now, add another
+blog, go back to the index page, and click again on this link. You will see
+a very different view (namely the 'list' view).
+
+.. image:: ../../images/tutos-base_blogs-list_en.png
+   :alt: the list view when there are more than one blog to display
+
+This is because in the first case, the framework chose to use the 'primary'
+view since there was only one entity in the data to be displayed. Now that there
+are two entities, the 'list' view is more appropriate and hence is being used.
+
+There are various other places where |cubicweb| adapts to display data in the best
+way, the main being provided by the view *selection* mechanism that will be detailed
+later.
+
+
+Digging deeper
+~~~~~~~~~~~~~~
+
+By following principles explained below, you should now be able to
+create new users for your application, to configure with a finer
+grain, etc... You will notice that the index page lists a lot of types
+you don't know about. Most are built-in types provided by the framework
+to make the whole system work. You may ignore them in a first time and
+discover them as time goes.
+
+One thing that is worth playing with is the search box. It may be used in various
+way, from simple full text search to advanced queries using the :ref:`RQL` .
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,48 @@
+.. -*- coding: utf-8 -*-
+
+.. _TutosBase:
+
+Building a simple blog with |cubicweb|
+======================================
+
+|cubicweb| is a semantic web application framework that favors reuse and
+object-oriented design.
+
+
+This tutorial is designed to help in your very first steps to start with
+|cubicweb|. We will tour through basic concepts such as:
+
+* getting an application running by using existing components
+* discovering the default user interface
+* basic extending and customizing the look and feel of that application
+
+More advanced concepts are covered in :ref:`TutosPhotoWebSite`.
+
+
+.. _TutosBaseVocab:
+
+Some vocabulary
+---------------
+
+|cubicweb| comes with a few words of vocabulary that you should know to
+understand what we're talking about. To follow this tutorial, you should at least
+know that:
+
+* a `cube` is a component that usually includes a model defining some data types
+  and a set of views to display them. A cube can be built by assembling other
+  cubes;
+
+* an `instance` is a specific installation of one or more cubes and includes
+  configuration files, a web server and a database.
+
+Reading :ref:`Concepts` for more vocabulary will be required at some point.
+
+Now, let's start the hot stuff!
+
+.. toctree::
+   :maxdepth: 2
+
+   blog-in-five-minutes
+   discovering-the-ui
+   customizing-the-application
+   conclusion
--- a/doc/tutorials/dataimport/data_import_tutorial.rst	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,646 +0,0 @@
-Importing relational data into a CubicWeb instance
-==================================================
-
-Introduction
-~~~~~~~~~~~~
-
-This tutorial explains how to import data from an external source (e.g. a collection of files) 
-into a CubicWeb cube instance.
-
-First, once we know the format of the data we wish to import, we devise a 
-*data model*, that is, a CubicWeb (Yams) schema which reflects the way the data
-is structured. This schema is implemented in the ``schema.py`` file.
-In this tutorial, we will describe such a schema for a particular data set, 
-the Diseasome data (see below).
-
-Once the schema is defined, we create a cube and an instance. 
-The cube is a specification of an application, whereas an instance 
-is the application per se. 
-
-Once the schema is defined and the instance is created, the import can be performed, via
-the following steps:
-
-1. Build a custom parser for the data to be imported. Thus, one obtains a Python
-   memory representation of the data.
-
-2. Map the parsed data to the data model defined in ``schema.py``.
-
-3. Perform the actual import of the data. This comes down to "populating"
-   the data model with the memory representation obtained at 1, according to
-   the mapping defined at 2.
-
-This tutorial illustrates all the above steps in the context of relational data
-stored in the RDF format.
-
-More specifically, we describe the import of Diseasome_ RDF/OWL data.
-
-.. _Diseasome: http://datahub.io/dataset/fu-berlin-diseasome
-
-Building a data model
-~~~~~~~~~~~~~~~~~~~~~
-
-The first thing to do when using CubicWeb for creating an application from scratch
-is to devise a *data model*, that is, a relational representation of the problem to be
-modeled or of the structure of the data to be imported. 
-
-In such a schema, we define
-an entity type (``EntityType`` objects) for each type of entity to import. Each such type
-has several attributes. If the attributes are of known CubicWeb (Yams) types, viz. numbers,
-strings or characters, then they are defined as attributes, as e.g. ``attribute = Int()``
-for an attribute named ``attribute`` which is an integer. 
-
-Each such type also has a set of
-relations, which are defined like the attributes, except that they represent, in fact,
-relations between the entities of the type under discussion and the objects of a type which
-is specified in the relation definition. 
-
-For example, for the Diseasome data, we have two types of entities, genes and diseases.
-Thus, we create two classes which inherit from ``EntityType``::
-
-    class Disease(EntityType):
-        # Corresponds to http://www.w3.org/2000/01/rdf-schema#label
-        label = String(maxsize=512, fulltextindexed=True)
-        ...
-
-        #Corresponds to http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene
-        associated_genes = SubjectRelation('Gene', cardinality='**')
-        ...
-
-        #Corresponds to 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/chromosomalLocation'
-        chromosomal_location = SubjectRelation('ExternalUri', cardinality='?*', inlined=True)
-
-
-    class Gene(EntityType):
-        ...
-
-In this schema, there are attributes whose values are numbers or strings. Thus, they are 
-defined by using the CubicWeb / Yams primitive types, e.g., ``label = String(maxsize=12)``. 
-These types can have several constraints or attributes, such as ``maxsize``. 
-There are also relations, either between the entity types themselves, or between them
-and a CubicWeb type, ``ExternalUri``. The latter defines a class of URI objects in 
-CubicWeb. For instance, the ``chromosomal_location`` attribute is a relation between 
-a ``Disease`` entity and an ``ExternalUri`` entity. The relation is marked by the CubicWeb /
-Yams ``SubjectRelation`` method. The latter can have several optional keyword arguments, such as
-``cardinality`` which specifies the number of subjects and objects related by the relation type 
-specified. For example, the ``'?*'`` cardinality in the ``chromosomal_relation`` relation type says
-that zero or more ``Disease`` entities are related to zero or one ``ExternalUri`` entities.
-In other words, a ``Disease`` entity is related to at most one ``ExternalUri`` entity via the
-``chromosomal_location`` relation type, and that we can have zero or more ``Disease`` entities in the
-data base. 
-For a relation between the entity types themselves, the ``associated_genes`` between a ``Disease``
-entity and a ``Gene`` entity is defined, so that any number of ``Gene`` entities can be associated
-to a ``Disease``, and there can be any number of ``Disease`` s if a ``Gene`` exists.
-
-Of course, before being able to use the CubicWeb / Yams built-in objects, we need to import them::
-
-    
-    from yams.buildobjs import EntityType, SubjectRelation, String, Int
-    from cubicweb.schemas.base import ExternalUri
-
-Building a custom data parser
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The data we wish to import is structured in the RDF format,
-as a text file containing a set of lines. 
-On each line, there are three fields. 
-The first two fields are URIs ("Universal Resource Identifiers"). 
-The third field is either an URI or a string. Each field bares a particular meaning:
-
-- the leftmost field is an URI that holds the entity to be imported. 
-  Note that the entities defined in the data model (i.e., in ``schema.py``) should 
-  correspond to the entities whose URIs are specified in the import file.
-
-- the middle field is an URI that holds a relation whose subject is the  entity 
-  defined by the leftmost field. Note that this should also correspond
-  to the definitions in the data model.
-
-- the rightmost field is either an URI or a string. When this field is an URI, 
-  it gives the object of the relation defined by the middle field.
-  When the rightmost field is a string, the middle field is interpreted as an attribute
-  of the subject (introduced by the leftmost field) and the rightmost field is
-  interpreted as the value of the attribute.
-
-Note however that some attributes (i.e. relations whose objects are strings) 
-have their objects defined as strings followed by ``^^`` and by another URI;
-we ignore this part.
-
-Let us show some examples:
-
-- of line holding an attribute definition:
-  ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/CYP17A1> 
-  <http://www.w3.org/2000/01/rdf-schema#label> "CYP17A1" .``
-  The line contains the definition of the ``label`` attribute of an
-  entity of type ``gene``. The value of ``label`` is '``CYP17A1``'.
-
-- of line holding a relation definition:
-  ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/1> 
-  <http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene> 
-  <http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HADH2> .``
-  The line contains the definition of the ``associatedGene`` relation between
-  a ``disease`` subject entity identified by ``1`` and a ``gene`` object 
-  entity defined by ``HADH2``.
-
-Thus, for parsing the data, we can (:note: see the ``diseasome_parser`` module):
-
-1. define a couple of regular expressions for parsing the two kinds of lines, 
-   ``RE_ATTS`` for parsing the attribute definitions, and ``RE_RELS`` for parsing
-   the relation definitions.
-
-2. define a function that iterates through the lines of the file and retrieves
-   (``yield`` s) a (subject, relation, object) tuple for each line.
-   We called it ``_retrieve_structure`` in the ``diseasome_parser`` module.
-   The function needs the file name and the types for which information
-   should be retrieved.
-
-Alternatively, instead of hand-making the parser, one could use the RDF parser provided
-in the ``dataio`` cube.
-
-.. XXX To further study and detail the ``dataio`` cube usage.
-
-Once we get to have the (subject, relation, object) triples, we need to map them into
-the data model.
-
-
-Mapping the data to the schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the case of diseasome data, we can just define two dictionaries for mapping
-the names of the relations as extracted by the parser, to the names of the relations
-as defined in the ``schema.py`` data model. In the ``diseasome_parser`` module 
-they are called ``MAPPING_ATTS`` and ``MAPPING_RELS``. 
-Given that the relation and attribute names are given in CamelCase in the original data,
-mappings are necessary if we follow the PEP08 when naming the attributes in the data model.
-For example, the RDF relation ``chromosomalLocation`` is mapped into the schema relation 
-``chromosomal_location``.
-
-Once these mappings have been defined, we just iterate over the (subject, relation, object)
-tuples provided by the parser and we extract the entities, with their attributes and relations.
-For each entity, we thus have a dictionary with two keys, ``attributes`` and ``relations``.
-The value associated to the ``attributes`` key is a dictionary containing (attribute: value) 
-pairs, where "value" is a string, plus the ``cwuri`` key / attribute holding the URI of 
-the entity itself.
-The value associated to the ``relations`` key is a dictionary containing (relation: value)
-pairs, where "value" is an URI.
-This is implemented in the ``entities_from_rdf`` interface function of the module 
-``diseasome_parser``. This function provides an iterator on the dictionaries containing
-the ``attributes`` and ``relations`` keys for all entities.
-
-However, this is a simple case. In real life, things can get much more complicated, and the 
-mapping can be far from trivial, especially when several data sources (which can follow 
-different formatting and even structuring conventions) must be mapped into the same data model.
-
-Importing the data
-~~~~~~~~~~~~~~~~~~
-
-The data import code should be placed in a Python module. Let us call it 
-``diseasome_import.py``. Then, this module should be called via
-``cubicweb-ctl``, as follows::
-
-    cubicweb-ctl shell diseasome_import.py -- <other arguments e.g. data file>
-
-In the import module, we should use a *store* for doing the import.
-A store is an object which provides three kinds of methods for
-importing data:
-
-- a method for importing the entities, along with the values
-  of their attributes.
-- a method for importing the relations between the entities.
-- a method for committing the imports to the database.
-
-In CubicWeb, we have four stores:
-
-1. ``ObjectStore`` base class for the stores in CubicWeb.
-   It only provides a skeleton for all other stores and
-   provides the means for creating the memory structures
-   (dictionaries) that hold the entities and the relations
-   between them.
-
-2. ``RQLObjectStore``: store which uses the RQL language for performing
-   database insertions and updates. It relies on all the CubicWeb hooks 
-   machinery, especially for dealing with security issues (database access
-   permissions).
-
-2. ``NoHookRQLObjectStore``: store which uses the RQL language for
-   performing database insertions and updates, but for which 
-   all hooks are deactivated. This implies that 
-   certain checks with respect to the CubicWeb / Yams schema 
-   (data model) are not performed. However, all SQL queries 
-   obtained from the RQL ones are executed in a sequential
-   manner, one query per inserted entity.
-
-4. ``SQLGenObjectStore``: store which uses the SQL language directly. 
-   It inserts entities either sequentially, by executing an SQL query 
-   for each entity, or directly by using one PostGRES ``COPY FROM`` 
-   query for a set of similarly structured entities. 
-
-For really massive imports (millions or billions of entities), there
-is a cube ``dataio`` which contains another store, called 
-``MassiveObjectStore``. This store is similar to ``SQLGenObjectStore``,
-except that anything related to CubicWeb is bypassed. That is, even the
-CubicWeb EID entity identifiers are not handled. This store is the fastest,
-but has a slightly different API from the other four stores mentioned above.
-Moreover, it has an important limitation, in that it doesn't insert inlined [#]_
-relations in the database. 
-
-.. [#] An inlined relation is a relation defined in the schema
-       with the keyword argument ``inlined=True``. Such a relation
-       is inserted in the database as an attribute of the entity
-       whose subject it is.
-
-In the following section we will see how to import data by using the stores
-in CubicWeb's ``dataimport`` module.
-
-Using the stores in ``dataimport``
-++++++++++++++++++++++++++++++++++
-
-``ObjectStore`` is seldom used in real life for importing data, since it is
-only the base store for the other stores and it doesn't perform an actual
-import of the data. Nevertheless, the other three stores, which import data,
-are based on ``ObjectStore`` and provide the same API.
-
-All three stores ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
-``SQLGenObjectStore`` provide exactly the same API for importing data, that is
-entities and relations, in an SQL database. 
-
-Before using a store, one must import the ``dataimport`` module and then initialize 
-the store, with the current ``session`` as a parameter::
-
-    import cubicweb.dataimport as cwdi
-    ...
-
-    store = cwdi.RQLObjectStore(session)
-
-Each such store provides three methods for data import:
-
-#. ``create_entity(Etype, **attributes)``, which allows us to add
-   an entity of the Yams type ``Etype`` to the database. This entity's attributes
-   are specified in the ``attributes`` dictionary. The method returns the entity 
-   created in the database. For example, we add two entities,
-   a person, of ``Person`` type, and a location, of ``Location`` type::
-
-        person = store.create_entity('Person', name='Toto', age='18', height='190')
-
-        location = store.create_entity('Location', town='Paris', arrondissement='13')
-
-#. ``relate(subject_eid, r_type, object_eid)``, which allows us to add a relation
-   of the Yams type ``r_type`` to the database. The relation's subject is an entity
-   whose EID is ``subject_eid``; its object is another entity, whose EID is 
-   ``object_eid``.  For example [#]_::
-
-        store.relate(person.eid(), 'lives_in', location.eid(), **kwargs)
-
-   ``kwargs`` is only used by the ``SQLGenObjectStore``'s ``relate`` method and is here
-   to allow us to specify the type of the subject of the relation, when the relation is
-   defined as inlined in the schema. 
-
-.. [#] The ``eid`` method of an entity defined via ``create_entity`` returns
-       the entity identifier as assigned by CubicWeb when creating the entity.
-       This only works for entities defined via the stores in the CubicWeb's
-       ``dataimport`` module.
-
-    The keyword argument that is understood by ``SQLGenObjectStore`` is called 
-   ``subjtype`` and holds the type of the subject entity. For the example considered here,
-   this comes to having [#]_::
-
-        store.relate(person.eid(), 'lives_in', location.eid(), subjtype=person.cw_etype)
-
-   If ``subjtype`` is not specified, then the store tries to infer the type of the subject.
-   However, this doesn't always work, e.g. when there are several possible subject types
-   for a given relation type. 
-
-.. [#] The ``cw_etype`` attribute of an entity defined via ``create_entity`` holds
-       the type of the entity just created. This only works for entities defined via
-       the stores in the CubicWeb's ``dataimport`` module. In the example considered
-       here, ``person.cw_etype`` holds ``'Person'``.
-    
-   All the other stores but ``SQLGenObjectStore`` ignore the ``kwargs`` parameters.
-
-#. ``flush()``, which allows us to perform the actual commit into the database, along
-   with some cleanup operations. Ideally, this method should be called as often as 
-   possible, that is after each insertion in the database, so that database sessions
-   are kept as atomic as possible. In practice, we usually call this method twice: 
-   first, after all the entities have been created, second, after all relations have
-   been created. 
-
-   Note however that before each commit the database insertions
-   have to be consistent with the schema. Thus, if, for instance,
-   an entity has an attribute defined through a relation (viz.
-   a ``SubjectRelation``) with a ``"1"`` or ``"+"`` object 
-   cardinality, we have to create the entity under discussion,
-   the object entity of the relation under discussion, and the
-   relation itself, before committing the additions to the database.
-
-   The ``flush`` method is simply called as::
-
-        store.flush().
-
-
-Using the ``MassiveObjectStore`` in the ``dataio`` cube
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-This store, available in the ``dataio`` cube, allows us to
-fully dispense with the CubicWeb import mechanisms and hence
-to interact directly with the database server, via SQL queries.
-
-Moreover, these queries rely on PostGreSQL's ``COPY FROM`` instruction
-to create several entities in a single query. This brings tremendous 
-performance improvements with respect to the RQL-based data insertion
-procedures.
-
-However, the API of this store is slightly different from the API of
-the stores in CubicWeb's ``dataimport`` module.
-
-Before using the store, one has to import the ``dataio`` cube's 
-``dataimport`` module, then initialize the store by giving it the
-``session`` parameter::
-
-    from cubes.dataio import dataimport as mcwdi
-    ...
-
-    store = mcwdi.MassiveObjectStore(session)
-
-The ``MassiveObjectStore`` provides six methods for inserting data
-into the database:
-
-#. ``init_rtype_table(SubjEtype, r_type, ObjEtype)``, which specifies the
-   creation of the tables associated to the relation types in the database.
-   Each such table has three column, the type of the subject entity, the
-   type of the relation (that is, the name of the attribute in the subject
-   entity which is defined via the relation), and the type of the object
-   entity. For example::
-
-        store.init_rtype_table('Person', 'lives_in', 'Location')
-
-   Please note that these tables can be created before the entities, since
-   they only specify their types, not their unique identifiers.
-
-#. ``create_entity(Etype, **attributes)``, which allows us to add new entities,
-   whose attributes are given in the ``attributes`` dictionary. 
-   Please note however that, by default, this method does *not* return 
-   the created entity. The method is called, for example, as in::
-
-        store.create_entity('Person', name='Toto', age='18', height='190', 
-                            uri='http://link/to/person/toto_18_190')
-        store.create_entity('Location', town='Paris', arrondissement='13',
-                            uri='http://link/to/location/paris_13')
-   
-   In order to be able to link these entities via the relations when needed,
-   we must provide ourselves a means for uniquely identifying the entities.
-   In general, this is done via URIs, stored in attributes like ``uri`` or
-   ``cwuri``. The name of the attribute is irrelevant as long as its value is
-   unique for each entity.
-
-#. ``relate_by_iid(subject_iid, r_type, object_iid)`` allows us to actually 
-   relate the entities uniquely identified by ``subject_iid`` and 
-   ``object_iid`` via a relation of type ``r_type``. For example::
-
-        store.relate_by_iid('http://link/to/person/toto_18_190',
-                            'lives_in',
-                            'http://link/to/location/paris_13')
-
-   Please note that this method does *not* work for inlined relations!
-
-#. ``convert_relations(SubjEtype, r_type, ObjEtype, subj_iid_attribute,
-   obj_iid_attribute)``
-   allows us to actually insert
-   the relations in the database. At one call of this method, one inserts
-   all the relations of type ``rtype`` between entities of given types.
-   ``subj_iid_attribute`` and ``object_iid_attribute`` are the names
-   of the attributes which store the unique identifiers of the entities,
-   as assigned by the user. These names can be identical, as long as
-   their values are unique. For example, for inserting all relations
-   of type ``lives_in`` between ``People`` and ``Location`` entities,
-   we write::
-        
-        store.convert_relations('Person', 'lives_in', 'Location', 'uri', 'uri')
-
-#. ``flush()`` performs the actual commit in the database. It only needs 
-   to be called after ``create_entity`` and ``relate_by_iid`` calls. 
-   Please note that ``relate_by_iid`` does *not* perform insertions into
-   the database, hence calling ``flush()`` for it would have no effect.
-
-#. ``cleanup()`` performs database cleanups, by removing temporary tables.
-   It should only be called at the end of the import.
-
-
-
-.. XXX to add smth on the store's parameter initialization.
-
-
-
-Application to the Diseasome data
-+++++++++++++++++++++++++++++++++
-
-Import setup
-############
-
-We define an import function, ``diseasome_import``, which does basically four things:
-
-#. creates and initializes the store to be used, via a line such as::
-    
-        store = cwdi.SQLGenObjectStore(session)
-   
-   where ``cwdi`` is the imported ``cubicweb.dataimport`` or 
-   ``cubes.dataio.dataimport``.
-
-#. calls the diseasome parser, that is, the ``entities_from_rdf`` function in the 
-   ``diseasome_parser`` module and iterates on its result, in a line such as::
-        
-        for entity, relations in parser.entities_from_rdf(filename, ('gene', 'disease')):
-        
-   where ``parser`` is the imported ``diseasome_parser`` module, and ``filename`` is the 
-   name of the file containing the data (with its path), e.g. ``../data/diseasome_dump.nt``.
-
-#. creates the entities to be inserted in the database; for Diseasome, there are two 
-   kinds of entities:
-   
-   #. entities defined in the data model, viz. ``Gene`` and ``Disease`` in our case.
-   #. entities which are built in CubicWeb / Yams, viz. ``ExternalUri`` which define
-      URIs.
-   
-   As we are working with RDF data, each entity is defined through a series of URIs. Hence,
-   each "relational attribute" [#]_ of an entity is defined via an URI, that is, in CubicWeb
-   terms, via an ``ExternalUri`` entity. The entities are created, in the loop presented above,
-   as such::
-        
-        ent = store.create_entity(etype, **entity)
-        
-   where ``etype`` is the appropriate entity type, either ``Gene`` or ``Disease``.
-
-.. [#] By "relational attribute" we denote an attribute (of an entity) which
-       is defined through a relation, e.g. the ``chromosomal_location`` attribute
-       of ``Disease`` entities, which is defined through a relation between a
-       ``Disease`` and an ``ExternalUri``.
-   
-   The ``ExternalUri`` entities are as many as URIs in the data file. For them, we define a unique
-   attribute, ``uri``, which holds the URI under discussion::
-        
-        extu = store.create_entity('ExternalUri', uri="http://path/of/the/uri")
-
-#. creates the relations between the entities. We have relations between:
-   
-   #. entities defined in the schema, e.g. between ``Disease`` and ``Gene``
-      entities, such as the ``associated_genes`` relation defined for 
-      ``Disease`` entities.
-   #. entities defined in the schema and ``ExternalUri`` entities, such as ``gene_id``.
-   
-   The way relations are added to the database depends on the store: 
-   
-   - for the stores in the CubicWeb ``dataimport`` module, we only use 
-     ``store.relate``, in 
-     another loop, on the relations (that is, a 
-     loop inside the preceding one, mentioned at step 2)::
-        
-        for rtype, rels in relations.iteritems():
-            ...
-            
-            store.relate(ent.eid(), rtype, extu.eid(), **kwargs)
-        
-     where ``kwargs`` is a dictionary designed to accommodate the need for specifying
-     the type of the subject entity of the relation, when the relation is inlined and
-     ``SQLGenObjectStore`` is used. For example::
-            
-            ...
-            store.relate(ent.eid(), 'chromosomal_location', extu.eid(), subjtype='Disease')
-   
-   - for the ``MassiveObjectStore`` in the ``dataio`` cube's ``dataimport`` module, 
-     the relations are created in three steps:
-     
-     #. first, a table is created for each relation type, as in::
-            
-            ...
-            store.init_rtype_table(ent.cw_etype, rtype, extu.cw_etype)
-            
-        which comes down to lines such as::
-            
-            store.init_rtype_table('Disease', 'associated_genes', 'Gene')
-            store.init_rtype_table('Gene', 'gene_id', 'ExternalUri')
-            
-     #. second, the URI of each entity will be used as its identifier, in the 
-        ``relate_by_iid`` method, such as::
-            
-            disease_uri = 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/3'
-            gene_uri = '<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HSD3B2'
-            store.relate_by_iid(disease_uri, 'associated_genes', gene_uri)
-            
-     #. third, the relations for each relation type will be added to the database, 
-        via the ``convert_relations`` method, such as in::
-            
-            store.convert_relations('Disease', 'associated_genes', 'Gene', 'cwuri', 'cwuri')
-            
-        and::
-            
-            store.convert_relations('Gene', 'hgnc_id', 'ExternalUri', 'cwuri', 'uri')
-            
-        where ``cwuri`` and ``uri`` are the attributes which store the URIs of the entities
-        defined in the data model, and of the ``ExternalUri`` entities, respectively.
-
-#. flushes all relations and entities::
-    
-    store.flush()
-
-   which performs the actual commit of the inserted entities and relations in the database.
-
-If the ``MassiveObjectStore`` is used, then a cleanup of temporary SQL tables should be performed
-at the end of the import::
-
-    store.cleanup()
-
-Timing benchmarks
-#################
-
-In order to time the import script, we just decorate the import function with the ``timed``
-decorator::
-    
-    from logilab.common.decorators import timed
-    ...
-    
-    @timed
-    def diseasome_import(session, filename):
-        ...
-
-After running the import function as shown in the "Importing the data" section, we obtain two time measurements::
-
-    diseasome_import clock: ... / time: ...
-
-Here, the meanings of these measurements are [#]_:
-
-- ``clock`` is the time spent by CubicWeb, on the server side (i.e. hooks and data pre- / post-processing on SQL 
-  queries),
-
-- ``time`` is the sum between ``clock`` and the time spent in PostGreSQL.
-
-.. [#] The meanings of the ``clock`` and ``time`` measurements, when using the ``@timed``
-       decorators, were taken from `a blog post on massive data import in CubicWeb`_.
-
-.. _a blog post on massive data import in CubicWeb: http://www.cubicweb.org/blogentry/2116712
-
-The import function is put in an import module, named ``diseasome_import`` here. The module is called
-directly from the CubicWeb shell, as follows::
-
-    cubicweb-ctl shell diseasome_instance diseasome_import.py \
-    -- -df diseasome_import_file.nt -st StoreName
-
-The module accepts two arguments:
-
-- the data file, introduced by ``-df [--datafile]``, and
-- the store, introduced by ``-st [--store]``.
-
-The timings (in seconds) for different stores are given in the following table, for 
-importing 4213 ``Disease`` entities and 3919 ``Gene`` entities with the import module
-just described:
-
-+--------------------------+------------------------+--------------------------------+------------+
-| Store                    | CubicWeb time (clock)  | PostGreSQL time (time - clock) | Total time |
-+==========================+========================+================================+============+
-| ``RQLObjectStore``       | 225.98                 | 62.05                          | 288.03     |
-+--------------------------+------------------------+--------------------------------+------------+
-| ``NoHookRQLObjectStore`` | 62.73                  | 51.38                          | 114.11     |
-+--------------------------+------------------------+--------------------------------+------------+
-| ``SQLGenObjectStore``    | 20.41                  | 11.03                          | 31.44      |
-+--------------------------+------------------------+--------------------------------+------------+
-| ``MassiveObjectStore``   | 4.84                   | 6.93                           | 11.77      |
-+--------------------------+------------------------+--------------------------------+------------+
-
-
-Conclusions
-~~~~~~~~~~~
-
-In this tutorial we have seen how to import data in a CubicWeb application instance. We have first seen how to
-create a schema, then how to create a parser of the data and a mapping of the data to the schema.
-Finally, we have seen four ways of importing data into CubicWeb.
-
-Three of those are integrated into CubicWeb, namely the ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
-``SQLGenObjectStore`` stores, which have a common API:
-
-- ``RQLObjectStore`` is by far the slowest, especially its time spent on the 
-  CubicWeb side, and so it should be used only for small amounts of 
-  "sensitive" data (i.e. where security is a concern).
-
-- ``NoHookRQLObjectStore`` slashes by almost four the time spent on the CubicWeb side, 
-  but is also quite slow; on the PostGres side it is as slow as the previous store. 
-  It should be used for data where security is not a concern,
-  but consistency (with the data model) is.
-
-- ``SQLGenObjectStore`` slashes by three the time spent on the CubicWeb side and by five the time 
-  spent on the PostGreSQL side. It should be used for relatively great amounts of data, where
-  security and data consistency are not a concern. Compared to the previous store, it has the
-  disadvantage that, for inlined relations, we must specify their subjects' types.
-
-For really huge amounts of data there is a fourth store, ``MassiveObjectStore``, available
-from the ``dataio`` cube. It provides a blazing performance with respect to all other stores:
-it is almost 25 times faster than ``RQLObjectStore`` and almost three times faster than 
-``SQLGenObjectStore``. However, it has a few usage caveats that should be taken into account:
-
-#. it cannot insert relations defined as inlined in the schema,
-#. no security or consistency check is performed on the data,
-#. its API is slightly different from the other stores.
-
-Hence, this store should be used when security and data consistency are not a concern,
-and there are no inlined relations in the schema.
-
-
-
-
-
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/dataimport/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,646 @@
+Importing relational data into a CubicWeb instance
+==================================================
+
+Introduction
+~~~~~~~~~~~~
+
+This tutorial explains how to import data from an external source (e.g. a collection of files) 
+into a CubicWeb cube instance.
+
+First, once we know the format of the data we wish to import, we devise a 
+*data model*, that is, a CubicWeb (Yams) schema which reflects the way the data
+is structured. This schema is implemented in the ``schema.py`` file.
+In this tutorial, we will describe such a schema for a particular data set, 
+the Diseasome data (see below).
+
+Once the schema is defined, we create a cube and an instance. 
+The cube is a specification of an application, whereas an instance 
+is the application per se. 
+
+Once the schema is defined and the instance is created, the import can be performed, via
+the following steps:
+
+1. Build a custom parser for the data to be imported. Thus, one obtains a Python
+   memory representation of the data.
+
+2. Map the parsed data to the data model defined in ``schema.py``.
+
+3. Perform the actual import of the data. This comes down to "populating"
+   the data model with the memory representation obtained at 1, according to
+   the mapping defined at 2.
+
+This tutorial illustrates all the above steps in the context of relational data
+stored in the RDF format.
+
+More specifically, we describe the import of Diseasome_ RDF/OWL data.
+
+.. _Diseasome: http://datahub.io/dataset/fu-berlin-diseasome
+
+Building a data model
+~~~~~~~~~~~~~~~~~~~~~
+
+The first thing to do when using CubicWeb for creating an application from scratch
+is to devise a *data model*, that is, a relational representation of the problem to be
+modeled or of the structure of the data to be imported. 
+
+In such a schema, we define
+an entity type (``EntityType`` objects) for each type of entity to import. Each such type
+has several attributes. If the attributes are of known CubicWeb (Yams) types, viz. numbers,
+strings or characters, then they are defined as attributes, as e.g. ``attribute = Int()``
+for an attribute named ``attribute`` which is an integer. 
+
+Each such type also has a set of
+relations, which are defined like the attributes, except that they represent, in fact,
+relations between the entities of the type under discussion and the objects of a type which
+is specified in the relation definition. 
+
+For example, for the Diseasome data, we have two types of entities, genes and diseases.
+Thus, we create two classes which inherit from ``EntityType``::
+
+    class Disease(EntityType):
+        # Corresponds to http://www.w3.org/2000/01/rdf-schema#label
+        label = String(maxsize=512, fulltextindexed=True)
+        ...
+
+        #Corresponds to http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene
+        associated_genes = SubjectRelation('Gene', cardinality='**')
+        ...
+
+        #Corresponds to 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/chromosomalLocation'
+        chromosomal_location = SubjectRelation('ExternalUri', cardinality='?*', inlined=True)
+
+
+    class Gene(EntityType):
+        ...
+
+In this schema, there are attributes whose values are numbers or strings. Thus, they are 
+defined by using the CubicWeb / Yams primitive types, e.g., ``label = String(maxsize=12)``. 
+These types can have several constraints or attributes, such as ``maxsize``. 
+There are also relations, either between the entity types themselves, or between them
+and a CubicWeb type, ``ExternalUri``. The latter defines a class of URI objects in 
+CubicWeb. For instance, the ``chromosomal_location`` attribute is a relation between 
+a ``Disease`` entity and an ``ExternalUri`` entity. The relation is marked by the CubicWeb /
+Yams ``SubjectRelation`` method. The latter can have several optional keyword arguments, such as
+``cardinality`` which specifies the number of subjects and objects related by the relation type 
+specified. For example, the ``'?*'`` cardinality in the ``chromosomal_relation`` relation type says
+that zero or more ``Disease`` entities are related to zero or one ``ExternalUri`` entities.
+In other words, a ``Disease`` entity is related to at most one ``ExternalUri`` entity via the
+``chromosomal_location`` relation type, and that we can have zero or more ``Disease`` entities in the
+data base. 
+For a relation between the entity types themselves, the ``associated_genes`` between a ``Disease``
+entity and a ``Gene`` entity is defined, so that any number of ``Gene`` entities can be associated
+to a ``Disease``, and there can be any number of ``Disease`` s if a ``Gene`` exists.
+
+Of course, before being able to use the CubicWeb / Yams built-in objects, we need to import them::
+
+    
+    from yams.buildobjs import EntityType, SubjectRelation, String, Int
+    from cubicweb.schemas.base import ExternalUri
+
+Building a custom data parser
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The data we wish to import is structured in the RDF format,
+as a text file containing a set of lines. 
+On each line, there are three fields. 
+The first two fields are URIs ("Universal Resource Identifiers"). 
+The third field is either an URI or a string. Each field bares a particular meaning:
+
+- the leftmost field is an URI that holds the entity to be imported. 
+  Note that the entities defined in the data model (i.e., in ``schema.py``) should 
+  correspond to the entities whose URIs are specified in the import file.
+
+- the middle field is an URI that holds a relation whose subject is the  entity 
+  defined by the leftmost field. Note that this should also correspond
+  to the definitions in the data model.
+
+- the rightmost field is either an URI or a string. When this field is an URI, 
+  it gives the object of the relation defined by the middle field.
+  When the rightmost field is a string, the middle field is interpreted as an attribute
+  of the subject (introduced by the leftmost field) and the rightmost field is
+  interpreted as the value of the attribute.
+
+Note however that some attributes (i.e. relations whose objects are strings) 
+have their objects defined as strings followed by ``^^`` and by another URI;
+we ignore this part.
+
+Let us show some examples:
+
+- of line holding an attribute definition:
+  ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/CYP17A1> 
+  <http://www.w3.org/2000/01/rdf-schema#label> "CYP17A1" .``
+  The line contains the definition of the ``label`` attribute of an
+  entity of type ``gene``. The value of ``label`` is '``CYP17A1``'.
+
+- of line holding a relation definition:
+  ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/1> 
+  <http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene> 
+  <http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HADH2> .``
+  The line contains the definition of the ``associatedGene`` relation between
+  a ``disease`` subject entity identified by ``1`` and a ``gene`` object 
+  entity defined by ``HADH2``.
+
+Thus, for parsing the data, we can (:note: see the ``diseasome_parser`` module):
+
+1. define a couple of regular expressions for parsing the two kinds of lines, 
+   ``RE_ATTS`` for parsing the attribute definitions, and ``RE_RELS`` for parsing
+   the relation definitions.
+
+2. define a function that iterates through the lines of the file and retrieves
+   (``yield`` s) a (subject, relation, object) tuple for each line.
+   We called it ``_retrieve_structure`` in the ``diseasome_parser`` module.
+   The function needs the file name and the types for which information
+   should be retrieved.
+
+Alternatively, instead of hand-making the parser, one could use the RDF parser provided
+in the ``dataio`` cube.
+
+.. XXX To further study and detail the ``dataio`` cube usage.
+
+Once we get to have the (subject, relation, object) triples, we need to map them into
+the data model.
+
+
+Mapping the data to the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the case of diseasome data, we can just define two dictionaries for mapping
+the names of the relations as extracted by the parser, to the names of the relations
+as defined in the ``schema.py`` data model. In the ``diseasome_parser`` module 
+they are called ``MAPPING_ATTS`` and ``MAPPING_RELS``. 
+Given that the relation and attribute names are given in CamelCase in the original data,
+mappings are necessary if we follow the PEP08 when naming the attributes in the data model.
+For example, the RDF relation ``chromosomalLocation`` is mapped into the schema relation 
+``chromosomal_location``.
+
+Once these mappings have been defined, we just iterate over the (subject, relation, object)
+tuples provided by the parser and we extract the entities, with their attributes and relations.
+For each entity, we thus have a dictionary with two keys, ``attributes`` and ``relations``.
+The value associated to the ``attributes`` key is a dictionary containing (attribute: value) 
+pairs, where "value" is a string, plus the ``cwuri`` key / attribute holding the URI of 
+the entity itself.
+The value associated to the ``relations`` key is a dictionary containing (relation: value)
+pairs, where "value" is an URI.
+This is implemented in the ``entities_from_rdf`` interface function of the module 
+``diseasome_parser``. This function provides an iterator on the dictionaries containing
+the ``attributes`` and ``relations`` keys for all entities.
+
+However, this is a simple case. In real life, things can get much more complicated, and the 
+mapping can be far from trivial, especially when several data sources (which can follow 
+different formatting and even structuring conventions) must be mapped into the same data model.
+
+Importing the data
+~~~~~~~~~~~~~~~~~~
+
+The data import code should be placed in a Python module. Let us call it 
+``diseasome_import.py``. Then, this module should be called via
+``cubicweb-ctl``, as follows::
+
+    cubicweb-ctl shell diseasome_import.py -- <other arguments e.g. data file>
+
+In the import module, we should use a *store* for doing the import.
+A store is an object which provides three kinds of methods for
+importing data:
+
+- a method for importing the entities, along with the values
+  of their attributes.
+- a method for importing the relations between the entities.
+- a method for committing the imports to the database.
+
+In CubicWeb, we have four stores:
+
+1. ``ObjectStore`` base class for the stores in CubicWeb.
+   It only provides a skeleton for all other stores and
+   provides the means for creating the memory structures
+   (dictionaries) that hold the entities and the relations
+   between them.
+
+2. ``RQLObjectStore``: store which uses the RQL language for performing
+   database insertions and updates. It relies on all the CubicWeb hooks 
+   machinery, especially for dealing with security issues (database access
+   permissions).
+
+2. ``NoHookRQLObjectStore``: store which uses the RQL language for
+   performing database insertions and updates, but for which 
+   all hooks are deactivated. This implies that 
+   certain checks with respect to the CubicWeb / Yams schema 
+   (data model) are not performed. However, all SQL queries 
+   obtained from the RQL ones are executed in a sequential
+   manner, one query per inserted entity.
+
+4. ``SQLGenObjectStore``: store which uses the SQL language directly. 
+   It inserts entities either sequentially, by executing an SQL query 
+   for each entity, or directly by using one PostGRES ``COPY FROM`` 
+   query for a set of similarly structured entities. 
+
+For really massive imports (millions or billions of entities), there
+is a cube ``dataio`` which contains another store, called 
+``MassiveObjectStore``. This store is similar to ``SQLGenObjectStore``,
+except that anything related to CubicWeb is bypassed. That is, even the
+CubicWeb EID entity identifiers are not handled. This store is the fastest,
+but has a slightly different API from the other four stores mentioned above.
+Moreover, it has an important limitation, in that it doesn't insert inlined [#]_
+relations in the database. 
+
+.. [#] An inlined relation is a relation defined in the schema
+       with the keyword argument ``inlined=True``. Such a relation
+       is inserted in the database as an attribute of the entity
+       whose subject it is.
+
+In the following section we will see how to import data by using the stores
+in CubicWeb's ``dataimport`` module.
+
+Using the stores in ``dataimport``
+++++++++++++++++++++++++++++++++++
+
+``ObjectStore`` is seldom used in real life for importing data, since it is
+only the base store for the other stores and it doesn't perform an actual
+import of the data. Nevertheless, the other three stores, which import data,
+are based on ``ObjectStore`` and provide the same API.
+
+All three stores ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
+``SQLGenObjectStore`` provide exactly the same API for importing data, that is
+entities and relations, in an SQL database. 
+
+Before using a store, one must import the ``dataimport`` module and then initialize 
+the store, with the current ``session`` as a parameter::
+
+    import cubicweb.dataimport as cwdi
+    ...
+
+    store = cwdi.RQLObjectStore(session)
+
+Each such store provides three methods for data import:
+
+#. ``create_entity(Etype, **attributes)``, which allows us to add
+   an entity of the Yams type ``Etype`` to the database. This entity's attributes
+   are specified in the ``attributes`` dictionary. The method returns the entity 
+   created in the database. For example, we add two entities,
+   a person, of ``Person`` type, and a location, of ``Location`` type::
+
+        person = store.create_entity('Person', name='Toto', age='18', height='190')
+
+        location = store.create_entity('Location', town='Paris', arrondissement='13')
+
+#. ``relate(subject_eid, r_type, object_eid)``, which allows us to add a relation
+   of the Yams type ``r_type`` to the database. The relation's subject is an entity
+   whose EID is ``subject_eid``; its object is another entity, whose EID is 
+   ``object_eid``.  For example [#]_::
+
+        store.relate(person.eid(), 'lives_in', location.eid(), **kwargs)
+
+   ``kwargs`` is only used by the ``SQLGenObjectStore``'s ``relate`` method and is here
+   to allow us to specify the type of the subject of the relation, when the relation is
+   defined as inlined in the schema. 
+
+.. [#] The ``eid`` method of an entity defined via ``create_entity`` returns
+       the entity identifier as assigned by CubicWeb when creating the entity.
+       This only works for entities defined via the stores in the CubicWeb's
+       ``dataimport`` module.
+
+   The keyword argument that is understood by ``SQLGenObjectStore`` is called 
+   ``subjtype`` and holds the type of the subject entity. For the example considered here,
+   this comes to having [#]_::
+
+        store.relate(person.eid(), 'lives_in', location.eid(), subjtype=person.cw_etype)
+
+   If ``subjtype`` is not specified, then the store tries to infer the type of the subject.
+   However, this doesn't always work, e.g. when there are several possible subject types
+   for a given relation type. 
+
+.. [#] The ``cw_etype`` attribute of an entity defined via ``create_entity`` holds
+       the type of the entity just created. This only works for entities defined via
+       the stores in the CubicWeb's ``dataimport`` module. In the example considered
+       here, ``person.cw_etype`` holds ``'Person'``.
+    
+   All the other stores but ``SQLGenObjectStore`` ignore the ``kwargs`` parameters.
+
+#. ``flush()``, which allows us to perform the actual commit into the database, along
+   with some cleanup operations. Ideally, this method should be called as often as 
+   possible, that is after each insertion in the database, so that database sessions
+   are kept as atomic as possible. In practice, we usually call this method twice: 
+   first, after all the entities have been created, second, after all relations have
+   been created. 
+
+   Note however that before each commit the database insertions
+   have to be consistent with the schema. Thus, if, for instance,
+   an entity has an attribute defined through a relation (viz.
+   a ``SubjectRelation``) with a ``"1"`` or ``"+"`` object 
+   cardinality, we have to create the entity under discussion,
+   the object entity of the relation under discussion, and the
+   relation itself, before committing the additions to the database.
+
+   The ``flush`` method is simply called as::
+
+        store.flush().
+
+
+Using the ``MassiveObjectStore`` in the ``dataio`` cube
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+This store, available in the ``dataio`` cube, allows us to
+fully dispense with the CubicWeb import mechanisms and hence
+to interact directly with the database server, via SQL queries.
+
+Moreover, these queries rely on PostGreSQL's ``COPY FROM`` instruction
+to create several entities in a single query. This brings tremendous 
+performance improvements with respect to the RQL-based data insertion
+procedures.
+
+However, the API of this store is slightly different from the API of
+the stores in CubicWeb's ``dataimport`` module.
+
+Before using the store, one has to import the ``dataio`` cube's 
+``dataimport`` module, then initialize the store by giving it the
+``session`` parameter::
+
+    from cubes.dataio import dataimport as mcwdi
+    ...
+
+    store = mcwdi.MassiveObjectStore(session)
+
+The ``MassiveObjectStore`` provides six methods for inserting data
+into the database:
+
+#. ``init_rtype_table(SubjEtype, r_type, ObjEtype)``, which specifies the
+   creation of the tables associated to the relation types in the database.
+   Each such table has three column, the type of the subject entity, the
+   type of the relation (that is, the name of the attribute in the subject
+   entity which is defined via the relation), and the type of the object
+   entity. For example::
+
+        store.init_rtype_table('Person', 'lives_in', 'Location')
+
+   Please note that these tables can be created before the entities, since
+   they only specify their types, not their unique identifiers.
+
+#. ``create_entity(Etype, **attributes)``, which allows us to add new entities,
+   whose attributes are given in the ``attributes`` dictionary. 
+   Please note however that, by default, this method does *not* return 
+   the created entity. The method is called, for example, as in::
+
+        store.create_entity('Person', name='Toto', age='18', height='190', 
+                            uri='http://link/to/person/toto_18_190')
+        store.create_entity('Location', town='Paris', arrondissement='13',
+                            uri='http://link/to/location/paris_13')
+   
+   In order to be able to link these entities via the relations when needed,
+   we must provide ourselves a means for uniquely identifying the entities.
+   In general, this is done via URIs, stored in attributes like ``uri`` or
+   ``cwuri``. The name of the attribute is irrelevant as long as its value is
+   unique for each entity.
+
+#. ``relate_by_iid(subject_iid, r_type, object_iid)`` allows us to actually 
+   relate the entities uniquely identified by ``subject_iid`` and 
+   ``object_iid`` via a relation of type ``r_type``. For example::
+
+        store.relate_by_iid('http://link/to/person/toto_18_190',
+                            'lives_in',
+                            'http://link/to/location/paris_13')
+
+   Please note that this method does *not* work for inlined relations!
+
+#. ``convert_relations(SubjEtype, r_type, ObjEtype, subj_iid_attribute,
+   obj_iid_attribute)``
+   allows us to actually insert
+   the relations in the database. At one call of this method, one inserts
+   all the relations of type ``rtype`` between entities of given types.
+   ``subj_iid_attribute`` and ``object_iid_attribute`` are the names
+   of the attributes which store the unique identifiers of the entities,
+   as assigned by the user. These names can be identical, as long as
+   their values are unique. For example, for inserting all relations
+   of type ``lives_in`` between ``People`` and ``Location`` entities,
+   we write::
+        
+        store.convert_relations('Person', 'lives_in', 'Location', 'uri', 'uri')
+
+#. ``flush()`` performs the actual commit in the database. It only needs 
+   to be called after ``create_entity`` and ``relate_by_iid`` calls. 
+   Please note that ``relate_by_iid`` does *not* perform insertions into
+   the database, hence calling ``flush()`` for it would have no effect.
+
+#. ``cleanup()`` performs database cleanups, by removing temporary tables.
+   It should only be called at the end of the import.
+
+
+
+.. XXX to add smth on the store's parameter initialization.
+
+
+
+Application to the Diseasome data
++++++++++++++++++++++++++++++++++
+
+Import setup
+############
+
+We define an import function, ``diseasome_import``, which does basically four things:
+
+#. creates and initializes the store to be used, via a line such as::
+    
+        store = cwdi.SQLGenObjectStore(session)
+   
+   where ``cwdi`` is the imported ``cubicweb.dataimport`` or 
+   ``cubes.dataio.dataimport``.
+
+#. calls the diseasome parser, that is, the ``entities_from_rdf`` function in the 
+   ``diseasome_parser`` module and iterates on its result, in a line such as::
+        
+        for entity, relations in parser.entities_from_rdf(filename, ('gene', 'disease')):
+        
+   where ``parser`` is the imported ``diseasome_parser`` module, and ``filename`` is the 
+   name of the file containing the data (with its path), e.g. ``../data/diseasome_dump.nt``.
+
+#. creates the entities to be inserted in the database; for Diseasome, there are two 
+   kinds of entities:
+   
+   #. entities defined in the data model, viz. ``Gene`` and ``Disease`` in our case.
+   #. entities which are built in CubicWeb / Yams, viz. ``ExternalUri`` which define
+      URIs.
+   
+   As we are working with RDF data, each entity is defined through a series of URIs. Hence,
+   each "relational attribute" [#]_ of an entity is defined via an URI, that is, in CubicWeb
+   terms, via an ``ExternalUri`` entity. The entities are created, in the loop presented above,
+   as such::
+        
+        ent = store.create_entity(etype, **entity)
+        
+   where ``etype`` is the appropriate entity type, either ``Gene`` or ``Disease``.
+
+.. [#] By "relational attribute" we denote an attribute (of an entity) which
+       is defined through a relation, e.g. the ``chromosomal_location`` attribute
+       of ``Disease`` entities, which is defined through a relation between a
+       ``Disease`` and an ``ExternalUri``.
+   
+   The ``ExternalUri`` entities are as many as URIs in the data file. For them, we define a unique
+   attribute, ``uri``, which holds the URI under discussion::
+        
+        extu = store.create_entity('ExternalUri', uri="http://path/of/the/uri")
+
+#. creates the relations between the entities. We have relations between:
+   
+   #. entities defined in the schema, e.g. between ``Disease`` and ``Gene``
+      entities, such as the ``associated_genes`` relation defined for 
+      ``Disease`` entities.
+   #. entities defined in the schema and ``ExternalUri`` entities, such as ``gene_id``.
+   
+   The way relations are added to the database depends on the store: 
+   
+   - for the stores in the CubicWeb ``dataimport`` module, we only use 
+     ``store.relate``, in 
+     another loop, on the relations (that is, a 
+     loop inside the preceding one, mentioned at step 2)::
+        
+        for rtype, rels in relations.iteritems():
+            ...
+            
+            store.relate(ent.eid(), rtype, extu.eid(), **kwargs)
+        
+     where ``kwargs`` is a dictionary designed to accommodate the need for specifying
+     the type of the subject entity of the relation, when the relation is inlined and
+     ``SQLGenObjectStore`` is used. For example::
+            
+            ...
+            store.relate(ent.eid(), 'chromosomal_location', extu.eid(), subjtype='Disease')
+   
+   - for the ``MassiveObjectStore`` in the ``dataio`` cube's ``dataimport`` module, 
+     the relations are created in three steps:
+     
+     #. first, a table is created for each relation type, as in::
+            
+            ...
+            store.init_rtype_table(ent.cw_etype, rtype, extu.cw_etype)
+            
+        which comes down to lines such as::
+            
+            store.init_rtype_table('Disease', 'associated_genes', 'Gene')
+            store.init_rtype_table('Gene', 'gene_id', 'ExternalUri')
+            
+     #. second, the URI of each entity will be used as its identifier, in the 
+        ``relate_by_iid`` method, such as::
+            
+            disease_uri = 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/3'
+            gene_uri = '<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HSD3B2'
+            store.relate_by_iid(disease_uri, 'associated_genes', gene_uri)
+            
+     #. third, the relations for each relation type will be added to the database, 
+        via the ``convert_relations`` method, such as in::
+            
+            store.convert_relations('Disease', 'associated_genes', 'Gene', 'cwuri', 'cwuri')
+            
+        and::
+            
+            store.convert_relations('Gene', 'hgnc_id', 'ExternalUri', 'cwuri', 'uri')
+            
+        where ``cwuri`` and ``uri`` are the attributes which store the URIs of the entities
+        defined in the data model, and of the ``ExternalUri`` entities, respectively.
+
+#. flushes all relations and entities::
+    
+    store.flush()
+
+   which performs the actual commit of the inserted entities and relations in the database.
+
+If the ``MassiveObjectStore`` is used, then a cleanup of temporary SQL tables should be performed
+at the end of the import::
+
+    store.cleanup()
+
+Timing benchmarks
+#################
+
+In order to time the import script, we just decorate the import function with the ``timed``
+decorator::
+    
+    from logilab.common.decorators import timed
+    ...
+    
+    @timed
+    def diseasome_import(session, filename):
+        ...
+
+After running the import function as shown in the "Importing the data" section, we obtain two time measurements::
+
+    diseasome_import clock: ... / time: ...
+
+Here, the meanings of these measurements are [#]_:
+
+- ``clock`` is the time spent by CubicWeb, on the server side (i.e. hooks and data pre- / post-processing on SQL 
+  queries),
+
+- ``time`` is the sum between ``clock`` and the time spent in PostGreSQL.
+
+.. [#] The meanings of the ``clock`` and ``time`` measurements, when using the ``@timed``
+       decorators, were taken from `a blog post on massive data import in CubicWeb`_.
+
+.. _a blog post on massive data import in CubicWeb: http://www.cubicweb.org/blogentry/2116712
+
+The import function is put in an import module, named ``diseasome_import`` here. The module is called
+directly from the CubicWeb shell, as follows::
+
+    cubicweb-ctl shell diseasome_instance diseasome_import.py \
+    -- -df diseasome_import_file.nt -st StoreName
+
+The module accepts two arguments:
+
+- the data file, introduced by ``-df [--datafile]``, and
+- the store, introduced by ``-st [--store]``.
+
+The timings (in seconds) for different stores are given in the following table, for 
+importing 4213 ``Disease`` entities and 3919 ``Gene`` entities with the import module
+just described:
+
++--------------------------+------------------------+--------------------------------+------------+
+| Store                    | CubicWeb time (clock)  | PostGreSQL time (time - clock) | Total time |
++==========================+========================+================================+============+
+| ``RQLObjectStore``       | 225.98                 | 62.05                          | 288.03     |
++--------------------------+------------------------+--------------------------------+------------+
+| ``NoHookRQLObjectStore`` | 62.73                  | 51.38                          | 114.11     |
++--------------------------+------------------------+--------------------------------+------------+
+| ``SQLGenObjectStore``    | 20.41                  | 11.03                          | 31.44      |
++--------------------------+------------------------+--------------------------------+------------+
+| ``MassiveObjectStore``   | 4.84                   | 6.93                           | 11.77      |
++--------------------------+------------------------+--------------------------------+------------+
+
+
+Conclusions
+~~~~~~~~~~~
+
+In this tutorial we have seen how to import data in a CubicWeb application instance. We have first seen how to
+create a schema, then how to create a parser of the data and a mapping of the data to the schema.
+Finally, we have seen four ways of importing data into CubicWeb.
+
+Three of those are integrated into CubicWeb, namely the ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
+``SQLGenObjectStore`` stores, which have a common API:
+
+- ``RQLObjectStore`` is by far the slowest, especially its time spent on the 
+  CubicWeb side, and so it should be used only for small amounts of 
+  "sensitive" data (i.e. where security is a concern).
+
+- ``NoHookRQLObjectStore`` slashes by almost four the time spent on the CubicWeb side, 
+  but is also quite slow; on the PostGres side it is as slow as the previous store. 
+  It should be used for data where security is not a concern,
+  but consistency (with the data model) is.
+
+- ``SQLGenObjectStore`` slashes by three the time spent on the CubicWeb side and by five the time 
+  spent on the PostGreSQL side. It should be used for relatively great amounts of data, where
+  security and data consistency are not a concern. Compared to the previous store, it has the
+  disadvantage that, for inlined relations, we must specify their subjects' types.
+
+For really huge amounts of data there is a fourth store, ``MassiveObjectStore``, available
+from the ``dataio`` cube. It provides a blazing performance with respect to all other stores:
+it is almost 25 times faster than ``RQLObjectStore`` and almost three times faster than 
+``SQLGenObjectStore``. However, it has a few usage caveats that should be taken into account:
+
+#. it cannot insert relations defined as inlined in the schema,
+#. no security or consistency check is performed on the data,
+#. its API is slightly different from the other stores.
+
+Hence, this store should be used when security and data consistency are not a concern,
+and there are no inlined relations in the schema.
+
+
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,18 @@
+Tutorials
+~~~~~~~~~
+
+We present a few tutorials of different levels. The blog building
+tutorial introduces one smoothly to the basic concepts.
+
+Then there is a photo gallery construction tutorial which highlights
+more advanced concepts such as unit tests, security settings,
+migration scripts.
+
+.. toctree::
+   :maxdepth: 1
+
+   base/index
+   advanced/index
+   tools/windmill.rst
+   textreports/index
+   dataimport/index
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/textreports/index.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,13 @@
+.. -*- coding: utf-8 -*-
+
+Writing text reports with RestructuredText
+==========================================
+
+|cubicweb| offers several text formats for the RichString type used in schemas,
+including restructuredtext.
+
+Three additional restructuredtext roles are defined by |cubicweb|:
+
+.. autofunction:: cubicweb.ext.rest.eid_reference_role
+.. autofunction:: cubicweb.ext.rest.rql_role
+.. autofunction:: cubicweb.ext.rest.bookmark_role
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/tools/windmill.rst	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,229 @@
+==========================
+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::
+
+    $ 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>
+
+And enter python commands:
+
+.. 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:
+
+.. sourcecode:: python
+
+    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
+:attr:`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.
+
+
--- a/entities/__init__.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/entities/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -53,7 +53,7 @@
         """
         restrictions = ['X is %s' % cls.__regid__]
         selected = ['X']
-        for attrschema in cls.e_schema.indexable_attributes():
+        for attrschema in sorted(cls.e_schema.indexable_attributes()):
             varname = attrschema.type.upper()
             restrictions.append('X %s %s' % (attrschema, varname))
             selected.append(varname)
--- a/entities/adapters.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/entities/adapters.py	Fri Oct 09 17:52:14 2015 +0200
@@ -24,11 +24,13 @@
 
 from itertools import chain
 from warnings import warn
+from hashlib import md5
 
 from logilab.mtconverter import TransformError
 from logilab.common.decorators import cached
 
-from cubicweb import ValidationError, view
+from cubicweb import ValidationError, view, ViolatedConstraint
+from cubicweb.schema import CONSTRAINTS
 from cubicweb.predicates import is_instance, relation_possible, match_exception
 
 
@@ -79,6 +81,8 @@
         itree = self.entity.cw_adapt_to('ITree')
         if itree is not None:
             return itree.path()[:-1]
+        if view.msgid_timestamp:
+            return (self.entity.eid,)
         return ()
 
 
@@ -139,7 +143,7 @@
                 continue
             weight = self.attr_weight.get(rschema, 'C')
             try:
-                value = entity.printable_value(rschema, format='text/plain')
+                value = entity.printable_value(rschema, format=u'text/plain')
             except TransformError:
                 continue
             except Exception:
@@ -370,3 +374,25 @@
             i18nvalues.append(rtype + '-rtype')
         errors[''] = _('some relations violate a unicity constraint')
         raise ValidationError(self.entity.eid, errors, msgargs=msgargs, i18nvalues=i18nvalues)
+
+
+class IUserFriendlyCheckConstraint(IUserFriendlyError):
+    __select__ = match_exception(ViolatedConstraint)
+
+    def raise_user_exception(self):
+        _ = self._cw._
+        cstrname = self.exc.cstrname
+        eschema = self.entity.e_schema
+        for rschema, attrschema in eschema.attribute_definitions():
+            rdef = rschema.rdef(eschema, attrschema)
+            for constraint in rdef.constraints:
+                if cstrname == 'cstr' + md5(eschema.type + rschema.type + constraint.type() + (constraint.serialize() or '')).hexdigest():
+                    break
+            else:
+                continue
+            break
+        else:
+            assert 0
+        key = rschema.type + '-subject'
+        msg, args = constraint.failed_message(key, self.entity.cw_edited[rschema.type])
+        raise ValidationError(self.entity.eid, {key: msg}, args)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+docutils
--- a/entities/test/unittest_base.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/entities/test/unittest_base.py	Fri Oct 09 17:52:14 2015 +0200
@@ -66,8 +66,8 @@
     def test_fti_rql_method(self):
         with self.admin_access.web_request() as req:
             eclass = self.vreg['etypes'].etype_class('EmailAddress')
-            self.assertEqual(['Any X, ALIAS, ADDRESS WHERE X is EmailAddress, '
-                              'X alias ALIAS, X address ADDRESS'],
+            self.assertEqual(['Any X, ADDRESS, ALIAS WHERE X is EmailAddress, '
+                              'X address ADDRESS, X alias ALIAS'],
                              eclass.cw_fti_index_rql_queries(req))
 
 
--- a/entities/test/unittest_wfobjs.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/entities/test/unittest_wfobjs.py	Fri Oct 09 17:52:14 2015 +0200
@@ -87,12 +87,12 @@
             shell.rollback()
             # no pb if not in the same workflow
             wf2 = add_wf(shell, 'Company')
-            foo = wf.add_state(u'foo', initial=True)
-            bar = wf.add_state(u'bar')
-            wf.add_transition(u'baz', (foo,), bar, ('managers',))
+            foo = wf2.add_state(u'foo', initial=True)
+            bar = wf2.add_state(u'bar')
+            wf2.add_transition(u'baz', (foo,), bar, ('managers',))
             shell.commit()
             # gnark gnark
-            biz = wf.add_transition(u'biz', (bar,), foo)
+            biz = wf2.add_transition(u'biz', (bar,), foo)
             shell.commit()
             with self.assertRaises(ValidationError) as cm:
                 biz.cw_set(name=u'baz')
@@ -416,6 +416,32 @@
                 group.cw_clear_all_caches()
                 self.assertEqual(iworkflowable.state, nextstate)
 
+    def test_replace_state(self):
+        with self.admin_access.shell() as shell:
+            wf = add_wf(shell, 'CWGroup', name='groupwf', default=True)
+            s_new = wf.add_state('new', initial=True)
+            s_state1 = wf.add_state('state1')
+            wf.add_transition('tr', (s_new,), s_state1)
+            shell.commit()
+
+        with self.admin_access.repo_cnx() as cnx:
+            group = cnx.create_entity('CWGroup', name=u'grp1')
+            cnx.commit()
+
+            iwf = group.cw_adapt_to('IWorkflowable')
+            iwf.fire_transition('tr')
+            cnx.commit()
+            group.cw_clear_all_caches()
+
+            wf = cnx.entity_from_eid(wf.eid)
+            wf.add_state('state2')
+            with cnx.security_enabled(write=False):
+                wf.replace_state('state1', 'state2')
+            cnx.commit()
+
+            self.assertEqual(iwf.state, 'state2')
+            self.assertEqual(iwf.latest_trinfo().to_state[0].name, 'state2')
+
 
 class CustomWorkflowTC(CubicWebTC):
 
@@ -569,8 +595,9 @@
         with self.admin_access.web_request() as req:
             user = self.create_user(req, 'member', surname=u'toto')
             req.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
-                     {'wf': wf.eid, 'x': user.eid})
+                        {'wf': wf.eid, 'x': user.eid})
             req.cnx.commit()
+            user.cw_clear_all_caches()
             iworkflowable = user.cw_adapt_to('IWorkflowable')
             self.assertEqual(iworkflowable.state, 'dead')
 
--- a/entities/wfobjs.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/entities/wfobjs.py	Fri Oct 09 17:52:14 2015 +0200
@@ -174,12 +174,14 @@
             todelstate = self.state_by_name(todelstate)
         if not hasattr(replacement, 'eid'):
             replacement = self.state_by_name(replacement)
+        args = {'os': todelstate.eid, 'ns': replacement.eid}
         execute = self._cw.execute
-        execute('SET X in_state S WHERE S eid %(s)s', {'s': todelstate.eid})
-        execute('SET X from_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s',
-                {'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})
+        execute('SET X in_state NS WHERE X in_state OS, '
+                'NS eid %(ns)s, OS eid %(os)s', args)
+        execute('SET X from_state NS WHERE X from_state OS, '
+                'OS eid %(os)s, NS eid %(ns)s', args)
+        execute('SET X to_state NS WHERE X to_state OS, '
+                'OS eid %(os)s, NS eid %(ns)s', args)
         todelstate.cw_delete()
 
 
@@ -412,7 +414,7 @@
         """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.cw_etype})
+                                  'ET name %(et)s', {'et': unicode(self.entity.cw_etype)})
         if wfrset:
             return wfrset.get_entity(0, 0)
         self.warning("can't find any workflow for %s", self.entity.cw_etype)
--- a/entity.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/entity.py	Fri Oct 09 17:52:14 2015 +0200
@@ -336,7 +336,7 @@
         else:
             visited.add(eschema.type)
         _fetchattrs = []
-        for attr in fetchattrs:
+        for attr in sorted(fetchattrs):
             try:
                 rschema = eschema.subjrels[attr]
             except KeyError:
@@ -425,7 +425,7 @@
         else:
             for rschema in cls.e_schema.subject_relations():
                 if (rschema.final
-                    and rschema != 'eid'
+                    and rschema not in ('eid', 'cwuri')
                     and cls.e_schema.has_unique_values(rschema)
                     and cls.e_schema.rdef(rschema.type).cardinality[0] == '1'):
                     mainattr = str(rschema)
@@ -514,7 +514,7 @@
         prefixing the relation name by 'reverse_'. Also, relation values may be
         an entity or eid, a list of entities or eids.
         """
-        rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
+        rql, qargs, pendingrels, _attrcache = cls._cw_build_entity_query(kwargs)
         if rql:
             rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
         else:
@@ -524,7 +524,6 @@
         except IndexError:
             raise Exception('could not create a %r with %r (%r)' %
                             (cls.__regid__, rql, qargs))
-        created._cw_update_attr_cache(attrcache)
         cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
         return created
 
@@ -555,43 +554,6 @@
             return self.eid
         return super(Entity, self).__hash__()
 
-    def _cw_update_attr_cache(self, attrcache):
-        # if context is a repository session, don't consider dont-cache-attrs as
-        # the instance already holds modified values and loosing them could
-        # introduce severe problems
-        trdata = self._cw.transaction_data
-        uncached_attrs = trdata.get('%s.storage-special-process-attrs' % self.eid, set())
-        if self._cw.is_request:
-            uncached_attrs.update(trdata.get('%s.dont-cache-attrs' % self.eid, set()))
-        for attr in uncached_attrs:
-            attrcache.pop(attr, None)
-            self.cw_attr_cache.pop(attr, None)
-        self.cw_attr_cache.update(attrcache)
-
-    def _cw_dont_cache_attribute(self, attr, repo_side=False):
-        """Repository side method called when some attribute has been
-        transformed by a hook, hence original value should not be cached by
-        the client.
-
-        If repo_side is True, this means that the attribute has been
-        transformed by a *storage*, hence the original value should
-        not be cached **by anyone**.
-
-        This only applies to a storage special case where the value
-        specified in creation or update is **not** the value that will
-        be transparently exposed later.
-
-        For example we have a special "fs_importing" mode in BFSS
-        where a file path is given as attribute value and stored as is
-        in the data base. Later access to the attribute will provide
-        the content of the file at the specified path. We do not want
-        the "filepath" value to be cached.
-        """
-        self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
-        if repo_side:
-            trdata = self._cw.transaction_data
-            trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
-
     def __json_encode__(self):
         """custom json dumps hook to dump the entity's eid
         which is not part of dict structure itself
@@ -836,7 +798,6 @@
 
     # data fetching methods ###################################################
 
-    @cached
     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',
@@ -865,7 +826,7 @@
             attr = rschema.type
             if attr == 'eid':
                 continue
-            # password retreival is blocked at the repository server level
+            # password retrieval is blocked at the repository server level
             rdef = rschema.rdef(self.e_schema, attrschema)
             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
                    or (attrschema.type == 'Password' and skip_pwd):
@@ -1329,10 +1290,6 @@
             else:
                 rql += ' WHERE X eid %(x)s'
             self._cw.execute(rql, qargs)
-        # update current local object _after_ the rql query to avoid
-        # interferences between the query execution itself and the cw_edited /
-        # skip_security machinery
-        self._cw_update_attr_cache(attrcache)
         self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
         # XXX update relation cache
 
--- a/etwist/server.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/etwist/server.py	Fri Oct 09 17:52:14 2015 +0200
@@ -24,6 +24,7 @@
 import threading
 from urlparse import urlsplit, urlunsplit
 from cgi import FieldStorage, parse_header
+from cubicweb.statsd_logger import statsd_timeit
 
 from twisted.internet import reactor, task, threads
 from twisted.web import http, server
@@ -65,14 +66,6 @@
         # when we have an in-memory repository, clean unused sessions every XX
         # seconds and properly shutdown the server
         if config['repository-uri'] == 'inmemory://':
-            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
-                # requests
-                self.appli.repo.warning('remote repository access through pyro is deprecated')
-                self.pyro_daemon = self.appli.repo.pyro_register()
-                self.pyro_listen_timeout = 0.02
-                self.appli.repo.looping_task(1, self.pyro_loop_event)
             if config.mode != 'test':
                 reactor.addSystemEventTrigger('before', 'shutdown',
                                               self.shutdown_event)
@@ -93,13 +86,6 @@
         """
         self.appli.repo.shutdown()
 
-    def pyro_loop_event(self):
-        """listen for pyro events"""
-        try:
-            self.pyro_daemon.handleRequests(self.pyro_listen_timeout)
-        except select.error:
-            return
-
     def getChild(self, path, request):
         """Indicate which resource to use to process down the URL's path"""
         return self
@@ -118,6 +104,7 @@
             deferred = threads.deferToThread(self.render_request, request)
             return NOT_DONE_YET
 
+    @statsd_timeit
     def render_request(self, request):
         try:
             # processing HUGE files (hundred of megabytes) in http.processReceived
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etwist/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+Twisted
--- a/etwist/twconfig.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/etwist/twconfig.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,9 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """twisted server configurations:
 
-* the "twisted" configuration to get a web instance running in a standalone
-  twisted web server which talk to a repository server using Pyro
-
 * the "all-in-one" configuration to get a web instance running in a twisted
   web server integrating a repository server in the same process (only available
   if the repository part of the software is installed
@@ -82,13 +79,6 @@
 the repository rather than the user running the command',
           'group': 'main', 'level': WebConfiguration.mode == 'system'
           }),
-        ('pyro-server',
-         {'type' : 'yn',
-          # pyro is only a recommends by default, so don't activate it here
-          'default': False,
-          'help': 'run a pyro server',
-          'group': 'main', 'level': 1,
-          }),
         ('webserver-threadpool-size',
          {'type': 'int',
           'default': 4,
@@ -117,9 +107,6 @@
 
         cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
         cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path
-        def pyro_enabled(self):
-            """tell if pyro is activated for the in memory repository"""
-            return self['pyro-server']
 
 
     CONFIGURATIONS.append(AllInOneConfiguration)
--- a/etwist/twctl.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/etwist/twctl.py	Fri Oct 09 17:52:14 2015 +0200
@@ -71,11 +71,14 @@
         cfgname = 'all-in-one'
         subcommand = 'cubicweb-twisted'
 
-    class AllInOneStopHandler(serverctl.RepositoryStopHandler):
+    class AllInOneStopHandler(CommandHandler):
         cmdname = 'stop'
         cfgname = 'all-in-one'
         subcommand = 'cubicweb-twisted'
 
+        def poststop(self):
+            pass
+
     class AllInOneUpgradeHandler(TWUpgradeHandler):
         cfgname = 'all-in-one'
 
--- a/ext/rest.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/ext/rest.py	Fri Oct 09 17:52:14 2015 +0200
@@ -34,7 +34,6 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cStringIO import StringIO
 from itertools import chain
 from logging import getLogger
 from os.path import join
@@ -405,7 +404,7 @@
         # remove unprintable characters unauthorized in xml
         data = data.translate(ESC_CAR_TABLE)
     settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
-                'warning_stream': StringIO(),
+                'warning_stream': False,
                 'traceback': True, # don't sys.exit
                 'stylesheet': None, # don't try to embed stylesheet (may cause
                                     # obscure bug due to docutils computing
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+docutils
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/logstats.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,59 @@
+# copyright 2014 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/>.
+
+"""looping task for dumping instance's stats in a file
+"""
+
+__docformat__ = "restructuredtext en"
+
+from datetime import datetime
+import json
+
+from cubicweb.server import hook
+
+class LogStatsStartHook(hook.Hook):
+    """register task to regularly dump instance's stats in a file
+
+    data are stored as one json entry per row
+    """
+    __regid__ = 'cubicweb.hook.logstats.start'
+    events = ('server_startup',)
+
+    def __call__(self):
+        interval = self.repo.config.get('logstat-interval', 0)
+        if interval <= 0:
+            return            
+
+        def dump_stats(repo):
+            statsfile = repo.config.get('logstat-file')
+            with repo.internal_cnx() as cnx:
+                stats = cnx.call_service('repo_stats')
+                gcstats = cnx.call_service('repo_gc_stats', nmax=5)
+                
+            allstats = {'resources': stats,
+                        'memory': gcstats,
+                        'timestamp': datetime.utcnow().isoformat(),
+                       }
+            try:
+                with open(statsfile, 'ab') as ofile:
+                    json.dump(allstats, ofile)
+                    ofile.write('\n')
+            except IOError:
+                repo.warning('Cannot open stats file for writing: %s', statsfile)
+                    
+        self.repo.looping_task(interval, dump_stats, self.repo)
--- a/hooks/metadata.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/hooks/metadata.py	Fri Oct 09 17:52:14 2015 +0200
@@ -20,6 +20,7 @@
 __docformat__ = "restructuredtext en"
 
 from datetime import datetime
+from base64 import b64encode
 
 from cubicweb.predicates import is_instance
 from cubicweb.server import hook
@@ -199,17 +200,17 @@
             oldsource = self._cw.entity_from_eid(schange[self.eidfrom])
             entity = self._cw.entity_from_eid(self.eidfrom)
             # we don't want the moved entity to be reimported later.  To
-            # distinguish this state, the trick is to change the associated
-            # record in the 'entities' system table with eid=-eid while leaving
-            # other fields unchanged, and to add a new record with eid=eid,
-            # source='system'. External source will then have consider case
-            # where `extid2eid` return a negative eid as 'this entity was known
-            # but has been moved, ignore it'.
-            self._cw.system_sql('UPDATE entities SET eid=-eid WHERE eid=%(eid)s',
-                                {'eid': self.eidfrom})
+            # distinguish this state, move the record from the 'entities' table
+            # to 'moved_entities'.  External source will then have consider
+            # case where `extid2eid` returns a negative eid as 'this entity was
+            # known but has been moved, ignore it'.
+            extid = self._cw.entity_metas(entity.eid)['extid']
+            assert extid is not None
+            attrs = {'eid': entity.eid, 'extid': b64encode(extid).decode('ascii')}
+            self._cw.system_sql(syssource.sqlgen.insert('moved_entities', attrs), attrs)
             attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': None,
                      'asource': 'system'}
-            self._cw.system_sql(syssource.sqlgen.insert('entities', attrs), attrs)
+            self._cw.system_sql(syssource.sqlgen.update('entities', attrs, ['eid']), attrs)
             # register an operation to update repository/sources caches
             ChangeEntitySourceUpdateCaches(self._cw, entity=entity,
                                            oldsource=oldsource.repo_source,
--- a/hooks/notification.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/hooks/notification.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -165,7 +165,7 @@
 class EntityUpdateHook(NotificationHook):
     __regid__ = 'notifentityupdated'
     __abstract__ = True # do not register by default
-    __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
+    __select__ = NotificationHook.__select__ & hook.issued_from_user_query()
     events = ('before_update_entity',)
     skip_attrs = set()
 
@@ -200,7 +200,7 @@
 
 class SomethingChangedHook(NotificationHook):
     __regid__ = 'supervising'
-    __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
+    __select__ = NotificationHook.__select__ & hook.issued_from_user_query()
     events = ('before_add_relation', 'before_delete_relation',
               'after_add_entity', 'before_update_entity')
 
--- a/hooks/syncschema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/hooks/syncschema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -27,9 +27,10 @@
 _ = unicode
 
 from copy import copy
+from hashlib import md5
 from yams.schema import (BASE_TYPES, BadSchemaDefinition,
                          RelationSchema, RelationDefinitionSchema)
-from yams import buildobjs as ybo, schema2sql as y2sql, convert_default_value
+from yams import buildobjs as ybo, convert_default_value
 
 from logilab.common.decorators import clear_cache
 
@@ -37,7 +38,7 @@
 from cubicweb.predicates import is_instance
 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
-from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server import hook, schemaserial as ss, schema2sql as y2sql
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.hooks.synccomputed import RecomputeAttributeOperation
 
@@ -72,7 +73,7 @@
     table = SQL_PREFIX + etype
     column = SQL_PREFIX + rtype
     try:
-        cnx.system_sql(str('ALTER TABLE %s ADD %s integer' % (table, column)),
+        cnx.system_sql(str('ALTER TABLE %s ADD %s integer REFERENCES entities (eid)' % (table, column)),
                        rollback_on_failure=False)
         cnx.info('added column %s to table %s', column, table)
     except Exception:
@@ -319,8 +320,12 @@
         if 'fulltext_container' in self.values:
             op = UpdateFTIndexOp.get_instance(cnx)
             for subjtype, objtype in rschema.rdefs:
-                op.add_data(subjtype)
-                op.add_data(objtype)
+                if self.values['fulltext_container'] == 'subject':
+                    op.add_data(subjtype)
+                    op.add_data(objtype)
+                else:
+                    op.add_data(objtype)
+                    op.add_data(subjtype)
         # update the in-memory schema first
         self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
         self.rschema.__dict__.update(self.values)
@@ -711,6 +716,10 @@
         elif cstrtype == 'UniqueConstraint':
             syssource.update_rdef_unique(cnx, rdef)
             self.unique_changed = True
+        if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+            cstrname = 'cstr' + md5(rdef.subject.type + rdef.rtype.type + cstrtype +
+                                    (self.oldcstr.serialize() or '')).hexdigest()
+            cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' % (SQL_PREFIX, rdef.subject.type, cstrname))
 
     def revertprecommit_event(self):
         # revert changes on in memory schema
@@ -758,6 +767,16 @@
         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
             syssource.update_rdef_unique(cnx, rdef)
             self.unique_changed = True
+        if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+            if oldcstr is not None:
+                oldcstrname = 'cstr' + md5(rdef.subject.type + rdef.rtype.type + cstrtype +
+                                           (self.oldcstr.serialize() or '')).hexdigest()
+                cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' %
+                               (SQL_PREFIX, rdef.subject.type, oldcstrname))
+            cstrname, check = y2sql.check_constraint(rdef.subject, rdef.object, rdef.rtype.type,
+                    newcstr, syssource.dbhelper, prefix=SQL_PREFIX)
+            cnx.system_sql('ALTER TABLE %s%s ADD CONSTRAINT %s CHECK(%s)' %
+                           (SQL_PREFIX, rdef.subject.type, cstrname, check))
 
 
 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
@@ -1335,6 +1354,7 @@
     We wait after the commit to as the schema in memory is only updated after
     the commit.
     """
+    containercls = list
 
     def postcommit_event(self):
         cnx = self.cnx
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+psycopg2
--- a/hooks/test/unittest_hooks.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/hooks/test/unittest_hooks.py	Fri Oct 09 17:52:14 2015 +0200
@@ -146,20 +146,6 @@
 
 class UserGroupHooksTC(CubicWebTC):
 
-    def test_user_synchronization(self):
-        with self.admin_access.repo_cnx() as cnx:
-            self.create_user(cnx, 'toto', password='hop', commit=False)
-            self.assertRaises(AuthenticationError,
-                              self.repo.connect, u'toto', password='hop')
-            cnx.commit()
-            cnxid = self.repo.connect(u'toto', password='hop')
-            self.assertNotEqual(cnxid, cnx.sessionid)
-            cnx.execute('DELETE CWUser X WHERE X login "toto"')
-            self.repo.execute(cnxid, 'State X')
-            cnx.commit()
-            self.assertRaises(BadConnectionId,
-                              self.repo.execute, cnxid, 'State X')
-
     def test_user_group_synchronization(self):
         with self.admin_access.repo_cnx() as cnx:
             user = cnx.user
--- a/hooks/test/unittest_syncschema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/hooks/test/unittest_syncschema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -21,15 +21,23 @@
 
 from cubicweb import ValidationError, Binary
 from cubicweb.schema import META_RTYPES
+from cubicweb.devtools import startpgcluster, stoppgcluster, PostgresApptestConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.devtools.repotest import schema_eids_idx
 
 
+def setUpModule():
+    startpgcluster(__file__)
+
+
 def tearDownModule(*args):
+    stoppgcluster(__file__)
     del SchemaModificationHooksTC.schema_eids
 
+
 class SchemaModificationHooksTC(CubicWebTC):
+    configcls = PostgresApptestConfiguration
 
     def setUp(self):
         super(SchemaModificationHooksTC, self).setUp()
@@ -38,12 +46,11 @@
 
     def index_exists(self, cnx, etype, attr, unique=False):
         dbhelper = self.repo.system_source.dbhelper
-        with cnx.ensure_cnx_set:
-            sqlcursor = cnx.cnxset.cu
-            return dbhelper.index_exists(sqlcursor,
-                                         SQL_PREFIX + etype,
-                                         SQL_PREFIX + attr,
-                                         unique=unique)
+        sqlcursor = cnx.cnxset.cu
+        return dbhelper.index_exists(sqlcursor,
+                                     SQL_PREFIX + etype,
+                                     SQL_PREFIX + attr,
+                                     unique=unique)
 
     def _set_perms(self, cnx, eid):
         cnx.execute('SET X read_permission G WHERE X eid %(x)s, G is CWGroup',
@@ -296,6 +303,7 @@
             cnx.execute('SET DEF cardinality "11" '
                          'WHERE DEF relation_type RT, DEF from_entity E,'
                          'RT name "surname", E name "CWUser"')
+            cnx.execute('SET U surname "Doe" WHERE U surname NULL')
             cnx.commit()
             # should not be able anymore to add cwuser without surname
             self.assertRaises(ValidationError, self.create_user, cnx, "toto")
--- a/hooks/zmq.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/hooks/zmq.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -50,30 +50,3 @@
         self.repo.app_instances_bus.start()
 
 
-class ZMQRepositoryServerStopHook(hook.Hook):
-    __regid__ = 'zmqrepositoryserverstop'
-    events = ('server_shutdown',)
-
-    def __call__(self):
-        server = getattr(self.repo, 'zmq_repo_server', None)
-        if server:
-            self.repo.zmq_repo_server.quit()
-
-class ZMQRepositoryServerStartHook(hook.Hook):
-    __regid__ = 'zmqrepositoryserverstart'
-    events = ('server_startup',)
-
-    def __call__(self):
-        config = self.repo.config
-        if config.name == 'repository':
-            # start-repository command already starts a zmq repo
-            return
-        address = config.get('zmq-repository-address')
-        if not address:
-            return
-        self.repo.warning('remote access to the repository via zmq/pickle is deprecated')
-        from cubicweb.server import cwzmq
-        self.repo.zmq_repo_server = server = cwzmq.ZMQRepositoryServer(self.repo)
-        server.connect(address)
-        self.repo.threaded_task(server.run)
-
--- a/i18n/de.po	Fri Oct 09 09:40:08 2015 +0200
+++ b/i18n/de.po	Fri Oct 09 17:52:14 2015 +0200
@@ -642,10 +642,6 @@
 msgid "New WorkflowTransition"
 msgstr "Neuer workflow-Ãœbergang"
 
-#, python-format
-msgid "No account? Try public access at %s"
-msgstr "Kein Konto? Zur öffentlichen Website: %s"
-
 msgid "No result matching query"
 msgstr "Ihre Suche ergab keine Treffer."
 
@@ -1132,6 +1128,9 @@
 msgid "add a CWCache"
 msgstr ""
 
+msgid "add a CWComputedRType"
+msgstr ""
+
 msgid "add a CWConstraint"
 msgstr ""
 
@@ -4158,9 +4157,6 @@
 msgid "thursday"
 msgstr "Donnerstag"
 
-msgid "timeline"
-msgstr "Zeitleiste"
-
 msgid "timestamp"
 msgstr "Datum"
 
@@ -4509,7 +4505,7 @@
 msgstr "Wert"
 
 #, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
 msgstr ""
 
 #, python-format
@@ -4517,6 +4513,10 @@
 msgstr ""
 
 #, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr ""
+
+#, python-format
 msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
 msgstr ""
 
@@ -4663,6 +4663,9 @@
 #~ msgid "Browse by category"
 #~ msgstr "nach Kategorien navigieren"
 
+#~ msgid "No account? Try public access at %s"
+#~ msgstr "Kein Konto? Zur öffentlichen Website: %s"
+
 #~ msgid "anonymous"
 #~ msgstr "anonym"
 
@@ -4682,3 +4685,6 @@
 
 #~ msgid "no edited fields specified for entity %s"
 #~ msgstr "kein Eingabefeld spezifiziert Für Entität %s"
+
+#~ msgid "timeline"
+#~ msgstr "Zeitleiste"
--- a/i18n/en.po	Fri Oct 09 09:40:08 2015 +0200
+++ b/i18n/en.po	Fri Oct 09 17:52:14 2015 +0200
@@ -620,10 +620,6 @@
 msgid "New WorkflowTransition"
 msgstr "New workflow-transition"
 
-#, python-format
-msgid "No account? Try public access at %s"
-msgstr ""
-
 msgid "No result matching query"
 msgstr ""
 
@@ -1094,6 +1090,9 @@
 msgid "add a CWCache"
 msgstr ""
 
+msgid "add a CWComputedRType"
+msgstr ""
+
 msgid "add a CWConstraint"
 msgstr ""
 
@@ -4060,9 +4059,6 @@
 msgid "thursday"
 msgstr ""
 
-msgid "timeline"
-msgstr ""
-
 msgid "timestamp"
 msgstr ""
 
@@ -4402,7 +4398,7 @@
 msgstr ""
 
 #, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
 msgstr ""
 
 #, python-format
@@ -4410,6 +4406,10 @@
 msgstr ""
 
 #, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr ""
+
+#, python-format
 msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
 msgstr ""
 
--- a/i18n/es.po	Fri Oct 09 09:40:08 2015 +0200
+++ b/i18n/es.po	Fri Oct 09 17:52:14 2015 +0200
@@ -651,10 +651,6 @@
 msgid "New WorkflowTransition"
 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"
 
@@ -1151,6 +1147,9 @@
 msgid "add a CWCache"
 msgstr ""
 
+msgid "add a CWComputedRType"
+msgstr ""
+
 msgid "add a CWConstraint"
 msgstr ""
 
@@ -4222,9 +4221,6 @@
 msgid "thursday"
 msgstr "Jueves"
 
-msgid "timeline"
-msgstr "Escala de Tiempo"
-
 msgid "timestamp"
 msgstr "Fecha"
 
@@ -4573,14 +4569,18 @@
 msgstr "Vampr"
 
 #, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
-msgstr "El valor %(KEY-value)s debe ser %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
+msgstr ""
 
 #, python-format
 msgid "value %(KEY-value)s must be <= %(KEY-boundary)s"
 msgstr "el valor %(KEY-value)s debe ser <= %(KEY-boundary)s"
 
 #, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr ""
+
+#, python-format
 msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
 msgstr "el valor %(KEY-value)s debe ser >= %(KEY-boundary)s"
 
@@ -4729,6 +4729,9 @@
 #~ msgid "Browse by category"
 #~ msgstr "Busca por categoría"
 
+#~ msgid "No account? Try public access at %s"
+#~ msgstr "No esta registrado? Use el acceso público en %s"
+
 #~ msgid "anonymous"
 #~ msgstr "anónimo"
 
@@ -4765,9 +4768,15 @@
 #~ msgid "no edited fields specified for entity %s"
 #~ msgstr "Ningún campo editable especificado para la entidad %s"
 
+#~ msgid "timeline"
+#~ msgstr "Escala de Tiempo"
+
 #~ msgid "unknown option(s): %s"
 #~ msgstr "opcion(es) desconocida(s): %s"
 
+#~ msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
+#~ msgstr "El valor %(KEY-value)s debe ser %(KEY-op)s %(KEY-boundary)s"
+
 #~ msgid "web sessions without CNX"
 #~ msgstr "sesiones web sin conexión asociada"
 
--- a/i18n/fr.po	Fri Oct 09 09:40:08 2015 +0200
+++ b/i18n/fr.po	Fri Oct 09 17:52:14 2015 +0200
@@ -647,10 +647,6 @@
 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 ne correspond à la requête"
 
@@ -1147,6 +1143,9 @@
 msgid "add a CWCache"
 msgstr ""
 
+msgid "add a CWComputedRType"
+msgstr ""
+
 msgid "add a CWConstraint"
 msgstr ""
 
@@ -4221,9 +4220,6 @@
 msgid "thursday"
 msgstr "jeudi"
 
-msgid "timeline"
-msgstr "échelle de temps"
-
 msgid "timestamp"
 msgstr "date"
 
@@ -4570,18 +4566,22 @@
 msgstr "valeur"
 
 #, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
-msgstr "la valeur %(KEY-value)s n'est pas %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
+msgstr "la valeur %(KEY-value)s doit être strictement inférieure à %(KEY-boundary)s"
 
 #, python-format
 msgid "value %(KEY-value)s must be <= %(KEY-boundary)s"
 msgstr ""
-"la valeur %(KEY-value)s n'est pas inférieure ou égale à %(KEY-boundary)s"
+"la valeur %(KEY-value)s doit être inférieure ou égale à %(KEY-boundary)s"
+
+#, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr "la valeur %(KEY-value)s doit être strictement supérieure à %(KEY-boundary)s"
 
 #, python-format
 msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
 msgstr ""
-"la valeur %(KEY-value)s n'est pas supérieure ou égale à %(KEY-boundary)s"
+"la valeur %(KEY-value)s doit être supérieure ou égale à %(KEY-boundary)s"
 
 msgid "value associated to this key is not editable manually"
 msgstr "la valeur associée à cette clé n'est pas éditable manuellement"
@@ -4723,69 +4723,3 @@
 
 msgid "you should probably delete that property"
 msgstr "vous devriez probablement supprimer cette propriété"
-
-#~ msgid "%s relation should not be in mapped"
-#~ msgstr "la relation %s ne devrait pas ếtre mappé"
-
-#~ msgid "Any"
-#~ msgstr "Tous"
-
-#~ msgid "Browse by category"
-#~ msgstr "Naviguer par catégorie"
-
-#~ msgid "anonymous"
-#~ msgstr "anonyme"
-
-#~ msgid "attribute/relation can't be mapped, only entity and relation types"
-#~ msgstr ""
-#~ "les attributs et relations ne peuvent être mappés, uniquement les types "
-#~ "d'entité et de relation"
-
-#~ msgid "can't connect to source %s, some data may be missing"
-#~ msgstr "ne peut se connecter à la source %s, des données peuvent manquer"
-
-#~ msgid "can't mix dontcross and maycross options"
-#~ msgstr "ne peut mélanger dontcross et maycross options"
-
-#~ msgid "can't mix dontcross and write options"
-#~ msgstr "ne peut mélanger dontcross et write options"
-
-#~ msgid "components_etypenavigation"
-#~ msgstr "filtrage par type"
-
-#~ msgid "components_etypenavigation_description"
-#~ msgstr "permet de filtrer par type d'entité les résultats d'une recherche"
-
-#~ msgid "error while querying source %s, some data may be missing"
-#~ msgstr ""
-#~ "une erreur est survenue en interrogeant %s, il est possible que les\n"
-#~ "données affichées soient incomplètes"
-
-#~ msgid "inlined relation %(rtype)s of %(etype)s should be supported"
-#~ msgstr ""
-#~ "la relation %(rtype)s du type d'entité %(etype)s doit être supportée "
-#~ "('inlined')"
-
-#~ msgid "no edited fields specified for entity %s"
-#~ msgstr "aucun champ à éditer spécifié pour l'entité %s"
-
-#~ msgid "unknown option(s): %s"
-#~ msgstr "option(s) inconnue(s) : %s"
-
-#~ msgid "web sessions without CNX"
-#~ msgstr "sessions web sans connexion associée"
-
-#~ msgid "workflow already has a state of that name"
-#~ msgstr "le workflow a déja un état du même nom"
-
-#~ msgid "workflow already has a transition of that name"
-#~ msgstr "le workflow a déja une transition du même nom"
-
-#~ msgid "you may want to specify something for %s"
-#~ msgstr "vous désirez peut-être spécifié quelque chose pour la relation %s"
-
-#~ msgid ""
-#~ "you should un-inline relation %s which is supported and may be crossed "
-#~ msgstr ""
-#~ "vous devriez enlevé la mise en ligne de la relation %s qui est supportée "
-#~ "et peut-être croisée"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.21.0_Any.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,172 @@
+from cubicweb.schema import PURE_VIRTUAL_RTYPES
+from cubicweb.server.schema2sql import rschema_has_table
+
+
+def add_foreign_keys():
+    source = repo.system_source
+    if not source.dbhelper.alter_column_support:
+        return
+    for rschema in schema.relations():
+        if rschema.inlined:
+            add_foreign_keys_inlined(rschema)
+        elif rschema_has_table(rschema, skip_relations=PURE_VIRTUAL_RTYPES):
+            add_foreign_keys_relation(rschema)
+    for eschema in schema.entities():
+        if eschema.final:
+            continue
+        add_foreign_key_etype(eschema)
+
+
+def add_foreign_keys_relation(rschema):
+    args = {'r': rschema.type}
+    count = sql('SELECT COUNT(*) FROM ('
+                '    SELECT eid_from FROM %(r)s_relation'
+                '  UNION'
+                '    SELECT eid_to FROM %(r)s_relation'
+                '  EXCEPT'
+                '    SELECT eid FROM entities) AS eids' % args,
+                ask_confirm=False)[0][0]
+    if count:
+        print '%s references %d unknown entities, deleting' % (rschema, count)
+        sql('DELETE FROM %(r)s_relation '
+            'WHERE eid_from IN (SELECT eid_from FROM %(r)s_relation EXCEPT SELECT eid FROM entities)' % args)
+        sql('DELETE FROM %(r)s_relation '
+            'WHERE eid_to IN (SELECT eid_to FROM %(r)s_relation EXCEPT SELECT eid FROM entities)' % args)
+
+    args['from_fk'] = '%(r)s_relation_eid_from_fkey' % args
+    args['to_fk'] = '%(r)s_relation_eid_to_fkey' % args
+    args['table'] = '%(r)s_relation' % args
+    if repo.system_source.dbdriver == 'postgres':
+        sql('ALTER TABLE %(table)s DROP CONSTRAINT IF EXISTS %(from_fk)s' % args,
+            ask_confirm=False)
+        sql('ALTER TABLE %(table)s DROP CONSTRAINT IF EXISTS %(to_fk)s' % args,
+            ask_confirm=False)
+    elif repo.system_source.dbdriver.startswith('sqlserver'):
+        sql("IF OBJECT_ID('%(from_fk)s', 'F') IS NOT NULL "
+            "ALTER TABLE %(table)s DROP CONSTRAINT %(from_fk)s" % args,
+            ask_confirm=False)
+        sql("IF OBJECT_ID('%(to_fk)s', 'F') IS NOT NULL "
+            "ALTER TABLE %(table)s DROP CONSTRAINT %(to_fk)s" % args,
+            ask_confirm=False)
+    sql('ALTER TABLE %(table)s ADD CONSTRAINT %(from_fk)s '
+        'FOREIGN KEY (eid_from) REFERENCES entities (eid)' % args,
+        ask_confirm=False)
+    sql('ALTER TABLE %(table)s ADD CONSTRAINT %(to_fk)s '
+        'FOREIGN KEY (eid_to) REFERENCES entities (eid)' % args,
+        ask_confirm=False)
+
+
+def add_foreign_keys_inlined(rschema):
+    for eschema in rschema.subjects():
+        args = {'e': eschema.type, 'r': rschema.type}
+        args['c'] = 'cw_%(e)s_cw_%(r)s_fkey' % args
+
+        if eschema.rdef(rschema).cardinality[0] == '1':
+            broken_eids = sql('SELECT cw_eid FROM cw_%(e)s WHERE cw_%(r)s IS NULL' % args,
+                              ask_confirm=False)
+            if broken_eids:
+                print 'Required relation %(e)s.%(r)s missing' % args
+                args['eids'] = ', '.join(str(eid) for eid, in broken_eids)
+                rql('DELETE %(e)s X WHERE X eid IN (%(eids)s)' % args)
+            broken_eids = sql('SELECT cw_eid FROM cw_%(e)s WHERE cw_%(r)s IN (SELECT cw_%(r)s FROM cw_%(e)s '
+                              'EXCEPT SELECT eid FROM entities)' % args,
+                              ask_confirm=False)
+            if broken_eids:
+                print 'Required relation %(e)s.%(r)s references unknown objects, deleting subject entities' % args
+                args['eids'] = ', '.join(str(eid) for eid, in broken_eids)
+                rql('DELETE %(e)s X WHERE X eid IN (%(eids)s)' % args)
+        else:
+            if sql('SELECT COUNT(*) FROM ('
+                   '    SELECT cw_%(r)s FROM cw_%(e)s WHERE cw_%(r)s IS NOT NULL'
+                   '  EXCEPT'
+                   '    SELECT eid FROM entities) AS eids' % args,
+                   ask_confirm=False)[0][0]:
+                print '%(e)s.%(r)s references unknown entities, deleting relation' % args
+                sql('UPDATE cw_%(e)s SET cw_%(r)s = NULL WHERE cw_%(r)s IS NOT NULL AND cw_%(r)s IN '
+                    '(SELECT cw_%(r)s FROM cw_%(e)s EXCEPT SELECT eid FROM entities)' % args)
+
+        if repo.system_source.dbdriver == 'postgres':
+            sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args,
+                ask_confirm=False)
+        elif repo.system_source.dbdriver.startswith('sqlserver'):
+            sql("IF OBJECT_ID('%(c)s', 'F') IS NOT NULL "
+                "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args,
+                ask_confirm=False)
+        sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s '
+            'FOREIGN KEY (cw_%(r)s) references entities(eid)' % args,
+            ask_confirm=False)
+
+
+def add_foreign_key_etype(eschema):
+    args = {'e': eschema.type}
+    if sql('SELECT COUNT(*) FROM ('
+           '    SELECT cw_eid FROM cw_%(e)s'
+           '  EXCEPT'
+           '    SELECT eid FROM entities) AS eids' % args,
+           ask_confirm=False)[0][0]:
+        print '%(e)s has nonexistent entities, deleting' % args
+        sql('DELETE FROM cw_%(e)s WHERE cw_eid IN '
+            '(SELECT cw_eid FROM cw_%(e)s EXCEPT SELECT eid FROM entities)' % args)
+    args['c'] = 'cw_%(e)s_cw_eid_fkey' % args
+    if repo.system_source.dbdriver == 'postgres':
+        sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args,
+            ask_confirm=False)
+    elif repo.system_source.dbdriver.startswith('sqlserver'):
+        sql("IF OBJECT_ID('%(c)s', 'F') IS NOT NULL "
+            "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args,
+            ask_confirm=False)
+    sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s '
+        'FOREIGN KEY (cw_eid) REFERENCES entities (eid)' % args,
+        ask_confirm=False)
+
+
+add_foreign_keys()
+
+cu = session.cnxset.cu
+helper = repo.system_source.dbhelper
+
+helper.drop_index(cu, 'entities', 'extid', False)
+# don't use create_index because it doesn't work for columns that may be NULL
+# on sqlserver
+for query in helper.sqls_create_multicol_unique_index('entities', ['extid']):
+    cu.execute(query)
+
+if 'moved_entities' not in helper.list_tables(cu):
+    sql('''
+    CREATE TABLE moved_entities (
+      eid INTEGER PRIMARY KEY NOT NULL,
+      extid VARCHAR(256) UNIQUE
+    )
+    ''')
+
+moved_entities = sql('SELECT -eid, extid FROM entities WHERE eid < 0',
+                     ask_confirm=False)
+if moved_entities:
+    cu.executemany('INSERT INTO moved_entities (eid, extid) VALUES (%s, %s)',
+                   moved_entities)
+    sql('DELETE FROM entities WHERE eid < 0')
+
+commit()
+
+sync_schema_props_perms('CWEType')
+
+sync_schema_props_perms('cwuri')
+
+from cubicweb.server.schema2sql import check_constraint
+
+for cwconstraint in rql('Any C WHERE R constrained_by C').entities():
+    cwrdef = cwconstraint.reverse_constrained_by[0]
+    rdef = cwrdef.yams_schema()
+    cstr = rdef.constraint_by_eid(cwconstraint.eid)
+    if cstr.type() not in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+        continue
+    cstrname, check = check_constraint(rdef.subject, rdef.object, rdef.rtype.type,
+            cstr, helper, prefix='cw_')
+    args = {'e': rdef.subject.type, 'c': cstrname, 'v': check}
+    if repo.system_source.dbdriver == 'postgres':
+        sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args)
+    elif repo.system_source.dbdriver.startswith('sqlserver'):
+        sql("IF OBJECT_ID('%(c)s', 'C') IS NOT NULL "
+            "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args)
+    sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s CHECK(%(v)s)' % args)
+commit()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.21.1_Any.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,4 @@
+# re-read ComputedRelation permissions from schema.py now that we're
+# able to serialize them
+for computedrtype in schema.iter_computed_relations():
+    sync_schema_props_perms(computedrtype.type)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.21.2_Any.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,7 @@
+sync_schema_props_perms('cwuri')
+
+helper = repo.system_source.dbhelper
+cu = session.cnxset.cu
+helper.set_null_allowed(cu, 'moved_entities', 'extid', 'VARCHAR(256)', False)
+
+commit()
--- a/misc/migration/bootstrapmigration_repository.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/misc/migration/bootstrapmigration_repository.py	Fri Oct 09 17:52:14 2015 +0200
@@ -57,7 +57,7 @@
     commit()
 
 if applcubicwebversion <= (3, 14, 4) and cubicwebversion >= (3, 14, 4):
-    from yams import schema2sql as y2sql
+    from cubicweb.server import schema2sql as y2sql
     dbhelper = repo.system_source.dbhelper
     rdefdef = schema['CWSource'].rdef('name')
     attrtype = y2sql.type_from_constraints(dbhelper, rdefdef.object, rdefdef.constraints).split()[0]
@@ -434,6 +434,12 @@
 if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0):
     add_cube('card', update_database=False)
 
+
+if applcubicwebversion < (3, 21, 1) and cubicwebversion >= (3, 21, 1):
+    add_relation_definition('CWComputedRType', 'read_permission', 'CWGroup')
+    add_relation_definition('CWComputedRType', 'read_permission', 'RQLExpression')
+
+
 def sync_constraint_types():
     """Make sure the repository knows about all constraint types defined in the code"""
     from cubicweb.schema import CONSTRAINTS
--- a/misc/migration/postcreate.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/misc/migration/postcreate.py	Fri Oct 09 17:52:14 2015 +0200
@@ -38,9 +38,9 @@
 activated = userwf.add_state(_('activated'), initial=True)
 deactivated = userwf.add_state(_('deactivated'))
 userwf.add_transition(_('deactivate'), (activated,), deactivated,
-                      requiredgroups=('managers',))
+                      requiredgroups=(u'managers',))
 userwf.add_transition(_('activate'), (deactivated,), activated,
-                      requiredgroups=('managers',))
+                      requiredgroups=(u'managers',))
 
 # create anonymous user if all-in-one config and anonymous user has been specified
 if hasattr(config, 'anonymous_user'):
@@ -50,7 +50,7 @@
         print 'Hopefully this is not a production instance...'
     elif anonlogin:
         from cubicweb.server import create_user
-        create_user(session, unicode(anonlogin), anonpwd, 'guests')
+        create_user(session, unicode(anonlogin), anonpwd, u'guests')
 
 # need this since we already have at least one user in the database (the default admin)
 for user in rql('Any X WHERE X is CWUser').entities():
@@ -63,7 +63,7 @@
     cfg.input_config(inputlevel=0)
     for section, options in cfg.options_by_section():
         for optname, optdict, value in options:
-            key = '%s.%s' % (section, optname)
+            key = u'%s.%s' % (section, optname)
             default = cfg.option_default(optname, optdict)
             # only record values differing from default
             if value != default:
--- a/misc/scripts/ldapuser2ldapfeed.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/misc/scripts/ldapuser2ldapfeed.py	Fri Oct 09 17:52:14 2015 +0200
@@ -31,8 +31,6 @@
 from cubicweb.server.edition import EditedEntity
 
 
-session.mode = 'write' # hold on the connections set
-
 print '******************** backport entity content ***************************'
 
 todelete = defaultdict(list)
--- a/misc/scripts/pyroforge2datafeed.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/misc/scripts/pyroforge2datafeed.py	Fri Oct 09 17:52:14 2015 +0200
@@ -39,8 +39,6 @@
         ))
 
 
-session.mode = 'write' # hold on the connections set
-
 print '******************** backport entity content ***************************'
 
 from cubicweb.server import debugged
--- a/predicates.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/predicates.py	Fri Oct 09 17:52:14 2015 +0200
@@ -15,171 +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/>.
-""".. _Selectors:
-
-Predicates and selectors
-------------------------
-
-A predicate is a class testing a particular aspect of a context. A selector is
-built by combining existant predicates or even selectors.
-
-Using and combining existant predicates
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can combine predicates using the `&`, `|` and `~` operators.
-
-When two predicates are combined using the `&` operator, it means that
-both should return a positive score. On success, the sum of scores is
-returned.
-
-When two predicates are combined using the `|` operator, it means that
-one of them should return a positive score. On success, the first
-positive score is returned.
-
-You can also "negate" a predicate by precedeing it by the `~` unary operator.
-
-Of course you can use parenthesis to balance expressions.
-
-Example
-~~~~~~~
-
-The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
-the blog entity itself.
-
-To do that, one defines a method on entity classes that returns the
-RSS stream url for a given entity. The default implementation on
-:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
-as base for all others) and a specific implementation on `Blog` will
-do what we want.
-
-But when we have a result set containing several `Blog` entities (or
-different entities), we don't know on which entity to call the
-aforementioned method. In this case, we keep the generic behaviour.
-
-Hence we have two cases here, one for a single-entity rsets, the other for
-multi-entities rsets.
-
-In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
-
-.. sourcecode:: python
-
-  class RSSIconBox(box.Box):
-    ''' just display the RSS icon on uniform result set '''
-    __select__ = box.Box.__select__ & non_final_entity()
-
-It takes into account:
-
-* the inherited selection criteria (one has to look them up in the class
-  hierarchy to know the details)
-
-* :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets
-  containing non final entities (a 'final entity' being synonym for entity
-  attributes type, eg `String`, `Int`, etc)
-
-This matches our second case. Hence we have to provide a specific component for
-the first case:
-
-.. sourcecode:: python
-
-  class EntityRSSIconBox(RSSIconBox):
-    '''just display the RSS icon on uniform result set for a single entity'''
-    __select__ = RSSIconBox.__select__ & one_line_rset()
-
-Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which
-filters result sets of size 1. Thus, on a result set containing multiple
-entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
-selectable. However for a result set with one entity, the `EntityRSSIconBox`
-class will have a higher score than `RSSIconBox`, which is what we wanted.
-
-Of course, once this is done, you have to:
-
-* fill in the call method of `EntityRSSIconBox`
-
-* provide the default implementation of the method returning the RSS stream url
-  on :class:`~cubicweb.entities.AnyEntity`
-
-* redefine this method on `Blog`.
-
-
-When to use selectors?
-~~~~~~~~~~~~~~~~~~~~~~
-
-Selectors are to be used whenever arises the need of dispatching on the shape or
-content of a result set or whatever else context (value in request form params,
-authenticated user groups, etc...). That is, almost all the time.
-
-Here is a quick example:
-
-.. sourcecode:: python
-
-    class UserLink(component.Component):
-	'''if the user is the anonymous user, build a link to login else a link
-	to the connected user object with a logout link
-	'''
-	__regid__ = 'loggeduserlink'
-
-	def call(self):
-	    if self._cw.session.anonymous_session:
-		# display login link
-		...
-	    else:
-		# display a link to the connected user object with a loggout link
-		...
-
-The proper way to implement this with |cubicweb| is two have two different
-classes sharing the same identifier but with different selectors so you'll get
-the correct one according to the context.
-
-.. sourcecode:: python
-
-    class UserLink(component.Component):
-	'''display a link to the connected user object with a loggout link'''
-	__regid__ = 'loggeduserlink'
-	__select__ = component.Component.__select__ & authenticated_user()
-
-	def call(self):
-            # display useractions and siteactions
-	    ...
-
-    class AnonUserLink(component.Component):
-	'''build a link to login'''
-	__regid__ = 'loggeduserlink'
-	__select__ = component.Component.__select__ & anonymous_user()
-
-	def call(self):
-	    # display login link
-            ...
-
-The big advantage, aside readability once you're familiar with the
-system, is that your cube becomes much more easily customizable by
-improving componentization.
-
-
-.. _CustomPredicates:
-
-Defining your own predicates
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. autodocstring:: cubicweb.appobject::objectify_predicate
-
-In other cases, you can take a look at the following abstract base classes:
-
-.. autoclass:: cubicweb.predicates.ExpectedValuePredicate
-.. autoclass:: cubicweb.predicates.EClassPredicate
-.. autoclass:: cubicweb.predicates.EntityPredicate
-
-.. _DebuggingSelectors:
-
-Debugging selection
-~~~~~~~~~~~~~~~~~~~
-
-Once in a while, one needs to understand why a view (or any application object)
-is, or is not selected appropriately. Looking at which predicates fired (or did
-not) is the way. The :class:`logilab.common.registry.traced_selection` context
-manager to help with that, *if you're running your instance in debug mode*.
-
-.. autoclass:: logilab.common.registry.traced_selection
-
+"""Predicate classes
 """
 
 __docformat__ = "restructuredtext en"
@@ -394,7 +230,7 @@
     """
     def __init__(self, *expected, **kwargs):
         assert expected, self
-        if len(expected) == 1 and isinstance(expected[0], set):
+        if len(expected) == 1 and isinstance(expected[0], (set, dict)):
             self.expected = expected[0]
         else:
             self.expected = frozenset(expected)
@@ -409,7 +245,21 @@
 
     def __call__(self, cls, req, **kwargs):
         values = self._values_set(cls, req, **kwargs)
-        matching = len(values & self.expected)
+        if isinstance(values, dict):
+            if isinstance(self.expected, dict):
+                matching = 0
+                for key, expected_value in self.expected.items():
+                    if key in values:
+                        if (isinstance(expected_value, (list, tuple, frozenset, set))
+                            and values[key] in expected_value):
+                            matching += 1
+                        elif values[key] == expected_value:
+                            matching += 1
+            if isinstance(self.expected, (set, frozenset)):
+                values = frozenset(values)
+                matching = len(values & self.expected)
+        else:
+            matching = len(values & self.expected)
         if self.once_is_enough:
             return matching
         if matching == len(self.expected):
@@ -438,7 +288,7 @@
     """
 
     def _values_set(self, cls, req, **kwargs):
-        return frozenset(kwargs)
+        return kwargs
 
 
 class appobject_selectable(Predicate):
@@ -1278,31 +1128,29 @@
 def no_cnx(cls, req, **kwargs):
     """Return 1 if the web session has no connection set. This occurs when
     anonymous access is not allowed and user isn't authenticated.
-
-    May only be used on the web side, not on the data repository side.
     """
     if not req.cnx:
         return 1
     return 0
 
+
 @objectify_predicate
 def authenticated_user(cls, req, **kwargs):
     """Return 1 if the user is authenticated (i.e. not the anonymous user).
-
-    May only be used on the web side, not on the data repository side.
     """
     if req.session.anonymous_session:
         return 0
     return 1
 
 
-# XXX == ~ authenticated_user()
-def anonymous_user():
+@objectify_predicate
+def anonymous_user(cls, req, **kwargs):
     """Return 1 if the user is not authenticated (i.e. is the anonymous user).
+    """
+    if req.session.anonymous_session:
+        return 1
+    return 0
 
-    May only be used on the web side, not on the data repository side.
-    """
-    return ~ authenticated_user()
 
 class match_user_groups(ExpectedValuePredicate):
     """Return a non-zero score if request's user is in at least one of the
@@ -1435,8 +1283,23 @@
     in which case a single matching parameter is enough.
     """
 
+    def __init__(self, *expected, **kwargs):
+        """override default __init__ to allow either named or positional
+        parameters.
+        """
+        if kwargs and expected:
+            raise ValueError("match_form_params() can't be called with both "
+                             "positional and named arguments")
+        if expected:
+            if len(expected) == 1 and not isinstance(expected[0], basestring):
+                raise ValueError("match_form_params() positional arguments "
+                                 "must be strings")
+            super(match_form_params, self).__init__(*expected)
+        else:
+            super(match_form_params, self).__init__(kwargs)
+
     def _values_set(self, cls, req, **kwargs):
-        return frozenset(req.form)
+        return req.form
 
 
 class match_http_method(ExpectedValuePredicate):
--- a/repoapi.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/repoapi.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2013-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2013-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,14 +17,12 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Official API to access the content of a repository
 """
-from logilab.common.deprecation import deprecated
+from logilab.common.deprecation import class_deprecated
 
 from cubicweb.utils import parse_repo_uri
-from cubicweb import ConnectionError, ProgrammingError, AuthenticationError
-from uuid import uuid4
-from contextlib import contextmanager
-from cubicweb.req import RequestSessionBase
-from functools import wraps
+from cubicweb import ConnectionError, AuthenticationError
+from cubicweb.server.session import Connection
+
 
 ### private function for specific method ############################
 
@@ -41,7 +39,7 @@
     loading the repository for a client, eg web server, configuration).
 
     The returned repository may be an in-memory repository or a proxy object
-    using a specific RPC method, depending on the given URI (pyro or zmq).
+    using a specific RPC method, depending on the given URI.
     """
     if uri is None:
         return _get_inmemory_repo(config, vreg)
@@ -52,49 +50,20 @@
         # me may have been called with a dummy 'inmemory://' uri ...
         return _get_inmemory_repo(config, vreg)
 
-    if protocol == 'pyroloc':  # direct connection to the instance
-        from logilab.common.pyro_ext import get_proxy
-        uri = uri.replace('pyroloc', 'PYRO')
-        return get_proxy(uri)
-
-    if protocol == 'pyro':  # connection mediated through the pyro ns
-        from logilab.common.pyro_ext import ns_get_proxy
-        path = appid.strip('/')
-        if not path:
-            raise ConnectionError(
-                "can't find instance name in %s (expected to be the path component)"
-                % uri)
-        if '.' in path:
-            nsgroup, nsid = path.rsplit('.', 1)
-        else:
-            nsgroup = 'cubicweb'
-            nsid = path
-        return ns_get_proxy(nsid, defaultnsgroup=nsgroup, nshost=hostport)
-
-    if protocol.startswith('zmqpickle-'):
-        from cubicweb.zmqclient import ZMQRepositoryClient
-        return ZMQRepositoryClient(uri)
-    else:
-        raise ConnectionError('unknown protocol: `%s`' % protocol)
+    raise ConnectionError('unknown protocol: `%s`' % protocol)
 
 def connect(repo, login, **kwargs):
-    """Take credential and return associated ClientConnection.
-
-    The ClientConnection is associated to a new Session object that will be
-    closed when the ClientConnection is closed.
+    """Take credential and return associated Connection.
 
     raise AuthenticationError if the credential are invalid."""
     sessionid = repo.connect(login, **kwargs)
     session = repo._get_session(sessionid)
     # XXX the autoclose_session should probably be handle on the session directly
     # this is something to consider once we have proper server side Connection.
-    return ClientConnection(session, autoclose_session=True)
+    return Connection(session)
 
 def anonymous_cnx(repo):
-    """return a ClientConnection for Anonymous user.
-
-    The ClientConnection is associated to a new Session object that will be
-    closed when the ClientConnection is closed.
+    """return a Connection for Anonymous user.
 
     raises an AuthenticationError if anonymous usage is not allowed
     """
@@ -105,292 +74,7 @@
     # use vreg's repository cache
     return connect(repo, anon_login, password=anon_password)
 
-def _srv_cnx_func(name):
-    """Decorate ClientConnection method blindly forward to Connection
-    THIS TRANSITIONAL PURPOSE
 
-    will be dropped when we have standalone connection"""
-    def proxy(clt_cnx, *args, **kwargs):
-        # the ``with`` dance is transitional. We do not have Standalone
-        # Connection yet so we use this trick to unsure the session have the
-        # proper cnx loaded. This can be simplified one we have Standalone
-        # Connection object
-        if not clt_cnx._open:
-            raise ProgrammingError('Closed client connection')
-        return getattr(clt_cnx._cnx, name)(*args, **kwargs)
-    return proxy
-
-def _open_only(func):
-    """decorator for ClientConnection method that check it is open"""
-    @wraps(func)
-    def check_open(clt_cnx, *args, **kwargs):
-        if not clt_cnx._open:
-            raise ProgrammingError('Closed client connection')
-        return func(clt_cnx, *args, **kwargs)
-    return check_open
-
-
-class ClientConnection(RequestSessionBase):
-    """A Connection object to be used Client side.
-
-    This object is aimed to be used client side (so potential communication
-    with the repo through RPC) and aims to offer some compatibility with the
-    cubicweb.dbapi.Connection interface.
-
-    The autoclose_session parameter informs the connection that this session
-    has been opened explicitly and only for this client connection. The
-    connection will close the session on exit.
-    """
-    # make exceptions available through the connection object
-    ProgrammingError = ProgrammingError
-    # attributes that may be overriden per connection instance
-    anonymous_connection = False # XXX really needed ?
-    is_repo_in_memory = True # BC, always true
-
-    def __init__(self, session, autoclose_session=False):
-        super(ClientConnection, self).__init__(session.vreg)
-        self._session = session # XXX there is no real reason to keep the
-                                # session around function still using it should
-                                # be rewritten and migrated.
-        self._cnx = None
-        self._open = None
-        self._web_request = False
-        #: cache entities built during the connection
-        self._eid_cache = {}
-        self._set_user(session.user)
-        self._autoclose_session = autoclose_session
-
-    def __enter__(self):
-        assert self._open is None
-        self._open = True
-        self._cnx = self._session.new_cnx()
-        self._cnx.__enter__()
-        self._cnx.ctx_count += 1
-        return self
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        self._open = False
-        self._cnx.ctx_count -= 1
-        self._cnx.__exit__(exc_type, exc_val, exc_tb)
-        self._cnx = None
-        if self._autoclose_session:
-            # we have to call repo.close to ensure the repo properly forgets the
-            # session; calling session.close() is not enough :-(
-            self._session.repo.close(self._session.sessionid)
-
-
-    # begin silly BC
-    @property
-    def _closed(self):
-        return not self._open
-
-    def close(self):
-        if self._open:
-            self.__exit__(None, None, None)
-
-    def __repr__(self):
-        # XXX we probably want to reference the user of the session here
-        if self._open is None:
-            return '<ClientConnection (not open yet)>'
-        elif not self._open:
-            return '<ClientConnection (closed)>'
-        elif self.anonymous_connection:
-            return '<ClientConnection %s (anonymous)>' % self._cnx.connectionid
-        else:
-            return '<ClientConnection %s>' % self._cnx.connectionid
-    # end silly BC
-
-    # Main Connection purpose in life #########################################
-
-    call_service = _srv_cnx_func('call_service')
-
-    @_open_only
-    def execute(self, *args, **kwargs):
-        # the ``with`` dance is transitional. We do not have Standalone
-        # Connection yet so we use this trick to unsure the session have the
-        # proper cnx loaded. This can be simplified one we have Standalone
-        # Connection object
-        rset = self._cnx.execute(*args, **kwargs)
-        rset.req = self
-        return rset
-
-    @_open_only
-    def commit(self, *args, **kwargs):
-        try:
-            return self._cnx.commit(*args, **kwargs)
-        finally:
-            self.drop_entity_cache()
-
-    @_open_only
-    def rollback(self, *args, **kwargs):
-        try:
-            return self._cnx.rollback(*args, **kwargs)
-        finally:
-            self.drop_entity_cache()
-
-    # security #################################################################
-
-    allow_all_hooks_but = _srv_cnx_func('allow_all_hooks_but')
-    deny_all_hooks_but = _srv_cnx_func('deny_all_hooks_but')
-    security_enabled = _srv_cnx_func('security_enabled')
-
-    # direct sql ###############################################################
-
-    system_sql = _srv_cnx_func('system_sql')
-
-    # session data methods #####################################################
-
-    get_shared_data = _srv_cnx_func('get_shared_data')
-    set_shared_data = _srv_cnx_func('set_shared_data')
-
-    @property
-    def transaction_data(self):
-        return self._cnx.transaction_data
-
-    # meta-data accessors ######################################################
-
-    @_open_only
-    def source_defs(self):
-        """Return the definition of sources used by the repository."""
-        return self._session.repo.source_defs()
-
-    @_open_only
-    def get_schema(self):
-        """Return the schema currently used by the repository."""
-        return self._session.repo.source_defs()
-
-    @_open_only
-    def get_option_value(self, option):
-        """Return the value for `option` in the configuration."""
-        return self._session.repo.get_option_value(option)
-
-    entity_metas = _srv_cnx_func('entity_metas')
-    describe = _srv_cnx_func('describe') # XXX deprecated in 3.19
-
-    # undo support ############################################################
-
-    @_open_only
-    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.
-
-        Managers may filter according to user (eid) who has done the transaction
-        using the `ueid` argument. Others will only see their own transactions.
-
-        Additional filtering capabilities is provided by using the following
-        named arguments:
-
-        * `etype` to get only transactions creating/updating/deleting entities
-          of the given type
-
-        * `eid` to get only transactions applied to entity of the given eid
-
-        * `action` to get only transactions doing the given action (action in
-          'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
-          'D'.
-
-        * `public`: when additional filtering is provided, their are by default
-          only searched in 'public' actions, unless a `public` argument is given
-          and set to false.
-        """
-        # the ``with`` dance is transitional. We do not have Standalone
-        # Connection yet so we use this trick to unsure the session have the
-        # proper cnx loaded. This can be simplified one we have Standalone
-        # Connection object
-        source = self._cnx.repo.system_source
-        txinfos = source.undoable_transactions(self._cnx, ueid, **actionfilters)
-        for txinfo in txinfos:
-            txinfo.req = req or self  # XXX mostly wrong
-        return txinfos
-
-    @_open_only
-    def transaction_info(self, txuuid, req=None):
-        """Return transaction object for the given uid.
-
-        raise `NoSuchTransaction` if not found or if session's user is not
-        allowed (eg not in managers group and the transaction doesn't belong to
-        him).
-        """
-        # the ``with`` dance is transitional. We do not have Standalone
-        # Connection yet so we use this trick to unsure the session have the
-        # proper cnx loaded. This can be simplified one we have Standalone
-        # Connection object
-        txinfo = self._cnx.repo.system_source.tx_info(self._cnx, txuuid)
-        if req:
-            txinfo.req = req
-        else:
-            txinfo.cnx = self
-        return txinfo
-
-    @_open_only
-    def transaction_actions(self, txuuid, public=True):
-        """Return an ordered list of action effectued during that transaction.
-
-        If public is true, return only 'public' actions, eg not ones triggered
-        under the cover by hooks, else return all actions.
-
-        raise `NoSuchTransaction` if the transaction is not found or if
-        session's user is not allowed (eg not in managers group and the
-        transaction doesn't belong to him).
-        """
-        # the ``with`` dance is transitional. We do not have Standalone
-        # Connection yet so we use this trick to unsure the session have the
-        # proper cnx loaded. This can be simplified one we have Standalone
-        # Connection object
-        return self._cnx.repo.system_source.tx_actions(self._cnx, txuuid, public)
-
-    @_open_only
-    def undo_transaction(self, txuuid):
-        """Undo the given transaction. Return potential restoration errors.
-
-        raise `NoSuchTransaction` if not found or if session's user is not
-        allowed (eg not in managers group and the transaction doesn't belong to
-        him).
-        """
-        # the ``with`` dance is transitional. We do not have Standalone
-        # Connection yet so we use this trick to unsure the session have the
-        # proper cnx loaded. This can be simplified one we have Standalone
-        # Connection object
-        return self._cnx.repo.system_source.undo_transaction(self._cnx, txuuid)
-
-    # cache management
-
-    def entity_cache(self, eid):
-        return self._eid_cache[eid]
-
-    def set_entity_cache(self, entity):
-        self._eid_cache[entity.eid] = entity
-
-    def cached_entities(self):
-        return self._eid_cache.values()
-
-    def drop_entity_cache(self, eid=None):
-        if eid is None:
-            self._eid_cache = {}
-        else:
-            del self._eid_cache[eid]
-
-    # deprecated stuff
-
-    @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
-    def request(self):
-        return self
-
-    @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
-    def cursor(self):
-        return self
-
-    @property
-    @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
-    def sessionid(self):
-        return self._session.sessionid
-
-    @property
-    @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
-    def connection(self):
-        return self
-
-    @property
-    @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
-    def _repo(self):
-        return self._session.repo
+class ClientConnection(Connection):
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.20] %(cls)s is deprecated, use Connection instead'
--- a/req.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/req.py	Fri Oct 09 17:52:14 2015 +0200
@@ -81,7 +81,6 @@
         A special method is needed to ensure the linked user is linked to the
         connection too.
         """
-        # cnx validity is checked by the call to .user_info
         rset = self.eid_rset(orig_user.eid, 'CWUser')
         user_cls = self.vreg['etypes'].etype_class('CWUser')
         user = user_cls(self, rset, row=0, groups=orig_user.groups,
@@ -357,7 +356,7 @@
         for key, val in sorted(newparams.iteritems()):
             query[key] = (self.url_quote(val),)
         query = '&'.join(u'%s=%s' % (param, value)
-                         for param, values in query.items()
+                         for param, values in sorted(query.items())
                          for value in values)
         return urlunsplit((schema, netloc, path, query, fragment))
 
--- a/rqlrewrite.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/rqlrewrite.py	Fri Oct 09 17:52:14 2015 +0200
@@ -89,7 +89,7 @@
                 mytyperel.r_type = 'is'
                 if len(possibletypes) > 1:
                     node = n.Function('IN')
-                    for etype in possibletypes:
+                    for etype in sorted(possibletypes):
                         node.append(n.Constant(etype, 'etype'))
                 else:
                     etype = iter(possibletypes).next()
--- a/rset.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/rset.py	Fri Oct 09 17:52:14 2015 +0200
@@ -21,13 +21,16 @@
 
 from warnings import warn
 
+from logilab.common import nullobject
 from logilab.common.decorators import cached, clear_cache, copy_cache
-
 from rql import nodes, stmts
 
 from cubicweb import NotAnEntity, NoResultError, MultipleResultsError
 
 
+_MARKER = nullobject()
+
+
 class ResultSet(object):
     """A result set wraps a RQL query result. This object implements
     partially the list protocol to allow direct use as a list of
@@ -362,12 +365,14 @@
         rset.limited = (limit, offset)
         return rset
 
-    def printable_rql(self, encoded=False):
+    def printable_rql(self, encoded=_MARKER):
         """return the result set's origin rql as a string, with arguments
         substitued
         """
+        if encoded is not _MARKER:
+            warn('[3.21] the "encoded" argument is deprecated', DeprecationWarning)
         encoding = self.req.encoding
-        rqlstr = self.syntax_tree().as_string(encoding, self.args)
+        rqlstr = self.syntax_tree().as_string(kwargs=self.args)
         # sounds like we get encoded or unicode string due to a bug in as_string
         if not encoded:
             if isinstance(rqlstr, unicode):
@@ -478,6 +483,7 @@
         #     new attributes found in this resultset ?
         try:
             entity = req.entity_cache(eid)
+            entity._cw = req
         except KeyError:
             pass
         else:
--- a/schema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -32,13 +32,14 @@
 from logilab.common.textutils import splitstrip
 from logilab.common.graph import get_cycles
 
+import yams
 from yams import BadSchemaDefinition, buildobjs as ybo
 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
      RelationDefinitionSchema, PermissionMixIn, role_name
-from yams.constraints import BaseConstraint, FormatConstraint
+from yams.constraints import (BaseConstraint, FormatConstraint, BoundaryConstraint,
+                              IntervalBoundConstraint, StaticVocabularyConstraint)
 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
-                         obsolete as yobsolete, cleanup_sys_modules,
-                         fill_schema_from_namespace)
+                         cleanup_sys_modules, fill_schema_from_namespace)
 
 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
 from rql.analyze import ETypeResolver
@@ -462,6 +463,13 @@
 ybo.DEFAULT_ATTRPERMS['update'] = ('managers', ERQLExpression('U has_update_permission X'))
 ybo.DEFAULT_ATTRPERMS['add'] = ('managers', ERQLExpression('U has_add_permission X'))
 
+# we don't want 'add' or 'delete' permissions on computed relation types
+# (they're hardcoded to '()' on computed relation definitions)
+if 'add' in yams.DEFAULT_COMPUTED_RELPERMS:
+    del yams.DEFAULT_COMPUTED_RELPERMS['add']
+if 'delete' in yams.DEFAULT_COMPUTED_RELPERMS:
+    del yams.DEFAULT_COMPUTED_RELPERMS['delete']
+
 
 PUB_SYSTEM_ENTITY_PERMS = {
     'read':   ('managers', 'users', 'guests',),
@@ -657,7 +665,7 @@
     groups = self.get_groups(action)
     if _cw.user.matching_groups(groups):
         if DBG:
-            print 'check_perm: %r %r: user matches %s' % (action, _self_str, groups)
+            print ('check_perm: %r %r: user matches %s' % (action, _self_str, groups))
         return
     # if 'owners' in allowed groups, check if the user actually owns this
     # object, if so that's enough
@@ -859,7 +867,9 @@
         return ERQLExpression(expression, mainvars, eid)
 
 
-class CubicWebRelationSchema(RelationSchema):
+class CubicWebRelationSchema(PermissionMixIn, RelationSchema):
+    permissions = {}
+    ACTIONS = ()
 
     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
         if rdef is not None:
@@ -870,6 +880,17 @@
             eid = getattr(rdef, 'eid', None)
         self.eid = eid
 
+    def init_computed_relation(self, rdef):
+        self.ACTIONS = ('read',)
+        super(CubicWebRelationSchema, self).init_computed_relation(rdef)
+
+    def advertise_new_add_permission(self):
+        pass
+
+    def check_permission_definitions(self):
+        RelationSchema.check_permission_definitions(self)
+        PermissionMixIn.check_permission_definitions(self)
+
     @property
     def meta(self):
         return self.type in META_RTYPES
@@ -1097,7 +1118,7 @@
                     subjtype, rschema.type, objtype,
                     __permissions__={'add': (),
                                      'delete': (),
-                                     'read': ('managers', 'users', 'guests')})
+                                     'read': rschema.permissions['read']})
                 rdef.infered = True
                 self.add_relation_def(rdef)
 
@@ -1108,6 +1129,12 @@
 
 # additional cw specific constraints ###########################################
 
+# these are implemented as CHECK constraints in sql, don't do the work
+# twice
+StaticVocabularyConstraint.check = lambda *args: True
+IntervalBoundConstraint.check = lambda *args: True
+BoundaryConstraint.check = lambda *args: True
+
 class BaseRQLConstraint(RRQLExpression, BaseConstraint):
     """base class for rql constraints"""
     distinct_query = None
@@ -1396,13 +1423,6 @@
             return self.regular_formats + tuple(NEED_PERM_FORMATS)
     return self.regular_formats
 
-# XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is
-# necessary
-orig_import_erschema = PyFileReader.import_erschema
-def bw_import_erschema(self, ertype, schemamod=None, instantiate=True):
-    return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate)
-PyFileReader.import_erschema = bw_import_erschema
-
 # XXX itou for some Statement methods
 from rql import stmts
 orig_get_etype = stmts.ScopeNode.get_etype
@@ -1424,16 +1444,3 @@
 def bw_set_statement_type(self, etype):
     return orig_set_statement_type(self, bw_normalize_etype(etype))
 stmts.Select.set_statement_type = bw_set_statement_type
-
-# XXX deprecated
-
-from yams.constraints import StaticVocabularyConstraint
-
-RichString = moved('yams.buildobjs', 'RichString')
-
-StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint)
-FormatConstraint = class_moved(FormatConstraint)
-
-PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
-PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
-PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
--- a/schemas/base.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/schemas/base.py	Fri Oct 09 17:52:14 2015 +0200
@@ -23,7 +23,7 @@
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
                             SubjectRelation,
                             String, TZDatetime, Datetime, Password, Interval,
-                            Boolean)
+                            Boolean, UniqueConstraint)
 from cubicweb.schema import (
     RQLConstraint, WorkflowableEntityType, ERQLExpression, RRQLExpression,
     PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS, PUB_SYSTEM_ATTR_PERMS,
@@ -184,7 +184,6 @@
     cardinality = '?*'
 
 
-
 class ExternalUri(EntityType):
     """a URI representing an object in external data store"""
     uri = String(required=True, unique=True, maxsize=256,
--- a/schemas/bootstrap.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/schemas/bootstrap.py	Fri Oct 09 17:52:14 2015 +0200
@@ -38,7 +38,7 @@
     description = RichString(internationalizable=True,
                              description=_('semantic description of this entity type'))
     # necessary to filter using RQL
-    final = Boolean(description=_('automatic'))
+    final = Boolean(default=False, description=_('automatic'))
 
 
 class CWRType(EntityType):
@@ -239,7 +239,7 @@
     """groups allowed to read entities/relations of this type"""
     __permissions__ = PUB_SYSTEM_REL_PERMS
     name = 'read_permission'
-    subject = ('CWEType', 'CWAttribute', 'CWRelation')
+    subject = ('CWEType', 'CWAttribute', 'CWRelation', 'CWComputedRType')
     object = 'CWGroup'
     cardinality = '**'
 
@@ -271,7 +271,7 @@
     """rql expression allowing to read entities/relations of this type"""
     __permissions__ = PUB_SYSTEM_REL_PERMS
     name = 'read_permission'
-    subject = ('CWEType', 'CWAttribute', 'CWRelation')
+    subject = ('CWEType', 'CWAttribute', 'CWRelation', 'CWComputedRType')
     object = 'RQLExpression'
     cardinality = '*?'
     composite = 'subject'
--- a/server/__init__.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -280,7 +280,7 @@
         # sort for eid predicatability as expected in some server tests
         for group in sorted(BASE_GROUPS):
             cnx.create_entity('CWGroup', name=unicode(group))
-        admin = create_user(cnx, login, pwd, 'managers')
+        admin = create_user(cnx, login, pwd, u'managers')
         cnx.execute('SET X owned_by U WHERE X is IN (CWGroup,CWSource), U eid %(u)s',
                         {'u': admin.eid})
         cnx.commit()
--- a/server/checkintegrity.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/checkintegrity.py	Fri Oct 09 17:52:14 2015 +0200
@@ -88,11 +88,10 @@
     # to be updated due to the reindexation
     repo = cnx.repo
     dbhelper = repo.system_source.dbhelper
-    with cnx.ensure_cnx_set:
-        cursor = cnx.cnxset.cu
-        if not dbhelper.has_fti_table(cursor):
-            print 'no text index table'
-            dbhelper.init_fti(cursor)
+    cursor = cnx.cnxset.cu
+    if not dbhelper.has_fti_table(cursor):
+        print 'no text index table'
+        dbhelper.init_fti(cursor)
     repo.system_source.do_fti = True  # ensure full-text indexation is activated
     if etypes is None:
         print 'Reindexing entities'
@@ -208,7 +207,7 @@
                                 '  WHERE cs.eid_from=e.eid AND cs.eid_to=s.cw_eid) '
                                 'ORDER BY e.eid')
     for row in cursor.fetchall():
-        sys.stderr.write(msg % row)
+        sys.stderr.write(msg % tuple(row))
     if fix:
         cnx.system_sql('INSERT INTO is_relation (eid_from, eid_to) '
                            'SELECT e.eid, s.cw_eid FROM entities as e, cw_CWEType as s '
@@ -222,7 +221,7 @@
                                 '  WHERE cs.eid_from=e.eid AND cs.eid_to=s.cw_eid) '
                                 'ORDER BY e.eid')
     for row in cursor.fetchall():
-        sys.stderr.write(msg % row)
+        sys.stderr.write(msg % tuple(row))
     if fix:
         cnx.system_sql('INSERT INTO is_instance_of_relation (eid_from, eid_to) '
                            'SELECT e.eid, s.cw_eid FROM entities as e, cw_CWEType as s '
@@ -400,8 +399,7 @@
         with cnx.security_enabled(read=False, write=False): # ensure no read security
             for check in checks:
                 check_func = globals()['check_%s' % check]
-                with cnx.ensure_cnx_set:
-                    check_func(repo.schema, cnx, eids_cache, fix=fix)
+                check_func(repo.schema, cnx, eids_cache, fix=fix)
         if fix:
             cnx.commit()
         else:
@@ -410,6 +408,5 @@
             print 'WARNING: Diagnostic run, nothing has been corrected'
     if reindex:
         cnx.rollback()
-        with cnx.ensure_cnx_set:
-            reindex_entities(repo.schema, cnx, withpb=withpb)
+        reindex_entities(repo.schema, cnx, withpb=withpb)
         cnx.commit()
--- a/server/cwzmq.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/cwzmq.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2012-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2012-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,8 +17,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/>.
 
-import cPickle
-import traceback
 from threading import Thread
 from logging import getLogger
 
@@ -27,16 +25,10 @@
 import zmq.eventloop.zmqstream
 
 from cubicweb import set_log_methods
-from cubicweb.server.server import QuitEvent, Finished
+
 
 ctx = zmq.Context()
 
-def cwproto_to_zmqaddr(address):
-    """ converts a cw-zmq address (like zmqpickle-tcp://<ip>:<port>)
-    into a proper zmq address (tcp://<ip>:<port>)
-    """
-    assert address.startswith('zmqpickle-'), 'bad protocol string %s' % address
-    return address.split('-', 1)[1] # chop the `zmqpickle-` prefix
 
 class ZMQComm(object):
     """
@@ -134,132 +126,5 @@
         self.ioloop.add_callback(lambda: self.stream.setsockopt(zmq.SUBSCRIBE, topic))
 
 
-class ZMQRepositoryServer(object):
-
-    def __init__(self, repository):
-        """make the repository available as a PyRO object"""
-        self.address = None
-        self.repo = repository
-        self.socket = None
-        self.stream = None
-        self.loop = ioloop.IOLoop()
-
-        # event queue
-        self.events = []
-
-    def connect(self, address):
-        self.address = cwproto_to_zmqaddr(address)
-
-    def run(self):
-        """enter the service loop"""
-        # start repository looping tasks
-        self.socket = ctx.socket(zmq.REP)
-        self.stream = zmq.eventloop.zmqstream.ZMQStream(self.socket, io_loop=self.loop)
-        self.stream.bind(self.address)
-        self.info('ZMQ server bound on: %s', self.address)
-
-        self.stream.on_recv(self.process_cmds)
-
-        try:
-            self.loop.start()
-        except zmq.ZMQError:
-            self.warning('ZMQ event loop killed')
-        self.quit()
-
-    def trigger_events(self):
-        """trigger ready events"""
-        for event in self.events[:]:
-            if event.is_ready():
-                self.info('starting event %s', event)
-                event.fire(self)
-                try:
-                    event.update()
-                except Finished:
-                    self.events.remove(event)
-
-    def process_cmd(self, cmd):
-        """Delegate the given command to the repository.
-
-        ``cmd`` is a list of (method_name, args, kwargs)
-        where ``args`` is a list of positional arguments
-        and ``kwargs`` is a dictionnary of named arguments.
-
-        >>> rset = delegate_to_repo(["execute", [sessionid], {'rql': rql}])
-
-        :note1: ``kwargs`` may be ommited
-
-            >>> rset = delegate_to_repo(["execute", [sessionid, rql]])
-
-        :note2: both ``args`` and ``kwargs`` may be omitted
-
-            >>> schema = delegate_to_repo(["get_schema"])
-            >>> schema = delegate_to_repo("get_schema") # also allowed
-
-        """
-        cmd = cPickle.loads(cmd)
-        if not cmd:
-            raise AttributeError('function name required')
-        if isinstance(cmd, basestring):
-            cmd = [cmd]
-        if len(cmd) < 2:
-            cmd.append(())
-        if len(cmd) < 3:
-            cmd.append({})
-        cmd  = list(cmd) + [(), {}]
-        funcname, args, kwargs = cmd[:3]
-        result = getattr(self.repo, funcname)(*args, **kwargs)
-        return result
-
-    def process_cmds(self, cmds):
-        """Callback intended to be used with ``on_recv``.
-
-        Call ``delegate_to_repo`` on each command and send a pickled of
-        each result recursively.
-
-        Any exception are catched, pickled and sent.
-        """
-        try:
-            for cmd in cmds:
-                result = self.process_cmd(cmd)
-                self.send_data(result)
-        except Exception as exc:
-            traceback.print_exc()
-            self.send_data(exc)
-
-    def send_data(self, data):
-        self.socket.send_pyobj(data)
-
-    def quit(self, shutdown_repo=False):
-        """stop the server"""
-        self.info('Quitting ZMQ server')
-        try:
-            self.loop.add_callback(self.loop.stop)
-            self.stream.on_recv(None)
-            self.stream.close()
-        except Exception as e:
-            print e
-            pass
-        if shutdown_repo and not self.repo.shutting_down:
-            event = QuitEvent()
-            event.fire(self)
-
-    # server utilitities ######################################################
-
-    def install_sig_handlers(self):
-        """install signal handlers"""
-        import signal
-        self.info('installing signal handlers')
-        signal.signal(signal.SIGINT, lambda x, y, s=self: s.quit(shutdown_repo=True))
-        signal.signal(signal.SIGTERM, lambda x, y, s=self: s.quit(shutdown_repo=True))
-
-
-    # these are overridden by set_log_methods below
-    # only defining here to prevent pylint from complaining
-    @classmethod
-    def info(cls, msg, *a, **kw):
-        pass
-
-
 set_log_methods(Publisher, getLogger('cubicweb.zmq.pub'))
 set_log_methods(Subscriber, getLogger('cubicweb.zmq.sub'))
-set_log_methods(ZMQRepositoryServer, getLogger('cubicweb.zmq.repo'))
--- a/server/edition.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/edition.py	Fri Oct 09 17:52:14 2015 +0200
@@ -103,8 +103,6 @@
         assert not self.saved, 'too late to modify edited attributes'
         super(EditedEntity, self).__setitem__(attr, value)
         self.entity.cw_attr_cache[attr] = value
-        # mark attribute as needing purge by the client
-        self.entity._cw_dont_cache_attribute(attr)
 
     def oldnewvalue(self, attr):
         """returns the couple (old attr value, new attr value)
--- a/server/hook.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/hook.py	Fri Oct 09 17:52:14 2015 +0200
@@ -320,6 +320,7 @@
                 eids_from_to = []
             pruned = self.get_pruned_hooks(cnx, event,
                                            entities, eids_from_to, kwargs)
+
             # by default, hooks are executed with security turned off
             with cnx.security_enabled(read=False):
                 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
@@ -327,10 +328,11 @@
                                    key=lambda x: x.order)
                     debug = server.DEBUG & server.DBG_HOOKS
                     with cnx.security_enabled(write=False):
-                        for hook in hooks:
-                            if debug:
-                                print event, _kwargs, hook
-                            hook()
+                        with cnx.running_hooks_ops():
+                            for hook in hooks:
+                                if debug:
+                                    print event, _kwargs, hook
+                                hook()
 
     def get_pruned_hooks(self, cnx, event, entities, eids_from_to, kwargs):
         """return a set of hooks that should not be considered by filtered_possible objects
@@ -425,10 +427,13 @@
     return req.is_hook_activated(cls)
 
 @objectify_predicate
-def from_dbapi_query(cls, req, **kwargs):
-    if req.running_dbapi_query:
-        return 1
-    return 0
+def issued_from_user_query(cls, req, **kwargs):
+    return 0 if req.hooks_in_progress else 1
+
+from_dbapi_query = class_renamed('from_dbapi_query',
+                                 issued_from_user_query,
+                                 message='[3.21] ')
+
 
 class rechain(object):
     def __init__(self, *iterators):
--- a/server/migractions.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/migractions.py	Fri Oct 09 17:52:14 2015 +0200
@@ -44,7 +44,6 @@
 from logilab.common.decorators import cached, clear_cache
 
 from yams.constraints import SizeConstraint
-from yams.schema2sql import eschema2sql, rschema2sql, unique_index_name
 from yams.schema import RelationDefinitionSchema
 
 from cubicweb import CW_SOFTWARE_ROOT, AuthenticationError, ExecutionError
@@ -56,13 +55,11 @@
 from cubicweb import repoapi
 from cubicweb.migration import MigrationHelper, yes
 from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server.schema2sql import eschema2sql, rschema2sql, unique_index_name
 from cubicweb.server.utils import manager_userpasswd
 from cubicweb.server.sqlutils import sqlexec, SQL_PREFIX
 
 
-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')
@@ -94,10 +91,10 @@
             assert repo
             self.cnx = cnx
             self.repo = repo
-            self.session = cnx._session
+            self.session = cnx.session
         elif connect:
             self.repo_connect()
-            self.set_session()
+            self.set_cnx()
         else:
             self.session = None
         # no config on shell to a remote instance
@@ -125,7 +122,9 @@
         self.fs_schema = schema
         self._synchronized = set()
 
-    def set_session(self):
+    # overriden from base MigrationHelper ######################################
+
+    def set_cnx(self):
         try:
             login = self.repo.config.default_admin_config['login']
             pwd = self.repo.config.default_admin_config['password']
@@ -149,9 +148,7 @@
                 print 'aborting...'
                 sys.exit(0)
         self.session = self.repo._get_session(self.cnx.sessionid)
-        self.session.keep_cnxset_mode('transaction')
 
-    # overriden from base MigrationHelper ######################################
 
     @cached
     def repo_connect(self):
@@ -178,15 +175,14 @@
             super(ServerMigrationHelper, self).migrate(vcconf, toupgrade, options)
 
     def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
-        with self.cnx._cnx.ensure_cnx_set:
-            try:
-                return super(ServerMigrationHelper, self).cmd_process_script(
-                      migrscript, funcname, *args, **kwargs)
-            except ExecutionError as err:
-                sys.stderr.write("-> %s\n" % err)
-            except BaseException:
-                self.rollback()
-                raise
+        try:
+            return super(ServerMigrationHelper, self).cmd_process_script(
+                  migrscript, funcname, *args, **kwargs)
+        except ExecutionError as err:
+            sys.stderr.write("-> %s\n" % err)
+        except BaseException:
+            self.rollback()
+            raise
 
     # Adjust docstring
     cmd_process_script.__doc__ = MigrationHelper.cmd_process_script.__doc__
@@ -287,12 +283,10 @@
         print '-> database restored.'
 
     def commit(self):
-        if hasattr(self, 'cnx'):
-            self.cnx.commit(free_cnxset=False)
+        self.cnx.commit()
 
     def rollback(self):
-        if hasattr(self, 'cnx'):
-            self.cnx.rollback(free_cnxset=False)
+        self.cnx.rollback()
 
     def rqlexecall(self, rqliter, ask_confirm=False):
         for rql, kwargs in rqliter:
@@ -310,7 +304,7 @@
                         'schema': self.repo.get_schema(),
                         'cnx': self.cnx,
                         'fsschema': self.fs_schema,
-                        'session' : self.cnx._cnx,
+                        'session' : self.cnx,
                         'repo' : self.repo,
                         })
         return context
@@ -961,7 +955,6 @@
                              % (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( (eid for eid, in self.sqlexec(
                     'SELECT cw_eid FROM cw_%s WHERE cw_from_entity=%%(eid)s OR '
@@ -971,10 +964,10 @@
                 # 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.CleanupDeletedEidsCacheOp.get_instance(session).union(thispending)
+                hook.CleanupDeletedEidsCacheOp.get_instance(self.cnx).union(thispending)
                 # and don't forget to remove record from system tables
                 entities = [self.cnx.entity_from_eid(eid, rdeftype) for eid in thispending]
-                self.repo.system_source.delete_info_multi(self.cnx._cnx, entities)
+                self.repo.system_source.delete_info_multi(self.cnx, entities)
                 self.sqlexec('DELETE FROM cw_%s WHERE cw_from_entity=%%(eid)s OR '
                              'cw_to_entity=%%(eid)s' % rdeftype,
                              {'eid': oldeid}, ask_confirm=False)
@@ -1027,7 +1020,8 @@
             print 'warning: relation type %s is already known, skip addition' % (
                 rtype)
         elif rschema.rule:
-            ss.execschemarql(execute, rschema, ss.crschema2rql(rschema))
+            gmap = self.group_mapping()
+            ss.execschemarql(execute, rschema, ss.crschema2rql(rschema, gmap))
         else:
             # register the relation into CWRType and insert necessary relation
             # definitions
@@ -1086,7 +1080,7 @@
             if not self.confirm('Relation %s is still present in the filesystem schema,'
                                 ' do you really want to drop it?' % oldname,
                                 default='n'):
-                raise SystemExit(1)
+                return
         self.cmd_add_relation_type(newname, commit=True)
         if not self.repo.schema[oldname].rule:
             self.rqlexec('SET X %s Y WHERE X %s Y' % (newname, oldname),
@@ -1284,6 +1278,7 @@
             return 'missing workflow relations, see make_workflowable(%s)' % etype
         for etype in wfof:
             eschema = self.repo.schema[etype]
+            etype = unicode(etype)
             if ensure_workflowable:
                 assert 'in_state' in eschema.subjrels, _missing_wf_rel(etype)
                 assert 'custom_workflow' in eschema.subjrels, _missing_wf_rel(etype)
@@ -1396,7 +1391,7 @@
         indexable entity types
         """
         from cubicweb.server.checkintegrity import reindex_entities
-        reindex_entities(self.repo.schema, self.cnx._cnx, etypes=etypes)
+        reindex_entities(self.repo.schema, self.cnx, etypes=etypes)
 
     @contextmanager
     def cmd_dropped_constraints(self, etype, attrname, cstrtype=None,
@@ -1486,19 +1481,28 @@
         * the actual schema won't be updated until next startup
         """
         rschema = self.repo.schema.rschema(attr)
-        oldtype = rschema.objects(etype)[0]
-        rdefeid = rschema.rdef(etype, oldtype).eid
-        allownull = rschema.rdef(etype, oldtype).cardinality[0] != '1'
+        oldschema = rschema.objects(etype)[0]
+        rdef = rschema.rdef(etype, oldschema)
         sql = ("UPDATE cw_CWAttribute "
                "SET cw_to_entity=(SELECT cw_eid FROM cw_CWEType WHERE cw_name='%s')"
-               "WHERE cw_eid=%s") % (newtype, rdefeid)
+               "WHERE cw_eid=%s") % (newtype, rdef.eid)
         self.sqlexec(sql, ask_confirm=False)
         dbhelper = self.repo.system_source.dbhelper
         sqltype = dbhelper.TYPE_MAPPING[newtype]
-        cursor = self.cnx._cnx.cnxset.cu
-        dbhelper.change_col_type(cursor, 'cw_%s'  % etype, 'cw_%s' % attr, sqltype, allownull)
+        cursor = self.cnx.cnxset.cu
+        allownull = rdef.cardinality[0] != '1'
+        dbhelper.change_col_type(cursor, 'cw_%s' % etype, 'cw_%s' % attr, sqltype, allownull)
         if commit:
             self.commit()
+            # manually update live schema
+            eschema = self.repo.schema[etype]
+            rschema._subj_schemas[eschema].remove(oldschema)
+            rschema._obj_schemas[oldschema].remove(eschema)
+            newschema = self.repo.schema[newtype]
+            rschema._update(eschema, newschema)
+            rdef.object = newschema
+            del rschema.rdefs[(eschema, oldschema)]
+            rschema.rdefs[(eschema, newschema)] = rdef
 
     def cmd_add_entity_type_table(self, etype, commit=True):
         """low level method to create the sql table for an existing entity.
--- a/server/querier.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/querier.py	Fri Oct 09 17:52:14 2015 +0200
@@ -37,6 +37,7 @@
 from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
 from cubicweb.server.edition import EditedEntity
 from cubicweb.server.ssplanner import SSPlanner
+from cubicweb.statsd_logger import statsd_timeit, statsd_c
 
 ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
 
@@ -75,7 +76,7 @@
 
 def check_relations_read_access(cnx, select, args):
     """Raise :exc:`Unauthorized` if the given user doesn't have credentials to
-    read relations used in the givel syntaxt tree
+    read relations used in the given syntax tree
     """
     # use `term_etype` since we've to deal with rewritten constants here,
     # when used as an external source by another repository.
@@ -516,6 +517,7 @@
             return InsertPlan(self, rqlst, args, cnx)
         return ExecutionPlan(self, rqlst, args, cnx)
 
+    @statsd_timeit
     def execute(self, cnx, rql, args=None, build_descr=True):
         """execute a rql query, return resulting rows and their description in
         a `ResultSet` object
@@ -558,8 +560,10 @@
                         return empty_rset(rql, args)
             rqlst = self._rql_cache[cachekey]
             self.cache_hit += 1
+            statsd_c('cache_hit')
         except KeyError:
             self.cache_miss += 1
+            statsd_c('cache_miss')
             rqlst = self.parse(rql)
             try:
                 # compute solutions for rqlst and return named args in query
@@ -570,7 +574,7 @@
             except UnknownEid:
                 # we want queries such as "Any X WHERE X eid 9999" return an
                 # empty result instead of raising UnknownEid
-                return empty_rset(rql, args, rqlst)
+                return empty_rset(rql, args)
             if args and rql not in self._rql_ck_cache:
                 self._rql_ck_cache[rql] = eidkeys
                 if eidkeys:
@@ -580,9 +584,6 @@
         if rqlst.TYPE != 'select':
             if cnx.read_security:
                 check_no_password_selected(rqlst)
-            # write query, ensure connection's mode is 'write' so connections
-            # won't be released until commit/rollback
-            cnx.mode = 'write'
             cachekey = None
         else:
             if cnx.read_security:
--- a/server/repository.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/repository.py	Fri Oct 09 17:52:14 2015 +0200
@@ -24,34 +24,29 @@
 * brings these classes all together to provide a single access
   point to a cubicweb instance.
 * handles session management
-* provides method for pyro registration, to call if pyro is enabled
 """
 __docformat__ = "restructuredtext en"
 
-import sys
 import threading
 import Queue
 from warnings import warn
 from itertools import chain
 from time import time, localtime, strftime
 from contextlib import contextmanager
-from warnings import warn
 
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import deprecated
 
 from yams import BadSchemaDefinition
-from rql import RQLSyntaxError
 from rql.utils import rqlvar_maker
 
 from cubicweb import (CW_MIGRATION_MAP, QueryError,
                       UnknownEid, AuthenticationError, ExecutionError,
-                      BadConnectionId, Unauthorized, ValidationError,
-                      UniqueTogetherError, onevent)
+                      BadConnectionId, ValidationError, Unauthorized,
+                      UniqueTogetherError, onevent, ViolatedConstraint)
 from cubicweb import cwvreg, schema, server
 from cubicweb.server import ShuttingDown, utils, hook, querier, sources
-from cubicweb.server.session import Session, InternalSession, InternalManager
-from cubicweb.server.ssplanner import EditedEntity
+from cubicweb.server.session import Session, InternalManager
 
 NO_CACHE_RELATIONS = set( [('owned_by', 'object'),
                            ('created_by', 'object'),
@@ -59,7 +54,7 @@
                            ])
 
 def prefill_entity_caches(entity):
-    session = entity._cw
+    cnx = entity._cw
     # prefill entity relation caches
     for rschema in entity.e_schema.subject_relations():
         rtype = str(rschema)
@@ -69,14 +64,14 @@
             entity.cw_attr_cache.setdefault(rtype, None)
         else:
             entity.cw_set_relation_cache(rtype, 'subject',
-                                         session.empty_rset())
+                                         cnx.empty_rset())
     for rschema in entity.e_schema.object_relations():
         rtype = str(rschema)
         if rtype in schema.VIRTUAL_RTYPES or (rtype, 'object') in NO_CACHE_RELATIONS:
             continue
-        entity.cw_set_relation_cache(rtype, 'object', session.empty_rset())
+        entity.cw_set_relation_cache(rtype, 'object', cnx.empty_rset())
 
-def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
+def del_existing_rel_if_needed(cnx, eidfrom, rtype, eidto):
     """delete existing relation when adding a new one if card is 1 or ?
 
     have to be done once the new relation has been inserted to avoid having
@@ -86,9 +81,9 @@
     hooks order hazardness
     """
     # skip that if integrity explicitly disabled
-    if not session.is_hook_category_activated('activeintegrity'):
+    if not cnx.is_hook_category_activated('activeintegrity'):
         return
-    rdef = session.rtype_eids_rdef(rtype, eidfrom, eidto)
+    rdef = cnx.rtype_eids_rdef(rtype, eidfrom, eidto)
     card = rdef.cardinality
     # one may be tented to check for neweids but this may cause more than one
     # relation even with '1?'  cardinality if thoses relations are added in the
@@ -102,34 +97,34 @@
     # * we don't want read permissions to be applied but we want delete
     #   permission to be checked
     if card[0] in '1?':
-        with session.security_enabled(read=False):
-            session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
-                            'NOT Y eid %%(y)s' % rtype,
-                                {'x': eidfrom, 'y': eidto})
+        with cnx.security_enabled(read=False):
+            cnx.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 session.security_enabled(read=False):
-            session.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
-                            'NOT X eid %%(x)s' % rtype,
-                            {'x': eidfrom, 'y': eidto})
+        with cnx.security_enabled(read=False):
+            cnx.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
+                        'NOT X eid %%(x)s' % rtype,
+                        {'x': eidfrom, 'y': eidto})
 
 
-def preprocess_inlined_relations(session, entity):
+def preprocess_inlined_relations(cnx, entity):
     """when an entity is added, check if it has some inlined relation which
     requires to be extrated for proper call hooks
     """
     relations = []
-    activeintegrity = session.is_hook_category_activated('activeintegrity')
+    activeintegrity = cnx.is_hook_category_activated('activeintegrity')
     eschema = entity.e_schema
     for attr in entity.cw_edited:
         rschema = eschema.subjrels[attr]
         if not rschema.final: # inlined relation
             value = entity.cw_edited[attr]
             relations.append((attr, value))
-            session.update_rel_cache_add(entity.eid, attr, value)
-            rdef = session.rtype_eids_rdef(attr, entity.eid, value)
+            cnx.update_rel_cache_add(entity.eid, attr, value)
+            rdef = cnx.rtype_eids_rdef(attr, entity.eid, value)
             if rdef.cardinality[1] in '1?' and activeintegrity:
-                with session.security_enabled(read=False):
-                    session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
+                with cnx.security_enabled(read=False):
+                    cnx.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
                                     {'x': entity.eid, 'y': value})
     return relations
 
@@ -151,8 +146,6 @@
 class Repository(object):
     """a repository provides access to a set of persistent storages for
     entities and relations
-
-    XXX protect pyro access
     """
 
     def __init__(self, config, tasks_manager=None, vreg=None):
@@ -162,17 +155,11 @@
         self.vreg = vreg
         self._tasks_manager = tasks_manager
 
-        self.pyro_registered = False
-        self.pyro_uri = None
-        # every pyro client is handled in its own thread; map these threads to
-        # the session we opened for them so we can clean up when they go away
-        self._pyro_sessions = {}
         self.app_instances_bus = NullEventBus()
         self.info('starting repository from %s', self.config.apphome)
         # dictionary of opened sessions
         self._sessions = {}
 
-
         # list of functions to be called at regular interval
         # list of running threads
         self._running_threads = []
@@ -234,7 +221,7 @@
         if config.quick_start or config.creating or not config.read_instance_schema:
             # load schema from the file system
             if not config.creating:
-                self.warning("set fs instance'schema")
+                self.info("set fs instance'schema")
             self.set_schema(config.load_schema(expand_cubes=True))
         else:
             # normal start: load the instance schema from the database
@@ -435,10 +422,6 @@
             except Exception:
                 self.exception('error while closing %s' % cnxset)
                 continue
-        if self.pyro_registered:
-            if self._use_pyrons():
-                pyro_unregister(self.config)
-            self.pyro_uri = None
         hits, misses = self.querier.cache_hit, self.querier.cache_miss
         try:
             self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses,
@@ -461,8 +444,7 @@
         for source in self.sources_by_uri.itervalues():
             if self.config.source_enabled(source) and source.support_entity('CWUser'):
                 try:
-                    with cnx.ensure_cnx_set:
-                        return source.authenticate(cnx, login, **authinfo)
+                    return source.authenticate(cnx, login, **authinfo)
                 except AuthenticationError:
                     continue
         else:
@@ -481,19 +463,18 @@
 
     def _build_user(self, cnx, eid):
         """return a CWUser entity for user with the given eid"""
-        with cnx.ensure_cnx_set:
-            cls = self.vreg['etypes'].etype_class('CWUser')
-            st = cls.fetch_rqlst(cnx.user, ordermethod=None)
-            st.add_eid_restriction(st.get_variable('X'), 'x', 'Substitute')
-            rset = cnx.execute(st.as_string(), {'x': eid})
-            assert len(rset) == 1, rset
-            cwuser = rset.get_entity(0, 0)
-            # pylint: disable=W0104
-            # prefetch / cache cwuser's groups and properties. This is especially
-            # useful for internal sessions to avoid security insertions
-            cwuser.groups
-            cwuser.properties
-            return cwuser
+        cls = self.vreg['etypes'].etype_class('CWUser')
+        st = cls.fetch_rqlst(cnx.user, ordermethod=None)
+        st.add_eid_restriction(st.get_variable('X'), 'x', 'Substitute')
+        rset = cnx.execute(st.as_string(), {'x': eid})
+        assert len(rset) == 1, rset
+        cwuser = rset.get_entity(0, 0)
+        # pylint: disable=W0104
+        # prefetch / cache cwuser's groups and properties. This is especially
+        # useful for internal sessions to avoid security insertions
+        cwuser.groups
+        cwuser.properties
+        return cwuser
 
     # public (dbapi) interface ################################################
 
@@ -640,14 +621,14 @@
         for k in chain(fetch_attrs, query_attrs):
             if k not in cwuserattrs:
                 raise Exception('bad input for find_user')
-        with self.internal_session() as session:
+        with self.internal_cnx() as cnx:
             varmaker = rqlvar_maker()
             vars = [(attr, varmaker.next()) for attr in fetch_attrs]
             rql = 'Any %s WHERE X is CWUser, ' % ','.join(var[1] for var in vars)
             rql += ','.join('X %s %s' % (var[0], var[1]) for var in vars) + ','
-            rset = session.execute(rql + ','.join('X %s %%(%s)s' % (attr, attr)
-                                                  for attr in query_attrs),
-                                   query_attrs)
+            rset = cnx.execute(rql + ','.join('X %s %%(%s)s' % (attr, attr)
+                                              for attr in query_attrs),
+                               query_attrs)
             return rset.rows
 
     def new_session(self, login, **kwargs):
@@ -662,12 +643,6 @@
             # try to get a user object
             user = self.authenticate_user(cnx, login, **kwargs)
         session = Session(user, self, cnxprops)
-        if threading.currentThread() in self._pyro_sessions:
-            # assume no pyro client does one get_repository followed by
-            # multiple repo.connect
-            assert self._pyro_sessions[threading.currentThread()] == None
-            self.debug('record session %s', session)
-            self._pyro_sessions[threading.currentThread()] = session
         user._cw = user.cw_rset.req = session
         user.cw_clear_relation_cache()
         self._sessions[session.sessionid] = session
@@ -683,190 +658,26 @@
         """open a new session for a given user and return its sessionid """
         return self.new_session(login, **kwargs).sessionid
 
-    def execute(self, sessionid, rqlstring, args=None, build_descr=True,
-                txid=None):
-        """execute a RQL query
-
-        * rqlstring should be a unicode string or a plain ascii string
-        * args the optional parameters used in the query
-        * build_descr is a flag indicating if the description should be
-          built on select queries
-        """
-        session = self._get_session(sessionid, setcnxset=True, txid=txid)
-        try:
-            try:
-                rset = self.querier.execute(session, rqlstring, args,
-                                            build_descr)
-                # NOTE: the web front will (re)build it when needed
-                #       e.g in facets
-                #       Zeroed to avoid useless overhead with pyro
-                rset._rqlst = None
-                return rset
-            except (ValidationError, Unauthorized, RQLSyntaxError):
-                raise
-            except Exception:
-                # FIXME: check error to catch internal errors
-                self.exception('unexpected error while executing %s with %s', rqlstring, args)
-                raise
-        finally:
-            session.free_cnxset()
-
-    @deprecated('[3.19] use .entity_metas(sessionid, eid, txid) instead')
-    def describe(self, sessionid, eid, txid=None):
-        """return a tuple `(type, physical source uri, extid, actual source
-        uri)` for the entity of the given `eid`
-
-        As of 3.19, physical source uri is always the system source.
-        """
-        session = self._get_session(sessionid, setcnxset=True, txid=txid)
-        try:
-            etype, extid, source = self.type_and_source_from_eid(eid, session)
-            return etype, source, extid, source
-        finally:
-            session.free_cnxset()
-
-    def entity_metas(self, sessionid, eid, txid=None):
-        """return a dictionary containing meta-datas for the entity of the given
-        `eid`. Available keys are:
-
-        * 'type', the entity's type name,
-
-        * 'source', the name of the source from which this entity's coming from,
-
-        * 'extid', the identifierfor this entity in its originating source, as
-          an encoded string or `None` for entities from the 'system' source.
-        """
-        session = self._get_session(sessionid, setcnxset=True, txid=txid)
-        try:
-            etype, extid, source = self.type_and_source_from_eid(eid, session)
-            return {'type': etype, 'source': source, 'extid': extid}
-        finally:
-            session.free_cnxset()
-
     def check_session(self, sessionid):
         """raise `BadConnectionId` if the connection is no more valid, else
         return its latest activity timestamp.
         """
-        return self._get_session(sessionid, setcnxset=False).timestamp
-
-    @deprecated('[3.19] use session or transaction data')
-    def get_shared_data(self, sessionid, key, default=None, pop=False, txdata=False):
-        """return value associated to key in the session's data dictionary or
-        session's transaction's data if `txdata` is true.
-
-        If pop is True, value will be removed from the dictionary.
-
-        If key isn't defined in the dictionary, value specified by the
-        `default` argument will be returned.
-        """
-        session = self._get_session(sessionid, setcnxset=False)
-        return session.get_shared_data(key, default, pop, txdata)
-
-    @deprecated('[3.19] use session or transaction data')
-    def set_shared_data(self, sessionid, key, value, txdata=False):
-        """set value associated to `key` in shared data
-
-        if `txdata` is true, the value will be added to the repository session's
-        transaction's data which are cleared on commit/rollback of the current
-        transaction.
-        """
-        session = self._get_session(sessionid, setcnxset=False)
-        session.set_shared_data(key, value, txdata)
-
-    def commit(self, sessionid, txid=None):
-        """commit transaction for the session with the given id"""
-        self.debug('begin commit for session %s', sessionid)
-        try:
-            session = self._get_session(sessionid)
-            session.set_cnx(txid)
-            return session.commit()
-        except (ValidationError, Unauthorized):
-            raise
-        except Exception:
-            self.exception('unexpected error')
-            raise
-
-    def rollback(self, sessionid, txid=None):
-        """commit transaction for the session with the given id"""
-        self.debug('begin rollback for session %s', sessionid)
-        try:
-            session = self._get_session(sessionid)
-            session.set_cnx(txid)
-            session.rollback()
-        except Exception:
-            self.exception('unexpected error')
-            raise
+        return self._get_session(sessionid).timestamp
 
     def close(self, sessionid, txid=None, checkshuttingdown=True):
         """close the session with the given id"""
         session = self._get_session(sessionid, txid=txid,
                                     checkshuttingdown=checkshuttingdown)
         # operation uncommited before close are rolled back before hook is called
-        if session._cnx._session_handled:
-            session._cnx.rollback(free_cnxset=False)
         with session.new_cnx() as cnx:
             self.hm.call_hooks('session_close', cnx)
             # commit connection at this point in case write operation has been
             # done during `session_close` hooks
             cnx.commit()
         session.close()
-        if threading.currentThread() in self._pyro_sessions:
-            self._pyro_sessions[threading.currentThread()] = None
         del self._sessions[sessionid]
         self.info('closed session %s for user %s', sessionid, session.user.login)
 
-    def call_service(self, sessionid, regid, **kwargs):
-        """
-        See :class:`cubicweb.dbapi.Connection.call_service`
-        and :class:`cubicweb.server.Service`
-        """
-        # XXX lack a txid
-        session = self._get_session(sessionid)
-        return session._cnx.call_service(regid, **kwargs)
-
-    def user_info(self, sessionid, props=None):
-        """this method should be used by client to:
-        * check session id validity
-        * update user information on each user's request (i.e. groups and
-          custom properties)
-        """
-        user = self._get_session(sessionid, setcnxset=False).user
-        return user.eid, user.login, user.groups, user.properties
-
-    def undoable_transactions(self, sessionid, ueid=None, txid=None,
-                              **actionfilters):
-        """See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
-        session = self._get_session(sessionid, setcnxset=True, txid=txid)
-        try:
-            return self.system_source.undoable_transactions(session, ueid,
-                                                            **actionfilters)
-        finally:
-            session.free_cnxset()
-
-    def transaction_info(self, sessionid, txuuid, txid=None):
-        """See :class:`cubicweb.dbapi.Connection.transaction_info`"""
-        session = self._get_session(sessionid, setcnxset=True, txid=txid)
-        try:
-            return self.system_source.tx_info(session, txuuid)
-        finally:
-            session.free_cnxset()
-
-    def transaction_actions(self, sessionid, txuuid, public=True, txid=None):
-        """See :class:`cubicweb.dbapi.Connection.transaction_actions`"""
-        session = self._get_session(sessionid, setcnxset=True, txid=txid)
-        try:
-            return self.system_source.tx_actions(session, txuuid, public)
-        finally:
-            session.free_cnxset()
-
-    def undo_transaction(self, sessionid, txuuid, txid=None):
-        """See :class:`cubicweb.dbapi.Connection.undo_transaction`"""
-        session = self._get_session(sessionid, setcnxset=True, txid=txid)
-        try:
-            return self.system_source.undo_transaction(session, txuuid)
-        finally:
-            session.free_cnxset()
-
     # session handling ########################################################
 
     def close_sessions(self):
@@ -891,27 +702,6 @@
                 nbclosed += 1
         return nbclosed
 
-    @deprecated("[3.19] use internal_cnx now\n"
-                "(Beware that integrity hook are now enabled by default)")
-    def internal_session(self, cnxprops=None, safe=False):
-        """return a dbapi like connection/cursor using internal user which have
-        every rights on the repository. The `safe` argument is a boolean flag
-        telling if integrity hooks should be activated or not.
-
-        /!\ the safe argument is False by default.
-
-        *YOU HAVE TO* commit/rollback or close (rollback implicitly) the
-        session once the job's done, else you'll leak connections set up to the
-        time where no one is available, causing irremediable freeze...
-        """
-        session = InternalSession(self, cnxprops)
-        if not safe:
-            session.disable_hook_categories('integrity')
-        session.disable_hook_categories('security')
-        session._cnx.ctx_count += 1
-        session.set_cnxset()
-        return session
-
     @contextmanager
     def internal_cnx(self):
         """Context manager returning a Connection using internal user which have
@@ -920,14 +710,14 @@
         Beware that unlike the older :meth:`internal_session`, internal
         connections have all hooks beside security enabled.
         """
-        with InternalSession(self) as session:
+        with Session(InternalManager(), self) as session:
             with session.new_cnx() as cnx:
+                cnx.user._cw = cnx  # XXX remove when "vreg = user._cw.vreg"
+                                    # hack in entity.py is gone
                 with cnx.security_enabled(read=False, write=False):
-                    with cnx.ensure_cnx_set:
-                        yield cnx
+                    yield cnx
 
-    def _get_session(self, sessionid, setcnxset=False, txid=None,
-                     checkshuttingdown=True):
+    def _get_session(self, sessionid, txid=None, checkshuttingdown=True):
         """return the session associated with the given session identifier"""
         if checkshuttingdown and self.shutting_down:
             raise ShuttingDown('Repository is shutting down')
@@ -935,9 +725,6 @@
             session = self._sessions[sessionid]
         except KeyError:
             raise BadConnectionId('No such session %s' % sessionid)
-        if setcnxset:
-            session.set_cnx(txid) # must be done before set_cnxset
-            session.set_cnxset()
         return session
 
     # data sources handling ###################################################
@@ -977,11 +764,11 @@
         """return the type of the entity with id <eid>"""
         return self.type_and_source_from_eid(eid, cnx)[0]
 
-    def querier_cache_key(self, session, rql, args, eidkeys):
+    def querier_cache_key(self, cnx, rql, args, eidkeys):
         cachekey = [rql]
         for key in sorted(eidkeys):
             try:
-                etype = self.type_from_eid(args[key], session)
+                etype = self.type_from_eid(args[key], cnx)
             except KeyError:
                 raise QueryError('bad cache key %s (no value)' % key)
             except TypeError:
@@ -1020,13 +807,7 @@
             return self._extid_cache[extid]
         except KeyError:
             pass
-        try:
-            # bw compat: cnx may be a session, get at the Connection
-            cnx = cnx._cnx
-        except AttributeError:
-            pass
-        with cnx.ensure_cnx_set:
-            eid = self.system_source.extid2eid(cnx, extid)
+        eid = self.system_source.extid2eid(cnx, extid)
         if eid is not None:
             self._extid_cache[extid] = eid
             self._type_source_cache[eid] = (etype, extid, source.uri)
@@ -1034,123 +815,80 @@
         if not insert:
             return
         # no link between extid and eid, create one
-        with cnx.ensure_cnx_set:
-            # write query, ensure connection's mode is 'write' so connections
-            # won't be released until commit/rollback
-            cnx.mode = 'write'
-            try:
-                eid = self.system_source.create_eid(cnx)
-                self._extid_cache[extid] = eid
-                self._type_source_cache[eid] = (etype, extid, source.uri)
-                entity = source.before_entity_insertion(
-                    cnx, extid, etype, eid, sourceparams)
+        # write query, ensure connection's mode is 'write' so connections
+        # won't be released until commit/rollback
+        try:
+            eid = self.system_source.create_eid(cnx)
+            self._extid_cache[extid] = eid
+            self._type_source_cache[eid] = (etype, extid, source.uri)
+            entity = source.before_entity_insertion(
+                cnx, extid, etype, eid, sourceparams)
+            if source.should_call_hooks:
+                # get back a copy of operation for later restore if
+                # necessary, see below
+                pending_operations = cnx.pending_operations[:]
+                self.hm.call_hooks('before_add_entity', cnx, entity=entity)
+            self.add_info(cnx, entity, source, extid)
+            source.after_entity_insertion(cnx, extid, entity, sourceparams)
+            if source.should_call_hooks:
+                self.hm.call_hooks('after_add_entity', cnx, entity=entity)
+            return eid
+        except Exception:
+            # XXX do some cleanup manually so that the transaction has a
+            # chance to be commited, with simply this entity discarded
+            self._extid_cache.pop(extid, None)
+            self._type_source_cache.pop(eid, None)
+            if 'entity' in locals():
+                hook.CleanupDeletedEidsCacheOp.get_instance(cnx).add_data(entity.eid)
+                self.system_source.delete_info_multi(cnx, [entity])
                 if source.should_call_hooks:
-                    # get back a copy of operation for later restore if
-                    # necessary, see below
-                    pending_operations = cnx.pending_operations[:]
-                    self.hm.call_hooks('before_add_entity', cnx, entity=entity)
-                self.add_info(cnx, entity, source, extid)
-                source.after_entity_insertion(cnx, extid, entity, sourceparams)
-                if source.should_call_hooks:
-                    self.hm.call_hooks('after_add_entity', cnx, entity=entity)
-                return eid
-            except Exception:
-                # XXX do some cleanup manually so that the transaction has a
-                # chance to be commited, with simply this entity discarded
-                self._extid_cache.pop(extid, None)
-                self._type_source_cache.pop(eid, None)
-                if 'entity' in locals():
-                    hook.CleanupDeletedEidsCacheOp.get_instance(cnx).add_data(entity.eid)
-                    self.system_source.delete_info_multi(cnx, [entity])
-                    if source.should_call_hooks:
-                        cnx.pending_operations = pending_operations
-                raise
+                    cnx.pending_operations = pending_operations
+            raise
 
-    def add_info(self, session, entity, source, extid=None):
+    def add_info(self, cnx, entity, source, extid=None):
         """add type and source info for an eid into the system table,
         and index the entity with the full text index
         """
         # begin by inserting eid/type/source/extid into the entities table
-        hook.CleanupNewEidsCacheOp.get_instance(session).add_data(entity.eid)
-        self.system_source.add_info(session, entity, source, extid)
+        hook.CleanupNewEidsCacheOp.get_instance(cnx).add_data(entity.eid)
+        self.system_source.add_info(cnx, entity, source, extid)
 
-    def delete_info(self, session, entity, sourceuri):
-        """called by external source when some entity known by the system source
-        has been deleted in the external source
+    def _delete_cascade_multi(self, cnx, entities):
+        """same as _delete_cascade but accepts a list of entities with
+        the same etype and belonging to the same source.
         """
-        # mark eid as being deleted in session info and setup cache update
-        # operation
-        hook.CleanupDeletedEidsCacheOp.get_instance(session).add_data(entity.eid)
-        self._delete_info(session, entity, sourceuri)
-
-    def _delete_info(self, session, entity, sourceuri):
-        """delete system information on deletion of an entity:
-
-        * delete all remaining relations from/to this entity
-        * call delete info on the system source
-        """
-        pendingrtypes = session.transaction_data.get('pendingrtypes', ())
+        pendingrtypes = cnx.transaction_data.get('pendingrtypes', ())
         # delete remaining relations: if user can delete the entity, he can
         # delete all its relations without security checking
-        with session.security_enabled(read=False, write=False):
-            eid = entity.eid
-            for rschema, _, role in entity.e_schema.relation_definitions():
-                if rschema.rule:
-                    continue # computed relation
-                rtype = rschema.type
-                if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
-                    continue
-                if role == 'subject':
-                    # don't skip inlined relation so they are regularly
-                    # deleted and so hooks are correctly called
-                    rql = 'DELETE X %s Y WHERE X eid %%(x)s' % rtype
-                else:
-                    rql = 'DELETE Y %s X WHERE X eid %%(x)s' % rtype
-                try:
-                    session.execute(rql, {'x': eid}, build_descr=False)
-                except Exception:
-                    if self.config.mode == 'test':
+        with cnx.security_enabled(read=False, write=False):
+            in_eids = ','.join([str(_e.eid) for _e in entities])
+            with cnx.running_hooks_ops():
+                for rschema, _, role in entities[0].e_schema.relation_definitions():
+                    if rschema.rule:
+                        continue # computed relation
+                    rtype = rschema.type
+                    if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
+                        continue
+                    if role == 'subject':
+                        # don't skip inlined relation so they are regularly
+                        # deleted and so hooks are correctly called
+                        rql = 'DELETE X %s Y WHERE X eid IN (%s)' % (rtype, in_eids)
+                    else:
+                        rql = 'DELETE Y %s X WHERE X eid IN (%s)' % (rtype, in_eids)
+                    try:
+                        cnx.execute(rql, build_descr=False)
+                    except ValidationError:
                         raise
-                    self.exception('error while cascading delete for entity %s '
-                                   'from %s. RQL: %s', entity, sourceuri, rql)
-        self.system_source.delete_info_multi(session, [entity])
-
-    def _delete_info_multi(self, session, entities):
-        """same as _delete_info but accepts a list of entities with
-        the same etype and belinging to the same source.
-        """
-        pendingrtypes = session.transaction_data.get('pendingrtypes', ())
-        # delete remaining relations: if user can delete the entity, he can
-        # delete all its relations without security checking
-        with session.security_enabled(read=False, write=False):
-            in_eids = ','.join([str(_e.eid) for _e in entities])
-            for rschema, _, role in entities[0].e_schema.relation_definitions():
-                if rschema.rule:
-                    continue # computed relation
-                rtype = rschema.type
-                if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
-                    continue
-                if role == 'subject':
-                    # don't skip inlined relation so they are regularly
-                    # deleted and so hooks are correctly called
-                    rql = 'DELETE X %s Y WHERE X eid IN (%s)' % (rtype, in_eids)
-                else:
-                    rql = 'DELETE Y %s X WHERE X eid IN (%s)' % (rtype, in_eids)
-                try:
-                    session.execute(rql, build_descr=False)
-                except ValidationError:
-                    raise
-                except Unauthorized:
-                    self.exception('Unauthorized exception while cascading delete for entity %s. '
-                                   'RQL: %s.\nThis should not happen since security is disabled here.',
-                                   entities, rql)
-                    raise
-                except Exception:
-                    if self.config.mode == 'test':
+                    except Unauthorized:
+                        self.exception('Unauthorized exception while cascading delete for entity %s. '
+                                       'RQL: %s.\nThis should not happen since security is disabled here.',
+                                       entities, rql)
                         raise
-                    self.exception('error while cascading delete for entity %s. RQL: %s',
-                                   entities, rql)
-        self.system_source.delete_info_multi(session, entities)
+                    except Exception:
+                        if self.config.mode == 'test':
+                            raise
+                        self.exception('error while cascading delete for entity %s. RQL: %s',
+                                       entities, rql)
 
     def init_entity_caches(self, cnx, entity, source):
         """add entity to connection entities cache and repo's extid cache.
@@ -1188,13 +926,13 @@
         edited.set_defaults()
         if cnx.is_hook_category_activated('integrity'):
             edited.check(creation=True)
+        self.add_info(cnx, entity, source, extid)
         try:
             source.add_entity(cnx, entity)
-        except UniqueTogetherError as exc:
+        except (UniqueTogetherError, ViolatedConstraint) as exc:
             userhdlr = cnx.vreg['adapters'].select(
                 'IUserFriendlyError', cnx, entity=entity, exc=exc)
             userhdlr.raise_user_exception()
-        self.add_info(cnx, entity, source, extid)
         edited.saved = entity._cw_is_saved = True
         # trigger after_add_entity after after_add_relation
         self.hm.call_hooks('after_add_entity', cnx, entity=entity)
@@ -1254,7 +992,7 @@
             try:
                 source.update_entity(cnx, entity)
                 edited.saved = True
-            except UniqueTogetherError as exc:
+            except (UniqueTogetherError, ViolatedConstraint) as exc:
                 userhdlr = cnx.vreg['adapters'].select(
                     'IUserFriendlyError', cnx, entity=entity, exc=exc)
                 userhdlr.raise_user_exception()
@@ -1309,8 +1047,9 @@
             if server.DEBUG & server.DBG_REPO:
                 print 'DELETE entities', etype, [entity.eid for entity in entities]
             self.hm.call_hooks('before_delete_entity', cnx, entities=entities)
-            self._delete_info_multi(cnx, entities)
+            self._delete_cascade_multi(cnx, entities)
             source.delete_entities(cnx, entities)
+            source.delete_info_multi(cnx, entities)
             self.hm.call_hooks('after_delete_entity', cnx, entities=entities)
         # don't clear cache here, it is done in a hook on commit
 
@@ -1341,7 +1080,7 @@
                     continue
                 # take care to relation of cardinality '?1', as all eids will
                 # be inserted later, we've remove duplicated eids since they
-                # won't be catched by `del_existing_rel_if_needed`
+                # won't be caught by `del_existing_rel_if_needed`
                 rdef = cnx.rtype_eids_rdef(rtype, subjeid, objeid)
                 card = rdef.cardinality
                 if card[0] in '?1':
@@ -1392,79 +1131,12 @@
                            eidfrom=subject, rtype=rtype, eidto=object)
 
 
-    # pyro handling ###########################################################
-
-    @property
-    @cached
-    def pyro_appid(self):
-        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
-        return appid
-
-    def _use_pyrons(self):
-        """return True if the pyro-ns-host is set to something else
-        than NO_PYRONS, meaning we want to go through a pyro
-        nameserver"""
-        return self.config['pyro-ns-host'] != 'NO_PYRONS'
-
-    def pyro_register(self, host=''):
-        """register the repository as a pyro object"""
-        from logilab.common import pyro_ext as pyro
-        daemon = pyro.register_object(self, self.pyro_appid,
-                                      daemonhost=self.config['pyro-host'],
-                                      nshost=self.config['pyro-ns-host'],
-                                      use_pyrons=self._use_pyrons())
-        self.info('repository registered as a pyro object %s', self.pyro_appid)
-        self.pyro_uri =  pyro.get_object_uri(self.pyro_appid)
-        self.info('pyro uri is: %s', self.pyro_uri)
-        self.pyro_registered = True
-        # register a looping task to regularly ensure we're still registered
-        # into the pyro name server
-        if self._use_pyrons():
-            self.looping_task(60*10, self._ensure_pyro_ns)
-        pyro_sessions = self._pyro_sessions
-        # install hacky function to free cnxset
-        def handleConnection(conn, tcpserver, sessions=pyro_sessions):
-            sessions[threading.currentThread()] = None
-            return tcpserver.getAdapter().__class__.handleConnection(tcpserver.getAdapter(), conn, tcpserver)
-        daemon.getAdapter().handleConnection = handleConnection
-        def removeConnection(conn, sessions=pyro_sessions):
-            daemon.__class__.removeConnection(daemon, conn)
-            session = sessions.pop(threading.currentThread(), None)
-            if session is None:
-                # client was not yet connected to the repo
-                return
-            if not session.closed:
-                self.close(session.sessionid)
-        daemon.removeConnection = removeConnection
-        return daemon
-
-    def _ensure_pyro_ns(self):
-        if not self._use_pyrons():
-            return
-        from logilab.common import pyro_ext as pyro
-        pyro.ns_reregister(self.pyro_appid, nshost=self.config['pyro-ns-host'])
-        self.info('repository re-registered as a pyro object %s',
-                  self.pyro_appid)
 
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
 
-
-def pyro_unregister(config):
-    """unregister the repository from the pyro name server"""
-    from logilab.common.pyro_ext import ns_unregister
-    appid = config['pyro-instance-id'] or config.appid
-    ns_unregister(appid, config['pyro-ns-group'], config['pyro-ns-host'])
-
-
 from logging import getLogger
 from cubicweb import set_log_methods
 set_log_methods(Repository, getLogger('cubicweb.repository'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/schema2sql.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,293 @@
+# copyright 2004-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of cubicweb.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+"""write a schema as sql"""
+
+__docformat__ = "restructuredtext en"
+
+from hashlib import md5
+
+from six import string_types
+from six.moves import range
+
+from yams.constraints import (SizeConstraint, UniqueConstraint, Attribute,
+                              NOW, TODAY)
+
+# default are usually not handled at the sql level. If you want them, set
+# SET_DEFAULT to True
+SET_DEFAULT = False
+
+def rschema_has_table(rschema, skip_relations):
+    """Return True if the given schema should have a table in the database"""
+    return not (rschema.final or rschema.inlined or rschema.rule or rschema.type in skip_relations)
+
+
+def schema2sql(dbhelper, schema, skip_entities=(), skip_relations=(), prefix=''):
+    """write to the output stream a SQL schema to store the objects
+    corresponding to the given schema
+    """
+    output = []
+    w = output.append
+    for etype in sorted(schema.entities()):
+        eschema = schema.eschema(etype)
+        if eschema.final or eschema.type in skip_entities:
+            continue
+        w(eschema2sql(dbhelper, eschema, skip_relations, prefix=prefix))
+    for rtype in sorted(schema.relations()):
+        rschema = schema.rschema(rtype)
+        if rschema_has_table(rschema, skip_relations):
+            w(rschema2sql(rschema))
+    return '\n'.join(output)
+
+
+def dropschema2sql(dbhelper, schema, skip_entities=(), skip_relations=(), prefix=''):
+    """write to the output stream a SQL schema to store the objects
+    corresponding to the given schema
+    """
+    output = []
+    w = output.append
+    for etype in sorted(schema.entities()):
+        eschema = schema.eschema(etype)
+        if eschema.final or eschema.type in skip_entities:
+            continue
+        stmts = dropeschema2sql(dbhelper, eschema, skip_relations, prefix=prefix)
+        for stmt in stmts:
+            w(stmt)
+    for rtype in sorted(schema.relations()):
+        rschema = schema.rschema(rtype)
+        if rschema_has_table(rschema, skip_relations):
+            w(droprschema2sql(rschema))
+    return '\n'.join(output)
+
+
+def eschema_attrs(eschema, skip_relations):
+    attrs = [attrdef for attrdef in eschema.attribute_definitions()
+             if not attrdef[0].type in skip_relations]
+    attrs += [(rschema, None)
+              for rschema in eschema.subject_relations()
+              if not rschema.final and rschema.inlined]
+    return attrs
+
+def unique_index_name(eschema, columns):
+    return u'unique_%s' % md5((eschema.type +
+                              ',' +
+                              ','.join(sorted(columns))).encode('ascii')).hexdigest()
+
+def iter_unique_index_names(eschema):
+    for columns in eschema._unique_together or ():
+        yield columns, unique_index_name(eschema, columns)
+
+def dropeschema2sql(dbhelper, eschema, skip_relations=(), prefix=''):
+    """return sql to drop an entity type's table"""
+    # not necessary to drop indexes, that's implictly done when
+    # dropping the table, but we need to drop SQLServer views used to
+    # create multicol unique indices
+    statements = []
+    tablename = prefix + eschema.type
+    if eschema._unique_together is not None:
+        for columns, index_name in iter_unique_index_names(eschema):
+            cols  = ['%s%s' % (prefix, col) for col in columns]
+            sqls = dbhelper.sqls_drop_multicol_unique_index(tablename, cols, index_name)
+            statements += sqls
+    statements += ['DROP TABLE %s;' % (tablename)]
+    return statements
+
+
+def eschema2sql(dbhelper, eschema, skip_relations=(), prefix=''):
+    """write an entity schema as SQL statements to stdout"""
+    output = []
+    w = output.append
+    table = prefix + eschema.type
+    w('CREATE TABLE %s(' % (table))
+    attrs = eschema_attrs(eschema, skip_relations)
+    # XXX handle objectinline physical mode
+    for i in range(len(attrs)):
+        rschema, attrschema = attrs[i]
+        if attrschema is not None:
+            sqltype = aschema2sql(dbhelper, eschema, rschema, attrschema,
+                                  indent=' ')
+        else: # inline relation
+            sqltype = 'integer REFERENCES entities (eid)'
+        if i == len(attrs) - 1:
+            w(' %s%s %s' % (prefix, rschema.type, sqltype))
+        else:
+            w(' %s%s %s,' % (prefix, rschema.type, sqltype))
+    for rschema, aschema in attrs:
+        if aschema is None:  # inline relation
+            continue
+        attr = rschema.type
+        rdef = rschema.rdef(eschema.type, aschema.type)
+        for constraint in rdef.constraints:
+            cstrname, check = check_constraint(eschema, aschema, attr, constraint, dbhelper, prefix=prefix)
+            if cstrname is not None:
+                w(', CONSTRAINT %s CHECK(%s)' % (cstrname, check))
+    w(');')
+    # create indexes
+    for i in range(len(attrs)):
+        rschema, attrschema = attrs[i]
+        if attrschema is None or eschema.rdef(rschema).indexed:
+            w(dbhelper.sql_create_index(table, prefix + rschema.type))
+    for columns, index_name in iter_unique_index_names(eschema):
+        cols  = ['%s%s' % (prefix, col) for col in columns]
+        sqls = dbhelper.sqls_create_multicol_unique_index(table, cols, index_name)
+        for sql in sqls:
+            w(sql)
+    w('')
+    return '\n'.join(output)
+
+def as_sql(value, dbhelper, prefix):
+    if isinstance(value, Attribute):
+        return prefix + value.attr
+    elif isinstance(value, TODAY):
+        return dbhelper.sql_current_date()
+    elif isinstance(value, NOW):
+        return dbhelper.sql_current_timestamp()
+    else:
+        # XXX more quoting for literals?
+        return value
+
+def check_constraint(eschema, aschema, attr, constraint, dbhelper, prefix=''):
+    # XXX should find a better name
+    cstrname = 'cstr' + md5(eschema.type + attr + constraint.type() +
+                            (constraint.serialize() or '')).hexdigest()
+    if constraint.type() == 'BoundaryConstraint':
+        value = as_sql(constraint.boundary, dbhelper, prefix)
+        return cstrname, '%s%s %s %s' % (prefix, attr, constraint.operator, value)
+    elif constraint.type() == 'IntervalBoundConstraint':
+        condition = []
+        if constraint.minvalue is not None:
+            value = as_sql(constraint.minvalue, dbhelper, prefix)
+            condition.append('%s%s >= %s' % (prefix, attr, value))
+        if constraint.maxvalue is not None:
+            value = as_sql(constraint.maxvalue, dbhelper, prefix)
+            condition.append('%s%s <= %s' % (prefix, attr, value))
+        return cstrname, ' AND '.join(condition)
+    elif constraint.type() == 'StaticVocabularyConstraint':
+        sample = next(iter(constraint.vocabulary()))
+        if not isinstance(sample, string_types):
+            values = ', '.join(str(word) for word in constraint.vocabulary())
+        else:
+            # XXX better quoting?
+            values = ', '.join("'%s'" % word.replace("'", "''") for word in constraint.vocabulary())
+        return cstrname, '%s%s IN (%s)' % (prefix, attr, values)
+    return None, None
+
+def aschema2sql(dbhelper, eschema, rschema, aschema, creating=True, indent=''):
+    """write an attribute schema as SQL statements to stdout"""
+    attr = rschema.type
+    rdef = rschema.rdef(eschema.type, aschema.type)
+    sqltype = type_from_constraints(dbhelper, aschema.type, rdef.constraints,
+                                    creating)
+    if SET_DEFAULT:
+        default = eschema.default(attr)
+        if default is not None:
+            if aschema.type == 'Boolean':
+                sqltype += ' DEFAULT %s' % dbhelper.boolean_value(default)
+            elif aschema.type == 'String':
+                sqltype += ' DEFAULT %r' % str(default)
+            elif aschema.type in ('Int', 'BigInt', 'Float'):
+                sqltype += ' DEFAULT %s' % default
+            # XXX ignore default for other type
+            # this is expected for NOW / TODAY
+    if creating:
+        if rdef.uid:
+            sqltype += ' PRIMARY KEY REFERENCES entities (eid)'
+        elif rdef.cardinality[0] == '1':
+            # don't set NOT NULL if backend isn't able to change it later
+            if dbhelper.alter_column_support:
+                sqltype += ' NOT NULL'
+    # else we're getting sql type to alter a column, we don't want key / indexes
+    # / null modifiers
+    return sqltype
+
+
+def type_from_constraints(dbhelper, etype, constraints, creating=True):
+    """return a sql type string corresponding to the constraints"""
+    constraints = list(constraints)
+    unique, sqltype = False, None
+    size_constrained_string = dbhelper.TYPE_MAPPING.get('SizeConstrainedString', 'varchar(%s)')
+    if etype == 'String':
+        for constraint in constraints:
+            if isinstance(constraint, SizeConstraint):
+                if constraint.max is not None:
+                    sqltype = size_constrained_string % constraint.max
+            elif isinstance(constraint, UniqueConstraint):
+                unique = True
+    if sqltype is None:
+        sqltype = dbhelper.TYPE_MAPPING[etype]
+    if creating and unique:
+        sqltype += ' UNIQUE'
+    return sqltype
+
+
+_SQL_SCHEMA = """
+CREATE TABLE %(table)s (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT %(table)s_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX %(table)s_from_idx ON %(table)s(eid_from);
+CREATE INDEX %(table)s_to_idx ON %(table)s(eid_to);"""
+
+
+def rschema2sql(rschema):
+    assert not rschema.rule
+    return _SQL_SCHEMA % {'table': '%s_relation' % rschema.type}
+
+
+def droprschema2sql(rschema):
+    """return sql to drop a relation type's table"""
+    # not necessary to drop indexes, that's implictly done when dropping
+    # the table
+    return 'DROP TABLE %s_relation;' % rschema.type
+
+
+def grant_schema(schema, user, set_owner=True, skip_entities=(), prefix=''):
+    """write to the output stream a SQL schema to store the objects
+    corresponding to the given schema
+    """
+    output = []
+    w = output.append
+    for etype in sorted(schema.entities()):
+        eschema = schema.eschema(etype)
+        if eschema.final or etype in skip_entities:
+            continue
+        w(grant_eschema(eschema, user, set_owner, prefix=prefix))
+    for rtype in sorted(schema.relations()):
+        rschema = schema.rschema(rtype)
+        if rschema_has_table(rschema, skip_relations=()):  # XXX skip_relations should be specified
+            w(grant_rschema(rschema, user, set_owner))
+    return '\n'.join(output)
+
+
+def grant_eschema(eschema, user, set_owner=True, prefix=''):
+    output = []
+    w = output.append
+    etype = eschema.type
+    if set_owner:
+        w('ALTER TABLE %s%s OWNER TO %s;' % (prefix, etype, user))
+    w('GRANT ALL ON %s%s TO %s;' % (prefix, etype, user))
+    return '\n'.join(output)
+
+
+def grant_rschema(rschema, user, set_owner=True):
+    output = []
+    if set_owner:
+        output.append('ALTER TABLE %s_relation OWNER TO %s;' % (rschema.type, user))
+    output.append('GRANT ALL ON %s_relation TO %s;' % (rschema.type, user))
+    return '\n'.join(output)
--- a/server/schemaserial.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/schemaserial.py	Fri Oct 09 17:52:14 2015 +0200
@@ -25,13 +25,12 @@
 
 from logilab.common.shellutils import ProgressBar, DummyProgressBar
 
-from yams import (BadSchemaDefinition, schema as schemamod, buildobjs as ybo,
-                  schema2sql as y2sql)
+from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo
 
 from cubicweb import Binary
 from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP,
                              VIRTUAL_RTYPES)
-from cubicweb.server import sqlutils
+from cubicweb.server import sqlutils, schema2sql as y2sql
 
 
 def group_mapping(cnx, interactive=True):
@@ -93,14 +92,6 @@
     with cnx.ensure_cnx_set:
         tables = set(t.lower() for t in dbhelper.list_tables(cnx.cnxset.cu))
         has_computed_relations = 'cw_cwcomputedrtype' in tables
-    if has_computed_relations:
-        rset = cnx.execute(
-            'Any X, N, R, D WHERE X is CWComputedRType, X name N, '
-            'X rule R, X description D')
-        for eid, rule_name, rule, description in rset.rows:
-            rtype = ybo.ComputedRelation(name=rule_name, rule=rule, eid=eid,
-                                         description=description)
-            schema.add_relation_type(rtype)
     # computed attribute
     try:
         cnx.system_sql("SELECT cw_formula FROM cw_CWAttribute")
@@ -110,14 +101,13 @@
         has_computed_attributes = False
 
     # XXX bw compat (3.6 migration)
-    with cnx.ensure_cnx_set:
-        sqlcu = cnx.system_sql("SELECT * FROM cw_CWRType WHERE cw_name='symetric'")
-        if sqlcu.fetchall():
-            sql = dbhelper.sql_rename_col('cw_CWRType', 'cw_symetric', 'cw_symmetric',
-                                          dbhelper.TYPE_MAPPING['Boolean'], True)
-            sqlcu.execute(sql)
-            sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'")
-            cnx.commit(False)
+    sqlcu = cnx.system_sql("SELECT * FROM cw_CWRType WHERE cw_name='symetric'")
+    if sqlcu.fetchall():
+        sql = dbhelper.sql_rename_col('cw_CWRType', 'cw_symetric', 'cw_symmetric',
+                                      dbhelper.TYPE_MAPPING['Boolean'], True)
+        sqlcu.execute(sql)
+        sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'")
+        cnx.commit()
     ertidx = {}
     copiedeids = set()
     permsidx = deserialize_ertype_permissions(cnx)
@@ -179,6 +169,15 @@
         stype = ETYPE_NAME_MAP.get(stype, stype)
         schema.eschema(etype)._specialized_type = stype
         schema.eschema(stype)._specialized_by.append(etype)
+    if has_computed_relations:
+        rset = cnx.execute(
+            'Any X, N, R, D WHERE X is CWComputedRType, X name N, '
+            'X rule R, X description D')
+        for eid, rule_name, rule, description in rset.rows:
+            rtype = ybo.ComputedRelation(name=rule_name, rule=rule, eid=eid,
+                                         description=description)
+            rschema = schema.add_relation_type(rtype)
+            set_perms(rschema, permsidx)
     # load every relation types
     for eid, rtype, desc, sym, il, ftc in cnx.execute(
         'Any X,N,D,S,I,FTC WHERE X is CWRType, X name N, X description D, '
@@ -377,7 +376,7 @@
             pb.update()
             continue
         if rschema.rule:
-            execschemarql(execute, rschema, crschema2rql(rschema))
+            execschemarql(execute, rschema, crschema2rql(rschema, groupmap))
             pb.update()
             continue
         execschemarql(execute, rschema, rschema2rql(rschema, addrdef=False))
@@ -527,9 +526,12 @@
     relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
     return relations, values
 
-def crschema2rql(crschema):
+def crschema2rql(crschema, groupmap):
     relations, values = crschema_relations_values(crschema)
     yield 'INSERT CWComputedRType X: %s' % ','.join(relations), values
+    if groupmap:
+        for rql, args in _erperms2rql(crschema, groupmap):
+            yield rql, args
 
 def crschema_relations_values(crschema):
     values = _ervalues(crschema)
--- a/server/server.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,140 +0,0 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# 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/>.
-"""Pyro RQL server"""
-
-__docformat__ = "restructuredtext en"
-
-import select
-from time import localtime, mktime
-
-from cubicweb.server.utils import TasksManager
-from cubicweb.server.repository import Repository
-
-class Finished(Exception):
-    """raise to remove an event from the event loop"""
-
-class TimeEvent:
-    """base event"""
-    # timefunc = staticmethod(localtime)
-    timefunc = localtime
-
-    def __init__(self, absolute=None, period=None):
-        # local time tuple
-        if absolute is None:
-            absolute = self.timefunc()
-        self.absolute = absolute
-        # optional period in seconds
-        self.period = period
-
-    def is_ready(self):
-        """return  true if the event is ready to be fired"""
-        now = self.timefunc()
-        if self.absolute <= now:
-            return True
-        return False
-
-    def fire(self, server):
-        """fire the event
-        must be overridden by concrete events
-        """
-        raise NotImplementedError()
-
-    def update(self):
-        """update the absolute date for the event or raise a finished exception
-        """
-        if self.period is None:
-            raise Finished
-        self.absolute = localtime(mktime(self.absolute) + self.period)
-
-
-class QuitEvent(TimeEvent):
-    """stop the server"""
-    def fire(self, server):
-        server.repo.shutdown()
-        server.quiting = True
-
-
-class RepositoryServer(object):
-
-    def __init__(self, config):
-        """make the repository available as a PyRO object"""
-        self.config = config
-        self.repo = Repository(config, TasksManager())
-        self.ns = None
-        self.quiting = None
-        # event queue
-        self.events = []
-
-    def add_event(self, event):
-        """add an event to the loop"""
-        self.info('adding event %s', event)
-        self.events.append(event)
-
-    def trigger_events(self):
-        """trigger ready events"""
-        for event in self.events[:]:
-            if event.is_ready():
-                self.info('starting event %s', event)
-                event.fire(self)
-                try:
-                    event.update()
-                except Finished:
-                    self.events.remove(event)
-
-    def run(self, req_timeout=5.0):
-        """enter the service loop"""
-        # start repository looping tasks
-        self.repo.start_looping_tasks()
-        while self.quiting is None:
-            try:
-                self.daemon.handleRequests(req_timeout)
-            except select.error:
-                continue
-            finally:
-                self.trigger_events()
-
-    def quit(self):
-        """stop the server"""
-        self.add_event(QuitEvent())
-
-    def connect(self, host='', port=0):
-        """the connect method on the repository only register to pyro if
-        necessary
-        """
-        self.daemon = self.repo.pyro_register(host)
-
-    # server utilitities ######################################################
-
-    def install_sig_handlers(self):
-        """install signal handlers"""
-        import signal
-        self.info('installing signal handlers')
-        signal.signal(signal.SIGINT, lambda x, y, s=self: s.quit())
-        signal.signal(signal.SIGTERM, lambda x, y, s=self: s.quit())
-
-
-    # these are overridden by set_log_methods below
-    # only defining here to prevent pylint from complaining
-    @classmethod
-    def info(cls, msg, *a, **kw):
-        pass
-
-from logging import getLogger
-from cubicweb import set_log_methods
-LOGGER = getLogger('cubicweb.reposerver')
-set_log_methods(RepositoryServer, LOGGER)
--- a/server/serverconfig.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/serverconfig.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -197,44 +197,6 @@
 notified of every changes.',
           'group': 'email', 'level': 2,
           }),
-        # pyro services config
-        ('pyro-host',
-         {'type' : 'string',
-          'default': None,
-          'help': 'Pyro server host, if not detectable correctly through \
-gethostname(). It may contains port information using <host>:<port> notation, \
-and if not set, it will be choosen randomly',
-          'group': 'pyro', 'level': 3,
-          }),
-        ('pyro-instance-id',
-         {'type' : 'string',
-          'default': lgconfig.Method('default_instance_id'),
-          'help': 'identifier of the CubicWeb instance in the Pyro name server',
-          'group': 'pyro', 'level': 1,
-          }),
-        ('pyro-ns-host',
-         {'type' : 'string',
-          'default': '',
-          'help': 'Pyro name server\'s host. If not set, will be detected by a \
-broadcast query. It may contains port information using <host>:<port> notation. \
-Use "NO_PYRONS" to create a Pyro server but not register to a pyro nameserver',
-          'group': 'pyro', 'level': 1,
-          }),
-        ('pyro-ns-group',
-         {'type' : 'string',
-          'default': 'cubicweb',
-          'help': 'Pyro name server\'s group where the repository will be \
-registered.',
-          'group': 'pyro', 'level': 1,
-          }),
-        # zmq services config
-        ('zmq-repository-address',
-         {'type' : 'string',
-          'default': None,
-          'help': ('ZMQ URI on which the repository will be bound '
-                   'to (of the form `zmqpickle-tcp://<ipaddr>:<port>`).'),
-          'group': 'zmq', 'level': 3,
-          }),
          ('zmq-address-sub',
           {'type' : 'csv',
            'default' : (),
@@ -350,10 +312,6 @@
             stream.write('[%s]\n%s\n' % (section, generate_source_config(sconfig)))
         restrict_perms_to_user(sourcesfile)
 
-    def pyro_enabled(self):
-        """pyro is always enabled in standalone repository configuration"""
-        return True
-
     def load_schema(self, expand_cubes=False, **kwargs):
         from cubicweb.schema import CubicWebSchemaLoader
         if expand_cubes:
@@ -387,6 +345,3 @@
         return ServerMigrationHelper(self, schema, interactive=interactive,
                                      cnx=cnx, repo=repo, connect=connect,
                                      verbosity=verbosity)
-
-
-CONFIGURATIONS.append(ServerConfiguration)
--- a/server/serverctl.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/serverctl.py	Fri Oct 09 17:52:14 2015 +0200
@@ -38,7 +38,6 @@
 from cubicweb.toolsutils import Command, CommandHandler, underline_title
 from cubicweb.cwctl import CWCTL, check_options_consistency, ConfigureInstanceCommand
 from cubicweb.server import SOURCE_TYPES
-from cubicweb.server.repository import Repository
 from cubicweb.server.serverconfig import (
     USER_OPTIONS, ServerConfiguration, SourceConfiguration,
     ask_source_config, generate_source_config)
@@ -167,11 +166,6 @@
         if not automatic:
             print underline_title('Configuring the repository')
             config.input_config('email', inputlevel)
-            # ask for pyro configuration if pyro is activated and we're not
-            # using a all-in-one config, in which case this is done by the web
-            # side command handler
-            if config.pyro_enabled() and config.name != 'all-in-one':
-                config.input_config('pyro', inputlevel)
             print '\n'+underline_title('Configuring the sources')
         sourcesfile = config.sources_file()
         # hack to make Method('default_instance_id') usable in db option defs
@@ -301,33 +295,6 @@
                     raise ExecutionError(str(exc))
 
 
-class RepositoryStartHandler(CommandHandler):
-    cmdname = 'start'
-    cfgname = 'repository'
-
-    def start_server(self, config):
-        command = ['cubicweb-ctl', 'start-repository']
-        if config.debugmode:
-            command.append('--debug')
-        command.append('--loglevel')
-        command.append(config['log-threshold'].lower())
-        command.append(config.appid)
-        subprocess.call(command)
-        return 1
-
-
-class RepositoryStopHandler(CommandHandler):
-    cmdname = 'stop'
-    cfgname = 'repository'
-
-    def poststop(self):
-        """if pyro is enabled, ensure the repository is correctly unregistered
-        """
-        if self.config.pyro_enabled():
-            from cubicweb.server.repository import pyro_unregister
-            pyro_unregister(self.config)
-
-
 # repository specific commands ################################################
 
 def createdb(helper, source, dbcnx, cursor, **kwargs):
@@ -530,51 +497,55 @@
         appid = args[0]
         config = ServerConfiguration.config_for(appid)
         repo, cnx = repo_cnx(config)
-        with cnx:
-            used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
-            cubes = repo.get_cubes()
-            while True:
-                type = raw_input('source type (%s): '
-                                    % ', '.join(sorted(SOURCE_TYPES)))
-                if type not in SOURCE_TYPES:
-                    print '-> unknown source type, use one of the available types.'
-                    continue
-                sourcemodule = SOURCE_TYPES[type].module
-                if not sourcemodule.startswith('cubicweb.'):
-                    # module names look like cubes.mycube.themodule
-                    sourcecube = SOURCE_TYPES[type].module.split('.', 2)[1]
-                    # if the source adapter is coming from an external component,
-                    # ensure it's specified in used cubes
-                    if not sourcecube in cubes:
-                        print ('-> this source type require the %s cube which is '
-                               'not used by the instance.')
+        repo.hm.call_hooks('server_maintenance', repo=repo)
+        try:
+            with cnx:
+                used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
+                cubes = repo.get_cubes()
+                while True:
+                    type = raw_input('source type (%s): '
+                                        % ', '.join(sorted(SOURCE_TYPES)))
+                    if type not in SOURCE_TYPES:
+                        print '-> unknown source type, use one of the available types.'
                         continue
-                break
-            while True:
-                parser = raw_input('parser type (%s): '
-                                    % ', '.join(sorted(repo.vreg['parsers'])))
-                if parser in repo.vreg['parsers']:
+                    sourcemodule = SOURCE_TYPES[type].module
+                    if not sourcemodule.startswith('cubicweb.'):
+                        # module names look like cubes.mycube.themodule
+                        sourcecube = SOURCE_TYPES[type].module.split('.', 2)[1]
+                        # if the source adapter is coming from an external component,
+                        # ensure it's specified in used cubes
+                        if not sourcecube in cubes:
+                            print ('-> this source type require the %s cube which is '
+                                   'not used by the instance.')
+                            continue
                     break
-                print '-> unknown parser identifier, use one of the available types.'
-            while True:
-                sourceuri = raw_input('source identifier (a unique name used to '
-                                      'tell sources apart): ').strip()
-                if not sourceuri:
-                    print '-> mandatory.'
-                else:
-                    sourceuri = unicode(sourceuri, sys.stdin.encoding)
-                    if sourceuri in used:
-                        print '-> uri already used, choose another one.'
+                while True:
+                    parser = raw_input('parser type (%s): '
+                                        % ', '.join(sorted(repo.vreg['parsers'])))
+                    if parser in repo.vreg['parsers']:
+                        break
+                    print '-> unknown parser identifier, use one of the available types.'
+                while True:
+                    sourceuri = raw_input('source identifier (a unique name used to '
+                                          'tell sources apart): ').strip()
+                    if not sourceuri:
+                        print '-> mandatory.'
                     else:
-                        break
-            url = raw_input('source URL (leave empty for none): ').strip()
-            url = unicode(url) if url else None
-            # XXX configurable inputlevel
-            sconfig = ask_source_config(config, type, inputlevel=self.config.config_level)
-            cfgstr = unicode(generate_source_config(sconfig), sys.stdin.encoding)
-            cnx.create_entity('CWSource', name=sourceuri, type=unicode(type),
-                              config=cfgstr, parser=unicode(parser), url=unicode(url))
-            cnx.commit()
+                        sourceuri = unicode(sourceuri, sys.stdin.encoding)
+                        if sourceuri in used:
+                            print '-> uri already used, choose another one.'
+                        else:
+                            break
+                url = raw_input('source URL (leave empty for none): ').strip()
+                url = unicode(url) if url else None
+                # XXX configurable inputlevel
+                sconfig = ask_source_config(config, type, inputlevel=self.config.config_level)
+                cfgstr = unicode(generate_source_config(sconfig), sys.stdin.encoding)
+                cnx.create_entity('CWSource', name=sourceuri, type=unicode(type),
+                                  config=cfgstr, parser=unicode(parser), url=unicode(url))
+                cnx.commit()
+        finally:
+            repo.hm.call_hooks('server_shutdown')
 
 
 class GrantUserOnInstanceCommand(Command):
@@ -686,77 +657,6 @@
         cnx.close()
 
 
-class StartRepositoryCommand(Command):
-    """Start a CubicWeb RQL server for a given instance.
-
-    The server will be remotely accessible through pyro or ZMQ
-
-    <instance>
-      the identifier of the instance to initialize.
-    """
-    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',
-          }),
-        ('address',
-         {'short': 'a', 'type': 'string', 'metavar': '<protocol>://<host>:<port>',
-          'default': '',
-          'help': ('specify a ZMQ URI on which to bind, or use "pyro://"'
-                   'to create a pyro-based repository'),
-          }),
-        )
-
-    def create_repo(self, config):
-        address = self['address']
-        if not address:
-            address = config.get('zmq-repository-address') or 'pyro://'
-        if address.startswith('pyro://'):
-            from cubicweb.server.server import RepositoryServer
-            return RepositoryServer(config), config['host']
-        else:
-            from cubicweb.server.utils import TasksManager
-            from cubicweb.server.cwzmq import ZMQRepositoryServer
-            repo = Repository(config, TasksManager())
-            return ZMQRepositoryServer(repo), address
-
-    def run(self, args):
-        from logilab.common.daemon import daemonize, setugid
-        from cubicweb.cwctl import init_cmdline_log_threshold
-        print 'WARNING: Standalone repository with pyro or zmq access is deprecated'
-        appid = args[0]
-        debug = self['debug']
-        if sys.platform == 'win32' and not debug:
-            logger = logging.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, address = self.create_repo(config)
-        # ensure the directory where the pid-file should be set exists (for
-        # instance /var/run/cubicweb may be deleted on computer restart)
-        pidfile = config['pid-file']
-        piddir = os.path.dirname(pidfile)
-        # go ! (don't daemonize in debug mode)
-        if not os.path.exists(piddir):
-            os.makedirs(piddir)
-        if not debug and daemonize(pidfile, umask=config['umask']):
-            return
-        uid = config['uid']
-        if uid is not None:
-            setugid(uid)
-        server.install_sig_handlers()
-        server.connect(address)
-        server.run()
-
 
 def _remote_dump(host, appid, output, sudo=False):
     # XXX generate unique/portable file name
@@ -1061,7 +961,7 @@
         config = ServerConfiguration.config_for(appid)
         repo, cnx = repo_cnx(config)
         with cnx:
-            reindex_entities(repo.schema, cnx._cnx, etypes=etypes)
+            reindex_entities(repo.schema, cnx, etypes=etypes)
             cnx.commit()
 
 
@@ -1084,20 +984,23 @@
     )
 
     def run(self, args):
+        from cubicweb import repoapi
         from cubicweb.cwctl import init_cmdline_log_threshold
         config = ServerConfiguration.config_for(args[0])
         config.global_set_option('log-file', None)
         config.log_format = '%(levelname)s %(name)s: %(message)s'
         init_cmdline_log_threshold(config, self['loglevel'])
-        # only retrieve cnx to trigger authentication, close it right away
-        repo, cnx = repo_cnx(config)
-        cnx.close()
+        repo = repoapi.get_repository(config=config)
+        repo.hm.call_hooks('server_maintenance', repo=repo)
         try:
-            source = repo.sources_by_uri[args[1]]
-        except KeyError:
-            raise ExecutionError('no source named %r' % args[1])
-        session = repo.internal_session()
-        stats = source.pull_data(session, force=True, raise_on_error=True)
+            try:
+                source = repo.sources_by_uri[args[1]]
+            except KeyError:
+                raise ExecutionError('no source named %r' % args[1])
+            with repo.internal_cnx() as cnx:
+                stats = source.pull_data(cnx, force=True, raise_on_error=True)
+        finally:
+            repo.shutdown()
         for key, val in stats.iteritems():
             if val:
                 print key, ':', val
@@ -1135,18 +1038,17 @@
 
     def run(self, args):
         from yams.diff import schema_diff
+        from cubicweb import repoapi
         appid = args.pop(0)
         diff_tool = args.pop(0)
         config = ServerConfiguration.config_for(appid)
-        repo, cnx = repo_cnx(config)
-        cnx.close()
+        repo = repoapi.get_repository(config=config)
         fsschema = config.load_schema(expand_cubes=True)
         schema_diff(fsschema, repo.schema, permissionshandler, diff_tool, ignore=('eid',))
 
 
 for cmdclass in (CreateInstanceDBCommand, InitInstanceCommand,
                  GrantUserOnInstanceCommand, ResetAdminPasswordCommand,
-                 StartRepositoryCommand,
                  DBDumpCommand, DBRestoreCommand, DBCopyCommand,
                  AddSourceCommand, CheckRepositoryCommand, RebuildFTICommand,
                  SynchronizeSourceCommand, SchemaDiffCommand,
--- a/server/session.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/session.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -19,7 +19,6 @@
 __docformat__ = "restructuredtext en"
 
 import sys
-import threading
 from time import time
 from uuid import uuid4
 from warnings import warn
@@ -34,7 +33,6 @@
 from cubicweb.req import RequestSessionBase
 from cubicweb.utils import make_uid
 from cubicweb.rqlrewrite import RQLRewriter
-from cubicweb.server import ShuttingDown
 from cubicweb.server.edition import EditedEntity
 
 
@@ -68,27 +66,6 @@
     return req.vreg.config.repairing
 
 
-class transaction(object):
-    """Ensure that the transaction is either commited or rolled back at exit
-
-    Context manager to enter a transaction for a session: when exiting the
-    `with` block on exception, call `session.rollback()`, else call
-    `session.commit()` on normal exit
-    """
-    def __init__(self, session, free_cnxset=True):
-        self.session = session
-        self.free_cnxset = free_cnxset
-
-    def __enter__(self):
-        # ensure session has a cnxset
-        self.session.set_cnxset()
-
-    def __exit__(self, exctype, exc, traceback):
-        if exctype:
-            self.session.rollback(free_cnxset=self.free_cnxset)
-        else:
-            self.session.commit(free_cnxset=self.free_cnxset)
-
 @deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
 def hooks_control(obj, mode, *categories):
     assert mode in  (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
@@ -98,8 +75,7 @@
         return obj.deny_all_hooks_but(*categories)
 
 
-class _hooks_control(object): # XXX repoapi: remove me when
-                              # session stop being connection
+class _hooks_control(object):
     """context manager to control activated hooks categories.
 
     If mode is `HOOKS_DENY_ALL`, given hooks categories will
@@ -149,24 +125,6 @@
         finally:
             self.cnx.hooks_mode = self.oldmode
 
-class _session_hooks_control(_hooks_control): # XXX repoapi: remove me when
-                                              # session stop being connection
-    """hook control context manager for session
-
-    Necessary to handle some unholy transaction scope logic."""
-
-
-    def __init__(self, session, mode, *categories):
-        self.session = session
-        super_init = super(_session_hooks_control, self).__init__
-        super_init(session._cnx, mode, *categories)
-
-    def __exit__(self, exctype, exc, traceback):
-        super_exit = super(_session_hooks_control, self).__exit__
-        ret = super_exit(exctype, exc, traceback)
-        if self.cnx.ctx_count == 0:
-            self.session._close_cnx(self.cnx)
-        return ret
 
 @deprecated('[3.17] use <object>.security_enabled instead')
 def security_enabled(obj, *args, **kwargs):
@@ -205,24 +163,6 @@
         if self.oldwrite is not None:
             self.cnx.write_security = self.oldwrite
 
-class _session_security_enabled(_security_enabled):
-    """hook security context manager for session
-
-    Necessary To handle some unholy transaction scope logic."""
-
-
-    def __init__(self, session, read=None, write=None):
-        self.session = session
-        super_init = super(_session_security_enabled, self).__init__
-        super_init(session._cnx, read=read, write=write)
-
-    def __exit__(self, exctype, exc, traceback):
-        super_exit = super(_session_security_enabled, self).__exit__
-        ret = super_exit(exctype, exc, traceback)
-        if self.cnx.ctx_count == 0:
-            self.session._close_cnx(self.cnx)
-        return ret
-
 HOOKS_ALLOW_ALL = object()
 HOOKS_DENY_ALL = object()
 DEFAULT_SECURITY = object() # evaluated to true by design
@@ -230,146 +170,6 @@
 class SessionClosedError(RuntimeError):
     pass
 
-class CnxSetTracker(object):
-    """Keep track of which connection use which cnxset.
-
-    There should be one of these objects per session (including internal sessions).
-
-    Session objects are responsible for creating their CnxSetTracker object.
-
-    Connections should use the :meth:`record` and :meth:`forget` to inform the
-    tracker of cnxsets they have acquired.
-
-    .. automethod:: cubicweb.server.session.CnxSetTracker.record
-    .. automethod:: cubicweb.server.session.CnxSetTracker.forget
-
-    Sessions use the :meth:`close` and :meth:`wait` methods when closing.
-
-    .. automethod:: cubicweb.server.session.CnxSetTracker.close
-    .. automethod:: cubicweb.server.session.CnxSetTracker.wait
-
-    This object itself is threadsafe. It also requires caller to acquired its
-    lock in some situation.
-    """
-
-    def __init__(self):
-        self._active = True
-        self._condition = threading.Condition()
-        self._record = {}
-
-    def __enter__(self):
-        return self._condition.__enter__()
-
-    def __exit__(self, *args):
-        return self._condition.__exit__(*args)
-
-    def record(self, cnxid, cnxset):
-        """Inform the tracker that a cnxid has acquired a cnxset
-
-        This method is to be used by Connection objects.
-
-        This method fails when:
-        - The cnxid already has a recorded cnxset.
-        - The tracker is not active anymore.
-
-        Notes about the caller:
-        (1) It is responsible for retrieving a cnxset.
-        (2) It must be prepared to release the cnxset if the
-            `cnxsettracker.forget` call fails.
-        (3) It should acquire the tracker lock until the very end of the operation.
-        (4) However it must only lock the CnxSetTracker object after having
-            retrieved the cnxset to prevent deadlock.
-
-        A typical usage look like::
-
-        cnxset = repo._get_cnxset() # (1)
-        try:
-            with cnxset_tracker: # (3) and (4)
-                cnxset_tracker.record(caller.id, cnxset)
-                # (3') operation ends when caller is in expected state only
-                caller.cnxset = cnxset
-        except Exception:
-            repo._free_cnxset(cnxset) # (2)
-            raise
-        """
-        # dubious since the caller is supposed to have acquired it anyway.
-        with self._condition:
-            if not self._active:
-                raise SessionClosedError('Closed')
-            old = self._record.get(cnxid)
-            if old is not None:
-                raise ValueError('connection "%s" already has a cnx_set (%r)'
-                                 % (cnxid, old))
-            self._record[cnxid] = cnxset
-
-    def forget(self, cnxid, cnxset):
-        """Inform the tracker that a cnxid have release a cnxset
-
-        This methode is to be used by Connection object.
-
-        This method fails when:
-        - The cnxset for the cnxid does not match the recorded one.
-
-        Notes about the caller:
-        (1) It is responsible for releasing the cnxset.
-        (2) It should acquire the tracker lock during the operation to ensure
-            the internal tracker state is always accurate regarding its own state.
-
-        A typical usage look like::
-
-        cnxset = caller.cnxset
-        try:
-            with cnxset_tracker:
-                # (2) you can not have caller.cnxset out of sync with
-                #     cnxset_tracker state while unlocked
-                caller.cnxset = None
-                cnxset_tracker.forget(caller.id, cnxset)
-        finally:
-            cnxset = repo._free_cnxset(cnxset) # (1)
-        """
-        with self._condition:
-            old = self._record.get(cnxid, None)
-            if old is not cnxset:
-                raise ValueError('recorded cnxset for "%s" mismatch: %r != %r'
-                                 % (cnxid, old, cnxset))
-            self._record.pop(cnxid)
-            self._condition.notify_all()
-
-    def close(self):
-        """Marks the tracker as inactive.
-
-        This method is to be used by Session objects.
-
-        An inactive tracker does not accept new records anymore.
-        """
-        with self._condition:
-            self._active = False
-
-    def wait(self, timeout=10):
-        """Wait for all recorded cnxsets to be released
-
-        This method is to be used by Session objects.
-
-        Returns a tuple of connection ids that remain open.
-        """
-        with self._condition:
-            if  self._active:
-                raise RuntimeError('Cannot wait on active tracker.'
-                                   ' Call tracker.close() first')
-            while self._record and timeout > 0:
-                start = time()
-                self._condition.wait(timeout)
-                timeout -= time() - start
-            return tuple(self._record)
-
-
-def _with_cnx_set(func):
-    """decorator for Connection method that ensure they run with a cnxset """
-    @functools.wraps(func)
-    def wrapper(cnx, *args, **kwargs):
-        with cnx.ensure_cnx_set:
-            return func(cnx, *args, **kwargs)
-    return wrapper
 
 def _open_only(func):
     """decorator for Connection method that check it is open"""
@@ -389,8 +189,9 @@
 
     Database connection resources:
 
-      :attr:`running_dbapi_query`, boolean flag telling if the executing query
-      is coming from a dbapi connection or is a query from within the repository
+      :attr:`hooks_in_progress`, boolean flag telling if the executing
+      query is coming from a repoapi connection or is a query from
+      within the repository (e.g. started by hooks)
 
       :attr:`cnxset`, the connections set to use to execute queries on sources.
       If the transaction is read only, the connection set may be freed between
@@ -406,12 +207,18 @@
       'transaction' (we want to keep the connections set during all the
       transaction, with or without writing)
 
-    Internal transaction data:
+    Shared data:
 
-      :attr:`data` is a dictionary containing some shared data
-      cleared at the end of the transaction. Hooks and operations may put
-      arbitrary data in there, and this may also be used as a communication
-      channel between the client and the repository.
+      :attr:`data` is a dictionary bound to the underlying session,
+      who will be present for the life time of the session. This may
+      be useful for web clients that rely on the server for managing
+      bits of session-scoped data.
+
+      :attr:`transaction_data` is a dictionary cleared at the end of
+      the transaction. Hooks and operations may put arbitrary data in
+      there.
+
+    Internal state:
 
       :attr:`pending_operations`, ordered list of operations to be processed on
       commit/rollback
@@ -438,33 +245,20 @@
       read/write security is currently activated.
 
     """
+    is_request = False
+    hooks_in_progress = False
+    is_repo_in_memory = True # bw compat
 
-    is_request = False
-
-    def __init__(self, session, cnxid=None, session_handled=False):
+    def __init__(self, session):
         # using super(Connection, self) confuse some test hack
         RequestSessionBase.__init__(self, session.vreg)
-        # only the session provide explicite
-        if cnxid is not None:
-            assert session_handled # only session profive explicite cnxid
         #: connection unique id
         self._open = None
-        if cnxid is None:
-            cnxid = '%s-%s' % (session.sessionid, uuid4().hex)
-        self.connectionid = cnxid
+        self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex)
+        self.session = session
         self.sessionid = session.sessionid
-        #: self._session_handled
-        #: are the life cycle of this Connection automatically controlled by the
-        #: Session This is the old backward compatibility mode
-        self._session_handled = session_handled
         #: reentrance handling
         self.ctx_count = 0
-        #: count the number of entry in a context needing a cnxset
-        self._cnxset_count = 0
-        #: Boolean for compat with the older explicite set_cnxset/free_cnx API
-        #: When a call set_cnxset is done, no automatic freeing will be done
-        #: until free_cnx is called.
-        self._auto_free_cnx_set = True
 
         #: server.Repository object
         self.repo = session.repo
@@ -474,16 +268,8 @@
         # other session utility
         self._session_timestamp = session._timestamp
 
-        #: connection handling mode
-        self.mode = session.default_mode
-        #: connection set used to execute queries on sources
-        self._cnxset = None
-        #: CnxSetTracker used to report cnxset usage
-        self._cnxset_tracker = session._cnxset_tracker
-        #: is this connection from a client or internal to the repo
-        self.running_dbapi_query = True
         # internal (root) session
-        self.is_internal_session = session.is_internal_session
+        self.is_internal_session = isinstance(session.user, InternalManager)
 
         #: dict containing arbitrary data cleared at the end of the transaction
         self.transaction_data = {}
@@ -506,7 +292,7 @@
 
         # undo control
         config = session.repo.config
-        if config.creating or config.repairing or session.is_internal_session:
+        if config.creating or config.repairing or self.is_internal_session:
             self.undo_actions = False
         else:
             self.undo_actions = config['undo-enabled']
@@ -521,21 +307,109 @@
         else:
             self._set_user(session.user)
 
+    @_open_only
+    def source_defs(self):
+        """Return the definition of sources used by the repository."""
+        return self.session.repo.source_defs()
 
-    # live cycle handling ####################################################
+    @_open_only
+    def get_schema(self):
+        """Return the schema currently used by the repository."""
+        return self.session.repo.source_defs()
+
+    @_open_only
+    def get_option_value(self, option):
+        """Return the value for `option` in the configuration."""
+        return self.session.repo.get_option_value(option)
+
+    # transaction api
+
+    @_open_only
+    def undoable_transactions(self, ueid=None, **actionfilters):
+        """Return a list of undoable transaction objects by the connection's
+        user, ordered by descendant transaction time.
+
+        Managers may filter according to user (eid) who has done the transaction
+        using the `ueid` argument. Others will only see their own transactions.
+
+        Additional filtering capabilities is provided by using the following
+        named arguments:
+
+        * `etype` to get only transactions creating/updating/deleting entities
+          of the given type
+
+        * `eid` to get only transactions applied to entity of the given eid
+
+        * `action` to get only transactions doing the given action (action in
+          'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
+          'D'.
+
+        * `public`: when additional filtering is provided, they are by default
+          only searched in 'public' actions, unless a `public` argument is given
+          and set to false.
+        """
+        return self.repo.system_source.undoable_transactions(self, ueid,
+                                                             **actionfilters)
+
+    @_open_only
+    def transaction_info(self, txuuid):
+        """Return transaction object for the given uid.
+
+        raise `NoSuchTransaction` if not found or if session's user is
+        not allowed (eg not in managers group and the transaction
+        doesn't belong to him).
+        """
+        return self.repo.system_source.tx_info(self, txuuid)
+
+    @_open_only
+    def transaction_actions(self, txuuid, public=True):
+        """Return an ordered list of actions effectued during that transaction.
+
+        If public is true, return only 'public' actions, i.e. not ones
+        triggered under the cover by hooks, else return all actions.
+
+        raise `NoSuchTransaction` if the transaction is not found or
+        if the user is not allowed (eg not in managers group).
+        """
+        return self.repo.system_source.tx_actions(self, txuuid, public)
+
+    @_open_only
+    def undo_transaction(self, txuuid):
+        """Undo the given transaction. Return potential restoration errors.
+
+        raise `NoSuchTransaction` if not found or if user is not
+        allowed (eg not in managers group).
+        """
+        return self.repo.system_source.undo_transaction(self, txuuid)
+
+    # life cycle handling ####################################################
 
     def __enter__(self):
         assert self._open is None # first opening
         self._open = True
+        self.cnxset = self.repo._get_cnxset()
         return self
 
     def __exit__(self, exctype=None, excvalue=None, tb=None):
         assert self._open # actually already open
-        assert self._cnxset_count == 0
         self.rollback()
         self._open = False
+        self.cnxset.cnxset_freed()
+        self.repo._free_cnxset(self.cnxset)
+        self.cnxset = None
 
+    @contextmanager
+    def running_hooks_ops(self):
+        """this context manager should be called whenever hooks or operations
+        are about to be run (but after hook selection)
 
+        It will help the undo logic record pertinent metadata or some
+        hooks to run (or not) depending on who/what issued the query.
+        """
+        prevmode = self.hooks_in_progress
+        self.hooks_in_progress = True
+        yield
+        self.hooks_in_progress = prevmode
 
     # shared data handling ###################################################
 
@@ -580,83 +454,27 @@
         self.local_perm_cache.clear()
         self.rewriter = RQLRewriter(self)
 
-    # Connection Set Management ###############################################
-    @property
-    @_open_only
-    def cnxset(self):
-        return self._cnxset
-
-    @cnxset.setter
-    @_open_only
-    def cnxset(self, new_cnxset):
-        with self._cnxset_tracker:
-            old_cnxset = self._cnxset
-            if new_cnxset is old_cnxset:
-                return #nothing to do
-            if old_cnxset is not None:
-                old_cnxset.rollback()
-                self._cnxset = None
-                self.ctx_count -= 1
-                self._cnxset_tracker.forget(self.connectionid, old_cnxset)
-            if new_cnxset is not None:
-                self._cnxset_tracker.record(self.connectionid, new_cnxset)
-                self._cnxset = new_cnxset
-                self.ctx_count += 1
-
-    @_open_only
-    def _set_cnxset(self):
-        """the connection need a connections set to execute some queries"""
-        if self.cnxset is None:
-            cnxset = self.repo._get_cnxset()
-            try:
-                self.cnxset = cnxset
-            except:
-                self.repo._free_cnxset(cnxset)
-                raise
-        return self.cnxset
-
-    @_open_only
-    def _free_cnxset(self, ignoremode=False):
-        """the connection is no longer using its connections set, at least for some time"""
-        # cnxset may be none if no operation has been done since last commit
-        # or rollback
-        cnxset = self.cnxset
-        if cnxset is not None and (ignoremode or self.mode == 'read'):
-            assert self._cnxset_count == 0
-            try:
-                self.cnxset = None
-            finally:
-                cnxset.cnxset_freed()
-                self.repo._free_cnxset(cnxset)
-
     @deprecated('[3.19] cnxset are automatically managed now.'
                 ' stop using explicit set and free.')
     def set_cnxset(self):
-        self._auto_free_cnx_set = False
-        return self._set_cnxset()
+        pass
 
     @deprecated('[3.19] cnxset are automatically managed now.'
                 ' stop using explicit set and free.')
     def free_cnxset(self, ignoremode=False):
-        self._auto_free_cnx_set = True
-        return self._free_cnxset(ignoremode=ignoremode)
-
+        pass
 
     @property
     @contextmanager
     @_open_only
+    @deprecated('[3.21] a cnxset is automatically set on __enter__ call now.'
+                ' stop using .ensure_cnx_set')
     def ensure_cnx_set(self):
-        assert self._cnxset_count >= 0
-        if self._cnxset_count == 0:
-            self._set_cnxset()
-        try:
-            self._cnxset_count += 1
-            yield
-        finally:
-            self._cnxset_count = max(self._cnxset_count - 1, 0)
-            if self._cnxset_count == 0 and self._auto_free_cnx_set:
-                self._free_cnxset()
+        yield
 
+    @property
+    def anonymous_connection(self):
+        return self.session.anonymous_session
 
     # Entity cache management #################################################
     #
@@ -939,27 +757,7 @@
     @read_security.setter
     @_open_only
     def read_security(self, activated):
-        oldmode = self._read_security
         self._read_security = activated
-        # running_dbapi_query used to detect hooks triggered by a 'dbapi' query
-        # (eg not issued on the session). This is tricky since we the execution
-        # model of a (write) user query is:
-        #
-        # repository.execute (security enabled)
-        #  \-> querier.execute
-        #       \-> repo.glob_xxx (add/update/delete entity/relation)
-        #            \-> deactivate security before calling hooks
-        #                 \-> WE WANT TO CHECK QUERY NATURE HERE
-        #                      \-> potentially, other calls to querier.execute
-        #
-        # so we can't rely on simply checking session.read_security, but
-        # recalling the first transition from DEFAULT_SECURITY to something
-        # else (False actually) is not perfect but should be enough
-        #
-        # also reset running_dbapi_query to true when we go back to
-        # DEFAULT_SECURITY
-        self.running_dbapi_query = (oldmode is DEFAULT_SECURITY
-                                    or activated is DEFAULT_SECURITY)
 
     # undo support ############################################################
 
@@ -971,7 +769,7 @@
     def transaction_uuid(self, set=True):
         uuid = self.transaction_data.get('tx_uuid')
         if set and uuid is None:
-            self.transaction_data['tx_uuid'] = uuid = uuid4().hex
+            self.transaction_data['tx_uuid'] = uuid = unicode(uuid4().hex)
             self.repo.system_source.start_undoable_transaction(self, uuid)
         return uuid
 
@@ -988,7 +786,6 @@
         return self.repo.source_defs()
 
     @deprecated('[3.19] use .entity_metas(eid) instead')
-    @_with_cnx_set
     @_open_only
     def describe(self, eid, asdict=False):
         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
@@ -999,7 +796,6 @@
             return metas
         return etype, source, extid
 
-    @_with_cnx_set
     @_open_only
     def entity_metas(self, eid):
         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
@@ -1008,7 +804,6 @@
 
     # core method #############################################################
 
-    @_with_cnx_set
     @_open_only
     def execute(self, rql, kwargs=None, build_descr=True):
         """db-api like method directly linked to the querier execute method.
@@ -1022,21 +817,16 @@
         return rset
 
     @_open_only
-    def rollback(self, free_cnxset=True, reset_pool=None):
+    def rollback(self, free_cnxset=None, reset_pool=None):
         """rollback the current transaction"""
-        if reset_pool is not None:
-            warn('[3.13] use free_cnxset argument instead for reset_pool',
+        if free_cnxset is not None:
+            warn('[3.21] free_cnxset is now unneeded',
                  DeprecationWarning, stacklevel=2)
-            free_cnxset = reset_pool
-        if self._cnxset_count != 0:
-            # we are inside ensure_cnx_set, don't lose it
-            free_cnxset = False
+        if reset_pool is not None:
+            warn('[3.13] reset_pool is now unneeded',
+                 DeprecationWarning, stacklevel=2)
         cnxset = self.cnxset
-        if cnxset is None:
-            self.clear()
-            self._session_timestamp.touch()
-            self.debug('rollback transaction %s done (no db activity)', self.connectionid)
-            return
+        assert cnxset is not None
         try:
             # by default, operations are executed with security turned off
             with self.security_enabled(False, False):
@@ -1051,26 +841,18 @@
                 self.debug('rollback for transaction %s done', self.connectionid)
         finally:
             self._session_timestamp.touch()
-            if free_cnxset:
-                self._free_cnxset(ignoremode=True)
             self.clear()
 
     @_open_only
-    def commit(self, free_cnxset=True, reset_pool=None):
+    def commit(self, free_cnxset=None, reset_pool=None):
         """commit the current session's transaction"""
-        if reset_pool is not None:
-            warn('[3.13] use free_cnxset argument instead for reset_pool',
+        if free_cnxset is not None:
+            warn('[3.21] free_cnxset is now unneeded',
                  DeprecationWarning, stacklevel=2)
-            free_cnxset = reset_pool
-        if self.cnxset is None:
-            assert not self.pending_operations
-            self.clear()
-            self._session_timestamp.touch()
-            self.debug('commit transaction %s done (no db activity)', self.connectionid)
-            return
-        if self._cnxset_count != 0:
-            # we are inside ensure_cnx_set, don't lose it
-            free_cnxset = False
+        if reset_pool is not None:
+            warn('[3.13] reset_pool is now unneeded',
+                 DeprecationWarning, stacklevel=2)
+        assert self.cnxset is not None
         cstate = self.commit_state
         if cstate == 'uncommitable':
             raise QueryError('transaction must be rolled back')
@@ -1094,13 +876,14 @@
                 if debug:
                     print self.commit_state, '*' * 20
                 try:
-                    while self.pending_operations:
-                        operation = self.pending_operations.pop(0)
-                        operation.processed = 'precommit'
-                        processed.append(operation)
-                        if debug:
-                            print operation
-                        operation.handle_event('precommit_event')
+                    with self.running_hooks_ops():
+                        while self.pending_operations:
+                            operation = self.pending_operations.pop(0)
+                            operation.processed = 'precommit'
+                            processed.append(operation)
+                            if debug:
+                                print operation
+                            operation.handle_event('precommit_event')
                     self.pending_operations[:] = processed
                     self.debug('precommit transaction %s done', self.connectionid)
                 except BaseException:
@@ -1117,56 +900,52 @@
                     operation.failed = True
                     if debug:
                         print self.commit_state, '*' * 20
-                    for operation in reversed(processed):
-                        if debug:
-                            print operation
-                        try:
-                            operation.handle_event('revertprecommit_event')
-                        except BaseException:
-                            self.critical('error while reverting precommit',
-                                          exc_info=True)
+                    with self.running_hooks_ops():
+                        for operation in reversed(processed):
+                            if debug:
+                                print operation
+                            try:
+                                operation.handle_event('revertprecommit_event')
+                            except BaseException:
+                                self.critical('error while reverting precommit',
+                                              exc_info=True)
                     # XXX use slice notation since self.pending_operations is a
                     # read-only property.
                     self.pending_operations[:] = processed + self.pending_operations
-                    self.rollback(free_cnxset)
+                    self.rollback()
                     raise
                 self.cnxset.commit()
                 self.commit_state = 'postcommit'
                 if debug:
                     print self.commit_state, '*' * 20
-                while self.pending_operations:
-                    operation = self.pending_operations.pop(0)
-                    if debug:
-                        print operation
-                    operation.processed = 'postcommit'
-                    try:
-                        operation.handle_event('postcommit_event')
-                    except BaseException:
-                        self.critical('error while postcommit',
-                                      exc_info=sys.exc_info())
+                with self.running_hooks_ops():
+                    while self.pending_operations:
+                        operation = self.pending_operations.pop(0)
+                        if debug:
+                            print operation
+                        operation.processed = 'postcommit'
+                        try:
+                            operation.handle_event('postcommit_event')
+                        except BaseException:
+                            self.critical('error while postcommit',
+                                          exc_info=sys.exc_info())
                 self.debug('postcommit transaction %s done', self.connectionid)
                 return self.transaction_uuid(set=False)
         finally:
             self._session_timestamp.touch()
-            if free_cnxset:
-                self._free_cnxset(ignoremode=True)
             self.clear()
 
     # resource accessors ######################################################
 
-    @_with_cnx_set
     @_open_only
     def call_service(self, regid, **kwargs):
         self.debug('calling service %s', regid)
         service = self.vreg['services'].select(regid, self, **kwargs)
         return service.call(**kwargs)
 
-    @_with_cnx_set
     @_open_only
     def system_sql(self, sql, args=None, rollback_on_failure=True):
         """return a sql cursor on the system database"""
-        if sql.split(None, 1)[0].upper() != 'SELECT':
-            self.mode = 'write'
         source = self.repo.system_source
         try:
             return source.doexec(self, sql, args, rollback=rollback_on_failure)
@@ -1202,18 +981,6 @@
         args['fset'] = write_attr
     return property(**args)
 
-def cnx_meth(meth_name):
-    """return a function forwarding calls to connection.
-
-    This is to be used by session"""
-    @deprecated('[3.19] use a Connection object instead')
-    def meth_from_cnx(session, *args, **kwargs):
-        result = getattr(session._cnx, meth_name)(*args, **kwargs)
-        if getattr(result, '_cw', None) is not None:
-            result._cw = session
-        return result
-    meth_from_cnx.__doc__ = getattr(Connection, meth_name).__doc__
-    return meth_from_cnx
 
 class Timestamp(object):
 
@@ -1227,149 +994,37 @@
         return float(self.value)
 
 
-class Session(RequestSessionBase): # XXX repoapi: stop being a
-                                   # RequestSessionBase at some point
+class Session(object):
     """Repository user session
 
     This ties all together:
      * session id,
      * user,
-     * connections set,
      * other session data.
-
-    **About session storage / transactions**
-
-    Here is a description of internal session attributes. Besides :attr:`data`
-    and :attr:`transaction_data`, you should not have to use attributes
-    described here but higher level APIs.
-
-      :attr:`data` is a dictionary containing shared data, used to communicate
-      extra information between the client and the repository
-
-      :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one
-      for each running connection. The key is the connection id. By default
-      the connection id is the thread name but it can be otherwise (per dbapi
-      cursor for instance, or per thread name *from another process*).
-
-      :attr:`__threaddata` is a thread local storage whose `cnx` attribute
-      refers to the proper instance of :class:`Connection` according to the
-      connection.
-
-    You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`,
-    simply access connection data transparently through the :attr:`_cnx`
-    property. Also, you usually don't have to access it directly since current
-    connection's data may be accessed/modified through properties / methods:
-
-      :attr:`connection_data`, similarly to :attr:`data`, is a dictionary
-      containing some shared data that should be cleared at the end of the
-      connection. Hooks and operations may put arbitrary data in there, and
-      this may also be used as a communication channel between the client and
-      the repository.
-
-    .. automethod:: cubicweb.server.session.Session.get_shared_data
-    .. automethod:: cubicweb.server.session.Session.set_shared_data
-    .. automethod:: cubicweb.server.session.Session.added_in_transaction
-    .. automethod:: cubicweb.server.session.Session.deleted_in_transaction
-
-    Connection state information:
-
-      :attr:`running_dbapi_query`, boolean flag telling if the executing query
-      is coming from a dbapi connection or is a query from within the repository
-
-      :attr:`cnxset`, the connections set to use to execute queries on sources.
-      During a transaction, the connection set may be freed so that is may be
-      used by another session as long as no writing is done. This means we can
-      have multiple sessions with a reasonably low connections set pool size.
-
-      .. automethod:: cubicweb.server.session.Session.set_cnxset
-      .. automethod:: cubicweb.server.session.Session.free_cnxset
-
-      :attr:`mode`, string telling the connections set handling mode, may be one
-      of 'read' (connections set may be freed), 'write' (some write was done in
-      the connections set, it can't be freed before end of the transaction),
-      'transaction' (we want to keep the connections set during all the
-      transaction, with or without writing)
-
-      :attr:`pending_operations`, ordered list of operations to be processed on
-      commit/rollback
-
-      :attr:`commit_state`, describing the transaction commit state, may be one
-      of None (not yet committing), 'precommit' (calling precommit event on
-      operations), 'postcommit' (calling postcommit event on operations),
-      'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
-      has been raised during the transaction and so it must be rolled back).
-
-    .. automethod:: cubicweb.server.session.Session.commit
-    .. automethod:: cubicweb.server.session.Session.rollback
-    .. automethod:: cubicweb.server.session.Session.close
-    .. automethod:: cubicweb.server.session.Session.closed
-
-    Security level Management:
-
-      :attr:`read_security` and :attr:`write_security`, boolean flags telling if
-      read/write security is currently activated.
-
-    .. automethod:: cubicweb.server.session.Session.security_enabled
-
-    Hooks Management:
-
-      :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
-
-      :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is
-      `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
-
-      :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is
-      `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
-
-    .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but
-    .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but
-    .. automethod:: cubicweb.server.session.Session.is_hook_category_activated
-    .. automethod:: cubicweb.server.session.Session.is_hook_activated
-
-    Data manipulation:
-
-    .. automethod:: cubicweb.server.session.Session.add_relation
-    .. automethod:: cubicweb.server.session.Session.add_relations
-    .. automethod:: cubicweb.server.session.Session.delete_relation
-
-    Other:
-
-    .. automethod:: cubicweb.server.session.Session.call_service
-
-
-
     """
-    is_request = False
-    is_internal_session = False
 
     def __init__(self, user, repo, cnxprops=None, _id=None):
-        super(Session, self).__init__(repo.vreg)
         self.sessionid = _id or make_uid(unormalize(user.login).encode('UTF8'))
         self.user = user # XXX repoapi: deprecated and store only a login.
         self.repo = repo
+        self.vreg = repo.vreg
         self._timestamp = Timestamp()
-        self.default_mode = 'read'
-        # short cut to querier .execute method
-        self._execute = repo.querier.execute
-        # shared data, used to communicate extra information between the client
-        # and the rql server
         self.data = {}
-        # i18n initialization
-        self.set_language(user.prefered_language())
-        ### internals
-        # Connection of this section
-        self._cnxs = {} # XXX repoapi: remove this when nobody use the session
-                        # as a Connection
-        # Data local to the thread
-        self.__threaddata = threading.local() # XXX repoapi: remove this when
-                                              # nobody use the session as a Connection
-        self._cnxset_tracker = CnxSetTracker()
-        self._closed = False
-        self._lock = threading.RLock()
+        self.closed = False
+
+    def close(self):
+        self.closed = True
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        pass
 
     def __unicode__(self):
         return '<session %s (%s 0x%x)>' % (
             unicode(self.user.login), self.sessionid, id(self))
+
     @property
     def timestamp(self):
         return float(self._timestamp)
@@ -1390,55 +1045,6 @@
         """
         return Connection(self)
 
-    def _get_cnx(self, cnxid):
-        """return the <cnxid> connection attached to this session
-
-        Connection is created if necessary"""
-        with self._lock: # no connection exist with the same id
-            try:
-                if self.closed:
-                    raise SessionClosedError('try to access connections set on'
-                                             ' a closed session %s' % self.id)
-                cnx = self._cnxs[cnxid]
-                assert cnx._session_handled
-            except KeyError:
-                cnx = Connection(self, cnxid=cnxid, session_handled=True)
-                self._cnxs[cnxid] = cnx
-                cnx.__enter__()
-        return cnx
-
-    def _close_cnx(self, cnx):
-        """Close a Connection related to a session"""
-        assert cnx._session_handled
-        cnx.__exit__()
-        self._cnxs.pop(cnx.connectionid, None)
-        try:
-            if self.__threaddata.cnx is cnx:
-                del self.__threaddata.cnx
-        except AttributeError:
-            pass
-
-    def set_cnx(self, cnxid=None):
-        # XXX repoapi: remove this when nobody use the session as a Connection
-        """set the default connection of the current thread to <cnxid>
-
-        Connection is created if necessary"""
-        if cnxid is None:
-            cnxid = threading.currentThread().getName()
-        cnx = self._get_cnx(cnxid)
-        # New style session should not be accesed through the session.
-        assert cnx._session_handled
-        self.__threaddata.cnx = cnx
-
-    @property
-    def _cnx(self):
-        """default connection for current session in current thread"""
-        try:
-            return self.__threaddata.cnx
-        except AttributeError:
-            self.set_cnx()
-            return self.__threaddata.cnx
-
     @deprecated('[3.19] use a Connection object instead')
     def get_option_value(self, option, foreid=None):
         if foreid is not None:
@@ -1446,108 +1052,6 @@
                  stacklevel=2)
         return self.repo.get_option_value(option)
 
-    @deprecated('[3.19] use a Connection object instead')
-    def transaction(self, free_cnxset=True):
-        """return context manager to enter a transaction for the session: when
-        exiting the `with` block on exception, call `session.rollback()`, else
-        call `session.commit()` on normal exit.
-
-        The `free_cnxset` will be given to rollback/commit methods to indicate
-        whether the connections set should be freed or not.
-        """
-        return transaction(self, free_cnxset)
-
-    add_relation = cnx_meth('add_relation')
-    add_relations = cnx_meth('add_relations')
-    delete_relation = cnx_meth('delete_relation')
-
-    # relations cache handling #################################################
-
-    update_rel_cache_add = cnx_meth('update_rel_cache_add')
-    update_rel_cache_del = cnx_meth('update_rel_cache_del')
-
-    # resource accessors ######################################################
-
-    system_sql = cnx_meth('system_sql')
-    deleted_in_transaction = cnx_meth('deleted_in_transaction')
-    added_in_transaction = cnx_meth('added_in_transaction')
-    rtype_eids_rdef = cnx_meth('rtype_eids_rdef')
-
-    # security control #########################################################
-
-    @deprecated('[3.19] use a Connection object instead')
-    def security_enabled(self, read=None, write=None):
-        return _session_security_enabled(self, read=read, write=write)
-
-    read_security = cnx_attr('read_security', writable=True)
-    write_security = cnx_attr('write_security', writable=True)
-    running_dbapi_query = cnx_attr('running_dbapi_query')
-
-    # hooks activation control #################################################
-    # all hooks should be activated during normal execution
-
-
-    @deprecated('[3.19] use a Connection object instead')
-    def allow_all_hooks_but(self, *categories):
-        return _session_hooks_control(self, HOOKS_ALLOW_ALL, *categories)
-    @deprecated('[3.19] use a Connection object instead')
-    def deny_all_hooks_but(self, *categories):
-        return _session_hooks_control(self, HOOKS_DENY_ALL, *categories)
-
-    hooks_mode = cnx_attr('hooks_mode')
-
-    disabled_hook_categories = cnx_attr('disabled_hook_cats')
-    enabled_hook_categories = cnx_attr('enabled_hook_cats')
-    disable_hook_categories = cnx_meth('disable_hook_categories')
-    enable_hook_categories = cnx_meth('enable_hook_categories')
-    is_hook_category_activated = cnx_meth('is_hook_category_activated')
-    is_hook_activated = cnx_meth('is_hook_activated')
-
-    # connection management ###################################################
-
-    @deprecated('[3.19] use a Connection object instead')
-    def keep_cnxset_mode(self, mode):
-        """set `mode`, e.g. how the session will keep its connections set:
-
-        * if mode == 'write', the connections set is freed after each read
-          query, but kept until the transaction's end (eg commit or rollback)
-          when a write query is detected (eg INSERT/SET/DELETE queries)
-
-        * if mode == 'transaction', the connections set is only freed after the
-          transaction's end
-
-        notice that a repository has a limited set of connections sets, and a
-        session has to wait for a free connections set to run any rql query
-        (unless it already has one set).
-        """
-        assert mode in ('transaction', 'write')
-        if mode == 'transaction':
-            self.default_mode = 'transaction'
-        else: # mode == 'write'
-            self.default_mode = 'read'
-
-    mode = cnx_attr('mode', writable=True)
-    commit_state = cnx_attr('commit_state', writable=True)
-
-    @property
-    @deprecated('[3.19] use a Connection object instead')
-    def cnxset(self):
-        """connections set, set according to transaction mode for each query"""
-        if self._closed:
-            self.free_cnxset(True)
-            raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
-        return self._cnx.cnxset
-
-    def set_cnxset(self):
-        """the session need a connections set to execute some queries"""
-        with self._lock: # can probably be removed
-            if self._closed:
-                self.free_cnxset(True)
-                raise SessionClosedError('try to set connections set on a closed session %s' % self.id)
-            return self._cnx.set_cnxset()
-    free_cnxset = cnx_meth('free_cnxset')
-    ensure_cnx_set = cnx_attr('ensure_cnx_set')
-
     def _touch(self):
         """update latest session usage timestamp and reset mode to read"""
         self._timestamp.touch()
@@ -1559,156 +1063,6 @@
         assert value == {}
         pass
 
-    # shared data handling ###################################################
-
-    @deprecated('[3.19] use session or transaction data')
-    def get_shared_data(self, key, default=None, pop=False, txdata=False):
-        """return value associated to `key` in session data"""
-        if txdata:
-            return self._cnx.get_shared_data(key, default, pop, txdata=True)
-        else:
-            data = self.data
-        if pop:
-            return data.pop(key, default)
-        else:
-            return data.get(key, default)
-
-    @deprecated('[3.19] use session or transaction data')
-    def set_shared_data(self, key, value, txdata=False):
-        """set value associated to `key` in session data"""
-        if txdata:
-            return self._cnx.set_shared_data(key, value, txdata=True)
-        else:
-            self.data[key] = value
-
-    # server-side service call #################################################
-
-    call_service = cnx_meth('call_service')
-
-    # request interface #######################################################
-
-    @property
-    @deprecated('[3.19] use a Connection object instead')
-    def cursor(self):
-        """return a rql cursor"""
-        return self
-
-    set_entity_cache  = cnx_meth('set_entity_cache')
-    entity_cache      = cnx_meth('entity_cache')
-    cache_entities    = cnx_meth('cached_entities')
-    drop_entity_cache = cnx_meth('drop_entity_cache')
-
-    source_defs = cnx_meth('source_defs')
-    entity_metas = cnx_meth('entity_metas')
-    describe = cnx_meth('describe') # XXX deprecated in 3.19
-
-
-    @deprecated('[3.19] use a Connection object instead')
-    def execute(self, *args, **kwargs):
-        """db-api like method directly linked to the querier execute method.
-
-        See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
-        """
-        rset = self._cnx.execute(*args, **kwargs)
-        rset.req = self
-        return rset
-
-    def _clear_thread_data(self, free_cnxset=True):
-        """remove everything from the thread local storage, except connections set
-        which is explicitly removed by free_cnxset, and mode which is set anyway
-        by _touch
-        """
-        try:
-            cnx = self.__threaddata.cnx
-        except AttributeError:
-            pass
-        else:
-            if free_cnxset:
-                cnx._free_cnxset()
-                if cnx.ctx_count == 0:
-                    self._close_cnx(cnx)
-                else:
-                    cnx.clear()
-            else:
-                cnx.clear()
-
-    @deprecated('[3.19] use a Connection object instead')
-    def commit(self, free_cnxset=True, reset_pool=None):
-        """commit the current session's transaction"""
-        cstate = self._cnx.commit_state
-        if cstate == 'uncommitable':
-            raise QueryError('transaction must be rolled back')
-        try:
-            return self._cnx.commit(free_cnxset, reset_pool)
-        finally:
-            self._clear_thread_data(free_cnxset)
-
-    @deprecated('[3.19] use a Connection object instead')
-    def rollback(self, *args, **kwargs):
-        """rollback the current session's transaction"""
-        return self._rollback(*args, **kwargs)
-
-    def _rollback(self, free_cnxset=True, **kwargs):
-        try:
-            return self._cnx.rollback(free_cnxset, **kwargs)
-        finally:
-            self._clear_thread_data(free_cnxset)
-
-    def close(self):
-        # do not close connections set on session close, since they are shared now
-        tracker = self._cnxset_tracker
-        with self._lock:
-            self._closed = True
-        tracker.close()
-        if self._cnx._session_handled:
-            self._rollback()
-        self.debug('waiting for open connection of session: %s', self)
-        timeout = 10
-        pendings = tracker.wait(timeout)
-        if pendings:
-            self.error('%i connection still alive after 10 seconds, will close '
-                       'session anyway', len(pendings))
-            for cnxid in pendings:
-                cnx = self._cnxs.get(cnxid)
-                if cnx is not None:
-                    # drop cnx.cnxset
-                    with tracker:
-                        try:
-                            cnxset = cnx.cnxset
-                            if cnxset is None:
-                                continue
-                            cnx.cnxset = None
-                        except RuntimeError:
-                            msg = 'issue while force free of cnxset in %s'
-                            self.error(msg, cnx)
-                    # cnxset.reconnect() do an hard reset of the cnxset
-                    # it force it to be freed
-                    cnxset.reconnect()
-                    self.repo._free_cnxset(cnxset)
-        del self.__threaddata
-        del self._cnxs
-
-    @property
-    def closed(self):
-        return not hasattr(self, '_cnxs')
-
-    # transaction data/operations management ##################################
-
-    transaction_data = cnx_attr('transaction_data')
-    pending_operations = cnx_attr('pending_operations')
-    pruned_hooks_cache = cnx_attr('pruned_hooks_cache')
-    add_operation      = cnx_meth('add_operation')
-
-    # undo support ############################################################
-
-    ertype_supports_undo = cnx_meth('ertype_supports_undo')
-    transaction_inc_action_counter = cnx_meth('transaction_inc_action_counter')
-    transaction_uuid = cnx_meth('transaction_uuid')
-
-    # querier helpers #########################################################
-
-    rql_rewriter = cnx_attr('_rewriter')
-
     # deprecated ###############################################################
 
     @property
@@ -1725,52 +1079,10 @@
     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
         return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
 
-    @property
-    @deprecated("[3.13] use .cnxset attribute instead of .pool")
-    def pool(self):
-        return self.cnxset
-
-    @deprecated("[3.13] use .set_cnxset() method instead of .set_pool()")
-    def set_pool(self):
-        return self.set_cnxset()
-
-    @deprecated("[3.13] use .free_cnxset() method instead of .reset_pool()")
-    def reset_pool(self):
-        return self.free_cnxset()
-
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
 
-Session.HOOKS_ALLOW_ALL = HOOKS_ALLOW_ALL
-Session.HOOKS_DENY_ALL = HOOKS_DENY_ALL
-Session.DEFAULT_SECURITY = DEFAULT_SECURITY
-
-
-
-class InternalSession(Session):
-    """special session created internally by the repository"""
-    is_internal_session = True
-    running_dbapi_query = False
-
-    def __init__(self, repo, cnxprops=None, safe=False):
-        super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
-                                              _id='internal')
-        self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exctype, excvalue, tb):
-        self.close()
-
-    @property
-    def cnxset(self):
-        """connections set, set according to transaction mode for each query"""
-        if self.repo.shutting_down:
-            self.free_cnxset(True)
-            raise ShuttingDown('repository is shutting down')
-        return self._cnx.cnxset
 
 
 class InternalManager(object):
@@ -1778,6 +1090,7 @@
     bootstrapping the repository or creating regular users according to
     repository content
     """
+
     def __init__(self, lang='en'):
         self.eid = -1
         self.login = u'__internal_manager__'
--- a/server/sources/__init__.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/sources/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -128,6 +128,9 @@
     def __eq__(self, other):
         return self.uri == other.uri
 
+    def __ne__(self, other):
+        return not (self == other)
+
     def backup(self, backupfile, confirm, format='native'):
         """method called to create a backup of source's data"""
         pass
@@ -395,7 +398,7 @@
     # system source interface #################################################
 
     def eid_type_source(self, cnx, eid):
-        """return a tuple (type, source, extid) for the entity with id <eid>"""
+        """return a tuple (type, extid, source) for the entity with id <eid>"""
         raise NotImplementedError(self)
 
     def create_eid(self, cnx):
--- a/server/sources/datafeed.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/sources/datafeed.py	Fri Oct 09 17:52:14 2015 +0200
@@ -25,7 +25,7 @@
 from datetime import datetime, timedelta
 from base64 import b64decode
 from cookielib import CookieJar
-
+import urlparse
 from lxml import etree
 
 from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError, UnknownEid
@@ -319,24 +319,45 @@
                 return url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
         return url
 
-    def retrieve_url(self, url, data=None, headers=None):
+    def retrieve_url(self, url):
         """Return stream linked by the given url:
         * HTTP urls will be normalized (see :meth:`normalize_url`)
         * handle file:// URL
         * other will be considered as plain content, useful for testing purpose
+
+        For http URLs, it will try to find a cwclientlib config entry
+        (if available) and use it as requester.
         """
-        if headers is None:
-            headers = {}
-        if url.startswith('http'):
-            url = self.normalize_url(url)
-            if data:
-                self.source.info('POST %s %s', url, data)
-            else:
-                self.source.info('GET %s', url)
-            req = urllib2.Request(url, data, headers)
+        purl = urlparse.urlparse(url)
+        if purl.scheme == 'file':
+            return URLLibResponseAdapter(open(url[7:]), url)
+
+        url = self.normalize_url(url)
+
+        # first, try to use cwclientlib if it's available and if the
+        # url matches a configuration entry in ~/.config/cwclientlibrc
+        try:
+            from cwclientlib import cwproxy_for
+            # parse url again since it has been normalized
+            cnx = cwproxy_for(url)
+            cnx.timeout = self.source.http_timeout
+            self.source.info('Using cwclientlib for %s' % url)
+            resp = cnx.get(url)
+            resp.raise_for_status()
+            return URLLibResponseAdapter(StringIO.StringIO(resp.text), url)
+        except (ImportError, ValueError, EnvironmentError) as exc:
+            # ImportError: not available
+            # ValueError: no config entry found
+            # EnvironmentError: no cwclientlib config file found
+            self.source.debug(str(exc))
+
+        # no chance with cwclientlib, fall back to former implementation
+        if purl.scheme in ('http', 'https'):
+            self.source.info('GET %s', url)
+            req = urllib2.Request(url)
             return _OPENER.open(req, timeout=self.source.http_timeout)
-        if url.startswith('file://'):
-            return URLLibResponseAdapter(open(url[7:]), url)
+
+        # url is probably plain content
         return URLLibResponseAdapter(StringIO.StringIO(url), url)
 
     def add_schema_config(self, schemacfg, checkonly=False):
@@ -489,15 +510,32 @@
         raise NotImplementedError
 
     def is_deleted(self, extid, etype, eid):
-        if extid.startswith('http'):
+        if extid.startswith('file://'):
+            return exists(extid[7:])
+
+        url = self.normalize_url(extid)
+        # first, try to use cwclientlib if it's available and if the
+        # url matches a configuration entry in ~/.config/cwclientlibrc
+        try:
+            from cwclientlib import cwproxy_for
+            # parse url again since it has been normalized
+            cnx = cwproxy_for(url)
+            cnx.timeout = self.source.http_timeout
+            self.source.info('Using cwclientlib for checking %s' % url)
+            return cnx.get(url).status_code == 404
+        except (ImportError, ValueError, EnvironmentError) as exc:
+            # ImportError: not available
+            # ValueError: no config entry found
+            # EnvironmentError: no cwclientlib config file found
+            self.source.debug(str(exc))
+
+        # no chance with cwclientlib, fall back to former implementation
+        if urlparse.urlparse(url).scheme in ('http', 'https'):
             try:
-                _OPENER.open(self.normalize_url(extid), # XXX HTTP HEAD request
-                             timeout=self.source.http_timeout)
+                _OPENER.open(url, timeout=self.source.http_timeout)
             except urllib2.HTTPError as ex:
                 if ex.code == 404:
                     return True
-        elif extid.startswith('file://'):
-            return exists(extid[7:])
         return False
 
 
--- a/server/sources/native.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/sources/native.py	Fri Oct 09 17:52:14 2015 +0200
@@ -43,16 +43,16 @@
 from logilab.common.shellutils import getlogin
 from logilab.database import get_db_helper, sqlgen
 
-from yams import schema2sql as y2sql
 from yams.schema import role_name
 
 from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary,
-                      UniqueTogetherError, UndoTransactionException)
+                      UniqueTogetherError, UndoTransactionException, ViolatedConstraint)
 from cubicweb import transaction as tx, server, neg_role
 from cubicweb.utils import QueryCache
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
 from cubicweb.server import hook
+from cubicweb.server import schema2sql as y2sql
 from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update
 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
 from cubicweb.server.rqlannotation import set_qdata
@@ -60,6 +60,7 @@
 from cubicweb.server.edition import EditedEntity
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
+from cubicweb.statsd_logger import statsd_timeit
 
 
 ATTR_MAP = {}
@@ -272,7 +273,7 @@
          {'type' : 'string',
           'default': 'postgres',
           # XXX use choice type
-          'help': 'database driver (postgres, mysql, sqlite, sqlserver2005)',
+          'help': 'database driver (postgres, sqlite, sqlserver2005)',
           'group': 'native-source', 'level': 0,
           }),
         ('db-host',
@@ -376,6 +377,7 @@
         self._cache.pop('Any X WHERE X eid %s' % eid, None)
         self._cache.pop('Any %s' % eid, None)
 
+    @statsd_timeit
     def sqlexec(self, cnx, sql, args=None):
         """execute the query and return its result"""
         return self.process_result(self.doexec(cnx, sql, args))
@@ -480,6 +482,7 @@
 
     # ISource interface #######################################################
 
+    @statsd_timeit
     def compile_rql(self, rql, sols):
         rqlst = self.repo.vreg.rqlhelper.parse(rql)
         rqlst.restricted_vars = ()
@@ -517,6 +520,7 @@
         # can't claim not supporting a relation
         return True #not rtype == 'content_for'
 
+    @statsd_timeit
     def authenticate(self, cnx, login, **kwargs):
         """return CWUser eid for the given login and other authentication
         information found in kwargs, else raise `AuthenticationError`
@@ -553,31 +557,22 @@
                 self._cache[cachekey] = sql, qargs, cbs
         args = self.merge_args(args, qargs)
         assert isinstance(sql, basestring), repr(sql)
-        try:
-            cursor = self.doexec(cnx, sql, args)
-        except (self.OperationalError, self.InterfaceError):
-            if cnx.mode == 'write':
-                # do not attempt to reconnect if there has been some write
-                # during the transaction
-                raise
-            # FIXME: better detection of deconnection pb
-            self.warning("trying to reconnect")
-            cnx.cnxset.reconnect()
-            cursor = self.doexec(cnx, sql, args)
-        except self.DbapiError as exc:
-            # We get this one with pyodbc and SQL Server when connection was reset
-            if exc.args[0] == '08S01' and cnx.mode != 'write':
-                self.warning("trying to reconnect")
-                cnx.cnxset.reconnect()
-                cursor = self.doexec(cnx, sql, args)
-            else:
-                raise
+        cursor = self.doexec(cnx, sql, args)
         results = self.process_result(cursor, cnx, cbs)
         assert dbg_results(results)
         return results
 
     @contextmanager
-    def _storage_handler(self, entity, event):
+    def _fixup_cw(self, cnx, entity):
+        _cw = entity._cw
+        entity._cw = cnx
+        try:
+            yield
+        finally:
+            entity._cw = _cw
+
+    @contextmanager
+    def _storage_handler(self, cnx, entity, event):
         # 1/ memorize values as they are before the storage is called.
         #    For instance, the BFSStorage will replace the `data`
         #    binary value with a Binary containing the destination path
@@ -592,14 +587,15 @@
         etype = entities[0].__regid__
         for attr, storage in self._storages.get(etype, {}).items():
             for entity in entities:
-                if event == 'deleted':
-                    storage.entity_deleted(entity, attr)
-                else:
-                    edited = entity.cw_edited
-                    if attr in edited:
-                        handler = getattr(storage, 'entity_%s' % event)
-                        to_restore = handler(entity, attr)
-                        restore_values.append((entity, attr, to_restore))
+                with self._fixup_cw(cnx, entity):
+                    if event == 'deleted':
+                        storage.entity_deleted(entity, attr)
+                    else:
+                        edited = entity.cw_edited
+                        if attr in edited:
+                            handler = getattr(storage, 'entity_%s' % event)
+                            to_restore = handler(entity, attr)
+                            restore_values.append((entity, attr, to_restore))
         try:
             yield # 2/ execute the source's instructions
         finally:
@@ -609,22 +605,22 @@
 
     def add_entity(self, cnx, entity):
         """add a new entity to the source"""
-        with self._storage_handler(entity, 'added'):
+        with self._storage_handler(cnx, entity, 'added'):
             attrs = self.preprocess_entity(entity)
             sql = self.sqlgen.insert(SQL_PREFIX + entity.cw_etype, attrs)
             self.doexec(cnx, sql, attrs)
             if cnx.ertype_supports_undo(entity.cw_etype):
-                self._record_tx_action(cnx, 'tx_entity_actions', 'C',
-                                       etype=entity.cw_etype, eid=entity.eid)
+                self._record_tx_action(cnx, 'tx_entity_actions', u'C',
+                                       etype=unicode(entity.cw_etype), eid=entity.eid)
 
     def update_entity(self, cnx, entity):
         """replace an entity in the source"""
-        with self._storage_handler(entity, 'updated'):
+        with self._storage_handler(cnx, entity, 'updated'):
             attrs = self.preprocess_entity(entity)
             if cnx.ertype_supports_undo(entity.cw_etype):
                 changes = self._save_attrs(cnx, entity, attrs)
-                self._record_tx_action(cnx, 'tx_entity_actions', 'U',
-                                       etype=entity.cw_etype, eid=entity.eid,
+                self._record_tx_action(cnx, 'tx_entity_actions', u'U',
+                                       etype=unicode(entity.cw_etype), eid=entity.eid,
                                        changes=self._binary(dumps(changes)))
             sql = self.sqlgen.update(SQL_PREFIX + entity.cw_etype, attrs,
                                      ['cw_eid'])
@@ -632,14 +628,14 @@
 
     def delete_entity(self, cnx, entity):
         """delete an entity from the source"""
-        with self._storage_handler(entity, 'deleted'):
+        with self._storage_handler(cnx, entity, 'deleted'):
             if cnx.ertype_supports_undo(entity.cw_etype):
                 attrs = [SQL_PREFIX + r.type
                          for r in entity.e_schema.subject_relations()
                          if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
                 changes = self._save_attrs(cnx, entity, attrs)
-                self._record_tx_action(cnx, 'tx_entity_actions', 'D',
-                                       etype=entity.cw_etype, eid=entity.eid,
+                self._record_tx_action(cnx, 'tx_entity_actions', u'D',
+                                       etype=unicode(entity.cw_etype), eid=entity.eid,
                                        changes=self._binary(dumps(changes)))
             attrs = {'cw_eid': entity.eid}
             sql = self.sqlgen.delete(SQL_PREFIX + entity.cw_etype, attrs)
@@ -649,16 +645,16 @@
         """add a relation to the source"""
         self._add_relations(cnx,  rtype, [(subject, object)], inlined)
         if cnx.ertype_supports_undo(rtype):
-            self._record_tx_action(cnx, 'tx_relation_actions', 'A',
-                                   eid_from=subject, rtype=rtype, eid_to=object)
+            self._record_tx_action(cnx, 'tx_relation_actions', u'A',
+                                   eid_from=subject, rtype=unicode(rtype), eid_to=object)
 
     def add_relations(self, cnx,  rtype, subj_obj_list, inlined=False):
         """add a relations to the source"""
         self._add_relations(cnx, rtype, subj_obj_list, inlined)
         if cnx.ertype_supports_undo(rtype):
             for subject, object in subj_obj_list:
-                self._record_tx_action(cnx, 'tx_relation_actions', 'A',
-                                       eid_from=subject, rtype=rtype, eid_to=object)
+                self._record_tx_action(cnx, 'tx_relation_actions', u'A',
+                                       eid_from=subject, rtype=unicode(rtype), eid_to=object)
 
     def _add_relations(self, cnx, rtype, subj_obj_list, inlined=False):
         """add a relation to the source"""
@@ -689,8 +685,8 @@
         rschema = self.schema.rschema(rtype)
         self._delete_relation(cnx, subject, rtype, object, rschema.inlined)
         if cnx.ertype_supports_undo(rtype):
-            self._record_tx_action(cnx, 'tx_relation_actions', 'R',
-                                   eid_from=subject, rtype=rtype, eid_to=object)
+            self._record_tx_action(cnx, 'tx_relation_actions', u'R',
+                                   eid_from=subject, rtype=unicode(rtype), eid_to=object)
 
     def _delete_relation(self, cnx, subject, rtype, object, inlined=False):
         """delete a relation from the source"""
@@ -705,6 +701,7 @@
             sql = self.sqlgen.delete('%s_relation' % rtype, attrs)
         self.doexec(cnx, sql, attrs)
 
+    @statsd_timeit
     def doexec(self, cnx, query, args=None, rollback=True):
         """Execute a query.
         it's a function just so that it shows up in profiling
@@ -749,15 +746,28 @@
                         columns = arg.split(':', 1)[1].split(',')
                         rtypes = [c.split('.', 1)[1].strip()[3:] for c in columns]
                         raise UniqueTogetherError(cnx, rtypes=rtypes)
+
+                    mo = re.search('"cstr[a-f0-9]{32}"', arg)
+                    if mo is not None:
+                        # postgresql
+                        raise ViolatedConstraint(cnx, cstrname=mo.group(0)[1:-1])
+                    if arg.startswith('CHECK constraint failed:'):
+                        # sqlite3 (new)
+                        raise ViolatedConstraint(cnx, cstrname=arg.split(':', 1)[1].strip())
+                    mo = re.match('^constraint (cstr.*) failed$', arg)
+                    if mo is not None:
+                        # sqlite3 (old)
+                        raise ViolatedConstraint(cnx, cstrname=mo.group(1))
             raise
         return cursor
 
+    @statsd_timeit
     def doexecmany(self, cnx, query, args):
         """Execute a query.
         it's a function just so that it shows up in profiling
         """
         if server.DEBUG & server.DBG_SQL:
-            print 'execmany', query, 'with', len(args), 'arguments'
+            print 'execmany', query, 'with', len(args), 'arguments', cnx.cnxset.cnx
         cursor = cnx.cnxset.cu
         try:
             # str(query) to avoid error if it's a unicode string
@@ -829,23 +839,17 @@
 
     # system source interface #################################################
 
-    def _eid_type_source(self, cnx, eid, sql, _retry=True):
+    def _eid_type_source(self, cnx, eid, sql):
         try:
             res = self.doexec(cnx, sql).fetchone()
             if res is not None:
                 return res
-        except (self.OperationalError, self.InterfaceError):
-            if cnx.mode == 'read' and _retry:
-                self.warning("trying to reconnect (eid_type_source())")
-                cnx.cnxset.reconnect()
-                return self._eid_type_source(cnx, eid, sql, _retry=False)
         except Exception:
-            assert cnx.cnxset, 'connection has no connections set'
             self.exception('failed to query entities table for eid %s', eid)
         raise UnknownEid(eid)
 
     def eid_type_source(self, cnx, eid): # pylint: disable=E0202
-        """return a tuple (type, source, extid) for the entity with id <eid>"""
+        """return a tuple (type, extid, source) for the entity with id <eid>"""
         sql = 'SELECT type, extid, asource FROM entities WHERE eid=%s' % eid
         res = self._eid_type_source(cnx, eid, sql)
         if res[-2] is not None:
@@ -855,7 +859,7 @@
         return res
 
     def eid_type_source_pre_131(self, cnx, eid):
-        """return a tuple (type, source, extid) for the entity with id <eid>"""
+        """return a tuple (type, extid, source) for the entity with id <eid>"""
         sql = 'SELECT type, extid FROM entities WHERE eid=%s' % eid
         res = self._eid_type_source(cnx, eid, sql)
         if not isinstance(res, list):
@@ -868,9 +872,10 @@
     def extid2eid(self, cnx, extid):
         """get eid from an external id. Return None if no record found."""
         assert isinstance(extid, str)
+        args = {'x': b64encode(extid)}
         cursor = self.doexec(cnx,
                              'SELECT eid FROM entities WHERE extid=%(x)s',
-                             {'x': b64encode(extid)})
+                             args)
         # XXX testing rowcount cause strange bug with sqlite, results are there
         #     but rowcount is 0
         #if cursor.rowcount > 0:
@@ -880,6 +885,17 @@
                 return result[0]
         except Exception:
             pass
+        cursor = self.doexec(cnx,
+                             'SELECT eid FROM moved_entities WHERE extid=%(x)s',
+                             args)
+        try:
+            result = cursor.fetchone()
+            if result:
+                # entity was moved to the system source, return negative
+                # number to tell the external source to ignore it
+                return -result[0]
+        except Exception:
+            pass
         return None
 
     def _handle_is_relation_sql(self, cnx, sql, attrs):
@@ -897,7 +913,7 @@
         if extid is not None:
             assert isinstance(extid, str)
             extid = b64encode(extid)
-        attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': extid and unicode(extid),
+        attrs = {'type': unicode(entity.cw_etype), 'eid': entity.eid, 'extid': extid and unicode(extid),
                  'asource': unicode(source.uri)}
         self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
         # insert core relations: is, is_instance_of and cw_source
@@ -940,7 +956,7 @@
     # undo support #############################################################
 
     def undoable_transactions(self, cnx, ueid=None, **actionfilters):
-        """See :class:`cubicweb.repoapi.ClientConnection.undoable_transactions`"""
+        """See :class:`cubicweb.repoapi.Connection.undoable_transactions`"""
         # force filtering to connection's user if not a manager
         if not cnx.user.is_in_group('managers'):
             ueid = cnx.user.eid
@@ -965,7 +981,7 @@
                     # only, and with no eid specified
                     assert actionfilters.get('action', 'C') in 'CUD'
                     assert not 'eid' in actionfilters
-                    tearestr['etype'] = val
+                    tearestr['etype'] = unicode(val)
                 elif key == 'eid':
                     # eid filter may apply to 'eid' of tx_entity_actions or to
                     # 'eid_from' OR 'eid_to' of tx_relation_actions
@@ -976,10 +992,10 @@
                         trarestr['eid_to'] = val
                 elif key == 'action':
                     if val in 'CUD':
-                        tearestr['txa_action'] = val
+                        tearestr['txa_action'] = unicode(val)
                     else:
                         assert val in 'AR'
-                        trarestr['txa_action'] = val
+                        trarestr['txa_action'] = unicode(val)
                 else:
                     raise AssertionError('unknow filter %s' % key)
             assert trarestr or tearestr, "can't only filter on 'public'"
@@ -1007,17 +1023,17 @@
             restr.update(tearestr)
         # we want results ordered by transaction's time descendant
         sql += ' ORDER BY tx_time DESC'
-        with cnx.ensure_cnx_set:
-            cu = self.doexec(cnx, sql, restr)
-            # turn results into transaction objects
-            return [tx.Transaction(*args) for args in cu.fetchall()]
+        cu = self.doexec(cnx, sql, restr)
+        # turn results into transaction objects
+        return [tx.Transaction(cnx, *args) for args in cu.fetchall()]
 
     def tx_info(self, cnx, txuuid):
-        """See :class:`cubicweb.repoapi.ClientConnection.transaction_info`"""
-        return tx.Transaction(txuuid, *self._tx_info(cnx, txuuid))
+        """See :class:`cubicweb.repoapi.Connection.transaction_info`"""
+        return tx.Transaction(cnx, txuuid, *self._tx_info(cnx, unicode(txuuid)))
 
     def tx_actions(self, cnx, txuuid, public):
-        """See :class:`cubicweb.repoapi.ClientConnection.transaction_actions`"""
+        """See :class:`cubicweb.repoapi.Connection.transaction_actions`"""
+        txuuid = unicode(txuuid)
         self._tx_info(cnx, txuuid)
         restr = {'tx_uuid': txuuid}
         if public:
@@ -1039,13 +1055,11 @@
         return sorted(actions, key=lambda x: x.order)
 
     def undo_transaction(self, cnx, txuuid):
-        """See :class:`cubicweb.repoapi.ClientConnection.undo_transaction`
+        """See :class:`cubicweb.repoapi.Connection.undo_transaction`
 
         important note: while undoing of a transaction, only hooks in the
         'integrity', 'activeintegrity' and 'undo' categories are called.
         """
-        # set mode so connections set isn't released subsquently until commit/rollback
-        cnx.mode = 'write'
         errors = []
         cnx.transaction_data['undoing_uuid'] = txuuid
         with cnx.deny_all_hooks_but('integrity', 'activeintegrity', 'undo'):
@@ -1097,7 +1111,7 @@
         kwargs['tx_uuid'] = cnx.transaction_uuid()
         kwargs['txa_action'] = action
         kwargs['txa_order'] = cnx.transaction_inc_action_counter()
-        kwargs['txa_public'] = cnx.running_dbapi_query
+        kwargs['txa_public'] = not cnx.hooks_in_progress
         self.doexec(cnx, self.sqlgen.insert(table, kwargs), kwargs)
 
     def _tx_info(self, cnx, txuuid):
@@ -1106,19 +1120,18 @@
         raise `NoSuchTransaction` if there is no such transaction of if the
         connection's user isn't allowed to see it.
         """
-        with cnx.ensure_cnx_set:
-            restr = {'tx_uuid': txuuid}
-            sql = self.sqlgen.select('transactions', restr,
-                                     ('tx_time', 'tx_user'))
-            cu = self.doexec(cnx, sql, restr)
-            try:
-                time, ueid = cu.fetchone()
-            except TypeError:
-                raise tx.NoSuchTransaction(txuuid)
-            if not (cnx.user.is_in_group('managers')
-                    or cnx.user.eid == ueid):
-                raise tx.NoSuchTransaction(txuuid)
-            return time, ueid
+        restr = {'tx_uuid': txuuid}
+        sql = self.sqlgen.select('transactions', restr,
+                                 ('tx_time', 'tx_user'))
+        cu = self.doexec(cnx, sql, restr)
+        try:
+            time, ueid = cu.fetchone()
+        except TypeError:
+            raise tx.NoSuchTransaction(txuuid)
+        if not (cnx.user.is_in_group('managers')
+                or cnx.user.eid == ueid):
+            raise tx.NoSuchTransaction(txuuid)
+        return time, ueid
 
     def _reedit_entity(self, entity, changes, err):
         cnx = entity._cw
@@ -1151,6 +1164,7 @@
                         err(cnx._("can't restore entity %(eid)s of type %(eschema)s, "
                                       "target of %(rtype)s (eid %(value)s) does not exist any longer")
                             % locals())
+                        changes[column] = None
             elif eschema.destination(rtype) in ('Bytes', 'Password'):
                 changes[column] = self._binary(value)
                 edited[rtype] = Binary(value)
@@ -1182,10 +1196,10 @@
         self.repo.hm.call_hooks('before_add_entity', cnx, entity=entity)
         # restore the entity
         action.changes['cw_eid'] = eid
+        # restore record in entities (will update fti if needed)
+        self.add_info(cnx, entity, self, None)
         sql = self.sqlgen.insert(SQL_PREFIX + etype, action.changes)
         self.doexec(cnx, sql, action.changes)
-        # restore record in entities (will update fti if needed)
-        self.add_info(cnx, entity, self, None)
         self.repo.hm.call_hooks('after_add_entity', cnx, entity=entity)
         return errors
 
@@ -1390,7 +1404,10 @@
   extid VARCHAR(256)
 );;
 CREATE INDEX entities_type_idx ON entities(type);;
-CREATE INDEX entities_extid_idx ON entities(extid);;
+CREATE TABLE moved_entities (
+  eid INTEGER PRIMARY KEY NOT NULL,
+  extid VARCHAR(256) UNIQUE NOT NULL
+);;
 
 CREATE TABLE transactions (
   tx_uuid CHAR(32) PRIMARY KEY NOT NULL,
@@ -1442,18 +1459,22 @@
     DELETE FROM tx_relation_actions WHERE tx_uuid=OLD.tx_uuid;
 END;;
 '''
+    schema += ';;'.join(helper.sqls_create_multicol_unique_index('entities', ['extid']))
+    schema += ';;\n'
     return schema
 
 
 def sql_drop_schema(driver):
     helper = get_db_helper(driver)
     return """
+%s;
 %s
 DROP TABLE entities;
 DROP TABLE tx_entity_actions;
 DROP TABLE tx_relation_actions;
 DROP TABLE transactions;
-""" % helper.sql_drop_numrange('entities_id_seq')
+""" % (';'.join(helper.sqls_drop_multicol_unique_index('entities', ['extid'])),
+       helper.sql_drop_numrange('entities_id_seq'))
 
 
 def grant_schema(user, set_owner=True):
@@ -1525,7 +1546,7 @@
                                         SQL_PREFIX + 'login'),
                                        {'newhash': self.source._binary(newhash),
                                         'login': login})
-                    cnx.commit(free_cnxset=False)
+                    cnx.commit()
             return user
         except IndexError:
             raise AuthenticationError('bad password')
--- a/server/sources/rql2sql.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/sources/rql2sql.py	Fri Oct 09 17:52:14 2015 +0200
@@ -50,9 +50,7 @@
 __docformat__ = "restructuredtext en"
 
 import threading
-from datetime import datetime, time
 
-from logilab.common.date import utcdatetime, utctime
 from logilab.database import FunctionDescr, SQL_FUNCTIONS_REGISTRY
 
 from rql import BadRQLQuery, CoercionError
@@ -397,11 +395,14 @@
             yield 1
             return
         thisexistssols, thisexistsvars = self.existssols[exists]
+        notdone_outside_vars = set()
         # when iterating other solutions inner to an EXISTS subquery, we should
         # reset variables which have this exists node as scope at each iteration
         for var in exists.stmt.defined_vars.itervalues():
             if var.scope is exists:
                 thisexistsvars.add(var.name)
+            elif var.name not in self.done:
+                notdone_outside_vars.add(var)
         origsol = self.solution
         origtables = self.tables
         done = self.done
@@ -417,6 +418,10 @@
                 for var in thisexistsvars:
                     if var in done:
                         done.remove(var)
+                for var in list(notdone_outside_vars):
+                    if var.name in done and var._q_sqltable in self.tables:
+                        origtables[var._q_sqltable] = self.tables[var._q_sqltable]
+                        notdone_outside_vars.remove(var)
                 for rel in exists.iget_nodes(Relation):
                     if rel in done:
                         done.remove(rel)
@@ -1509,14 +1514,6 @@
                 _id = value
                 if isinstance(_id, unicode):
                     _id = _id.encode()
-                # convert timestamp to utc.
-                # expect SET TiME ZONE to UTC at connection opening time.
-                # This shouldn't change anything for datetime without TZ.
-                value = self._args[_id]
-                if isinstance(value, datetime) and value.tzinfo is not None:
-                    self._query_attrs[_id] = utcdatetime(value)
-                elif isinstance(value, time) and value.tzinfo is not None:
-                    self._query_attrs[_id] = utctime(value)
         else:
             _id = str(id(constant)).replace('-', '', 1)
             self._query_attrs[_id] = value
--- a/server/sources/storages.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/sources/storages.py	Fri Oct 09 17:52:14 2015 +0200
@@ -141,7 +141,6 @@
         """an entity using this storage for attr has been added"""
         if entity._cw.transaction_data.get('fs_importing'):
             binary = Binary.from_file(entity.cw_edited[attr].getvalue())
-            entity._cw_dont_cache_attribute(attr, repo_side=True)
         else:
             binary = entity.cw_edited.pop(attr)
             fd, fpath = self.new_fs_path(entity, attr)
@@ -160,7 +159,6 @@
             # We do not need to create it but we need to fetch the content of
             # the file as the actual content of the attribute
             fpath = entity.cw_edited[attr].getvalue()
-            entity._cw_dont_cache_attribute(attr, repo_side=True)
             assert fpath is not None
             binary = Binary.from_file(fpath)
         else:
--- a/server/sqlutils.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/sqlutils.py	Fri Oct 09 17:52:14 2015 +0200
@@ -20,17 +20,18 @@
 __docformat__ = "restructuredtext en"
 
 import sys
-import os
 import re
 import subprocess
 from os.path import abspath
 from itertools import ifilter
 from logging import getLogger
+from datetime import time, datetime
 
 from logilab import database as db, common as lgc
 from logilab.common.shellutils import ProgressBar, DummyProgressBar
 from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods
+from logilab.common.date import utctime, utcdatetime
 from logilab.database.sqlgen import SQLGenerator
 
 from cubicweb import Binary, ConfigurationError
@@ -106,7 +107,7 @@
     """return sql to give all access privileges to the given user on the system
     schema
     """
-    from yams.schema2sql import grant_schema
+    from cubicweb.server.schema2sql import grant_schema
     from cubicweb.server.sources import native
     output = []
     w = output.append
@@ -124,7 +125,7 @@
               user=None, set_owner=False,
               skip_relations=PURE_VIRTUAL_RTYPES, skip_entities=()):
     """return the system sql schema, according to the given parameters"""
-    from yams.schema2sql import schema2sql
+    from cubicweb.server.schema2sql import schema2sql
     from cubicweb.server.sources import native
     if set_owner:
         assert user, 'user is argument required when set_owner is true'
@@ -149,7 +150,7 @@
 def sqldropschema(schema, driver, text_index=True,
                   skip_relations=PURE_VIRTUAL_RTYPES, skip_entities=()):
     """return the sql to drop the schema, according to the given parameters"""
-    from yams.schema2sql import dropschema2sql
+    from cubicweb.server.schema2sql import dropschema2sql
     from cubicweb.server.sources import native
     output = []
     w = output.append
@@ -373,8 +374,17 @@
                 # convert cubicweb binary into db binary
                 if isinstance(val, Binary):
                     val = self._binary(val.getvalue())
+                # convert timestamp to utc.
+                # expect SET TiME ZONE to UTC at connection opening time.
+                # This shouldn't change anything for datetime without TZ.
+                elif isinstance(val, datetime) and val.tzinfo is not None:
+                    val = utcdatetime(val)
+                elif isinstance(val, time) and val.tzinfo is not None:
+                    val = utctime(val)
                 newargs[key] = val
             # should not collide
+            assert not (frozenset(newargs) & frozenset(query_args)), \
+                'unexpected collision: %s' % (frozenset(newargs) & frozenset(query_args))
             newargs.update(query_args)
             return newargs
         return query_args
@@ -503,6 +513,8 @@
         return (dt.weekday() + 1) % 7
     cnx.create_function("WEEKDAY", 1, weekday)
 
+    cnx.cursor().execute("pragma foreign_keys = on")
+
     import yams.constraints
     yams.constraints.patch_sqlite_decimal()
 
--- a/server/ssplanner.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/ssplanner.py	Fri Oct 09 17:52:14 2015 +0200
@@ -249,7 +249,7 @@
                 raise QueryError('can not assign to %r relation'
                                  % relation.r_type)
             lhs, rhs = relation.get_variable_parts()
-            lhskey = lhs.as_string('utf-8')
+            lhskey = lhs.as_string()
             if not lhskey in selectedidx:
                 if lhs.variable in eidconsts:
                     eid = eidconsts[lhs.variable]
@@ -262,7 +262,7 @@
                 selectedidx[lhskey] = lhsinfo
             else:
                 lhsinfo = selectedidx[lhskey][:-1] + (None,)
-            rhskey = rhs.as_string('utf-8')
+            rhskey = rhs.as_string()
             if not rhskey in selectedidx:
                 if isinstance(rhs, Constant):
                     rhsinfo = (_CONSTANT, rhs.eval(plan.args), residx)
--- a/server/test/data-cwep002/schema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/data-cwep002/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -32,4 +32,5 @@
 
 class has_employee(ComputedRelation):
     rule = 'O works_for S'
+    __permissions__ = {'read': ('managers',)}
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/bootstrap_cubes	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+card,comment,tag,basket,file,localperms,fakeemail
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/cubes/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+__import__('pkg_resources').declare_namespace(__name__)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/cubes/fakeemail/__pkginfo__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,53 @@
+# pylint: disable-msg=W0622
+"""cubicweb-fakeemail packaging information"""
+
+modname = 'fakeemail'
+distname = "cubicweb-%s" % modname
+
+numversion = (1, 10, 0)
+version = '.'.join(str(num) for num in numversion)
+
+license = 'LGPL'
+author = "Logilab"
+author_email = "contact@logilab.fr"
+web = 'http://www.cubicweb.org/project/%s' % distname
+description = "email component for the CubicWeb framework"
+classifiers = [
+           'Environment :: Web Environment',
+           'Framework :: CubicWeb',
+           'Programming Language :: Python',
+           'Programming Language :: JavaScript',
+]
+
+# used packages
+__depends__ = {'cubicweb': '>= 3.19.0',
+               'cubicweb-file': '>= 1.9.0',
+               'logilab-common': '>= 0.58.3',
+               }
+__recommends__ = {'cubicweb-comment': None}
+
+
+# packaging ###
+
+from os import listdir as _listdir
+from os.path import join, isdir
+from glob import glob
+
+THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
+
+def listdir(dirpath):
+    return [join(dirpath, fname) for fname in _listdir(dirpath)
+            if fname[0] != '.' and not fname.endswith('.pyc')
+            and not fname.endswith('~')
+            and not isdir(join(dirpath, fname))]
+
+data_files = [
+    # common files
+    [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
+    ]
+# check for possible extended cube layout
+for dirname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration', 'wdoc'):
+    if isdir(dirname):
+        data_files.append([join(THIS_CUBE_DIR, dirname), listdir(dirname)])
+# Note: here, you'll need to add subdirectories if you want
+# them to be included in the debian package
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/cubes/fakeemail/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,88 @@
+"""entity/relation schemas to store email in an cubicweb instance
+
+:organization: Logilab
+:copyright: 2006-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+# pylint: disable-msg=E0611,F0401
+from yams.buildobjs import (SubjectRelation, RelationType, EntityType,
+                            String, Datetime, Int, RelationDefinition)
+from yams.reader import context
+
+from cubicweb.schema import ERQLExpression
+
+
+class Email(EntityType):
+    """electronic mail"""
+    subject   = String(fulltextindexed=True)
+    date      = Datetime(description=_('UTC time on which the mail was sent'))
+    messageid = String(required=True, indexed=True)
+    headers   = String(description=_('raw headers'))
+
+    sender     = SubjectRelation('EmailAddress', cardinality='?*')
+    # an email with only Bcc is acceptable, don't require any recipients
+    recipients = SubjectRelation('EmailAddress')
+    cc         = SubjectRelation('EmailAddress')
+
+    parts       = SubjectRelation('EmailPart', cardinality='*1', composite='subject')
+    attachment  = SubjectRelation('File')
+
+    reply_to    = SubjectRelation('Email', cardinality='?*')
+    cites       = SubjectRelation('Email')
+    in_thread   = SubjectRelation('EmailThread', cardinality='?*')
+
+
+class EmailPart(EntityType):
+    """an email attachment"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',), # XXX if E parts X, U has_read_permission E
+        'add':    ('managers', ERQLExpression('E parts X, U has_update_permission E'),),
+        'delete': ('managers', ERQLExpression('E parts X, U has_update_permission E')),
+        'update': ('managers', 'owners',),
+        }
+
+    content  = String(fulltextindexed=True)
+    content_format = String(required=True, maxsize=50)
+    ordernum = Int(required=True)
+    alternative = SubjectRelation('EmailPart', symmetric=True)
+
+
+class EmailThread(EntityType):
+    """discussion thread"""
+    title = String(required=True, indexed=True, fulltextindexed=True)
+    see_also = SubjectRelation('EmailThread')
+    forked_from = SubjectRelation('EmailThread', cardinality='?*')
+
+class parts(RelationType):
+    """ """
+    fulltext_container = 'subject'
+
+class sender(RelationType):
+    """ """
+    inlined = True
+
+class in_thread(RelationType):
+    """ """
+    inlined = True
+
+class reply_to(RelationType):
+    """ """
+    inlined = True
+
+class generated_by(RelationType):
+    """mark an entity as generated from an email"""
+    cardinality = '?*'
+    subject = ('TrInfo',)
+    object = 'Email'
+
+# if comment is installed
+if 'Comment' in context.defined:
+    class comment_generated_by(RelationDefinition):
+        subject = 'Comment'
+        name = 'generated_by'
+        object = 'Email'
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/migratedapp/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -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/server/test/data-migractions/migratedapp/bootstrap_cubes	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+card,comment,tag,basket,fakeemail,file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/migratedapp/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,212 @@
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# 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/>.
+"""cw.server.migraction test"""
+import datetime as dt
+from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
+                            SubjectRelation, Bytes,
+                            RichString, String, Int, Boolean, Datetime, Date, Float)
+from yams.constraints import SizeConstraint, UniqueConstraint
+from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
+                             RQLVocabularyConstraint,
+                             ERQLExpression, RRQLExpression)
+
+class Affaire(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
+        'update': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+        'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+        }
+
+    ref = String(fulltextindexed=True, indexed=True,
+                 constraints=[SizeConstraint(16)])
+    sujet = String(fulltextindexed=True,
+                 constraints=[SizeConstraint(256)])
+    concerne = SubjectRelation('Societe')
+    opt_attr = Bytes()
+
+class Societe(WorkflowableEntityType):
+    __permissions__ = {
+        'read': ('managers', 'users', 'guests'),
+        'update': ('managers', 'owners'),
+        'delete': ('managers', 'owners'),
+        'add': ('managers', 'users',)
+        }
+    nom  = String(maxsize=64, fulltextindexed=True)
+    web  = String(maxsize=128)
+    tel  = Float()
+    fax  = Int()
+    rncs = String(maxsize=128)
+    ad1  = String(maxsize=128)
+    ad2  = String(maxsize=128)
+    ad3  = String(maxsize=128)
+    cp   = String(maxsize=12)
+    ville= String(maxsize=32)
+
+# Division and SubDivision are gone
+
+# New
+class Para(EntityType):
+    para = String(maxsize=512)
+    newattr = String()
+    newinlined = SubjectRelation('Affaire', cardinality='?*', inlined=True)
+    newnotinlined = SubjectRelation('Affaire', cardinality='?*')
+
+class Note(Para):
+    __specializes_schema__ = True
+
+    __permissions__ = {'read':   ('managers', 'users', 'guests',),
+                   'update': ('managers', 'owners',),
+                   'delete': ('managers', ),
+                   'add':    ('managers',
+                              ERQLExpression('X ecrit_part PE, U in_group G, '
+                                             'PE require_permission P, P name "add_note", '
+                                             'P require_group G'),)}
+
+    whatever = Int(default=0)  # keep it before `date` for unittest_migraction.test_add_attribute_int
+    yesno = Boolean(default=False)
+    date = Datetime()
+    type = String(maxsize=1)
+    unique_id = String(maxsize=1, required=True, unique=True)
+    mydate = Date(default='TODAY')
+    oldstyledefaultdate = Date(default='2013/01/01')
+    newstyledefaultdate = Date(default=dt.date(2013, 1, 1))
+    shortpara = String(maxsize=64, default='hop')
+    ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
+    attachment = SubjectRelation('File')
+
+
+class Frozable(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users'),
+        'add':    ('managers', 'users'),
+        'update': ('managers', ERQLExpression('X frozen False'),),
+        'delete': ('managers', ERQLExpression('X frozen False'),)
+    }
+    name = String()
+    frozen = Boolean(default=False,
+                     __permissions__ = {
+                         'read':   ('managers', 'users'),
+                         'add':    ('managers', 'users'),
+                         'update': ('managers', 'owners')
+                         })
+
+
+class Personne(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users'), # 'guests' was removed
+        'add':    ('managers', 'users'),
+        'update': ('managers', 'owners'),
+        'delete': ('managers', 'owners')
+    }
+    __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)
+    promo  = String(vocabulary=('bon','pasbon'))
+    titre  = String(fulltextindexed=True, maxsize=128)
+    adel   = String(maxsize=128)
+    ass    = String(maxsize=128)
+    web    = String(maxsize=128)
+    tel    = Int()
+    fax    = Int()
+    datenaiss = Datetime()
+    test   = Boolean()
+
+    travaille = SubjectRelation('Societe')
+    concerne = SubjectRelation('Affaire')
+    concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*')
+    connait = SubjectRelation('Personne', symmetric=True)
+
+class concerne(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', RRQLExpression('U has_update_permission S')),
+        'delete': ('managers', RRQLExpression('O owned_by U')),
+        }
+
+# `Old` entity type is gonce
+# `comments` is gone
+# `fiche` is gone
+# `multisource_*` rdefs are gone
+# `see_also_*` rdefs are gone
+
+class evaluee(RelationDefinition):
+    subject = ('Personne', 'CWUser', 'Societe')
+    object = ('Note')
+    constraints = [RQLVocabularyConstraint('S owned_by U')]
+
+class ecrit_par(RelationType):
+    __permissions__ = {'read':   ('managers', 'users', 'guests',),
+                   'delete': ('managers', ),
+                   'add':    ('managers',
+                              RRQLExpression('O require_permission P, P name "add_note", '
+                                             'U in_group G, P require_group G'),)
+                   }
+    inlined = True
+    cardinality = '?*'
+
+# `copain` rdef is gone
+# `tags` rdef is gone
+# `filed_under` rdef is gone
+# `require_permission` rdef is gone
+# `require_state` rdef is gone
+# `personne_composite` rdef is gone
+# `personne_inlined` rdef is gone
+# `login_user` rdef is gone
+# `ambiguous_inlined` rdef is gone
+
+class Folder(EntityType):
+    """folders are used to classify entities. They may be defined as a tree.
+    """
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=64)
+    description = RichString(fulltextindexed=True)
+    filed_under = SubjectRelation('Folder', description=_('parent folder'))
+
+
+# New
+class Text(Para):
+    __specializes_schema__ = True
+    summary = String(maxsize=512)
+
+
+# New
+class Folder2(EntityType):
+    """folders are used to classify entities. They may be defined as a tree.
+    When you include the Folder entity, all application specific entities
+    may then be classified using the "filed_under" relation.
+    """
+    name = String(required=True, indexed=True, internationalizable=True,
+                  constraints=[UniqueConstraint(), SizeConstraint(64)])
+    description = RichString(fulltextindexed=True)
+
+# New
+class filed_under2(RelationDefinition):
+    subject ='*'
+    object = 'Folder2'
+
+
+# New
+class New(EntityType):
+    new_name = String()
+
+# New
+class same_as(RelationDefinition):
+    subject = ('Societe',)
+    object = 'ExternalUri'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,288 @@
+# copyright 2003-2014 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 yams.buildobjs import (EntityType, RelationType, RelationDefinition, ComputedRelation,
+                            SubjectRelation, RichString, String, Int, Float,
+                            Boolean, Datetime, TZDatetime, Bytes)
+from yams.constraints import SizeConstraint
+from cubicweb.schema import (WorkflowableEntityType,
+                             RQLConstraint, RQLUniqueConstraint,
+                             RQLVocabularyConstraint,
+                             ERQLExpression, RRQLExpression)
+
+class Affaire(WorkflowableEntityType):
+    __permissions__ = {
+        'read':   ('managers',
+                   ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
+        'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
+        'update': ('managers', 'owners', ERQLExpression('X in_state S, S name in ("pitetre", "en cours")')),
+        'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+        }
+
+    ref = String(fulltextindexed=True, indexed=True,
+                 constraints=[SizeConstraint(16)])
+    sujet = String(fulltextindexed=True,
+                   constraints=[SizeConstraint(256)])
+    descr = RichString(fulltextindexed=True,
+                       description=_('more detailed description'))
+
+    duration = Int()
+    invoiced = Float()
+    opt_attr = Bytes()
+
+    depends_on = SubjectRelation('Affaire')
+    require_permission = SubjectRelation('CWPermission')
+    concerne = SubjectRelation(('Societe', 'Note'))
+    todo_by = SubjectRelation('Personne', cardinality='?*')
+    documented_by = SubjectRelation('Card')
+
+
+class Societe(EntityType):
+    __unique_together__ = [('nom', 'type', 'cp')]
+    __permissions__ = {
+        'read': ('managers', 'users', 'guests'),
+        'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'add': ('managers', 'users',)
+        }
+
+    nom  = String(maxsize=64, fulltextindexed=True)
+    web  = String(maxsize=128)
+    type  = String(maxsize=128) # attribute in common with Note
+    tel  = Int()
+    fax  = Int()
+    rncs = String(maxsize=128)
+    ad1  = String(maxsize=128)
+    ad2  = String(maxsize=128)
+    ad3  = String(maxsize=128)
+    cp   = String(maxsize=12)
+    ville= String(maxsize=32)
+
+
+class Division(Societe):
+    __specializes_schema__ = True
+
+class SubDivision(Division):
+    __specializes_schema__ = True
+
+class travaille_subdivision(RelationDefinition):
+    subject = 'Personne'
+    object = 'SubDivision'
+
+from cubicweb.schemas.base import CWUser
+CWUser.get_relations('login').next().fulltextindexed = True
+
+class Note(WorkflowableEntityType):
+    date = String(maxsize=10)
+    type = String(vocabulary=[u'todo', u'a', u'b', u'T', u'lalala'])
+    para = String(maxsize=512,
+                  __permissions__ = {
+                      'add': ('managers', ERQLExpression('X in_state S, S name "todo"')),
+                      'read':   ('managers', 'users', 'guests'),
+                      'update': ('managers', ERQLExpression('X in_state S, S name "todo"')),
+                      })
+    something = String(maxsize=1,
+                      __permissions__ = {
+                          'read': ('managers', 'users', 'guests'),
+                          'add': (ERQLExpression('NOT X para NULL'),),
+                          'update': ('managers', 'owners')
+                      })
+    migrated_from = SubjectRelation('Note')
+    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 Frozable(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users'),
+        'add':    ('managers', 'users'),
+        'update': ('managers', ERQLExpression('X frozen False'),),
+        'delete': ('managers', ERQLExpression('X frozen False'),)
+    }
+    name = String()
+    frozen = Boolean(default=False,
+                     __permissions__ = {
+                         'read':   ('managers', 'users'),
+                         'add':    ('managers', 'users'),
+                         'update': ('managers', 'owners')
+                         })
+
+
+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)
+    promo  = String(vocabulary=('bon','pasbon'))
+    titre  = String(fulltextindexed=True, maxsize=128)
+    adel   = String(maxsize=128)
+    ass    = String(maxsize=128)
+    web    = String(maxsize=128)
+    tel    = Int()
+    fax    = Int()
+    datenaiss = Datetime()
+    tzdatenaiss = TZDatetime()
+    test   = Boolean(__permissions__={
+        'read': ('managers', 'users', 'guests'),
+        'add': ('managers',),
+        'update': ('managers',),
+        })
+    description = String()
+    firstname = String(fulltextindexed=True, maxsize=64)
+
+    concerne = SubjectRelation('Affaire')
+    connait = SubjectRelation('Personne')
+    inline2 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
+
+
+class Old(EntityType):
+    name = String(__permissions__ = {
+        'read'   : ('managers', 'users', 'guests'),
+        'add'    : ('managers', 'users', 'guests'),
+        'update' : ()
+    })
+
+
+class connait(RelationType):
+    symmetric = True
+
+class concerne(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', RRQLExpression('U has_update_permission S')),
+        'delete': ('managers', RRQLExpression('O owned_by U')),
+        }
+
+class travaille(RelationDefinition):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', RRQLExpression('U has_update_permission S')),
+        'delete': ('managers', RRQLExpression('O owned_by U')),
+        }
+    subject = 'Personne'
+    object = 'Societe'
+    constraints = [RQLVocabularyConstraint('S owned_by U'),
+                   RQLVocabularyConstraint('S created_by U')]
+
+class comments(RelationDefinition):
+    subject = 'Comment'
+    object = 'Personne'
+
+class fiche(RelationDefinition):
+    inlined = True
+    subject = 'Personne'
+    object = 'Card'
+    cardinality = '??'
+
+class multisource_inlined_rel(RelationDefinition):
+    inlined = True
+    cardinality = '?*'
+    subject = ('Card', 'Note')
+    object = ('Affaire', 'Note')
+
+
+class see_also_1(RelationDefinition):
+    name = 'see_also'
+    subject = object = 'Folder'
+
+class see_also_2(RelationDefinition):
+    name = 'see_also'
+    subject = ('Bookmark', 'Note')
+    object = ('Bookmark', 'Note')
+
+class evaluee(RelationDefinition):
+    subject = ('Personne', 'CWUser', 'Societe')
+    object = ('Note')
+    constraints = [
+        RQLVocabularyConstraint('S created_by U'),
+        RQLVocabularyConstraint('S owned_by U'),
+    ]
+
+class ecrit_par(RelationType):
+    inlined = True
+
+class ecrit_par_1(RelationDefinition):
+    name = 'ecrit_par'
+    subject = 'Note'
+    object ='Personne'
+    cardinality = '?*'
+
+class ecrit_par_2(RelationDefinition):
+    name = 'ecrit_par'
+    subject = 'Note'
+    object ='CWUser'
+    cardinality='?*'
+
+
+class copain(RelationDefinition):
+    subject = object = 'CWUser'
+
+class tags(RelationDefinition):
+    subject = 'Tag'
+    object = ('CWUser', 'CWGroup', 'State', 'Note', 'Card', 'Affaire')
+
+class Folder(EntityType):
+    """folders are used to classify entities. They may be defined as a tree.
+    """
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=64)
+    description = RichString(fulltextindexed=True)
+    filed_under = SubjectRelation('Folder', description=_('parent folder'))
+
+class filed_under(RelationDefinition):
+    subject = ('Note', 'Affaire')
+    object = 'Folder'
+
+class require_permission(RelationDefinition):
+    subject = ('Card', 'Note', 'Personne')
+    object = 'CWPermission'
+
+class require_state(RelationDefinition):
+    subject = 'CWPermission'
+    object = 'State'
+
+class personne_composite(RelationDefinition):
+    subject='Personne'
+    object='Personne'
+    composite='subject'
+
+class personne_inlined(RelationDefinition):
+    subject='Personne'
+    object='Personne'
+    cardinality='?*'
+    inlined=True
+
+
+class login_user(RelationDefinition):
+    subject = 'Personne'
+    object = 'CWUser'
+    cardinality = '??'
+
+class ambiguous_inlined(RelationDefinition):
+    subject = ('Affaire', 'Note')
+    object = 'CWUser'
+    inlined = True
+    cardinality = '?*'
+
+
+class user_login(ComputedRelation):
+    rule = 'O login_user S'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/Company.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,67 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from yams.buildobjs import EntityType, RelationType, RelationDefinition, \
+     SubjectRelation, String
+
+class Company(EntityType):
+    name = String()
+
+class Subcompany(Company):
+    __specializes_schema__ = True
+    subcompany_of = SubjectRelation('Company')
+
+class Division(Company):
+    __specializes_schema__ = True
+    division_of = SubjectRelation('Company')
+
+class Subdivision(Division):
+    __specializes_schema__ = True
+    subdivision_of = SubjectRelation('Company')
+
+class Employee(EntityType):
+    works_for = SubjectRelation('Company')
+
+class require_permission(RelationType):
+    """link a permission to the entity. This permission should be used in the
+    security definition of the entity's type to be useful.
+    """
+    fulltext_container = 'subject'
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers',),
+        'delete': ('managers',),
+        }
+
+
+class missing_require_permission(RelationDefinition):
+    name = 'require_permission'
+    subject = 'Company'
+    object = 'EPermission'
+
+class EPermission(EntityType):
+    """entity type that may be used to construct some advanced security configuration
+    """
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers',),
+        'delete': ('managers',),
+        'update': ('managers', 'owners',),
+        }
+    name = String(required=True, indexed=True, internationalizable=True,
+                  fulltextindexed=True, maxsize=100,
+                  description=_('name or identifier of the permission'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/Dates.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,29 @@
+# copyright 2004-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from datetime import time, date
+from yams.buildobjs import EntityType, Datetime, Date, Time
+from yams.constraints import TODAY, BoundaryConstraint
+
+class Datetest(EntityType):
+    dt1 = Datetime(default=u'now')
+    dt2 = Datetime(default=u'today')
+    d1  = Date(default=u'today', constraints=[BoundaryConstraint('<=', TODAY())])
+    d2  = Date(default=date(2007, 12, 11))
+    t1  = Time(default=time(8, 40))
+    t2  = Time(default=time(9, 45))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/State.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,81 @@
+# copyright 2004-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
+                            SubjectRelation, Int, String,  Boolean)
+from yams.constraints import SizeConstraint, UniqueConstraint
+
+from __init__ import RESTRICTED_RTYPE_PERMS
+
+class State(EntityType):
+    """used to associate simple states to an entity
+    type and/or to define workflows
+    """
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+
+    # attributes
+    eid = Int(required=True, uid=True)
+    name = String(required=True,
+                  indexed=True, internationalizable=True,
+                  constraints=[SizeConstraint(256)])
+    description = String(fulltextindexed=True)
+    # relations
+    state_of = SubjectRelation('Eetype', cardinality='+*')
+    next_state = SubjectRelation('State', cardinality='**')
+
+
+class state_of(RelationType):
+    """link a state to one or more entity type"""
+    __permissions__ = RESTRICTED_RTYPE_PERMS
+
+class next_state(RelationType):
+    """define a workflow by associating a state to possible following states
+    """
+    __permissions__ = RESTRICTED_RTYPE_PERMS
+
+class initial_state(RelationType):
+    """indicate which state should be used by default when an entity using states
+    is created
+    """
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'users',),
+        }
+    subject = 'Eetype'
+    object = 'State'
+    cardinality = '?*'
+    inlined = True
+
+class Eetype(EntityType):
+    """define an entity type, used to build the application schema"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers',),
+        'delete': ('managers',),
+        'update': ('managers', 'owners',),
+        }
+    name = String(required=True, indexed=True, internationalizable=True,
+                  constraints=[UniqueConstraint(), SizeConstraint(64)])
+    description = String(fulltextindexed=True)
+    meta = Boolean()
+    final = Boolean()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/__init__.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,23 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+"""test schema"""
+RESTRICTED_RTYPE_PERMS = {
+    'read':   ('managers', 'users', 'guests',),
+    'add':    ('managers',),
+    'delete': ('managers',),
+    }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,113 @@
+# copyright 2004-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from yams.buildobjs import (EntityType, RelationDefinition, RelationType,
+                            SubjectRelation, String, Int, Float, Date, Boolean)
+from yams.constraints import Attribute, BoundaryConstraint
+
+class Affaire(EntityType):
+    sujet = String(maxsize=128)
+    ref = String(maxsize=12)
+
+    concerne = SubjectRelation('Societe')
+    obj_wildcard = SubjectRelation('*')
+    sym_rel = SubjectRelation('Person', symmetric=True)
+    inline_rel = SubjectRelation('Person', inlined=True, cardinality='?*')
+
+class subj_wildcard(RelationDefinition):
+    subject = '*'
+    object = 'Affaire'
+
+
+class Person(EntityType):
+    __unique_together__ = [('nom', 'prenom')]
+    nom    = String(maxsize=64, fulltextindexed=True, required=True)
+    prenom = String(maxsize=64, fulltextindexed=True)
+    sexe   = String(maxsize=1, default='M')
+    promo  = String(vocabulary=('bon','pasbon'))
+    titre  = String(maxsize=128, fulltextindexed=True)
+    adel   = String(maxsize=128)
+    ass    = String(maxsize=128)
+    web    = String(maxsize=128)
+    tel    = Int(__permissions__={'read': (),
+                                  'add': ('managers',),
+                                  'update': ('managers',)})
+    fax    = Int()
+    datenaiss = Date()
+    test   = Boolean()
+    salary = Float()
+    travaille = SubjectRelation('Societe',
+                                __permissions__={'read': (),
+                                                 'add': (),
+                                                 'delete': ('managers',),
+                                                 })
+
+    evaluee = SubjectRelation('Note')
+
+class Salaried(Person):
+    __specializes_schema__ = True
+
+class Societe(EntityType):
+    nom  = String(maxsize=64, fulltextindexed=True)
+    web = String(maxsize=128)
+    tel  = Int()
+    fax  = Int(constraints=[BoundaryConstraint('<=', Attribute('tel'))])
+    rncs = String(maxsize=32)
+    ad1  = String(maxsize=128)
+    ad2  = String(maxsize=128)
+    ad3  = String(maxsize=128)
+    cp   = String(maxsize=12)
+    ville = String(maxsize=32)
+
+    evaluee = SubjectRelation('Note')
+
+
+class Note(EntityType):
+    date = String(maxsize=10)
+    type = String(maxsize=1)
+    para = String(maxsize=512)
+
+
+class pkginfo(EntityType):
+    modname = String(maxsize=30, required=True)
+    version = String(maxsize=10, required=True, default='0.1')
+    copyright = String(required=True)
+    license = String(vocabulary=('GPL', 'ZPL'))
+    short_desc = String(maxsize=80, required=True)
+    long_desc = String(required=True, fulltextindexed=True)
+    author = String(maxsize=100, required=True)
+    author_email = String(maxsize=100, required=True)
+    mailinglist = String(maxsize=100)
+    debian_handler = String(vocabulary=('machin', 'bidule'))
+
+
+class evaluee(RelationType):
+    __permissions__ = {
+        'read': ('managers',),
+        'add': ('managers',),
+        'delete': ('managers',),
+        }
+
+class concerne(RelationDefinition):
+    subject = 'Person'
+    object = 'Affaire'
+    __permissions__ = {
+        'read': ('managers',),
+        'add': ('managers',),
+        'delete': ('managers',),
+        }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/toignore	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+coucou
--- a/server/test/data/bootstrap_cubes	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/data/bootstrap_cubes	Fri Oct 09 17:52:14 2015 +0200
@@ -1,1 +1,1 @@
-card,comment,folder,tag,basket,email,file,localperms
+card,comment,tag,basket,file,localperms
--- a/server/test/data/migratedapp/__init__.py	Fri Oct 09 09:40:08 2015 +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/server/test/data/migratedapp/bootstrap_cubes	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-card,comment,folder,tag,basket,email,file
--- a/server/test/data/migratedapp/schema.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +0,0 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# 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/>.
-"""cw.server.migraction test"""
-import datetime as dt
-from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
-                            SubjectRelation, Bytes,
-                            RichString, String, Int, Boolean, Datetime, Date)
-from yams.constraints import SizeConstraint, UniqueConstraint
-from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
-                             RQLVocabularyConstraint,
-                             ERQLExpression, RRQLExpression)
-
-class Affaire(EntityType):
-    __permissions__ = {
-        'read':   ('managers', 'users', 'guests'),
-        'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
-        'update': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
-        'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
-        }
-
-    ref = String(fulltextindexed=True, indexed=True,
-                 constraints=[SizeConstraint(16)])
-    sujet = String(fulltextindexed=True,
-                 constraints=[SizeConstraint(256)])
-    concerne = SubjectRelation('Societe')
-    opt_attr = Bytes()
-
-class Societe(WorkflowableEntityType):
-    __permissions__ = {
-        'read': ('managers', 'users', 'guests'),
-        'update': ('managers', 'owners'),
-        'delete': ('managers', 'owners'),
-        'add': ('managers', 'users',)
-        }
-    nom  = String(maxsize=64, fulltextindexed=True)
-    web  = String(maxsize=128)
-    tel  = Int()
-    fax  = Int()
-    rncs = String(maxsize=128)
-    ad1  = String(maxsize=128)
-    ad2  = String(maxsize=128)
-    ad3  = String(maxsize=128)
-    cp   = String(maxsize=12)
-    ville= String(maxsize=32)
-
-# Division and SubDivision are gone
-
-# New
-class Para(EntityType):
-    para = String(maxsize=512)
-    newattr = String()
-    newinlined = SubjectRelation('Affaire', cardinality='?*', inlined=True)
-    newnotinlined = SubjectRelation('Affaire', cardinality='?*')
-
-class Note(Para):
-    __specializes_schema__ = True
-
-    __permissions__ = {'read':   ('managers', 'users', 'guests',),
-                   'update': ('managers', 'owners',),
-                   'delete': ('managers', ),
-                   'add':    ('managers',
-                              ERQLExpression('X ecrit_part PE, U in_group G, '
-                                             'PE require_permission P, P name "add_note", '
-                                             'P require_group G'),)}
-
-    whatever = Int(default=0)  # keep it before `date` for unittest_migraction.test_add_attribute_int
-    yesno = Boolean(default=False)
-    date = Datetime()
-    type = String(maxsize=1)
-    unique_id = String(maxsize=1, required=True, unique=True)
-    mydate = Date(default='TODAY')
-    oldstyledefaultdate = Date(default='2013/01/01')
-    newstyledefaultdate = Date(default=dt.date(2013, 1, 1))
-    shortpara = String(maxsize=64, default='hop')
-    ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
-    attachment = SubjectRelation('File')
-
-
-class Frozable(EntityType):
-    __permissions__ = {
-        'read':   ('managers', 'users'),
-        'add':    ('managers', 'users'),
-        'update': ('managers', ERQLExpression('X frozen False'),),
-        'delete': ('managers', ERQLExpression('X frozen False'),)
-    }
-    name = String()
-    frozen = Boolean(default=False,
-                     __permissions__ = {
-                         'read':   ('managers', 'users'),
-                         'add':    ('managers', 'users'),
-                         'update': ('managers', 'owners')
-                         })
-
-
-class Personne(EntityType):
-    __permissions__ = {
-        'read':   ('managers', 'users'), # 'guests' was removed
-        'add':    ('managers', 'users'),
-        'update': ('managers', 'owners'),
-        'delete': ('managers', 'owners')
-    }
-    __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)
-    promo  = String(vocabulary=('bon','pasbon'))
-    titre  = String(fulltextindexed=True, maxsize=128)
-    adel   = String(maxsize=128)
-    ass    = String(maxsize=128)
-    web    = String(maxsize=128)
-    tel    = Int()
-    fax    = Int()
-    datenaiss = Datetime()
-    test   = Boolean()
-
-    travaille = SubjectRelation('Societe')
-    concerne = SubjectRelation('Affaire')
-    concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*')
-    connait = SubjectRelation('Personne', symmetric=True)
-
-class concerne(RelationType):
-    __permissions__ = {
-        'read':   ('managers', 'users', 'guests'),
-        'add':    ('managers', RRQLExpression('U has_update_permission S')),
-        'delete': ('managers', RRQLExpression('O owned_by U')),
-        }
-
-# `Old` entity type is gonce
-# `comments` is gone
-# `fiche` is gone
-# `multisource_*` rdefs are gone
-# `see_also_*` rdefs are gone
-
-class evaluee(RelationDefinition):
-    subject = ('Personne', 'CWUser', 'Societe')
-    object = ('Note')
-    constraints = [RQLVocabularyConstraint('S owned_by U')]
-
-class ecrit_par(RelationType):
-    __permissions__ = {'read':   ('managers', 'users', 'guests',),
-                   'delete': ('managers', ),
-                   'add':    ('managers',
-                              RRQLExpression('O require_permission P, P name "add_note", '
-                                             'U in_group G, P require_group G'),)
-                   }
-    inlined = True
-    cardinality = '?*'
-
-# `copain` rdef is gone
-# `tags` rdef is gone
-# `filed_under` rdef is gone
-# `require_permission` rdef is gone
-# `require_state` rdef is gone
-# `personne_composite` rdef is gone
-# `personne_inlined` rdef is gone
-# `login_user` rdef is gone
-# `ambiguous_inlined` rdef is gone
-
-# New
-class Text(Para):
-    __specializes_schema__ = True
-    summary = String(maxsize=512)
-
-
-# New
-class Folder2(EntityType):
-    """folders are used to classify entities. They may be defined as a tree.
-    When you include the Folder entity, all application specific entities
-    may then be classified using the "filed_under" relation.
-    """
-    name = String(required=True, indexed=True, internationalizable=True,
-                  constraints=[UniqueConstraint(), SizeConstraint(64)])
-    description = RichString(fulltextindexed=True)
-
-# New
-class filed_under2(RelationDefinition):
-    subject ='*'
-    object = 'Folder2'
-
-
-# New
-class New(EntityType):
-    new_name = String()
-
-# New
-class same_as(RelationDefinition):
-    subject = ('Societe',)
-    object = 'ExternalUri'
--- a/server/test/data/schema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/data/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -89,7 +89,7 @@
 
 class Note(WorkflowableEntityType):
     date = String(maxsize=10)
-    type = String(maxsize=6)
+    type = String(vocabulary=[u'todo', u'a', u'b', u'T', u'lalala'])
     para = String(maxsize=512,
                   __permissions__ = {
                       'add': ('managers', ERQLExpression('X in_state S, S name "todo"')),
@@ -168,6 +168,22 @@
     })
 
 
+class Email(EntityType):
+    subject = String(fulltextindexed=True)
+    messageid = String(required=True, indexed=True, unique=True)
+    sender = SubjectRelation('EmailAddress', cardinality='?*')
+    recipients = SubjectRelation('EmailAddress')
+    attachment = SubjectRelation('File')
+
+
+class EmailPart(EntityType):
+    pass
+
+
+class EmailThread(EntityType):
+    see_also = SubjectRelation('EmailThread')
+
+
 class connait(RelationType):
     symmetric = True
 
@@ -246,6 +262,14 @@
     subject = 'Tag'
     object = ('CWUser', 'CWGroup', 'State', 'Note', 'Card', 'Affaire')
 
+class Folder(EntityType):
+    """folders are used to classify entities. They may be defined as a tree.
+    """
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=64)
+    description = RichString(fulltextindexed=True)
+    filed_under = SubjectRelation('Folder', description=_('parent folder'))
+
 class filed_under(RelationDefinition):
     subject = ('Note', 'Affaire')
     object = 'Folder'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,7 @@
+psycopg2
+cubicweb-basket
+cubicweb-card
+cubicweb-comment
+cubicweb-file
+cubicweb-localperms
+cubicweb-tag
--- a/server/test/unittest_ldapsource.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_ldapsource.py	Fri Oct 09 17:52:14 2015 +0200
@@ -25,8 +25,6 @@
 import subprocess
 import tempfile
 
-from logilab.common.testlib import TestCase, unittest_main, mock_object, Tags
-
 from cubicweb import AuthenticationError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import RQLGeneratorTC
@@ -480,4 +478,5 @@
 
 
 if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
     unittest_main()
--- a/server/test/unittest_migractions.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_migractions.py	Fri Oct 09 17:52:14 2015 +0200
@@ -18,7 +18,7 @@
 """unit tests for module cubicweb.server.migractions"""
 
 from datetime import date
-import os.path as osp
+import os, os.path as osp
 from contextlib import contextmanager
 
 from logilab.common.testlib import unittest_main, Tags, tag
@@ -26,6 +26,7 @@
 from yams.constraints import UniqueConstraint
 
 from cubicweb import ConfigurationError, ValidationError, ExecutionError
+from cubicweb.devtools import startpgcluster, stoppgcluster
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.migractions import ServerMigrationHelper
@@ -35,6 +36,11 @@
 
 HERE = osp.dirname(osp.abspath(__file__))
 
+
+def setUpModule():
+    startpgcluster(__file__)
+
+
 migrschema = None
 def tearDownModule(*args):
     global migrschema
@@ -43,10 +49,19 @@
         del MigrationCommandsTC.origschema
     if hasattr(MigrationCommandsComputedTC, 'origschema'):
         del MigrationCommandsComputedTC.origschema
+    stoppgcluster(__file__)
+
+
+class MigrationConfig(cubicweb.devtools.TestServerConfiguration):
+    default_sources = cubicweb.devtools.DEFAULT_PSQL_SOURCES
+    CUBES_PATH = [osp.join(HERE, 'data-migractions', 'cubes')]
+
 
 class MigrationTC(CubicWebTC):
 
-    configcls = cubicweb.devtools.TestServerConfiguration
+    appid = 'data-migractions'
+
+    configcls = MigrationConfig
 
     tags = CubicWebTC.tags | Tags(('server', 'migration', 'migractions'))
 
@@ -64,31 +79,32 @@
         config._apphome = osp.join(HERE, self.appid)
 
     def setUp(self):
-        CubicWebTC.setUp(self)
+        self.configcls.cls_adjust_sys_path()
+        super(MigrationTC, self).setUp()
 
     def tearDown(self):
-        CubicWebTC.tearDown(self)
+        super(MigrationTC, self).tearDown()
         self.repo.vreg['etypes'].clear_caches()
 
     @contextmanager
     def mh(self):
-        with self.admin_access.client_cnx() as cnx:
+        with self.admin_access.repo_cnx() as cnx:
             yield cnx, ServerMigrationHelper(self.repo.config, migrschema,
                                              repo=self.repo, cnx=cnx,
                                              interactive=False)
 
     def table_sql(self, mh, tablename):
-        result = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
-                            "and name=%(table)s", {'table': tablename})
+        result = mh.sqlexec("SELECT table_name FROM information_schema.tables WHERE LOWER(table_name)=%(table)s",
+                            {'table': tablename.lower()})
         if result:
             return result[0][0]
         return None # no such table
 
     def table_schema(self, mh, tablename):
-        sql = self.table_sql(mh, tablename)
-        assert sql, 'no table %s' % tablename
-        return dict(x.split()[:2]
-                    for x in sql.split('(', 1)[1].rsplit(')', 1)[0].split(','))
+        result = mh.sqlexec("SELECT column_name, data_type, character_maximum_length FROM information_schema.columns "
+                            "WHERE LOWER(table_name) = %(table)s", {'table': tablename.lower()})
+        assert result, 'no table %s' % tablename
+        return dict((x[0], (x[1], x[2])) for x in result)
 
 
 class MigrationCommandsTC(MigrationTC):
@@ -160,7 +176,7 @@
             self.assertEqual(self.schema['shortpara'].objects(), ('String', ))
             # test created column is actually a varchar(64)
             fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
-            self.assertEqual(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
+            self.assertEqual(fields['%sshortpara' % SQL_PREFIX], ('character varying', 64))
             # test default value set on existing entities
             self.assertEqual(cnx.execute('Note X').get_entity(0, 0).shortpara, 'hop')
             # test default value set for next entities
@@ -212,6 +228,7 @@
                                             droprequired=True):
                 mh.cmd_add_attribute('Note', 'unique_id')
                 mh.rqlexec('INSERT Note N')
+                mh.rqlexec('SET N unique_id "x"')
             # make sure the required=True was restored
             self.assertRaises(ValidationError, mh.rqlexec, 'INSERT Note N')
             mh.rollback()
@@ -259,8 +276,8 @@
                                'filed_under2', 'has_text',
                                'identity', 'in_basket', 'is', 'is_instance_of',
                                'modification_date', 'name', 'owned_by'])
-            self.assertEqual([str(rs) for rs in self.schema['Folder2'].object_relations()],
-                              ['filed_under2', 'identity'])
+            self.assertCountEqual([str(rs) for rs in self.schema['Folder2'].object_relations()],
+                                  ['filed_under2', 'identity'])
             # Old will be missing as it has been renamed into 'New' in the migrated
             # schema while New hasn't been added here.
             self.assertEqual(sorted(str(e) for e in self.schema['filed_under2'].subjects()),
@@ -567,15 +584,15 @@
                                      ('Bookmark', 'Bookmark'), ('Bookmark', 'Note'),
                                      ('Note', 'Note'), ('Note', 'Bookmark')]))
             try:
-                mh.cmd_drop_cube('email', removedeps=True)
+                mh.cmd_drop_cube('fakeemail', removedeps=True)
                 # file was there because it's an email dependancy, should have been removed
-                self.assertNotIn('email', self.config.cubes())
-                self.assertNotIn(self.config.cube_dir('email'), self.config.cubes_path())
+                self.assertNotIn('fakeemail', self.config.cubes())
+                self.assertNotIn(self.config.cube_dir('fakeemail'), self.config.cubes_path())
                 self.assertNotIn('file', self.config.cubes())
                 self.assertNotIn(self.config.cube_dir('file'), self.config.cubes_path())
                 for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
                                'sender', 'in_thread', 'reply_to', 'data_format'):
-                    self.assertFalse(ertype in schema, ertype)
+                    self.assertNotIn(ertype, schema)
                 self.assertEqual(sorted(schema['see_also'].rdefs.iterkeys()),
                                   sorted([('Folder', 'Folder'),
                                           ('Bookmark', 'Bookmark'),
@@ -584,17 +601,17 @@
                                           ('Note', 'Bookmark')]))
                 self.assertEqual(sorted(schema['see_also'].subjects()), ['Bookmark', 'Folder', 'Note'])
                 self.assertEqual(sorted(schema['see_also'].objects()), ['Bookmark', 'Folder', 'Note'])
-                self.assertEqual(cnx.execute('Any X WHERE X pkey "system.version.email"').rowcount, 0)
+                self.assertEqual(cnx.execute('Any X WHERE X pkey "system.version.fakeemail"').rowcount, 0)
                 self.assertEqual(cnx.execute('Any X WHERE X pkey "system.version.file"').rowcount, 0)
             finally:
-                mh.cmd_add_cube('email')
-                self.assertIn('email', self.config.cubes())
-                self.assertIn(self.config.cube_dir('email'), self.config.cubes_path())
+                mh.cmd_add_cube('fakeemail')
+                self.assertIn('fakeemail', self.config.cubes())
+                self.assertIn(self.config.cube_dir('fakeemail'), self.config.cubes_path())
                 self.assertIn('file', self.config.cubes())
                 self.assertIn(self.config.cube_dir('file'), self.config.cubes_path())
                 for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
                                'sender', 'in_thread', 'reply_to', 'data_format'):
-                    self.assertTrue(ertype in schema, ertype)
+                    self.assertIn(ertype, schema)
                 self.assertEqual(sorted(schema['see_also'].rdefs.iterkeys()),
                                   sorted([('EmailThread', 'EmailThread'), ('Folder', 'Folder'),
                                           ('Bookmark', 'Bookmark'),
@@ -603,9 +620,9 @@
                                           ('Note', 'Bookmark')]))
                 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.fakeemail.__pkginfo__ import version as email_version
                 from cubes.file.__pkginfo__ import version as file_version
-                self.assertEqual(cnx.execute('Any V WHERE X value V, X pkey "system.version.email"')[0][0],
+                self.assertEqual(cnx.execute('Any V WHERE X value V, X pkey "system.version.fakeemail"')[0][0],
                                   email_version)
                 self.assertEqual(cnx.execute('Any V WHERE X value V, X pkey "system.version.file"')[0][0],
                                   file_version)
@@ -623,16 +640,16 @@
             cubes = set(self.config.cubes())
             schema = self.repo.schema
             try:
-                mh.cmd_drop_cube('email')
-                cubes.remove('email')
-                self.assertNotIn('email', self.config.cubes())
+                mh.cmd_drop_cube('fakeemail')
+                cubes.remove('fakeemail')
+                self.assertNotIn('fakeemail', self.config.cubes())
                 self.assertIn('file', self.config.cubes())
                 for ertype in ('Email', 'EmailThread', 'EmailPart',
                                'sender', 'in_thread', 'reply_to'):
-                    self.assertFalse(ertype in schema, ertype)
+                    self.assertNotIn(ertype, schema)
             finally:
-                mh.cmd_add_cube('email')
-                self.assertIn('email', self.config.cubes())
+                mh.cmd_add_cube('fakeemail')
+                self.assertIn('fakeemail', self.config.cubes())
                 # trick: overwrite self.maxeid to avoid deletion of just reintroduced
                 #        types (and their associated tables!)
                 self.maxeid = cnx.execute('Any MAX(X)')[0][0] # XXXXXXX KILL KENNY
@@ -690,6 +707,18 @@
             mh.cmd_add_relation_type('same_as')
             self.assertTrue(self.table_sql(mh, 'same_as_relation'))
 
+    def test_change_attribute_type(self):
+        with self.mh() as (cnx, mh):
+            mh.cmd_create_entity('Societe', tel=1)
+            mh.commit()
+            mh.change_attribute_type('Societe', 'tel', 'Float')
+            self.assertNotIn(('Societe', 'Int'), self.schema['tel'].rdefs)
+            self.assertIn(('Societe', 'Float'), self.schema['tel'].rdefs)
+            self.assertEqual(self.schema['tel'].rdefs[('Societe', 'Float')].object, 'Float')
+            tel = mh.rqlexec('Any T WHERE X tel T')[0][0]
+            self.assertEqual(tel, 1.0)
+            self.assertIsInstance(tel, float)
+
 
 class MigrationCommandsComputedTC(MigrationTC):
     """ Unit tests for computed relations and attributes
@@ -787,7 +816,7 @@
         self.assertEqual(self.schema['score'].rdefs['Company', 'Float'].formula,
                          'Any AVG(NN) WHERE X employees E, N concerns E, N note NN')
         fields = self.table_schema(mh, '%sCompany' % SQL_PREFIX)
-        self.assertEqual(fields['%sscore' % SQL_PREFIX], 'float')
+        self.assertEqual(fields['%sscore' % SQL_PREFIX], ('double precision', None))
         self.assertEqual([[3.0]],
                          mh.rqlexec('Any CS WHERE C score CS, C is Company').rows)
 
@@ -811,10 +840,9 @@
 
     def assert_computed_attribute_dropped(self):
         self.assertNotIn('note20', self.schema)
-        # DROP COLUMN not supported by sqlite
-        #with self.mh() as (cnx, mh):
-        #    fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
-        #self.assertNotIn('%snote20' % SQL_PREFIX, fields)
+        with self.mh() as (cnx, mh):
+            fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
+        self.assertNotIn('%snote20' % SQL_PREFIX, fields)
 
     def test_computed_attribute_drop_type(self):
         self.assertIn('note20', self.schema)
--- a/server/test/unittest_postgres.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_postgres.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,17 +21,29 @@
 
 from logilab.common.testlib import SkipTest
 
-from cubicweb.devtools import PostgresApptestConfiguration
+from cubicweb import ValidationError
+from cubicweb.devtools import PostgresApptestConfiguration, startpgcluster, stoppgcluster
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.predicates import is_instance
 from cubicweb.entities.adapters import IFTIndexableAdapter
 
 from unittest_querier import FixedOffset
 
+
+def setUpModule():
+    startpgcluster(__file__)
+
+
+def tearDownModule():
+    stoppgcluster(__file__)
+
+
 class PostgresTimeoutConfiguration(PostgresApptestConfiguration):
-    default_sources = PostgresApptestConfiguration.default_sources.copy()
-    default_sources['system'] = PostgresApptestConfiguration.default_sources['system'].copy()
-    default_sources['system']['db-statement-timeout'] = 200
+    def __init__(self, *args, **kwargs):
+        self.default_sources = PostgresApptestConfiguration.default_sources.copy()
+        self.default_sources['system'] = PostgresApptestConfiguration.default_sources['system'].copy()
+        self.default_sources['system']['db-statement-timeout'] = 200
+        super(PostgresTimeoutConfiguration, self).__init__(*args, **kwargs)
 
 
 class PostgresFTITC(CubicWebTC):
@@ -119,6 +131,24 @@
             self.assertEqual(datenaiss.tzinfo, None)
             self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 2, 0))
 
+    def test_constraint_validationerror(self):
+        with self.admin_access.repo_cnx() as cnx:
+            with cnx.allow_all_hooks_but('integrity'):
+                with self.assertRaises(ValidationError) as cm:
+                    cnx.execute("INSERT Note N: N type 'nogood'")
+                self.assertEqual(cm.exception.errors,
+                        {'type-subject': u'invalid value %(KEY-value)s, it must be one of %(KEY-choices)s'})
+                self.assertEqual(cm.exception.msgargs,
+                        {'type-subject-value': u'"nogood"',
+                         'type-subject-choices': u'"todo", "a", "b", "T", "lalala"'})
+
+    def test_statement_timeout(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.system_sql('select pg_sleep(0.1)')
+            with self.assertRaises(Exception):
+                cnx.system_sql('select pg_sleep(0.3)')
+
+
 class PostgresLimitSizeTC(CubicWebTC):
     configcls = PostgresApptestConfiguration
 
@@ -137,12 +167,6 @@
             yield self.assertEqual, sql("SELECT limit_size('<span>a>b</span>', 'text/html', 2)"), \
                 'a>...'
 
-    def test_statement_timeout(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.system_sql('select pg_sleep(0.1)')
-            with self.assertRaises(Exception):
-                cnx.system_sql('select pg_sleep(0.3)')
-
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/server/test/unittest_querier.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_querier.py	Fri Oct 09 17:52:14 2015 +0200
@@ -69,7 +69,6 @@
 
 def tearDownClass(cls, *args):
     global repo, cnx
-    cnx.close()
     repo.shutdown()
     del repo, cnx
 
@@ -1173,11 +1172,10 @@
     def test_delete_3(self):
         s = self.user_groups_session('users')
         with s.new_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                peid, = self.o.execute(cnx, "INSERT Personne P: P nom 'toto'")[0]
-                seid, = self.o.execute(cnx, "INSERT Societe S: S nom 'logilab'")[0]
-                self.o.execute(cnx, "SET P travaille S")
-                cnx.commit()
+            peid, = self.o.execute(cnx, "INSERT Personne P: P nom 'toto'")[0]
+            seid, = self.o.execute(cnx, "INSERT Societe S: S nom 'logilab'")[0]
+            self.o.execute(cnx, "SET P travaille S")
+            cnx.commit()
         rset = self.qexecute('Personne P WHERE P travaille S')
         self.assertEqual(len(rset.rows), 1)
         self.qexecute("DELETE X travaille Y WHERE X eid %s, Y eid %s" % (peid, seid))
@@ -1212,12 +1210,11 @@
                               'X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
         self.qexecute("DELETE Email X")
         with self.session.new_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                sqlc = cnx.cnxset.cu
-                sqlc.execute('SELECT * FROM recipients_relation')
-                self.assertEqual(len(sqlc.fetchall()), 0)
-                sqlc.execute('SELECT * FROM owned_by_relation WHERE eid_from=%s'%eeid)
-                self.assertEqual(len(sqlc.fetchall()), 0)
+            sqlc = cnx.cnxset.cu
+            sqlc.execute('SELECT * FROM recipients_relation')
+            self.assertEqual(len(sqlc.fetchall()), 0)
+            sqlc.execute('SELECT * FROM owned_by_relation WHERE eid_from=%s'%eeid)
+            self.assertEqual(len(sqlc.fetchall()), 0)
 
     def test_nonregr_delete_cache2(self):
         eid = self.qexecute("INSERT Folder T: T name 'toto'")[0][0]
@@ -1364,12 +1361,11 @@
         self.assertRaises(Unauthorized,
                           self.qexecute, "Any P WHERE X is CWUser, X login 'bob', X upassword P")
         with self.session.new_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                cursor = cnx.cnxset.cu
-                cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
-                               % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
-                passwd = str(cursor.fetchone()[0])
-                self.assertEqual(passwd, crypt_password('toto', passwd))
+            cursor = cnx.cnxset.cu
+            cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
+                           % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
+            passwd = str(cursor.fetchone()[0])
+            self.assertEqual(passwd, crypt_password('toto', passwd))
         rset = self.qexecute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
         self.assertEqual(len(rset.rows), 1)
@@ -1377,21 +1373,20 @@
 
     def test_update_upassword(self):
         with self.session.new_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                rset = cnx.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s",
-                                   {'pwd': 'toto'})
-                self.assertEqual(rset.description[0][0], 'CWUser')
-                rset = cnx.execute("SET X upassword %(pwd)s WHERE X is CWUser, X login 'bob'",
-                                   {'pwd': 'tutu'})
-                cursor = cnx.cnxset.cu
-                cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
-                               % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
-                passwd = str(cursor.fetchone()[0])
-                self.assertEqual(passwd, crypt_password('tutu', passwd))
-                rset = cnx.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
-                                   {'pwd': Binary(passwd)})
-                self.assertEqual(len(rset.rows), 1)
-                self.assertEqual(rset.description, [('CWUser',)])
+            rset = cnx.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s",
+                               {'pwd': 'toto'})
+            self.assertEqual(rset.description[0][0], 'CWUser')
+            rset = cnx.execute("SET X upassword %(pwd)s WHERE X is CWUser, X login 'bob'",
+                               {'pwd': 'tutu'})
+            cursor = cnx.cnxset.cu
+            cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
+                           % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
+            passwd = str(cursor.fetchone()[0])
+            self.assertEqual(passwd, crypt_password('tutu', passwd))
+            rset = cnx.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
+                               {'pwd': Binary(passwd)})
+            self.assertEqual(len(rset.rows), 1)
+            self.assertEqual(rset.description, [('CWUser',)])
 
     # ZT datetime tests ########################################################
 
@@ -1402,6 +1397,13 @@
         self.assertEqual(datenaiss.tzinfo, None)
         self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 1, 0))
 
+    def test_tz_datetime_cache_nonregr(self):
+        datenaiss = datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1))
+        self.qexecute("INSERT Personne X: X nom 'bob', X tzdatenaiss %(date)s",
+                     {'date': datenaiss})
+        self.assertTrue(self.qexecute("Any X WHERE X tzdatenaiss %(d)s", {'d': datenaiss}))
+        self.assertFalse(self.qexecute("Any X WHERE X tzdatenaiss %(d)s", {'d': datenaiss - timedelta(1)}))
+
     # non regression tests #####################################################
 
     def test_nonregr_1(self):
@@ -1512,9 +1514,9 @@
     def test_nonregr_has_text_cache(self):
         eid1 = self.qexecute("INSERT Personne X: X nom 'bidule'")[0][0]
         eid2 = self.qexecute("INSERT Personne X: X nom 'tag'")[0][0]
-        rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': 'bidule'})
+        rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': u'bidule'})
         self.assertEqual(rset.rows, [[eid1]])
-        rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': 'tag'})
+        rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': u'tag'})
         self.assertEqual(rset.rows, [[eid2]])
 
     def test_nonregr_sortterm_management(self):
@@ -1625,9 +1627,9 @@
             aff2 = cnx.create_entity('Societe', nom=u'aff2')
             cnx.commit()
         with self.new_access('user').repo_cnx() as cnx:
-            res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': 'aff1'})
+            res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': u'aff1'})
             self.assertEqual(res.rows, [[aff1.eid]])
-            res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': 'aff2'})
+            res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': u'aff2'})
             self.assertEqual(res.rows, [[aff2.eid]])
 
     def test_set_relations_eid(self):
--- a/server/test/unittest_repository.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_repository.py	Fri Oct 09 17:52:14 2015 +0200
@@ -31,10 +31,9 @@
                       UnknownEid, AuthenticationError, Unauthorized, QueryError)
 from cubicweb.predicates import is_instance
 from cubicweb.schema import RQLConstraint
-from cubicweb.dbapi import connect, multiple_connections_unfix
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import tuplify
-from cubicweb.server import repository, hook
+from cubicweb.server import hook
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.hook import Hook
 from cubicweb.server.sources import native
@@ -93,35 +92,17 @@
         self.assertRaises(AuthenticationError,
                           self.repo.connect, None)
 
-    def test_execute(self):
+    def test_login_upassword_accent(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, '
+                        'X in_group G WHERE G name "users"',
+                        {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')})
+            cnx.commit()
         repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        repo.execute(cnxid, 'Any X')
-        repo.execute(cnxid, 'Any X where X is Personne')
-        repo.execute(cnxid, 'Any X where X is Personne, X nom ~= "to"')
-        repo.execute(cnxid, 'Any X WHERE X has_text %(text)s', {'text': u'\xe7a'})
-        repo.close(cnxid)
-
-    def test_login_upassword_accent(self):
-        repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_group G WHERE G name "users"',
-                     {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')})
-        repo.commit(cnxid)
-        repo.close(cnxid)
         cnxid = repo.connect(u"barnabé", password=u"héhéhé".encode('UTF8'))
         self.assert_(cnxid)
         repo.close(cnxid)
 
-    def test_rollback_on_commit_error(self):
-        cnxid = self.repo.connect(self.admlogin, password=self.admpassword)
-        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.assertFalse(self.repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"'))
-        self.repo.close(cnxid)
-
     def test_rollback_on_execute_validation_error(self):
         class ValidationErrorAfterHook(Hook):
             __regid__ = 'valerror-after-hook'
@@ -166,31 +147,6 @@
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
         self.assert_(cnxid)
         repo.close(cnxid)
-        self.assertRaises(BadConnectionId, repo.execute, cnxid, 'Any X')
-
-    def test_invalid_cnxid(self):
-        self.assertRaises(BadConnectionId, self.repo.execute, 0, 'Any X')
-        self.assertRaises(BadConnectionId, self.repo.close, None)
-
-    def test_shared_data(self):
-        repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        repo.set_shared_data(cnxid, 'data', 4)
-        cnxid2 = repo.connect(self.admlogin, password=self.admpassword)
-        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.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.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')
-        self.assertRaises(BadConnectionId, repo.get_shared_data, cnxid2, 'data')
-        self.assertRaises(BadConnectionId, repo.set_shared_data, cnxid, 'data', 1)
-        self.assertRaises(BadConnectionId, repo.set_shared_data, cnxid2, 'data', 1)
 
     def test_check_session(self):
         repo = self.repo
@@ -199,76 +155,6 @@
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.check_session, cnxid)
 
-    def test_transaction_base(self):
-        repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        # check db state
-        result = repo.execute(cnxid, 'Personne X')
-        self.assertEqual(result.rowcount, 0)
-        # rollback entity insertion
-        repo.execute(cnxid, "INSERT Personne X: X nom 'bidule'")
-        result = repo.execute(cnxid, 'Personne X')
-        self.assertEqual(result.rowcount, 1)
-        repo.rollback(cnxid)
-        result = repo.execute(cnxid, 'Personne X')
-        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.assertEqual(result.rowcount, 1)
-        repo.close(cnxid)
-
-    def test_transaction_base2(self):
-        repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        # 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.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.assertEqual(result.rowcount, 0, result.rows)
-        repo.close(cnxid)
-
-    def test_transaction_base3(self):
-        repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        # rollback state change which trigger TrInfo insertion
-        session = repo._get_session(cnxid)
-        user = session.user
-        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.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.assertEqual(len(rset), 0)
-        repo.close(cnxid)
-
-    def test_close_kill_processing_request(self):
-        repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        repo.execute(cnxid, 'INSERT CWUser X: X login "toto", X upassword "tutu", X in_group G WHERE G name "users"')
-        repo.commit(cnxid)
-        lock = threading.Lock()
-        lock.acquire()
-        # close has to be in the thread due to sqlite limitations
-        def close_in_a_few_moment():
-            lock.acquire()
-            repo.close(cnxid)
-        t = threading.Thread(target=close_in_a_few_moment)
-        t.start()
-        def run_transaction():
-            lock.release()
-            repo.execute(cnxid, 'DELETE CWUser X WHERE X login "toto"')
-            repo.commit(cnxid)
-        try:
-            with self.assertRaises(SessionClosedError) as cm:
-                run_transaction()
-            self.assertEqual(str(cm.exception), 'try to access connections set on a closed session %s' % cnxid)
-        finally:
-            t.join()
-
     def test_initial_schema(self):
         schema = self.repo.schema
         # check order of attributes is respected
@@ -312,118 +198,14 @@
         ownedby = schema.rschema('owned_by')
         self.assertEqual(ownedby.objects('CWEType'), ('CWUser',))
 
-    def test_pyro(self):
-        import Pyro
-        Pyro.config.PYRO_MULTITHREADED = 0
-        done = []
-        self.repo.config.global_set_option('pyro-ns-host', 'NO_PYRONS')
-        daemon = self.repo.pyro_register()
-        try:
-            uri = self.repo.pyro_uri.replace('PYRO', 'pyroloc')
-            # the client part has to be in the thread due to sqlite limitations
-            t = threading.Thread(target=self._pyro_client, args=(uri, done))
-            t.start()
-            while not done:
-                daemon.handleRequests(1.0)
-            t.join(1)
-            if t.isAlive():
-                self.fail('something went wrong, thread still alive')
-        finally:
-            repository.pyro_unregister(self.repo.config)
-            from logilab.common import pyro_ext
-            pyro_ext._DAEMONS.clear()
-
-
-    def _pyro_client(self, uri, done):
-        cnx = connect(uri,
-                      u'admin', password='gingkow',
-                      initlog=False) # don't reset logging configuration
-        try:
-            cnx.load_appobjects(subpath=('entities',))
-            # check we can get the schema
-            schema = cnx.get_schema()
-            self.assertTrue(cnx.vreg)
-            self.assertTrue('etypes'in cnx.vreg)
-            cu = cnx.cursor()
-            rset = cu.execute('Any U,G WHERE U in_group G')
-            user = iter(rset.entities()).next()
-            self.assertTrue(user._cw)
-            self.assertTrue(user._cw.vreg)
-            from cubicweb.entities import authobjs
-            self.assertIsInstance(user._cw.user, authobjs.CWUser)
-            # make sure the tcp connection is closed properly; yes, it's disgusting.
-            adapter = cnx._repo.adapter
-            cnx.close()
-            adapter.release()
-            done.append(True)
-        finally:
-            # connect monkey patch some method by default, remove them
-            multiple_connections_unfix()
-
-
-    def test_zmq(self):
-        try:
-            import zmq
-        except ImportError:
-            self.skipTest("zmq in not available")
-        done = []
-        from cubicweb.devtools import TestServerConfiguration as ServerConfiguration
-        from cubicweb.server.cwzmq import ZMQRepositoryServer
-        # the client part has to be in a thread due to sqlite limitations
-        t = threading.Thread(target=self._zmq_client, args=(done,))
-        t.start()
-
-        zmq_server = ZMQRepositoryServer(self.repo)
-        zmq_server.connect('zmqpickle-tcp://127.0.0.1:41415')
-
-        t2 = threading.Thread(target=self._zmq_quit, args=(done, zmq_server,))
-        t2.start()
-
-        zmq_server.run()
-
-        t2.join(1)
-        t.join(1)
-
-        if t.isAlive():
-            self.fail('something went wrong, thread still alive')
-
-    def _zmq_quit(self, done, srv):
-        while not done:
-            time.sleep(0.1)
-        srv.quit()
-
-    def _zmq_client(self, done):
-        try:
-            cnx = connect('zmqpickle-tcp://127.0.0.1:41415', u'admin', password=u'gingkow',
-                          initlog=False) # don't reset logging configuration
-            try:
-                cnx.load_appobjects(subpath=('entities',))
-                # check we can get the schema
-                schema = cnx.get_schema()
-                self.assertTrue(cnx.vreg)
-                self.assertTrue('etypes'in cnx.vreg)
-                cu = cnx.cursor()
-                rset = cu.execute('Any U,G WHERE U in_group G')
-                user = iter(rset.entities()).next()
-                self.assertTrue(user._cw)
-                self.assertTrue(user._cw.vreg)
-                from cubicweb.entities import authobjs
-                self.assertIsInstance(user._cw.user, authobjs.CWUser)
-                cnx.close()
-                done.append(True)
-            finally:
-                # connect monkey patch some method by default, remove them
-                multiple_connections_unfix()
-        finally:
-            done.append(False)
-
     def test_internal_api(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        session = repo._get_session(cnxid, setcnxset=True)
-        self.assertEqual(repo.type_and_source_from_eid(2, session),
-                         ('CWGroup', None, 'system'))
-        self.assertEqual(repo.type_from_eid(2, session), 'CWGroup')
+        session = repo._get_session(cnxid)
+        with session.new_cnx() as cnx:
+            self.assertEqual(repo.type_and_source_from_eid(2, cnx),
+                             ('CWGroup', None, 'system'))
+            self.assertEqual(repo.type_from_eid(2, cnx), 'CWGroup')
         repo.close(cnxid)
 
     def test_public_api(self):
@@ -435,45 +217,11 @@
         # .properties() return a result set
         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.assertEqual(repo.user_info(cnxid), (6, 'admin', set([u'managers']), {}))
-        self.assertEqual({'type': u'CWGroup', 'extid': None, 'source': 'system'},
-                         repo.entity_metas(cnxid, 2))
-        self.assertEqual(repo.describe(cnxid, 2), (u'CWGroup', 'system', None, 'system'))
-        repo.close(cnxid)
-        self.assertRaises(BadConnectionId, repo.user_info, cnxid)
-        self.assertRaises(BadConnectionId, repo.describe, cnxid, 1)
-
-    def test_shared_data_api(self):
-        repo = self.repo
-        cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        self.assertEqual(repo.get_shared_data(cnxid, 'data'), None)
-        repo.set_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.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')
-
     def test_schema_is_relation(self):
         with self.admin_access.repo_cnx() as cnx:
             no_is_rset = cnx.execute('Any X WHERE NOT X is ET')
             self.assertFalse(no_is_rset, no_is_rset.description)
 
-#     def test_perfo(self):
-#         self.set_debug(True)
-#         from time import time, clock
-#         t, c = time(), clock()
-#         try:
-#             self.create_user('toto')
-#         finally:
-#             self.set_debug(False)
-#         print 'test time: %.3f (time) %.3f (cpu)' % ((time() - t), clock() - c)
-
     def test_delete_if_singlecard1(self):
         with self.admin_access.repo_cnx() as cnx:
             note = cnx.create_entity('Affaire')
@@ -626,38 +374,37 @@
             namecol = SQL_PREFIX + 'name'
             finalcol = SQL_PREFIX + 'final'
             with self.admin_access.repo_cnx() as cnx:
-                with cnx.ensure_cnx_set:
-                    cu = cnx.system_sql('SELECT %s FROM %s WHERE %s is NULL'
-                                        % (namecol, table, finalcol))
-                    self.assertEqual(cu.fetchall(), [])
-                    cu = cnx.system_sql('SELECT %s FROM %s '
-                                        'WHERE %s=%%(final)s ORDER BY %s'
-                                        % (namecol, table, finalcol, namecol),
-                                        {'final': True})
-                    self.assertEqual(cu.fetchall(),
-                                     [(u'BabarTestType',),
-                                      (u'BigInt',), (u'Boolean',), (u'Bytes',),
-                                      (u'Date',), (u'Datetime',),
-                                      (u'Decimal',),(u'Float',),
-                                      (u'Int',),
-                                      (u'Interval',), (u'Password',),
-                                      (u'String',),
-                                      (u'TZDatetime',), (u'TZTime',), (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 = cnx.system_sql(sql)
-                    rows = cu.fetchall()
-                    self.assertEqual(len(rows), 3)
-                    person = self.repo.schema.eschema('Personne')
-                    self.assertEqual(len(person._unique_together), 1)
-                    self.assertItemsEqual(person._unique_together[0],
-                                          ('nom', 'prenom', 'inline2'))
+                cu = cnx.system_sql('SELECT %s FROM %s WHERE %s is NULL'
+                                    % (namecol, table, finalcol))
+                self.assertEqual(cu.fetchall(), [])
+                cu = cnx.system_sql('SELECT %s FROM %s '
+                                    'WHERE %s=%%(final)s ORDER BY %s'
+                                    % (namecol, table, finalcol, namecol),
+                                    {'final': True})
+                self.assertEqual(cu.fetchall(),
+                                 [(u'BabarTestType',),
+                                  (u'BigInt',), (u'Boolean',), (u'Bytes',),
+                                  (u'Date',), (u'Datetime',),
+                                  (u'Decimal',),(u'Float',),
+                                  (u'Int',),
+                                  (u'Interval',), (u'Password',),
+                                  (u'String',),
+                                  (u'TZDatetime',), (u'TZTime',), (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 = cnx.system_sql(sql)
+                rows = cu.fetchall()
+                self.assertEqual(len(rows), 3)
+                person = self.repo.schema.eschema('Personne')
+                self.assertEqual(len(person._unique_together), 1)
+                self.assertItemsEqual(person._unique_together[0],
+                                      ('nom', 'prenom', 'inline2'))
 
         finally:
             self.repo.set_schema(origshema)
@@ -680,30 +427,26 @@
 
     def test_type_from_eid(self):
         with self.admin_access.repo_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                self.assertEqual(self.repo.type_from_eid(2, cnx), 'CWGroup')
+            self.assertEqual(self.repo.type_from_eid(2, cnx), 'CWGroup')
 
     def test_type_from_eid_raise(self):
         with self.admin_access.repo_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                self.assertRaises(UnknownEid, self.repo.type_from_eid, -2, cnx)
+            self.assertRaises(UnknownEid, self.repo.type_from_eid, -2, cnx)
 
     def test_add_delete_info(self):
         with self.admin_access.repo_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                cnx.mode = 'write'
-                entity = self.repo.vreg['etypes'].etype_class('Personne')(cnx)
-                entity.eid = -1
-                entity.complete = lambda x: None
-                self.repo.add_info(cnx, entity, self.repo.system_source)
-                cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
-                data = cu.fetchall()
-                self.assertEqual(tuplify(data), [(-1, 'Personne', 'system', None)])
-                self.repo.delete_info(cnx, entity, 'system')
-                #self.repo.commit()
-                cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
-                data = cu.fetchall()
-                self.assertEqual(data, [])
+            entity = self.repo.vreg['etypes'].etype_class('Personne')(cnx)
+            entity.eid = -1
+            entity.complete = lambda x: None
+            self.repo.add_info(cnx, entity, self.repo.system_source)
+            cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
+            data = cu.fetchall()
+            self.assertEqual(tuplify(data), [(-1, 'Personne', 'system', None)])
+            self.repo._delete_cascade_multi(cnx, [entity])
+            self.repo.system_source.delete_info_multi(cnx, [entity])
+            cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
+            data = cu.fetchall()
+            self.assertEqual(data, [])
 
 
 class FTITC(CubicWebTC):
@@ -757,9 +500,7 @@
                               u'system.version.card',
                               u'system.version.comment',
                               u'system.version.cubicweb',
-                              u'system.version.email',
                               u'system.version.file',
-                              u'system.version.folder',
                               u'system.version.localperms',
                               u'system.version.tag'])
 
--- a/server/test/unittest_rql2sql.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_rql2sql.py	Fri Oct 09 17:52:14 2015 +0200
@@ -396,13 +396,13 @@
 ORDER BY 1'''),
 
     # DISTINCT, can use relation under exists scope as principal
-    ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
+    ('DISTINCT Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
      '''SELECT DISTINCT _X.cw_eid, rel_read_permission0.eid_to
 FROM cw_CWEType AS _X, read_permission_relation AS rel_read_permission0
 WHERE _X.cw_name=CWGroup AND rel_read_permission0.eid_to IN(1, 2, 3) AND EXISTS(SELECT 1 WHERE rel_read_permission0.eid_from=_X.cw_eid)'''),
 
     # no distinct, Y can't be invariant
-    ('Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
+    ('Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
      '''SELECT _X.cw_eid, _Y.cw_eid
 FROM cw_CWEType AS _X, cw_CWGroup AS _Y
 WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid)
@@ -412,7 +412,7 @@
 WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid)'''),
 
     # DISTINCT but NEGED exists, can't be invariant
-    ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)',
+    ('DISTINCT Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)',
      '''SELECT DISTINCT _X.cw_eid, _Y.cw_eid
 FROM cw_CWEType AS _X, cw_CWGroup AS _Y
 WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))
@@ -422,7 +422,7 @@
 WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))'''),
 
     # should generate the same query as above
-    ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y',
+    ('DISTINCT Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), NOT X read_permission Y',
      '''SELECT DISTINCT _X.cw_eid, _Y.cw_eid
 FROM cw_CWEType AS _X, cw_CWGroup AS _Y
 WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))
@@ -432,7 +432,7 @@
 WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))'''),
 
     # neged relation, can't be inveriant
-    ('Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y',
+    ('Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), NOT X read_permission Y',
      '''SELECT _X.cw_eid, _Y.cw_eid
 FROM cw_CWEType AS _X, cw_CWGroup AS _Y
 WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))
@@ -563,6 +563,17 @@
      '''SELECT _X.cw_eid
 FROM cw_Note AS _X
 WHERE _X.cw_eid IN(999998, 999999) AND NOT (EXISTS(SELECT 1 FROM cw_source_relation AS rel_cw_source0 WHERE rel_cw_source0.eid_from=_X.cw_eid))'''),
+
+    # Test for https://www.cubicweb.org/ticket/5503548
+    ('''Any X
+        WHERE X is CWSourceSchemaConfig,
+        EXISTS(X created_by U, U login L),
+        X cw_schema X_CW_SCHEMA,
+        X owned_by X_OWNED_BY?
+    ''', '''SELECT _X.cw_eid
+FROM cw_CWSourceSchemaConfig AS _X LEFT OUTER JOIN owned_by_relation AS rel_owned_by1 ON (rel_owned_by1.eid_from=_X.cw_eid)
+WHERE EXISTS(SELECT 1 FROM created_by_relation AS rel_created_by0, cw_CWUser AS _U WHERE rel_created_by0.eid_from=_X.cw_eid AND rel_created_by0.eid_to=_U.cw_eid) AND _X.cw_cw_schema IS NOT NULL
+''')
     ]
 
 ADVANCED_WITH_GROUP_CONCAT = [
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_schema2sql.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,285 @@
+# copyright 2004-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""unit tests for module cubicweb.server.schema2sql
+"""
+
+import os.path as osp
+
+from logilab.common.testlib import TestCase, unittest_main
+from logilab.database import get_db_helper
+
+from yams.reader import SchemaLoader
+from cubicweb.server import schema2sql
+
+schema2sql.SET_DEFAULT = True
+
+DATADIR = osp.abspath(osp.join(osp.dirname(__file__), 'data-schema2sql'))
+
+schema = SchemaLoader().load([DATADIR])
+
+
+EXPECTED_DATA_NO_DROP = """
+CREATE TABLE Affaire(
+ sujet varchar(128),
+ ref varchar(12),
+ inline_rel integer REFERENCES entities (eid)
+);
+CREATE INDEX affaire_inline_rel_idx ON Affaire(inline_rel);
+
+CREATE TABLE Company(
+ name text
+);
+
+CREATE TABLE Datetest(
+ dt1 timestamp,
+ dt2 timestamp,
+ d1 date,
+ d2 date,
+ t1 time,
+ t2 time
+, CONSTRAINT cstredd407706bdfbd2285714dd689e8fcc0 CHECK(d1 <= CAST(clock_timestamp() AS DATE))
+);
+
+CREATE TABLE Division(
+ name text
+);
+
+CREATE TABLE EPermission(
+ name varchar(100) NOT NULL
+);
+CREATE INDEX epermission_name_idx ON EPermission(name);
+
+CREATE TABLE Eetype(
+ name varchar(64) UNIQUE NOT NULL,
+ description text,
+ meta boolean,
+ final boolean,
+ initial_state integer REFERENCES entities (eid)
+);
+CREATE INDEX eetype_name_idx ON Eetype(name);
+CREATE INDEX eetype_initial_state_idx ON Eetype(initial_state);
+
+CREATE TABLE Employee(
+);
+
+CREATE TABLE Note(
+ date varchar(10),
+ type varchar(1),
+ para varchar(512)
+);
+
+CREATE TABLE Person(
+ nom varchar(64) NOT NULL,
+ prenom varchar(64),
+ sexe varchar(1) DEFAULT 'M',
+ promo varchar(6),
+ titre varchar(128),
+ adel varchar(128),
+ ass varchar(128),
+ web varchar(128),
+ tel integer,
+ fax integer,
+ datenaiss date,
+ test boolean,
+ salary float
+, CONSTRAINT cstr41fe7db9ce1d5be95de2477e26590386 CHECK(promo IN ('bon', 'pasbon'))
+);
+CREATE UNIQUE INDEX unique_e6c2d219772dbf1715597f7d9a6b3892 ON Person(nom,prenom);
+
+CREATE TABLE Salaried(
+ nom varchar(64) NOT NULL,
+ prenom varchar(64),
+ sexe varchar(1) DEFAULT 'M',
+ promo varchar(6),
+ titre varchar(128),
+ adel varchar(128),
+ ass varchar(128),
+ web varchar(128),
+ tel integer,
+ fax integer,
+ datenaiss date,
+ test boolean,
+ salary float
+, CONSTRAINT cstrc8556fcc665865217761cdbcd220cae0 CHECK(promo IN ('bon', 'pasbon'))
+);
+CREATE UNIQUE INDEX unique_98da0f9de8588baa8966f0b1a6f850a3 ON Salaried(nom,prenom);
+
+CREATE TABLE Societe(
+ nom varchar(64),
+ web varchar(128),
+ tel integer,
+ fax integer,
+ rncs varchar(32),
+ ad1 varchar(128),
+ ad2 varchar(128),
+ ad3 varchar(128),
+ cp varchar(12),
+ ville varchar(32)
+, CONSTRAINT cstrc51dd462e9f6115506a0fe468d4c8114 CHECK(fax <= tel)
+);
+
+CREATE TABLE State(
+ eid integer PRIMARY KEY REFERENCES entities (eid),
+ name varchar(256) NOT NULL,
+ description text
+);
+CREATE INDEX state_name_idx ON State(name);
+
+CREATE TABLE Subcompany(
+ name text
+);
+
+CREATE TABLE Subdivision(
+ name text
+);
+
+CREATE TABLE pkginfo(
+ modname varchar(30) NOT NULL,
+ version varchar(10) DEFAULT '0.1' NOT NULL,
+ copyright text NOT NULL,
+ license varchar(3),
+ short_desc varchar(80) NOT NULL,
+ long_desc text NOT NULL,
+ author varchar(100) NOT NULL,
+ author_email varchar(100) NOT NULL,
+ mailinglist varchar(100),
+ debian_handler varchar(6)
+, CONSTRAINT cstr70f766f834557c715815d76f0a0db956 CHECK(license IN ('GPL', 'ZPL'))
+, CONSTRAINT cstr831a117424d0007ae0278cc15f344f5e CHECK(debian_handler IN ('machin', 'bidule'))
+);
+
+
+CREATE TABLE concerne_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT concerne_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX concerne_relation_from_idx ON concerne_relation(eid_from);
+CREATE INDEX concerne_relation_to_idx ON concerne_relation(eid_to);
+
+CREATE TABLE division_of_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT division_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX division_of_relation_from_idx ON division_of_relation(eid_from);
+CREATE INDEX division_of_relation_to_idx ON division_of_relation(eid_to);
+
+CREATE TABLE evaluee_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT evaluee_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX evaluee_relation_from_idx ON evaluee_relation(eid_from);
+CREATE INDEX evaluee_relation_to_idx ON evaluee_relation(eid_to);
+
+CREATE TABLE next_state_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT next_state_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX next_state_relation_from_idx ON next_state_relation(eid_from);
+CREATE INDEX next_state_relation_to_idx ON next_state_relation(eid_to);
+
+CREATE TABLE obj_wildcard_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT obj_wildcard_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX obj_wildcard_relation_from_idx ON obj_wildcard_relation(eid_from);
+CREATE INDEX obj_wildcard_relation_to_idx ON obj_wildcard_relation(eid_to);
+
+CREATE TABLE require_permission_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT require_permission_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX require_permission_relation_from_idx ON require_permission_relation(eid_from);
+CREATE INDEX require_permission_relation_to_idx ON require_permission_relation(eid_to);
+
+CREATE TABLE state_of_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT state_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX state_of_relation_from_idx ON state_of_relation(eid_from);
+CREATE INDEX state_of_relation_to_idx ON state_of_relation(eid_to);
+
+CREATE TABLE subcompany_of_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT subcompany_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX subcompany_of_relation_from_idx ON subcompany_of_relation(eid_from);
+CREATE INDEX subcompany_of_relation_to_idx ON subcompany_of_relation(eid_to);
+
+CREATE TABLE subdivision_of_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT subdivision_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX subdivision_of_relation_from_idx ON subdivision_of_relation(eid_from);
+CREATE INDEX subdivision_of_relation_to_idx ON subdivision_of_relation(eid_to);
+
+CREATE TABLE subj_wildcard_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT subj_wildcard_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX subj_wildcard_relation_from_idx ON subj_wildcard_relation(eid_from);
+CREATE INDEX subj_wildcard_relation_to_idx ON subj_wildcard_relation(eid_to);
+
+CREATE TABLE sym_rel_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT sym_rel_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX sym_rel_relation_from_idx ON sym_rel_relation(eid_from);
+CREATE INDEX sym_rel_relation_to_idx ON sym_rel_relation(eid_to);
+
+CREATE TABLE travaille_relation (
+  eid_from INTEGER NOT NULL REFERENCES entities (eid),
+  eid_to INTEGER NOT NULL REFERENCES entities (eid),
+  CONSTRAINT travaille_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX travaille_relation_from_idx ON travaille_relation(eid_from);
+CREATE INDEX travaille_relation_to_idx ON travaille_relation(eid_to);
+"""
+
+class SQLSchemaTC(TestCase):
+
+    def test_known_values(self):
+        dbhelper = get_db_helper('postgres')
+        output = schema2sql.schema2sql(dbhelper, schema, skip_relations=('works_for',))
+        self.assertMultiLineEqual(EXPECTED_DATA_NO_DROP.strip(), output.strip())
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/server/test/unittest_schemaserial.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_schemaserial.py	Fri Oct 09 17:52:14 2015 +0200
@@ -17,9 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for schema rql (de)serialization"""
 
-import sys
-from cStringIO import StringIO
-
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb import Binary
@@ -437,6 +434,8 @@
         self.repo.set_schema(self.repo.deserialize_schema(), resetvreg=False)
         schema = self.repo.schema
         self.assertEqual([('Company', 'Person')], list(schema['has_employee'].rdefs))
+        self.assertEqual(schema['has_employee'].rdef('Company', 'Person').permissions['read'],
+                         (u'managers',))
         self.assertEqual('O works_for S',
                          schema['has_employee'].rule)
         self.assertEqual([('Company', 'Int')], list(schema['total_salary'].rdefs))
--- a/server/test/unittest_security.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_security.py	Fri Oct 09 17:52:14 2015 +0200
@@ -31,9 +31,9 @@
     def setup_database(self):
         super(BaseSecurityTC, self).setup_database()
         with self.admin_access.client_cnx() as cnx:
-            self.create_user(cnx, 'iaminusersgrouponly')
+            self.create_user(cnx, u'iaminusersgrouponly')
             hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
-            self.create_user(cnx, 'oldpassword', password=Binary(hash))
+            self.create_user(cnx, u'oldpassword', password=Binary(hash))
 
 class LowLevelSecurityFunctionTC(BaseSecurityTC):
 
@@ -45,7 +45,7 @@
             with self.admin_access.repo_cnx() as cnx:
                 self.repo.vreg.solutions(cnx, rqlst, None)
                 check_relations_read_access(cnx, rqlst, {})
-            with self.new_access('anon').repo_cnx() as cnx:
+            with self.new_access(u'anon').repo_cnx() as cnx:
                 self.assertRaises(Unauthorized,
                                   check_relations_read_access,
                                   cnx, rqlst, {})
@@ -60,7 +60,7 @@
                 solution = rqlst.solutions[0]
                 localchecks = get_local_checks(cnx, rqlst, solution)
                 self.assertEqual({}, localchecks)
-            with self.new_access('anon').repo_cnx() as cnx:
+            with self.new_access(u'anon').repo_cnx() as cnx:
                 self.assertRaises(Unauthorized,
                                   get_local_checks,
                                   cnx, rqlst, solution)
@@ -70,7 +70,7 @@
         with self.admin_access.repo_cnx() as cnx:
             self.assertRaises(Unauthorized,
                               cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             self.assertRaises(Unauthorized,
                               cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
 
@@ -104,7 +104,7 @@
         super(SecurityRewritingTC, self).tearDown()
 
     def test_not_relation_read_security(self):
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             self.hijack_source_execute()
             cnx.execute('Any U WHERE NOT A todo_by U, A is Affaire')
             self.assertEqual(self.query[0][1].as_string(),
@@ -126,13 +126,13 @@
             cnx.commit()
 
     def test_insert_security(self):
-        with self.new_access('anon').repo_cnx() as cnx:
+        with self.new_access(u'anon').repo_cnx() as cnx:
             cnx.execute("INSERT Personne X: X nom 'bidule'")
             self.assertRaises(Unauthorized, cnx.commit)
             self.assertEqual(cnx.execute('Personne X').rowcount, 1)
 
     def test_insert_security_2(self):
-        with self.new_access('anon').repo_cnx() as cnx:
+        with self.new_access(u'anon').repo_cnx() as cnx:
             cnx.execute("INSERT Affaire X")
             self.assertRaises(Unauthorized, cnx.commit)
             # anon has no read permission on Affaire entities, so
@@ -141,20 +141,20 @@
 
     def test_insert_rql_permission(self):
         # test user can only add une affaire related to a societe he owns
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute("INSERT Affaire X: X sujet 'cool'")
             self.assertRaises(Unauthorized, cnx.commit)
         # test nothing has actually been inserted
         with self.admin_access.repo_cnx() as cnx:
             self.assertEqual(cnx.execute('Affaire X').rowcount, 1)
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute("INSERT Affaire X: X sujet 'cool'")
             cnx.execute("INSERT Societe X: X nom 'chouette'")
             cnx.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
             cnx.commit()
 
     def test_update_security_1(self):
-        with self.new_access('anon').repo_cnx() as cnx:
+        with self.new_access(u'anon').repo_cnx() as cnx:
             # local security check
             cnx.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
             self.assertRaises(Unauthorized, cnx.commit)
@@ -164,7 +164,7 @@
     def test_update_security_2(self):
         with self.temporary_permissions(Personne={'read': ('users', 'managers'),
                                                   'add': ('guests', 'users', 'managers')}):
-            with self.new_access('anon').repo_cnx() as cnx:
+            with self.new_access(u'anon').repo_cnx() as cnx:
                 self.assertRaises(Unauthorized, cnx.execute,
                                   "SET X nom 'bidulechouette' WHERE X is Personne")
         # test nothing has actually been inserted
@@ -172,7 +172,7 @@
             self.assertEqual(cnx.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
 
     def test_update_security_3(self):
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute("INSERT Personne X: X nom 'biduuule'")
             cnx.execute("INSERT Societe X: X nom 'looogilab'")
             cnx.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
@@ -191,7 +191,7 @@
             cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
             cnx.commit()
         # test user can only update une affaire related to a societe he owns
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute("SET X sujet 'pascool' WHERE X is Affaire")
             # this won't actually do anything since the selection query won't return anything
             cnx.commit()
@@ -212,7 +212,7 @@
         #self.assertRaises(Unauthorized,
         #                  self.o.execute, user, "DELETE CWUser X WHERE X login 'bidule'")
         # check local security
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             self.assertRaises(Unauthorized, cnx.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
 
     def test_delete_rql_permission(self):
@@ -220,7 +220,7 @@
             cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
             cnx.commit()
         # test user can only dele une affaire related to a societe he owns
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             # this won't actually do anything since the selection query won't return anything
             cnx.execute("DELETE Affaire X")
             cnx.commit()
@@ -239,7 +239,7 @@
             cnx.commit()
 
     def test_insert_relation_rql_permission(self):
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
             # should raise Unauthorized since user don't own S though this won't
             # actually do anything since the selection query won't return
@@ -266,7 +266,7 @@
         with self.admin_access.repo_cnx() as cnx:
             cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             # this won't actually do anything since the selection query won't return anything
             cnx.execute("DELETE A concerne S")
             cnx.commit()
@@ -277,7 +277,7 @@
                          {'x': eid})
             cnx.execute("SET A concerne S WHERE A sujet 'pascool', S is Societe")
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             self.assertRaises(Unauthorized, cnx.execute, "DELETE A concerne S")
             self.assertRaises(QueryError, cnx.commit) # can't commit anymore
             cnx.rollback()
@@ -290,8 +290,8 @@
 
     def test_user_can_change_its_upassword(self):
         with self.admin_access.repo_cnx() as cnx:
-            ueid = self.create_user(cnx, 'user').eid
-        with self.new_access('user').repo_cnx() as cnx:
+            ueid = self.create_user(cnx, u'user').eid
+        with self.new_access(u'user').repo_cnx() as cnx:
             cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
                        {'x': ueid, 'passwd': 'newpwd'})
             cnx.commit()
@@ -299,8 +299,8 @@
 
     def test_user_cant_change_other_upassword(self):
         with self.admin_access.repo_cnx() as cnx:
-            ueid = self.create_user(cnx, 'otheruser').eid
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+            ueid = self.create_user(cnx, u'otheruser').eid
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
                        {'x': ueid, 'passwd': 'newpwd'})
             self.assertRaises(Unauthorized, cnx.commit)
@@ -309,7 +309,7 @@
 
     def test_read_base(self):
         with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
-            with self.new_access('anon').repo_cnx() as cnx:
+            with self.new_access(u'anon').repo_cnx() as cnx:
                 self.assertRaises(Unauthorized,
                                   cnx.execute, 'Personne U where U nom "managers"')
 
@@ -317,7 +317,7 @@
         with self.admin_access.repo_cnx() as cnx:
             eid = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             rset = cnx.execute('Affaire X')
             self.assertEqual(rset.rows, [])
             self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
@@ -342,7 +342,7 @@
     def test_entity_created_in_transaction(self):
         affschema = self.schema['Affaire']
         with self.temporary_permissions(Affaire={'read': affschema.permissions['add']}):
-            with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
                 aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
                 # entity created in transaction are readable *by eid*
                 self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
@@ -358,7 +358,7 @@
             cnx.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"',
                         {'x': card1})
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
             soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
             cnx.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
@@ -376,7 +376,7 @@
             cnx.execute("INSERT Societe X: X nom 'bidule'")
             cnx.commit()
         with self.temporary_permissions(Personne={'read': ('managers',)}):
-            with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
                 rset = cnx.execute('Any N WHERE N has_text "bidule"')
                 self.assertEqual(len(rset.rows), 1, rset.rows)
                 rset = cnx.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
@@ -388,7 +388,7 @@
             cnx.execute("INSERT Societe X: X nom 'bidule'")
             cnx.commit()
         with self.temporary_permissions(Personne={'read': ('managers',)}):
-            with self.new_access('anon').repo_cnx() as cnx:
+            with self.new_access(u'anon').repo_cnx() as cnx:
                 rset = cnx.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
                 self.assertEqual(len(rset.rows), 1, rset.rows)
 
@@ -396,7 +396,7 @@
         with self.admin_access.repo_cnx() as cnx:
             cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             rset = cnx.execute('Any COUNT(X) WHERE X is Affaire')
             self.assertEqual(rset.rows, [[0]])
             aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
@@ -424,7 +424,7 @@
                                "X web 'http://www.debian.org', X test TRUE")[0][0]
             cnx.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute("INSERT Personne X: X nom 'bidule', "
                        "X web 'http://www.debian.org', X test TRUE")
             self.assertRaises(Unauthorized, cnx.commit)
@@ -440,7 +440,7 @@
             self.assertRaises(Unauthorized, cnx.commit)
             cnx.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute('INSERT Frozable F: F name "Foo"')
             cnx.commit()
             cnx.execute('SET F name "Bar" WHERE F is Frozable')
@@ -464,7 +464,7 @@
             note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
             cnx.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
             self.assertRaises(Unauthorized, cnx.commit)
             note2 = cnx.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
@@ -496,10 +496,11 @@
         login_rdef = self.repo.schema['CWUser'].rdef('login')
         with self.temporary_permissions((login_rdef, {'read': ('users', 'managers')}),
                                         CWUser={'read': ('guests', 'users', 'managers')}):
-            with self.new_access('anon').repo_cnx() as cnx:
+            with self.new_access(u'anon').repo_cnx() as cnx:
                 rset = cnx.execute('CWUser X')
                 self.assertTrue(rset)
                 x = rset.get_entity(0, 0)
+                x.complete()
                 self.assertEqual(x.login, None)
                 self.assertTrue(x.creation_date)
                 x = rset.get_entity(1, 0)
@@ -510,7 +511,7 @@
     def test_yams_inheritance_and_security_bug(self):
         with self.temporary_permissions(Division={'read': ('managers',
                                                            ERQLExpression('X owned_by U'))}):
-            with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
                 querier = cnx.repo.querier
                 rqlst = querier.parse('Any X WHERE X is_instance_of Societe')
                 querier.solutions(cnx, rqlst, {})
@@ -519,7 +520,7 @@
                 plan.preprocess(rqlst)
                 self.assertEqual(
                     rqlst.as_string(),
-                    '(Any X WHERE X is IN(SubDivision, Societe)) UNION '
+                    '(Any X WHERE X is IN(Societe, SubDivision)) UNION '
                     '(Any X WHERE X is Division, EXISTS(X owned_by %(B)s))')
 
 
@@ -528,7 +529,7 @@
 
     def test_user_can_delete_object_he_created(self):
         # even if some other user have changed object'state
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             # due to security test, affaire has to concerne a societe the user owns
             cnx.execute('INSERT Societe X: X nom "ARCTIA"')
             cnx.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
@@ -542,7 +543,7 @@
             self.assertEqual(len(cnx.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
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             cnx.execute('DELETE Affaire X WHERE X ref "ARCT01"')
             cnx.commit()
             self.assertFalse(cnx.execute('Affaire X'))
@@ -550,7 +551,7 @@
     def test_users_and_groups_non_readable_by_guests(self):
         with self.repo.internal_cnx() as cnx:
             admineid = cnx.execute('CWUser U WHERE U login "admin"').rows[0][0]
-        with self.new_access('anon').repo_cnx() as cnx:
+        with self.new_access(u'anon').repo_cnx() as cnx:
             anon = cnx.user
             # anonymous user can only read itself
             rset = cnx.execute('Any L WHERE X owned_by U, U login L')
@@ -569,7 +570,7 @@
             self.assertRaises(Unauthorized, cnx.commit)
 
     def test_in_group_relation(self):
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             rql = u"DELETE U in_group G WHERE U login 'admin'"
             self.assertRaises(Unauthorized, cnx.execute, rql)
             rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
@@ -579,7 +580,7 @@
         with self.admin_access.repo_cnx() as cnx:
             cnx.execute("INSERT Personne X: X nom 'bidule'")
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
             self.assertRaises(Unauthorized, cnx.execute, rql)
 
@@ -589,7 +590,7 @@
             beid2 = cnx.execute('INSERT Bookmark B: B path "?vid=index", B title "index", '
                                 'B bookmarked_by U WHERE U login "anon"')[0][0]
             cnx.commit()
-        with self.new_access('anon').repo_cnx() as cnx:
+        with self.new_access(u'anon').repo_cnx() as cnx:
             anoneid = cnx.user.eid
             self.assertEqual(cnx.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,
@@ -606,7 +607,7 @@
                               {'x': anoneid, 'b': beid1})
 
     def test_ambigous_ordered(self):
-        with self.new_access('anon').repo_cnx() as cnx:
+        with self.new_access(u'anon').repo_cnx() as cnx:
             names = [t for t, in cnx.execute('Any N ORDERBY lower(N) WHERE X name N')]
             self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
 
@@ -617,7 +618,7 @@
         with self.admin_access.repo_cnx() as cnx:
             eid = cnx.execute('INSERT Affaire X: X ref "ARCT01"')[0][0]
             cnx.commit()
-        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
             # needed to remove rql expr granting update perm to the user
             affschema = self.schema['Affaire']
             with self.temporary_permissions(Affaire={'update': affschema.get_groups('update'),
@@ -675,7 +676,7 @@
                          'U use_email X WHERE U login "anon"').get_entity(0, 0)
             cnx.commit()
             self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 2)
-        with self.new_access('anon').repo_cnx() as cnx:
+        with self.new_access(u'anon').repo_cnx() as cnx:
             self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 1)
 
 if __name__ == '__main__':
--- a/server/test/unittest_session.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-
-from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import HOOKS_ALLOW_ALL, HOOKS_DENY_ALL
-from cubicweb.server import hook
-from cubicweb.predicates import is_instance
-
-class InternalSessionTC(CubicWebTC):
-    def test_dbapi_query(self):
-        session = self.repo.internal_session()
-        self.assertFalse(session.running_dbapi_query)
-        session.close()
-
-    def test_integrity_hooks(self):
-        with self.repo.internal_session() as session:
-            self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
-            self.assertEqual(set(('integrity', 'security')), session.disabled_hook_categories)
-            self.assertEqual(set(), session.enabled_hook_categories)
-            session.commit()
-            self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
-            self.assertEqual(set(('integrity', 'security')), session.disabled_hook_categories)
-            self.assertEqual(set(), session.enabled_hook_categories)
-
-class SessionTC(CubicWebTC):
-
-    def test_hooks_control(self):
-        session = self.session
-        # this test check the "old" behavior of session with automatic connection management
-        # close the default cnx, we do nto want it to interfer with the test
-        self.cnx.close()
-        # open a dedicated one
-        session.set_cnx('Some-random-cnx-unrelated-to-the-default-one')
-        # go test go
-        self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
-        self.assertEqual(set(), session.disabled_hook_categories)
-        self.assertEqual(set(), session.enabled_hook_categories)
-        self.assertEqual(1, len(session._cnxs))
-        with session.deny_all_hooks_but('metadata'):
-            self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
-            self.assertEqual(set(), session.disabled_hook_categories)
-            self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
-            session.commit()
-            self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
-            self.assertEqual(set(), session.disabled_hook_categories)
-            self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
-            session.rollback()
-            self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
-            self.assertEqual(set(), session.disabled_hook_categories)
-            self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
-            with session.allow_all_hooks_but('integrity'):
-                self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
-                self.assertEqual(set(('integrity',)), session.disabled_hook_categories)
-                self.assertEqual(set(('metadata',)), session.enabled_hook_categories) # not changed in such case
-            self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
-            self.assertEqual(set(), session.disabled_hook_categories)
-            self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
-        # leaving context manager with no transaction running should reset the
-        # transaction local storage (and associated cnxset)
-        self.assertEqual({}, session._cnxs)
-        self.assertEqual(None, session.cnxset)
-        self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode, session.HOOKS_ALLOW_ALL)
-        self.assertEqual(set(), session.disabled_hook_categories)
-        self.assertEqual(set(), session.enabled_hook_categories)
-
-    def test_explicit_connection(self):
-        with self.session.new_cnx() as cnx:
-            rset = cnx.execute('Any X LIMIT 1 WHERE X is CWUser')
-            self.assertEqual(1, len(rset))
-            user = rset.get_entity(0, 0)
-            user.cw_delete()
-            cnx.rollback()
-            new_user = cnx.entity_from_eid(user.eid)
-            self.assertIsNotNone(new_user.login)
-        self.assertFalse(cnx._open)
-
-    def test_internal_cnx(self):
-        with self.repo.internal_cnx() as cnx:
-            rset = cnx.execute('Any X LIMIT 1 WHERE X is CWUser')
-            self.assertEqual(1, len(rset))
-            user = rset.get_entity(0, 0)
-            user.cw_delete()
-            cnx.rollback()
-            new_user = cnx.entity_from_eid(user.eid)
-            self.assertIsNotNone(new_user.login)
-        self.assertFalse(cnx._open)
-
-    def test_connection_exit(self):
-        """exiting a connection should roll back the transaction, including any
-        pending operations"""
-        self.rollbacked = False
-        class RollbackOp(hook.Operation):
-            _test = self
-            def rollback_event(self):
-                self._test.rollbacked = True
-        class RollbackHook(hook.Hook):
-            __regid__ = 'rollback'
-            events = ('after_update_entity',)
-            __select__ = hook.Hook.__select__ & is_instance('CWGroup')
-            def __call__(self):
-                RollbackOp(self._cw)
-        with self.temporary_appobjects(RollbackHook):
-            with self.admin_access.client_cnx() as cnx:
-                cnx.execute('SET G name "foo" WHERE G is CWGroup, G name "managers"')
-            self.assertTrue(self.rollbacked)
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/server/test/unittest_storage.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_storage.py	Fri Oct 09 17:52:14 2015 +0200
@@ -87,27 +87,31 @@
                              'managed attribute. Is FSPATH() argument BFSS managed?')
 
     def test_bfss_storage(self):
-        with self.admin_access.repo_cnx() as cnx:
-            f1 = self.create_file(cnx)
+        with self.admin_access.web_request() as req:
+            cnx = req.cnx
+            f1 = self.create_file(req)
             filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
             self.assertEqual(len(filepaths), 1, filepaths)
             expected_filepath = filepaths[0]
             # file should be read only
             self.assertFalse(os.access(expected_filepath, os.W_OK))
-            self.assertEqual(file(expected_filepath).read(), 'the-data')
+            self.assertEqual(open(expected_filepath).read(), 'the-data')
             cnx.rollback()
             self.assertFalse(osp.isfile(expected_filepath))
             filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
             self.assertEqual(len(filepaths), 0, filepaths)
-            f1 = self.create_file(cnx)
+            f1 = self.create_file(req)
             cnx.commit()
             filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
             self.assertEqual(len(filepaths), 1, filepaths)
             expected_filepath = filepaths[0]
-            self.assertEqual(file(expected_filepath).read(), 'the-data')
+            self.assertEqual(open(expected_filepath).read(), 'the-data')
+
+            # add f1 back to the entity cache with req as _cw
+            f1 = req.entity_from_eid(f1.eid)
             f1.cw_set(data=Binary('the new data'))
             cnx.rollback()
-            self.assertEqual(file(expected_filepath).read(), 'the-data')
+            self.assertEqual(open(expected_filepath).read(), 'the-data')
             f1.cw_delete()
             self.assertTrue(osp.isfile(expected_filepath))
             cnx.rollback()
--- a/server/test/unittest_tools.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_tools.py	Fri Oct 09 17:52:14 2015 +0200
@@ -23,7 +23,6 @@
 class ImportTC(TestCase):
     def test(self):
         # the minimal test: module is importable...
-        import cubicweb.server.server
         import cubicweb.server.checkintegrity
         import cubicweb.server.serverctl
 
--- a/server/test/unittest_undo.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/server/test/unittest_undo.py	Fri Oct 09 17:52:14 2015 +0200
@@ -48,7 +48,6 @@
 
     def tearDown(self):
         cubicweb.server.session.Connection = OldConnection
-        self.restore_connection()
         super(UndoableTransactionTC, self).tearDown()
 
     def check_transaction_deleted(self, cnx, txuuid):
@@ -210,13 +209,12 @@
                               ['CWUser'])
             # undoing shouldn't be visble in undoable transaction, and the undone
             # transaction should be removed
-            txs = self.cnx.undoable_transactions()
+            txs = cnx.undoable_transactions()
             self.assertEqual(len(txs), 2)
             self.assertRaises(NoSuchTransaction,
-                              self.cnx.transaction_info, txuuid)
+                              cnx.transaction_info, txuuid)
         with self.admin_access.repo_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                self.check_transaction_deleted(cnx, txuuid)
+            self.check_transaction_deleted(cnx, txuuid)
             # the final test: check we can login with the previously deleted user
         with self.new_access('toto').client_cnx():
             pass
@@ -238,6 +236,8 @@
             cnx.commit()
             p.cw_clear_all_caches()
             self.assertEqual(p.fiche[0].eid, c2.eid)
+            # we restored the card
+            self.assertTrue(cnx.entity_from_eid(c.eid))
 
     def test_undo_deletion_integrity_2(self):
         with self.admin_access.client_cnx() as cnx:
@@ -272,18 +272,17 @@
             self.assertFalse(cnx.execute('Any X WHERE X eid %(x)s', {'x': p.eid}))
             self.assertFalse(cnx.execute('Any X,Y WHERE X fiche Y'))
         with self.admin_access.repo_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                for eid in (p.eid, c.eid):
-                    self.assertFalse(cnx.system_sql(
-                        'SELECT * FROM entities WHERE eid=%s' % eid).fetchall())
-                    self.assertFalse(cnx.system_sql(
-                        'SELECT 1 FROM owned_by_relation WHERE eid_from=%s' % eid).fetchall())
-                    # added by sql in hooks (except when using dataimport)
-                    self.assertFalse(cnx.system_sql(
-                        'SELECT 1 FROM is_relation WHERE eid_from=%s' % eid).fetchall())
-                    self.assertFalse(cnx.system_sql(
-                        'SELECT 1 FROM is_instance_of_relation WHERE eid_from=%s' % eid).fetchall())
-                self.check_transaction_deleted(cnx, txuuid)
+            for eid in (p.eid, c.eid):
+                self.assertFalse(cnx.system_sql(
+                    'SELECT * FROM entities WHERE eid=%s' % eid).fetchall())
+                self.assertFalse(cnx.system_sql(
+                    'SELECT 1 FROM owned_by_relation WHERE eid_from=%s' % eid).fetchall())
+                # added by sql in hooks (except when using dataimport)
+                self.assertFalse(cnx.system_sql(
+                    'SELECT 1 FROM is_relation WHERE eid_from=%s' % eid).fetchall())
+                self.assertFalse(cnx.system_sql(
+                    'SELECT 1 FROM is_instance_of_relation WHERE eid_from=%s' % eid).fetchall())
+            self.check_transaction_deleted(cnx, txuuid)
 
     def test_undo_creation_integrity_1(self):
         with self.admin_access.client_cnx() as cnx:
@@ -356,9 +355,8 @@
             p.cw_clear_all_caches()
             self.assertFalse(p.fiche)
         with self.admin_access.repo_cnx() as cnx:
-            with cnx.ensure_cnx_set:
-                self.assertIsNone(cnx.system_sql(
-                    'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0])
+            self.assertIsNone(cnx.system_sql(
+                'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0])
 
     def test_undo_inline_rel_add_ok(self):
         """Undo add relation  Personne (?) fiche (?) Card
@@ -375,6 +373,17 @@
             p.cw_clear_all_caches()
             self.assertFalse(p.fiche)
 
+    def test_undo_inline_rel_delete_ko(self):
+        with self.admin_access.client_cnx() as cnx:
+            c = cnx.create_entity('Card', title=u'hop', content=u'hop')
+            txuuid = cnx.commit()
+            p = cnx.create_entity('Personne', nom=u'louis', fiche=c)
+            cnx.commit()
+            integrityerror = self.repo.sources_by_uri['system'].dbhelper.dbapi_module.IntegrityError
+            with self.assertRaises(integrityerror):
+                cnx.undo_transaction(txuuid)
+
+
     def test_undo_inline_rel_add_ko(self):
         """Undo add relation  Personne (?) fiche (?) Card
 
--- a/skeleton/__pkginfo__.py.tmpl	Fri Oct 09 09:40:08 2015 +0200
+++ b/skeleton/__pkginfo__.py.tmpl	Fri Oct 09 17:52:14 2015 +0200
@@ -13,7 +13,7 @@
 description = '%(shortdesc)s'
 web = 'http://www.cubicweb.org/project/%%s' %% distname
 
-__depends__ =  %(dependencies)s
+__depends__ = %(dependencies)s
 __recommends__ = {}
 
 classifiers = [
@@ -29,6 +29,7 @@
 
 THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
 
+
 def listdir(dirpath):
     return [join(dirpath, fname) for fname in _listdir(dirpath)
             if fname[0] != '.' and not fname.endswith('.pyc')
@@ -40,9 +41,9 @@
     [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
     ]
 # check for possible extended cube layout
-for dname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'wdoc', 'i18n', 'migration'):
+for dname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data',
+              'wdoc', 'i18n', 'migration'):
     if isdir(dname):
         data_files.append([join(THIS_CUBE_DIR, dname), listdir(dname)])
 # Note: here, you'll need to add subdirectories if you want
 # them to be included in the debian package
-
--- a/skeleton/migration/postcreate.py.tmpl	Fri Oct 09 09:40:08 2015 +0200
+++ b/skeleton/migration/postcreate.py.tmpl	Fri Oct 09 17:52:14 2015 +0200
@@ -11,4 +11,3 @@
 
 # Example of site property change
 #set_property('ui.site-title', "<sitename>")
-
--- a/skeleton/setup.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/skeleton/setup.py	Fri Oct 09 17:52:14 2015 +0200
@@ -16,8 +16,8 @@
 # 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/>.
+# 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 Setup script, takes package info from __pkginfo__.py file
 """
 __docformat__ = "restructuredtext en"
@@ -25,11 +25,11 @@
 import os
 import sys
 import shutil
-from os.path import isdir, exists, join, walk
+from os.path import exists, join, walk
 
 try:
     if os.environ.get('NO_SETUPTOOLS'):
-        raise ImportError() # do as there is no setuptools
+        raise ImportError()  # do as there is no setuptools
     from setuptools import setup
     from setuptools.command import install_lib
     USE_SETUPTOOLS = True
@@ -41,7 +41,7 @@
 
 # import required features
 from __pkginfo__ import modname, version, license, description, web, \
-     author, author_email, classifiers
+    author, author_email, classifiers
 
 if exists('README'):
     long_description = file('README').read()
@@ -52,10 +52,10 @@
 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()]
+                        for d, v in requires.iteritems()]
 else:
     install_requires = []
 
@@ -82,6 +82,7 @@
         scripts_ = linux_scripts
     return scripts_
 
+
 def export(from_dir, to_dir,
            blacklist=BASE_BLACKLIST,
            ignore_ext=IGNORED_EXTENSIONS,
@@ -150,13 +151,15 @@
             old_install_data.run(self)
             self.install_dir = _old_install_dir
     try:
-        import setuptools.command.easy_install # only if easy_install available
+        # only if easy_install available
+        import setuptools.command.easy_install  # noqa
         # 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) # here for side effect from setuptools
+            out = old_ok(self, path)  # here for side effect from setuptools
             realpath = os.path.normcase(os.path.realpath(path))
             allowed_path = os.path.normcase(sys.prefix)
             if realpath.startswith(allowed_path):
@@ -166,6 +169,7 @@
     except ImportError:
         pass
 
+
 def install(**kwargs):
     """setup entry point"""
     if USE_SETUPTOOLS:
@@ -181,21 +185,22 @@
         kwargs['zip_safe'] = False
         cmdclass['install_data'] = MyInstallData
 
-    return setup(name = distname,
-                 version = version,
-                 license = license,
-                 description = description,
-                 long_description = long_description,
-                 author = author,
-                 author_email = author_email,
-                 url = web,
-                 scripts = ensure_scripts(scripts),
-                 data_files = data_files,
-                 ext_modules = ext_modules,
-                 cmdclass = cmdclass,
-                 classifiers = classifiers,
+    return setup(name=distname,
+                 version=version,
+                 license=license,
+                 description=description,
+                 long_description=long_description,
+                 author=author,
+                 author_email=author_email,
+                 url=web,
+                 scripts=ensure_scripts(scripts),
+                 data_files=data_files,
+                 ext_modules=ext_modules,
+                 cmdclass=cmdclass,
+                 classifiers=classifiers,
                  **kwargs
                  )
 
-if __name__ == '__main__' :
+
+if __name__ == '__main__':
     install()
--- a/skeleton/test/pytestconf.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/skeleton/test/pytestconf.py	Fri Oct 09 17:52:14 2015 +0200
@@ -13,8 +13,8 @@
 # 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/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """
 
 """
@@ -23,6 +23,7 @@
 
 from logilab.common.pytest import PyTester
 
+
 def getlogin():
     """avoid usinng os.getlogin() because of strange tty / stdin problems
     (man 3 getlogin)
--- a/skeleton/test/realdb_test_CUBENAME.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/skeleton/test/realdb_test_CUBENAME.py	Fri Oct 09 17:52:14 2015 +0200
@@ -13,14 +13,15 @@
 # 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/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """
 
 """
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.realdbtest import buildconfig, loadconfig
 
+
 def setUpModule(options):
     if options.source:
         configcls = loadconfig(options.source)
@@ -28,13 +29,13 @@
         raise Exception('either <sourcefile> or <dbname> options are required')
     else:
         configcls = buildconfig(options.dbuser, options.dbpassword,
-                                               options.dbname, options.euser,
-                                               options.epassword)
+                                options.dbname,
+                                options.euser, options.epassword)
     RealDatabaseTC.configcls = configcls
 
 
 class RealDatabaseTC(CubicWebTC):
-    configcls = None # set by setUpModule()
+    configcls = None  # set by setUpModule()
 
     def test_all_primaries(self):
         for rset in self.iter_individual_rsets(limit=50):
--- a/skeleton/test/test_CUBENAME.py.tmpl	Fri Oct 09 09:40:08 2015 +0200
+++ b/skeleton/test/test_CUBENAME.py.tmpl	Fri Oct 09 17:52:14 2015 +0200
@@ -27,6 +27,7 @@
 
 from cubicweb.devtools import testlib
 
+
 class DefaultTC(testlib.CubicWebTC):
     def test_something(self):
         self.skipTest('this cube has no test')
--- a/sobjects/cwxmlparser.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/sobjects/cwxmlparser.py	Fri Oct 09 17:52:14 2015 +0200
@@ -32,7 +32,8 @@
 """
 
 from datetime import datetime, time
-from cgi import parse_qs # in urlparse with python >= 2.6
+import urlparse
+import urllib
 
 from logilab.common.date import todate, totime
 from logilab.common.textutils import splitstrip, text_to_dict
@@ -238,20 +239,17 @@
         attrs = extract_typed_attrs(entity.e_schema, sourceparams['item'])
         entity.cw_edited.update(attrs)
 
-
     def normalize_url(self, url):
-        """overriden to add vid=xml"""
+        """overridden to add vid=xml if vid is not set in the qs"""
         url = super(CWEntityXMLParser, self).normalize_url(url)
-        if url.startswith('http'):
-            try:
-                url, qs = url.split('?', 1)
-            except ValueError:
-                params = {}
-            else:
-                params = parse_qs(qs)
-            if not 'vid' in params:
+        purl = urlparse.urlparse(url)
+        if purl.scheme in ('http', 'https'):
+            params = urlparse.parse_qs(purl.query)
+            if 'vid' not in params:
                 params['vid'] = ['xml']
-            return url + '?' + self._cw.build_url_params(**params)
+                purl = list(purl)
+                purl[4] = urllib.urlencode(params, doseq=True)
+                return urlparse.urlunparse(purl)
         return url
 
     def complete_url(self, url, etype=None, known_relations=None):
@@ -265,29 +263,22 @@
         If `known_relations` is given, it should be a dictionary of already
         known relations, so they don't get queried again.
         """
-        try:
-            url, qs = url.split('?', 1)
-        except ValueError:
-            qs = ''
-        # XXX vid will be added by later call to normalize_url (in parent class)
-        params = parse_qs(qs)
+        purl = urlparse.urlparse(url)
+        params = urlparse.parse_qs(purl.query)
         if etype is None:
-            try:
-                etype = url.rsplit('/', 1)[1]
-            except ValueError:
-                return url + '?' + self._cw.build_url_params(**params)
-            try:
-                etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
-            except KeyError:
-                return url + '?' + self._cw.build_url_params(**params)
-        relations = params.setdefault('relation', [])
+            etype = purl.path.split('/')[-1]
+        try:
+            etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
+        except KeyError:
+            return url
+        relations = params['relation'] = set(params.get('relation', ()))
         for rtype, role, _ in self.source.mapping.get(etype, ()):
             if known_relations and rtype in known_relations.get('role', ()):
                 continue
-            reldef = '%s-%s' % (rtype, role)
-            if not reldef in relations:
-                relations.append(reldef)
-        return url + '?' + self._cw.build_url_params(**params)
+            relations.add('%s-%s' % (rtype, role))
+        purl = list(purl)
+        purl[4] = urllib.urlencode(params, doseq=True)
+        return urlparse.urlunparse(purl)
 
     def complete_item(self, item, rels):
         try:
--- a/sobjects/notification.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/sobjects/notification.py	Fri Oct 09 17:52:14 2015 +0200
@@ -270,7 +270,7 @@
     """
     __abstract__ = True
     __regid__ = 'notif_entity_updated'
-    msgid_timestamp = False
+    msgid_timestamp = True
     message = _('updated')
     no_detailed_change_attrs = ()
     content = """
--- a/sobjects/services.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/sobjects/services.py	Fri Oct 09 17:52:14 2015 +0200
@@ -43,7 +43,7 @@
             (len(source._cache), repo.config['rql-cache-size'],
             source.cache_hit, source.cache_miss, 'sql'),
             ):
-            results['%s_cache_size' % title] = '%s / %s' % (size, maxsize)
+            results['%s_cache_size' % title] = {'size': size, 'maxsize': maxsize}
             results['%s_cache_hit' % title] = hits
             results['%s_cache_miss' % title] = misses
             results['%s_cache_hit_percent' % title] = (hits * 100) / (hits + misses)
@@ -53,9 +53,9 @@
         results['nb_open_sessions'] = len(repo._sessions)
         results['nb_active_threads'] = threading.activeCount()
         looping_tasks = repo._tasks_manager._looping_tasks
-        results['looping_tasks'] = ', '.join(str(t) for t in looping_tasks)
+        results['looping_tasks'] = [(t.name, t.interval) for t in looping_tasks]
         results['available_cnxsets'] = repo._cnxsets_pool.qsize()
-        results['threads'] = ', '.join(sorted(str(t) for t in threading.enumerate()))
+        results['threads'] = [t.name for t in threading.enumerate()]
         return results
 
 class GcStatsService(Service):
@@ -79,13 +79,11 @@
         from cubicweb._gcdebug import gc_info
         from cubicweb.appobject import AppObject
         from cubicweb.rset import ResultSet
-        from cubicweb.dbapi import Connection, Cursor
         from cubicweb.web.request import CubicWebRequestBase
         from rql.stmts import Union
 
         lookupclasses = (AppObject,
                          Union, ResultSet,
-                         Connection, Cursor,
                          CubicWebRequestBase)
         try:
             from cubicweb.server.session import Session, InternalSession
@@ -100,7 +98,7 @@
         results['lookupclasses'] = values
         values = sorted(ocounters.iteritems(), key=lambda x: x[1], reverse=True)[:nmax]
         results['referenced'] = values
-        results['unreachable'] = len(garbage)
+        results['unreachable'] = garbage
         return results
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,2 @@
+cubicweb-card
+cubicweb-comment
--- a/sobjects/test/unittest_cwxmlparser.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/sobjects/test/unittest_cwxmlparser.py	Fri Oct 09 17:52:14 2015 +0200
@@ -17,6 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
 from datetime import datetime
+from urlparse import urlsplit, parse_qsl
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.sobjects.cwxmlparser import CWEntityXMLParser
@@ -133,6 +134,16 @@
     """
     test_db_id = 'xmlparser'
 
+    def assertURLEquiv(self, first, second):
+        # ignore ordering differences in query params
+        parsed_first = urlsplit(first)
+        parsed_second = urlsplit(second)
+        self.assertEqual(parsed_first.scheme, parsed_second.scheme)
+        self.assertEqual(parsed_first.netloc, parsed_second.netloc)
+        self.assertEqual(parsed_first.path, parsed_second.path)
+        self.assertEqual(parsed_first.fragment, parsed_second.fragment)
+        self.assertCountEqual(parse_qsl(parsed_first.query), parse_qsl(parsed_second.query))
+
     @classmethod
     def pre_setup_database(cls, cnx, config):
         myfeed = cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
@@ -161,16 +172,16 @@
         dfsource = self.repo.sources_by_uri['myfeed']
         with self.admin_access.repo_cnx() as cnx:
             parser = dfsource._get_parser(cnx)
-            self.assertEqual(parser.complete_url('http://www.cubicweb.org/CWUser'),
-                             'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
-            self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser'),
-                             'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
-            self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
-                             'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
-            self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
-                             'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
-            self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
-                             'http://www.cubicweb.org/?rql=cwuser&relation=hop')
+            self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/CWUser'),
+                                'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+            self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/cwuser'),
+                                'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+            self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
+                                'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
+            self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
+                                'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
+            self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
+                                'http://www.cubicweb.org/?rql=cwuser&relation=hop')
 
 
     def test_actions(self):
@@ -256,7 +267,11 @@
             self.assertEqual(e.cw_source[0].name, 'system')
             self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
             # test everything is still fine after source synchronization
+            # clear caches to make sure we look at the moved_entities table
+            self.repo._type_source_cache.clear()
+            self.repo._extid_cache.clear()
             stats = dfsource.pull_data(cnx, force=True, raise_on_error=True)
+            self.assertEqual(stats['updated'], set((email.eid,)))
             rset = cnx.execute('EmailAddress X WHERE X address "syt@logilab.fr"')
             self.assertEqual(len(rset), 1)
             e = rset.get_entity(0, 0)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/statsd_logger.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,135 @@
+# copyright 2015 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/>.
+
+"""Simple statsd_ logger for cubicweb.
+
+This module is meant to be configured by setting a couple of global variables:
+
+- ``bucket`` global variable will be used as statsd bucket in every
+statsd_ UDP sent packet.
+
+`- `address`` is a pair (IP, port) specifying the address of the
+statsd_ server
+
+
+There are 3 kinds of statds_ message::
+
+- ``statsd_c(context, n)`` is a simple function to send statsd_
+  counter-type of messages like::
+
+    <bucket>.<context>:<n>|c\n
+
+- ``statsd_g(context, value)`` to send statsd_ gauge-type of messages
+  like::
+
+    <bucket>.<context>:<n>|g\n
+
+- ``statsd_t(context, ms)`` to send statsd_ time-type of messages
+  like::
+
+    <bucket>.<context>:<ms>|ms\n
+
+There is also a decorator (``statsd_timeit``) that may be used to
+measure and send to the statsd_ server the time passed in a function
+or a method and the number of calls. It will send a message like::
+   
+    <bucket>.<funcname>:<ms>|ms\n<bucket>.<funcname>:1|c\n
+
+
+.. _statsd: https://github.com/etsy/statsd
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+import time
+import socket
+
+_bucket = 'cubicweb'
+_address = None
+_socket = None
+
+
+def setup(bucket, address):
+    """Configure the statsd endpoint
+
+    :param bucket: the name of the statsd bucket that will be used to
+                   build messages.
+
+    :param address: the UDP endpoint of the statsd server. Must a
+                    couple (ip, port).
+    """
+    global _bucket, _address, _socket
+    packed = None
+    for family in (socket.AF_INET6, socket.AF_INET):
+        try:
+            packed = socket.inet_pton(family, address)
+            break
+        except socket.error:
+            continue
+    if packed is None:
+        return
+    _bucket, _address = bucket, address
+    _socket = socket.socket(family, socket.SOCK_DGRAM)
+
+
+def statsd_c(context, n=1):
+    if _address is not None:
+        _socket.sendto('{0}.{1}:{2}|c\n'.format(_bucket, context, n), _address)
+
+
+def statsd_g(context, value):
+    if _address is not None:
+        _socket.sendto('{0}.{1}:{2}|g\n'.format(_bucket, context, value), _address)
+
+
+def statsd_t(context, value):
+    if _address is not None:
+        _socket.sendto('{0}.{1}:{2:.4f}|ms\n'.format(_bucket, context, value), _address)
+
+
+class statsd_timeit(object):
+    __slots__ = ('callable',)
+
+    def __init__(self, callableobj):
+        self.callable = callableobj
+
+    @property
+    def __doc__(self):
+        return self.callable.__doc__
+    @property
+    def __name__(self):
+        return self.callable.__name__
+    
+    def __call__(self, *args, **kw):
+        if _address is None:
+            return self.callable(*args, **kw)
+        t0 = time.time()
+        try:
+            return self.callable(*args, **kw)
+        finally:
+            dt = 1000*(time.time()-t0)
+            msg = '{0}.{1}:{2:.4f}|ms\n{0}.{1}:1|c\n'.format(_bucket, self.__name__, dt)
+            _socket.sendto(msg, _address)
+                
+    def __get__(self, obj, objtype):
+        """Support instance methods."""
+        if obj is None: # class method or some already wrapped method
+            return self
+        import functools
+        return functools.partial(self.__call__, obj)
--- a/test/data/bootstrap_cubes	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/data/bootstrap_cubes	Fri Oct 09 17:52:14 2015 +0200
@@ -1,1 +1,1 @@
-card, file, tag, localperms
+card, tag, localperms
--- a/test/data/entities.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/data/entities.py	Fri Oct 09 17:52:14 2015 +0200
@@ -16,7 +16,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.entities import AnyEntity, fetch_config
+from cubicweb.entities import AnyEntity, fetch_config, adapters
+from cubicweb.predicates import is_instance
+
 
 class Societe(AnyEntity):
     __regid__ = 'Societe'
@@ -34,3 +36,7 @@
 
 class Note(AnyEntity):
     __regid__ = 'Note'
+
+
+class FakeFileIDownloadableAdapter(adapters.IDownloadableAdapter):
+    __select__ = is_instance('FakeFile')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rqlexpr_on_computedrel.py	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,18 @@
+from yams.buildobjs import ComputedRelation, EntityType, RelationDefinition
+from cubicweb.schema import RRQLExpression
+
+class Subject(EntityType):
+    pass
+
+class Object(EntityType):
+    pass
+
+class relation(RelationDefinition):
+    subject = 'Subject'
+    object = 'Object'
+
+class computed(ComputedRelation):
+    rule = 'S relation O'
+    __permissions__ = {'read': (RRQLExpression('S is ET'),)}
+
+
--- a/test/data/schema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/data/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -16,13 +16,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/>.
 
-from yams.buildobjs import (EntityType, String, SubjectRelation,
-                            RelationDefinition)
+from yams.buildobjs import (EntityType, String, RichString, Bytes,
+                            SubjectRelation, RelationDefinition)
 
 from cubicweb.schema import (WorkflowableEntityType,
                              RQLConstraint, RQLVocabularyConstraint)
 
 
+_ = unicode
+
+
 class Personne(EntityType):
     nom = String(required=True)
     prenom = String()
@@ -94,3 +97,17 @@
 class Reference(EntityType):
     nom = String(unique=True)
     ean = String(unique=True, required=True)
+
+
+class FakeFile(EntityType):
+    title = String(fulltextindexed=True, maxsize=256)
+    data = Bytes(required=True, fulltextindexed=True, description=_('file to upload'))
+    data_format = String(required=True, maxsize=128,
+                         description=_('MIME type of the file. Should be dynamically set at upload time.'))
+    data_encoding = String(maxsize=32,
+                           description=_('encoding of the file when it applies (e.g. text). '
+                                         'Should be dynamically set at upload time.'))
+    data_name = String(required=True, fulltextindexed=True,
+                       description=_('name of the file. Should be dynamically set at upload time.'))
+    description = RichString(fulltextindexed=True, internationalizable=True,
+                             default_format='text/rest')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,6 @@
+Pygments
+#fyzz XXX pip install fails
+cubicweb-card
+cubicweb-file
+cubicweb-localperms
+cubicweb-tag
--- a/test/unittest_cwconfig.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/unittest_cwconfig.py	Fri Oct 09 17:52:14 2015 +0200
@@ -104,11 +104,14 @@
     def test_appobjects_path(self):
         self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
         self.config.adjust_sys_path()
-        self.assertEqual([unabsolutize(p) for p in self.config.appobjects_path()],
-                          ['entities', 'web/views', 'sobjects', 'hooks',
-                           'file/entities', 'file/views.py', 'file/hooks',
-                           'email/entities.py', 'email/views', 'email/hooks.py',
-                           'test/data/entities.py', 'test/data/views.py'])
+        path = [unabsolutize(p) for p in self.config.appobjects_path()]
+        self.assertEqual(path[0], 'entities')
+        self.assertCountEqual(path[1:4], ['web/views', 'sobjects', 'hooks'])
+        self.assertEqual(path[4], 'file/entities')
+        self.assertCountEqual(path[5:7], ['file/views.py', 'file/hooks'])
+        self.assertEqual(path[7], 'email/entities.py')
+        self.assertCountEqual(path[8:10], ['email/views', 'email/hooks.py'])
+        self.assertEqual(path[10:], ['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
--- a/test/unittest_cwctl.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/unittest_cwctl.py	Fri Oct 09 17:52:14 2015 +0200
@@ -44,7 +44,7 @@
 
     def test_process_script_args_context(self):
         repo = self.repo
-        with self.admin_access.client_cnx() as cnx:
+        with self.admin_access.repo_cnx() as cnx:
             mih = ServerMigrationHelper(None, repo=repo, cnx=cnx,
                                         interactive=False,
                                         # hack so it don't try to load fs schema
--- a/test/unittest_dataimport.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import datetime as DT
-from StringIO import StringIO
-
-from logilab.common.testlib import TestCase, unittest_main
-
-from cubicweb import dataimport
-from cubicweb.devtools.testlib import CubicWebTC
-
-
-class RQLObjectStoreTC(CubicWebTC):
-
-    def test_all(self):
-        with self.admin_access.repo_cnx() as cnx:
-            store = dataimport.RQLObjectStore(cnx)
-            group_eid = store.create_entity('CWGroup', name=u'grp').eid
-            user_eid = store.create_entity('CWUser', login=u'lgn', upassword=u'pwd').eid
-            store.relate(user_eid, 'in_group', group_eid)
-            cnx.commit()
-
-        with self.admin_access.repo_cnx() as cnx:
-            users = cnx.execute('CWUser X WHERE X login "lgn"')
-            self.assertEqual(1, len(users))
-            self.assertEqual(user_eid, users.one().eid)
-            groups = cnx.execute('CWGroup X WHERE U in_group X, U login "lgn"')
-            self.assertEqual(1, len(users))
-            self.assertEqual(group_eid, groups.one().eid)
-
-
-class CreateCopyFromBufferTC(TestCase):
-
-    # test converters
-
-    def test_convert_none(self):
-        cnvt = dataimport._copyfrom_buffer_convert_None
-        self.assertEqual('NULL', cnvt(None))
-
-    def test_convert_number(self):
-        cnvt = dataimport._copyfrom_buffer_convert_number
-        self.assertEqual('42', cnvt(42))
-        self.assertEqual('42', cnvt(42L))
-        self.assertEqual('42.42', cnvt(42.42))
-
-    def test_convert_string(self):
-        cnvt = dataimport._copyfrom_buffer_convert_string
-        # simple
-        self.assertEqual('babar', cnvt('babar'))
-        # unicode
-        self.assertEqual('\xc3\xa9l\xc3\xa9phant', cnvt(u'éléphant'))
-        self.assertEqual('\xe9l\xe9phant', cnvt(u'éléphant', encoding='latin1'))
-        self.assertEqual('babar#', cnvt('babar\t', replace_sep='#'))
-        self.assertRaises(ValueError, cnvt, 'babar\t')
-
-    def test_convert_date(self):
-        cnvt = dataimport._copyfrom_buffer_convert_date
-        self.assertEqual('0666-01-13', cnvt(DT.date(666, 1, 13)))
-
-    def test_convert_time(self):
-        cnvt = dataimport._copyfrom_buffer_convert_time
-        self.assertEqual('06:06:06.000100', cnvt(DT.time(6, 6, 6, 100)))
-
-    def test_convert_datetime(self):
-        cnvt = dataimport._copyfrom_buffer_convert_datetime
-        self.assertEqual('0666-06-13 06:06:06.000000', cnvt(DT.datetime(666, 6, 13, 6, 6, 6)))
-
-    # test buffer
-    def test_create_copyfrom_buffer_tuple(self):
-        cnvt = dataimport._create_copyfrom_buffer
-        data = ((42, 42L, 42.42, u'éléphant', DT.date(666, 1, 13), DT.time(6, 6, 6), DT.datetime(666, 6, 13, 6, 6, 6)),
-                (6, 6L, 6.6, u'babar', DT.date(2014, 1, 14), DT.time(4, 2, 1), DT.datetime(2014, 1, 1, 0, 0, 0)))
-        results = dataimport._create_copyfrom_buffer(data)
-        # all columns
-        expected = '''42\t42\t42.42\téléphant\t0666-01-13\t06:06:06.000000\t0666-06-13 06:06:06.000000
-6\t6\t6.6\tbabar\t2014-01-14\t04:02:01.000000\t2014-01-01 00:00:00.000000'''
-        self.assertMultiLineEqual(expected, results.getvalue())
-        # selected columns
-        results = dataimport._create_copyfrom_buffer(data, columns=(1, 3, 6))
-        expected = '''42\téléphant\t0666-06-13 06:06:06.000000
-6\tbabar\t2014-01-01 00:00:00.000000'''
-        self.assertMultiLineEqual(expected, results.getvalue())
-
-    def test_create_copyfrom_buffer_dict(self):
-        cnvt = dataimport._create_copyfrom_buffer
-        data = (dict(integer=42, double=42.42, text=u'éléphant', date=DT.datetime(666, 6, 13, 6, 6, 6)),
-                dict(integer=6, double=6.6, text=u'babar', date=DT.datetime(2014, 1, 1, 0, 0, 0)))
-        results = dataimport._create_copyfrom_buffer(data, ('integer', 'text'))
-        expected = '''42\téléphant\n6\tbabar'''
-        self.assertMultiLineEqual(expected, results.getvalue())
-
-
-class UcsvreaderTC(TestCase):
-
-    def test_empty_lines_skipped(self):
-        stream = StringIO('''a,b,c,d,
-1,2,3,4,
-,,,,
-,,,,
-''')
-        self.assertEqual([[u'a', u'b', u'c', u'd', u''],
-                          [u'1', u'2', u'3', u'4', u''],
-                          ],
-                         list(dataimport.ucsvreader(stream)))
-        stream.seek(0)
-        self.assertEqual([[u'a', u'b', u'c', u'd', u''],
-                          [u'1', u'2', u'3', u'4', u''],
-                          [u'', u'', u'', u'', u''],
-                          [u'', u'', u'', u'', u'']
-                          ],
-                         list(dataimport.ucsvreader(stream, skip_empty=False)))
-
-    def test_skip_first(self):
-        stream = StringIO('a,b,c,d,\n'
-                          '1,2,3,4,\n')
-        reader = dataimport.ucsvreader(stream, skipfirst=True,
-                                       ignore_errors=True)
-        self.assertEqual(list(reader),
-                         [[u'1', u'2', u'3', u'4', u'']])
-
-        stream.seek(0)
-        reader = dataimport.ucsvreader(stream, skipfirst=True,
-                                       ignore_errors=False)
-        self.assertEqual(list(reader),
-                         [[u'1', u'2', u'3', u'4', u'']])
-
-        stream.seek(0)
-        reader = dataimport.ucsvreader(stream, skipfirst=False,
-                                       ignore_errors=True)
-        self.assertEqual(list(reader),
-                         [[u'a', u'b', u'c', u'd', u''],
-                          [u'1', u'2', u'3', u'4', u'']])
-
-        stream.seek(0)
-        reader = dataimport.ucsvreader(stream, skipfirst=False,
-                                       ignore_errors=False)
-        self.assertEqual(list(reader),
-                         [[u'a', u'b', u'c', u'd', u''],
-                          [u'1', u'2', u'3', u'4', u'']])
-
-
-class MetaGeneratorTC(CubicWebTC):
-
-    def test_dont_generate_relation_to_internal_manager(self):
-        with self.admin_access.repo_cnx() as cnx:
-            metagen = dataimport.MetaGenerator(cnx)
-            self.assertIn('created_by', metagen.etype_rels)
-            self.assertIn('owned_by', metagen.etype_rels)
-        with self.repo.internal_cnx() as cnx:
-            metagen = dataimport.MetaGenerator(cnx)
-            self.assertNotIn('created_by', metagen.etype_rels)
-            self.assertNotIn('owned_by', metagen.etype_rels)
-
-    def test_dont_generate_specified_values(self):
-        with self.admin_access.repo_cnx() as cnx:
-            metagen = dataimport.MetaGenerator(cnx)
-            # hijack gen_modification_date to ensure we don't go through it
-            metagen.gen_modification_date = None
-            md = DT.datetime.now() - DT.timedelta(days=1)
-            entity, rels = metagen.base_etype_dicts('CWUser')
-            entity.cw_edited.update(dict(modification_date=md))
-            with cnx.ensure_cnx_set:
-                metagen.init_entity(entity)
-            self.assertEqual(entity.cw_edited['modification_date'], md)
-
-
-if __name__ == '__main__':
-    unittest_main()
--- a/test/unittest_dbapi.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unittest for cubicweb.dbapi"""
-
-from copy import copy
-
-from logilab.common import tempattr
-
-from cubicweb import ConnectionError, cwconfig, NoSelectableObject
-from cubicweb.dbapi import ProgrammingError, _repo_connect
-from cubicweb.devtools.testlib import CubicWebTC
-
-
-class DBAPITC(CubicWebTC):
-
-    def test_public_repo_api(self):
-        cnx = _repo_connect(self.repo, login='anon', password='anon')
-        self.assertEqual(cnx.get_schema(), self.repo.schema)
-        self.assertEqual(cnx.source_defs(), {'system': {'type': 'native', 'uri': 'system',
-                                                        'use-cwuri-as-url': False}})
-        cnx.close()
-        self.assertRaises(ProgrammingError, cnx.get_schema)
-        self.assertRaises(ProgrammingError, cnx.source_defs)
-
-    def test_db_api(self):
-        cnx = _repo_connect(self.repo, login='anon', password='anon')
-        self.assertEqual(cnx.rollback(), None)
-        self.assertEqual(cnx.commit(), None)
-        cnx.close()
-        self.assertRaises(ProgrammingError, cnx.rollback)
-        self.assertRaises(ProgrammingError, cnx.commit)
-        self.assertRaises(ProgrammingError, cnx.close)
-
-    def test_api(self):
-        cnx = _repo_connect(self.repo, login='anon', password='anon')
-        self.assertEqual(cnx.user(None).login, 'anon')
-        self.assertEqual({'type': u'CWSource', 'source': u'system', 'extid': None},
-                         cnx.entity_metas(1))
-        self.assertEqual(cnx.describe(1), (u'CWSource', u'system', None))
-        cnx.close()
-        self.assertRaises(ProgrammingError, cnx.user, None)
-        self.assertRaises(ProgrammingError, cnx.entity_metas, 1)
-        self.assertRaises(ProgrammingError, cnx.describe, 1)
-
-    def test_shared_data_api(self):
-        cnx = _repo_connect(self.repo, login='anon', password='anon')
-        self.assertEqual(cnx.get_shared_data('data'), None)
-        cnx.set_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.assertEqual(cnx.get_shared_data('data'), None)
-        cnx.set_shared_data('data', 4)
-        self.assertEqual(cnx.get_shared_data('data'), 4)
-        cnx.close()
-        self.assertRaises(ProgrammingError, cnx.check)
-        self.assertRaises(ProgrammingError, cnx.set_shared_data, 'data', 0)
-        self.assertRaises(ProgrammingError, cnx.get_shared_data, 'data')
-
-    def test_web_compatible_request(self):
-        config = cwconfig.CubicWebNoAppConfiguration()
-        cnx = _repo_connect(self.repo, login='admin', password='gingkow')
-        with tempattr(cnx.vreg, 'config', config):
-            cnx.use_web_compatible_requests('http://perdu.com')
-            req = cnx.request()
-            self.assertEqual(req.base_url(), 'http://perdu.com/')
-            self.assertEqual(req.from_controller(), 'view')
-            self.assertEqual(req.relative_path(), '')
-            req.ajax_replace_url('domid') # don't crash
-            req.user.cw_adapt_to('IBreadCrumbs') # don't crash
-
-    def test_call_service(self):
-        ServiceClass = self.vreg['services']['test_service'][0]
-        for _cw in (self.request(), self.session):
-            ret_value = _cw.call_service('test_service', msg='coucou')
-            self.assertEqual('coucou', ServiceClass.passed_here.pop())
-            self.assertEqual('babar', ret_value)
-        with self.login('anon') as ctm:
-            for _cw in (self.request(), self.session):
-                with self.assertRaises(NoSelectableObject):
-                    _cw.call_service('test_service', msg='toto')
-                self.rollback()
-                self.assertEqual([], ServiceClass.passed_here)
-
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/test/unittest_entity.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/unittest_entity.py	Fri Oct 09 17:52:14 2015 +0200
@@ -140,13 +140,24 @@
         with self.admin_access.web_request() as req:
             user = req.execute('Any X WHERE X eid %(x)s', {'x':req.user.eid}).get_entity(0, 0)
             adeleid = req.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
+            self.assertEqual({}, user._cw_related_cache)
             req.cnx.commit()
-            self.assertEqual(user._cw_related_cache, {})
+            self.assertEqual(['primary_email_subject', 'use_email_subject', 'wf_info_for_object'],
+                             sorted(user._cw_related_cache))
             email = user.primary_email[0]
-            self.assertEqual(sorted(user._cw_related_cache), ['primary_email_subject'])
-            self.assertEqual(list(email._cw_related_cache), ['primary_email_object'])
+            self.assertEqual(u'toto@logilab.org', email.address)
+            self.assertEqual(['created_by_subject',
+                              'cw_source_subject',
+                              'is_instance_of_subject',
+                              'is_subject',
+                              'owned_by_subject',
+                              'prefered_form_object',
+                              'prefered_form_subject',
+                              'primary_email_object',
+                              'use_email_object'],
+                             sorted(email._cw_related_cache))
+            self.assertEqual('admin', email._cw_related_cache['primary_email_object'][1][0].login)
             groups = user.in_group
-            self.assertEqual(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject'])
             for group in groups:
                 self.assertNotIn('in_group_subject', group._cw_related_cache)
             user.cw_clear_all_caches()
@@ -223,8 +234,8 @@
                 user = req.user
                 # testing basic fetch_attrs attribute
                 self.assertEqual(Personne.fetch_rql(user),
-                                 'Any X,AA,AB,AC ORDERBY AA '
-                                 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X modification_date AC')
+                                 'Any X,AA,AB,AC ORDERBY AB '
+                                 'WHERE X is_instance_of Personne, X modification_date AA, X nom AB, X prenom AC')
                 # testing unknown attributes
                 Personne.fetch_attrs = ('bloug', 'beep')
                 self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is_instance_of Personne')
@@ -236,21 +247,20 @@
                 # testing two non final relations
                 Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee')
                 self.assertEqual(Personne.fetch_rql(user),
-                                 'Any X,AA,AB,AC,AD,AE ORDERBY AA '
-                                 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
-                                 'X evaluee AE?')
+                                 'Any X,AA,AB,AC,AD,AE ORDERBY AB '
+                                 'WHERE X is_instance_of Personne, X evaluee AA?, X nom AB, X prenom AC, X travaille AD?, '
+                                 'AD nom AE')
                 # testing one non final relation with recursion
                 Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
                 Societe.fetch_attrs = ('nom', 'evaluee')
                 self.assertEqual(Personne.fetch_rql(user),
-                                 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA,AF DESC '
-                                 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
-                                 'AC evaluee AE?, AE modification_date AF'
-                                  )
+                                 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA,AE DESC '
+                                 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, '
+                                 'AC evaluee AD?, AD modification_date AE, AC nom AF')
                 # testing symmetric relation
                 Personne.fetch_attrs = ('nom', 'connait')
-                self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA '
-                                 'WHERE X is_instance_of Personne, X nom AA, X connait AB?')
+                self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AB '
+                                 'WHERE X is_instance_of Personne, X connait AA?, X nom AB')
                 # testing optional relation
                 peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '?*'
                 Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
@@ -278,8 +288,8 @@
         with self.admin_access.web_request() as req:
             p = req.create_entity('Personne', nom=u'pouet')
             self.assertEqual(p.cw_related_rql('evaluee'),
-                             'Any X,AA,AB ORDERBY AA WHERE E eid %(x)s, E evaluee X, '
-                             'X type AA, X modification_date AB')
+                             'Any X,AA,AB ORDERBY AB WHERE E eid %(x)s, E evaluee X, '
+                             'X modification_date AA, X type AB')
             n = req.create_entity('Note')
             self.assertEqual(n.cw_related_rql('evaluee', role='object',
                                               targettypes=('Societe', 'Personne')),
@@ -297,9 +307,9 @@
                               'Any X,AA ORDERBY AA DESC '
                               'WHERE E eid %(x)s, E tags X, X modification_date AA')
             self.assertEqual(tag.cw_related_rql('tags', 'subject', ('Personne',)),
-                              'Any X,AA,AB ORDERBY AA '
-                              'WHERE E eid %(x)s, E tags X, X is Personne, X nom AA, '
-                              'X modification_date AB')
+                              'Any X,AA,AB ORDERBY AB '
+                              'WHERE E eid %(x)s, E tags X, X is Personne, X modification_date AA, '
+                              'X nom AB')
 
     def test_related_rql_ambiguous_cant_use_fetch_order(self):
         with self.admin_access.web_request() as req:
@@ -363,9 +373,9 @@
         with self.admin_access.web_request() as req:
             email = req.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
             rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
-            self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+            self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
                              'WHERE NOT S use_email O, O eid %(x)s, S is_instance_of CWUser, '
-                             'S login AA, S firstname AB, S surname AC, S modification_date AD')
+                             'S firstname AA, S login AB, S modification_date AC, S surname AD')
             req.cnx.commit()
         rperms = self.schema['EmailAddress'].permissions['read']
         clear_cache(self.schema['EmailAddress'], 'get_groups')
@@ -375,9 +385,9 @@
             with self.new_access('anon').web_request() as req:
                 email = req.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
                 rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
-                self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+                self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
                              '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, '
+                             'S firstname AA, S login AB, S modification_date AC, S surname AD, '
                              'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
         finally:
             clear_cache(self.schema['EmailAddress'], 'get_groups')
@@ -388,17 +398,17 @@
         with self.admin_access.web_request() as req:
             email = req.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
             rql = email.cw_linkable_rql('use_email', 'CWUser', 'object')[0]
-            self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+            self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
                              'WHERE O eid %(x)s, S is_instance_of CWUser, '
-                             'S login AA, S firstname AB, S surname AC, S modification_date AD')
+                             'S firstname AA, S login AB, S modification_date AC, S surname AD')
 
     def test_unrelated_rql_security_nonexistant(self):
         with self.new_access('anon').web_request() as req:
             email = self.vreg['etypes'].etype_class('EmailAddress')(req)
             rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
-            self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+            self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
                          'WHERE S is CWUser, '
-                         'S login AA, S firstname AB, S surname AC, S modification_date AD, '
+                         'S firstname AA, S login AB, S modification_date AC, S surname AD, '
                          'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
 
     def test_unrelated_rql_constraints_creation_subject(self):
@@ -406,16 +416,16 @@
             person = self.vreg['etypes'].etype_class('Personne')(req)
             rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
             self.assertEqual(
-            rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
-            'O is_instance_of Personne, O nom AA, O prenom AB, O modification_date AC')
+            rql, 'Any O,AA,AB,AC ORDERBY AA DESC WHERE '
+            'O is_instance_of Personne, O modification_date AA, O nom AB, O prenom AC')
 
     def test_unrelated_rql_constraints_creation_object(self):
         with self.admin_access.web_request() as req:
             person = self.vreg['etypes'].etype_class('Personne')(req)
             rql = person.cw_unrelated_rql('connait', 'Personne', 'object')[0]
             self.assertEqual(
-            rql, 'Any S,AA,AB,AC ORDERBY AC DESC WHERE '
-            'S is Personne, S nom AA, S prenom AB, S modification_date AC, '
+            rql, 'Any S,AA,AB,AC ORDERBY AA DESC WHERE '
+            'S is Personne, S modification_date AA, S nom AB, S prenom AC, '
             'NOT (S connait AD, AD nom "toto"), AD is Personne, '
             'EXISTS(S travaille AE, AE nom "tutu")')
 
@@ -428,18 +438,18 @@
             with self.admin_access.web_request() as req:
                 person = self.vreg['etypes'].etype_class('Personne')(req)
                 rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
-                self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
-                         'O is_instance_of Personne, O nom AA, O prenom AB, '
-                         'O modification_date AC')
+                self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AA DESC WHERE '
+                         'O is_instance_of Personne, O modification_date AA, O nom AB, '
+                         'O prenom AC')
 
     def test_unrelated_rql_constraints_edition_subject(self):
         with self.admin_access.web_request() as req:
             person = req.create_entity('Personne', nom=u'sylvain')
             rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
             self.assertEqual(
-                rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
+                rql, 'Any O,AA,AB,AC ORDERBY AA DESC WHERE '
             'NOT S connait O, S eid %(x)s, O is Personne, '
-            'O nom AA, O prenom AB, O modification_date AC, '
+            'O modification_date AA, O nom AB, O prenom AC, '
             'NOT S identity O')
 
     def test_unrelated_rql_constraints_edition_object(self):
@@ -447,9 +457,9 @@
             person = req.create_entity('Personne', nom=u'sylvain')
             rql = person.cw_unrelated_rql('connait', 'Personne', 'object')[0]
             self.assertEqual(
-            rql, 'Any S,AA,AB,AC ORDERBY AC DESC WHERE '
+            rql, 'Any S,AA,AB,AC ORDERBY AA DESC WHERE '
             'NOT S connait O, O eid %(x)s, S is Personne, '
-            'S nom AA, S prenom AB, S modification_date AC, '
+            'S modification_date AA, S nom AB, S prenom AC, '
             'NOT S identity O, NOT (S connait AD, AD nom "toto"), '
             'EXISTS(S travaille AE, AE nom "tutu")')
 
@@ -634,7 +644,7 @@
 
     def test_printable_value_bytes(self):
         with self.admin_access.web_request() as req:
-            e = req.create_entity('File', data=Binary('lambda x: 1'), data_format=u'text/x-python',
+            e = req.create_entity('FakeFile', data=Binary('lambda x: 1'), data_format=u'text/x-python',
                                   data_encoding=u'ascii', data_name=u'toto.py')
             from cubicweb import mttransforms
             if mttransforms.HAS_PYGMENTS_TRANSFORMS:
@@ -653,7 +663,7 @@
     <span style="color: #C00000;">lambda</span> <span style="color: #000000;">x</span><span style="color: #0000C0;">:</span> <span style="color: #0080C0;">1</span>
 </pre>''')
 
-            e = req.create_entity('File', data=Binary('*héhéhé*'), data_format=u'text/rest',
+            e = req.create_entity('FakeFile', data=Binary('*héhéhé*'), data_format=u'text/rest',
                                 data_encoding=u'utf-8', data_name=u'toto.txt')
             self.assertEqual(e.printable_value('data'),
                               u'<p><em>héhéhé</em></p>')
@@ -704,7 +714,7 @@
 
     def test_fulltextindex(self):
         with self.admin_access.web_request() as req:
-            e = self.vreg['etypes'].etype_class('File')(req)
+            e = self.vreg['etypes'].etype_class('FakeFile')(req)
             e.cw_attr_cache['description'] = 'du <em>html</em>'
             e.cw_attr_cache['description_format'] = 'text/html'
             e.cw_attr_cache['data'] = Binary('some <em>data</em>')
--- a/test/unittest_predicates.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/unittest_predicates.py	Fri Oct 09 17:52:14 2015 +0200
@@ -27,7 +27,7 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.predicates import (is_instance, adaptable, match_kwargs, match_user_groups,
                                 multi_lines_rset, score_entity, is_in_state,
-                                rql_condition, relation_possible)
+                                rql_condition, relation_possible, match_form_params)
 from cubicweb.selectors import on_transition # XXX on_transition is deprecated
 from cubicweb.view import EntityAdapter
 from cubicweb.web import action
@@ -37,12 +37,13 @@
 class ImplementsTC(CubicWebTC):
     def test_etype_priority(self):
         with self.admin_access.web_request() as req:
-            f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
+            f = req.create_entity('FakeFile', data_name=u'hop.txt', data=Binary('hop'),
+                                  data_format=u'text/plain')
             rset = f.as_rset()
             anyscore = is_instance('Any')(f.__class__, req, rset=rset)
             idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
             self.assertTrue(idownscore > anyscore, (idownscore, anyscore))
-            filescore = is_instance('File')(f.__class__, req, rset=rset)
+            filescore = is_instance('FakeFile')(f.__class__, req, rset=rset)
             self.assertTrue(filescore > idownscore, (filescore, idownscore))
 
     def test_etype_inheritance_no_yams_inheritance(self):
@@ -391,6 +392,102 @@
                 rset = req.execute('Any X WHERE X is IN(CWGroup, CWUser)')
                 self.assertTrue(selector(None, req, rset=rset))
 
+
+class MatchFormParamsTC(CubicWebTC):
+    """tests for match_form_params predicate"""
+
+    def test_keyonly_match(self):
+        """test standard usage: ``match_form_params('param1', 'param2')``
+
+        ``param1`` and ``param2`` must be specified in request's form.
+        """
+        web_request = self.admin_access.web_request
+        vid_selector = match_form_params('vid')
+        vid_subvid_selector = match_form_params('vid', 'subvid')
+        # no parameter => KO,KO
+        with web_request() as req:
+            self.assertEqual(vid_selector(None, req), 0)
+            self.assertEqual(vid_subvid_selector(None, req), 0)
+        # one expected parameter found => OK,KO
+        with web_request(vid='foo') as req:
+            self.assertEqual(vid_selector(None, req), 1)
+            self.assertEqual(vid_subvid_selector(None, req), 0)
+        # all expected parameters found => OK,OK
+        with web_request(vid='foo', subvid='bar') as req:
+            self.assertEqual(vid_selector(None, req), 1)
+            self.assertEqual(vid_subvid_selector(None, req), 2)
+
+    def test_keyvalue_match_one_parameter(self):
+        """test dict usage: ``match_form_params(param1=value1)``
+
+        ``param1`` must be specified in the request's form and its value
+        must be ``value1``.
+        """
+        web_request = self.admin_access.web_request
+        # test both positional and named parameters
+        vid_selector = match_form_params(vid='foo')
+        # no parameter => should fail
+        with web_request() as req:
+            self.assertEqual(vid_selector(None, req), 0)
+        # expected parameter found with expected value => OK
+        with web_request(vid='foo', subvid='bar') as req:
+            self.assertEqual(vid_selector(None, req), 1)
+        # expected parameter found but value is incorrect => KO
+        with web_request(vid='bar') as req:
+            self.assertEqual(vid_selector(None, req), 0)
+
+    def test_keyvalue_match_two_parameters(self):
+        """test dict usage: ``match_form_params(param1=value1, param2=value2)``
+
+        ``param1`` and ``param2`` must be specified in the request's form and
+        their respective value must be ``value1`` and ``value2``.
+        """
+        web_request = self.admin_access.web_request
+        vid_subvid_selector = match_form_params(vid='list', subvid='tsearch')
+        # missing one expected parameter => KO
+        with web_request(vid='list') as req:
+            self.assertEqual(vid_subvid_selector(None, req), 0)
+        # expected parameters found but values are incorrect => KO
+        with web_request(vid='list', subvid='foo') as req:
+            self.assertEqual(vid_subvid_selector(None, req), 0)
+        # expected parameters found and values are correct => OK
+        with web_request(vid='list', subvid='tsearch') as req:
+            self.assertEqual(vid_subvid_selector(None, req), 2)
+
+    def test_keyvalue_multiple_match(self):
+        """test dict usage with multiple values
+
+        i.e. as in ``match_form_params(param1=('value1', 'value2'))``
+
+        ``param1`` must be specified in the request's form and its value
+        must be either ``value1`` or ``value2``.
+        """
+        web_request = self.admin_access.web_request
+        vid_subvid_selector = match_form_params(vid='list', subvid=('tsearch', 'listitem'))
+        # expected parameters found and values correct => OK
+        with web_request(vid='list', subvid='tsearch') as req:
+            self.assertEqual(vid_subvid_selector(None, req), 2)
+        with web_request(vid='list', subvid='listitem') as req:
+            self.assertEqual(vid_subvid_selector(None, req), 2)
+        # expected parameters found but values are incorrect => OK
+        with web_request(vid='list', subvid='foo') as req:
+            self.assertEqual(vid_subvid_selector(None, req), 0)
+
+    def test_invalid_calls(self):
+        """checks invalid calls raise a ValueError"""
+        # mixing named and positional arguments should fail
+        with self.assertRaises(ValueError) as cm:
+            match_form_params('list', x='1', y='2')
+        self.assertEqual(str(cm.exception),
+                         "match_form_params() can't be called with both "
+                         "positional and named arguments")
+        # using a dict as first and unique argument should fail
+        with self.assertRaises(ValueError) as cm:
+            match_form_params({'x': 1})
+        self.assertEqual(str(cm.exception),
+                         "match_form_params() positional arguments must be strings")
+
+
 if __name__ == '__main__':
     unittest_main()
 
--- a/test/unittest_repoapi.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/unittest_repoapi.py	Fri Oct 09 17:52:14 2015 +0200
@@ -15,18 +15,18 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unittest for cubicweb.dbapi"""
+"""unittest for cubicweb.repoapi"""
 
 
 from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb import ProgrammingError
-from cubicweb.repoapi import ClientConnection, connect, anonymous_cnx
+from cubicweb.repoapi import Connection, connect, anonymous_cnx
 
 
 class REPOAPITC(CubicWebTC):
 
-    def test_clt_cnx_basic_usage(self):
+    def test_cnx_basic_usage(self):
         """Test that a client connection can be used to access the database"""
         with self.admin_access.client_cnx() as cltcnx:
             # (1) some RQL request
@@ -52,11 +52,11 @@
                                   ''')
             self.assertTrue(rset)
 
-    def test_clt_cnx_life_cycle(self):
+    def test_cnx_life_cycle(self):
         """Check that ClientConnection requires explicit open and close
         """
         access = self.admin_access
-        cltcnx = ClientConnection(access._session)
+        cltcnx = Connection(access._session)
         # connection not open yet
         with self.assertRaises(ProgrammingError):
             cltcnx.execute('Any X WHERE X is CWUser')
@@ -69,18 +69,18 @@
 
     def test_connect(self):
         """check that repoapi.connect works and returns a usable connection"""
-        clt_cnx = connect(self.repo, login='admin', password='gingkow')
-        self.assertEqual('admin', clt_cnx.user.login)
-        with clt_cnx:
-            rset = clt_cnx.execute('Any X WHERE X is CWUser')
+        cnx = connect(self.repo, login='admin', password='gingkow')
+        self.assertEqual('admin', cnx.user.login)
+        with cnx:
+            rset = cnx.execute('Any X WHERE X is CWUser')
             self.assertTrue(rset)
 
     def test_anonymous_connect(self):
         """check that you can get anonymous connection when the data exist"""
-        clt_cnx = anonymous_cnx(self.repo)
-        self.assertEqual('anon', clt_cnx.user.login)
-        with clt_cnx:
-            rset = clt_cnx.execute('Any X WHERE X is CWUser')
+        cnx = anonymous_cnx(self.repo)
+        self.assertEqual('anon', cnx.user.login)
+        with cnx:
+            rset = cnx.execute('Any X WHERE X is CWUser')
             self.assertTrue(rset)
 
 
--- a/test/unittest_schema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/unittest_schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -169,7 +169,7 @@
                              'CWRelation', 'CWPermission', 'CWProperty', 'CWRType',
                              'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig',
                              'CWUniqueTogetherConstraint', 'CWUser',
-                             'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
+                             'ExternalUri', 'FakeFile', 'Float', 'Int', 'Interval', 'Note',
                              'Password', 'Personne', 'Produit',
                              'RQLExpression', 'Reference',
                              'Service', 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint',
@@ -221,8 +221,6 @@
                               'value',
 
                               'wf_info_for', 'wikiid', 'workflow_of', 'tr_count']
-        if config.cube_version('file') >= (1, 14, 0):
-            expected_relations.append('data_sha1hex')
 
         self.assertListEqual(sorted(expected_relations), relations)
 
@@ -360,8 +358,8 @@
                          schema['produces_and_buys'].rdefs.keys())
         self.assertEqual([('Person','Service')],
                          schema['produces_and_buys2'].rdefs.keys())
-        self.assertEqual([('Company', 'Service'), ('Person', 'Service')],
-                         schema['reproduce'].rdefs.keys())
+        self.assertCountEqual([('Company', 'Service'), ('Person', 'Service')],
+                              schema['reproduce'].rdefs.keys())
         # check relation definitions are marked infered
         rdef = schema['produces_and_buys'].rdefs[('Person','Service')]
         self.assertTrue(rdef.infered)
@@ -419,6 +417,10 @@
         self._test('rrqlexpr_on_attr.py',
                    "can't use RRQLExpression on attribute ToTo.attr[String], use an ERQLExpression")
 
+    def test_rqlexpr_on_computedrel(self):
+        self._test('rqlexpr_on_computedrel.py',
+                   "can't use rql expression for read permission of relation Subject computed Object")
+
 
 class NormalizeExpressionTC(TestCase):
 
@@ -484,6 +486,7 @@
                        ('cw_schema', 'CWSourceSchemaConfig', 'CWRelation', 'object'),
                        ('delete_permission', 'CWRelation', 'RQLExpression', 'subject'),
                        ('read_permission', 'CWRelation', 'RQLExpression', 'subject')],
+        'CWComputedRType': [('read_permission', 'CWComputedRType', 'RQLExpression', 'subject')],
         'CWSource': [('cw_for_source', 'CWSourceSchemaConfig', 'CWSource', 'object'),
                      ('cw_host_config_of', 'CWSourceHostConfig', 'CWSource', 'object'),
                      ('cw_import_of', 'CWDataImport', 'CWSource', 'object'),
@@ -510,7 +513,7 @@
                      ('cw_source', 'Card', 'CWSource', 'object'),
                      ('cw_source', 'EmailAddress', 'CWSource', 'object'),
                      ('cw_source', 'ExternalUri', 'CWSource', 'object'),
-                     ('cw_source', 'File', 'CWSource', 'object'),
+                     ('cw_source', 'FakeFile', 'CWSource', 'object'),
                      ('cw_source', 'Note', 'CWSource', 'object'),
                      ('cw_source', 'Personne', 'CWSource', 'object'),
                      ('cw_source', 'Produit', 'CWSource', 'object'),
--- a/test/unittest_utils.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/test/unittest_utils.py	Fri Oct 09 17:52:14 2015 +0200
@@ -58,12 +58,6 @@
                          parse_repo_uri('myapp'))
         self.assertEqual(('inmemory', None, 'myapp'),
                          parse_repo_uri('inmemory://myapp'))
-        self.assertEqual(('pyro', 'pyro-ns-host:pyro-ns-port', '/myapp'),
-                         parse_repo_uri('pyro://pyro-ns-host:pyro-ns-port/myapp'))
-        self.assertEqual(('pyroloc', 'host:port', '/appkey'),
-                         parse_repo_uri('pyroloc://host:port/appkey'))
-        self.assertEqual(('zmqpickle-tcp', '127.0.0.1:666', ''),
-                         parse_repo_uri('zmqpickle-tcp://127.0.0.1:666'))
         with self.assertRaises(NotImplementedError):
             parse_repo_uri('foo://bar')
 
--- a/toolsutils.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/toolsutils.py	Fri Oct 09 17:52:14 2015 +0200
@@ -257,18 +257,6 @@
       }),
     )
 
-def config_connect(appid, optconfig):
-    from cubicweb.dbapi import connect
-    from getpass import getpass
-    user = optconfig.user
-    if not user:
-        user = raw_input('login: ')
-    password = optconfig.password
-    if not password:
-        password = getpass('password: ')
-    return connect(login=user, password=password, host=optconfig.host, database=appid)
-
-
 ## cwshell helpers #############################################################
 
 class AbstractMatcher(object):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tox.ini	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,55 @@
+[tox]
+env = py27
+
+[testenv]
+sitepackages = True
+commands = pytest -t {envname}/test {posargs}
+
+[testenv:cubicweb]
+deps =
+  -r{toxinidir}/test/requirements.txt
+commands = pytest -t test {posargs}
+
+[testenv:dataimport]
+
+[testenv:devtools]
+deps =
+  -r{toxinidir}/devtools/test/requirements.txt
+
+[testenv:entities]
+deps =
+  -r{toxinidir}/entities/test/requirements.txt
+
+[testenv:etwist]
+deps =
+  -r{toxinidir}/etwist/test/requirements.txt
+
+[testenv:ext]
+deps =
+  -r{toxinidir}/ext/test/requirements.txt
+
+[testenv:hooks]
+
+[testenv:server]
+deps =
+  -r{toxinidir}/server/test/requirements.txt
+
+[testenv:sobjects]
+deps =
+  -r{toxinidir}/sobjects/test/requirements.txt
+
+[testenv:web]
+deps =
+  -r{toxinidir}/web/test/requirements.txt
+
+[testenv:wsgi]
+deps =
+  -r{toxinidir}/wsgi/test/requirements.txt
+
+[testenv:doc]
+changedir = doc
+whitelist_externals =
+  sphinx-build
+deps =
+  sphinx
+commands = sphinx-build -b html -d {envtmpdir}/doctrees .  {envtmpdir}/html
--- a/transaction.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/transaction.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,13 +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/>.
-"""undoable transaction objects.
-
-
-This module is in the cubicweb package and not in cubicweb.server because those
-objects should be accessible to client through pyro, where the cubicweb.server
-package may not be installed.
-"""
+""" undoable transaction objects. """
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -42,27 +36,21 @@
     msg = _("there is no transaction #%s")
 
     def __init__(self, txuuid):
-        super(RepositoryError, self).__init__(txuuid)
+        super(NoSuchTransaction, self).__init__(txuuid)
         self.txuuid = txuuid
 
 class Transaction(object):
     """an undoable transaction"""
 
-    def __init__(self, uuid, time, ueid):
+    def __init__(self, cnx, uuid, time, ueid):
+        self.cnx = cnx
         self.uuid = uuid
         self.datetime = time
         self.user_eid = ueid
-        # should be set by the dbapi connection
-        self.req = None  # old style
-        self.cnx = None  # new style
 
     def _execute(self, *args, **kwargs):
         """execute a query using either the req or the cnx"""
-        if self.req is None:
-            execute = self.cnx.execute
-        else:
-            execute = self.req
-        return execute(*args, **kwargs)
+        return self.cnx.execute(*args, **kwargs)
 
 
     def __repr__(self):
@@ -73,8 +61,7 @@
         """return the user entity which has done the transaction,
         none if not found.
         """
-        return self._execute('Any X WHERE X eid %(x)s',
-                             {'x': self.user_eid}).get_entity(0, 0)
+        return self.cnx.find('CWUser', eid=self.user_eid).one()
 
     def actions_list(self, public=True):
         """return an ordered list of action effectued during that transaction
@@ -82,14 +69,11 @@
         if public is true, return only 'public' action, eg not ones triggered
         under the cover by hooks.
         """
-        if self.req is not None:
-            cnx = self.req.cnx
-        else:
-            cnx = self.cnx
-        return cnx.transaction_actions(self.uuid, public)
+        return self.cnx.transaction_actions(self.uuid, public)
 
 
 class AbstractAction(object):
+
     def __init__(self, action, public, order):
         self.action = action
         self.public = public
@@ -106,8 +90,9 @@
 
 
 class EntityAction(AbstractAction):
+
     def __init__(self, action, public, order, etype, eid, changes):
-        AbstractAction.__init__(self, action, public, order)
+        super(EntityAction, self).__init__(action, public, order)
         self.etype = etype
         self.eid = eid
         self.changes = changes
@@ -124,8 +109,9 @@
 
 
 class RelationAction(AbstractAction):
+
     def __init__(self, action, public, order, rtype, eidfrom, eidto):
-        AbstractAction.__init__(self, action, public, order)
+        super(RelationAction, self).__init__(action, public, order)
         self.rtype = rtype
         self.eid_from = eidfrom
         self.eid_to = eidto
--- a/utils.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/utils.py	Fri Oct 09 17:52:14 2015 +0200
@@ -21,7 +21,6 @@
 
 __docformat__ = "restructuredtext en"
 
-import sys
 import decimal
 import datetime
 import random
@@ -553,8 +552,12 @@
     """
 
 def _dict2js(d, predictable=False):
+    if predictable:
+        it = sorted(d.iteritems())
+    else:
+        it = d.iteritems()
     res = [key + ': ' + js_dumps(val, predictable)
-           for key, val in d.iteritems()]
+           for key, val in it]
     return '{%s}' % ', '.join(res)
 
 def _list2js(l, predictable=False):
@@ -578,7 +581,7 @@
         return _list2js(something, predictable)
     if isinstance(something, JSString):
         return something
-    return json_dumps(something)
+    return json_dumps(something, sort_keys=predictable)
 
 PERCENT_IN_URLQUOTE_RE = re.compile(r'%(?=[0-9a-fA-F]{2})')
 def js_href(javascript_code):
@@ -608,8 +611,6 @@
     """ transform a command line uri into a (protocol, hostport, appid), e.g:
     <myapp>                      -> 'inmemory', None, '<myapp>'
     inmemory://<myapp>           -> 'inmemory', None, '<myapp>'
-    pyro://[host][:port]         -> 'pyro', 'host:port', None
-    zmqpickle://[host][:port]    -> 'zmqpickle', 'host:port', None
     """
     parseduri = urlparse(uri)
     scheme = parseduri.scheme
@@ -617,8 +618,6 @@
         return ('inmemory', None, parseduri.path)
     if scheme == 'inmemory':
         return (scheme, None, parseduri.netloc)
-    if scheme in ('pyro', 'pyroloc') or scheme.startswith('zmqpickle-'):
-        return (scheme, parseduri.netloc, parseduri.path)
     raise NotImplementedError('URI protocol not implemented for `%s`' % uri)
 
 
@@ -647,7 +646,7 @@
 
     Occasional elements can be buggy requests (server-side) or
     end-user (web-ui provided) requests. These have to be cleaned up
-    when they fill the cache, without evicting the usefull, frequently
+    when they fill the cache, without evicting the useful, frequently
     used entries.
     """
     # quite arbitrary, but we want to never
--- a/view.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/view.py	Fri Oct 09 17:52:14 2015 +0200
@@ -20,7 +20,7 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from cStringIO import StringIO
+from io import BytesIO
 from warnings import warn
 from functools import partial
 
@@ -101,7 +101,7 @@
             return
         if w is None:
             if self.binary:
-                self._stream = stream = StringIO()
+                self._stream = stream = BytesIO()
             else:
                 self._stream = stream = UStringIO()
             w = stream.write
@@ -471,7 +471,7 @@
             return
         if w is None:
             if self.binary:
-                self._stream = stream = StringIO()
+                self._stream = stream = BytesIO()
             else:
                 self._stream = stream = HTMLStream(self._cw)
             w = stream.write
--- a/web/application.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/application.py	Fri Oct 09 17:52:14 2015 +0200
@@ -52,103 +52,14 @@
 @contextmanager
 def anonymized_request(req):
     orig_cnx = req.cnx
-    anon_clt_cnx = anonymous_cnx(orig_cnx._session.repo)
-    req.set_cnx(anon_clt_cnx)
+    anon_cnx = anonymous_cnx(orig_cnx.session.repo)
+    req.set_cnx(anon_cnx)
     try:
-        with anon_clt_cnx:
+        with anon_cnx:
             yield req
     finally:
         req.set_cnx(orig_cnx)
 
-class AbstractSessionManager(component.Component):
-    """manage session data associated to a session identifier"""
-    __regid__ = 'sessionmanager'
-
-    def __init__(self, repo):
-        vreg = repo.vreg
-        self.session_time = vreg.config['http-session-time'] or None
-        self.authmanager = vreg['components'].select('authmanager', repo=repo)
-        interval = (self.session_time or 0) / 2.
-        if vreg.config.anonymous_user()[0] is not None:
-            self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
-            assert self.cleanup_anon_session_time > 0
-            if self.session_time is not None:
-                self.cleanup_anon_session_time = min(self.session_time,
-                                                     self.cleanup_anon_session_time)
-            interval = self.cleanup_anon_session_time / 2.
-        # we don't want to check session more than once every 5 minutes
-        self.clean_sessions_interval = max(5 * 60, interval)
-
-    def clean_sessions(self):
-        """cleanup sessions which has not been unused since a given amount of
-        time. Return the number of sessions which have been closed.
-        """
-        self.debug('cleaning http sessions')
-        session_time = self.session_time
-        closed, total = 0, 0
-        for session in self.current_sessions():
-            total += 1
-            last_usage_time = session.mtime
-            no_use_time = (time() - last_usage_time)
-            if session.anonymous_session:
-                if no_use_time >= self.cleanup_anon_session_time:
-                    self.close_session(session)
-                    closed += 1
-            elif session_time is not None and no_use_time >= session_time:
-                self.close_session(session)
-                closed += 1
-        return closed, total - closed
-
-    def current_sessions(self):
-        """return currently open sessions"""
-        raise NotImplementedError()
-
-    def get_session(self, req, sessionid):
-        """return existing session for the given session identifier"""
-        raise NotImplementedError()
-
-    def open_session(self, req):
-        """open and return a new session for the given request.
-
-        raise :exc:`cubicweb.AuthenticationError` if authentication failed
-        (no authentication info found or wrong user/password)
-        """
-        raise NotImplementedError()
-
-    def close_session(self, session):
-        """close session on logout or on invalid session detected (expired out,
-        corrupted...)
-        """
-        raise NotImplementedError()
-
-
-class AbstractAuthenticationManager(component.Component):
-    """authenticate user associated to a request and check session validity"""
-    __regid__ = 'authmanager'
-
-    def __init__(self, repo):
-        self.vreg = repo.vreg
-
-    def validate_session(self, req, session):
-        """check session validity, reconnecting it to the repository if the
-        associated connection expired in the repository side (hence the
-        necessity for this method).
-
-        raise :exc:`InvalidSession` if session is corrupted for a reason or
-        another and should be closed
-        """
-        raise NotImplementedError()
-
-    def authenticate(self, req):
-        """authenticate user using connection information found in the request,
-        and return corresponding a :class:`~cubicweb.dbapi.Connection` instance,
-        as well as login and authentication information dictionary used to open
-        the connection.
-
-        raise :exc:`cubicweb.AuthenticationError` if authentication failed
-        (no authentication info found or wrong user/password)
-        """
-        raise NotImplementedError()
 
 
 class CookieSessionHandler(object):
@@ -350,7 +261,7 @@
             try:
                 session = self.get_session(req)
                 from  cubicweb import repoapi
-                cnx = repoapi.ClientConnection(session)
+                cnx = repoapi.Connection(session)
                 req.set_cnx(cnx)
             except AuthenticationError:
                 # Keep the dummy session set at initialisation.
@@ -365,12 +276,6 @@
                 # several cubes like registration or forgotten password rely on
                 # this principle.
 
-            # DENY https acces for anonymous_user
-            if (req.https
-                and req.session.anonymous_session
-                and self.vreg.config['https-deny-anonymous']):
-                # don't allow anonymous on https connection
-                raise AuthenticationError()
             # nested try to allow LogOut to delegate logic to AuthenticationError
             # handler
             try:
--- a/web/component.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/component.py	Fri Oct 09 17:52:14 2015 +0200
@@ -353,7 +353,7 @@
         has some content to display. If not, you can still raise
         :exc:`EmptyComponent` to inform it should be skipped.
 
-        Also, :exc:`Unauthorized` will be catched, logged, then the component
+        Also, :exc:`Unauthorized` will be caught, logged, then the component
         will be skipped.
         """
         self.items = []
--- a/web/data/cubicweb.ajax.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/data/cubicweb.ajax.js	Fri Oct 09 17:52:14 2015 +0200
@@ -39,7 +39,7 @@
     },
 
     addCallback: function(callback) {
-        if ((this._req.readyState == 4) && this._result) {
+        if (this._req && (this._req.readyState == 4) && this._result) {
             var args = [this._result, this._req];
             jQuery.merge(args, cw.utils.sliceList(arguments, 1));
             callback.apply(null, args);
@@ -51,7 +51,7 @@
     },
 
     addErrback: function(callback) {
-        if (this._req.readyState == 4 && this._error) {
+        if (this._req && this._req.readyState == 4 && this._error) {
             callback.apply(null, [this._error, this._req]);
         }
         else {
@@ -275,9 +275,6 @@
     if (typeof roundedCorners != 'undefined') {
         roundedCorners(node);
     }
-    if (typeof setFormsTarget != 'undefined') {
-        setFormsTarget(node);
-    }
     _loadDynamicFragments(node);
     jQuery(cw).trigger('server-response', [true, node]);
     jQuery(node).trigger('server-response', [true, node]);
@@ -503,7 +500,7 @@
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'POST', true);
 }
 
 function removeBookmark(beid) {
@@ -518,59 +515,6 @@
     });
 }
 
-userCallback = cw.utils.deprecatedFunction(
-    '[3.19] use a plain ajaxfunc instead of user callbacks',
-    function userCallback(cbname) {
-    setProgressCursor();
-    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('user_callback', null, cbname));
-    d.addCallback(resetCursor);
-    d.addErrback(resetCursor);
-    d.addErrback(remoteCallFailed);
-    return d;
-});
-
-userCallbackThenUpdateUI = cw.utils.deprecatedFunction(
-    '[3.19] use a plain ajaxfunc instead of user callbacks',
-    function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
-    var d = userCallback(cbname);
-    d.addCallback(function() {
-        $('#' + nodeid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {'rql': rql},
-                                                       registry, compid), null, 'swap');
-        if (msg) {
-            updateMessage(msg);
-        }
-    });
-});
-
-userCallbackThenReloadPage = cw.utils.deprecatedFunction(
-    '[3.19] use a plain ajaxfunc instead of user callbacks',
-    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.
- */
-unregisterUserCallback = cw.utils.deprecatedFunction(
-    '[3.19] use a plain ajaxfunc instead of user callbacks',
-    function unregisterUserCallback(cbname) {
-    setProgressCursor();
-    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unregister_user_callback',
-                                            null, cbname));
-    d.addCallback(resetCursor);
-    d.addErrback(resetCursor);
-    d.addErrback(remoteCallFailed);
-});
-
 
 //============= XXX move those functions? ====================================//
 function openHash() {
--- a/web/data/cubicweb.edition.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/data/cubicweb.edition.js	Fri Oct 09 17:52:14 2015 +0200
@@ -171,7 +171,7 @@
     var entityForm = jQuery('#entityForm');
     var oid = optionNode.id.substring(2); // option id is prefixed by "id"
     loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null,
-                                           [oid.split(':')]), 'GET', true);
+                                           [oid.split(':')]), 'POST', true);
     var selectNode = optionNode.parentNode;
     // remove option node
     selectNode.removeChild(optionNode);
@@ -537,53 +537,6 @@
 }
 
 /**
- * .. function:: setFormsTarget(node)
- *
- * called on load to set target and iframeso object.
- *
- * .. note::
- *
- *    This was a hack to make form loop handling XHTML compliant.
- *    Since we do not care about xhtml any longer, this may go away.
- *
- * .. note::
- *
- *   `object` nodes might be a potential replacement for iframes
- *
- * .. note::
- *
- *    The form's `target` attribute should probably become a simple data-target
- *    immediately generated server-side.
- *    Since we don't do xhtml any longer, the iframe should probably be either
- *    reconsidered or at least emitted server-side.
- */
-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
-             * with no display */
-            form.append(IFRAME({
-                name: target,
-                id: target,
-                src: 'javascript: void(0)',
-                width: '0px',
-                height: '0px'
-            }));
-            form.removeAttr('cubicweb:target'); // useles from now on, pop it
-                                                // to make IE9 happy
-        }
-    });
-}
-
-jQuery(document).ready(function() {
-    setFormsTarget();
-});
-
-/**
  * .. function:: validateForm(formid, action, onsuccess, onfailure)
  *
  * called on traditionnal form submission : the idea is to try
--- a/web/data/cubicweb.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/data/cubicweb.js	Fri Oct 09 17:52:14 2015 +0200
@@ -16,7 +16,7 @@
     detachEvent: function() {},
 
     log: function log() {
-        if (typeof(window) != "undefined" && window.console && window.console.log) {
+        if (typeof window !== "undefined" && window.console && window.console.log) {
             // NOTE console.log requires "console" to be the console to be "this"
             window.console.log.apply(console, arguments);
         }
@@ -33,7 +33,7 @@
      * safe version of jQuery('#nodeid') because we use ':' in nodeids
      * which messes with jQuery selection mechanism
      */
-        if (typeof(node) == 'string') {
+        if (typeof node === 'string') {
             node = document.getElementById(node);
         }
         if (node) {
@@ -44,7 +44,7 @@
 
     // escapes string selectors (e.g. "foo.[subject]:42" -> "foo\.\[subject\]\:42"
     escape: function(selector) {
-        if (typeof(selector) == 'string') {
+        if (typeof selector === 'string') {
             return  selector.replace( /(:|\.|\[|\])/g, "\\$1" );
         }
         // cw.log('non string selector', selector);
@@ -52,7 +52,7 @@
     },
 
     getNode: function (node) {
-        if (typeof(node) == 'string') {
+        if (typeof node === 'string') {
             return document.getElementById(node);
         }
         return node;
@@ -69,7 +69,7 @@
     },
 
     urlEncode: function (str) {
-        if (typeof(encodeURIComponent) != "undefined") {
+        if (typeof encodeURIComponent !== "undefined") {
             return encodeURIComponent(str).replace(/\'/g, '%27');
         } else {
             return escape(str).replace(/\+/g, '%2B').replace(/\"/g, '%22').
@@ -93,7 +93,7 @@
         var $node = $(node);
         var sortvalue = $node.attr('cubicweb:sortvalue');
         // No metadata found, use cell content as sort key
-        if (sortvalue === undefined) {
+        if (typeof sortvalue === 'undefined') {
             return $node.text();
         }
         return cw.evalJSON(sortvalue);
@@ -117,9 +117,9 @@
             var node = document.createElement(tag);
             for (key in params) {
                 var value = params[key];
-                if (key.substring(0, 2) == 'on') {
+                if (key.substring(0, 2) === 'on') {
                     // this is an event handler definition
-                    if (typeof value == 'string') {
+                    if (typeof value === 'string') {
                         // litteral definition
                         value = new Function(value);
                     }
@@ -142,7 +142,7 @@
                 }
                 for (var i = 0; i < children.length; i++) {
                     var child = children[i];
-                    if (typeof child == "string" || typeof child == "number") {
+                    if (typeof child === "string" || typeof child === "number") {
                         child = document.createTextNode(child);
                     }
                     node.appendChild(child);
@@ -158,7 +158,7 @@
      *
      */
     toISOTimestamp: function (date) {
-        if (typeof(date) == "undefined" || date === null) {
+        if (date == null) {
             return null;
         }
 
@@ -188,11 +188,11 @@
     },
 
     isArray: function (it) { // taken from dojo
-        return it && (it instanceof Array || typeof it == "array");
+        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);
+        return !!arguments.length && it != null && (typeof it === "string" || it instanceof String);
     },
 
     isArrayLike: function (it) { // taken from dojo
@@ -405,11 +405,11 @@
         var node = document.createElement('iframe');
     }
     for (key in params) {
-        if (key != 'name') {
+        if (key !== 'name') {
             var value = params[key];
-            if (key.substring(0, 2) == 'on') {
+            if (key.substring(0, 2) === 'on') {
                 // this is an event handler definition
-                if (typeof value == 'string') {
+                if (typeof value === 'string') {
                     // litteral definition
                     value = new Function(value);
                 }
--- a/web/data/cubicweb.timeline-bundle.js	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10129 +0,0 @@
-/**
- *  This file contains timeline utilities
- *  :organization: Logilab
- */
-
-var SimileAjax_urlPrefix = BASE_URL + 'data/';
-var Timeline_urlPrefix = BASE_URL + '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") {
-    var SimileAjax = {
-        loaded:                 false,
-        loadingScriptsCount:    0,
-        error:                  null,
-        params:                 { bundle:"true" }
-    };
-
-    SimileAjax.Platform = new Object();
-        /*
-            HACK: We need these 2 things here because we cannot simply append
-            a <script> element containing code that accesses SimileAjax.Platform
-            to initialize it because IE executes that <script> code first
-            before it loads ajax.js and platform.js.
-        */
-
-    var getHead = function(doc) {
-        return doc.getElementsByTagName("head")[0];
-    };
-
-    SimileAjax.findScript = function(doc, substring) {
-        var heads = doc.documentElement.getElementsByTagName("head");
-        for (var h = 0; h < heads.length; h++) {
-            var node = heads[h].firstChild;
-            while (node != null) {
-                if (node.nodeType == 1 && node.tagName.toLowerCase() == "script") {
-                    var url = node.src;
-                    var i = url.indexOf(substring);
-                    if (i >= 0) {
-                        return url;
-                    }
-                }
-                node = node.nextSibling;
-            }
-        }
-        return null;
-    };
-    SimileAjax.includeJavascriptFile = function(doc, url, onerror, charset) {
-        onerror = onerror || "";
-        if (doc.body == null) {
-            try {
-                var q = "'" + onerror.replace( /'/g, '&apos' ) + "'"; // "
-                doc.write("<script src='" + url + "' onerror="+ q +
-                          (charset ? " charset='"+ charset +"'" : "") +
-                          " type='text/javascript'>"+ onerror + "</script>");
-                return;
-            } catch (e) {
-                // fall through
-            }
-        }
-
-        var script = doc.createElement("script");
-        if (onerror) {
-            try { script.innerHTML = onerror; } catch(e) {}
-            script.setAttribute("onerror", onerror);
-        }
-        if (charset) {
-            script.setAttribute("charset", charset);
-        }
-        script.type = "text/javascript";
-        script.language = "JavaScript";
-        script.src = url;
-        return getHead(doc).appendChild(script);
-    };
-    SimileAjax.includeJavascriptFiles = function(doc, urlPrefix, filenames) {
-        for (var i = 0; i < filenames.length; i++) {
-            SimileAjax.includeJavascriptFile(doc, urlPrefix + filenames[i]);
-        }
-        SimileAjax.loadingScriptsCount += filenames.length;
-        // XXX adim SimileAjax.includeJavascriptFile(doc, SimileAjax.urlPrefix + "scripts/signal.js?" + filenames.length);
-    };
-    SimileAjax.includeCssFile = function(doc, url) {
-        if (doc.body == null) {
-            try {
-                doc.write("<link rel='stylesheet' href='" + url + "' type='text/css'/>");
-                return;
-            } catch (e) {
-                // fall through
-            }
-        }
-
-        var link = doc.createElement("link");
-        link.setAttribute("rel", "stylesheet");
-        link.setAttribute("type", "text/css");
-        link.setAttribute("href", url);
-        getHead(doc).appendChild(link);
-    };
-    SimileAjax.includeCssFiles = function(doc, urlPrefix, filenames) {
-        for (var i = 0; i < filenames.length; i++) {
-            SimileAjax.includeCssFile(doc, urlPrefix + filenames[i]);
-        }
-    };
-
-    /**
-     * Append into urls each string in suffixes after prefixing it with urlPrefix.
-     * @param {Array} urls
-     * @param {String} urlPrefix
-     * @param {Array} suffixes
-     */
-    SimileAjax.prefixURLs = function(urls, urlPrefix, suffixes) {
-        for (var i = 0; i < suffixes.length; i++) {
-            urls.push(urlPrefix + suffixes[i]);
-        }
-    };
-
-    /**
-     * Parse out the query parameters from a URL
-     * @param {String} url    the url to parse, or location.href if undefined
-     * @param {Object} to     optional object to extend with the parameters
-     * @param {Object} types  optional object mapping keys to value types
-     *        (String, Number, Boolean or Array, String by default)
-     * @return a key/value Object whose keys are the query parameter names
-     * @type Object
-     */
-    SimileAjax.parseURLParameters = function(url, to, types) {
-        to = to || {};
-        types = types || {};
-
-        if (typeof url == "undefined") {
-            url = location.href;
-        }
-        var q = url.indexOf("?");
-        if (q < 0) {
-            return to;
-        }
-        url = (url+"#").slice(q+1, url.indexOf("#")); // toss the URL fragment
-
-        var params = url.split("&"), param, parsed = {};
-        var decode = window.decodeURIComponent || unescape;
-        for (var i = 0; param = params[i]; i++) {
-            var eq = param.indexOf("=");
-            var name = decode(param.slice(0,eq));
-            var old = parsed[name];
-            if (typeof old == "undefined") {
-                old = [];
-            } else if (!(old instanceof Array)) {
-                old = [old];
-            }
-            parsed[name] = old.concat(decode(param.slice(eq+1)));
-        }
-        for (var i in parsed) {
-            if (!parsed.hasOwnProperty(i)) continue;
-            var type = types[i] || String;
-            var data = parsed[i];
-            if (!(data instanceof Array)) {
-                data = [data];
-            }
-            if (type === Boolean && data[0] == "false") {
-                to[i] = false; // because Boolean("false") === true
-            } else {
-                to[i] = type.apply(this, data);
-            }
-        }
-        return to;
-    };
-
-    (function() {
-        var javascriptFiles = [
-            "jquery-1.2.6.js",
-            "platform.js",
-            "debug.js",
-            "xmlhttp.js",
-            "json.js",
-            "dom.js",
-            "graphics.js",
-            "date-time.js",
-            "string.js",
-            "html.js",
-            "data-structure.js",
-            "units.js",
-
-            "ajax.js",
-            "history.js",
-            "window-manager.js"
-        ];
-        var cssFiles = [
-            "graphics.css"
-        ];
-
-        if (typeof SimileAjax_urlPrefix == "string") {
-            SimileAjax.urlPrefix = SimileAjax_urlPrefix;
-        } else {
-            var url = SimileAjax.findScript(document, "simile-ajax-api.js");
-            if (url == null) {
-                SimileAjax.error = new Error("Failed to derive URL prefix for Simile Ajax API code files");
-                return;
-            }
-
-            SimileAjax.urlPrefix = url.substr(0, url.indexOf("simile-ajax-api.js"));
-        }
-
-        SimileAjax.parseURLParameters(url, SimileAjax.params, {bundle:Boolean});
-//         if (SimileAjax.params.bundle) {
-//             SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix, [ "simile-ajax-bundle.js" ]);
-//         } else {
-//             SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix + "scripts/", javascriptFiles);
-//         }
-//         SimileAjax.includeCssFiles(document, SimileAjax.urlPrefix + "styles/", cssFiles);
-
-        SimileAjax.loaded = true;
-    })();
-}
-/*
- *  Platform Utility Functions and Constants
- *
- */
-
-/*  This must be called after our jQuery has been loaded
-    but before control returns to user-code.
-*/
-SimileAjax.jQuery = jQuery;
-// SimileAjax.jQuery = jQuery.noConflict(true);
-if (typeof window["$"] == "undefined") {
-    window.$ = SimileAjax.jQuery;
-}
-
-SimileAjax.Platform.os = {
-    isMac:   false,
-    isWin:   false,
-    isWin32: false,
-    isUnix:  false
-};
-SimileAjax.Platform.browser = {
-    isIE:           false,
-    isNetscape:     false,
-    isMozilla:      false,
-    isFirefox:      false,
-    isOpera:        false,
-    isSafari:       false,
-
-    majorVersion:   0,
-    minorVersion:   0
-};
-
-(function() {
-    var an = navigator.appName.toLowerCase();
-	var ua = navigator.userAgent.toLowerCase();
-
-    /*
-     *  Operating system
-     */
-	SimileAjax.Platform.os.isMac = (ua.indexOf('mac') != -1);
-	SimileAjax.Platform.os.isWin = (ua.indexOf('win') != -1);
-	SimileAjax.Platform.os.isWin32 = SimileAjax.Platform.isWin && (
-        ua.indexOf('95') != -1 ||
-        ua.indexOf('98') != -1 ||
-        ua.indexOf('nt') != -1 ||
-        ua.indexOf('win32') != -1 ||
-        ua.indexOf('32bit') != -1
-    );
-	SimileAjax.Platform.os.isUnix = (ua.indexOf('x11') != -1);
-
-    /*
-     *  Browser
-     */
-    SimileAjax.Platform.browser.isIE = (an.indexOf("microsoft") != -1);
-    SimileAjax.Platform.browser.isNetscape = (an.indexOf("netscape") != -1);
-    SimileAjax.Platform.browser.isMozilla = (ua.indexOf("mozilla") != -1);
-    SimileAjax.Platform.browser.isFirefox = (ua.indexOf("firefox") != -1);
-    SimileAjax.Platform.browser.isOpera = (an.indexOf("opera") != -1);
-    SimileAjax.Platform.browser.isSafari = (an.indexOf("safari") != -1);
-
-    var parseVersionString = function(s) {
-        var a = s.split(".");
-        SimileAjax.Platform.browser.majorVersion = parseInt(a[0]);
-        SimileAjax.Platform.browser.minorVersion = parseInt(a[1]);
-    };
-    var indexOf = function(s, sub, start) {
-        var i = s.indexOf(sub, start);
-        return i >= 0 ? i : s.length;
-    };
-
-    if (SimileAjax.Platform.browser.isMozilla) {
-        var offset = ua.indexOf("mozilla/");
-        if (offset >= 0) {
-            parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
-        }
-    }
-    if (SimileAjax.Platform.browser.isIE) {
-        var offset = ua.indexOf("msie ");
-        if (offset >= 0) {
-            parseVersionString(ua.substring(offset + 5, indexOf(ua, ";", offset)));
-        }
-    }
-    if (SimileAjax.Platform.browser.isNetscape) {
-        var offset = ua.indexOf("rv:");
-        if (offset >= 0) {
-            parseVersionString(ua.substring(offset + 3, indexOf(ua, ")", offset)));
-        }
-    }
-    if (SimileAjax.Platform.browser.isFirefox) {
-        var offset = ua.indexOf("firefox/");
-        if (offset >= 0) {
-            parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
-        }
-    }
-
-    if (!("localeCompare" in String.prototype)) {
-        String.prototype.localeCompare = function (s) {
-            if (this < s) return -1;
-            else if (this > s) return 1;
-            else return 0;
-        };
-    }
-})();
-
-SimileAjax.Platform.getDefaultLocale = function() {
-    return SimileAjax.Platform.clientLocale;
-};
-/*
- *  Debug Utility Functions
- *
- */
-
-SimileAjax.Debug = {
-    silent: false
-};
-
-SimileAjax.Debug.log = function(msg) {
-    var f;
-    if ("console" in window && "log" in window.console) { // FireBug installed
-        f = function(msg2) {
-            console.log(msg2);
-        }
-    } else {
-        f = function(msg2) {
-            if (!SimileAjax.Debug.silent) {
-                alert(msg2);
-            }
-        }
-    }
-    SimileAjax.Debug.log = f;
-    f(msg);
-};
-
-SimileAjax.Debug.warn = function(msg) {
-    var f;
-    if ("console" in window && "warn" in window.console) { // FireBug installed
-        f = function(msg2) {
-            console.warn(msg2);
-        }
-    } else {
-        f = function(msg2) {
-            if (!SimileAjax.Debug.silent) {
-                alert(msg2);
-            }
-        }
-    }
-    SimileAjax.Debug.warn = f;
-    f(msg);
-};
-
-SimileAjax.Debug.exception = function(e, msg) {
-    var f, params = SimileAjax.parseURLParameters();
-    if (params.errors == "throw" || SimileAjax.params.errors == "throw") {
-        f = function(e2, msg2) {
-            throw(e2); // do not hide from browser's native debugging features
-        };
-    } else if ("console" in window && "error" in window.console) { // FireBug installed
-        f = function(e2, msg2) {
-            if (msg2 != null) {
-                console.error(msg2 + " %o", e2);
-            } else {
-                console.error(e2);
-            }
-            throw(e2); // do not hide from browser's native debugging features
-        };
-    } else {
-        f = function(e2, msg2) {
-            if (!SimileAjax.Debug.silent) {
-                alert("Caught exception: " + msg2 + "\n\nDetails: " + ("description" in e2 ? e2.description : e2));
-            }
-            throw(e2); // do not hide from browser's native debugging features
-        };
-    }
-    SimileAjax.Debug.exception = f;
-    f(e, msg);
-};
-
-SimileAjax.Debug.objectToString = function(o) {
-    return SimileAjax.Debug._objectToString(o, "");
-};
-
-SimileAjax.Debug._objectToString = function(o, indent) {
-    var indent2 = indent + " ";
-    if (typeof o == "object") {
-        var s = "{";
-        for (n in o) {
-            s += indent2 + n + ": " + SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
-        }
-        s += indent + "}";
-        return s;
-    } else if (typeof o == "array") {
-        var s = "[";
-        for (var n = 0; n < o.length; n++) {
-            s += SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
-        }
-        s += indent + "]";
-        return s;
-    } else {
-        return o;
-    }
-};
-/**
- * @fileOverview XmlHttp utility functions
- * @name SimileAjax.XmlHttp
- */
-
-SimileAjax.XmlHttp = new Object();
-
-/**
- *  Callback for XMLHttp onRequestStateChange.
- */
-SimileAjax.XmlHttp._onReadyStateChange = function(xmlhttp, fError, fDone) {
-    switch (xmlhttp.readyState) {
-    // 1: Request not yet made
-    // 2: Contact established with server but nothing downloaded yet
-    // 3: Called multiple while downloading in progress
-
-    // Download complete
-    case 4:
-        try {
-            if (xmlhttp.status == 0     // file:// urls, works on Firefox
-             || xmlhttp.status == 200   // http:// urls
-            ) {
-                if (fDone) {
-                    fDone(xmlhttp);
-                }
-            } else {
-                if (fError) {
-                    fError(
-                        xmlhttp.statusText,
-                        xmlhttp.status,
-                        xmlhttp
-                    );
-                }
-            }
-        } catch (e) {
-            SimileAjax.Debug.exception("XmlHttp: Error handling onReadyStateChange", e);
-        }
-        break;
-    }
-};
-
-/**
- *  Creates an XMLHttpRequest object. On the first run, this
- *  function creates a platform-specific function for
- *  instantiating an XMLHttpRequest object and then replaces
- *  itself with that function.
- */
-SimileAjax.XmlHttp._createRequest = function() {
-    if (SimileAjax.Platform.browser.isIE) {
-        var programIDs = [
-        "Msxml2.XMLHTTP",
-        "Microsoft.XMLHTTP",
-        "Msxml2.XMLHTTP.4.0"
-        ];
-        for (var i = 0; i < programIDs.length; i++) {
-            try {
-                var programID = programIDs[i];
-                var f = function() {
-                    return new ActiveXObject(programID);
-                };
-                var o = f();
-
-                // We are replacing the SimileAjax._createXmlHttpRequest
-                // function with this inner function as we've
-                // found out that it works. This is so that we
-                // don't have to do all the testing over again
-                // on subsequent calls.
-                SimileAjax.XmlHttp._createRequest = f;
-
-                return o;
-            } catch (e) {
-                // silent
-            }
-        }
-        // fall through to try new XMLHttpRequest();
-    }
-
-    try {
-        var f = function() {
-            return new XMLHttpRequest();
-        };
-        var o = f();
-
-        // We are replacing the SimileAjax._createXmlHttpRequest
-        // function with this inner function as we've
-        // found out that it works. This is so that we
-        // don't have to do all the testing over again
-        // on subsequent calls.
-        SimileAjax.XmlHttp._createRequest = f;
-
-        return o;
-    } catch (e) {
-        throw new Error("Failed to create an XMLHttpRequest object");
-    }
-};
-
-/**
- * Performs an asynchronous HTTP GET.
- *
- * @param {Function} fError a function of the form
-     function(statusText, statusCode, xmlhttp)
- * @param {Function} fDone a function of the form function(xmlhttp)
- */
-SimileAjax.XmlHttp.get = function(url, fError, fDone) {
-    var xmlhttp = SimileAjax.XmlHttp._createRequest();
-
-    xmlhttp.open("GET", url, true);
-    xmlhttp.onreadystatechange = function() {
-        SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
-    };
-    xmlhttp.send(null);
-};
-
-/**
- * Performs an asynchronous HTTP POST.
- *
- * @param {Function} fError a function of the form
-     function(statusText, statusCode, xmlhttp)
- * @param {Function} fDone a function of the form function(xmlhttp)
- */
-SimileAjax.XmlHttp.post = function(url, body, fError, fDone) {
-    var xmlhttp = SimileAjax.XmlHttp._createRequest();
-
-    xmlhttp.open("POST", url, true);
-    xmlhttp.onreadystatechange = function() {
-        SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
-    };
-    xmlhttp.send(body);
-};
-
-SimileAjax.XmlHttp._forceXML = function(xmlhttp) {
-    try {
-        xmlhttp.overrideMimeType("text/xml");
-    } catch (e) {
-        xmlhttp.setrequestheader("Content-Type", "text/xml");
-    }
-};/*
- *  Copied directly from http://www.json.org/json.js.
- */
-
-/*
-    json.js
-    2006-04-28
-
-    This file adds these methods to JavaScript:
-
-        object.toJSONString()
-
-            This method produces a JSON text from an object. The
-            object must not contain any cyclical references.
-
-        array.toJSONString()
-
-            This method produces a JSON text from an array. The
-            array must not contain any cyclical references.
-
-        string.parseJSON()
-
-            This method parses a JSON text to produce an object or
-            array. It will return false if there is an error.
-*/
-
-SimileAjax.JSON = new Object();
-
-(function () {
-    var m = {
-        '\b': '\\b',
-        '\t': '\\t',
-        '\n': '\\n',
-        '\f': '\\f',
-        '\r': '\\r',
-        '"' : '\\"',
-        '\\': '\\\\'
-    };
-    var s = {
-        array: function (x) {
-            var a = ['['], b, f, i, l = x.length, v;
-            for (i = 0; i < l; i += 1) {
-                v = x[i];
-                f = s[typeof v];
-                if (f) {
-                    v = f(v);
-                    if (typeof v == 'string') {
-                        if (b) {
-                            a[a.length] = ',';
-                        }
-                        a[a.length] = v;
-                        b = true;
-                    }
-                }
-            }
-            a[a.length] = ']';
-            return a.join('');
-        },
-        'boolean': function (x) {
-            return String(x);
-        },
-        'null': function (x) {
-            return "null";
-        },
-        number: function (x) {
-            return isFinite(x) ? String(x) : 'null';
-        },
-        object: function (x) {
-            if (x) {
-                if (x instanceof Array) {
-                    return s.array(x);
-                }
-                var a = ['{'], b, f, i, v;
-                for (i in x) {
-                    v = x[i];
-                    f = s[typeof v];
-                    if (f) {
-                        v = f(v);
-                        if (typeof v == 'string') {
-                            if (b) {
-                                a[a.length] = ',';
-                            }
-                            a.push(s.string(i), ':', v);
-                            b = true;
-                        }
-                    }
-                }
-                a[a.length] = '}';
-                return a.join('');
-            }
-            return 'null';
-        },
-        string: function (x) {
-            if (/["\\\x00-\x1f]/.test(x)) {
-                x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
-                    var c = m[b];
-                    if (c) {
-                        return c;
-                    }
-                    c = b.charCodeAt();
-                    return '\\u00' +
-                        Math.floor(c / 16).toString(16) +
-                        (c % 16).toString(16);
-                });
-            }
-            return '"' + x + '"';
-        }
-    };
-
-    SimileAjax.JSON.toJSONString = function(o) {
-        if (o instanceof Object) {
-            return s.object(o);
-        } else if (o instanceof Array) {
-            return s.array(o);
-        } else {
-            return o.toString();
-        }
-    };
-
-    SimileAjax.JSON.parseJSON = function () {
-        try {
-            return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
-                    this.replace(/"(\\.|[^"\\])*"/g, ''))) &&
-                eval('(' + this + ')');
-        } catch (e) {
-            return false;
-        }
-    };
-})();
-/*
- *  DOM Utility Functions
- *
- */
-
-SimileAjax.DOM = new Object();
-
-SimileAjax.DOM.registerEventWithObject = function(elmt, eventName, obj, handlerName) {
-    SimileAjax.DOM.registerEvent(elmt, eventName, function(elmt2, evt, target) {
-        return obj[handlerName].call(obj, elmt2, evt, target);
-    });
-};
-
-SimileAjax.DOM.registerEvent = function(elmt, eventName, handler) {
-    var handler2 = function(evt) {
-        evt = (evt) ? evt : ((event) ? event : null);
-        if (evt) {
-            var target = (evt.target) ?
-                evt.target : ((evt.srcElement) ? evt.srcElement : null);
-            if (target) {
-                target = (target.nodeType == 1 || target.nodeType == 9) ?
-                    target : target.parentNode;
-            }
-
-            return handler(elmt, evt, target);
-        }
-        return true;
-    }
-
-    if (SimileAjax.Platform.browser.isIE) {
-        elmt.attachEvent("on" + eventName, handler2);
-    } else {
-        elmt.addEventListener(eventName, handler2, false);
-    }
-};
-
-SimileAjax.DOM.getPageCoordinates = function(elmt) {
-    var left = 0;
-    var top = 0;
-
-    if (elmt.nodeType != 1) {
-        elmt = elmt.parentNode;
-    }
-
-    var elmt2 = elmt;
-    while (elmt2 != null) {
-        left += elmt2.offsetLeft;
-        top += elmt2.offsetTop;
-        elmt2 = elmt2.offsetParent;
-    }
-
-    var body = document.body;
-    while (elmt != null && elmt != body) {
-        if ("scrollLeft" in elmt) {
-            left -= elmt.scrollLeft;
-            top -= elmt.scrollTop;
-        }
-        elmt = elmt.parentNode;
-    }
-
-    return { left: left, top: top };
-};
-
-SimileAjax.DOM.getSize = function(elmt) {
-	var w = this.getStyle(elmt,"width");
-	var h = this.getStyle(elmt,"height");
-	if (w.indexOf("px") > -1) w = w.replace("px","");
-	if (h.indexOf("px") > -1) h = h.replace("px","");
-	return {
-		w: w,
-		h: h
-	}
-}
-
-SimileAjax.DOM.getStyle = function(elmt, styleProp) {
-    if (elmt.currentStyle) { // IE
-        var style = elmt.currentStyle[styleProp];
-    } else if (window.getComputedStyle) { // standard DOM
-        var style = document.defaultView.getComputedStyle(elmt, null).getPropertyValue(styleProp);
-    } else {
-    	var style = "";
-    }
-    return style;
-}
-
-SimileAjax.DOM.getEventRelativeCoordinates = function(evt, elmt) {
-    if (SimileAjax.Platform.browser.isIE) {
-      if (evt.type == "mousewheel") {
-        var coords = SimileAjax.DOM.getPageCoordinates(elmt);
-        return {
-          x: evt.clientX - coords.left,
-          y: evt.clientY - coords.top
-        };
-      } else {
-        return {
-          x: evt.offsetX,
-          y: evt.offsetY
-        };
-      }
-    } else {
-        var coords = SimileAjax.DOM.getPageCoordinates(elmt);
-
-        if ((evt.type == "DOMMouseScroll") &&
-          SimileAjax.Platform.browser.isFirefox &&
-          (SimileAjax.Platform.browser.majorVersion == 2)) {
-          // Due to: https://bugzilla.mozilla.org/show_bug.cgi?id=352179
-
-          return {
-            x: evt.screenX - coords.left,
-            y: evt.screenY - coords.top
-          };
-        } else {
-          return {
-              x: evt.pageX - coords.left,
-              y: evt.pageY - coords.top
-          };
-        }
-    }
-};
-
-SimileAjax.DOM.getEventPageCoordinates = function(evt) {
-    if (SimileAjax.Platform.browser.isIE) {
-        return {
-            x: evt.clientX + document.body.scrollLeft,
-            y: evt.clientY + document.body.scrollTop
-        };
-    } else {
-        return {
-            x: evt.pageX,
-            y: evt.pageY
-        };
-    }
-};
-
-SimileAjax.DOM.hittest = function(x, y, except) {
-    return SimileAjax.DOM._hittest(document.body, x, y, except);
-};
-
-SimileAjax.DOM._hittest = function(elmt, x, y, except) {
-    var childNodes = elmt.childNodes;
-    outer: for (var i = 0; i < childNodes.length; i++) {
-        var childNode = childNodes[i];
-        for (var j = 0; j < except.length; j++) {
-            if (childNode == except[j]) {
-                continue outer;
-            }
-        }
-
-        if (childNode.offsetWidth == 0 && childNode.offsetHeight == 0) {
-            /*
-             *  Sometimes SPAN elements have zero width and height but
-             *  they have children like DIVs that cover non-zero areas.
-             */
-            var hitNode = SimileAjax.DOM._hittest(childNode, x, y, except);
-            if (hitNode != childNode) {
-                return hitNode;
-            }
-        } else {
-            var top = 0;
-            var left = 0;
-
-            var node = childNode;
-            while (node) {
-                top += node.offsetTop;
-                left += node.offsetLeft;
-                node = node.offsetParent;
-            }
-
-            if (left <= x && top <= y && (x - left) < childNode.offsetWidth && (y - top) < childNode.offsetHeight) {
-                return SimileAjax.DOM._hittest(childNode, x, y, except);
-            } else if (childNode.nodeType == 1 && childNode.tagName == "TR") {
-                /*
-                 *  Table row might have cells that span several rows.
-                 */
-                var childNode2 = SimileAjax.DOM._hittest(childNode, x, y, except);
-                if (childNode2 != childNode) {
-                    return childNode2;
-                }
-            }
-        }
-    }
-    return elmt;
-};
-
-SimileAjax.DOM.cancelEvent = function(evt) {
-    evt.returnValue = false;
-    evt.cancelBubble = true;
-    if ("preventDefault" in evt) {
-        evt.preventDefault();
-    }
-};
-
-SimileAjax.DOM.appendClassName = function(elmt, className) {
-    var classes = elmt.className.split(" ");
-    for (var i = 0; i < classes.length; i++) {
-        if (classes[i] == className) {
-            return;
-        }
-    }
-    classes.push(className);
-    elmt.className = classes.join(" ");
-};
-
-SimileAjax.DOM.createInputElement = function(type) {
-    var div = document.createElement("div");
-    div.innerHTML = "<input type='" + type + "' />";
-
-    return div.firstChild;
-};
-
-SimileAjax.DOM.createDOMFromTemplate = function(template) {
-    var result = {};
-    result.elmt = SimileAjax.DOM._createDOMFromTemplate(template, result, null);
-
-    return result;
-};
-
-SimileAjax.DOM._createDOMFromTemplate = function(templateNode, result, parentElmt) {
-    if (templateNode == null) {
-        /*
-        var node = doc.createTextNode("--null--");
-        if (parentElmt != null) {
-            parentElmt.appendChild(node);
-        }
-        return node;
-        */
-        return null;
-    } else if (typeof templateNode != "object") {
-        var node = document.createTextNode(templateNode);
-        if (parentElmt != null) {
-            parentElmt.appendChild(node);
-        }
-        return node;
-    } else {
-        var elmt = null;
-        if ("tag" in templateNode) {
-            var tag = templateNode.tag;
-            if (parentElmt != null) {
-                if (tag == "tr") {
-                    elmt = parentElmt.insertRow(parentElmt.rows.length);
-                } else if (tag == "td") {
-                    elmt = parentElmt.insertCell(parentElmt.cells.length);
-                }
-            }
-            if (elmt == null) {
-                elmt = tag == "input" ?
-                    SimileAjax.DOM.createInputElement(templateNode.type) :
-                    document.createElement(tag);
-
-                if (parentElmt != null) {
-                    parentElmt.appendChild(elmt);
-                }
-            }
-        } else {
-            elmt = templateNode.elmt;
-            if (parentElmt != null) {
-                parentElmt.appendChild(elmt);
-            }
-        }
-
-        for (var attribute in templateNode) {
-            var value = templateNode[attribute];
-
-            if (attribute == "field") {
-                result[value] = elmt;
-
-            } else if (attribute == "className") {
-                elmt.className = value;
-            } else if (attribute == "id") {
-                elmt.id = value;
-            } else if (attribute == "title") {
-                elmt.title = value;
-            } else if (attribute == "type" && elmt.tagName == "input") {
-                // do nothing
-            } else if (attribute == "style") {
-                for (n in value) {
-                    var v = value[n];
-                    if (n == "float") {
-                        n = SimileAjax.Platform.browser.isIE ? "styleFloat" : "cssFloat";
-                    }
-                    elmt.style[n] = v;
-                }
-            } else if (attribute == "children") {
-                for (var i = 0; i < value.length; i++) {
-                    SimileAjax.DOM._createDOMFromTemplate(value[i], result, elmt);
-                }
-            } else if (attribute != "tag" && attribute != "elmt") {
-                elmt.setAttribute(attribute, value);
-            }
-        }
-        return elmt;
-    }
-}
-
-SimileAjax.DOM._cachedParent = null;
-SimileAjax.DOM.createElementFromString = function(s) {
-    if (SimileAjax.DOM._cachedParent == null) {
-        SimileAjax.DOM._cachedParent = document.createElement("div");
-    }
-    SimileAjax.DOM._cachedParent.innerHTML = s;
-    return SimileAjax.DOM._cachedParent.firstChild;
-};
-
-SimileAjax.DOM.createDOMFromString = function(root, s, fieldElmts) {
-    var elmt = typeof root == "string" ? document.createElement(root) : root;
-    elmt.innerHTML = s;
-
-    var dom = { elmt: elmt };
-    SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts != null ? fieldElmts : {} );
-
-    return dom;
-};
-
-SimileAjax.DOM._processDOMConstructedFromString = function(dom, elmt, fieldElmts) {
-    var id = elmt.id;
-    if (id != null && id.length > 0) {
-        elmt.removeAttribute("id");
-        if (id in fieldElmts) {
-            var parentElmt = elmt.parentNode;
-            parentElmt.insertBefore(fieldElmts[id], elmt);
-            parentElmt.removeChild(elmt);
-
-            dom[id] = fieldElmts[id];
-            return;
-        } else {
-            dom[id] = elmt;
-        }
-    }
-
-    if (elmt.hasChildNodes()) {
-        SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts);
-    }
-};
-
-SimileAjax.DOM._processDOMChildrenConstructedFromString = function(dom, elmt, fieldElmts) {
-    var node = elmt.firstChild;
-    while (node != null) {
-        var node2 = node.nextSibling;
-        if (node.nodeType == 1) {
-            SimileAjax.DOM._processDOMConstructedFromString(dom, node, fieldElmts);
-        }
-        node = node2;
-    }
-};
-/**
- * @fileOverview Graphics utility functions and constants
- * @name SimileAjax.Graphics
- */
-
-SimileAjax.Graphics = new Object();
-
-/**
- * A boolean value indicating whether PNG translucency is supported on the
- * user's browser or not.
- *
- * @type Boolean
- */
-SimileAjax.Graphics.pngIsTranslucent = (!SimileAjax.Platform.browser.isIE) || (SimileAjax.Platform.browser.majorVersion > 6);
-if (!SimileAjax.Graphics.pngIsTranslucent) {
-    SimileAjax.includeCssFile(document, SimileAjax.urlPrefix + "styles/graphics-ie6.css");
-}
-
-/*
- *  Opacity, translucency
- *
- */
-SimileAjax.Graphics._createTranslucentImage1 = function(url, verticalAlign) {
-    var elmt = document.createElement("img");
-    elmt.setAttribute("src", url);
-    if (verticalAlign != null) {
-        elmt.style.verticalAlign = verticalAlign;
-    }
-    return elmt;
-};
-SimileAjax.Graphics._createTranslucentImage2 = function(url, verticalAlign) {
-    var elmt = document.createElement("img");
-    elmt.style.width = "1px";  // just so that IE will calculate the size property
-    elmt.style.height = "1px";
-    elmt.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image')";
-    elmt.style.verticalAlign = (verticalAlign != null) ? verticalAlign : "middle";
-    return elmt;
-};
-
-/**
- * Creates a DOM element for an <code>img</code> tag using the URL given. This
- * is a convenience method that automatically includes the necessary CSS to
- * allow for translucency, even on IE.
- *
- * @function
- * @param {String} url the URL to the image
- * @param {String} verticalAlign the CSS value for the image's vertical-align
- * @return {Element} a DOM element containing the <code>img</code> tag
- */
-SimileAjax.Graphics.createTranslucentImage = SimileAjax.Graphics.pngIsTranslucent ?
-    SimileAjax.Graphics._createTranslucentImage1 :
-    SimileAjax.Graphics._createTranslucentImage2;
-
-SimileAjax.Graphics._createTranslucentImageHTML1 = function(url, verticalAlign) {
-    return "<img src=\"" + url + "\"" +
-        (verticalAlign != null ? " style=\"vertical-align: " + verticalAlign + ";\"" : "") +
-        " />";
-};
-SimileAjax.Graphics._createTranslucentImageHTML2 = function(url, verticalAlign) {
-    var style =
-        "width: 1px; height: 1px; " +
-        "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image');" +
-        (verticalAlign != null ? " vertical-align: " + verticalAlign + ";" : "");
-
-    return "<img src='" + url + "' style=\"" + style + "\" />";
-};
-
-/**
- * Creates an HTML string for an <code>img</code> tag using the URL given.
- * This is a convenience method that automatically includes the necessary CSS
- * to allow for translucency, even on IE.
- *
- * @function
- * @param {String} url the URL to the image
- * @param {String} verticalAlign the CSS value for the image's vertical-align
- * @return {String} a string containing the <code>img</code> tag
- */
-SimileAjax.Graphics.createTranslucentImageHTML = SimileAjax.Graphics.pngIsTranslucent ?
-    SimileAjax.Graphics._createTranslucentImageHTML1 :
-    SimileAjax.Graphics._createTranslucentImageHTML2;
-
-/**
- * Sets the opacity on the given DOM element.
- *
- * @param {Element} elmt the DOM element to set the opacity on
- * @param {Number} opacity an integer from 0 to 100 specifying the opacity
- */
-SimileAjax.Graphics.setOpacity = function(elmt, opacity) {
-    if (SimileAjax.Platform.browser.isIE) {
-        elmt.style.filter = "progid:DXImageTransform.Microsoft.Alpha(Style=0,Opacity=" + opacity + ")";
-    } else {
-        var o = (opacity / 100).toString();
-        elmt.style.opacity = o;
-        elmt.style.MozOpacity = o;
-    }
-};
-
-/*
- *  Bubble
- *
- */
-
-SimileAjax.Graphics.bubbleConfig = {
-    containerCSSClass:              "simileAjax-bubble-container",
-    innerContainerCSSClass:         "simileAjax-bubble-innerContainer",
-    contentContainerCSSClass:       "simileAjax-bubble-contentContainer",
-
-    borderGraphicSize:              50,
-    borderGraphicCSSClassPrefix:    "simileAjax-bubble-border-",
-
-    arrowGraphicTargetOffset:       33,  // from tip of arrow to the side of the graphic that touches the content of the bubble
-    arrowGraphicLength:             100, // dimension of arrow graphic along the direction that the arrow points
-    arrowGraphicWidth:              49,  // dimension of arrow graphic perpendicular to the direction that the arrow points
-    arrowGraphicCSSClassPrefix:     "simileAjax-bubble-arrow-",
-
-    closeGraphicCSSClass:           "simileAjax-bubble-close",
-
-    extraPadding:                   20
-};
-
-/**
- * Creates a nice, rounded bubble popup with the given content in a div,
- * page coordinates and a suggested width. The bubble will point to the
- * location on the page as described by pageX and pageY.  All measurements
- * should be given in pixels.
- *
- * @param {Element} the content div
- * @param {Number} pageX the x coordinate of the point to point to
- * @param {Number} pageY the y coordinate of the point to point to
- * @param {Number} contentWidth a suggested width of the content
- * @param {String} orientation a string ("top", "bottom", "left", or "right")
- *   that describes the orientation of the arrow on the bubble
- * @param {Number} maxHeight. Add a scrollbar div if bubble would be too tall.
- *   Default of 0 or null means no maximum
- */
-SimileAjax.Graphics.createBubbleForContentAndPoint = function(
-       div, pageX, pageY, contentWidth, orientation, maxHeight) {
-    if (typeof contentWidth != "number") {
-        contentWidth = 300;
-    }
-    if (typeof maxHeight != "number") {
-        maxHeight = 0;
-    }
-
-    div.style.position = "absolute";
-    div.style.left = "-5000px";
-    div.style.top = "0px";
-    div.style.width = contentWidth + "px";
-    document.body.appendChild(div);
-
-    window.setTimeout(function() {
-        var width = div.scrollWidth + 10;
-        var height = div.scrollHeight + 10;
-        var scrollDivW = 0; // width of the possible inner container when we want vertical scrolling
-        if (maxHeight > 0 && height > maxHeight) {
-          height = maxHeight;
-          scrollDivW = width - 25;
-        }
-
-        var bubble = SimileAjax.Graphics.createBubbleForPoint(pageX, pageY, width, height, orientation);
-
-        document.body.removeChild(div);
-        div.style.position = "static";
-        div.style.left = "";
-        div.style.top = "";
-
-        // create a scroll div if needed
-        if (scrollDivW > 0) {
-          var scrollDiv = document.createElement("div");
-          div.style.width = "";
-          scrollDiv.style.width = scrollDivW + "px";
-          scrollDiv.appendChild(div);
-          bubble.content.appendChild(scrollDiv);
-        } else {
-          div.style.width = width + "px";
-          bubble.content.appendChild(div);
-        }
-    }, 200);
-};
-
-/**
- * Creates a nice, rounded bubble popup with the given page coordinates and
- * content dimensions.  The bubble will point to the location on the page
- * as described by pageX and pageY.  All measurements should be given in
- * pixels.
- *
- * @param {Number} pageX the x coordinate of the point to point to
- * @param {Number} pageY the y coordinate of the point to point to
- * @param {Number} contentWidth the width of the content box in the bubble
- * @param {Number} contentHeight the height of the content box in the bubble
- * @param {String} orientation a string ("top", "bottom", "left", or "right")
- *   that describes the orientation of the arrow on the bubble
- * @return {Element} a DOM element for the newly created bubble
- */
-SimileAjax.Graphics.createBubbleForPoint = function(pageX, pageY, contentWidth, contentHeight, orientation) {
-    contentWidth = parseInt(contentWidth, 10); // harden against bad input bugs
-    contentHeight = parseInt(contentHeight, 10); // getting numbers-as-strings
-
-    var bubbleConfig = SimileAjax.Graphics.bubbleConfig;
-    var pngTransparencyClassSuffix =
-        SimileAjax.Graphics.pngIsTranslucent ? "pngTranslucent" : "pngNotTranslucent";
-
-    var bubbleWidth = contentWidth + 2 * bubbleConfig.borderGraphicSize;
-    var bubbleHeight = contentHeight + 2 * bubbleConfig.borderGraphicSize;
-
-    var generatePngSensitiveClass = function(className) {
-        return className + " " + className + "-" + pngTransparencyClassSuffix;
-    };
-
-    /*
-     *  Render container divs
-     */
-    var div = document.createElement("div");
-    div.className = generatePngSensitiveClass(bubbleConfig.containerCSSClass);
-    div.style.width = contentWidth + "px";
-    div.style.height = contentHeight + "px";
-
-    var divInnerContainer = document.createElement("div");
-    divInnerContainer.className = generatePngSensitiveClass(bubbleConfig.innerContainerCSSClass);
-    div.appendChild(divInnerContainer);
-
-    /*
-     *  Create layer for bubble
-     */
-    var close = function() {
-        if (!bubble._closed) {
-            document.body.removeChild(bubble._div);
-            bubble._doc = null;
-            bubble._div = null;
-            bubble._content = null;
-            bubble._closed = true;
-        }
-    }
-    var bubble = { _closed: false };
-    var layer = SimileAjax.WindowManager.pushLayer(close, true, div);
-    bubble._div = div;
-    bubble.close = function() { SimileAjax.WindowManager.popLayer(layer); }
-
-    /*
-     *  Render border graphics
-     */
-    var createBorder = function(classNameSuffix) {
-        var divBorderGraphic = document.createElement("div");
-        divBorderGraphic.className = generatePngSensitiveClass(bubbleConfig.borderGraphicCSSClassPrefix + classNameSuffix);
-        divInnerContainer.appendChild(divBorderGraphic);
-    };
-    createBorder("top-left");
-    createBorder("top-right");
-    createBorder("bottom-left");
-    createBorder("bottom-right");
-    createBorder("left");
-    createBorder("right");
-    createBorder("top");
-    createBorder("bottom");
-
-    /*
-     *  Render content
-     */
-    var divContentContainer = document.createElement("div");
-    divContentContainer.className = generatePngSensitiveClass(bubbleConfig.contentContainerCSSClass);
-    divInnerContainer.appendChild(divContentContainer);
-    bubble.content = divContentContainer;
-
-    /*
-     *  Render close button
-     */
-    var divClose = document.createElement("div");
-    divClose.className = generatePngSensitiveClass(bubbleConfig.closeGraphicCSSClass);
-    divInnerContainer.appendChild(divClose);
-    SimileAjax.WindowManager.registerEventWithObject(divClose, "click", bubble, "close");
-
-    (function() {
-        var dims = SimileAjax.Graphics.getWindowDimensions();
-        var docWidth = dims.w;
-        var docHeight = dims.h;
-
-        var halfArrowGraphicWidth = Math.ceil(bubbleConfig.arrowGraphicWidth / 2);
-
-        var createArrow = function(classNameSuffix) {
-            var divArrowGraphic = document.createElement("div");
-            divArrowGraphic.className = generatePngSensitiveClass(bubbleConfig.arrowGraphicCSSClassPrefix + "point-" + classNameSuffix);
-            divInnerContainer.appendChild(divArrowGraphic);
-            return divArrowGraphic;
-        };
-
-        if (pageX - halfArrowGraphicWidth - bubbleConfig.borderGraphicSize - bubbleConfig.extraPadding > 0 &&
-            pageX + halfArrowGraphicWidth + bubbleConfig.borderGraphicSize + bubbleConfig.extraPadding < docWidth) {
-
-            /*
-             *  Bubble can be positioned above or below the target point.
-             */
-
-            var left = pageX - Math.round(contentWidth / 2);
-            left = pageX < (docWidth / 2) ?
-                Math.max(left, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) :
-                Math.min(left, docWidth - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentWidth);
-
-            if ((orientation && orientation == "top") ||
-                (!orientation &&
-                    (pageY
-                        - bubbleConfig.arrowGraphicTargetOffset
-                        - contentHeight
-                        - bubbleConfig.borderGraphicSize
-                        - bubbleConfig.extraPadding > 0))) {
-
-                /*
-                 *  Position bubble above the target point.
-                 */
-
-                var divArrow = createArrow("down");
-                divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
-
-                div.style.left = left + "px";
-                div.style.top = (pageY - bubbleConfig.arrowGraphicTargetOffset - contentHeight) + "px";
-
-                return;
-            } else if ((orientation && orientation == "bottom") ||
-                (!orientation &&
-                    (pageY
-                        + bubbleConfig.arrowGraphicTargetOffset
-                        + contentHeight
-                        + bubbleConfig.borderGraphicSize
-                        + bubbleConfig.extraPadding < docHeight))) {
-
-                /*
-                 *  Position bubble below the target point.
-                 */
-
-                var divArrow = createArrow("up");
-                divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
-
-                div.style.left = left + "px";
-                div.style.top = (pageY + bubbleConfig.arrowGraphicTargetOffset) + "px";
-
-                return;
-            }
-        }
-
-        var top = pageY - Math.round(contentHeight / 2);
-        top = pageY < (docHeight / 2) ?
-            Math.max(top, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) :
-            Math.min(top, docHeight - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentHeight);
-
-        if ((orientation && orientation == "left") ||
-            (!orientation &&
-                (pageX
-                    - bubbleConfig.arrowGraphicTargetOffset
-                    - contentWidth
-                    - bubbleConfig.borderGraphicSize
-                    - bubbleConfig.extraPadding > 0))) {
-
-            /*
-             *  Position bubble left of the target point.
-             */
-
-            var divArrow = createArrow("right");
-            divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
-
-            div.style.top = top + "px";
-            div.style.left = (pageX - bubbleConfig.arrowGraphicTargetOffset - contentWidth) + "px";
-        } else {
-
-            /*
-             *  Position bubble right of the target point, as the last resort.
-             */
-
-            var divArrow = createArrow("left");
-            divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
-
-            div.style.top = top + "px";
-            div.style.left = (pageX + bubbleConfig.arrowGraphicTargetOffset) + "px";
-        }
-    })();
-
-    document.body.appendChild(div);
-
-    return bubble;
-};
-
-SimileAjax.Graphics.getWindowDimensions = function() {
-    if (typeof window.innerHeight == 'number') {
-        return { w:window.innerWidth, h:window.innerHeight }; // Non-IE
-    } else if (document.documentElement && document.documentElement.clientHeight) {
-        return { // IE6+, in "standards compliant mode"
-            w:document.documentElement.clientWidth,
-            h:document.documentElement.clientHeight
-        };
-    } else if (document.body && document.body.clientHeight) {
-        return { // IE 4 compatible
-            w:document.body.clientWidth,
-            h:document.body.clientHeight
-        };
-    }
-};
-
-
-/**
- * Creates a floating, rounded message bubble in the center of the window for
- * displaying modal information, e.g. "Loading..."
- *
- * @param {Document} doc the root document for the page to render on
- * @param {Object} an object with two properties, contentDiv and containerDiv,
- *   consisting of the newly created DOM elements
- */
-SimileAjax.Graphics.createMessageBubble = function(doc) {
-    var containerDiv = doc.createElement("div");
-    if (SimileAjax.Graphics.pngIsTranslucent) {
-        var topDiv = doc.createElement("div");
-        topDiv.style.height = "33px";
-        topDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-left.png) top left no-repeat";
-        topDiv.style.paddingLeft = "44px";
-        containerDiv.appendChild(topDiv);
-
-        var topRightDiv = doc.createElement("div");
-        topRightDiv.style.height = "33px";
-        topRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-right.png) top right no-repeat";
-        topDiv.appendChild(topRightDiv);
-
-        var middleDiv = doc.createElement("div");
-        middleDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-left.png) top left repeat-y";
-        middleDiv.style.paddingLeft = "44px";
-        containerDiv.appendChild(middleDiv);
-
-        var middleRightDiv = doc.createElement("div");
-        middleRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-right.png) top right repeat-y";
-        middleRightDiv.style.paddingRight = "44px";
-        middleDiv.appendChild(middleRightDiv);
-
-        var contentDiv = doc.createElement("div");
-        middleRightDiv.appendChild(contentDiv);
-
-        var bottomDiv = doc.createElement("div");
-        bottomDiv.style.height = "55px";
-        bottomDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-left.png) bottom left no-repeat";
-        bottomDiv.style.paddingLeft = "44px";
-        containerDiv.appendChild(bottomDiv);
-
-        var bottomRightDiv = doc.createElement("div");
-        bottomRightDiv.style.height = "55px";
-        bottomRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-right.png) bottom right no-repeat";
-        bottomDiv.appendChild(bottomRightDiv);
-    } else {
-        containerDiv.style.border = "2px solid #7777AA";
-        containerDiv.style.padding = "20px";
-        containerDiv.style.background = "white";
-        SimileAjax.Graphics.setOpacity(containerDiv, 90);
-
-        var contentDiv = doc.createElement("div");
-        containerDiv.appendChild(contentDiv);
-    }
-
-    return {
-        containerDiv:   containerDiv,
-        contentDiv:     contentDiv
-    };
-};
-
-/*
- *  Animation
- *
- */
-
-/**
- * Creates an animation for a function, and an interval of values.  The word
- * "animation" here is used in the sense of repeatedly calling a function with
- * a current value from within an interval, and a delta value.
- *
- * @param {Function} f a function to be called every 50 milliseconds throughout
- *   the animation duration, of the form f(current, delta), where current is
- *   the current value within the range and delta is the current change.
- * @param {Number} from a starting value
- * @param {Number} to an ending value
- * @param {Number} duration the duration of the animation in milliseconds
- * @param {Function} [cont] an optional function that is called at the end of
- *   the animation, i.e. a continuation.
- * @return {SimileAjax.Graphics._Animation} a new animation object
- */
-SimileAjax.Graphics.createAnimation = function(f, from, to, duration, cont) {
-    return new SimileAjax.Graphics._Animation(f, from, to, duration, cont);
-};
-
-SimileAjax.Graphics._Animation = function(f, from, to, duration, cont) {
-    this.f = f;
-    this.cont = (typeof cont == "function") ? cont : function() {};
-
-    this.from = from;
-    this.to = to;
-    this.current = from;
-
-    this.duration = duration;
-    this.start = new Date().getTime();
-    this.timePassed = 0;
-};
-
-/**
- * Runs this animation.
- */
-SimileAjax.Graphics._Animation.prototype.run = function() {
-    var a = this;
-    window.setTimeout(function() { a.step(); }, 50);
-};
-
-/**
- * Increments this animation by one step, and then continues the animation with
- * <code>run()</code>.
- */
-SimileAjax.Graphics._Animation.prototype.step = function() {
-    this.timePassed += 50;
-
-    var timePassedFraction = this.timePassed / this.duration;
-    var parameterFraction = -Math.cos(timePassedFraction * Math.PI) / 2 + 0.5;
-    var current = parameterFraction * (this.to - this.from) + this.from;
-
-    try {
-        this.f(current, current - this.current);
-    } catch (e) {
-    }
-    this.current = current;
-
-    if (this.timePassed < this.duration) {
-        this.run();
-    } else {
-        this.f(this.to, 0);
-        this["cont"]();
-    }
-};
-
-/*
- *  CopyPasteButton
- *
- *  Adapted from http://spaces.live.com/editorial/rayozzie/demo/liveclip/liveclipsample/techPreview.html.
- *
- */
-
-/**
- * Creates a button and textarea for displaying structured data and copying it
- * to the clipboard.  The data is dynamically generated by the given
- * createDataFunction parameter.
- *
- * @param {String} image an image URL to use as the background for the
- *   generated box
- * @param {Number} width the width in pixels of the generated box
- * @param {Number} height the height in pixels of the generated box
- * @param {Function} createDataFunction a function that is called with no
- *   arguments to generate the structured data
- * @return a new DOM element
- */
-SimileAjax.Graphics.createStructuredDataCopyButton = function(image, width, height, createDataFunction) {
-    var div = document.createElement("div");
-    div.style.position = "relative";
-    div.style.display = "inline";
-    div.style.width = width + "px";
-    div.style.height = height + "px";
-    div.style.overflow = "hidden";
-    div.style.margin = "2px";
-
-    if (SimileAjax.Graphics.pngIsTranslucent) {
-        div.style.background = "url(" + image + ") no-repeat";
-    } else {
-        div.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + image +"', sizingMethod='image')";
-    }
-
-    var style;
-    if (SimileAjax.Platform.browser.isIE) {
-        style = "filter:alpha(opacity=0)";
-    } else {
-        style = "opacity: 0";
-    }
-    div.innerHTML = "<textarea rows='1' autocomplete='off' value='none' style='" + style + "' />";
-
-    var textarea = div.firstChild;
-    textarea.style.width = width + "px";
-    textarea.style.height = height + "px";
-    textarea.onmousedown = function(evt) {
-        evt = (evt) ? evt : ((event) ? event : null);
-        if (evt.button == 2) {
-            textarea.value = createDataFunction();
-            textarea.select();
-        }
-    };
-
-    return div;
-};
-
-/*
- *  getWidthHeight
- *
- */
-SimileAjax.Graphics.getWidthHeight = function(el) {
-    // RETURNS hash {width:  w, height: h} in pixels
-
-    var w, h;
-    // offsetWidth rounds on FF, so doesn't work for us.
-    // See https://bugzilla.mozilla.org/show_bug.cgi?id=458617
-    if (el.getBoundingClientRect == null) {
-    	// use offsetWidth
-      w = el.offsetWidth;
-      h = el.offsetHeight;
-    } else {
-    	// use getBoundingClientRect
-      var rect = el.getBoundingClientRect();
-      w = Math.ceil(rect.right - rect.left);
-    	h = Math.ceil(rect.bottom - rect.top);
-    }
-    return {
-        width:  w,
-        height: h
-    };
-};
-
-
-/*
- *  FontRenderingContext
- *
- */
-SimileAjax.Graphics.getFontRenderingContext = function(elmt, width) {
-    return new SimileAjax.Graphics._FontRenderingContext(elmt, width);
-};
-
-SimileAjax.Graphics._FontRenderingContext = function(elmt, width) {
-    this._elmt = elmt;
-    this._elmt.style.visibility = "hidden";
-    if (typeof width == "string") {
-        this._elmt.style.width = width;
-    } else if (typeof width == "number") {
-        this._elmt.style.width = width + "px";
-    }
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.dispose = function() {
-    this._elmt = null;
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.update = function() {
-    this._elmt.innerHTML = "A";
-    this._lineHeight = this._elmt.offsetHeight;
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.computeSize = function(text, className) {
-    // className arg is optional
-    var el = this._elmt;
-    el.innerHTML = text;
-    el.className = className === undefined ? '' : className;
-    var wh = SimileAjax.Graphics.getWidthHeight(el);
-    el.className = ''; // reset for the next guy
-
-    return wh;
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.getLineHeight = function() {
-    return this._lineHeight;
-};
-
-/**
- * @fileOverview A collection of date/time utility functions
- * @name SimileAjax.DateTime
- */
-
-SimileAjax.DateTime = new Object();
-
-SimileAjax.DateTime.MILLISECOND    = 0;
-SimileAjax.DateTime.SECOND         = 1;
-SimileAjax.DateTime.MINUTE         = 2;
-SimileAjax.DateTime.HOUR           = 3;
-SimileAjax.DateTime.DAY            = 4;
-SimileAjax.DateTime.WEEK           = 5;
-SimileAjax.DateTime.MONTH          = 6;
-SimileAjax.DateTime.YEAR           = 7;
-SimileAjax.DateTime.DECADE         = 8;
-SimileAjax.DateTime.CENTURY        = 9;
-SimileAjax.DateTime.MILLENNIUM     = 10;
-
-SimileAjax.DateTime.EPOCH          = -1;
-SimileAjax.DateTime.ERA            = -2;
-
-/**
- * An array of unit lengths, expressed in milliseconds, of various lengths of
- * time.  The array indices are predefined and stored as properties of the
- * SimileAjax.DateTime object, e.g. SimileAjax.DateTime.YEAR.
- * @type Array
- */
-SimileAjax.DateTime.gregorianUnitLengths = [];
-    (function() {
-        var d = SimileAjax.DateTime;
-        var a = d.gregorianUnitLengths;
-
-        a[d.MILLISECOND] = 1;
-        a[d.SECOND]      = 1000;
-        a[d.MINUTE]      = a[d.SECOND] * 60;
-        a[d.HOUR]        = a[d.MINUTE] * 60;
-        a[d.DAY]         = a[d.HOUR] * 24;
-        a[d.WEEK]        = a[d.DAY] * 7;
-        a[d.MONTH]       = a[d.DAY] * 31;
-        a[d.YEAR]        = a[d.DAY] * 365;
-        a[d.DECADE]      = a[d.YEAR] * 10;
-        a[d.CENTURY]     = a[d.YEAR] * 100;
-        a[d.MILLENNIUM]  = a[d.YEAR] * 1000;
-    })();
-
-SimileAjax.DateTime._dateRegexp = new RegExp(
-    "^(-?)([0-9]{4})(" + [
-        "(-?([0-9]{2})(-?([0-9]{2}))?)", // -month-dayOfMonth
-        "(-?([0-9]{3}))",                // -dayOfYear
-        "(-?W([0-9]{2})(-?([1-7]))?)"    // -Wweek-dayOfWeek
-    ].join("|") + ")?$"
-);
-SimileAjax.DateTime._timezoneRegexp = new RegExp(
-    "Z|(([-+])([0-9]{2})(:?([0-9]{2}))?)$"
-);
-SimileAjax.DateTime._timeRegexp = new RegExp(
-    "^([0-9]{2})(:?([0-9]{2})(:?([0-9]{2})(\.([0-9]+))?)?)?$"
-);
-
-/**
- * Takes a date object and a string containing an ISO 8601 date and sets the
- * the date using information parsed from the string.  Note that this method
- * does not parse any time information.
- *
- * @param {Date} dateObject the date object to modify
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} the modified date object
- */
-SimileAjax.DateTime.setIso8601Date = function(dateObject, string) {
-    /*
-     *  This function has been adapted from dojo.date, v.0.3.0
-     *  http://dojotoolkit.org/.
-     */
-
-    var d = string.match(SimileAjax.DateTime._dateRegexp);
-    if(!d) {
-        throw new Error("Invalid date string: " + string);
-    }
-
-    var sign = (d[1] == "-") ? -1 : 1; // BC or AD
-    var year = sign * d[2];
-    var month = d[5];
-    var date = d[7];
-    var dayofyear = d[9];
-    var week = d[11];
-    var dayofweek = (d[13]) ? d[13] : 1;
-
-    dateObject.setUTCFullYear(year);
-    if (dayofyear) {
-        dateObject.setUTCMonth(0);
-        dateObject.setUTCDate(Number(dayofyear));
-    } else if (week) {
-        dateObject.setUTCMonth(0);
-        dateObject.setUTCDate(1);
-        var gd = dateObject.getUTCDay();
-        var day =  (gd) ? gd : 7;
-        var offset = Number(dayofweek) + (7 * Number(week));
-
-        if (day <= 4) {
-            dateObject.setUTCDate(offset + 1 - day);
-        } else {
-            dateObject.setUTCDate(offset + 8 - day);
-        }
-    } else {
-        if (month) {
-            dateObject.setUTCDate(1);
-            dateObject.setUTCMonth(month - 1);
-        }
-        if (date) {
-            dateObject.setUTCDate(date);
-        }
-    }
-
-    return dateObject;
-};
-
-/**
- * Takes a date object and a string containing an ISO 8601 time and sets the
- * the time using information parsed from the string.  Note that this method
- * does not parse any date information.
- *
- * @param {Date} dateObject the date object to modify
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} the modified date object
- */
-SimileAjax.DateTime.setIso8601Time = function (dateObject, string) {
-    /*
-     *  This function has been adapted from dojo.date, v.0.3.0
-     *  http://dojotoolkit.org/.
-     */
-
-    var d = string.match(SimileAjax.DateTime._timeRegexp);
-    if(!d) {
-        SimileAjax.Debug.warn("Invalid time string: " + string);
-        return false;
-    }
-    var hours = d[1];
-    var mins = Number((d[3]) ? d[3] : 0);
-    var secs = (d[5]) ? d[5] : 0;
-    var ms = d[7] ? (Number("0." + d[7]) * 1000) : 0;
-
-    dateObject.setUTCHours(hours);
-    dateObject.setUTCMinutes(mins);
-    dateObject.setUTCSeconds(secs);
-    dateObject.setUTCMilliseconds(ms);
-
-    return dateObject;
-};
-
-/**
- * The timezone offset in minutes in the user's browser.
- * @type Number
- */
-SimileAjax.DateTime.timezoneOffset = new Date().getTimezoneOffset();
-
-/**
- * Takes a date object and a string containing an ISO 8601 date and time and
- * sets the date object using information parsed from the string.
- *
- * @param {Date} dateObject the date object to modify
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} the modified date object
- */
-SimileAjax.DateTime.setIso8601 = function (dateObject, string){
-    /*
-     *  This function has been adapted from dojo.date, v.0.3.0
-     *  http://dojotoolkit.org/.
-     */
-
-    var offset = null;
-    var comps = (string.indexOf("T") == -1) ? string.split(" ") : string.split("T");
-
-    SimileAjax.DateTime.setIso8601Date(dateObject, comps[0]);
-    if (comps.length == 2) {
-        // first strip timezone info from the end
-        var d = comps[1].match(SimileAjax.DateTime._timezoneRegexp);
-        if (d) {
-            if (d[0] == 'Z') {
-                offset = 0;
-            } else {
-                offset = (Number(d[3]) * 60) + Number(d[5]);
-                offset *= ((d[2] == '-') ? 1 : -1);
-            }
-            comps[1] = comps[1].substr(0, comps[1].length - d[0].length);
-        }
-
-        SimileAjax.DateTime.setIso8601Time(dateObject, comps[1]);
-    }
-    if (offset == null) {
-        offset = dateObject.getTimezoneOffset(); // local time zone if no tz info
-    }
-    dateObject.setTime(dateObject.getTime() + offset * 60000);
-
-    return dateObject;
-};
-
-/**
- * Takes a string containing an ISO 8601 date and returns a newly instantiated
- * date object with the parsed date and time information from the string.
- *
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} a new date object created from the string
- */
-SimileAjax.DateTime.parseIso8601DateTime = function (string) {
-    try {
-        return SimileAjax.DateTime.setIso8601(new Date(0), string);
-    } catch (e) {
-        return null;
-    }
-};
-
-/**
- * Takes a string containing a Gregorian date and time and returns a newly
- * instantiated date object with the parsed date and time information from the
- * string.  If the param is actually an instance of Date instead of a string,
- * simply returns the given date instead.
- *
- * @param {Object} o an object, to either return or parse as a string
- * @return {Date} the date object
- */
-SimileAjax.DateTime.parseGregorianDateTime = function(o) {
-    if (o == null) {
-        return null;
-    } else if (o instanceof Date) {
-        return o;
-    }
-
-    var s = o.toString();
-    if (s.length > 0 && s.length < 8) {
-        var space = s.indexOf(" ");
-        if (space > 0) {
-            var year = parseInt(s.substr(0, space));
-            var suffix = s.substr(space + 1);
-            if (suffix.toLowerCase() == "bc") {
-                year = 1 - year;
-            }
-        } else {
-            var year = parseInt(s);
-        }
-
-        var d = new Date(0);
-        d.setUTCFullYear(year);
-
-        return d;
-    }
-
-    try {
-        return new Date(Date.parse(s));
-    } catch (e) {
-        return null;
-    }
-};
-
-/**
- * Rounds date objects down to the nearest interval or multiple of an interval.
- * This method modifies the given date object, converting it to the given
- * timezone if specified.
- *
- * @param {Date} date the date object to round
- * @param {Number} intervalUnit a constant, integer index specifying an
- *   interval, e.g. SimileAjax.DateTime.HOUR
- * @param {Number} timeZone a timezone shift, given in hours
- * @param {Number} multiple a multiple of the interval to round by
- * @param {Number} firstDayOfWeek an integer specifying the first day of the
- *   week, 0 corresponds to Sunday, 1 to Monday, etc.
- */
-SimileAjax.DateTime.roundDownToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
-    var timeShift = timeZone *
-        SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
-
-    var date2 = new Date(date.getTime() + timeShift);
-    var clearInDay = function(d) {
-        d.setUTCMilliseconds(0);
-        d.setUTCSeconds(0);
-        d.setUTCMinutes(0);
-        d.setUTCHours(0);
-    };
-    var clearInYear = function(d) {
-        clearInDay(d);
-        d.setUTCDate(1);
-        d.setUTCMonth(0);
-    };
-
-    switch(intervalUnit) {
-    case SimileAjax.DateTime.MILLISECOND:
-        var x = date2.getUTCMilliseconds();
-        date2.setUTCMilliseconds(x - (x % multiple));
-        break;
-    case SimileAjax.DateTime.SECOND:
-        date2.setUTCMilliseconds(0);
-
-        var x = date2.getUTCSeconds();
-        date2.setUTCSeconds(x - (x % multiple));
-        break;
-    case SimileAjax.DateTime.MINUTE:
-        date2.setUTCMilliseconds(0);
-        date2.setUTCSeconds(0);
-
-        var x = date2.getUTCMinutes();
-        date2.setTime(date2.getTime() -
-            (x % multiple) * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
-        break;
-    case SimileAjax.DateTime.HOUR:
-        date2.setUTCMilliseconds(0);
-        date2.setUTCSeconds(0);
-        date2.setUTCMinutes(0);
-
-        var x = date2.getUTCHours();
-        date2.setUTCHours(x - (x % multiple));
-        break;
-    case SimileAjax.DateTime.DAY:
-        clearInDay(date2);
-        break;
-    case SimileAjax.DateTime.WEEK:
-        clearInDay(date2);
-        var d = (date2.getUTCDay() + 7 - firstDayOfWeek) % 7;
-        date2.setTime(date2.getTime() -
-            d * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY]);
-        break;
-    case SimileAjax.DateTime.MONTH:
-        clearInDay(date2);
-        date2.setUTCDate(1);
-
-        var x = date2.getUTCMonth();
-        date2.setUTCMonth(x - (x % multiple));
-        break;
-    case SimileAjax.DateTime.YEAR:
-        clearInYear(date2);
-
-        var x = date2.getUTCFullYear();
-        date2.setUTCFullYear(x - (x % multiple));
-        break;
-    case SimileAjax.DateTime.DECADE:
-        clearInYear(date2);
-        date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 10) * 10);
-        break;
-    case SimileAjax.DateTime.CENTURY:
-        clearInYear(date2);
-        date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 100) * 100);
-        break;
-    case SimileAjax.DateTime.MILLENNIUM:
-        clearInYear(date2);
-        date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 1000) * 1000);
-        break;
-    }
-
-    date.setTime(date2.getTime() - timeShift);
-};
-
-/**
- * Rounds date objects up to the nearest interval or multiple of an interval.
- * This method modifies the given date object, converting it to the given
- * timezone if specified.
- *
- * @param {Date} date the date object to round
- * @param {Number} intervalUnit a constant, integer index specifying an
- *   interval, e.g. SimileAjax.DateTime.HOUR
- * @param {Number} timeZone a timezone shift, given in hours
- * @param {Number} multiple a multiple of the interval to round by
- * @param {Number} firstDayOfWeek an integer specifying the first day of the
- *   week, 0 corresponds to Sunday, 1 to Monday, etc.
- * @see SimileAjax.DateTime.roundDownToInterval
- */
-SimileAjax.DateTime.roundUpToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
-    var originalTime = date.getTime();
-    SimileAjax.DateTime.roundDownToInterval(date, intervalUnit, timeZone, multiple, firstDayOfWeek);
-    if (date.getTime() < originalTime) {
-        date.setTime(date.getTime() +
-            SimileAjax.DateTime.gregorianUnitLengths[intervalUnit] * multiple);
-    }
-};
-
-/**
- * Increments a date object by a specified interval, taking into
- * consideration the timezone.
- *
- * @param {Date} date the date object to increment
- * @param {Number} intervalUnit a constant, integer index specifying an
- *   interval, e.g. SimileAjax.DateTime.HOUR
- * @param {Number} timeZone the timezone offset in hours
- */
-SimileAjax.DateTime.incrementByInterval = function(date, intervalUnit, timeZone) {
-    timeZone = (typeof timeZone == 'undefined') ? 0 : timeZone;
-
-    var timeShift = timeZone *
-        SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
-
-    var date2 = new Date(date.getTime() + timeShift);
-
-    switch(intervalUnit) {
-    case SimileAjax.DateTime.MILLISECOND:
-        date2.setTime(date2.getTime() + 1)
-        break;
-    case SimileAjax.DateTime.SECOND:
-        date2.setTime(date2.getTime() + 1000);
-        break;
-    case SimileAjax.DateTime.MINUTE:
-        date2.setTime(date2.getTime() +
-            SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
-        break;
-    case SimileAjax.DateTime.HOUR:
-        date2.setTime(date2.getTime() +
-            SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
-        break;
-    case SimileAjax.DateTime.DAY:
-        date2.setUTCDate(date2.getUTCDate() + 1);
-        break;
-    case SimileAjax.DateTime.WEEK:
-        date2.setUTCDate(date2.getUTCDate() + 7);
-        break;
-    case SimileAjax.DateTime.MONTH:
-        date2.setUTCMonth(date2.getUTCMonth() + 1);
-        break;
-    case SimileAjax.DateTime.YEAR:
-        date2.setUTCFullYear(date2.getUTCFullYear() + 1);
-        break;
-    case SimileAjax.DateTime.DECADE:
-        date2.setUTCFullYear(date2.getUTCFullYear() + 10);
-        break;
-    case SimileAjax.DateTime.CENTURY:
-        date2.setUTCFullYear(date2.getUTCFullYear() + 100);
-        break;
-    case SimileAjax.DateTime.MILLENNIUM:
-        date2.setUTCFullYear(date2.getUTCFullYear() + 1000);
-        break;
-    }
-
-    date.setTime(date2.getTime() - timeShift);
-};
-
-/**
- * Returns a new date object with the given time offset removed.
- *
- * @param {Date} date the starting date
- * @param {Number} timeZone a timezone specified in an hour offset to remove
- * @return {Date} a new date object with the offset removed
- */
-SimileAjax.DateTime.removeTimeZoneOffset = function(date, timeZone) {
-    return new Date(date.getTime() +
-        timeZone * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
-};
-
-/**
- * Returns the timezone of the user's browser.
- *
- * @return {Number} the timezone in the user's locale in hours
- */
-SimileAjax.DateTime.getTimezone = function() {
-    var d = new Date().getTimezoneOffset();
-    return d / -60;
-};
-/*
- *  String Utility Functions and Constants
- *
- */
-
-String.prototype.trim = function() {
-    return this.replace(/^\s+|\s+$/g, '');
-};
-
-String.prototype.startsWith = function(prefix) {
-    return this.length >= prefix.length && this.substr(0, prefix.length) == prefix;
-};
-
-String.prototype.endsWith = function(suffix) {
-    return this.length >= suffix.length && this.substr(this.length - suffix.length) == suffix;
-};
-
-String.substitute = function(s, objects) {
-    var result = "";
-    var start = 0;
-    while (start < s.length - 1) {
-        var percent = s.indexOf("%", start);
-        if (percent < 0 || percent == s.length - 1) {
-            break;
-        } else if (percent > start && s.charAt(percent - 1) == "\\") {
-            result += s.substring(start, percent - 1) + "%";
-            start = percent + 1;
-        } else {
-            var n = parseInt(s.charAt(percent + 1));
-            if (isNaN(n) || n >= objects.length) {
-                result += s.substring(start, percent + 2);
-            } else {
-                result += s.substring(start, percent) + objects[n].toString();
-            }
-            start = percent + 2;
-        }
-    }
-
-    if (start < s.length) {
-        result += s.substring(start);
-    }
-    return result;
-};
-/*
- *  HTML Utility Functions
- *
- */
-
-SimileAjax.HTML = new Object();
-
-SimileAjax.HTML._e2uHash = {};
-(function() {
-    var e2uHash = SimileAjax.HTML._e2uHash;
-    e2uHash['nbsp']= '\u00A0[space]';
-    e2uHash['iexcl']= '\u00A1';
-    e2uHash['cent']= '\u00A2';
-    e2uHash['pound']= '\u00A3';
-    e2uHash['curren']= '\u00A4';
-    e2uHash['yen']= '\u00A5';
-    e2uHash['brvbar']= '\u00A6';
-    e2uHash['sect']= '\u00A7';
-    e2uHash['uml']= '\u00A8';
-    e2uHash['copy']= '\u00A9';
-    e2uHash['ordf']= '\u00AA';
-    e2uHash['laquo']= '\u00AB';
-    e2uHash['not']= '\u00AC';
-    e2uHash['shy']= '\u00AD';
-    e2uHash['reg']= '\u00AE';
-    e2uHash['macr']= '\u00AF';
-    e2uHash['deg']= '\u00B0';
-    e2uHash['plusmn']= '\u00B1';
-    e2uHash['sup2']= '\u00B2';
-    e2uHash['sup3']= '\u00B3';
-    e2uHash['acute']= '\u00B4';
-    e2uHash['micro']= '\u00B5';
-    e2uHash['para']= '\u00B6';
-    e2uHash['middot']= '\u00B7';
-    e2uHash['cedil']= '\u00B8';
-    e2uHash['sup1']= '\u00B9';
-    e2uHash['ordm']= '\u00BA';
-    e2uHash['raquo']= '\u00BB';
-    e2uHash['frac14']= '\u00BC';
-    e2uHash['frac12']= '\u00BD';
-    e2uHash['frac34']= '\u00BE';
-    e2uHash['iquest']= '\u00BF';
-    e2uHash['Agrave']= '\u00C0';
-    e2uHash['Aacute']= '\u00C1';
-    e2uHash['Acirc']= '\u00C2';
-    e2uHash['Atilde']= '\u00C3';
-    e2uHash['Auml']= '\u00C4';
-    e2uHash['Aring']= '\u00C5';
-    e2uHash['AElig']= '\u00C6';
-    e2uHash['Ccedil']= '\u00C7';
-    e2uHash['Egrave']= '\u00C8';
-    e2uHash['Eacute']= '\u00C9';
-    e2uHash['Ecirc']= '\u00CA';
-    e2uHash['Euml']= '\u00CB';
-    e2uHash['Igrave']= '\u00CC';
-    e2uHash['Iacute']= '\u00CD';
-    e2uHash['Icirc']= '\u00CE';
-    e2uHash['Iuml']= '\u00CF';
-    e2uHash['ETH']= '\u00D0';
-    e2uHash['Ntilde']= '\u00D1';
-    e2uHash['Ograve']= '\u00D2';
-    e2uHash['Oacute']= '\u00D3';
-    e2uHash['Ocirc']= '\u00D4';
-    e2uHash['Otilde']= '\u00D5';
-    e2uHash['Ouml']= '\u00D6';
-    e2uHash['times']= '\u00D7';
-    e2uHash['Oslash']= '\u00D8';
-    e2uHash['Ugrave']= '\u00D9';
-    e2uHash['Uacute']= '\u00DA';
-    e2uHash['Ucirc']= '\u00DB';
-    e2uHash['Uuml']= '\u00DC';
-    e2uHash['Yacute']= '\u00DD';
-    e2uHash['THORN']= '\u00DE';
-    e2uHash['szlig']= '\u00DF';
-    e2uHash['agrave']= '\u00E0';
-    e2uHash['aacute']= '\u00E1';
-    e2uHash['acirc']= '\u00E2';
-    e2uHash['atilde']= '\u00E3';
-    e2uHash['auml']= '\u00E4';
-    e2uHash['aring']= '\u00E5';
-    e2uHash['aelig']= '\u00E6';
-    e2uHash['ccedil']= '\u00E7';
-    e2uHash['egrave']= '\u00E8';
-    e2uHash['eacute']= '\u00E9';
-    e2uHash['ecirc']= '\u00EA';
-    e2uHash['euml']= '\u00EB';
-    e2uHash['igrave']= '\u00EC';
-    e2uHash['iacute']= '\u00ED';
-    e2uHash['icirc']= '\u00EE';
-    e2uHash['iuml']= '\u00EF';
-    e2uHash['eth']= '\u00F0';
-    e2uHash['ntilde']= '\u00F1';
-    e2uHash['ograve']= '\u00F2';
-    e2uHash['oacute']= '\u00F3';
-    e2uHash['ocirc']= '\u00F4';
-    e2uHash['otilde']= '\u00F5';
-    e2uHash['ouml']= '\u00F6';
-    e2uHash['divide']= '\u00F7';
-    e2uHash['oslash']= '\u00F8';
-    e2uHash['ugrave']= '\u00F9';
-    e2uHash['uacute']= '\u00FA';
-    e2uHash['ucirc']= '\u00FB';
-    e2uHash['uuml']= '\u00FC';
-    e2uHash['yacute']= '\u00FD';
-    e2uHash['thorn']= '\u00FE';
-    e2uHash['yuml']= '\u00FF';
-    e2uHash['quot']= '\u0022';
-    e2uHash['amp']= '\u0026';
-    e2uHash['lt']= '\u003C';
-    e2uHash['gt']= '\u003E';
-    e2uHash['OElig']= '';
-    e2uHash['oelig']= '\u0153';
-    e2uHash['Scaron']= '\u0160';
-    e2uHash['scaron']= '\u0161';
-    e2uHash['Yuml']= '\u0178';
-    e2uHash['circ']= '\u02C6';
-    e2uHash['tilde']= '\u02DC';
-    e2uHash['ensp']= '\u2002';
-    e2uHash['emsp']= '\u2003';
-    e2uHash['thinsp']= '\u2010';
-    e2uHash['zwnj']= '\u200C';
-    e2uHash['zwj']= '\u200D';
-    e2uHash['lrm']= '\u200E';
-    e2uHash['rlm']= '\u200F';
-    e2uHash['ndash']= '\u2013';
-    e2uHash['mdash']= '\u2014';
-    e2uHash['lsquo']= '\u2018';
-    e2uHash['rsquo']= '\u2019';
-    e2uHash['sbquo']= '\u201A';
-    e2uHash['ldquo']= '\u201C';
-    e2uHash['rdquo']= '\u201D';
-    e2uHash['bdquo']= '\u201E';
-    e2uHash['dagger']= '\u2020';
-    e2uHash['Dagger']= '\u2021';
-    e2uHash['permil']= '\u2030';
-    e2uHash['lsaquo']= '\u2039';
-    e2uHash['rsaquo']= '\u203A';
-    e2uHash['euro']= '\u20AC';
-    e2uHash['fnof']= '\u0192';
-    e2uHash['Alpha']= '\u0391';
-    e2uHash['Beta']= '\u0392';
-    e2uHash['Gamma']= '\u0393';
-    e2uHash['Delta']= '\u0394';
-    e2uHash['Epsilon']= '\u0395';
-    e2uHash['Zeta']= '\u0396';
-    e2uHash['Eta']= '\u0397';
-    e2uHash['Theta']= '\u0398';
-    e2uHash['Iota']= '\u0399';
-    e2uHash['Kappa']= '\u039A';
-    e2uHash['Lambda']= '\u039B';
-    e2uHash['Mu']= '\u039C';
-    e2uHash['Nu']= '\u039D';
-    e2uHash['Xi']= '\u039E';
-    e2uHash['Omicron']= '\u039F';
-    e2uHash['Pi']= '\u03A0';
-    e2uHash['Rho']= '\u03A1';
-    e2uHash['Sigma']= '\u03A3';
-    e2uHash['Tau']= '\u03A4';
-    e2uHash['Upsilon']= '\u03A5';
-    e2uHash['Phi']= '\u03A6';
-    e2uHash['Chi']= '\u03A7';
-    e2uHash['Psi']= '\u03A8';
-    e2uHash['Omega']= '\u03A9';
-    e2uHash['alpha']= '\u03B1';
-    e2uHash['beta']= '\u03B2';
-    e2uHash['gamma']= '\u03B3';
-    e2uHash['delta']= '\u03B4';
-    e2uHash['epsilon']= '\u03B5';
-    e2uHash['zeta']= '\u03B6';
-    e2uHash['eta']= '\u03B7';
-    e2uHash['theta']= '\u03B8';
-    e2uHash['iota']= '\u03B9';
-    e2uHash['kappa']= '\u03BA';
-    e2uHash['lambda']= '\u03BB';
-    e2uHash['mu']= '\u03BC';
-    e2uHash['nu']= '\u03BD';
-    e2uHash['xi']= '\u03BE';
-    e2uHash['omicron']= '\u03BF';
-    e2uHash['pi']= '\u03C0';
-    e2uHash['rho']= '\u03C1';
-    e2uHash['sigmaf']= '\u03C2';
-    e2uHash['sigma']= '\u03C3';
-    e2uHash['tau']= '\u03C4';
-    e2uHash['upsilon']= '\u03C5';
-    e2uHash['phi']= '\u03C6';
-    e2uHash['chi']= '\u03C7';
-    e2uHash['psi']= '\u03C8';
-    e2uHash['omega']= '\u03C9';
-    e2uHash['thetasym']= '\u03D1';
-    e2uHash['upsih']= '\u03D2';
-    e2uHash['piv']= '\u03D6';
-    e2uHash['bull']= '\u2022';
-    e2uHash['hellip']= '\u2026';
-    e2uHash['prime']= '\u2032';
-    e2uHash['Prime']= '\u2033';
-    e2uHash['oline']= '\u203E';
-    e2uHash['frasl']= '\u2044';
-    e2uHash['weierp']= '\u2118';
-    e2uHash['image']= '\u2111';
-    e2uHash['real']= '\u211C';
-    e2uHash['trade']= '\u2122';
-    e2uHash['alefsym']= '\u2135';
-    e2uHash['larr']= '\u2190';
-    e2uHash['uarr']= '\u2191';
-    e2uHash['rarr']= '\u2192';
-    e2uHash['darr']= '\u2193';
-    e2uHash['harr']= '\u2194';
-    e2uHash['crarr']= '\u21B5';
-    e2uHash['lArr']= '\u21D0';
-    e2uHash['uArr']= '\u21D1';
-    e2uHash['rArr']= '\u21D2';
-    e2uHash['dArr']= '\u21D3';
-    e2uHash['hArr']= '\u21D4';
-    e2uHash['forall']= '\u2200';
-    e2uHash['part']= '\u2202';
-    e2uHash['exist']= '\u2203';
-    e2uHash['empty']= '\u2205';
-    e2uHash['nabla']= '\u2207';
-    e2uHash['isin']= '\u2208';
-    e2uHash['notin']= '\u2209';
-    e2uHash['ni']= '\u220B';
-    e2uHash['prod']= '\u220F';
-    e2uHash['sum']= '\u2211';
-    e2uHash['minus']= '\u2212';
-    e2uHash['lowast']= '\u2217';
-    e2uHash['radic']= '\u221A';
-    e2uHash['prop']= '\u221D';
-    e2uHash['infin']= '\u221E';
-    e2uHash['ang']= '\u2220';
-    e2uHash['and']= '\u2227';
-    e2uHash['or']= '\u2228';
-    e2uHash['cap']= '\u2229';
-    e2uHash['cup']= '\u222A';
-    e2uHash['int']= '\u222B';
-    e2uHash['there4']= '\u2234';
-    e2uHash['sim']= '\u223C';
-    e2uHash['cong']= '\u2245';
-    e2uHash['asymp']= '\u2248';
-    e2uHash['ne']= '\u2260';
-    e2uHash['equiv']= '\u2261';
-    e2uHash['le']= '\u2264';
-    e2uHash['ge']= '\u2265';
-    e2uHash['sub']= '\u2282';
-    e2uHash['sup']= '\u2283';
-    e2uHash['nsub']= '\u2284';
-    e2uHash['sube']= '\u2286';
-    e2uHash['supe']= '\u2287';
-    e2uHash['oplus']= '\u2295';
-    e2uHash['otimes']= '\u2297';
-    e2uHash['perp']= '\u22A5';
-    e2uHash['sdot']= '\u22C5';
-    e2uHash['lceil']= '\u2308';
-    e2uHash['rceil']= '\u2309';
-    e2uHash['lfloor']= '\u230A';
-    e2uHash['rfloor']= '\u230B';
-    e2uHash['lang']= '\u2329';
-    e2uHash['rang']= '\u232A';
-    e2uHash['loz']= '\u25CA';
-    e2uHash['spades']= '\u2660';
-    e2uHash['clubs']= '\u2663';
-    e2uHash['hearts']= '\u2665';
-    e2uHash['diams']= '\u2666';
-})();
-
-SimileAjax.HTML.deEntify = function(s) {
-    var e2uHash = SimileAjax.HTML._e2uHash;
-
-    var re = /&(\w+?);/;
-    while (re.test(s)) {
-        var m = s.match(re);
-        s = s.replace(re, e2uHash[m[1]]);
-    }
-    return s;
-};/**
- * A basic set (in the mathematical sense) data structure
- *
- * @constructor
- * @param {Array or SimileAjax.Set} [a] an initial collection
- */
-SimileAjax.Set = function(a) {
-    this._hash = {};
-    this._count = 0;
-
-    if (a instanceof Array) {
-        for (var i = 0; i < a.length; i++) {
-            this.add(a[i]);
-        }
-    } else if (a instanceof SimileAjax.Set) {
-        this.addSet(a);
-    }
-}
-
-/**
- * Adds the given object to this set, assuming there it does not already exist
- *
- * @param {Object} o the object to add
- * @return {Boolean} true if the object was added, false if not
- */
-SimileAjax.Set.prototype.add = function(o) {
-    if (!(o in this._hash)) {
-        this._hash[o] = true;
-        this._count++;
-        return true;
-    }
-    return false;
-}
-
-/**
- * Adds each element in the given set to this set
- *
- * @param {SimileAjax.Set} set the set of elements to add
- */
-SimileAjax.Set.prototype.addSet = function(set) {
-    for (var o in set._hash) {
-        this.add(o);
-    }
-}
-
-/**
- * Removes the given element from this set
- *
- * @param {Object} o the object to remove
- * @return {Boolean} true if the object was successfully removed,
- *   false otherwise
- */
-SimileAjax.Set.prototype.remove = function(o) {
-    if (o in this._hash) {
-        delete this._hash[o];
-        this._count--;
-        return true;
-    }
-    return false;
-}
-
-/**
- * Removes the elements in this set that correspond to the elements in the
- * given set
- *
- * @param {SimileAjax.Set} set the set of elements to remove
- */
-SimileAjax.Set.prototype.removeSet = function(set) {
-    for (var o in set._hash) {
-        this.remove(o);
-    }
-}
-
-/**
- * Removes all elements in this set that are not present in the given set, i.e.
- * modifies this set to the intersection of the two sets
- *
- * @param {SimileAjax.Set} set the set to intersect
- */
-SimileAjax.Set.prototype.retainSet = function(set) {
-    for (var o in this._hash) {
-        if (!set.contains(o)) {
-            delete this._hash[o];
-            this._count--;
-        }
-    }
-}
-
-/**
- * Returns whether or not the given element exists in this set
- *
- * @param {SimileAjax.Set} o the object to test for
- * @return {Boolean} true if the object is present, false otherwise
- */
-SimileAjax.Set.prototype.contains = function(o) {
-    return (o in this._hash);
-}
-
-/**
- * Returns the number of elements in this set
- *
- * @return {Number} the number of elements in this set
- */
-SimileAjax.Set.prototype.size = function() {
-    return this._count;
-}
-
-/**
- * Returns the elements of this set as an array
- *
- * @return {Array} a new array containing the elements of this set
- */
-SimileAjax.Set.prototype.toArray = function() {
-    var a = [];
-    for (var o in this._hash) {
-        a.push(o);
-    }
-    return a;
-}
-
-/**
- * Iterates through the elements of this set, order unspecified, executing the
- * given function on each element until the function returns true
- *
- * @param {Function} f a function of form f(element)
- */
-SimileAjax.Set.prototype.visit = function(f) {
-    for (var o in this._hash) {
-        if (f(o) == true) {
-            break;
-        }
-    }
-}
-
-/**
- * A sorted array data structure
- *
- * @constructor
- */
-SimileAjax.SortedArray = function(compare, initialArray) {
-    this._a = (initialArray instanceof Array) ? initialArray : [];
-    this._compare = compare;
-};
-
-SimileAjax.SortedArray.prototype.add = function(elmt) {
-    var sa = this;
-    var index = this.find(function(elmt2) {
-        return sa._compare(elmt2, elmt);
-    });
-
-    if (index < this._a.length) {
-        this._a.splice(index, 0, elmt);
-    } else {
-        this._a.push(elmt);
-    }
-};
-
-SimileAjax.SortedArray.prototype.remove = function(elmt) {
-    var sa = this;
-    var index = this.find(function(elmt2) {
-        return sa._compare(elmt2, elmt);
-    });
-
-    while (index < this._a.length && this._compare(this._a[index], elmt) == 0) {
-        if (this._a[index] == elmt) {
-            this._a.splice(index, 1);
-            return true;
-        } else {
-            index++;
-        }
-    }
-    return false;
-};
-
-SimileAjax.SortedArray.prototype.removeAll = function() {
-    this._a = [];
-};
-
-SimileAjax.SortedArray.prototype.elementAt = function(index) {
-    return this._a[index];
-};
-
-SimileAjax.SortedArray.prototype.length = function() {
-    return this._a.length;
-};
-
-SimileAjax.SortedArray.prototype.find = function(compare) {
-    var a = 0;
-    var b = this._a.length;
-
-    while (a < b) {
-        var mid = Math.floor((a + b) / 2);
-        var c = compare(this._a[mid]);
-        if (mid == a) {
-            return c < 0 ? a+1 : a;
-        } else if (c < 0) {
-            a = mid;
-        } else {
-            b = mid;
-        }
-    }
-    return a;
-};
-
-SimileAjax.SortedArray.prototype.getFirst = function() {
-    return (this._a.length > 0) ? this._a[0] : null;
-};
-
-SimileAjax.SortedArray.prototype.getLast = function() {
-    return (this._a.length > 0) ? this._a[this._a.length - 1] : null;
-};
-
-/*
- *  Event Index
- *
- */
-
-SimileAjax.EventIndex = function(unit) {
-    var eventIndex = this;
-
-    this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
-    this._events = new SimileAjax.SortedArray(
-        function(event1, event2) {
-            return eventIndex._unit.compare(event1.getStart(), event2.getStart());
-        }
-    );
-    this._idToEvent = {};
-    this._indexed = true;
-};
-
-SimileAjax.EventIndex.prototype.getUnit = function() {
-    return this._unit;
-};
-
-SimileAjax.EventIndex.prototype.getEvent = function(id) {
-    return this._idToEvent[id];
-};
-
-SimileAjax.EventIndex.prototype.add = function(evt) {
-    this._events.add(evt);
-    this._idToEvent[evt.getID()] = evt;
-    this._indexed = false;
-};
-
-SimileAjax.EventIndex.prototype.removeAll = function() {
-    this._events.removeAll();
-    this._idToEvent = {};
-    this._indexed = false;
-};
-
-SimileAjax.EventIndex.prototype.getCount = function() {
-    return this._events.length();
-};
-
-SimileAjax.EventIndex.prototype.getIterator = function(startDate, endDate) {
-    if (!this._indexed) {
-        this._index();
-    }
-    return new SimileAjax.EventIndex._Iterator(this._events, startDate, endDate, this._unit);
-};
-
-SimileAjax.EventIndex.prototype.getReverseIterator = function(startDate, endDate) {
-    if (!this._indexed) {
-        this._index();
-    }
-    return new SimileAjax.EventIndex._ReverseIterator(this._events, startDate, endDate, this._unit);
-};
-
-SimileAjax.EventIndex.prototype.getAllIterator = function() {
-    return new SimileAjax.EventIndex._AllIterator(this._events);
-};
-
-SimileAjax.EventIndex.prototype.getEarliestDate = function() {
-    var evt = this._events.getFirst();
-    return (evt == null) ? null : evt.getStart();
-};
-
-SimileAjax.EventIndex.prototype.getLatestDate = function() {
-    var evt = this._events.getLast();
-    if (evt == null) {
-        return null;
-    }
-
-    if (!this._indexed) {
-        this._index();
-    }
-
-    var index = evt._earliestOverlapIndex;
-    var date = this._events.elementAt(index).getEnd();
-    for (var i = index + 1; i < this._events.length(); i++) {
-        date = this._unit.later(date, this._events.elementAt(i).getEnd());
-    }
-
-    return date;
-};
-
-SimileAjax.EventIndex.prototype._index = function() {
-    /*
-     *  For each event, we want to find the earliest preceding
-     *  event that overlaps with it, if any.
-     */
-
-    var l = this._events.length();
-    for (var i = 0; i < l; i++) {
-        var evt = this._events.elementAt(i);
-        evt._earliestOverlapIndex = i;
-    }
-
-    var toIndex = 1;
-    for (var i = 0; i < l; i++) {
-        var evt = this._events.elementAt(i);
-        var end = evt.getEnd();
-
-        toIndex = Math.max(toIndex, i + 1);
-        while (toIndex < l) {
-            var evt2 = this._events.elementAt(toIndex);
-            var start2 = evt2.getStart();
-
-            if (this._unit.compare(start2, end) < 0) {
-                evt2._earliestOverlapIndex = i;
-                toIndex++;
-            } else {
-                break;
-            }
-        }
-    }
-    this._indexed = true;
-};
-
-SimileAjax.EventIndex._Iterator = function(events, startDate, endDate, unit) {
-    this._events = events;
-    this._startDate = startDate;
-    this._endDate = endDate;
-    this._unit = unit;
-
-    this._currentIndex = events.find(function(evt) {
-        return unit.compare(evt.getStart(), startDate);
-    });
-    if (this._currentIndex - 1 >= 0) {
-        this._currentIndex = this._events.elementAt(this._currentIndex - 1)._earliestOverlapIndex;
-    }
-    this._currentIndex--;
-
-    this._maxIndex = events.find(function(evt) {
-        return unit.compare(evt.getStart(), endDate);
-    });
-
-    this._hasNext = false;
-    this._next = null;
-    this._findNext();
-};
-
-SimileAjax.EventIndex._Iterator.prototype = {
-    hasNext: function() { return this._hasNext; },
-    next: function() {
-        if (this._hasNext) {
-            var next = this._next;
-            this._findNext();
-
-            return next;
-        } else {
-            return null;
-        }
-    },
-    _findNext: function() {
-        var unit = this._unit;
-        while ((++this._currentIndex) < this._maxIndex) {
-            var evt = this._events.elementAt(this._currentIndex);
-            if (unit.compare(evt.getStart(), this._endDate) < 0 &&
-                unit.compare(evt.getEnd(), this._startDate) > 0) {
-
-                this._next = evt;
-                this._hasNext = true;
-                return;
-            }
-        }
-        this._next = null;
-        this._hasNext = false;
-    }
-};
-
-SimileAjax.EventIndex._ReverseIterator = function(events, startDate, endDate, unit) {
-    this._events = events;
-    this._startDate = startDate;
-    this._endDate = endDate;
-    this._unit = unit;
-
-    this._minIndex = events.find(function(evt) {
-        return unit.compare(evt.getStart(), startDate);
-    });
-    if (this._minIndex - 1 >= 0) {
-        this._minIndex = this._events.elementAt(this._minIndex - 1)._earliestOverlapIndex;
-    }
-
-    this._maxIndex = events.find(function(evt) {
-        return unit.compare(evt.getStart(), endDate);
-    });
-
-    this._currentIndex = this._maxIndex;
-    this._hasNext = false;
-    this._next = null;
-    this._findNext();
-};
-
-SimileAjax.EventIndex._ReverseIterator.prototype = {
-    hasNext: function() { return this._hasNext; },
-    next: function() {
-        if (this._hasNext) {
-            var next = this._next;
-            this._findNext();
-
-            return next;
-        } else {
-            return null;
-        }
-    },
-    _findNext: function() {
-        var unit = this._unit;
-        while ((--this._currentIndex) >= this._minIndex) {
-            var evt = this._events.elementAt(this._currentIndex);
-            if (unit.compare(evt.getStart(), this._endDate) < 0 &&
-                unit.compare(evt.getEnd(), this._startDate) > 0) {
-
-                this._next = evt;
-                this._hasNext = true;
-                return;
-            }
-        }
-        this._next = null;
-        this._hasNext = false;
-    }
-};
-
-SimileAjax.EventIndex._AllIterator = function(events) {
-    this._events = events;
-    this._index = 0;
-};
-
-SimileAjax.EventIndex._AllIterator.prototype = {
-    hasNext: function() {
-        return this._index < this._events.length();
-    },
-    next: function() {
-        return this._index < this._events.length() ?
-            this._events.elementAt(this._index++) : null;
-    }
-};/*
- *  Default Unit
- *
- */
-
-SimileAjax.NativeDateUnit = new Object();
-
-SimileAjax.NativeDateUnit.makeDefaultValue = function() {
-    return new Date();
-};
-
-SimileAjax.NativeDateUnit.cloneValue = function(v) {
-    return new Date(v.getTime());
-};
-
-SimileAjax.NativeDateUnit.getParser = function(format) {
-    if (typeof format == "string") {
-        format = format.toLowerCase();
-    }
-    return (format == "iso8601" || format == "iso 8601") ?
-        SimileAjax.DateTime.parseIso8601DateTime : 
-        SimileAjax.DateTime.parseGregorianDateTime;
-};
-
-SimileAjax.NativeDateUnit.parseFromObject = function(o) {
-    return SimileAjax.DateTime.parseGregorianDateTime(o);
-};
-
-SimileAjax.NativeDateUnit.toNumber = function(v) {
-    return v.getTime();
-};
-
-SimileAjax.NativeDateUnit.fromNumber = function(n) {
-    return new Date(n);
-};
-
-SimileAjax.NativeDateUnit.compare = function(v1, v2) {
-    var n1, n2;
-    if (typeof v1 == "object") {
-        n1 = v1.getTime();
-    } else {
-        n1 = Number(v1);
-    }
-    if (typeof v2 == "object") {
-        n2 = v2.getTime();
-    } else {
-        n2 = Number(v2);
-    }
-    
-    return n1 - n2;
-};
-
-SimileAjax.NativeDateUnit.earlier = function(v1, v2) {
-    return SimileAjax.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
-};
-
-SimileAjax.NativeDateUnit.later = function(v1, v2) {
-    return SimileAjax.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
-};
-
-SimileAjax.NativeDateUnit.change = function(v, n) {
-    return new Date(v.getTime() + n);
-};
-
-/*
- *  General, miscellaneous SimileAjax stuff
- *
- */
-
-SimileAjax.ListenerQueue = function(wildcardHandlerName) {
-    this._listeners = [];
-    this._wildcardHandlerName = wildcardHandlerName;
-};
-
-SimileAjax.ListenerQueue.prototype.add = function(listener) {
-    this._listeners.push(listener);
-};
-
-SimileAjax.ListenerQueue.prototype.remove = function(listener) {
-    var listeners = this._listeners;
-    for (var i = 0; i < listeners.length; i++) {
-        if (listeners[i] == listener) {
-            listeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-SimileAjax.ListenerQueue.prototype.fire = function(handlerName, args) {
-    var listeners = [].concat(this._listeners);
-    for (var i = 0; i < listeners.length; i++) {
-        var listener = listeners[i];
-        if (handlerName in listener) {
-            try {
-                listener[handlerName].apply(listener, args);
-            } catch (e) {
-                SimileAjax.Debug.exception("Error firing event of name " + handlerName, e);
-            }
-        } else if (this._wildcardHandlerName != null &&
-            this._wildcardHandlerName in listener) {
-            try {
-                listener[this._wildcardHandlerName].apply(listener, [ handlerName ]);
-            } catch (e) {
-                SimileAjax.Debug.exception("Error firing event of name " + handlerName + " to wildcard handler", e);
-            }
-        }
-    }
-};
-
-/*
- *  History
- *
- *  This is a singleton that keeps track of undoable user actions and
- *  performs undos and redos in response to the browser's Back and
- *  Forward buttons.
- *
- *  Call addAction(action) to register an undoable user action. action
- *  must have 4 fields:
- *
- *      perform: an argument-less function that carries out the action
- *      undo:    an argument-less function that undos the action
- *      label:   a short, user-friendly string describing the action
- *      uiLayer: the UI layer on which the action takes place
- *
- *  By default, the history keeps track of upto 10 actions. You can
- *  configure this behavior by setting
- *      SimileAjax.History.maxHistoryLength
- *  to a different number.
- *
- *  An iframe is inserted into the document's body element to track
- *  onload events.
- *
- */
-
-SimileAjax.History = {
-    maxHistoryLength:       10,
-    historyFile:            "__history__.html",
-    enabled:               true,
-
-    _initialized:           false,
-    _listeners:             new SimileAjax.ListenerQueue(),
-
-    _actions:               [],
-    _baseIndex:             0,
-    _currentIndex:          0,
-
-    _plainDocumentTitle:    document.title
-};
-
-SimileAjax.History.formatHistoryEntryTitle = function(actionLabel) {
-    return SimileAjax.History._plainDocumentTitle + " {" + actionLabel + "}";
-};
-
-SimileAjax.History.initialize = function() {
-    if (SimileAjax.History._initialized) {
-        return;
-    }
-
-    if (SimileAjax.History.enabled) {
-        var iframe = document.createElement("iframe");
-        iframe.id = "simile-ajax-history";
-        iframe.style.position = "absolute";
-        iframe.style.width = "10px";
-        iframe.style.height = "10px";
-        iframe.style.top = "0px";
-        iframe.style.left = "0px";
-        iframe.style.visibility = "hidden";
-        iframe.src = SimileAjax.History.historyFile + "?0";
-
-        document.body.appendChild(iframe);
-        SimileAjax.DOM.registerEvent(iframe, "load", SimileAjax.History._handleIFrameOnLoad);
-
-        SimileAjax.History._iframe = iframe;
-    }
-    SimileAjax.History._initialized = true;
-};
-
-SimileAjax.History.addListener = function(listener) {
-    SimileAjax.History.initialize();
-
-    SimileAjax.History._listeners.add(listener);
-};
-
-SimileAjax.History.removeListener = function(listener) {
-    SimileAjax.History.initialize();
-
-    SimileAjax.History._listeners.remove(listener);
-};
-
-SimileAjax.History.addAction = function(action) {
-    SimileAjax.History.initialize();
-
-    SimileAjax.History._listeners.fire("onBeforePerform", [ action ]);
-    window.setTimeout(function() {
-        try {
-            action.perform();
-            SimileAjax.History._listeners.fire("onAfterPerform", [ action ]);
-
-            if (SimileAjax.History.enabled) {
-                SimileAjax.History._actions = SimileAjax.History._actions.slice(
-                    0, SimileAjax.History._currentIndex - SimileAjax.History._baseIndex);
-
-                SimileAjax.History._actions.push(action);
-                SimileAjax.History._currentIndex++;
-
-                var diff = SimileAjax.History._actions.length - SimileAjax.History.maxHistoryLength;
-                if (diff > 0) {
-                    SimileAjax.History._actions = SimileAjax.History._actions.slice(diff);
-                    SimileAjax.History._baseIndex += diff;
-                }
-
-                try {
-                    SimileAjax.History._iframe.contentWindow.location.search =
-                        "?" + SimileAjax.History._currentIndex;
-                } catch (e) {
-                    /*
-                     *  We can't modify location.search most probably because it's a file:// url.
-                     *  We'll just going to modify the document's title.
-                     */
-                    var title = SimileAjax.History.formatHistoryEntryTitle(action.label);
-                    document.title = title;
-                }
-            }
-        } catch (e) {
-            SimileAjax.Debug.exception(e, "Error adding action {" + action.label + "} to history");
-        }
-    }, 0);
-};
-
-SimileAjax.History.addLengthyAction = function(perform, undo, label) {
-    SimileAjax.History.addAction({
-        perform:    perform,
-        undo:       undo,
-        label:      label,
-        uiLayer:    SimileAjax.WindowManager.getBaseLayer(),
-        lengthy:    true
-    });
-};
-
-SimileAjax.History._handleIFrameOnLoad = function() {
-    /*
-     *  This function is invoked when the user herself
-     *  navigates backward or forward. We need to adjust
-     *  the application's state accordingly.
-     */
-
-    try {
-        var q = SimileAjax.History._iframe.contentWindow.location.search;
-        var c = (q.length == 0) ? 0 : Math.max(0, parseInt(q.substr(1)));
-
-        var finishUp = function() {
-            var diff = c - SimileAjax.History._currentIndex;
-            SimileAjax.History._currentIndex += diff;
-            SimileAjax.History._baseIndex += diff;
-
-            SimileAjax.History._iframe.contentWindow.location.search = "?" + c;
-        };
-
-        if (c < SimileAjax.History._currentIndex) { // need to undo
-            SimileAjax.History._listeners.fire("onBeforeUndoSeveral", []);
-            window.setTimeout(function() {
-                while (SimileAjax.History._currentIndex > c &&
-                       SimileAjax.History._currentIndex > SimileAjax.History._baseIndex) {
-
-                    SimileAjax.History._currentIndex--;
-
-                    var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
-
-                    try {
-                        action.undo();
-                    } catch (e) {
-                        SimileAjax.Debug.exception(e, "History: Failed to undo action {" + action.label + "}");
-                    }
-                }
-
-                SimileAjax.History._listeners.fire("onAfterUndoSeveral", []);
-                finishUp();
-            }, 0);
-        } else if (c > SimileAjax.History._currentIndex) { // need to redo
-            SimileAjax.History._listeners.fire("onBeforeRedoSeveral", []);
-            window.setTimeout(function() {
-                while (SimileAjax.History._currentIndex < c &&
-                       SimileAjax.History._currentIndex - SimileAjax.History._baseIndex < SimileAjax.History._actions.length) {
-
-                    var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
-
-                    try {
-                        action.perform();
-                    } catch (e) {
-                        SimileAjax.Debug.exception(e, "History: Failed to redo action {" + action.label + "}");
-                    }
-
-                    SimileAjax.History._currentIndex++;
-                }
-
-                SimileAjax.History._listeners.fire("onAfterRedoSeveral", []);
-                finishUp();
-            }, 0);
-        } else {
-            var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
-            var title = (index >= 0 && index < SimileAjax.History._actions.length) ?
-                SimileAjax.History.formatHistoryEntryTitle(SimileAjax.History._actions[index].label) :
-                SimileAjax.History._plainDocumentTitle;
-
-            SimileAjax.History._iframe.contentWindow.document.title = title;
-            document.title = title;
-        }
-    } catch (e) {
-        // silent
-    }
-};
-
-SimileAjax.History.getNextUndoAction = function() {
-    try {
-        var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
-        return SimileAjax.History._actions[index];
-    } catch (e) {
-        return null;
-    }
-};
-
-SimileAjax.History.getNextRedoAction = function() {
-    try {
-        var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex;
-        return SimileAjax.History._actions[index];
-    } catch (e) {
-        return null;
-    }
-};
-/**
- * @fileOverview UI layers and window-wide dragging
- * @name SimileAjax.WindowManager
- */
-
-/**
- *  This is a singleton that keeps track of UI layers (modal and
- *  modeless) and enables/disables UI elements based on which layers
- *  they belong to. It also provides window-wide dragging
- *  implementation.
- */
-SimileAjax.WindowManager = {
-    _initialized:       false,
-    _listeners:         [],
-
-    _draggedElement:                null,
-    _draggedElementCallback:        null,
-    _dropTargetHighlightElement:    null,
-    _lastCoords:                    null,
-    _ghostCoords:                   null,
-    _draggingMode:                  "",
-    _dragging:                      false,
-
-    _layers:            []
-};
-
-SimileAjax.WindowManager.initialize = function() {
-    if (SimileAjax.WindowManager._initialized) {
-        return;
-    }
-
-    SimileAjax.DOM.registerEvent(document.body, "mousedown", SimileAjax.WindowManager._onBodyMouseDown);
-    SimileAjax.DOM.registerEvent(document.body, "mousemove", SimileAjax.WindowManager._onBodyMouseMove);
-    SimileAjax.DOM.registerEvent(document.body, "mouseup",   SimileAjax.WindowManager._onBodyMouseUp);
-    SimileAjax.DOM.registerEvent(document, "keydown",       SimileAjax.WindowManager._onBodyKeyDown);
-    SimileAjax.DOM.registerEvent(document, "keyup",         SimileAjax.WindowManager._onBodyKeyUp);
-
-    SimileAjax.WindowManager._layers.push({index: 0});
-
-    SimileAjax.WindowManager._historyListener = {
-        onBeforeUndoSeveral:    function() {},
-        onAfterUndoSeveral:     function() {},
-        onBeforeUndo:           function() {},
-        onAfterUndo:            function() {},
-
-        onBeforeRedoSeveral:    function() {},
-        onAfterRedoSeveral:     function() {},
-        onBeforeRedo:           function() {},
-        onAfterRedo:            function() {}
-    };
-    SimileAjax.History.addListener(SimileAjax.WindowManager._historyListener);
-
-    SimileAjax.WindowManager._initialized = true;
-};
-
-SimileAjax.WindowManager.getBaseLayer = function() {
-    SimileAjax.WindowManager.initialize();
-    return SimileAjax.WindowManager._layers[0];
-};
-
-SimileAjax.WindowManager.getHighestLayer = function() {
-    SimileAjax.WindowManager.initialize();
-    return SimileAjax.WindowManager._layers[SimileAjax.WindowManager._layers.length - 1];
-};
-
-SimileAjax.WindowManager.registerEventWithObject = function(elmt, eventName, obj, handlerName, layer) {
-    SimileAjax.WindowManager.registerEvent(
-        elmt,
-        eventName,
-        function(elmt2, evt, target) {
-            return obj[handlerName].call(obj, elmt2, evt, target);
-        },
-        layer
-    );
-};
-
-SimileAjax.WindowManager.registerEvent = function(elmt, eventName, handler, layer) {
-    if (layer == null) {
-        layer = SimileAjax.WindowManager.getHighestLayer();
-    }
-
-    var handler2 = function(elmt, evt, target) {
-        if (SimileAjax.WindowManager._canProcessEventAtLayer(layer)) {
-            SimileAjax.WindowManager._popToLayer(layer.index);
-            try {
-                handler(elmt, evt, target);
-            } catch (e) {
-                SimileAjax.Debug.exception(e);
-            }
-        }
-        SimileAjax.DOM.cancelEvent(evt);
-        return false;
-    }
-
-    SimileAjax.DOM.registerEvent(elmt, eventName, handler2);
-};
-
-SimileAjax.WindowManager.pushLayer = function(f, ephemeral, elmt) {
-    var layer = { onPop: f, index: SimileAjax.WindowManager._layers.length, ephemeral: (ephemeral), elmt: elmt };
-    SimileAjax.WindowManager._layers.push(layer);
-
-    return layer;
-};
-
-SimileAjax.WindowManager.popLayer = function(layer) {
-    for (var i = 1; i < SimileAjax.WindowManager._layers.length; i++) {
-        if (SimileAjax.WindowManager._layers[i] == layer) {
-            SimileAjax.WindowManager._popToLayer(i - 1);
-            break;
-        }
-    }
-};
-
-SimileAjax.WindowManager.popAllLayers = function() {
-    SimileAjax.WindowManager._popToLayer(0);
-};
-
-SimileAjax.WindowManager.registerForDragging = function(elmt, callback, layer) {
-    SimileAjax.WindowManager.registerEvent(
-        elmt,
-        "mousedown",
-        function(elmt, evt, target) {
-            SimileAjax.WindowManager._handleMouseDown(elmt, evt, callback);
-        },
-        layer
-    );
-};
-
-SimileAjax.WindowManager._popToLayer = function(level) {
-    while (level+1 < SimileAjax.WindowManager._layers.length) {
-        try {
-            var layer = SimileAjax.WindowManager._layers.pop();
-            if (layer.onPop != null) {
-                layer.onPop();
-            }
-        } catch (e) {
-        }
-    }
-};
-
-SimileAjax.WindowManager._canProcessEventAtLayer = function(layer) {
-    if (layer.index == (SimileAjax.WindowManager._layers.length - 1)) {
-        return true;
-    }
-    for (var i = layer.index + 1; i < SimileAjax.WindowManager._layers.length; i++) {
-        if (!SimileAjax.WindowManager._layers[i].ephemeral) {
-            return false;
-        }
-    }
-    return true;
-};
-
-SimileAjax.WindowManager.cancelPopups = function(evt) {
-    var evtCoords = (evt) ? SimileAjax.DOM.getEventPageCoordinates(evt) : { x: -1, y: -1 };
-
-    var i = SimileAjax.WindowManager._layers.length - 1;
-    while (i > 0 && SimileAjax.WindowManager._layers[i].ephemeral) {
-        var layer = SimileAjax.WindowManager._layers[i];
-        if (layer.elmt != null) { // if event falls within main element of layer then don't cancel
-            var elmt = layer.elmt;
-            var elmtCoords = SimileAjax.DOM.getPageCoordinates(elmt);
-            if (evtCoords.x >= elmtCoords.left && evtCoords.x < (elmtCoords.left + elmt.offsetWidth) &&
-                evtCoords.y >= elmtCoords.top && evtCoords.y < (elmtCoords.top + elmt.offsetHeight)) {
-                break;
-            }
-        }
-        i--;
-    }
-    SimileAjax.WindowManager._popToLayer(i);
-};
-
-SimileAjax.WindowManager._onBodyMouseDown = function(elmt, evt, target) {
-    if (!("eventPhase" in evt) || evt.eventPhase == evt.BUBBLING_PHASE) {
-        SimileAjax.WindowManager.cancelPopups(evt);
-    }
-};
-
-SimileAjax.WindowManager._handleMouseDown = function(elmt, evt, callback) {
-    SimileAjax.WindowManager._draggedElement = elmt;
-    SimileAjax.WindowManager._draggedElementCallback = callback;
-    SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
-
-    SimileAjax.DOM.cancelEvent(evt);
-    return false;
-};
-
-SimileAjax.WindowManager._onBodyKeyDown = function(elmt, evt, target) {
-    if (SimileAjax.WindowManager._dragging) {
-        if (evt.keyCode == 27) { // esc
-            SimileAjax.WindowManager._cancelDragging();
-        } else if ((evt.keyCode == 17 || evt.keyCode == 16) && SimileAjax.WindowManager._draggingMode != "copy") {
-            SimileAjax.WindowManager._draggingMode = "copy";
-
-            var img = SimileAjax.Graphics.createTranslucentImage(SimileAjax.urlPrefix + "images/copy.png");
-            img.style.position = "absolute";
-            img.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
-            img.style.top = (SimileAjax.WindowManager._ghostCoords.top) + "px";
-            document.body.appendChild(img);
-
-            SimileAjax.WindowManager._draggingModeIndicatorElmt = img;
-        }
-    }
-};
-
-SimileAjax.WindowManager._onBodyKeyUp = function(elmt, evt, target) {
-    if (SimileAjax.WindowManager._dragging) {
-        if (evt.keyCode == 17 || evt.keyCode == 16) {
-            SimileAjax.WindowManager._draggingMode = "";
-            if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
-                document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
-                SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
-            }
-        }
-    }
-};
-
-SimileAjax.WindowManager._onBodyMouseMove = function(elmt, evt, target) {
-    if (SimileAjax.WindowManager._draggedElement != null) {
-        var callback = SimileAjax.WindowManager._draggedElementCallback;
-
-        var lastCoords = SimileAjax.WindowManager._lastCoords;
-        var diffX = evt.clientX - lastCoords.x;
-        var diffY = evt.clientY - lastCoords.y;
-
-        if (!SimileAjax.WindowManager._dragging) {
-            if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) {
-                try {
-                    if ("onDragStart" in callback) {
-                        callback.onDragStart();
-                    }
-
-                    if ("ghost" in callback && callback.ghost) {
-                        var draggedElmt = SimileAjax.WindowManager._draggedElement;
-
-                        SimileAjax.WindowManager._ghostCoords = SimileAjax.DOM.getPageCoordinates(draggedElmt);
-                        SimileAjax.WindowManager._ghostCoords.left += diffX;
-                        SimileAjax.WindowManager._ghostCoords.top += diffY;
-
-                        var ghostElmt = draggedElmt.cloneNode(true);
-                        ghostElmt.style.position = "absolute";
-                        ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
-                        ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
-                        ghostElmt.style.zIndex = 1000;
-                        SimileAjax.Graphics.setOpacity(ghostElmt, 50);
-
-                        document.body.appendChild(ghostElmt);
-                        callback._ghostElmt = ghostElmt;
-                    }
-
-                    SimileAjax.WindowManager._dragging = true;
-                    SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
-
-                    document.body.focus();
-                } catch (e) {
-                    SimileAjax.Debug.exception("WindowManager: Error handling mouse down", e);
-                    SimileAjax.WindowManager._cancelDragging();
-                }
-            }
-        } else {
-            try {
-                SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
-
-                if ("onDragBy" in callback) {
-                    callback.onDragBy(diffX, diffY);
-                }
-
-                if ("_ghostElmt" in callback) {
-                    var ghostElmt = callback._ghostElmt;
-
-                    SimileAjax.WindowManager._ghostCoords.left += diffX;
-                    SimileAjax.WindowManager._ghostCoords.top += diffY;
-
-                    ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
-                    ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
-                    if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
-                        var indicatorElmt = SimileAjax.WindowManager._draggingModeIndicatorElmt;
-
-                        indicatorElmt.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
-                        indicatorElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
-                    }
-
-                    if ("droppable" in callback && callback.droppable) {
-                        var coords = SimileAjax.DOM.getEventPageCoordinates(evt);
-                        var target = SimileAjax.DOM.hittest(
-                            coords.x, coords.y,
-                            [   SimileAjax.WindowManager._ghostElmt,
-                                SimileAjax.WindowManager._dropTargetHighlightElement
-                            ]
-                        );
-                        target = SimileAjax.WindowManager._findDropTarget(target);
-
-                        if (target != SimileAjax.WindowManager._potentialDropTarget) {
-                            if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
-                                document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
-
-                                SimileAjax.WindowManager._dropTargetHighlightElement = null;
-                                SimileAjax.WindowManager._potentialDropTarget = null;
-                            }
-
-                            var droppable = false;
-                            if (target != null) {
-                                if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
-                                    (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
-
-                                    droppable = true;
-                                }
-                            }
-
-                            if (droppable) {
-                                var border = 4;
-                                var targetCoords = SimileAjax.DOM.getPageCoordinates(target);
-                                var highlight = document.createElement("div");
-                                highlight.style.border = border + "px solid yellow";
-                                highlight.style.backgroundColor = "yellow";
-                                highlight.style.position = "absolute";
-                                highlight.style.left = targetCoords.left + "px";
-                                highlight.style.top = targetCoords.top + "px";
-                                highlight.style.width = (target.offsetWidth - border * 2) + "px";
-                                highlight.style.height = (target.offsetHeight - border * 2) + "px";
-                                SimileAjax.Graphics.setOpacity(highlight, 30);
-                                document.body.appendChild(highlight);
-
-                                SimileAjax.WindowManager._potentialDropTarget = target;
-                                SimileAjax.WindowManager._dropTargetHighlightElement = highlight;
-                            }
-                        }
-                    }
-                }
-            } catch (e) {
-                SimileAjax.Debug.exception("WindowManager: Error handling mouse move", e);
-                SimileAjax.WindowManager._cancelDragging();
-            }
-        }
-
-        SimileAjax.DOM.cancelEvent(evt);
-        return false;
-    }
-};
-
-SimileAjax.WindowManager._onBodyMouseUp = function(elmt, evt, target) {
-    if (SimileAjax.WindowManager._draggedElement != null) {
-        try {
-            if (SimileAjax.WindowManager._dragging) {
-                var callback = SimileAjax.WindowManager._draggedElementCallback;
-                if ("onDragEnd" in callback) {
-                    callback.onDragEnd();
-                }
-                if ("droppable" in callback && callback.droppable) {
-                    var dropped = false;
-
-                    var target = SimileAjax.WindowManager._potentialDropTarget;
-                    if (target != null) {
-                        if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
-                            (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
-
-                            if ("onDropOn" in callback) {
-                                callback.onDropOn(target);
-                            }
-                            target.ondrop(SimileAjax.WindowManager._draggedElement, SimileAjax.WindowManager._draggingMode);
-
-                            dropped = true;
-                        }
-                    }
-
-                    if (!dropped) {
-                        // TODO: do holywood explosion here
-                    }
-                }
-            }
-        } finally {
-            SimileAjax.WindowManager._cancelDragging();
-        }
-
-        SimileAjax.DOM.cancelEvent(evt);
-        return false;
-    }
-};
-
-SimileAjax.WindowManager._cancelDragging = function() {
-    var callback = SimileAjax.WindowManager._draggedElementCallback;
-    if ("_ghostElmt" in callback) {
-        var ghostElmt = callback._ghostElmt;
-        document.body.removeChild(ghostElmt);
-
-        delete callback._ghostElmt;
-    }
-    if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
-        document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
-        SimileAjax.WindowManager._dropTargetHighlightElement = null;
-    }
-    if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
-        document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
-        SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
-    }
-
-    SimileAjax.WindowManager._draggedElement = null;
-    SimileAjax.WindowManager._draggedElementCallback = null;
-    SimileAjax.WindowManager._potentialDropTarget = null;
-    SimileAjax.WindowManager._dropTargetHighlightElement = null;
-    SimileAjax.WindowManager._lastCoords = null;
-    SimileAjax.WindowManager._ghostCoords = null;
-    SimileAjax.WindowManager._draggingMode = "";
-    SimileAjax.WindowManager._dragging = false;
-};
-
-SimileAjax.WindowManager._findDropTarget = function(elmt) {
-    while (elmt != null) {
-        if ("ondrop" in elmt && (typeof elmt.ondrop) == "function") {
-            break;
-        }
-        elmt = elmt.parentNode;
-    }
-    return elmt;
-};
-/*
- *  Timeline API
- *
- *  This file will load all the Javascript files
- *  necessary to make the standard timeline work.
- *  It also detects the default locale.
- *
- *  To run from the MIT copy of Timeline:
- *  Include this file in your HTML file as follows:
- *
- *    <script src="http://api.simile-widgets.org/timeline/2.3.1/timeline-api.js"
- *     type="text/javascript"></script>
- *
- *
- * To host the Timeline files on your own server:
- *   1) Install the Timeline and Simile-Ajax files onto your webserver using
- *      timeline_libraries.zip or timeline_source.zip
- *
- *   2) Set global js variables used to send parameters to this script:
- *        var Timeline_ajax_url -- url for simile-ajax-api.js
- *        var Timeline_urlPrefix -- url for the *directory* that contains timeline-api.js
- *            Include trailing slash
- *        var Timeline_parameters='bundle=true'; // you must set bundle to true if you are using
- *                                               // timeline_libraries.zip since only the
- *                                               // bundled libraries are included
- *
- * eg your html page would include
- *
- *   <script>
- *     var Timeline_ajax_url="http://YOUR_SERVER/javascripts/timeline/timeline_ajax/simile-ajax-api.js";
- *     var Timeline_urlPrefix='http://YOUR_SERVER/javascripts/timeline/timeline_js/';
- *     var Timeline_parameters='bundle=true';
- *   </script>
- *   <script src="http://YOUR_SERVER/javascripts/timeline/timeline_js/timeline-api.js"
- *     type="text/javascript">
- *   </script>
- *
- * SCRIPT PARAMETERS
- * This script auto-magically figures out locale and has defaults for other parameters
- * To set parameters explicity, set js global variable Timeline_parameters or include as
- * parameters on the url using GET style. Eg the two next lines pass the same parameters:
- *     Timeline_parameters='bundle=true';                    // pass parameter via js variable
- *     <script src="http://....timeline-api.js?bundle=true"  // pass parameter via url
- *
- * Parameters
- *   timeline-use-local-resources --
- *   bundle -- true: use the single js bundle file; false: load individual files (for debugging)
- *   locales --
- *   defaultLocale --
- *   forceLocale -- force locale to be a particular value--used for debugging. Normally locale is determined
- *                  by browser's and server's locale settings.
- *
- * DEBUGGING
- * If you have a problem with Timeline, the first step is to use the unbundled Javascript files. To do so:
- * To use the unbundled Timeline and Ajax libraries
- * Change
- *   <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=true" type="text/javascript"></script>
- * To
- *   <script>var Timeline_ajax_url = "http://api.simile-widgets.org/ajax/2.2.1/simile-ajax-api.js?bundle=false"</script>
- *   <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=false" type="text/javascript"></script>
- *
- * 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() {
-
-    var simile_ajax_ver = "2.2.1"; // ===========>>>  current Simile-Ajax version
-
-    var useLocalResources = false;
-    if (document.location.search.length > 0) {
-        var params = document.location.search.substr(1).split("&");
-        for (var i = 0; i < params.length; i++) {
-            if (params[i] == "timeline-use-local-resources") {
-                useLocalResources = true;
-            }
-        }
-    };
-
-    var loadMe = function() {
-        if ("Timeline" in window) {
-            return;
-        }
-
-        window.Timeline = new Object();
-        window.Timeline.DateTime = window.SimileAjax.DateTime; // for backward compatibility
-
-        var bundle = false;
-        var javascriptFiles = [
-            "timeline.js",
-            "band.js",
-            "themes.js",
-            "ethers.js",
-            "ether-painters.js",
-            "event-utils.js",
-            "labellers.js",
-            "sources.js",
-            "original-painter.js",
-            "detailed-painter.js",
-            "overview-painter.js",
-            "compact-painter.js",
-            "decorators.js",
-            "units.js"
-        ];
-        var cssFiles = [
-            "timeline.css",
-            "ethers.css",
-            "events.css"
-        ];
-
-        var localizedJavascriptFiles = [
-            "timeline.js",
-            "labellers.js"
-        ];
-        var localizedCssFiles = [
-        ];
-
-        // ISO-639 language codes, ISO-3166 country codes (2 characters)
-        var supportedLocales = [
-            "cs",       // Czech
-            "de",       // German
-            "en",       // English
-            "es",       // Spanish
-            "fr",       // French
-            "it",       // Italian
-            "nl",       // Dutch (The Netherlands)
-            "ru",       // Russian
-            "se",       // Swedish
-            "tr",       // Turkish
-            "vi",       // Vietnamese
-            "zh"        // Chinese
-        ];
-
-        try {
-            var desiredLocales = [ "en" ],
-                defaultServerLocale = "en",
-                forceLocale = null;
-
-            var parseURLParameters = function(parameters) {
-                var params = parameters.split("&");
-                for (var p = 0; p < params.length; p++) {
-                    var pair = params[p].split("=");
-                    if (pair[0] == "locales") {
-                        desiredLocales = desiredLocales.concat(pair[1].split(","));
-                    } else if (pair[0] == "defaultLocale") {
-                        defaultServerLocale = pair[1];
-                    } else if (pair[0] == "forceLocale") {
-                        forceLocale = pair[1];
-                        desiredLocales = desiredLocales.concat(pair[1].split(","));
-                    } else if (pair[0] == "bundle") {
-                        bundle = pair[1] != "false";
-                    }
-                }
-            };
-
-            (function() {
-                if (typeof Timeline_urlPrefix == "string") {
-                    Timeline.urlPrefix = Timeline_urlPrefix;
-                    if (typeof Timeline_parameters == "string") {
-                        parseURLParameters(Timeline_parameters);
-                    }
-                } else {
-                    var heads = document.documentElement.getElementsByTagName("head");
-                    for (var h = 0; h < heads.length; h++) {
-                        var scripts = heads[h].getElementsByTagName("script");
-                        for (var s = 0; s < scripts.length; s++) {
-                            var url = scripts[s].src;
-                            var i = url.indexOf("timeline-api.js");
-                            if (i >= 0) {
-                                Timeline.urlPrefix = url.substr(0, i);
-                                var q = url.indexOf("?");
-                                if (q > 0) {
-                                    parseURLParameters(url.substr(q + 1));
-                                }
-                                return;
-                            }
-                        }
-                    }
-                    throw new Error("Failed to derive URL prefix for Timeline API code files");
-                }
-            })();
-
-            var includeJavascriptFiles = function(urlPrefix, filenames) {
-                SimileAjax.includeJavascriptFiles(document, urlPrefix, filenames);
-            }
-            var includeCssFiles = function(urlPrefix, filenames) {
-                SimileAjax.includeCssFiles(document, urlPrefix, filenames);
-            }
-
-            /*
-             *  Include non-localized files
-             */
-            if (bundle) {
-                includeJavascriptFiles(Timeline.urlPrefix, [ "timeline-bundle.js" ]);
-                includeCssFiles(Timeline.urlPrefix, [ "timeline-bundle.css" ]);
-            } else {
-                // XXX adim includeJavascriptFiles(Timeline.urlPrefix + "scripts/", javascriptFiles);
-                // XXX adim includeCssFiles(Timeline.urlPrefix + "styles/", cssFiles);
-            }
-
-            /*
-             *  Include localized files
-             */
-            var loadLocale = [];
-            loadLocale[defaultServerLocale] = true;
-
-            var tryExactLocale = function(locale) {
-                for (var l = 0; l < supportedLocales.length; l++) {
-                    if (locale == supportedLocales[l]) {
-                        loadLocale[locale] = true;
-                        return true;
-                    }
-                }
-                return false;
-            }
-            var tryLocale = function(locale) {
-                if (tryExactLocale(locale)) {
-                    return locale;
-                }
-
-                var dash = locale.indexOf("-");
-                if (dash > 0 && tryExactLocale(locale.substr(0, dash))) {
-                    return locale.substr(0, dash);
-                }
-
-                return null;
-            }
-
-            for (var l = 0; l < desiredLocales.length; l++) {
-                tryLocale(desiredLocales[l]);
-            }
-
-            var defaultClientLocale = defaultServerLocale;
-            var defaultClientLocales = ("language" in navigator ? navigator.language : navigator.browserLanguage).split(";");
-            for (var l = 0; l < defaultClientLocales.length; l++) {
-                var locale = tryLocale(defaultClientLocales[l]);
-                if (locale != null) {
-                    defaultClientLocale = locale;
-                    break;
-                }
-            }
-
-            for (var l = 0; l < supportedLocales.length; l++) {
-                var locale = supportedLocales[l];
-                if (loadLocale[locale]) {
-                    // XXX adim includeJavascriptFiles(Timeline.urlPrefix + "scripts/l10n/" + locale + "/", localizedJavascriptFiles);
-                    // XXX adim includeCssFiles(Timeline.urlPrefix + "styles/l10n/" + locale + "/", localizedCssFiles);
-                }
-            }
-
-            if (forceLocale == null) {
-              Timeline.serverLocale = defaultServerLocale;
-              Timeline.clientLocale = defaultClientLocale;
-            } else {
-              Timeline.serverLocale = forceLocale;
-              Timeline.clientLocale = forceLocale;
-            }
-        } catch (e) {
-            alert(e);
-        }
-    };
-
-    /*
-     *  Load SimileAjax if it's not already loaded
-     */
-    if (typeof SimileAjax == "undefined") {
-        window.SimileAjax_onLoad = loadMe;
-
-        var url = useLocalResources ?
-            "http://127.0.0.1:9999/ajax/api/simile-ajax-api.js?bundle=false" :
-            "http://api.simile-widgets.org/ajax/" + simile_ajax_ver + "/simile-ajax-api.js";
-        if (typeof Timeline_ajax_url == "string") {
-           url = Timeline_ajax_url;
-        }
-        var createScriptElement = function() {
-            var script = document.createElement("script");
-            script.type = "text/javascript";
-            script.language = "JavaScript";
-            script.src = url;
-            document.getElementsByTagName("head")[0].appendChild(script);
-        }
-        if (document.body == null) {
-            try {
-                document.write("<script src='" + url + "' type='text/javascript'></script>");
-            } catch (e) {
-                createScriptElement();
-            }
-        } else {
-            createScriptElement();
-        }
-    } else {
-        loadMe();
-    }
-})();
-/*
- *
- * Coding standards:
- *
- * We aim towards Douglas Crockford's Javascript conventions.
- * See:  http://javascript.crockford.com/code.html
- * See also: http://www.crockford.com/javascript/javascript.html
- *
- * That said, this JS code was written before some recent JS
- * support libraries became widely used or available.
- * In particular, the _ character is used to indicate a class function or
- * variable that should be considered private to the class.
- *
- * The code mostly uses accessor methods for getting/setting the private
- * class variables.
- *
- * Over time, we'd like to formalize the convention by using support libraries
- * which enforce privacy in objects.
- *
- * 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
-Timeline.ajax_lib_version = SimileAjax.version;
-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;
-Timeline.VERTICAL = 1;
-Timeline._defaultTheme = null;
-
-Timeline.getDefaultLocale = function() {
-    return Timeline.clientLocale;
-};
-
-Timeline.create = function(elmt, bandInfos, orientation, unit) {
-    if (Timeline.timelines == null) {
-        Timeline.timelines = [];
-        // Timeline.timelines array can have null members--Timelines that
-        // once existed on the page, but were later disposed of.
-    }
-
-    var timelineID = Timeline.timelines.length;
-    Timeline.timelines[timelineID] = null; // placeholder until we have the object
-    var new_tl = new Timeline._Impl(elmt, bandInfos, orientation, unit,
-      timelineID);
-    Timeline.timelines[timelineID] = new_tl;
-    return new_tl;
-};
-
-Timeline.createBandInfo = function(params) {
-    var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
-
-    var eventSource = ("eventSource" in params) ? params.eventSource : null;
-
-    var etherParams = {
-        interval:           SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
-        pixelsPerInterval: params.intervalPixels,
-	theme: theme
-    };
-    if ('startsOn' in params || 'endsOn' in params) {
-	if ('startsOn' in params) {
-	    etherParams.startsOn = params.startsOn;
-	}
-	if ('endsOn' in params) {
-	    etherParams.endsOn = params.endsOn;
-	}
-    } else {
-	etherParams.centersOn = ("date" in params) ? params.date : new Date();
-    }
-    var ether = new Timeline.LinearEther(etherParams);
-
-    var etherPainter = new Timeline.GregorianEtherPainter({
-        unit:       params.intervalUnit,
-        multiple:   ("multiple" in params) ? params.multiple : 1,
-        theme:      theme,
-        align:      ("align" in params) ? params.align : undefined
-    });
-
-    var eventPainterParams = {
-        showText:   ("showEventText" in params) ? params.showEventText : true,
-        theme:      theme
-    };
-    // pass in custom parameters for the event painter
-    if ("eventPainterParams" in params) {
-        for (var prop in params.eventPainterParams) {
-            eventPainterParams[prop] = params.eventPainterParams[prop];
-        }
-    }
-
-    if ("trackHeight" in params) {
-        eventPainterParams.trackHeight = params.trackHeight;
-    }
-    if ("trackGap" in params) {
-        eventPainterParams.trackGap = params.trackGap;
-    }
-
-    var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
-    var eventPainter;
-    if ("eventPainter" in params) {
-        eventPainter = new params.eventPainter(eventPainterParams);
-    } else {
-        switch (layout) {
-            case "overview" :
-                eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
-                break;
-            case "detailed" :
-                eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
-                break;
-            default:
-                eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
-        }
-    }
-
-    return {
-        width:          params.width,
-        eventSource:    eventSource,
-        timeZone:       ("timeZone" in params) ? params.timeZone : 0,
-        ether:          ether,
-        etherPainter:   etherPainter,
-        eventPainter:   eventPainter,
-        theme:          theme,
-        zoomIndex:      ("zoomIndex" in params) ? params.zoomIndex : 0,
-        zoomSteps:      ("zoomSteps" in params) ? params.zoomSteps : null
-    };
-};
-
-Timeline.createHotZoneBandInfo = function(params) {
-    var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
-
-    var eventSource = ("eventSource" in params) ? params.eventSource : null;
-
-    var ether = new Timeline.HotZoneEther({
-        centersOn:          ("date" in params) ? params.date : new Date(),
-        interval:           SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
-        pixelsPerInterval:  params.intervalPixels,
-        zones:              params.zones,
-        theme:              theme
-    });
-
-    var etherPainter = new Timeline.HotZoneGregorianEtherPainter({
-        unit:       params.intervalUnit,
-        zones:      params.zones,
-        theme:      theme,
-        align:      ("align" in params) ? params.align : undefined
-    });
-
-    var eventPainterParams = {
-        showText:   ("showEventText" in params) ? params.showEventText : true,
-        theme:      theme
-    };
-    // pass in custom parameters for the event painter
-    if ("eventPainterParams" in params) {
-        for (var prop in params.eventPainterParams) {
-            eventPainterParams[prop] = params.eventPainterParams[prop];
-        }
-    }
-    if ("trackHeight" in params) {
-        eventPainterParams.trackHeight = params.trackHeight;
-    }
-    if ("trackGap" in params) {
-        eventPainterParams.trackGap = params.trackGap;
-    }
-
-    var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
-    var eventPainter;
-    if ("eventPainter" in params) {
-        eventPainter = new params.eventPainter(eventPainterParams);
-    } else {
-        switch (layout) {
-            case "overview" :
-                eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
-                break;
-            case "detailed" :
-                eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
-                break;
-            default:
-                eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
-        }
-    }
-    return {
-        width:          params.width,
-        eventSource:    eventSource,
-        timeZone:       ("timeZone" in params) ? params.timeZone : 0,
-        ether:          ether,
-        etherPainter:   etherPainter,
-        eventPainter:   eventPainter,
-        theme:          theme,
-        zoomIndex:      ("zoomIndex" in params) ? params.zoomIndex : 0,
-        zoomSteps:      ("zoomSteps" in params) ? params.zoomSteps : null
-    };
-};
-
-Timeline.getDefaultTheme = function() {
-    if (Timeline._defaultTheme == null) {
-        Timeline._defaultTheme = Timeline.ClassicTheme.create(Timeline.getDefaultLocale());
-    }
-    return Timeline._defaultTheme;
-};
-
-Timeline.setDefaultTheme = function(theme) {
-    Timeline._defaultTheme = theme;
-};
-
-Timeline.loadXML = function(url, f) {
-    var fError = function(statusText, status, xmlhttp) {
-        alert("Failed to load data xml from " + url + "\n" + statusText);
-    };
-    var fDone = function(xmlhttp) {
-        var xml = xmlhttp.responseXML;
-        if (!xml.documentElement && xmlhttp.responseStream) {
-            xml.load(xmlhttp.responseStream);
-        }
-        f(xml, url);
-    };
-    SimileAjax.XmlHttp.get(url, fError, fDone);
-};
-
-
-Timeline.loadJSON = function(url, f) {
-    var fError = function(statusText, status, xmlhttp) {
-        alert("Failed to load json data from " + url + "\n" + statusText);
-    };
-    var fDone = function(xmlhttp) {
-        f(eval('(' + xmlhttp.responseText + ')'), url);
-    };
-    SimileAjax.XmlHttp.get(url, fError, fDone);
-};
-
-Timeline.getTimelineFromID = function(timelineID) {
-    return Timeline.timelines[timelineID];
-};
-
-// Write the current Timeline version as the contents of element with id el_id
-Timeline.writeVersion = function(el_id) {
-  document.getElementById(el_id).innerHTML = this.display_version;
-};
-
-
-
-/*
- *  Timeline Implementation object
- *
- */
-Timeline._Impl = function(elmt, bandInfos, orientation, unit, timelineID) {
-    SimileAjax.WindowManager.initialize();
-
-    this._containerDiv = elmt;
-
-    this._bandInfos = bandInfos;
-    this._orientation = orientation == null ? Timeline.HORIZONTAL : orientation;
-    this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
-    this._starting = true; // is the Timeline being created? Used by autoWidth
-                           // functions
-    this._autoResizing = false;
-
-    // autoWidth is a "public" property of the Timeline object
-    this.autoWidth = bandInfos && bandInfos[0] && bandInfos[0].theme &&
-                     bandInfos[0].theme.autoWidth;
-    this.autoWidthAnimationTime = bandInfos && bandInfos[0] && bandInfos[0].theme &&
-                     bandInfos[0].theme.autoWidthAnimationTime;
-    this.timelineID = timelineID; // also public attribute
-    this.timeline_start = bandInfos && bandInfos[0] && bandInfos[0].theme &&
-                     bandInfos[0].theme.timeline_start;
-    this.timeline_stop  = bandInfos && bandInfos[0] && bandInfos[0].theme &&
-                     bandInfos[0].theme.timeline_stop;
-    this.timeline_at_start = false; // already at start or stop? Then won't
-    this.timeline_at_stop = false;  // try to move further in the wrong direction
-
-    this._initialize();
-};
-
-//
-// Public functions used by client sw
-//
-Timeline._Impl.prototype.dispose = function() {
-    for (var i = 0; i < this._bands.length; i++) {
-        this._bands[i].dispose();
-    }
-    this._bands = null;
-    this._bandInfos = null;
-    this._containerDiv.innerHTML = "";
-    // remove from array of Timelines
-    Timeline.timelines[this.timelineID] = null;
-};
-
-Timeline._Impl.prototype.getBandCount = function() {
-    return this._bands.length;
-};
-
-Timeline._Impl.prototype.getBand = function(index) {
-    return this._bands[index];
-};
-
-Timeline._Impl.prototype.finishedEventLoading = function() {
-    // Called by client after events have been loaded into Timeline
-    // Only used if the client has set autoWidth
-    // Sets width to Timeline's requested amount and will shrink down the div if
-    // need be.
-    this._autoWidthCheck(true);
-    this._starting = false;
-};
-
-Timeline._Impl.prototype.layout = function() {
-    // called by client when browser is resized
-    this._autoWidthCheck(true);
-    this._distributeWidths();
-};
-
-Timeline._Impl.prototype.paint = function() {
-    for (var i = 0; i < this._bands.length; i++) {
-        this._bands[i].paint();
-    }
-};
-
-Timeline._Impl.prototype.getDocument = function() {
-    return this._containerDiv.ownerDocument;
-};
-
-Timeline._Impl.prototype.addDiv = function(div) {
-    this._containerDiv.appendChild(div);
-};
-
-Timeline._Impl.prototype.removeDiv = function(div) {
-    this._containerDiv.removeChild(div);
-};
-
-Timeline._Impl.prototype.isHorizontal = function() {
-    return this._orientation == Timeline.HORIZONTAL;
-};
-
-Timeline._Impl.prototype.isVertical = function() {
-    return this._orientation == Timeline.VERTICAL;
-};
-
-Timeline._Impl.prototype.getPixelLength = function() {
-    return this._orientation == Timeline.HORIZONTAL ?
-        this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
-};
-
-Timeline._Impl.prototype.getPixelWidth = function() {
-    return this._orientation == Timeline.VERTICAL ?
-        this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
-};
-
-Timeline._Impl.prototype.getUnit = function() {
-    return this._unit;
-};
-
-Timeline._Impl.prototype.getWidthStyle = function() {
-    // which element.style attribute should be changed to affect Timeline's "width"
-    return this._orientation == Timeline.HORIZONTAL ? 'height' : 'width';
-};
-
-Timeline._Impl.prototype.loadXML = function(url, f) {
-    var tl = this;
-
-
-    var fError = function(statusText, status, xmlhttp) {
-        alert("Failed to load data xml from " + url + "\n" + statusText);
-        tl.hideLoadingMessage();
-    };
-    var fDone = function(xmlhttp) {
-        try {
-            var xml = xmlhttp.responseXML;
-            if (!xml.documentElement && xmlhttp.responseStream) {
-                xml.load(xmlhttp.responseStream);
-            }
-            f(xml, url);
-        } finally {
-            tl.hideLoadingMessage();
-        }
-    };
-
-    this.showLoadingMessage();
-    window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
-};
-
-Timeline._Impl.prototype.loadJSON = function(url, f) {
-    var tl = this;
-
-    var fError = function(statusText, status, xmlhttp) {
-        alert("Failed to load json data from " + url + "\n" + statusText);
-        tl.hideLoadingMessage();
-    };
-    var fDone = function(xmlhttp) {
-        try {
-            f(eval('(' + xmlhttp.responseText + ')'), url);
-        } finally {
-            tl.hideLoadingMessage();
-        }
-    };
-
-    this.showLoadingMessage();
-    window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
-};
-
-
-//
-// Private functions used by Timeline object functions
-//
-
-Timeline._Impl.prototype._autoWidthScrollListener = function(band) {
-    band.getTimeline()._autoWidthCheck(false);
-};
-
-// called to re-calculate auto width and adjust the overall Timeline div if needed
-Timeline._Impl.prototype._autoWidthCheck = function(okToShrink) {
-    var timeline = this; // this Timeline
-    var immediateChange = timeline._starting;
-    var newWidth = 0;
-
-    function changeTimelineWidth() {
-        var widthStyle = timeline.getWidthStyle();
-        if (immediateChange) {
-            timeline._containerDiv.style[widthStyle] = newWidth + 'px';
-        } else {
-        	  // animate change
-        	  timeline._autoResizing = true;
-        	  var animateParam ={};
-        	  animateParam[widthStyle] = newWidth + 'px';
-
-        	  SimileAjax.jQuery(timeline._containerDiv).animate(
-        	      animateParam, timeline.autoWidthAnimationTime,
-        	      'linear', function(){timeline._autoResizing = false;});
-        }
-    }
-
-    function checkTimelineWidth() {
-        var targetWidth = 0; // the new desired width
-        var currentWidth = timeline.getPixelWidth();
-
-        if (timeline._autoResizing) {
-        	return; // early return
-        }
-
-        // compute targetWidth
-        for (var i = 0; i < timeline._bands.length; i++) {
-            timeline._bands[i].checkAutoWidth();
-            targetWidth += timeline._bandInfos[i].width;
-        }
-
-        if (targetWidth > currentWidth || okToShrink) {
-            // yes, let's change the size
-            newWidth = targetWidth;
-            changeTimelineWidth();
-            timeline._distributeWidths();
-        }
-    }
-
-    // function's mainline
-    if (!timeline.autoWidth) {
-        return; // early return
-    }
-
-    checkTimelineWidth();
-};
-
-Timeline._Impl.prototype._initialize = function() {
-    var containerDiv = this._containerDiv;
-    var doc = containerDiv.ownerDocument;
-
-    containerDiv.className =
-        containerDiv.className.split(" ").concat("timeline-container").join(" ");
-
-	/*
-	 * Set css-class on container div that will define orientation
-	 */
-	var orientation = (this.isHorizontal()) ? 'horizontal' : 'vertical'
-	containerDiv.className +=' timeline-'+orientation;
-
-
-    while (containerDiv.firstChild) {
-        containerDiv.removeChild(containerDiv.firstChild);
-    }
-
-    /*
-     *  inserting copyright and link to simile
-     */
-    var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeline.urlPrefix + (this.isHorizontal() ? "images/copyright-vertical.png" : "images/copyright.png"));
-    elmtCopyright.className = "timeline-copyright";
-    elmtCopyright.title = "Timeline copyright SIMILE - www.code.google.com/p/simile-widgets/";
-    SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://www.simile-widgets.org/"; });
-    containerDiv.appendChild(elmtCopyright);
-
-    /*
-     *  creating bands
-     */
-    this._bands = [];
-    for (var i = 0; i < this._bandInfos.length; i++) {
-        var bandInfo = this._bandInfos[i];
-        var bandClass = bandInfo.bandClass || Timeline._Band;
-        var band = new bandClass(this, this._bandInfos[i], i);
-        this._bands.push(band);
-    }
-    this._distributeWidths();
-
-    /*
-     *  sync'ing bands
-     */
-    for (var i = 0; i < this._bandInfos.length; i++) {
-        var bandInfo = this._bandInfos[i];
-        if ("syncWith" in bandInfo) {
-            this._bands[i].setSyncWithBand(
-                this._bands[bandInfo.syncWith],
-                ("highlight" in bandInfo) ? bandInfo.highlight : false
-            );
-        }
-    }
-
-
-    if (this.autoWidth) {
-        for (var i = 0; i < this._bands.length; i++) {
-            this._bands[i].addOnScrollListener(this._autoWidthScrollListener);
-        }
-    }
-
-
-    /*
-     *  creating loading UI
-     */
-    var message = SimileAjax.Graphics.createMessageBubble(doc);
-    message.containerDiv.className = "timeline-message-container";
-    containerDiv.appendChild(message.containerDiv);
-
-    message.contentDiv.className = "timeline-message";
-    message.contentDiv.innerHTML = "<img src='" + Timeline.urlPrefix + "images/progress-running.gif' /> Loading...";
-
-    this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; };
-    this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; };
-};
-
-Timeline._Impl.prototype._distributeWidths = function() {
-    var length = this.getPixelLength();
-    var width = this.getPixelWidth();
-    var cumulativeWidth = 0;
-
-    for (var i = 0; i < this._bands.length; i++) {
-        var band = this._bands[i];
-        var bandInfos = this._bandInfos[i];
-        var widthString = bandInfos.width;
-        var bandWidth;
-
-        if (typeof widthString == 'string') {
-          var x =  widthString.indexOf("%");
-          if (x > 0) {
-              var percent = parseInt(widthString.substr(0, x));
-              bandWidth = Math.round(percent * width / 100);
-          } else {
-              bandWidth = parseInt(widthString);
-          }
-        } else {
-        	// was given an integer
-        	bandWidth = widthString;
-        }
-
-        band.setBandShiftAndWidth(cumulativeWidth, bandWidth);
-        band.setViewLength(length);
-
-        cumulativeWidth += bandWidth;
-    }
-};
-
-Timeline._Impl.prototype.shiftOK = function(index, shift) {
-    // Returns true if the proposed shift is ok
-    //
-    // Positive shift means going back in time
-    var going_back = shift > 0,
-        going_forward = shift < 0;
-
-    // Is there an edge?
-    if ((going_back    && this.timeline_start == null) ||
-        (going_forward && this.timeline_stop  == null) ||
-        (shift == 0)) {
-        return (true);  // early return
-    }
-
-    // If any of the bands has noted that it is changing the others,
-    // then this shift is a secondary shift in reaction to the real shift,
-    // which already happened. In such cases, ignore it. (The issue is
-    // that a positive original shift can cause a negative secondary shift,
-    // as the bands adjust.)
-    var secondary_shift = false;
-    for (var i = 0; i < this._bands.length && !secondary_shift; i++) {
-       secondary_shift = this._bands[i].busy();
-    }
-    if (secondary_shift) {
-        return(true); // early return
-    }
-
-    // If we are already at an edge, then don't even think about going any further
-    if ((going_back    && this.timeline_at_start) ||
-        (going_forward && this.timeline_at_stop)) {
-        return (false);  // early return
-    }
-
-    // Need to check all the bands
-    var ok = false; // return value
-    // If any of the bands will be or are showing an ok date, then let the shift proceed.
-    for (var i = 0; i < this._bands.length && !ok; i++) {
-       var band = this._bands[i];
-       if (going_back) {
-           ok = (i == index ? band.getMinVisibleDateAfterDelta(shift) : band.getMinVisibleDate())
-                >= this.timeline_start;
-       } else {
-           ok = (i == index ? band.getMaxVisibleDateAfterDelta(shift) : band.getMaxVisibleDate())
-                <= this.timeline_stop;
-       }
-    }
-
-    // process results
-    if (going_back) {
-       this.timeline_at_start = !ok;
-       this.timeline_at_stop = false;
-    } else {
-       this.timeline_at_stop = !ok;
-       this.timeline_at_start = false;
-    }
-    // This is where you could have an effect once per hitting an
-    // edge of the Timeline. Eg jitter the Timeline
-    //if (!ok) {
-        //alert(going_back ? "At beginning" : "At end");
-    //}
-    return (ok);
-};
-
-Timeline._Impl.prototype.zoom = function (zoomIn, x, y, target) {
-  var matcher = new RegExp("^timeline-band-([0-9]+)$");
-  var bandIndex = null;
-
-  var result = matcher.exec(target.id);
-  if (result) {
-    bandIndex = parseInt(result[1]);
-  }
-
-  if (bandIndex != null) {
-    this._bands[bandIndex].zoom(zoomIn, x, y, target);
-  }
-
-  this.paint();
-};
-
-/*
- *
- * Coding standards:
- *
- * We aim towards Douglas Crockford's Javascript conventions.
- * See:  http://javascript.crockford.com/code.html
- * See also: http://www.crockford.com/javascript/javascript.html
- *
- * That said, this JS code was written before some recent JS
- * support libraries became widely used or available.
- * In particular, the _ character is used to indicate a class function or
- * variable that should be considered private to the class.
- *
- * The code mostly uses accessor methods for getting/setting the private
- * class variables.
- *
- * Over time, we'd like to formalize the convention by using support libraries
- * which enforce privacy in objects.
- *
- * We also want to use jslint:  http://www.jslint.com/
- *
- *
- *
- */
-
-
-
-/*
- *  Band
- *
- */
-Timeline._Band = function(timeline, bandInfo, index) {
-    // hack for easier subclassing
-    if (timeline !== undefined) {
-        this.initialize(timeline, bandInfo, index);
-    }
-};
-
-Timeline._Band.prototype.initialize = function(timeline, bandInfo, index) {
-    // Set up the band's object
-
-    // Munge params: If autoWidth is on for the Timeline, then ensure that
-    // bandInfo.width is an integer
-    if (timeline.autoWidth && typeof bandInfo.width == 'string') {
-        bandInfo.width = bandInfo.width.indexOf("%") > -1 ? 0 : parseInt(bandInfo.width);
-    }
-
-    this._timeline = timeline;
-    this._bandInfo = bandInfo;
-
-    this._index = index;
-
-    this._locale = ("locale" in bandInfo) ? bandInfo.locale : Timeline.getDefaultLocale();
-    this._timeZone = ("timeZone" in bandInfo) ? bandInfo.timeZone : 0;
-    this._labeller = ("labeller" in bandInfo) ? bandInfo.labeller :
-        (("createLabeller" in timeline.getUnit()) ?
-            timeline.getUnit().createLabeller(this._locale, this._timeZone) :
-            new Timeline.GregorianDateLabeller(this._locale, this._timeZone));
-    this._theme = bandInfo.theme;
-    this._zoomIndex = ("zoomIndex" in bandInfo) ? bandInfo.zoomIndex : 0;
-    this._zoomSteps = ("zoomSteps" in bandInfo) ? bandInfo.zoomSteps : null;
-
-    this._dragging = false;
-    this._changing = false;
-    this._originalScrollSpeed = 5; // pixels
-    this._scrollSpeed = this._originalScrollSpeed;
-    this._viewOrthogonalOffset= 0; // vertical offset if the timeline is horizontal, and vice versa
-    this._onScrollListeners = [];
-
-    var b = this;
-    this._syncWithBand = null;
-    this._syncWithBandHandler = function(band) {
-        b._onHighlightBandScroll();
-    };
-    this._selectorListener = function(band) {
-        b._onHighlightBandScroll();
-    };
-
-    /*
-     *  Install a textbox to capture keyboard events
-     */
-    var inputDiv = this._timeline.getDocument().createElement("div");
-    inputDiv.className = "timeline-band-input";
-    this._timeline.addDiv(inputDiv);
-
-    this._keyboardInput = document.createElement("input");
-    this._keyboardInput.type = "text";
-    inputDiv.appendChild(this._keyboardInput);
-    SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keydown", this, "_onKeyDown");
-    SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keyup", this, "_onKeyUp");
-
-    /*
-     *  The band's outer most div that slides with respect to the timeline's div
-     */
-    this._div = this._timeline.getDocument().createElement("div");
-    this._div.id = "timeline-band-" + index;
-    this._div.className = "timeline-band timeline-band-" + index;
-    this._timeline.addDiv(this._div);
-
-    SimileAjax.DOM.registerEventWithObject(this._div, "mousedown", this, "_onMouseDown");
-    SimileAjax.DOM.registerEventWithObject(this._div, "mousemove", this, "_onMouseMove");
-    SimileAjax.DOM.registerEventWithObject(this._div, "mouseup", this, "_onMouseUp");
-    SimileAjax.DOM.registerEventWithObject(this._div, "mouseout", this, "_onMouseOut");
-    SimileAjax.DOM.registerEventWithObject(this._div, "dblclick", this, "_onDblClick");
-
-    var mouseWheel = this._theme!= null ? this._theme.mouseWheel : 'scroll'; // theme is not always defined
-    if (mouseWheel === 'zoom' || mouseWheel === 'scroll' || this._zoomSteps) {
-        // capture mouse scroll
-        if (SimileAjax.Platform.browser.isFirefox) {
-            SimileAjax.DOM.registerEventWithObject(this._div, "DOMMouseScroll", this, "_onMouseScroll");
-        } else {
-            SimileAjax.DOM.registerEventWithObject(this._div, "mousewheel", this, "_onMouseScroll");
-        }
-    }
-
-    /*
-     *  The inner div that contains layers
-     */
-    this._innerDiv = this._timeline.getDocument().createElement("div");
-    this._innerDiv.className = "timeline-band-inner";
-    this._div.appendChild(this._innerDiv);
-
-    /*
-     *  Initialize parts of the band
-     */
-    this._ether = bandInfo.ether;
-    bandInfo.ether.initialize(this, timeline);
-
-    this._etherPainter = bandInfo.etherPainter;
-    bandInfo.etherPainter.initialize(this, timeline);
-
-    this._eventSource = bandInfo.eventSource;
-    if (this._eventSource) {
-        this._eventListener = {
-            onAddMany: function() { b._onAddMany(); },
-            onClear:   function() { b._onClear(); }
-        }
-        this._eventSource.addListener(this._eventListener);
-    }
-
-    this._eventPainter = bandInfo.eventPainter;
-    this._eventTracksNeeded = 0;   // set by painter via updateEventTrackInfo
-    this._eventTrackIncrement = 0;
-    bandInfo.eventPainter.initialize(this, timeline);
-
-    this._decorators = ("decorators" in bandInfo) ? bandInfo.decorators : [];
-    for (var i = 0; i < this._decorators.length; i++) {
-        this._decorators[i].initialize(this, timeline);
-    }
-};
-
-Timeline._Band.SCROLL_MULTIPLES = 5;
-
-Timeline._Band.prototype.dispose = function() {
-    this.closeBubble();
-
-    if (this._eventSource) {
-        this._eventSource.removeListener(this._eventListener);
-        this._eventListener = null;
-        this._eventSource = null;
-    }
-
-    this._timeline = null;
-    this._bandInfo = null;
-
-    this._labeller = null;
-    this._ether = null;
-    this._etherPainter = null;
-    this._eventPainter = null;
-    this._decorators = null;
-
-    this._onScrollListeners = null;
-    this._syncWithBandHandler = null;
-    this._selectorListener = null;
-
-    this._div = null;
-    this._innerDiv = null;
-    this._keyboardInput = null;
-};
-
-Timeline._Band.prototype.addOnScrollListener = function(listener) {
-    this._onScrollListeners.push(listener);
-};
-
-Timeline._Band.prototype.removeOnScrollListener = function(listener) {
-    for (var i = 0; i < this._onScrollListeners.length; i++) {
-        if (this._onScrollListeners[i] == listener) {
-            this._onScrollListeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-Timeline._Band.prototype.setSyncWithBand = function(band, highlight) {
-    if (this._syncWithBand) {
-        this._syncWithBand.removeOnScrollListener(this._syncWithBandHandler);
-    }
-
-    this._syncWithBand = band;
-    this._syncWithBand.addOnScrollListener(this._syncWithBandHandler);
-    this._highlight = highlight;
-    this._positionHighlight();
-};
-
-Timeline._Band.prototype.getLocale = function() {
-    return this._locale;
-};
-
-Timeline._Band.prototype.getTimeZone = function() {
-    return this._timeZone;
-};
-
-Timeline._Band.prototype.getLabeller = function() {
-    return this._labeller;
-};
-
-Timeline._Band.prototype.getIndex = function() {
-    return this._index;
-};
-
-Timeline._Band.prototype.getEther = function() {
-    return this._ether;
-};
-
-Timeline._Band.prototype.getEtherPainter = function() {
-    return this._etherPainter;
-};
-
-Timeline._Band.prototype.getEventSource = function() {
-    return this._eventSource;
-};
-
-Timeline._Band.prototype.getEventPainter = function() {
-    return this._eventPainter;
-};
-
-Timeline._Band.prototype.getTimeline = function() {
-    return this._timeline;
-};
-
-// Autowidth support
-Timeline._Band.prototype.updateEventTrackInfo = function(tracks, increment) {
-    this._eventTrackIncrement = increment; // doesn't vary for a specific band
-
-    if (tracks > this._eventTracksNeeded) {
-        this._eventTracksNeeded = tracks;
-    }
-};
-
-// Autowidth support
-Timeline._Band.prototype.checkAutoWidth = function() {
-    // if a new (larger) width is needed by the band
-    // then: a) updates the band's bandInfo.width
-    //
-    // desiredWidth for the band is
-    //   (number of tracks + margin) * track increment
-    if (! this._timeline.autoWidth) {
-      return; // early return
-    }
-
-    var overviewBand = this._eventPainter.getType() == 'overview';
-    var margin = overviewBand ?
-       this._theme.event.overviewTrack.autoWidthMargin :
-       this._theme.event.track.autoWidthMargin;
-    var desiredWidth = Math.ceil((this._eventTracksNeeded + margin) *
-                       this._eventTrackIncrement);
-    // add offset amount (additional margin)
-    desiredWidth += overviewBand ? this._theme.event.overviewTrack.offset :
-                                   this._theme.event.track.offset;
-    var bandInfo = this._bandInfo;
-
-    if (desiredWidth != bandInfo.width) {
-        bandInfo.width = desiredWidth;
-    }
-};
-
-Timeline._Band.prototype.layout = function() {
-    this.paint();
-};
-
-Timeline._Band.prototype.paint = function() {
-    this._etherPainter.paint();
-    this._paintDecorators();
-    this._paintEvents();
-};
-
-Timeline._Band.prototype.softLayout = function() {
-    this.softPaint();
-};
-
-Timeline._Band.prototype.softPaint = function() {
-    this._etherPainter.softPaint();
-    this._softPaintDecorators();
-    this._softPaintEvents();
-};
-
-Timeline._Band.prototype.setBandShiftAndWidth = function(shift, width) {
-    var inputDiv = this._keyboardInput.parentNode;
-    var middle = shift + Math.floor(width / 2);
-    if (this._timeline.isHorizontal()) {
-        this._div.style.top = shift + "px";
-        this._div.style.height = width + "px";
-
-        inputDiv.style.top = middle + "px";
-        inputDiv.style.left = "-1em";
-    } else {
-        this._div.style.left = shift + "px";
-        this._div.style.width = width + "px";
-
-        inputDiv.style.left = middle + "px";
-        inputDiv.style.top = "-1em";
-    }
-};
-
-Timeline._Band.prototype.getViewWidth = function() {
-    if (this._timeline.isHorizontal()) {
-        return this._div.offsetHeight;
-    } else {
-        return this._div.offsetWidth;
-    }
-};
-
-Timeline._Band.prototype.setViewLength = function(length) {
-    this._viewLength = length;
-    this._recenterDiv();
-    this._onChanging();
-};
-
-Timeline._Band.prototype.getViewLength = function() {
-    return this._viewLength;
-};
-
-Timeline._Band.prototype.getTotalViewLength = function() {
-    return Timeline._Band.SCROLL_MULTIPLES * this._viewLength;
-};
-
-Timeline._Band.prototype.getViewOffset = function() {
-    return this._viewOffset;
-};
-
-Timeline._Band.prototype.getMinDate = function() {
-    return this._ether.pixelOffsetToDate(this._viewOffset);
-};
-
-Timeline._Band.prototype.getMaxDate = function() {
-    return this._ether.pixelOffsetToDate(this._viewOffset + Timeline._Band.SCROLL_MULTIPLES * this._viewLength);
-};
-
-Timeline._Band.prototype.getMinVisibleDate = function() {
-    return this._ether.pixelOffsetToDate(0);
-};
-
-Timeline._Band.prototype.getMinVisibleDateAfterDelta = function(delta) {
-    return this._ether.pixelOffsetToDate(delta);
-};
-
-Timeline._Band.prototype.getMaxVisibleDate = function() {
-    // Max date currently visible on band
-    return this._ether.pixelOffsetToDate(this._viewLength);
-};
-
-Timeline._Band.prototype.getMaxVisibleDateAfterDelta = function(delta) {
-    // Max date visible on band after delta px view change is applied
-    return this._ether.pixelOffsetToDate(this._viewLength + delta);
-};
-
-Timeline._Band.prototype.getCenterVisibleDate = function() {
-    return this._ether.pixelOffsetToDate(this._viewLength / 2);
-};
-
-Timeline._Band.prototype.setMinVisibleDate = function(date) {
-    if (!this._changing) {
-        this._moveEther(Math.round(-this._ether.dateToPixelOffset(date)));
-    }
-};
-
-Timeline._Band.prototype.setMaxVisibleDate = function(date) {
-    if (!this._changing) {
-        this._moveEther(Math.round(this._viewLength - this._ether.dateToPixelOffset(date)));
-    }
-};
-
-Timeline._Band.prototype.setCenterVisibleDate = function(date) {
-    if (!this._changing) {
-        this._moveEther(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)));
-    }
-};
-
-Timeline._Band.prototype.dateToPixelOffset = function(date) {
-    return this._ether.dateToPixelOffset(date) - this._viewOffset;
-};
-
-Timeline._Band.prototype.pixelOffsetToDate = function(pixels) {
-    return this._ether.pixelOffsetToDate(pixels + this._viewOffset);
-};
-
-Timeline._Band.prototype.getViewOrthogonalOffset = function() {
-    return this._viewOrthogonalOffset;
-};
-
-Timeline._Band.prototype.setViewOrthogonalOffset = function(offset) {
-    this._viewOrthogonalOffset = Math.max(0, offset);
-};
-
-Timeline._Band.prototype.createLayerDiv = function(zIndex, className) {
-    var div = this._timeline.getDocument().createElement("div");
-    div.className = "timeline-band-layer" + (typeof className == "string" ? (" " + className) : "");
-    div.style.zIndex = zIndex;
-    this._innerDiv.appendChild(div);
-
-    var innerDiv = this._timeline.getDocument().createElement("div");
-    innerDiv.className = "timeline-band-layer-inner";
-    if (SimileAjax.Platform.browser.isIE) {
-        innerDiv.style.cursor = "move";
-    } else {
-        innerDiv.style.cursor = "-moz-grab";
-    }
-    div.appendChild(innerDiv);
-
-    return innerDiv;
-};
-
-Timeline._Band.prototype.removeLayerDiv = function(div) {
-    this._innerDiv.removeChild(div.parentNode);
-};
-
-Timeline._Band.prototype.scrollToCenter = function(date, f) {
-    var pixelOffset = this._ether.dateToPixelOffset(date);
-    if (pixelOffset < -this._viewLength / 2) {
-        this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset + this._viewLength));
-    } else if (pixelOffset > 3 * this._viewLength / 2) {
-        this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset - this._viewLength));
-    }
-    this._autoScroll(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)), f);
-};
-
-Timeline._Band.prototype.showBubbleForEvent = function(eventID) {
-    var evt = this.getEventSource().getEvent(eventID);
-    if (evt) {
-        var self = this;
-        this.scrollToCenter(evt.getStart(), function() {
-            self._eventPainter.showBubble(evt);
-        });
-    }
-};
-
-Timeline._Band.prototype.zoom = function(zoomIn, x, y, target) {
-  if (!this._zoomSteps) {
-    // zoom disabled
-    return;
-  }
-
-  // shift the x value by our offset
-  x += this._viewOffset;
-
-  var zoomDate = this._ether.pixelOffsetToDate(x);
-  var netIntervalChange = this._ether.zoom(zoomIn);
-  this._etherPainter.zoom(netIntervalChange);
-
-  // shift our zoom date to the far left
-  this._moveEther(Math.round(-this._ether.dateToPixelOffset(zoomDate)));
-  // then shift it back to where the mouse was
-  this._moveEther(x);
-};
-
-Timeline._Band.prototype._onMouseDown = function(innerFrame, evt, target) {
-    this.closeBubble();
-
-    this._dragging = true;
-    this._dragX = evt.clientX;
-    this._dragY = evt.clientY;
-};
-
-Timeline._Band.prototype._onMouseMove = function(innerFrame, evt, target) {
-    if (this._dragging) {
-        var diffX = evt.clientX - this._dragX;
-        var diffY = evt.clientY - this._dragY;
-
-        this._dragX = evt.clientX;
-        this._dragY = evt.clientY;
-
-        if (this._timeline.isHorizontal()) {
-            this._moveEther(diffX, diffY);
-        } else {
-            this._moveEther(diffY, diffX);
-        }
-        this._positionHighlight();
-    }
-};
-
-Timeline._Band.prototype._onMouseUp = function(innerFrame, evt, target) {
-    this._dragging = false;
-    this._keyboardInput.focus();
-};
-
-Timeline._Band.prototype._onMouseOut = function(innerFrame, evt, target) {
-    var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
-    coords.x += this._viewOffset;
-    if (coords.x < 0 || coords.x > innerFrame.offsetWidth ||
-        coords.y < 0 || coords.y > innerFrame.offsetHeight) {
-        this._dragging = false;
-    }
-};
-
-Timeline._Band.prototype._onMouseScroll = function(innerFrame, evt, target) {
-  var now = new Date();
-  now = now.getTime();
-
-  if (!this._lastScrollTime || ((now - this._lastScrollTime) > 50)) {
-    // limit 1 scroll per 200ms due to FF3 sending multiple events back to back
-    this._lastScrollTime = now;
-
-    var delta = 0;
-    if (evt.wheelDelta) {
-      delta = evt.wheelDelta/120;
-    } else if (evt.detail) {
-      delta = -evt.detail/3;
-    }
-
-    // either scroll or zoom
-    var mouseWheel = this._theme.mouseWheel;
-
-    if (this._zoomSteps || mouseWheel === 'zoom') {
-      var loc = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
-      if (delta != 0) {
-        var zoomIn;
-        if (delta > 0)
-          zoomIn = true;
-        if (delta < 0)
-          zoomIn = false;
-        // call zoom on the timeline so we could zoom multiple bands if desired
-        this._timeline.zoom(zoomIn, loc.x, loc.y, innerFrame);
-      }
-    }
-    else if (mouseWheel === 'scroll') {
-    	var move_amt = 50 * (delta < 0 ? -1 : 1);
-      this._moveEther(move_amt);
-    }
-  }
-
-  // prevent bubble
-  if (evt.stopPropagation) {
-    evt.stopPropagation();
-  }
-  evt.cancelBubble = true;
-
-  // prevent the default action
-  if (evt.preventDefault) {
-    evt.preventDefault();
-  }
-  evt.returnValue = false;
-};
-
-Timeline._Band.prototype._onDblClick = function(innerFrame, evt, target) {
-    var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
-    var distance = coords.x - (this._viewLength / 2 - this._viewOffset);
-
-    this._autoScroll(-distance);
-};
-
-Timeline._Band.prototype._onKeyDown = function(keyboardInput, evt, target) {
-    if (!this._dragging) {
-        switch (evt.keyCode) {
-        case 27: // ESC
-            break;
-        case 37: // left arrow
-        case 38: // up arrow
-            this._scrollSpeed = Math.min(50, Math.abs(this._scrollSpeed * 1.05));
-            this._moveEther(this._scrollSpeed);
-            break;
-        case 39: // right arrow
-        case 40: // down arrow
-            this._scrollSpeed = -Math.min(50, Math.abs(this._scrollSpeed * 1.05));
-            this._moveEther(this._scrollSpeed);
-            break;
-        default:
-            return true;
-        }
-        this.closeBubble();
-
-        SimileAjax.DOM.cancelEvent(evt);
-        return false;
-    }
-    return true;
-};
-
-Timeline._Band.prototype._onKeyUp = function(keyboardInput, evt, target) {
-    if (!this._dragging) {
-        this._scrollSpeed = this._originalScrollSpeed;
-
-        switch (evt.keyCode) {
-        case 35: // end
-            this.setCenterVisibleDate(this._eventSource.getLatestDate());
-            break;
-        case 36: // home
-            this.setCenterVisibleDate(this._eventSource.getEarliestDate());
-            break;
-        case 33: // page up
-            this._autoScroll(this._timeline.getPixelLength());
-            break;
-        case 34: // page down
-            this._autoScroll(-this._timeline.getPixelLength());
-            break;
-        default:
-            return true;
-        }
-
-        this.closeBubble();
-
-        SimileAjax.DOM.cancelEvent(evt);
-        return false;
-    }
-    return true;
-};
-
-Timeline._Band.prototype._autoScroll = function(distance, f) {
-    var b = this;
-    var a = SimileAjax.Graphics.createAnimation(
-        function(abs, diff) {
-            b._moveEther(diff);
-        },
-        0,
-        distance,
-        1000,
-        f
-    );
-    a.run();
-};
-
-Timeline._Band.prototype._moveEther = function(shift, orthogonalShift) {
-    if (orthogonalShift === undefined) {
-        orthogonalShift = 0;
-    }
-
-    this.closeBubble();
-
-    // A positive shift means back in time
-    // Check that we're not moving beyond Timeline's limits
-    if (!this._timeline.shiftOK(this._index, shift)) {
-        return; // early return
-    }
-
-    this._viewOffset += shift;
-    this._viewOrthogonalOffset = Math.min(0, this._viewOrthogonalOffset + orthogonalShift);
-
-    this._ether.shiftPixels(-shift);
-    if (this._timeline.isHorizontal()) {
-        this._div.style.left = this._viewOffset + "px";
-    } else {
-        this._div.style.top = this._viewOffset + "px";
-    }
-
-    if (this._viewOffset > -this._viewLength * 0.5 ||
-        this._viewOffset < -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1.5)) {
-
-        this._recenterDiv();
-    } else {
-        this.softLayout();
-    }
-
-    this._onChanging();
-}
-
-Timeline._Band.prototype._onChanging = function() {
-    this._changing = true;
-
-    this._fireOnScroll();
-    this._setSyncWithBandDate();
-
-    this._changing = false;
-};
-
-Timeline._Band.prototype.busy = function() {
-    // Is this band busy changing other bands?
-    return(this._changing);
-};
-
-Timeline._Band.prototype._fireOnScroll = function() {
-    for (var i = 0; i < this._onScrollListeners.length; i++) {
-        this._onScrollListeners[i](this);
-    }
-};
-
-Timeline._Band.prototype._setSyncWithBandDate = function() {
-    if (this._syncWithBand) {
-        var centerDate = this._ether.pixelOffsetToDate(this.getViewLength() / 2);
-        this._syncWithBand.setCenterVisibleDate(centerDate);
-    }
-};
-
-Timeline._Band.prototype._onHighlightBandScroll = function() {
-    if (this._syncWithBand) {
-        var centerDate = this._syncWithBand.getCenterVisibleDate();
-        var centerPixelOffset = this._ether.dateToPixelOffset(centerDate);
-
-        this._moveEther(Math.round(this._viewLength / 2 - centerPixelOffset));
-
-        if (this._highlight) {
-            this._etherPainter.setHighlight(
-                this._syncWithBand.getMinVisibleDate(),
-                this._syncWithBand.getMaxVisibleDate());
-        }
-    }
-};
-
-Timeline._Band.prototype._onAddMany = function() {
-    this._paintEvents();
-};
-
-Timeline._Band.prototype._onClear = function() {
-    this._paintEvents();
-};
-
-Timeline._Band.prototype._positionHighlight = function() {
-    if (this._syncWithBand) {
-        var startDate = this._syncWithBand.getMinVisibleDate();
-        var endDate = this._syncWithBand.getMaxVisibleDate();
-
-        if (this._highlight) {
-            this._etherPainter.setHighlight(startDate, endDate);
-        }
-    }
-};
-
-Timeline._Band.prototype._recenterDiv = function() {
-    this._viewOffset = -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1) / 2;
-    if (this._timeline.isHorizontal()) {
-        this._div.style.left = this._viewOffset + "px";
-        this._div.style.width = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
-    } else {
-        this._div.style.top = this._viewOffset + "px";
-        this._div.style.height = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
-    }
-    this.layout();
-};
-
-Timeline._Band.prototype._paintEvents = function() {
-    this._eventPainter.paint();
-};
-
-Timeline._Band.prototype._softPaintEvents = function() {
-    this._eventPainter.softPaint();
-};
-
-Timeline._Band.prototype._paintDecorators = function() {
-    for (var i = 0; i < this._decorators.length; i++) {
-        this._decorators[i].paint();
-    }
-};
-
-Timeline._Band.prototype._softPaintDecorators = function() {
-    for (var i = 0; i < this._decorators.length; i++) {
-        this._decorators[i].softPaint();
-    }
-};
-
-Timeline._Band.prototype.closeBubble = function() {
-    SimileAjax.WindowManager.cancelPopups();
-};
-/*
- *  Classic Theme
- *
- */
-
-
-
-Timeline.ClassicTheme = new Object();
-
-Timeline.ClassicTheme.implementations = [];
-
-Timeline.ClassicTheme.create = function(locale) {
-    if (locale == null) {
-        locale = Timeline.getDefaultLocale();
-    }
-
-    var f = Timeline.ClassicTheme.implementations[locale];
-    if (f == null) {
-        f = Timeline.ClassicTheme._Impl;
-    }
-    return new f();
-};
-
-Timeline.ClassicTheme._Impl = function() {
-    this.firstDayOfWeek = 0; // Sunday
-
-    // Note: Many styles previously set here are now set using CSS
-    //       The comments indicate settings controlled by CSS, not
-    //       lines to be un-commented.
-    //
-    //
-    // Attributes autoWidth, autoWidthAnimationTime, timeline_start
-    // and timeline_stop must be set on the first band's theme.
-    // The other attributes can be set differently for each
-    // band by using different themes for the bands.
-    this.autoWidth = false; // Should the Timeline automatically grow itself, as
-                            // needed when too many events for the available width
-                            // are painted on the visible part of the Timeline?
-    this.autoWidthAnimationTime = 500; // mSec
-    this.timeline_start = null; // Setting a date, eg new Date(Date.UTC(2010,0,17,20,00,00,0)) will prevent the
-                                // Timeline from being moved to anytime before the date.
-    this.timeline_stop = null;  // Use for setting a maximum date. The Timeline will not be able
-                                // to be moved to anytime after this date.
-    this.ether = {
-        backgroundColors: [
-        //    "#EEE",
-        //    "#DDD",
-        //    "#CCC",
-        //    "#AAA"
-        ],
-     //   highlightColor:     "white",
-        highlightOpacity:   50,
-        interval: {
-            line: {
-                show:       true,
-                opacity:    25
-               // color:      "#aaa",
-            },
-            weekend: {
-                opacity:    30
-              //  color:      "#FFFFE0",
-            },
-            marker: {
-                hAlign:     "Bottom",
-                vAlign:     "Right"
-                                        /*
-                hBottomStyler: function(elmt) {
-                    elmt.className = "timeline-ether-marker-bottom";
-                },
-                hBottomEmphasizedStyler: function(elmt) {
-                    elmt.className = "timeline-ether-marker-bottom-emphasized";
-                },
-                hTopStyler: function(elmt) {
-                    elmt.className = "timeline-ether-marker-top";
-                },
-                hTopEmphasizedStyler: function(elmt) {
-                    elmt.className = "timeline-ether-marker-top-emphasized";
-                },
-                */
-
-
-               /*
-                                  vRightStyler: function(elmt) {
-                    elmt.className = "timeline-ether-marker-right";
-                },
-                vRightEmphasizedStyler: function(elmt) {
-                    elmt.className = "timeline-ether-marker-right-emphasized";
-                },
-                vLeftStyler: function(elmt) {
-                    elmt.className = "timeline-ether-marker-left";
-                },
-                vLeftEmphasizedStyler:function(elmt) {
-                    elmt.className = "timeline-ether-marker-left-emphasized";
-                }
-                */
-            }
-        }
-    };
-
-    this.event = {
-        track: {
-                   height: 10, // px. You will need to change the track
-                               //     height if you change the tape height.
-                      gap:  2, // px. Gap between tracks
-                   offset:  2, // px. top margin above tapes
-          autoWidthMargin:  1.5
-          /* autoWidthMargin is only used if autoWidth (see above) is true.
-             The autoWidthMargin setting is used to set how close the bottom of the
-             lowest track is to the edge of the band's div. The units are total track
-             width (tape + label + gap). A min of 0.5 is suggested. Use this setting to
-             move the bottom track's tapes above the axis markers, if needed for your
-             Timeline.
-          */
-        },
-        overviewTrack: {
-                  offset: 20, // px -- top margin above tapes
-              tickHeight:  6, // px
-                  height:  2, // px
-                     gap:  1, // px
-         autoWidthMargin:  5 // This attribute is only used if autoWidth (see above) is true.
-        },
-        tape: {
-            height:         4 // px. For thicker tapes, remember to change track height too.
-        },
-        instant: {
-                           icon: Timeline.urlPrefix + "images/dull-blue-circle.png",
-                                 // default icon. Icon can also be specified per event
-                      iconWidth: 10,
-                     iconHeight: 10,
-               impreciseOpacity: 20, // opacity of the tape when durationEvent is false
-            impreciseIconMargin: 3   // A tape and an icon are painted for imprecise instant
-                                     // events. This attribute is the margin between the
-                                     // bottom of the tape and the top of the icon in that
-                                     // case.
-    //        color:             "#58A0DC",
-    //        impreciseColor:    "#58A0DC",
-        },
-        duration: {
-            impreciseOpacity: 20 // tape opacity for imprecise part of duration events
-      //      color:            "#58A0DC",
-      //      impreciseColor:   "#58A0DC",
-        },
-        label: {
-            backgroundOpacity: 50,// only used in detailed painter
-               offsetFromLine:  3 // px left margin amount from icon's right edge
-      //      backgroundColor:   "white",
-      //      lineColor:         "#58A0DC",
-        },
-        highlightColors: [  // Use with getEventPainter().setHighlightMatcher
-                            // See webapp/examples/examples.js
-            "#FFFF00",
-            "#FFC000",
-            "#FF0000",
-            "#0000FF"
-        ],
-        highlightLabelBackground: false, // When highlighting an event, also change the event's label background?
-        bubble: {
-            width:          250, // px
-            maxHeight:        0, // px Maximum height of bubbles. 0 means no max height.
-                                 // scrollbar will be added for taller bubbles
-            titleStyler: function(elmt) {
-                elmt.className = "timeline-event-bubble-title";
-            },
-            bodyStyler: function(elmt) {
-                elmt.className = "timeline-event-bubble-body";
-            },
-            imageStyler: function(elmt) {
-                elmt.className = "timeline-event-bubble-image";
-            },
-            wikiStyler: function(elmt) {
-                elmt.className = "timeline-event-bubble-wiki";
-            },
-            timeStyler: function(elmt) {
-                elmt.className = "timeline-event-bubble-time";
-            }
-        }
-    };
-
-    this.mouseWheel = 'scroll'; // 'default', 'zoom', 'scroll'
-};/*
- *  An "ether" is a object that maps date/time to pixel coordinates.
- *
- */
-
-/*
- *  Linear Ether
- *
- */
-
-Timeline.LinearEther = function(params) {
-    this._params = params;
-    this._interval = params.interval;
-    this._pixelsPerInterval = params.pixelsPerInterval;
-};
-
-Timeline.LinearEther.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-    this._unit = timeline.getUnit();
-
-    if ("startsOn" in this._params) {
-        this._start = this._unit.parseFromObject(this._params.startsOn);
-    } else if ("endsOn" in this._params) {
-        this._start = this._unit.parseFromObject(this._params.endsOn);
-        this.shiftPixels(-this._timeline.getPixelLength());
-    } else if ("centersOn" in this._params) {
-        this._start = this._unit.parseFromObject(this._params.centersOn);
-        this.shiftPixels(-this._timeline.getPixelLength() / 2);
-    } else {
-        this._start = this._unit.makeDefaultValue();
-        this.shiftPixels(-this._timeline.getPixelLength() / 2);
-    }
-};
-
-Timeline.LinearEther.prototype.setDate = function(date) {
-    this._start = this._unit.cloneValue(date);
-};
-
-Timeline.LinearEther.prototype.shiftPixels = function(pixels) {
-    var numeric = this._interval * pixels / this._pixelsPerInterval;
-    this._start = this._unit.change(this._start, numeric);
-};
-
-Timeline.LinearEther.prototype.dateToPixelOffset = function(date) {
-    var numeric = this._unit.compare(date, this._start);
-    return this._pixelsPerInterval * numeric / this._interval;
-};
-
-Timeline.LinearEther.prototype.pixelOffsetToDate = function(pixels) {
-    var numeric = pixels * this._interval / this._pixelsPerInterval;
-    return this._unit.change(this._start, numeric);
-};
-
-Timeline.LinearEther.prototype.zoom = function(zoomIn) {
-  var netIntervalChange = 0;
-  var currentZoomIndex = this._band._zoomIndex;
-  var newZoomIndex = currentZoomIndex;
-
-  if (zoomIn && (currentZoomIndex > 0)) {
-    newZoomIndex = currentZoomIndex - 1;
-  }
-
-  if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
-    newZoomIndex = currentZoomIndex + 1;
-  }
-
-  this._band._zoomIndex = newZoomIndex;
-  this._interval =
-    SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
-  this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
-  netIntervalChange = this._band._zoomSteps[newZoomIndex].unit -
-    this._band._zoomSteps[currentZoomIndex].unit;
-
-  return netIntervalChange;
-};
-
-
-/*
- *  Hot Zone Ether
- *
- */
-
-Timeline.HotZoneEther = function(params) {
-    this._params = params;
-    this._interval = params.interval;
-    this._pixelsPerInterval = params.pixelsPerInterval;
-    this._theme = params.theme;
-};
-
-Timeline.HotZoneEther.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-    this._unit = timeline.getUnit();
-
-    this._zones = [{
-        startTime:  Number.NEGATIVE_INFINITY,
-        endTime:    Number.POSITIVE_INFINITY,
-        magnify:    1
-    }];
-    var params = this._params;
-    for (var i = 0; i < params.zones.length; i++) {
-        var zone = params.zones[i];
-        var zoneStart = this._unit.parseFromObject(zone.start);
-        var zoneEnd =   this._unit.parseFromObject(zone.end);
-
-        for (var j = 0; j < this._zones.length && this._unit.compare(zoneEnd, zoneStart) > 0; j++) {
-            var zone2 = this._zones[j];
-
-            if (this._unit.compare(zoneStart, zone2.endTime) < 0) {
-                if (this._unit.compare(zoneStart, zone2.startTime) > 0) {
-                    this._zones.splice(j, 0, {
-                        startTime:   zone2.startTime,
-                        endTime:     zoneStart,
-                        magnify:     zone2.magnify
-                    });
-                    j++;
-
-                    zone2.startTime = zoneStart;
-                }
-
-                if (this._unit.compare(zoneEnd, zone2.endTime) < 0) {
-                    this._zones.splice(j, 0, {
-                        startTime:  zoneStart,
-                        endTime:    zoneEnd,
-                        magnify:    zone.magnify * zone2.magnify
-                    });
-                    j++;
-
-                    zone2.startTime = zoneEnd;
-                    zoneStart = zoneEnd;
-                } else {
-                    zone2.magnify *= zone.magnify;
-                    zoneStart = zone2.endTime;
-                }
-            } // else, try the next existing zone
-        }
-    }
-
-    if ("startsOn" in this._params) {
-        this._start = this._unit.parseFromObject(this._params.startsOn);
-    } else if ("endsOn" in this._params) {
-        this._start = this._unit.parseFromObject(this._params.endsOn);
-        this.shiftPixels(-this._timeline.getPixelLength());
-    } else if ("centersOn" in this._params) {
-        this._start = this._unit.parseFromObject(this._params.centersOn);
-        this.shiftPixels(-this._timeline.getPixelLength() / 2);
-    } else {
-        this._start = this._unit.makeDefaultValue();
-        this.shiftPixels(-this._timeline.getPixelLength() / 2);
-    }
-};
-
-Timeline.HotZoneEther.prototype.setDate = function(date) {
-    this._start = this._unit.cloneValue(date);
-};
-
-Timeline.HotZoneEther.prototype.shiftPixels = function(pixels) {
-    this._start = this.pixelOffsetToDate(pixels);
-};
-
-Timeline.HotZoneEther.prototype.dateToPixelOffset = function(date) {
-    return this._dateDiffToPixelOffset(this._start, date);
-};
-
-Timeline.HotZoneEther.prototype.pixelOffsetToDate = function(pixels) {
-    return this._pixelOffsetToDate(pixels, this._start);
-};
-
-Timeline.HotZoneEther.prototype.zoom = function(zoomIn) {
-  var netIntervalChange = 0;
-  var currentZoomIndex = this._band._zoomIndex;
-  var newZoomIndex = currentZoomIndex;
-
-  if (zoomIn && (currentZoomIndex > 0)) {
-    newZoomIndex = currentZoomIndex - 1;
-  }
-
-  if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
-    newZoomIndex = currentZoomIndex + 1;
-  }
-
-  this._band._zoomIndex = newZoomIndex;
-  this._interval =
-    SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
-  this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
-  netIntervalChange = this._band._zoomSteps[newZoomIndex].unit -
-    this._band._zoomSteps[currentZoomIndex].unit;
-
-  return netIntervalChange;
-};
-
-Timeline.HotZoneEther.prototype._dateDiffToPixelOffset = function(fromDate, toDate) {
-    var scale = this._getScale();
-    var fromTime = fromDate;
-    var toTime = toDate;
-
-    var pixels = 0;
-    if (this._unit.compare(fromTime, toTime) < 0) {
-        var z = 0;
-        while (z < this._zones.length) {
-            if (this._unit.compare(fromTime, this._zones[z].endTime) < 0) {
-                break;
-            }
-            z++;
-        }
-
-        while (this._unit.compare(fromTime, toTime) < 0) {
-            var zone = this._zones[z];
-            var toTime2 = this._unit.earlier(toTime, zone.endTime);
-
-            pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
-
-            fromTime = toTime2;
-            z++;
-        }
-    } else {
-        var z = this._zones.length - 1;
-        while (z >= 0) {
-            if (this._unit.compare(fromTime, this._zones[z].startTime) > 0) {
-                break;
-            }
-            z--;
-        }
-
-        while (this._unit.compare(fromTime, toTime) > 0) {
-            var zone = this._zones[z];
-            var toTime2 = this._unit.later(toTime, zone.startTime);
-
-            pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
-
-            fromTime = toTime2;
-            z--;
-        }
-    }
-    return pixels;
-};
-
-Timeline.HotZoneEther.prototype._pixelOffsetToDate = function(pixels, fromDate) {
-    var scale = this._getScale();
-    var time = fromDate;
-    if (pixels > 0) {
-        var z = 0;
-        while (z < this._zones.length) {
-            if (this._unit.compare(time, this._zones[z].endTime) < 0) {
-                break;
-            }
-            z++;
-        }
-
-        while (pixels > 0) {
-            var zone = this._zones[z];
-            var scale2 = scale / zone.magnify;
-
-            if (zone.endTime == Number.POSITIVE_INFINITY) {
-                time = this._unit.change(time, pixels * scale2);
-                pixels = 0;
-            } else {
-                var pixels2 = this._unit.compare(zone.endTime, time) / scale2;
-                if (pixels2 > pixels) {
-                    time = this._unit.change(time, pixels * scale2);
-                    pixels = 0;
-                } else {
-                    time = zone.endTime;
-                    pixels -= pixels2;
-                }
-            }
-            z++;
-        }
-    } else {
-        var z = this._zones.length - 1;
-        while (z >= 0) {
-            if (this._unit.compare(time, this._zones[z].startTime) > 0) {
-                break;
-            }
-            z--;
-        }
-
-        pixels = -pixels;
-        while (pixels > 0) {
-            var zone = this._zones[z];
-            var scale2 = scale / zone.magnify;
-
-            if (zone.startTime == Number.NEGATIVE_INFINITY) {
-                time = this._unit.change(time, -pixels * scale2);
-                pixels = 0;
-            } else {
-                var pixels2 = this._unit.compare(time, zone.startTime) / scale2;
-                if (pixels2 > pixels) {
-                    time = this._unit.change(time, -pixels * scale2);
-                    pixels = 0;
-                } else {
-                    time = zone.startTime;
-                    pixels -= pixels2;
-                }
-            }
-            z--;
-        }
-    }
-    return time;
-};
-
-Timeline.HotZoneEther.prototype._getScale = function() {
-    return this._interval / this._pixelsPerInterval;
-};
-/*
- *  Gregorian Ether Painter
- *
- */
-
-Timeline.GregorianEtherPainter = function(params) {
-    this._params = params;
-    this._theme = params.theme;
-    this._unit = params.unit;
-    this._multiple = ("multiple" in params) ? params.multiple : 1;
-};
-
-Timeline.GregorianEtherPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._backgroundLayer = band.createLayerDiv(0);
-    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
-    this._backgroundLayer.className = 'timeline-ether-bg';
-  //  this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
-
-    this._markerLayer = null;
-    this._lineLayer = null;
-
-    var align = ("align" in this._params && this._params.align != undefined) ? this._params.align :
-        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
-    var showLine = ("showLine" in this._params) ? this._params.showLine :
-        this._theme.ether.interval.line.show;
-
-    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
-        this._timeline, this._band, this._theme, align, showLine);
-
-    this._highlight = new Timeline.EtherHighlight(
-        this._timeline, this._band, this._theme, this._backgroundLayer);
-}
-
-Timeline.GregorianEtherPainter.prototype.setHighlight = function(startDate, endDate) {
-    this._highlight.position(startDate, endDate);
-}
-
-Timeline.GregorianEtherPainter.prototype.paint = function() {
-    if (this._markerLayer) {
-        this._band.removeLayerDiv(this._markerLayer);
-    }
-    this._markerLayer = this._band.createLayerDiv(100);
-    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
-    this._markerLayer.style.display = "none";
-
-    if (this._lineLayer) {
-        this._band.removeLayerDiv(this._lineLayer);
-    }
-    this._lineLayer = this._band.createLayerDiv(1);
-    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
-    this._lineLayer.style.display = "none";
-
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    var timeZone = this._band.getTimeZone();
-    var labeller = this._band.getLabeller();
-
-    SimileAjax.DateTime.roundDownToInterval(minDate, this._unit, timeZone, this._multiple, this._theme.firstDayOfWeek);
-
-    var p = this;
-    var incrementDate = function(date) {
-        for (var i = 0; i < p._multiple; i++) {
-            SimileAjax.DateTime.incrementByInterval(date, p._unit);
-        }
-    };
-
-    while (minDate.getTime() < maxDate.getTime()) {
-        this._intervalMarkerLayout.createIntervalMarker(
-            minDate, labeller, this._unit, this._markerLayer, this._lineLayer);
-
-        incrementDate(minDate);
-    }
-    this._markerLayer.style.display = "block";
-    this._lineLayer.style.display = "block";
-};
-
-Timeline.GregorianEtherPainter.prototype.softPaint = function() {
-};
-
-Timeline.GregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
-  if (netIntervalChange != 0) {
-    this._unit += netIntervalChange;
-  }
-};
-
-
-/*
- *  Hot Zone Gregorian Ether Painter
- *
- */
-
-Timeline.HotZoneGregorianEtherPainter = function(params) {
-    this._params = params;
-    this._theme = params.theme;
-
-    this._zones = [{
-        startTime:  Number.NEGATIVE_INFINITY,
-        endTime:    Number.POSITIVE_INFINITY,
-        unit:       params.unit,
-        multiple:   1
-    }];
-    for (var i = 0; i < params.zones.length; i++) {
-        var zone = params.zones[i];
-        var zoneStart = SimileAjax.DateTime.parseGregorianDateTime(zone.start).getTime();
-        var zoneEnd = SimileAjax.DateTime.parseGregorianDateTime(zone.end).getTime();
-
-        for (var j = 0; j < this._zones.length && zoneEnd > zoneStart; j++) {
-            var zone2 = this._zones[j];
-
-            if (zoneStart < zone2.endTime) {
-                if (zoneStart > zone2.startTime) {
-                    this._zones.splice(j, 0, {
-                        startTime:   zone2.startTime,
-                        endTime:     zoneStart,
-                        unit:        zone2.unit,
-                        multiple:    zone2.multiple
-                    });
-                    j++;
-
-                    zone2.startTime = zoneStart;
-                }
-
-                if (zoneEnd < zone2.endTime) {
-                    this._zones.splice(j, 0, {
-                        startTime:  zoneStart,
-                        endTime:    zoneEnd,
-                        unit:       zone.unit,
-                        multiple:   (zone.multiple) ? zone.multiple : 1
-                    });
-                    j++;
-
-                    zone2.startTime = zoneEnd;
-                    zoneStart = zoneEnd;
-                } else {
-                    zone2.multiple = zone.multiple;
-                    zone2.unit = zone.unit;
-                    zoneStart = zone2.endTime;
-                }
-            } // else, try the next existing zone
-        }
-    }
-};
-
-Timeline.HotZoneGregorianEtherPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._backgroundLayer = band.createLayerDiv(0);
-    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
-    this._backgroundLayer.className ='timeline-ether-bg';
-    //this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
-    this._markerLayer = null;
-    this._lineLayer = null;
-
-    var align = ("align" in this._params && this._params.align != undefined) ? this._params.align :
-        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
-    var showLine = ("showLine" in this._params) ? this._params.showLine :
-        this._theme.ether.interval.line.show;
-
-    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
-        this._timeline, this._band, this._theme, align, showLine);
-
-    this._highlight = new Timeline.EtherHighlight(
-        this._timeline, this._band, this._theme, this._backgroundLayer);
-}
-
-Timeline.HotZoneGregorianEtherPainter.prototype.setHighlight = function(startDate, endDate) {
-    this._highlight.position(startDate, endDate);
-}
-
-Timeline.HotZoneGregorianEtherPainter.prototype.paint = function() {
-    if (this._markerLayer) {
-        this._band.removeLayerDiv(this._markerLayer);
-    }
-    this._markerLayer = this._band.createLayerDiv(100);
-    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
-    this._markerLayer.style.display = "none";
-
-    if (this._lineLayer) {
-        this._band.removeLayerDiv(this._lineLayer);
-    }
-    this._lineLayer = this._band.createLayerDiv(1);
-    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
-    this._lineLayer.style.display = "none";
-
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    var timeZone = this._band.getTimeZone();
-    var labeller = this._band.getLabeller();
-
-    var p = this;
-    var incrementDate = function(date, zone) {
-        for (var i = 0; i < zone.multiple; i++) {
-            SimileAjax.DateTime.incrementByInterval(date, zone.unit);
-        }
-    };
-
-    var zStart = 0;
-    while (zStart < this._zones.length) {
-        if (minDate.getTime() < this._zones[zStart].endTime) {
-            break;
-        }
-        zStart++;
-    }
-    var zEnd = this._zones.length - 1;
-    while (zEnd >= 0) {
-        if (maxDate.getTime() > this._zones[zEnd].startTime) {
-            break;
-        }
-        zEnd--;
-    }
-
-    for (var z = zStart; z <= zEnd; z++) {
-        var zone = this._zones[z];
-
-        var minDate2 = new Date(Math.max(minDate.getTime(), zone.startTime));
-        var maxDate2 = new Date(Math.min(maxDate.getTime(), zone.endTime));
-
-        SimileAjax.DateTime.roundDownToInterval(minDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
-        SimileAjax.DateTime.roundUpToInterval(maxDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
-
-        while (minDate2.getTime() < maxDate2.getTime()) {
-            this._intervalMarkerLayout.createIntervalMarker(
-                minDate2, labeller, zone.unit, this._markerLayer, this._lineLayer);
-
-            incrementDate(minDate2, zone);
-        }
-    }
-    this._markerLayer.style.display = "block";
-    this._lineLayer.style.display = "block";
-};
-
-Timeline.HotZoneGregorianEtherPainter.prototype.softPaint = function() {
-};
-
-Timeline.HotZoneGregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
-  if (netIntervalChange != 0) {
-    for (var i = 0; i < this._zones.length; ++i) {
-      if (this._zones[i]) {
-        this._zones[i].unit += netIntervalChange;
-      }
-    }
-  }
-};
-
-/*
- *  Year Count Ether Painter
- *
- */
-
-Timeline.YearCountEtherPainter = function(params) {
-    this._params = params;
-    this._theme = params.theme;
-    this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
-    this._multiple = ("multiple" in params) ? params.multiple : 1;
-};
-
-Timeline.YearCountEtherPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._backgroundLayer = band.createLayerDiv(0);
-    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
-    this._backgroundLayer.className = 'timeline-ether-bg';
-   // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
-    this._markerLayer = null;
-    this._lineLayer = null;
-
-    var align = ("align" in this._params) ? this._params.align :
-        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
-    var showLine = ("showLine" in this._params) ? this._params.showLine :
-        this._theme.ether.interval.line.show;
-
-    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
-        this._timeline, this._band, this._theme, align, showLine);
-
-    this._highlight = new Timeline.EtherHighlight(
-        this._timeline, this._band, this._theme, this._backgroundLayer);
-};
-
-Timeline.YearCountEtherPainter.prototype.setHighlight = function(startDate, endDate) {
-    this._highlight.position(startDate, endDate);
-};
-
-Timeline.YearCountEtherPainter.prototype.paint = function() {
-    if (this._markerLayer) {
-        this._band.removeLayerDiv(this._markerLayer);
-    }
-    this._markerLayer = this._band.createLayerDiv(100);
-    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
-    this._markerLayer.style.display = "none";
-
-    if (this._lineLayer) {
-        this._band.removeLayerDiv(this._lineLayer);
-    }
-    this._lineLayer = this._band.createLayerDiv(1);
-    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
-    this._lineLayer.style.display = "none";
-
-    var minDate = new Date(this._startDate.getTime());
-    var maxDate = this._band.getMaxDate();
-    var yearDiff = this._band.getMinDate().getUTCFullYear() - this._startDate.getUTCFullYear();
-    minDate.setUTCFullYear(this._band.getMinDate().getUTCFullYear() - yearDiff % this._multiple);
-
-    var p = this;
-    var incrementDate = function(date) {
-        for (var i = 0; i < p._multiple; i++) {
-            SimileAjax.DateTime.incrementByInterval(date, SimileAjax.DateTime.YEAR);
-        }
-    };
-    var labeller = {
-        labelInterval: function(date, intervalUnit) {
-            var diff = date.getUTCFullYear() - p._startDate.getUTCFullYear();
-            return {
-                text: diff,
-                emphasized: diff == 0
-            };
-        }
-    };
-
-    while (minDate.getTime() < maxDate.getTime()) {
-        this._intervalMarkerLayout.createIntervalMarker(
-            minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
-
-        incrementDate(minDate);
-    }
-    this._markerLayer.style.display = "block";
-    this._lineLayer.style.display = "block";
-};
-
-Timeline.YearCountEtherPainter.prototype.softPaint = function() {
-};
-
-/*
- *  Quarterly Ether Painter
- *
- */
-
-Timeline.QuarterlyEtherPainter = function(params) {
-    this._params = params;
-    this._theme = params.theme;
-    this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
-};
-
-Timeline.QuarterlyEtherPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._backgroundLayer = band.createLayerDiv(0);
-    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
-    this._backgroundLayer.className = 'timeline-ether-bg';
- //   this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
-    this._markerLayer = null;
-    this._lineLayer = null;
-
-    var align = ("align" in this._params) ? this._params.align :
-        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
-    var showLine = ("showLine" in this._params) ? this._params.showLine :
-        this._theme.ether.interval.line.show;
-
-    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
-        this._timeline, this._band, this._theme, align, showLine);
-
-    this._highlight = new Timeline.EtherHighlight(
-        this._timeline, this._band, this._theme, this._backgroundLayer);
-};
-
-Timeline.QuarterlyEtherPainter.prototype.setHighlight = function(startDate, endDate) {
-    this._highlight.position(startDate, endDate);
-};
-
-Timeline.QuarterlyEtherPainter.prototype.paint = function() {
-    if (this._markerLayer) {
-        this._band.removeLayerDiv(this._markerLayer);
-    }
-    this._markerLayer = this._band.createLayerDiv(100);
-    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
-    this._markerLayer.style.display = "none";
-
-    if (this._lineLayer) {
-        this._band.removeLayerDiv(this._lineLayer);
-    }
-    this._lineLayer = this._band.createLayerDiv(1);
-    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
-    this._lineLayer.style.display = "none";
-
-    var minDate = new Date(0);
-    var maxDate = this._band.getMaxDate();
-
-    minDate.setUTCFullYear(Math.max(this._startDate.getUTCFullYear(), this._band.getMinDate().getUTCFullYear()));
-    minDate.setUTCMonth(this._startDate.getUTCMonth());
-
-    var p = this;
-    var incrementDate = function(date) {
-        date.setUTCMonth(date.getUTCMonth() + 3);
-    };
-    var labeller = {
-        labelInterval: function(date, intervalUnit) {
-            var quarters = (4 + (date.getUTCMonth() - p._startDate.getUTCMonth()) / 3) % 4;
-            if (quarters != 0) {
-                return { text: "Q" + (quarters + 1), emphasized: false };
-            } else {
-                return { text: "Y" + (date.getUTCFullYear() - p._startDate.getUTCFullYear() + 1), emphasized: true };
-            }
-        }
-    };
-
-    while (minDate.getTime() < maxDate.getTime()) {
-        this._intervalMarkerLayout.createIntervalMarker(
-            minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
-
-        incrementDate(minDate);
-    }
-    this._markerLayer.style.display = "block";
-    this._lineLayer.style.display = "block";
-};
-
-Timeline.QuarterlyEtherPainter.prototype.softPaint = function() {
-};
-
-/*
- *  Ether Interval Marker Layout
- *
- */
-
-Timeline.EtherIntervalMarkerLayout = function(timeline, band, theme, align, showLine) {
-    var horizontal = timeline.isHorizontal();
-    if (horizontal) {
-        if (align == "Top") {
-            this.positionDiv = function(div, offset) {
-                div.style.left = offset + "px";
-                div.style.top = "0px";
-            };
-        } else {
-            this.positionDiv = function(div, offset) {
-                div.style.left = offset + "px";
-                div.style.bottom = "0px";
-            };
-        }
-    } else {
-        if (align == "Left") {
-            this.positionDiv = function(div, offset) {
-                div.style.top = offset + "px";
-                div.style.left = "0px";
-            };
-        } else {
-            this.positionDiv = function(div, offset) {
-                div.style.top = offset + "px";
-                div.style.right = "0px";
-            };
-        }
-    }
-
-    var markerTheme = theme.ether.interval.marker;
-    var lineTheme = theme.ether.interval.line;
-    var weekendTheme = theme.ether.interval.weekend;
-
-    var stylePrefix = (horizontal ? "h" : "v") + align;
-    var labelStyler = markerTheme[stylePrefix + "Styler"];
-    var emphasizedLabelStyler = markerTheme[stylePrefix + "EmphasizedStyler"];
-    var day = SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY];
-
-    this.createIntervalMarker = function(date, labeller, unit, markerDiv, lineDiv) {
-        var offset = Math.round(band.dateToPixelOffset(date));
-
-        if (showLine && unit != SimileAjax.DateTime.WEEK) {
-            var divLine = timeline.getDocument().createElement("div");
-            divLine.className = "timeline-ether-lines";
-
-            if (lineTheme.opacity < 100) {
-                SimileAjax.Graphics.setOpacity(divLine, lineTheme.opacity);
-            }
-
-            if (horizontal) {
-				//divLine.className += " timeline-ether-lines-vertical";
-				divLine.style.left = offset + "px";
-            } else {
-				//divLine.className += " timeline-ether-lines-horizontal";
-                divLine.style.top = offset + "px";
-            }
-            lineDiv.appendChild(divLine);
-        }
-        if (unit == SimileAjax.DateTime.WEEK) {
-            var firstDayOfWeek = theme.firstDayOfWeek;
-
-            var saturday = new Date(date.getTime() + (6 - firstDayOfWeek - 7) * day);
-            var monday = new Date(saturday.getTime() + 2 * day);
-
-            var saturdayPixel = Math.round(band.dateToPixelOffset(saturday));
-            var mondayPixel = Math.round(band.dateToPixelOffset(monday));
-            var length = Math.max(1, mondayPixel - saturdayPixel);
-
-            var divWeekend = timeline.getDocument().createElement("div");
-			divWeekend.className = 'timeline-ether-weekends'
-
-            if (weekendTheme.opacity < 100) {
-                SimileAjax.Graphics.setOpacity(divWeekend, weekendTheme.opacity);
-            }
-
-            if (horizontal) {
-                divWeekend.style.left = saturdayPixel + "px";
-                divWeekend.style.width = length + "px";
-            } else {
-                divWeekend.style.top = saturdayPixel + "px";
-                divWeekend.style.height = length + "px";
-            }
-            lineDiv.appendChild(divWeekend);
-        }
-
-        var label = labeller.labelInterval(date, unit);
-
-        var div = timeline.getDocument().createElement("div");
-        div.innerHTML = label.text;
-
-
-
-		div.className = 'timeline-date-label'
-		if(label.emphasized) div.className += ' timeline-date-label-em'
-
-        this.positionDiv(div, offset);
-        markerDiv.appendChild(div);
-
-        return div;
-    };
-};
-
-/*
- *  Ether Highlight Layout
- *
- */
-
-Timeline.EtherHighlight = function(timeline, band, theme, backgroundLayer) {
-    var horizontal = timeline.isHorizontal();
-
-    this._highlightDiv = null;
-    this._createHighlightDiv = function() {
-        if (this._highlightDiv == null) {
-            this._highlightDiv = timeline.getDocument().createElement("div");
-            this._highlightDiv.setAttribute("name", "ether-highlight"); // for debugging
-            this._highlightDiv.className = 'timeline-ether-highlight'
-
-            var opacity = theme.ether.highlightOpacity;
-            if (opacity < 100) {
-                SimileAjax.Graphics.setOpacity(this._highlightDiv, opacity);
-            }
-
-            backgroundLayer.appendChild(this._highlightDiv);
-        }
-    }
-
-    this.position = function(startDate, endDate) {
-        this._createHighlightDiv();
-
-        var startPixel = Math.round(band.dateToPixelOffset(startDate));
-        var endPixel = Math.round(band.dateToPixelOffset(endDate));
-        var length = Math.max(endPixel - startPixel, 3);
-        if (horizontal) {
-            this._highlightDiv.style.left = startPixel + "px";
-            this._highlightDiv.style.width = length + "px";
-            this._highlightDiv.style.height = (band.getViewWidth() - 4) + "px";
-        } else {
-            this._highlightDiv.style.top = startPixel + "px";
-            this._highlightDiv.style.height = length + "px";
-            this._highlightDiv.style.width = (band.getViewWidth() - 4) + "px";
-        }
-    }
-};
-/*
- *  Event Utils
- *
- */
-Timeline.EventUtils = {};
-
-Timeline.EventUtils.getNewEventID = function() {
-    // global across page
-    if (this._lastEventID == null) {
-        this._lastEventID = 0;
-    }
-
-    this._lastEventID += 1;
-    return "e" + this._lastEventID;
-};
-
-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).
-     *
-     * Returns {band: <bandObj>, evt: <eventObj>}
-     *
-     * To enable a single event listener to monitor everything
-     * on a Timeline, a set format is used for the id's of the
-     * elements on the Timeline--
-     *
-     * element id format for labels, icons, tapes:
-     *   labels: label-tl-<timelineID>-<band_index>-<evt.id>
-     *    icons: icon-tl-<timelineID>-<band_index>-<evt.id>
-     *    tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
-     *           tape2-tl-<timelineID>-<band_index>-<evt.id>
-     *           // some events have more than one tape
-     *    highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
-     *               highlight2-tl-<timelineID>-<band_index>-<evt.id>
-     *           // some events have more than one highlight div (future)
-     * Note: use split('-') to get array of the format's parts
-     *
-     * You can then retrieve the timeline object and event object
-     * by using Timeline.getTimeline, Timeline.getBand, or
-     * Timeline.getEvent and passing in the element's id
-     *
-     *
-     */
-
-    var parts = elementID.split('-');
-    if (parts[1] != 'tl') {
-        alert("Internal Timeline problem 101, please consult support");
-        return {band: null, evt: null}; // early return
-    }
-
-    var timeline = Timeline.getTimelineFromID(parts[2]);
-    var band = timeline.getBand(parts[3]);
-    var evt = band.getEventSource.getEvent(parts[4]);
-
-    return {band: band, evt: evt};
-};
-
-Timeline.EventUtils.encodeEventElID = function(timeline, band, elType, evt) {
-    // 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) {
-    this._locale = locale;
-    this._timeZone = timeZone;
-};
-
-Timeline.GregorianDateLabeller.monthNames = [];
-Timeline.GregorianDateLabeller.dayNames = [];
-Timeline.GregorianDateLabeller.labelIntervalFunctions = [];
-
-Timeline.GregorianDateLabeller.getMonthName = function(month, locale) {
-    return Timeline.GregorianDateLabeller.monthNames[locale][month];
-};
-
-Timeline.GregorianDateLabeller.prototype.labelInterval = function(date, intervalUnit) {
-    var f = Timeline.GregorianDateLabeller.labelIntervalFunctions[this._locale];
-    if (f == null) {
-        f = Timeline.GregorianDateLabeller.prototype.defaultLabelInterval;
-    }
-    return f.call(this, date, intervalUnit);
-};
-
-Timeline.GregorianDateLabeller.prototype.labelPrecise = function(date) {
-    return SimileAjax.DateTime.removeTimeZoneOffset(
-        date,
-        this._timeZone //+ (new Date().getTimezoneOffset() / 60)
-    ).toUTCString();
-};
-
-Timeline.GregorianDateLabeller.prototype.defaultLabelInterval = function(date, intervalUnit) {
-    var text;
-    var emphasized = false;
-
-    date = SimileAjax.DateTime.removeTimeZoneOffset(date, this._timeZone);
-
-    switch(intervalUnit) {
-    case SimileAjax.DateTime.MILLISECOND:
-        text = date.getUTCMilliseconds();
-        break;
-    case SimileAjax.DateTime.SECOND:
-        text = date.getUTCSeconds();
-        break;
-    case SimileAjax.DateTime.MINUTE:
-        var m = date.getUTCMinutes();
-        if (m == 0) {
-            text = date.getUTCHours() + ":00";
-            emphasized = true;
-        } else {
-            text = m;
-        }
-        break;
-    case SimileAjax.DateTime.HOUR:
-        text = date.getUTCHours() + "hr";
-        break;
-    case SimileAjax.DateTime.DAY:
-        text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
-        break;
-    case SimileAjax.DateTime.WEEK:
-        text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
-        break;
-    case SimileAjax.DateTime.MONTH:
-        var m = date.getUTCMonth();
-        if (m != 0) {
-            text = Timeline.GregorianDateLabeller.getMonthName(m, this._locale);
-            break;
-        } // else, fall through
-    case SimileAjax.DateTime.YEAR:
-    case SimileAjax.DateTime.DECADE:
-    case SimileAjax.DateTime.CENTURY:
-    case SimileAjax.DateTime.MILLENNIUM:
-        var y = date.getUTCFullYear();
-        if (y > 0) {
-            text = date.getUTCFullYear();
-        } else {
-            text = (1 - y) + "BC";
-        }
-        emphasized =
-            (intervalUnit == SimileAjax.DateTime.MONTH) ||
-            (intervalUnit == SimileAjax.DateTime.DECADE && y % 100 == 0) ||
-            (intervalUnit == SimileAjax.DateTime.CENTURY && y % 1000 == 0);
-        break;
-    default:
-        text = date.toUTCString();
-    }
-    return { text: text, emphasized: emphasized };
-}
-
-/*
- *  Default Event Source
- *
- */
-
-
-Timeline.DefaultEventSource = function(eventIndex) {
-    this._events = (eventIndex instanceof Object) ? eventIndex : new SimileAjax.EventIndex();
-    this._listeners = [];
-};
-
-Timeline.DefaultEventSource.prototype.addListener = function(listener) {
-    this._listeners.push(listener);
-};
-
-Timeline.DefaultEventSource.prototype.removeListener = function(listener) {
-    for (var i = 0; i < this._listeners.length; i++) {
-        if (this._listeners[i] == listener) {
-            this._listeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-Timeline.DefaultEventSource.prototype.loadXML = function(xml, url) {
-    var base = this._getBaseURL(url);
-
-    var wikiURL = xml.documentElement.getAttribute("wiki-url");
-    var wikiSection = xml.documentElement.getAttribute("wiki-section");
-
-    var dateTimeFormat = xml.documentElement.getAttribute("date-time-format");
-    var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
-
-    var node = xml.documentElement.firstChild;
-    var added = false;
-    while (node != null) {
-        if (node.nodeType == 1) {
-            var description = "";
-            if (node.firstChild != null && node.firstChild.nodeType == 3) {
-                description = node.firstChild.nodeValue;
-            }
-            // instant event: default is true. Or use values from isDuration or durationEvent
-            var instant = (node.getAttribute("isDuration")    === null &&
-                           node.getAttribute("durationEvent") === null) ||
-                          node.getAttribute("isDuration") == "false" ||
-                          node.getAttribute("durationEvent") == "false";
-
-            var evt = new Timeline.DefaultEventSource.Event( {
-                          id: node.getAttribute("id"),
-                       start: parseDateTimeFunction(node.getAttribute("start")),
-                         end: parseDateTimeFunction(node.getAttribute("end")),
-                 latestStart: parseDateTimeFunction(node.getAttribute("latestStart")),
-                 earliestEnd: parseDateTimeFunction(node.getAttribute("earliestEnd")),
-                     instant: instant,
-                        text: node.getAttribute("title"),
-                 description: description,
-                       image: this._resolveRelativeURL(node.getAttribute("image"), base),
-                        link: this._resolveRelativeURL(node.getAttribute("link") , base),
-                        icon: this._resolveRelativeURL(node.getAttribute("icon") , base),
-                       color: node.getAttribute("color"),
-                   textColor: node.getAttribute("textColor"),
-                   hoverText: node.getAttribute("hoverText"),
-                   classname: node.getAttribute("classname"),
-                   tapeImage: node.getAttribute("tapeImage"),
-                  tapeRepeat: node.getAttribute("tapeRepeat"),
-                     caption: node.getAttribute("caption"),
-                     eventID: node.getAttribute("eventID"),
-                    trackNum: node.getAttribute("trackNum")
-            });
-
-            evt._node = node;
-            evt.getProperty = function(name) {
-                return this._node.getAttribute(name);
-            };
-            evt.setWikiInfo(wikiURL, wikiSection);
-
-            this._events.add(evt);
-
-            added = true;
-        }
-        node = node.nextSibling;
-    }
-
-    if (added) {
-        this._fire("onAddMany", []);
-    }
-};
-
-
-Timeline.DefaultEventSource.prototype.loadJSON = function(data, url) {
-    var base = this._getBaseURL(url);
-    var added = false;
-    if (data && data.events){
-        var wikiURL = ("wikiURL" in data) ? data.wikiURL : null;
-        var wikiSection = ("wikiSection" in data) ? data.wikiSection : null;
-
-        var dateTimeFormat = ("dateTimeFormat" in data) ? data.dateTimeFormat : null;
-        var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
-
-        for (var i=0; i < data.events.length; i++){
-            var event = data.events[i];
-            // Fixing issue 33:
-            // instant event: default (for JSON only) is false. Or use values from isDuration or durationEvent
-            // isDuration was negated (see issue 33, so keep that interpretation
-            var instant = event.isDuration || (event.durationEvent != null && !event.durationEvent);
-
-            var evt = new Timeline.DefaultEventSource.Event({
-                          id: ("id" in event) ? event.id : undefined,
-                       start: parseDateTimeFunction(event.start),
-                         end: parseDateTimeFunction(event.end),
-                 latestStart: parseDateTimeFunction(event.latestStart),
-                 earliestEnd: parseDateTimeFunction(event.earliestEnd),
-                     instant: instant,
-                        text: event.title,
-                 description: event.description,
-                       image: this._resolveRelativeURL(event.image, base),
-                        link: this._resolveRelativeURL(event.link , base),
-                        icon: this._resolveRelativeURL(event.icon , base),
-                       color: event.color,
-                   textColor: event.textColor,
-                   hoverText: event.hoverText,
-                   classname: event.classname,
-                   tapeImage: event.tapeImage,
-                  tapeRepeat: event.tapeRepeat,
-                     caption: event.caption,
-                     eventID: event.eventID,
-                    trackNum: event.trackNum
-            });
-            evt._obj = event;
-            evt.getProperty = function(name) {
-                return this._obj[name];
-            };
-            evt.setWikiInfo(wikiURL, wikiSection);
-
-            this._events.add(evt);
-            added = true;
-        }
-    }
-
-    if (added) {
-        this._fire("onAddMany", []);
-    }
-};
-
-/*
- *  Contributed by Morten Frederiksen, http://www.wasab.dk/morten/
- */
-Timeline.DefaultEventSource.prototype.loadSPARQL = function(xml, url) {
-    var base = this._getBaseURL(url);
-
-    var dateTimeFormat = 'iso8601';
-    var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
-
-    if (xml == null) {
-        return;
-    }
-
-    /*
-     *  Find <results> tag
-     */
-    var node = xml.documentElement.firstChild;
-    while (node != null && (node.nodeType != 1 || node.nodeName != 'results')) {
-        node = node.nextSibling;
-    }
-
-    var wikiURL = null;
-    var wikiSection = null;
-    if (node != null) {
-        wikiURL = node.getAttribute("wiki-url");
-        wikiSection = node.getAttribute("wiki-section");
-
-        node = node.firstChild;
-    }
-
-    var added = false;
-    while (node != null) {
-        if (node.nodeType == 1) {
-            var bindings = { };
-            var binding = node.firstChild;
-            while (binding != null) {
-                if (binding.nodeType == 1 &&
-                    binding.firstChild != null &&
-                    binding.firstChild.nodeType == 1 &&
-                    binding.firstChild.firstChild != null &&
-                    binding.firstChild.firstChild.nodeType == 3) {
-                    bindings[binding.getAttribute('name')] = binding.firstChild.firstChild.nodeValue;
-                }
-                binding = binding.nextSibling;
-            }
-
-            if (bindings["start"] == null && bindings["date"] != null) {
-                bindings["start"] = bindings["date"];
-            }
-
-            // instant event: default is true. Or use values from isDuration or durationEvent
-            var instant = (bindings["isDuration"]    === null &&
-                           bindings["durationEvent"] === null) ||
-                          bindings["isDuration"] == "false" ||
-                          bindings["durationEvent"] == "false";
-
-            var evt = new Timeline.DefaultEventSource.Event({
-                          id: bindings["id"],
-                       start: parseDateTimeFunction(bindings["start"]),
-                         end: parseDateTimeFunction(bindings["end"]),
-                 latestStart: parseDateTimeFunction(bindings["latestStart"]),
-                 earliestEnd: parseDateTimeFunction(bindings["earliestEnd"]),
-                     instant: instant, // instant
-                        text: bindings["title"], // text
-                 description: bindings["description"],
-                       image: this._resolveRelativeURL(bindings["image"], base),
-                        link: this._resolveRelativeURL(bindings["link"] , base),
-                        icon: this._resolveRelativeURL(bindings["icon"] , base),
-                       color: bindings["color"],
-                   textColor: bindings["textColor"],
-                   hoverText: bindings["hoverText"],
-                     caption: bindings["caption"],
-                   classname: bindings["classname"],
-                   tapeImage: bindings["tapeImage"],
-                  tapeRepeat: bindings["tapeRepeat"],
-                     eventID: bindings["eventID"],
-                    trackNum: bindings["trackNum"]
-            });
-            evt._bindings = bindings;
-            evt.getProperty = function(name) {
-                return this._bindings[name];
-            };
-            evt.setWikiInfo(wikiURL, wikiSection);
-
-            this._events.add(evt);
-            added = true;
-        }
-        node = node.nextSibling;
-    }
-
-    if (added) {
-        this._fire("onAddMany", []);
-    }
-};
-
-Timeline.DefaultEventSource.prototype.add = function(evt) {
-    this._events.add(evt);
-    this._fire("onAddOne", [evt]);
-};
-
-Timeline.DefaultEventSource.prototype.addMany = function(events) {
-    for (var i = 0; i < events.length; i++) {
-        this._events.add(events[i]);
-    }
-    this._fire("onAddMany", []);
-};
-
-Timeline.DefaultEventSource.prototype.clear = function() {
-    this._events.removeAll();
-    this._fire("onClear", []);
-};
-
-Timeline.DefaultEventSource.prototype.getEvent = function(id) {
-    return this._events.getEvent(id);
-};
-
-Timeline.DefaultEventSource.prototype.getEventIterator = function(startDate, endDate) {
-    return this._events.getIterator(startDate, endDate);
-};
-
-Timeline.DefaultEventSource.prototype.getEventReverseIterator = function(startDate, endDate) {
-    return this._events.getReverseIterator(startDate, endDate);
-};
-
-Timeline.DefaultEventSource.prototype.getAllEventIterator = function() {
-    return this._events.getAllIterator();
-};
-
-Timeline.DefaultEventSource.prototype.getCount = function() {
-    return this._events.getCount();
-};
-
-Timeline.DefaultEventSource.prototype.getEarliestDate = function() {
-    return this._events.getEarliestDate();
-};
-
-Timeline.DefaultEventSource.prototype.getLatestDate = function() {
-    return this._events.getLatestDate();
-};
-
-Timeline.DefaultEventSource.prototype._fire = function(handlerName, args) {
-    for (var i = 0; i < this._listeners.length; i++) {
-        var listener = this._listeners[i];
-        if (handlerName in listener) {
-            try {
-                listener[handlerName].apply(listener, args);
-            } catch (e) {
-                SimileAjax.Debug.exception(e);
-            }
-        }
-    }
-};
-
-Timeline.DefaultEventSource.prototype._getBaseURL = function(url) {
-    if (url.indexOf("://") < 0) {
-        var url2 = this._getBaseURL(document.location.href);
-        if (url.substr(0,1) == "/") {
-            url = url2.substr(0, url2.indexOf("/", url2.indexOf("://") + 3)) + url;
-        } else {
-            url = url2 + url;
-        }
-    }
-
-    var i = url.lastIndexOf("/");
-    if (i < 0) {
-        return "";
-    } else {
-        return url.substr(0, i+1);
-    }
-};
-
-Timeline.DefaultEventSource.prototype._resolveRelativeURL = function(url, base) {
-    if (url == null || url == "") {
-        return url;
-    } else if (url.indexOf("://") > 0) {
-        return url;
-    } else if (url.substr(0,1) == "/") {
-        return base.substr(0, base.indexOf("/", base.indexOf("://") + 3)) + url;
-    } else {
-        return base + url;
-    }
-};
-
-
-Timeline.DefaultEventSource.Event = function(args) {
-  //
-  // Attention developers!
-  // If you add a new event attribute, please be sure to add it to
-  // all three load functions: loadXML, loadSPARCL, loadJSON.
-  // Thanks!
-  //
-  // args is a hash/object. It supports the following keys. Most are optional
-  //   id            -- an internal id. Really shouldn't be used by events.
-  //                    Timeline library clients should use eventID
-  //   eventID       -- For use by library client when writing custom painters or
-  //                    custom fillInfoBubble
-  //   start
-  //   end
-  //   latestStart
-  //   earliestEnd
-  //   instant      -- boolean. Controls precise/non-precise logic & duration/instant issues
-  //   text         -- event source attribute 'title' -- used as the label on Timelines and in bubbles.
-  //   description  -- used in bubbles
-  //   image        -- used in bubbles
-  //   link         -- used in bubbles
-  //   icon         -- on the Timeline
-  //   color        -- Timeline label and tape color
-  //   textColor    -- Timeline label color, overrides color attribute
-  //   hoverText    -- deprecated, here for backwards compatibility.
-  //                   Superceeded by caption
-  //   caption      -- tooltip-like caption on the Timeline. Uses HTML title attribute
-  //   classname    -- used to set classname in Timeline. Enables better CSS selector rules
-  //   tapeImage    -- background image of the duration event's tape div on the Timeline
-  //   tapeRepeat   -- repeat attribute for tapeImage. {repeat | repeat-x | repeat-y }
-
-  function cleanArg(arg) {
-      // clean up an arg
-      return (args[arg] != null && args[arg] != "") ? args[arg] : null;
-  }
-
-  var id = args.id ? args.id.trim() : "";
-  this._id = id.length > 0 ? id : Timeline.EventUtils.getNewEventID();
-
-  this._instant = args.instant || (args.end == null);
-
-  this._start = args.start;
-  this._end = (args.end != null) ? args.end : args.start;
-
-  this._latestStart = (args.latestStart != null) ?
-                       args.latestStart : (args.instant ? this._end : this._start);
-  this._earliestEnd = (args.earliestEnd != null) ? args.earliestEnd : this._end;
-
-  // check sanity of dates since incorrect dates will later cause calculation errors
-  // when painting
-  var err=[];
-  if (this._start > this._latestStart) {
-          this._latestStart = this._start;
-          err.push("start is > latestStart");}
-  if (this._start > this._earliestEnd) {
-          this._earliestEnd = this._latestStart;
-          err.push("start is > earliestEnd");}
-  if (this._start > this._end) {
-          this._end = this._earliestEnd;
-          err.push("start is > end");}
-  if (this._latestStart > this._earliestEnd) {
-          this._earliestEnd = this._latestStart;
-          err.push("latestStart is > earliestEnd");}
-  if (this._latestStart > this._end) {
-          this._end = this._earliestEnd;
-          err.push("latestStart is > end");}
-  if (this._earliestEnd > this._end) {
-          this._end = this._earliestEnd;
-          err.push("earliestEnd is > end");}
-
-  this._eventID = cleanArg('eventID');
-  this._text = (args.text != null) ? SimileAjax.HTML.deEntify(args.text) : ""; // Change blank titles to ""
-  if (err.length > 0) {
-          this._text += " PROBLEM: " + err.join(", ");
-  }
-
-  this._description = SimileAjax.HTML.deEntify(args.description);
-  this._image = cleanArg('image');
-  this._link =  cleanArg('link');
-  this._title = cleanArg('hoverText');
-  this._title = cleanArg('caption');
-
-  this._icon = cleanArg('icon');
-  this._color = cleanArg('color');
-  this._textColor = cleanArg('textColor');
-  this._classname = cleanArg('classname');
-  this._tapeImage = cleanArg('tapeImage');
-  this._tapeRepeat = cleanArg('tapeRepeat');
-  this._trackNum = cleanArg('trackNum');
-  if (this._trackNum != null) {
-      this._trackNum = parseInt(this._trackNum);
-  }
-
-  this._wikiURL = null;
-  this._wikiSection = null;
-};
-
-Timeline.DefaultEventSource.Event.prototype = {
-    getID:          function() { return this._id; },
-
-    isInstant:      function() { return this._instant; },
-    isImprecise:    function() { return this._start != this._latestStart || this._end != this._earliestEnd; },
-
-    getStart:       function() { return this._start; },
-    getEnd:         function() { return this._end; },
-    getLatestStart: function() { return this._latestStart; },
-    getEarliestEnd: function() { return this._earliestEnd; },
-
-    getEventID:     function() { return this._eventID; },
-    getText:        function() { return this._text; }, // title
-    getDescription: function() { return this._description; },
-    getImage:       function() { return this._image; },
-    getLink:        function() { return this._link; },
-
-    getIcon:        function() { return this._icon; },
-    getColor:       function() { return this._color; },
-    getTextColor:   function() { return this._textColor; },
-    getClassName:   function() { return this._classname; },
-    getTapeImage:   function() { return this._tapeImage; },
-    getTapeRepeat:  function() { return this._tapeRepeat; },
-    getTrackNum:    function() { return this._trackNum; },
-
-    getProperty:    function(name) { return null; },
-
-    getWikiURL:     function() { return this._wikiURL; },
-    getWikiSection: function() { return this._wikiSection; },
-    setWikiInfo: function(wikiURL, wikiSection) {
-        this._wikiURL = wikiURL;
-        this._wikiSection = wikiSection;
-    },
-
-    fillDescription: function(elmt) {
-        elmt.innerHTML = this._description;
-    },
-    fillWikiInfo: function(elmt) {
-        // Many bubbles will not support a wiki link.
-        //
-        // Strategy: assume no wiki link. If we do have
-        // enough parameters for one, then create it.
-        elmt.style.display = "none"; // default
-
-        if (this._wikiURL == null || this._wikiSection == null) {
-          return; // EARLY RETURN
-        }
-
-        // create the wikiID from the property or from the event text (the title)
-        var wikiID = this.getProperty("wikiID");
-        if (wikiID == null || wikiID.length == 0) {
-            wikiID = this.getText(); // use the title as the backup wiki id
-        }
-
-        if (wikiID == null || wikiID.length == 0) {
-          return; // No wikiID. Thus EARLY RETURN
-        }
-
-        // ready to go...
-        elmt.style.display = "inline";
-        wikiID = wikiID.replace(/\s/g, "_");
-        var url = this._wikiURL + this._wikiSection.replace(/\s/g, "_") + "/" + wikiID;
-        var a = document.createElement("a");
-        a.href = url;
-        a.target = "new";
-        a.innerHTML = Timeline.strings[Timeline.clientLocale].wikiLinkLabel;
-
-        elmt.appendChild(document.createTextNode("["));
-        elmt.appendChild(a);
-        elmt.appendChild(document.createTextNode("]"));
-    },
-
-    fillTime: function(elmt, labeller) {
-        if (this._instant) {
-            if (this.isImprecise()) {
-                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
-                elmt.appendChild(elmt.ownerDocument.createElement("br"));
-                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
-            } else {
-                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
-            }
-        } else {
-            if (this.isImprecise()) {
-                elmt.appendChild(elmt.ownerDocument.createTextNode(
-                    labeller.labelPrecise(this._start) + " ~ " + labeller.labelPrecise(this._latestStart)));
-                elmt.appendChild(elmt.ownerDocument.createElement("br"));
-                elmt.appendChild(elmt.ownerDocument.createTextNode(
-                    labeller.labelPrecise(this._earliestEnd) + " ~ " + labeller.labelPrecise(this._end)));
-            } else {
-                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
-                elmt.appendChild(elmt.ownerDocument.createElement("br"));
-                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
-            }
-        }
-    },
-
-    fillInfoBubble: function(elmt, theme, labeller) {
-        var doc = elmt.ownerDocument;
-
-        var title = this.getText();
-        var link = this.getLink();
-        var image = this.getImage();
-
-        if (image != null) {
-            var img = doc.createElement("img");
-            img.src = image;
-
-            theme.event.bubble.imageStyler(img);
-            elmt.appendChild(img);
-        }
-
-        var divTitle = doc.createElement("div");
-        var textTitle = doc.createTextNode(title);
-        if (link != null) {
-            var a = doc.createElement("a");
-            a.href = link;
-            a.appendChild(textTitle);
-            divTitle.appendChild(a);
-        } else {
-            divTitle.appendChild(textTitle);
-        }
-        theme.event.bubble.titleStyler(divTitle);
-        elmt.appendChild(divTitle);
-
-        var divBody = doc.createElement("div");
-        this.fillDescription(divBody);
-        theme.event.bubble.bodyStyler(divBody);
-        elmt.appendChild(divBody);
-
-        var divTime = doc.createElement("div");
-        this.fillTime(divTime, labeller);
-        theme.event.bubble.timeStyler(divTime);
-        elmt.appendChild(divTime);
-
-        var divWiki = doc.createElement("div");
-        this.fillWikiInfo(divWiki);
-        theme.event.bubble.wikiStyler(divWiki);
-        elmt.appendChild(divWiki);
-    }
-};
-
-
-/*
- *  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,
- * label or tape element to the associated timeline, band and
- * specific event.
- *
- * Thus a set format is used for the id's of the
- * events' elements on the Timeline--
- *
- * element id format for labels, icons, tapes:
- *   labels: label-tl-<timelineID>-<band_index>-<evt.id>
- *    icons: icon-tl-<timelineID>-<band_index>-<evt.id>
- *    tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
- *           tape2-tl-<timelineID>-<band_index>-<evt.id>
- *           // some events have more than one tape
- *    highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
- *               highlight2-tl-<timelineID>-<band_index>-<evt.id>
- *           // some events have more than one highlight div (future)
- * You can then retrieve the band/timeline objects and event object
- * by using Timeline.EventUtils.decodeEventElID
- *
- *
- */
-
-/*
- *    eventPaintListener functions receive calls about painting.
- *    function(band, op, evt, els)
- *       context: 'this' will be an OriginalEventPainter object.
- *                It has properties and methods for obtaining
- *                the relevant band, timeline, etc
- *       band = the band being painted
- *       op = 'paintStarting' // the painter is about to remove
- *            all previously painted events, if any. It will
- *            then start painting all of the visible events that
- *            pass the filter.
- *            evt = null, els = null
- *       op = 'paintEnded' // the painter has finished painting
- *            all of the visible events that passed the filter
- *            evt = null, els = null
- *       op = 'paintedEvent' // the painter just finished painting an event
- *            evt = event just painted
- *            els = array of painted elements' divs. Depending on the event,
- *                  the array could be just a tape or icon (if no label).
- *                  Or could include label, multiple tape divs (imprecise event),
- *                  highlight divs. The array is not ordered. The meaning of
- *                  each el is available by decoding the el's id
- *      Note that there may be no paintedEvent calls if no events were visible
- *      or passed the filter.
- */
-
-Timeline.OriginalEventPainter = function(params) {
-    this._params = params;
-    this._onSelectListeners = [];
-    this._eventPaintListeners = [];
-
-    this._filterMatcher = null;
-    this._highlightMatcher = null;
-    this._frc = null;
-
-    this._eventIdToElmt = {};
-};
-
-Timeline.OriginalEventPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._backLayer = null;
-    this._eventLayer = null;
-    this._lineLayer = null;
-    this._highlightLayer = null;
-
-    this._eventIdToElmt = null;
-};
-
-Timeline.OriginalEventPainter.prototype.getType = function() {
-    return 'original';
-};
-
-Timeline.OriginalEventPainter.prototype.addOnSelectListener = function(listener) {
-    this._onSelectListeners.push(listener);
-};
-
-Timeline.OriginalEventPainter.prototype.removeOnSelectListener = function(listener) {
-    for (var i = 0; i < this._onSelectListeners.length; i++) {
-        if (this._onSelectListeners[i] == listener) {
-            this._onSelectListeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-Timeline.OriginalEventPainter.prototype.addEventPaintListener = function(listener) {
-    this._eventPaintListeners.push(listener);
-};
-
-Timeline.OriginalEventPainter.prototype.removeEventPaintListener = function(listener) {
-    for (var i = 0; i < this._eventPaintListeners.length; i++) {
-        if (this._eventPaintListeners[i] == listener) {
-            this._eventPaintListeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-Timeline.OriginalEventPainter.prototype.getFilterMatcher = function() {
-    return this._filterMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
-    this._filterMatcher = filterMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.getHighlightMatcher = function() {
-    return this._highlightMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
-    this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.paint = function() {
-    // Paints the events for a given section of the band--what is
-    // visible on screen and some extra.
-    var eventSource = this._band.getEventSource();
-    if (eventSource == null) {
-        return;
-    }
-
-    this._eventIdToElmt = {};
-    this._fireEventPaintListeners('paintStarting', null, null);
-    this._prepareForPainting();
-
-    var metrics = this._computeMetrics();
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    var filterMatcher = (this._filterMatcher != null) ?
-        this._filterMatcher :
-        function(evt) { return true; };
-    var highlightMatcher = (this._highlightMatcher != null) ?
-        this._highlightMatcher :
-        function(evt) { return -1; };
-
-    var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
-    while (iterator.hasNext()) {
-        var evt = iterator.next();
-        if (filterMatcher(evt)) {
-            this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
-        }
-    }
-
-    this._highlightLayer.style.display = "block";
-    this._lineLayer.style.display = "block";
-    this._eventLayer.style.display = "block";
-    // update the band object for max number of tracks in this section of the ether
-    this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement);
-    this._fireEventPaintListeners('paintEnded', null, null);
-
-    this._setOrthogonalOffset(metrics);
-};
-
-Timeline.OriginalEventPainter.prototype.softPaint = function() {
-    this._setOrthogonalOffset(this._computeMetrics());
-};
-
-Timeline.OriginalEventPainter.prototype._setOrthogonalOffset = function(metrics) {
-    var actualViewWidth = 2 * metrics.trackOffset + this._tracks.length * metrics.trackIncrement;
-    var minOrthogonalOffset = Math.min(0, this._band.getViewWidth() - actualViewWidth);
-    var orthogonalOffset = Math.max(minOrthogonalOffset, this._band.getViewOrthogonalOffset());
-
-    this._highlightLayer.style.top =
-        this._lineLayer.style.top =
-            this._eventLayer.style.top =
-                orthogonalOffset + "px";
-};
-
-Timeline.OriginalEventPainter.prototype._computeMetrics = function() {
-     var eventTheme = this._params.theme.event;
-     var trackHeight = Math.max(eventTheme.track.height, eventTheme.tape.height +
-                         this._frc.getLineHeight());
-     var metrics = {
-            trackOffset: eventTheme.track.offset,
-            trackHeight: trackHeight,
-               trackGap: eventTheme.track.gap,
-         trackIncrement: trackHeight + eventTheme.track.gap,
-                   icon: eventTheme.instant.icon,
-              iconWidth: eventTheme.instant.iconWidth,
-             iconHeight: eventTheme.instant.iconHeight,
-             labelWidth: eventTheme.label.width,
-           maxLabelChar: eventTheme.label.maxLabelChar,
-    impreciseIconMargin: eventTheme.instant.impreciseIconMargin
-     };
-
-     return metrics;
-};
-
-Timeline.OriginalEventPainter.prototype._prepareForPainting = function() {
-    // Remove everything previously painted: highlight, line and event layers.
-    // Prepare blank layers for painting.
-    var band = this._band;
-
-    if (this._backLayer == null) {
-        this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
-        this._backLayer.style.visibility = "hidden";
-
-        var eventLabelPrototype = document.createElement("span");
-        eventLabelPrototype.className = "timeline-event-label";
-        this._backLayer.appendChild(eventLabelPrototype);
-        this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
-    }
-    this._frc.update();
-    this._tracks = [];
-
-    if (this._highlightLayer != null) {
-        band.removeLayerDiv(this._highlightLayer);
-    }
-    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
-    this._highlightLayer.style.display = "none";
-
-    if (this._lineLayer != null) {
-        band.removeLayerDiv(this._lineLayer);
-    }
-    this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
-    this._lineLayer.style.display = "none";
-
-    if (this._eventLayer != null) {
-        band.removeLayerDiv(this._eventLayer);
-    }
-    this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
-    this._eventLayer.style.display = "none";
-};
-
-Timeline.OriginalEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isInstant()) {
-        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
-    }
-};
-
-Timeline.OriginalEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isImprecise()) {
-        this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
-    }
-}
-
-Timeline.OriginalEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isImprecise()) {
-        this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
-    }
-}
-
-Timeline.OriginalEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
-    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
-    var labelDivClassName = this._getLabelDivClassName(evt);
-    var labelSize = this._frc.computeSize(text, labelDivClassName);
-    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
-    var labelRight = labelLeft + labelSize.width;
-
-    var rightEdge = labelRight;
-    var track = this._findFreeTrack(evt, rightEdge);
-
-    var labelTop = Math.round(
-        metrics.trackOffset + track * metrics.trackIncrement +
-        metrics.trackHeight / 2 - labelSize.height / 2);
-
-    var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, 0);
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
-        labelSize.height, theme, labelDivClassName, highlightIndex);
-    var els = [iconElmtData.elmt, labelElmtData.elmt];
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
-    if (hDiv != null) {els.push(hDiv);}
-    this._fireEventPaintListeners('paintedEvent', evt, els);
-
-
-    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
-    this._tracks[track] = iconLeftEdge;
-};
-
-Timeline.OriginalEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var endDate = evt.getEnd();
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
-    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
-    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
-    var labelDivClassName = this._getLabelDivClassName(evt);
-    var labelSize = this._frc.computeSize(text, labelDivClassName);
-    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
-    var labelRight = labelLeft + labelSize.width;
-
-    var rightEdge = Math.max(labelRight, endPixel);
-    var track = this._findFreeTrack(evt, rightEdge);
-    var tapeHeight = theme.event.tape.height;
-    var labelTop = Math.round(
-        metrics.trackOffset + track * metrics.trackIncrement + tapeHeight);
-
-    var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, tapeHeight);
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
-                        labelSize.height, theme, labelDivClassName, highlightIndex);
-
-    var color = evt.getColor();
-    color = color != null ? color : theme.event.instant.impreciseColor;
-
-    var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel,
-        color, theme.event.instant.impreciseOpacity, metrics, theme, 0);
-    var els = [iconElmtData.elmt, labelElmtData.elmt, tapeElmtData.elmt];
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
-    if (hDiv != null) {els.push(hDiv);}
-    this._fireEventPaintListeners('paintedEvent', evt, els);
-
-    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
-    this._tracks[track] = iconLeftEdge;
-};
-
-Timeline.OriginalEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var endDate = evt.getEnd();
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
-    var labelDivClassName = this._getLabelDivClassName(evt);
-    var labelSize = this._frc.computeSize(text, labelDivClassName);
-    var labelLeft = startPixel;
-    var labelRight = labelLeft + labelSize.width;
-
-    var rightEdge = Math.max(labelRight, endPixel);
-    var track = this._findFreeTrack(evt, rightEdge);
-    var labelTop = Math.round(
-        metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
-
-    var color = evt.getColor();
-    color = color != null ? color : theme.event.duration.color;
-
-    var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel, color, 100, metrics, theme, 0);
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
-      labelSize.height, theme, labelDivClassName, highlightIndex);
-    var els = [tapeElmtData.elmt, labelElmtData.elmt];
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
-    if (hDiv != null) {els.push(hDiv);}
-    this._fireEventPaintListeners('paintedEvent', evt, els);
-
-    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
-    this._tracks[track] = startPixel;
-};
-
-Timeline.OriginalEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var latestStartDate = evt.getLatestStart();
-    var endDate = evt.getEnd();
-    var earliestEndDate = evt.getEarliestEnd();
-
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
-    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-    var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
-
-    var labelDivClassName = this._getLabelDivClassName(evt);
-    var labelSize = this._frc.computeSize(text, labelDivClassName);
-    var labelLeft = latestStartPixel;
-    var labelRight = labelLeft + labelSize.width;
-
-    var rightEdge = Math.max(labelRight, endPixel);
-    var track = this._findFreeTrack(evt, rightEdge);
-    var labelTop = Math.round(
-        metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
-
-    var color = evt.getColor();
-    color = color != null ? color : theme.event.duration.color;
-
-    // Imprecise events can have two event tapes
-    // The imprecise dates tape, uses opacity to be dimmer than precise dates
-    var impreciseTapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel,
-        theme.event.duration.impreciseColor,
-        theme.event.duration.impreciseOpacity, metrics, theme, 0);
-    // The precise dates tape, regular (100%) opacity
-    var tapeElmtData = this._paintEventTape(evt, track, latestStartPixel,
-        earliestEndPixel, color, 100, metrics, theme, 1);
-
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop,
-        labelSize.width, labelSize.height, theme, labelDivClassName, highlightIndex);
-    var els = [impreciseTapeElmtData.elmt, tapeElmtData.elmt, labelElmtData.elmt];
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
-    if (hDiv != null) {els.push(hDiv);}
-    this._fireEventPaintListeners('paintedEvent', evt, els);
-
-    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
-    this._tracks[track] = startPixel;
-};
-
-Timeline.OriginalEventPainter.prototype._encodeEventElID = function(elType, evt) {
-    return Timeline.EventUtils.encodeEventElID(this._timeline, this._band, elType, evt);
-};
-
-Timeline.OriginalEventPainter.prototype._findFreeTrack = function(event, rightEdge) {
-    var trackAttribute = event.getTrackNum();
-    if (trackAttribute != null) {
-        return trackAttribute; // early return since event includes track number
-    }
-
-    // normal case: find an open track
-    for (var i = 0; i < this._tracks.length; i++) {
-        var t = this._tracks[i];
-        if (t > rightEdge) {
-            break;
-        }
-    }
-    return i;
-};
-
-Timeline.OriginalEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme, tapeHeight) {
-    // If no tape, then paint the icon in the middle of the track.
-    // If there is a tape, paint the icon below the tape + impreciseIconMargin
-    var icon = evt.getIcon();
-    icon = icon != null ? icon : metrics.icon;
-
-    var top; // top of the icon
-    if (tapeHeight > 0) {
-        top = metrics.trackOffset + iconTrack * metrics.trackIncrement +
-              tapeHeight + metrics.impreciseIconMargin;
-    } else {
-        var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement +
-                     metrics.trackHeight / 2;
-        top = Math.round(middle - metrics.iconHeight / 2);
-    }
-    var img = SimileAjax.Graphics.createTranslucentImage(icon);
-    var iconDiv = this._timeline.getDocument().createElement("div");
-    iconDiv.className = this._getElClassName('timeline-event-icon', evt, 'icon');
-    iconDiv.id = this._encodeEventElID('icon', evt);
-    iconDiv.style.left = left + "px";
-    iconDiv.style.top = top + "px";
-    iconDiv.appendChild(img);
-
-    if(evt._title != null)
-        iconDiv.title = evt._title;
-
-    this._eventLayer.appendChild(iconDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  metrics.iconWidth,
-        height: metrics.iconHeight,
-        elmt:   iconDiv
-    };
-};
-
-Timeline.OriginalEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width,
-    height, theme, labelDivClassName, highlightIndex) {
-    var doc = this._timeline.getDocument();
-
-    var labelDiv = doc.createElement("div");
-    labelDiv.className = labelDivClassName;
-    labelDiv.id = this._encodeEventElID('label', evt);
-    labelDiv.style.left = left + "px";
-    labelDiv.style.width = width + "px";
-    labelDiv.style.top = top + "px";
-    labelDiv.innerHTML = text;
-
-    if(evt._title != null)
-        labelDiv.title = evt._title;
-
-    var color = evt.getTextColor();
-    if (color == null) {
-        color = evt.getColor();
-    }
-    if (color != null) {
-        labelDiv.style.color = color;
-    }
-    if (theme.event.highlightLabelBackground && highlightIndex >= 0) {
-        labelDiv.style.background = this._getHighlightColor(highlightIndex, theme);
-    }
-
-    this._eventLayer.appendChild(labelDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  width,
-        height: height,
-        elmt:   labelDiv
-    };
-};
-
-Timeline.OriginalEventPainter.prototype._paintEventTape = function(
-    evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme, tape_index) {
-
-    var tapeWidth = endPixel - startPixel;
-    var tapeHeight = theme.event.tape.height;
-    var top = metrics.trackOffset + iconTrack * metrics.trackIncrement;
-
-    var tapeDiv = this._timeline.getDocument().createElement("div");
-    tapeDiv.className = this._getElClassName('timeline-event-tape', evt, 'tape');
-    tapeDiv.id = this._encodeEventElID('tape' + tape_index, evt);
-    tapeDiv.style.left = startPixel + "px";
-    tapeDiv.style.width = tapeWidth + "px";
-    tapeDiv.style.height = tapeHeight + "px";
-    tapeDiv.style.top = top + "px";
-
-    if(evt._title != null)
-        tapeDiv.title = evt._title;
-
-    if(color != null) {
-        tapeDiv.style.backgroundColor = color;
-    }
-
-    var backgroundImage = evt.getTapeImage();
-    var backgroundRepeat = evt.getTapeRepeat();
-    backgroundRepeat = backgroundRepeat != null ? backgroundRepeat : 'repeat';
-    if(backgroundImage != null) {
-      tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
-      tapeDiv.style.backgroundRepeat = backgroundRepeat;
-    }
-
-    SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
-    this._eventLayer.appendChild(tapeDiv);
-
-    return {
-        left:   startPixel,
-        top:    top,
-        width:  tapeWidth,
-        height: tapeHeight,
-        elmt:   tapeDiv
-    };
-}
-
-Timeline.OriginalEventPainter.prototype._getLabelDivClassName = function(evt) {
-    return this._getElClassName('timeline-event-label', evt, 'label');
-};
-
-Timeline.OriginalEventPainter.prototype._getElClassName = function(elClassName, evt, prefix) {
-    // Prefix and '_' is added to the event's classname. Set to null for no prefix
-    var evt_classname = evt.getClassName(),
-        pieces = [];
-
-    if (evt_classname) {
-      if (prefix) {pieces.push(prefix + '-' + evt_classname + ' ');}
-      pieces.push(evt_classname + ' ');
-    }
-    pieces.push(elClassName);
-    return(pieces.join(''));
-};
-
-Timeline.OriginalEventPainter.prototype._getHighlightColor = function(highlightIndex, theme) {
-    var highlightColors = theme.event.highlightColors;
-    return highlightColors[Math.min(highlightIndex, highlightColors.length - 1)];
-};
-
-Timeline.OriginalEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme, evt) {
-    var div = null;
-    if (highlightIndex >= 0) {
-        var doc = this._timeline.getDocument();
-        var color = this._getHighlightColor(highlightIndex, theme);
-
-        div = doc.createElement("div");
-        div.className = this._getElClassName('timeline-event-highlight', evt, 'highlight');
-        div.id = this._encodeEventElID('highlight0', evt); // in future will have other
-                                                           // highlight divs for tapes + icons
-        div.style.position = "absolute";
-        div.style.overflow = "hidden";
-        div.style.left =    (dimensions.left - 2) + "px";
-        div.style.width =   (dimensions.width + 4) + "px";
-        div.style.top =     (dimensions.top - 2) + "px";
-        div.style.height =  (dimensions.height + 4) + "px";
-        div.style.background = color;
-
-        this._highlightLayer.appendChild(div);
-    }
-    return div;
-};
-
-Timeline.OriginalEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
-    var c = SimileAjax.DOM.getPageCoordinates(icon);
-    this._showBubble(
-        c.left + Math.ceil(icon.offsetWidth / 2),
-        c.top + Math.ceil(icon.offsetHeight / 2),
-        evt
-    );
-    this._fireOnSelect(evt.getID());
-
-    domEvt.cancelBubble = true;
-    SimileAjax.DOM.cancelEvent(domEvt);
-    return false;
-};
-
-Timeline.OriginalEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
-    if ("pageX" in domEvt) {
-        var x = domEvt.pageX;
-        var y = domEvt.pageY;
-    } else {
-        var c = SimileAjax.DOM.getPageCoordinates(target);
-        var x = domEvt.offsetX + c.left;
-        var y = domEvt.offsetY + c.top;
-    }
-    this._showBubble(x, y, evt);
-    this._fireOnSelect(evt.getID());
-
-    domEvt.cancelBubble = true;
-    SimileAjax.DOM.cancelEvent(domEvt);
-    return false;
-};
-
-Timeline.OriginalEventPainter.prototype.showBubble = function(evt) {
-    var elmt = this._eventIdToElmt[evt.getID()];
-    if (elmt) {
-        var c = SimileAjax.DOM.getPageCoordinates(elmt);
-        this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
-    }
-};
-
-Timeline.OriginalEventPainter.prototype._showBubble = function(x, y, evt) {
-    var div = document.createElement("div");
-    var themeBubble = this._params.theme.event.bubble;
-    evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
-
-    SimileAjax.WindowManager.cancelPopups();
-    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y,
-        themeBubble.width, null, themeBubble.maxHeight);
-};
-
-Timeline.OriginalEventPainter.prototype._fireOnSelect = function(eventID) {
-    for (var i = 0; i < this._onSelectListeners.length; i++) {
-        this._onSelectListeners[i](eventID);
-    }
-};
-
-Timeline.OriginalEventPainter.prototype._fireEventPaintListeners = function(op, evt, els) {
-    for (var i = 0; i < this._eventPaintListeners.length; i++) {
-        this._eventPaintListeners[i](this._band, op, evt, els);
-    }
-};
-/*
- *  Detailed Event Painter
- *
- */
-
-// Note: a number of features from original-painter
-//       are not yet implemented in detailed painter.
-//       Eg classname, id attributes for icons, labels, tapes
-
-Timeline.DetailedEventPainter = function(params) {
-    this._params = params;
-    this._onSelectListeners = [];
-
-    this._filterMatcher = null;
-    this._highlightMatcher = null;
-    this._frc = null;
-
-    this._eventIdToElmt = {};
-};
-
-Timeline.DetailedEventPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._backLayer = null;
-    this._eventLayer = null;
-    this._lineLayer = null;
-    this._highlightLayer = null;
-
-    this._eventIdToElmt = null;
-};
-
-Timeline.DetailedEventPainter.prototype.getType = function() {
-    return 'detailed';
-};
-
-Timeline.DetailedEventPainter.prototype.addOnSelectListener = function(listener) {
-    this._onSelectListeners.push(listener);
-};
-
-Timeline.DetailedEventPainter.prototype.removeOnSelectListener = function(listener) {
-    for (var i = 0; i < this._onSelectListeners.length; i++) {
-        if (this._onSelectListeners[i] == listener) {
-            this._onSelectListeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-Timeline.DetailedEventPainter.prototype.getFilterMatcher = function() {
-    return this._filterMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
-    this._filterMatcher = filterMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.getHighlightMatcher = function() {
-    return this._highlightMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
-    this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.paint = function() {
-    var eventSource = this._band.getEventSource();
-    if (eventSource == null) {
-        return;
-    }
-
-    this._eventIdToElmt = {};
-    this._prepareForPainting();
-
-    var eventTheme = this._params.theme.event;
-    var trackHeight = Math.max(eventTheme.track.height, this._frc.getLineHeight());
-    var metrics = {
-        trackOffset:    Math.round(this._band.getViewWidth() / 2 - trackHeight / 2),
-        trackHeight:    trackHeight,
-        trackGap:       eventTheme.track.gap,
-        trackIncrement: trackHeight + eventTheme.track.gap,
-        icon:           eventTheme.instant.icon,
-        iconWidth:      eventTheme.instant.iconWidth,
-        iconHeight:     eventTheme.instant.iconHeight,
-        labelWidth:     eventTheme.label.width
-    }
-
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    var filterMatcher = (this._filterMatcher != null) ?
-        this._filterMatcher :
-        function(evt) { return true; };
-    var highlightMatcher = (this._highlightMatcher != null) ?
-        this._highlightMatcher :
-        function(evt) { return -1; };
-
-    var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
-    while (iterator.hasNext()) {
-        var evt = iterator.next();
-        if (filterMatcher(evt)) {
-            this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
-        }
-    }
-
-    this._highlightLayer.style.display = "block";
-    this._lineLayer.style.display = "block";
-    this._eventLayer.style.display = "block";
-    // update the band object for max number of tracks in this section of the ether
-    this._band.updateEventTrackInfo(this._lowerTracks.length + this._upperTracks.length,
-                                 metrics.trackIncrement);
-};
-
-Timeline.DetailedEventPainter.prototype.softPaint = function() {
-};
-
-Timeline.DetailedEventPainter.prototype._prepareForPainting = function() {
-    var band = this._band;
-
-    if (this._backLayer == null) {
-        this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
-        this._backLayer.style.visibility = "hidden";
-
-        var eventLabelPrototype = document.createElement("span");
-        eventLabelPrototype.className = "timeline-event-label";
-        this._backLayer.appendChild(eventLabelPrototype);
-        this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
-    }
-    this._frc.update();
-    this._lowerTracks = [];
-    this._upperTracks = [];
-
-    if (this._highlightLayer != null) {
-        band.removeLayerDiv(this._highlightLayer);
-    }
-    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
-    this._highlightLayer.style.display = "none";
-
-    if (this._lineLayer != null) {
-        band.removeLayerDiv(this._lineLayer);
-    }
-    this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
-    this._lineLayer.style.display = "none";
-
-    if (this._eventLayer != null) {
-        band.removeLayerDiv(this._eventLayer);
-    }
-    this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
-    this._eventLayer.style.display = "none";
-};
-
-Timeline.DetailedEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isInstant()) {
-        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
-    }
-};
-
-Timeline.DetailedEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isImprecise()) {
-        this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
-    }
-}
-
-Timeline.DetailedEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isImprecise()) {
-        this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
-    }
-}
-
-Timeline.DetailedEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
-    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
-    var labelSize = this._frc.computeSize(text);
-    var iconTrack = this._findFreeTrackForSolid(iconRightEdge, startPixel);
-    var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
-
-    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
-    var labelTrack = iconTrack;
-
-    var iconTrackData = this._getTrackData(iconTrack);
-    if (Math.min(iconTrackData.solid, iconTrackData.text) >= labelLeft + labelSize.width) { // label on the same track, to the right of icon
-        iconTrackData.solid = iconLeftEdge;
-        iconTrackData.text = labelLeft;
-    } else { // label on a different track, below icon
-        iconTrackData.solid = iconLeftEdge;
-
-        labelLeft = startPixel + theme.event.label.offsetFromLine;
-        labelTrack = this._findFreeTrackForText(iconTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
-        this._getTrackData(labelTrack).text = iconLeftEdge;
-
-        this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
-    }
-
-    var labelTop = Math.round(
-        metrics.trackOffset + labelTrack * metrics.trackIncrement +
-        metrics.trackHeight / 2 - labelSize.height / 2);
-
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-
-    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var endDate = evt.getEnd();
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
-    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
-    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
-    var labelSize = this._frc.computeSize(text);
-    var iconTrack = this._findFreeTrackForSolid(endPixel, startPixel);
-
-    var tapeElmtData = this._paintEventTape(evt, iconTrack, startPixel, endPixel,
-        theme.event.instant.impreciseColor, theme.event.instant.impreciseOpacity, metrics, theme);
-    var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
-
-    var iconTrackData = this._getTrackData(iconTrack);
-    iconTrackData.solid = iconLeftEdge;
-
-    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
-    var labelRight = labelLeft + labelSize.width;
-    var labelTrack;
-    if (labelRight < endPixel) {
-        labelTrack = iconTrack;
-    } else {
-        labelLeft = startPixel + theme.event.label.offsetFromLine;
-        labelRight = labelLeft + labelSize.width;
-
-        labelTrack = this._findFreeTrackForText(iconTrack, labelRight, function(t) { t.line = startPixel - 2; });
-        this._getTrackData(labelTrack).text = iconLeftEdge;
-
-        this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
-    }
-    var labelTop = Math.round(
-        metrics.trackOffset + labelTrack * metrics.trackIncrement +
-        metrics.trackHeight / 2 - labelSize.height / 2);
-
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-
-    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var endDate = evt.getEnd();
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
-    var labelSize = this._frc.computeSize(text);
-    var tapeTrack = this._findFreeTrackForSolid(endPixel);
-    var color = evt.getColor();
-    color = color != null ? color : theme.event.duration.color;
-
-    var tapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel, color, 100, metrics, theme);
-
-    var tapeTrackData = this._getTrackData(tapeTrack);
-    tapeTrackData.solid = startPixel;
-
-    var labelLeft = startPixel + theme.event.label.offsetFromLine;
-    var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
-    this._getTrackData(labelTrack).text = startPixel - 2;
-
-    this._paintEventLine(evt, startPixel, tapeTrack, labelTrack, metrics, theme);
-
-    var labelTop = Math.round(
-        metrics.trackOffset + labelTrack * metrics.trackIncrement +
-        metrics.trackHeight / 2 - labelSize.height / 2);
-
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
-
-    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    var doc = this._timeline.getDocument();
-    var text = evt.getText();
-
-    var startDate = evt.getStart();
-    var latestStartDate = evt.getLatestStart();
-    var endDate = evt.getEnd();
-    var earliestEndDate = evt.getEarliestEnd();
-
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-    var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
-    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-    var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
-
-    var labelSize = this._frc.computeSize(text);
-    var tapeTrack = this._findFreeTrackForSolid(endPixel);
-    var color = evt.getColor();
-    color = color != null ? color : theme.event.duration.color;
-
-    var impreciseTapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel,
-        theme.event.duration.impreciseColor, theme.event.duration.impreciseOpacity, metrics, theme);
-    var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel, color, 100, metrics, theme);
-
-    var tapeTrackData = this._getTrackData(tapeTrack);
-    tapeTrackData.solid = startPixel;
-
-    var labelLeft = latestStartPixel + theme.event.label.offsetFromLine;
-    var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = latestStartPixel - 2; });
-    this._getTrackData(labelTrack).text = latestStartPixel - 2;
-
-    this._paintEventLine(evt, latestStartPixel, tapeTrack, labelTrack, metrics, theme);
-
-    var labelTop = Math.round(
-        metrics.trackOffset + labelTrack * metrics.trackIncrement +
-        metrics.trackHeight / 2 - labelSize.height / 2);
-
-    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-    this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
-
-    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeTrackForSolid = function(solidEdge, softEdge) {
-    for (var i = 0; true; i++) {
-        if (i < this._lowerTracks.length) {
-            var t = this._lowerTracks[i];
-            if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
-                return i;
-            }
-        } else {
-            this._lowerTracks.push({
-                solid:  Number.POSITIVE_INFINITY,
-                text:   Number.POSITIVE_INFINITY,
-                line:   Number.POSITIVE_INFINITY
-            });
-
-            return i;
-        }
-
-        if (i < this._upperTracks.length) {
-            var t = this._upperTracks[i];
-            if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
-                return -1 - i;
-            }
-        } else {
-            this._upperTracks.push({
-                solid:  Number.POSITIVE_INFINITY,
-                text:   Number.POSITIVE_INFINITY,
-                line:   Number.POSITIVE_INFINITY
-            });
-
-            return -1 - i;
-        }
-    }
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeTrackForText = function(fromTrack, edge, occupiedTrackVisitor) {
-    var extendUp;
-    var index;
-    var firstIndex;
-    var result;
-
-    if (fromTrack < 0) {
-        extendUp = true;
-        firstIndex = -fromTrack;
-
-        index = this._findFreeUpperTrackForText(firstIndex, edge);
-        result = -1 - index;
-    } else if (fromTrack > 0) {
-        extendUp = false;
-        firstIndex = fromTrack + 1;
-
-        index = this._findFreeLowerTrackForText(firstIndex, edge);
-        result = index;
-    } else {
-        var upIndex = this._findFreeUpperTrackForText(0, edge);
-        var downIndex = this._findFreeLowerTrackForText(1, edge);
-
-        if (downIndex - 1 <= upIndex) {
-            extendUp = false;
-            firstIndex = 1;
-            index = downIndex;
-            result = index;
-        } else {
-            extendUp = true;
-            firstIndex = 0;
-            index = upIndex;
-            result = -1 - index;
-        }
-    }
-
-    if (extendUp) {
-        if (index == this._upperTracks.length) {
-            this._upperTracks.push({
-                solid:  Number.POSITIVE_INFINITY,
-                text:   Number.POSITIVE_INFINITY,
-                line:   Number.POSITIVE_INFINITY
-            });
-        }
-        for (var i = firstIndex; i < index; i++) {
-            occupiedTrackVisitor(this._upperTracks[i]);
-        }
-    } else {
-        if (index == this._lowerTracks.length) {
-            this._lowerTracks.push({
-                solid:  Number.POSITIVE_INFINITY,
-                text:   Number.POSITIVE_INFINITY,
-                line:   Number.POSITIVE_INFINITY
-            });
-        }
-        for (var i = firstIndex; i < index; i++) {
-            occupiedTrackVisitor(this._lowerTracks[i]);
-        }
-    }
-    return result;
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeLowerTrackForText = function(index, edge) {
-    for (; index < this._lowerTracks.length; index++) {
-        var t = this._lowerTracks[index];
-        if (Math.min(t.solid, t.text) >= edge) {
-            break;
-        }
-    }
-    return index;
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeUpperTrackForText = function(index, edge) {
-    for (; index < this._upperTracks.length; index++) {
-        var t = this._upperTracks[index];
-        if (Math.min(t.solid, t.text) >= edge) {
-            break;
-        }
-    }
-    return index;
-};
-
-Timeline.DetailedEventPainter.prototype._getTrackData = function(index) {
-    return (index < 0) ? this._upperTracks[-index - 1] : this._lowerTracks[index];
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventLine = function(evt, left, startTrack, endTrack, metrics, theme) {
-    var top = Math.round(metrics.trackOffset + startTrack * metrics.trackIncrement + metrics.trackHeight / 2);
-    var height = Math.round(Math.abs(endTrack - startTrack) * metrics.trackIncrement);
-
-    var lineStyle = "1px solid " + theme.event.label.lineColor;
-    var lineDiv = this._timeline.getDocument().createElement("div");
-	lineDiv.style.position = "absolute";
-    lineDiv.style.left = left + "px";
-    lineDiv.style.width = theme.event.label.offsetFromLine + "px";
-    lineDiv.style.height = height + "px";
-    if (startTrack > endTrack) {
-        lineDiv.style.top = (top - height) + "px";
-        lineDiv.style.borderTop = lineStyle;
-    } else {
-        lineDiv.style.top = top + "px";
-        lineDiv.style.borderBottom = lineStyle;
-    }
-    lineDiv.style.borderLeft = lineStyle;
-    this._lineLayer.appendChild(lineDiv);
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme) {
-    var icon = evt.getIcon();
-    icon = icon != null ? icon : metrics.icon;
-
-    var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
-    var top = Math.round(middle - metrics.iconHeight / 2);
-
-    var img = SimileAjax.Graphics.createTranslucentImage(icon);
-    var iconDiv = this._timeline.getDocument().createElement("div");
-    iconDiv.style.position = "absolute";
-    iconDiv.style.left = left + "px";
-    iconDiv.style.top = top + "px";
-    iconDiv.appendChild(img);
-    iconDiv.style.cursor = "pointer";
-
-    if(evt._title != null)
-        iconDiv.title = evt._title
-
-    this._eventLayer.appendChild(iconDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  metrics.iconWidth,
-        height: metrics.iconHeight,
-        elmt:   iconDiv
-    };
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width, height, theme) {
-    var doc = this._timeline.getDocument();
-
-    var labelBackgroundDiv = doc.createElement("div");
-    labelBackgroundDiv.style.position = "absolute";
-    labelBackgroundDiv.style.left = left + "px";
-    labelBackgroundDiv.style.width = width + "px";
-    labelBackgroundDiv.style.top = top + "px";
-    labelBackgroundDiv.style.height = height + "px";
-    labelBackgroundDiv.style.backgroundColor = theme.event.label.backgroundColor;
-    SimileAjax.Graphics.setOpacity(labelBackgroundDiv, theme.event.label.backgroundOpacity);
-    this._eventLayer.appendChild(labelBackgroundDiv);
-
-    var labelDiv = doc.createElement("div");
-    labelDiv.style.position = "absolute";
-    labelDiv.style.left = left + "px";
-    labelDiv.style.width = width + "px";
-    labelDiv.style.top = top + "px";
-    labelDiv.innerHTML = text;
-    labelDiv.style.cursor = "pointer";
-
-    if(evt._title != null)
-        labelDiv.title = evt._title;
-
-    var color = evt.getTextColor();
-    if (color == null) {
-        color = evt.getColor();
-    }
-    if (color != null) {
-        labelDiv.style.color = color;
-    }
-
-    this._eventLayer.appendChild(labelDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  width,
-        height: height,
-        elmt:   labelDiv
-    };
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventTape = function(
-    evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme) {
-
-    var tapeWidth = endPixel - startPixel;
-    var tapeHeight = theme.event.tape.height;
-    var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
-    var top = Math.round(middle - tapeHeight / 2);
-
-    var tapeDiv = this._timeline.getDocument().createElement("div");
-    tapeDiv.style.position = "absolute";
-    tapeDiv.style.left = startPixel + "px";
-    tapeDiv.style.width = tapeWidth + "px";
-    tapeDiv.style.top = top + "px";
-    tapeDiv.style.height = tapeHeight + "px";
-    tapeDiv.style.backgroundColor = color;
-    tapeDiv.style.overflow = "hidden";
-    tapeDiv.style.cursor = "pointer";
-
-    if(evt._title != null)
-        tapeDiv.title = evt._title;
-
-    SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
-    this._eventLayer.appendChild(tapeDiv);
-
-    return {
-        left:   startPixel,
-        top:    top,
-        width:  tapeWidth,
-        height: tapeHeight,
-        elmt:   tapeDiv
-    };
-}
-
-Timeline.DetailedEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
-    if (highlightIndex >= 0) {
-        var doc = this._timeline.getDocument();
-        var eventTheme = theme.event;
-
-        var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
-
-        var div = doc.createElement("div");
-        div.style.position = "absolute";
-        div.style.overflow = "hidden";
-        div.style.left =    (dimensions.left - 2) + "px";
-        div.style.width =   (dimensions.width + 4) + "px";
-        div.style.top =     (dimensions.top - 2) + "px";
-        div.style.height =  (dimensions.height + 4) + "px";
-        div.style.background = color;
-
-        this._highlightLayer.appendChild(div);
-    }
-};
-
-Timeline.DetailedEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
-    var c = SimileAjax.DOM.getPageCoordinates(icon);
-    this._showBubble(
-        c.left + Math.ceil(icon.offsetWidth / 2),
-        c.top + Math.ceil(icon.offsetHeight / 2),
-        evt
-    );
-    this._fireOnSelect(evt.getID());
-
-    domEvt.cancelBubble = true;
-    SimileAjax.DOM.cancelEvent(domEvt);
-    return false;
-};
-
-Timeline.DetailedEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
-    if ("pageX" in domEvt) {
-        var x = domEvt.pageX;
-        var y = domEvt.pageY;
-    } else {
-        var c = SimileAjax.DOM.getPageCoordinates(target);
-        var x = domEvt.offsetX + c.left;
-        var y = domEvt.offsetY + c.top;
-    }
-    this._showBubble(x, y, evt);
-    this._fireOnSelect(evt.getID());
-
-    domEvt.cancelBubble = true;
-    SimileAjax.DOM.cancelEvent(domEvt);
-    return false;
-};
-
-Timeline.DetailedEventPainter.prototype.showBubble = function(evt) {
-    var elmt = this._eventIdToElmt[evt.getID()];
-    if (elmt) {
-        var c = SimileAjax.DOM.getPageCoordinates(elmt);
-        this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
-    }
-};
-
-Timeline.DetailedEventPainter.prototype._showBubble = function(x, y, evt) {
-    var div = document.createElement("div");
-    var themeBubble = this._params.theme.event.bubble;
-    evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
-
-    SimileAjax.WindowManager.cancelPopups();
-    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y,
-       themeBubble.width, null, themeBubble.maxHeight);
-};
-
-Timeline.DetailedEventPainter.prototype._fireOnSelect = function(eventID) {
-    for (var i = 0; i < this._onSelectListeners.length; i++) {
-        this._onSelectListeners[i](eventID);
-    }
-};
-/*
- *  Overview Event Painter
- *
- */
-
-Timeline.OverviewEventPainter = function(params) {
-    this._params = params;
-    this._onSelectListeners = [];
-
-    this._filterMatcher = null;
-    this._highlightMatcher = null;
-};
-
-Timeline.OverviewEventPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._eventLayer = null;
-    this._highlightLayer = null;
-};
-
-Timeline.OverviewEventPainter.prototype.getType = function() {
-    return 'overview';
-};
-
-Timeline.OverviewEventPainter.prototype.addOnSelectListener = function(listener) {
-    this._onSelectListeners.push(listener);
-};
-
-Timeline.OverviewEventPainter.prototype.removeOnSelectListener = function(listener) {
-    for (var i = 0; i < this._onSelectListeners.length; i++) {
-        if (this._onSelectListeners[i] == listener) {
-            this._onSelectListeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-Timeline.OverviewEventPainter.prototype.getFilterMatcher = function() {
-    return this._filterMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
-    this._filterMatcher = filterMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.getHighlightMatcher = function() {
-    return this._highlightMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
-    this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.paint = function() {
-    var eventSource = this._band.getEventSource();
-    if (eventSource == null) {
-        return;
-    }
-
-    this._prepareForPainting();
-
-    var eventTheme = this._params.theme.event;
-    var metrics = {
-        trackOffset:    eventTheme.overviewTrack.offset,
-        trackHeight:    eventTheme.overviewTrack.height,
-        trackGap:       eventTheme.overviewTrack.gap,
-        trackIncrement: eventTheme.overviewTrack.height + eventTheme.overviewTrack.gap
-    }
-
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    var filterMatcher = (this._filterMatcher != null) ?
-        this._filterMatcher :
-        function(evt) { return true; };
-    var highlightMatcher = (this._highlightMatcher != null) ?
-        this._highlightMatcher :
-        function(evt) { return -1; };
-
-    var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
-    while (iterator.hasNext()) {
-        var evt = iterator.next();
-        if (filterMatcher(evt)) {
-            this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
-        }
-    }
-
-    this._highlightLayer.style.display = "block";
-    this._eventLayer.style.display = "block";
-    // update the band object for max number of tracks in this section of the ether
-    this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement);
-};
-
-Timeline.OverviewEventPainter.prototype.softPaint = function() {
-};
-
-Timeline.OverviewEventPainter.prototype._prepareForPainting = function() {
-    var band = this._band;
-
-    this._tracks = [];
-
-    if (this._highlightLayer != null) {
-        band.removeLayerDiv(this._highlightLayer);
-    }
-    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
-    this._highlightLayer.style.display = "none";
-
-    if (this._eventLayer != null) {
-        band.removeLayerDiv(this._eventLayer);
-    }
-    this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
-    this._eventLayer.style.display = "none";
-};
-
-Timeline.OverviewEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isInstant()) {
-        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
-    }
-};
-
-Timeline.OverviewEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    var startDate = evt.getStart();
-    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-
-    var color = evt.getColor(),
-        klassName = evt.getClassName();
-    if (klassName) {
-      color = null;
-    } else {
-      color = color != null ? color : theme.event.duration.color;
-    }
-
-    var tickElmtData = this._paintEventTick(evt, startPixel, color, 100, metrics, theme);
-
-    this._createHighlightDiv(highlightIndex, tickElmtData, theme);
-};
-
-Timeline.OverviewEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    var latestStartDate = evt.getLatestStart();
-    var earliestEndDate = evt.getEarliestEnd();
-
-    var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
-    var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
-
-    var tapeTrack = 0;
-    for (; tapeTrack < this._tracks.length; tapeTrack++) {
-        if (earliestEndPixel < this._tracks[tapeTrack]) {
-            break;
-        }
-    }
-    this._tracks[tapeTrack] = earliestEndPixel;
-
-    var color = evt.getColor(),
-        klassName = evt.getClassName();
-    if (klassName) {
-      color = null;
-    } else {
-      color = color != null ? color : theme.event.duration.color;
-    }
-
-    var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel,
-      color, 100, metrics, theme, klassName);
-
-    this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
-};
-
-Timeline.OverviewEventPainter.prototype._paintEventTape = function(
-    evt, track, left, right, color, opacity, metrics, theme, klassName) {
-
-    var top = metrics.trackOffset + track * metrics.trackIncrement;
-    var width = right - left;
-    var height = metrics.trackHeight;
-
-    var tapeDiv = this._timeline.getDocument().createElement("div");
-    tapeDiv.className = 'timeline-small-event-tape'
-    if (klassName) {tapeDiv.className += ' small-' + klassName;}
-    tapeDiv.style.left = left + "px";
-    tapeDiv.style.width = width + "px";
-    tapeDiv.style.top = top + "px";
-    tapeDiv.style.height = height + "px";
-
-    if (color) {
-      tapeDiv.style.backgroundColor = color; // set color here if defined by event. Else use css
-    }
- //   tapeDiv.style.overflow = "hidden";   // now set in css
- //   tapeDiv.style.position = "absolute";
-    if(opacity<100) SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
-    this._eventLayer.appendChild(tapeDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  width,
-        height: height,
-        elmt:   tapeDiv
-    };
-}
-
-Timeline.OverviewEventPainter.prototype._paintEventTick = function(
-    evt, left, color, opacity, metrics, theme) {
-
-    var height = theme.event.overviewTrack.tickHeight;
-    var top = metrics.trackOffset - height;
-    var width = 1;
-
-    var tickDiv = this._timeline.getDocument().createElement("div");
-	  tickDiv.className = 'timeline-small-event-icon'
-    tickDiv.style.left = left + "px";
-    tickDiv.style.top = top + "px";
-  //  tickDiv.style.width = width + "px";
-  //  tickDiv.style.position = "absolute";
-  //  tickDiv.style.height = height + "px";
-  //  tickDiv.style.backgroundColor = color;
-  //  tickDiv.style.overflow = "hidden";
-
-    var klassName = evt.getClassName()
-    if (klassName) {tickDiv.className +=' small-' + klassName};
-
-    if(opacity<100) {SimileAjax.Graphics.setOpacity(tickDiv, opacity)};
-
-    this._eventLayer.appendChild(tickDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  width,
-        height: height,
-        elmt:   tickDiv
-    };
-}
-
-Timeline.OverviewEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
-    if (highlightIndex >= 0) {
-        var doc = this._timeline.getDocument();
-        var eventTheme = theme.event;
-
-        var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
-
-        var div = doc.createElement("div");
-        div.style.position = "absolute";
-        div.style.overflow = "hidden";
-        div.style.left =    (dimensions.left - 1) + "px";
-        div.style.width =   (dimensions.width + 2) + "px";
-        div.style.top =     (dimensions.top - 1) + "px";
-        div.style.height =  (dimensions.height + 2) + "px";
-        div.style.background = color;
-
-        this._highlightLayer.appendChild(div);
-    }
-};
-
-Timeline.OverviewEventPainter.prototype.showBubble = function(evt) {
-    // not implemented
-};
-/*
- *  Compact Event Painter
- *
- */
-
-Timeline.CompactEventPainter = function(params) {
-    this._params = params;
-    this._onSelectListeners = [];
-
-    this._filterMatcher = null;
-    this._highlightMatcher = null;
-    this._frc = null;
-
-    this._eventIdToElmt = {};
-};
-
-Timeline.CompactEventPainter.prototype.getType = function() {
-    return 'compact';
-};
-
-Timeline.CompactEventPainter.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._backLayer = null;
-    this._eventLayer = null;
-    this._lineLayer = null;
-    this._highlightLayer = null;
-
-    this._eventIdToElmt = null;
-};
-
-Timeline.CompactEventPainter.prototype.addOnSelectListener = function(listener) {
-    this._onSelectListeners.push(listener);
-};
-
-Timeline.CompactEventPainter.prototype.removeOnSelectListener = function(listener) {
-    for (var i = 0; i < this._onSelectListeners.length; i++) {
-        if (this._onSelectListeners[i] == listener) {
-            this._onSelectListeners.splice(i, 1);
-            break;
-        }
-    }
-};
-
-Timeline.CompactEventPainter.prototype.getFilterMatcher = function() {
-    return this._filterMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
-    this._filterMatcher = filterMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.getHighlightMatcher = function() {
-    return this._highlightMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
-    this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.paint = function() {
-    var eventSource = this._band.getEventSource();
-    if (eventSource == null) {
-        return;
-    }
-
-    this._eventIdToElmt = {};
-    this._prepareForPainting();
-
-    var metrics = this._computeMetrics();
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    var filterMatcher = (this._filterMatcher != null) ?
-        this._filterMatcher :
-        function(evt) { return true; };
-
-    var highlightMatcher = (this._highlightMatcher != null) ?
-        this._highlightMatcher :
-        function(evt) { return -1; };
-
-    var iterator = eventSource.getEventIterator(minDate, maxDate);
-
-    var stackConcurrentPreciseInstantEvents = "stackConcurrentPreciseInstantEvents" in this._params && typeof this._params.stackConcurrentPreciseInstantEvents == "object";
-    var collapseConcurrentPreciseInstantEvents = "collapseConcurrentPreciseInstantEvents" in this._params && this._params.collapseConcurrentPreciseInstantEvents;
-    if (collapseConcurrentPreciseInstantEvents || stackConcurrentPreciseInstantEvents) {
-        var bufferedEvents = [];
-        var previousInstantEvent = null;
-
-        while (iterator.hasNext()) {
-            var evt = iterator.next();
-            if (filterMatcher(evt)) {
-                if (!evt.isInstant() || evt.isImprecise()) {
-                    this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
-                } else if (previousInstantEvent != null &&
-                        previousInstantEvent.getStart().getTime() == evt.getStart().getTime()) {
-                    bufferedEvents[bufferedEvents.length - 1].push(evt);
-                } else {
-                    bufferedEvents.push([ evt ]);
-                    previousInstantEvent = evt;
-                }
-            }
-        }
-
-        for (var i = 0; i < bufferedEvents.length; i++) {
-            var compositeEvents = bufferedEvents[i];
-            if (compositeEvents.length == 1) {
-                this.paintEvent(compositeEvents[0], metrics, this._params.theme, highlightMatcher(evt));
-            } else {
-                var match = -1;
-                for (var j = 0; match < 0 && j < compositeEvents.length; j++) {
-                    match = highlightMatcher(compositeEvents[j]);
-                }
-
-                if (stackConcurrentPreciseInstantEvents) {
-                    this.paintStackedPreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
-                } else {
-                    this.paintCompositePreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
-                }
-            }
-        }
-    } else {
-        while (iterator.hasNext()) {
-            var evt = iterator.next();
-            if (filterMatcher(evt)) {
-                this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
-            }
-        }
-    }
-
-    this._highlightLayer.style.display = "block";
-    this._lineLayer.style.display = "block";
-    this._eventLayer.style.display = "block";
-
-    this._setOrthogonalOffset(metrics);
-};
-
-Timeline.CompactEventPainter.prototype.softPaint = function() {
-    this._setOrthogonalOffset(this._computeMetrics());
-};
-
-Timeline.CompactEventPainter.prototype._setOrthogonalOffset = function(metrics) {
-    var actualViewWidth = 2 * metrics.trackOffset + this._tracks.length * metrics.trackHeight;
-    var minOrthogonalOffset = Math.min(0, this._band.getViewWidth() - actualViewWidth);
-    var orthogonalOffset = Math.max(minOrthogonalOffset, this._band.getViewOrthogonalOffset());
-
-    this._highlightLayer.style.top =
-        this._lineLayer.style.top =
-            this._eventLayer.style.top =
-                orthogonalOffset + "px";
-};
-
-Timeline.CompactEventPainter.prototype._computeMetrics = function() {
-    var theme = this._params.theme;
-    var eventTheme = theme.event;
-
-    var metrics = {
-        trackOffset:            "trackOffset" in this._params ? this._params.trackOffset : 10,
-        trackHeight:            "trackHeight" in this._params ? this._params.trackHeight : 10,
-
-        tapeHeight:             theme.event.tape.height,
-        tapeBottomMargin:       "tapeBottomMargin" in this._params ? this._params.tapeBottomMargin : 2,
-
-        labelBottomMargin:      "labelBottomMargin" in this._params ? this._params.labelBottomMargin : 5,
-        labelRightMargin:       "labelRightMargin" in this._params ? this._params.labelRightMargin : 5,
-
-        defaultIcon:            eventTheme.instant.icon,
-        defaultIconWidth:       eventTheme.instant.iconWidth,
-        defaultIconHeight:      eventTheme.instant.iconHeight,
-
-        customIconWidth:        "iconWidth" in this._params ? this._params.iconWidth : eventTheme.instant.iconWidth,
-        customIconHeight:       "iconHeight" in this._params ? this._params.iconHeight : eventTheme.instant.iconHeight,
-
-        iconLabelGap:           "iconLabelGap" in this._params ? this._params.iconLabelGap : 2,
-        iconBottomMargin:       "iconBottomMargin" in this._params ? this._params.iconBottomMargin : 2
-    };
-    if ("compositeIcon" in this._params) {
-        metrics.compositeIcon = this._params.compositeIcon;
-        metrics.compositeIconWidth = this._params.compositeIconWidth || metrics.customIconWidth;
-        metrics.compositeIconHeight = this._params.compositeIconHeight || metrics.customIconHeight;
-    } else {
-        metrics.compositeIcon = metrics.defaultIcon;
-        metrics.compositeIconWidth = metrics.defaultIconWidth;
-        metrics.compositeIconHeight = metrics.defaultIconHeight;
-    }
-    metrics.defaultStackIcon = "icon" in this._params.stackConcurrentPreciseInstantEvents ?
-        this._params.stackConcurrentPreciseInstantEvents.icon : metrics.defaultIcon;
-    metrics.defaultStackIconWidth = "iconWidth" in this._params.stackConcurrentPreciseInstantEvents ?
-        this._params.stackConcurrentPreciseInstantEvents.iconWidth : metrics.defaultIconWidth;
-    metrics.defaultStackIconHeight = "iconHeight" in this._params.stackConcurrentPreciseInstantEvents ?
-        this._params.stackConcurrentPreciseInstantEvents.iconHeight : metrics.defaultIconHeight;
-
-    return metrics;
-};
-
-Timeline.CompactEventPainter.prototype._prepareForPainting = function() {
-    var band = this._band;
-
-    if (this._backLayer == null) {
-        this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
-        this._backLayer.style.visibility = "hidden";
-
-        var eventLabelPrototype = document.createElement("span");
-        eventLabelPrototype.className = "timeline-event-label";
-        this._backLayer.appendChild(eventLabelPrototype);
-        this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
-    }
-    this._frc.update();
-    this._tracks = [];
-
-    if (this._highlightLayer != null) {
-        band.removeLayerDiv(this._highlightLayer);
-    }
-    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
-    this._highlightLayer.style.display = "none";
-
-    if (this._lineLayer != null) {
-        band.removeLayerDiv(this._lineLayer);
-    }
-    this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
-    this._lineLayer.style.display = "none";
-
-    if (this._eventLayer != null) {
-        band.removeLayerDiv(this._eventLayer);
-    }
-    this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
-    this._eventLayer.style.display = "none";
-};
-
-Timeline.CompactEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isInstant()) {
-        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
-    }
-};
-
-Timeline.CompactEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isImprecise()) {
-        this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
-    }
-}
-
-Timeline.CompactEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    if (evt.isImprecise()) {
-        this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
-    } else {
-        this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
-    }
-}
-
-Timeline.CompactEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    var commonData = {
-        tooltip: evt.getProperty("tooltip") || evt.getText()
-    };
-
-    var iconData = {
-        url: evt.getIcon()
-    };
-    if (iconData.url == null) {
-        iconData.url = metrics.defaultIcon;
-        iconData.width = metrics.defaultIconWidth;
-        iconData.height = metrics.defaultIconHeight;
-        iconData.className = "timeline-event-icon-default";
-    } else {
-        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
-        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
-    }
-
-    var labelData = {
-        text:       evt.getText(),
-        color:      evt.getTextColor() || evt.getColor(),
-        className:  evt.getClassName()
-    };
-
-    var result = this.paintTapeIconLabel(
-        evt.getStart(),
-        commonData,
-        null, // no tape data
-        iconData,
-        labelData,
-        metrics,
-        theme,
-        highlightIndex
-    );
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
-    };
-    SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
-
-    this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
-};
-
-Timeline.CompactEventPainter.prototype.paintCompositePreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
-    var evt = events[0];
-
-    var tooltips = [];
-    for (var i = 0; i < events.length; i++) {
-        tooltips.push(events[i].getProperty("tooltip") || events[i].getText());
-    }
-    var commonData = {
-        tooltip: tooltips.join("; ")
-    };
-
-    var iconData = {
-        url: metrics.compositeIcon,
-        width: metrics.compositeIconWidth,
-        height: metrics.compositeIconHeight,
-        className: "timeline-event-icon-composite"
-    };
-
-    var labelData = {
-        text: String.substitute(this._params.compositeEventLabelTemplate, [ events.length ])
-    };
-
-    var result = this.paintTapeIconLabel(
-        evt.getStart(),
-        commonData,
-        null, // no tape data
-        iconData,
-        labelData,
-        metrics,
-        theme,
-        highlightIndex
-    );
-
-    var self = this;
-    var clickHandler = function(elmt, domEvt, target) {
-        return self._onClickMultiplePreciseInstantEvent(result.iconElmtData.elmt, domEvt, events);
-    };
-
-    SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
-
-    for (var i = 0; i < events.length; i++) {
-        this._eventIdToElmt[events[i].getID()] = result.iconElmtData.elmt;
-    }
-};
-
-Timeline.CompactEventPainter.prototype.paintStackedPreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
-    var limit = "limit" in this._params.stackConcurrentPreciseInstantEvents ?
-        this._params.stackConcurrentPreciseInstantEvents.limit : 10;
-    var moreMessageTemplate = "moreMessageTemplate" in this._params.stackConcurrentPreciseInstantEvents ?
-        this._params.stackConcurrentPreciseInstantEvents.moreMessageTemplate : "%0 More Events";
-    var showMoreMessage = limit <= events.length - 2; // We want at least 2 more events above the limit.
-                                                      // Otherwise we'd need the singular case of "1 More Event"
-
-    var band = this._band;
-    var getPixelOffset = function(date) {
-        return Math.round(band.dateToPixelOffset(date));
-    };
-    var getIconData = function(evt) {
-        var iconData = {
-            url: evt.getIcon()
-        };
-        if (iconData.url == null) {
-            iconData.url = metrics.defaultStackIcon;
-            iconData.width = metrics.defaultStackIconWidth;
-            iconData.height = metrics.defaultStackIconHeight;
-            iconData.className = "timeline-event-icon-stack timeline-event-icon-default";
-        } else {
-            iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
-            iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
-            iconData.className = "timeline-event-icon-stack";
-        }
-        return iconData;
-    };
-
-    var firstIconData = getIconData(events[0]);
-    var horizontalIncrement = 5;
-    var leftIconEdge = 0;
-    var totalLabelWidth = 0;
-    var totalLabelHeight = 0;
-    var totalIconHeight = 0;
-
-    var records = [];
-    for (var i = 0; i < events.length && (!showMoreMessage || i < limit); i++) {
-        var evt = events[i];
-        var text = evt.getText();
-        var iconData = getIconData(evt);
-        var labelSize = this._frc.computeSize(text);
-        var record = {
-            text:       text,
-            iconData:   iconData,
-            labelSize:  labelSize,
-            iconLeft:   firstIconData.width + i * horizontalIncrement - iconData.width
-        };
-        record.labelLeft = firstIconData.width + i * horizontalIncrement + metrics.iconLabelGap;
-        record.top = totalLabelHeight;
-        records.push(record);
-
-        leftIconEdge = Math.min(leftIconEdge, record.iconLeft);
-        totalLabelHeight += labelSize.height;
-        totalLabelWidth = Math.max(totalLabelWidth, record.labelLeft + labelSize.width);
-        totalIconHeight = Math.max(totalIconHeight, record.top + iconData.height);
-    }
-    if (showMoreMessage) {
-        var moreMessage = String.substitute(moreMessageTemplate, [ events.length - limit ]);
-
-        var moreMessageLabelSize = this._frc.computeSize(moreMessage);
-        var moreMessageLabelLeft = firstIconData.width + (limit - 1) * horizontalIncrement + metrics.iconLabelGap;
-        var moreMessageLabelTop = totalLabelHeight;
-
-        totalLabelHeight += moreMessageLabelSize.height;
-        totalLabelWidth = Math.max(totalLabelWidth, moreMessageLabelLeft + moreMessageLabelSize.width);
-    }
-    totalLabelWidth += metrics.labelRightMargin;
-    totalLabelHeight += metrics.labelBottomMargin;
-    totalIconHeight += metrics.iconBottomMargin;
-
-    var anchorPixel = getPixelOffset(events[0].getStart());
-    var newTracks = [];
-
-    var trackCount = Math.ceil(Math.max(totalIconHeight, totalLabelHeight) / metrics.trackHeight);
-    var rightIconEdge = firstIconData.width + (events.length - 1) * horizontalIncrement;
-    for (var i = 0; i < trackCount; i++) {
-        newTracks.push({ start: leftIconEdge, end: rightIconEdge });
-    }
-    var labelTrackCount = Math.ceil(totalLabelHeight / metrics.trackHeight);
-    for (var i = 0; i < labelTrackCount; i++) {
-        var track = newTracks[i];
-        track.end = Math.max(track.end, totalLabelWidth);
-    }
-
-    var firstTrack = this._fitTracks(anchorPixel, newTracks);
-    var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
-
-    var iconStackDiv = this._timeline.getDocument().createElement("div");
-    iconStackDiv.className = 'timeline-event-icon-stack';
-    iconStackDiv.style.position = "absolute";
-    iconStackDiv.style.overflow = "visible";
-    iconStackDiv.style.left = anchorPixel + "px";
-    iconStackDiv.style.top = verticalPixelOffset + "px";
-    iconStackDiv.style.width = rightIconEdge + "px";
-    iconStackDiv.style.height = totalIconHeight + "px";
-    iconStackDiv.innerHTML = "<div style='position: relative'></div>";
-    this._eventLayer.appendChild(iconStackDiv);
-
-    var self = this;
-    var onMouseOver = function(domEvt) {
-        try {
-            var n = parseInt(this.getAttribute("index"));
-            var childNodes = iconStackDiv.firstChild.childNodes;
-            for (var i = 0; i < childNodes.length; i++) {
-                var child = childNodes[i];
-                if (i == n) {
-                    child.style.zIndex = childNodes.length;
-                } else {
-                    child.style.zIndex = childNodes.length - i;
-                }
-            }
-        } catch (e) {
-        }
-    };
-    var paintEvent = function(index) {
-        var record = records[index];
-        var evt = events[index];
-        var tooltip = evt.getProperty("tooltip") || evt.getText();
-
-        var labelElmtData = self._paintEventLabel(
-            { tooltip: tooltip },
-            { text: record.text },
-            anchorPixel + record.labelLeft,
-            verticalPixelOffset + record.top,
-            record.labelSize.width,
-            record.labelSize.height,
-            theme
-        );
-        labelElmtData.elmt.setAttribute("index", index);
-        labelElmtData.elmt.onmouseover = onMouseOver;
-
-        var img = SimileAjax.Graphics.createTranslucentImage(record.iconData.url);
-        var iconDiv = self._timeline.getDocument().createElement("div");
-        iconDiv.className = 'timeline-event-icon' + ("className" in record.iconData ? (" " + record.iconData.className) : "");
-        iconDiv.style.left = record.iconLeft + "px";
-        iconDiv.style.top = record.top + "px";
-        iconDiv.style.zIndex = (records.length - index);
-        iconDiv.appendChild(img);
-        iconDiv.setAttribute("index", index);
-        iconDiv.onmouseover = onMouseOver;
-
-        iconStackDiv.firstChild.appendChild(iconDiv);
-
-        var clickHandler = function(elmt, domEvt, target) {
-            return self._onClickInstantEvent(labelElmtData.elmt, domEvt, evt);
-        };
-
-        SimileAjax.DOM.registerEvent(iconDiv, "mousedown", clickHandler);
-        SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
-        self._eventIdToElmt[evt.getID()] = iconDiv;
-    };
-    for (var i = 0; i < records.length; i++) {
-        paintEvent(i);
-    }
-
-    if (showMoreMessage) {
-        var moreEvents = events.slice(limit);
-        var moreMessageLabelElmtData = this._paintEventLabel(
-            { tooltip: moreMessage },
-            { text: moreMessage },
-            anchorPixel + moreMessageLabelLeft,
-            verticalPixelOffset + moreMessageLabelTop,
-            moreMessageLabelSize.width,
-            moreMessageLabelSize.height,
-            theme
-        );
-
-        var moreMessageClickHandler = function(elmt, domEvt, target) {
-            return self._onClickMultiplePreciseInstantEvent(moreMessageLabelElmtData.elmt, domEvt, moreEvents);
-        };
-        SimileAjax.DOM.registerEvent(moreMessageLabelElmtData.elmt, "mousedown", moreMessageClickHandler);
-
-        for (var i = 0; i < moreEvents.length; i++) {
-            this._eventIdToElmt[moreEvents[i].getID()] = moreMessageLabelElmtData.elmt;
-        }
-    }
-    //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-};
-
-Timeline.CompactEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
-    var commonData = {
-        tooltip: evt.getProperty("tooltip") || evt.getText()
-    };
-
-    var tapeData = {
-        start:          evt.getStart(),
-        end:            evt.getEnd(),
-        latestStart:    evt.getLatestStart(),
-        earliestEnd:    evt.getEarliestEnd(),
-        isInstant:      true
-    };
-
-    var iconData = {
-        url: evt.getIcon()
-    };
-    if (iconData.url == null) {
-        iconData = null;
-    } else {
-        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
-        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
-    }
-
-    var labelData = {
-        text:       evt.getText(),
-        color:      evt.getTextColor() || evt.getColor(),
-        className:  evt.getClassName()
-    };
-
-    var result = this.paintTapeIconLabel(
-        evt.getStart(),
-        commonData,
-        tapeData, // no tape data
-        iconData,
-        labelData,
-        metrics,
-        theme,
-        highlightIndex
-    );
-
-    var self = this;
-    var clickHandler = iconData != null ?
-        function(elmt, domEvt, target) {
-            return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
-        } :
-        function(elmt, domEvt, target) {
-            return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
-        };
-
-    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(result.impreciseTapeElmtData.elmt, "mousedown", clickHandler);
-
-    if (iconData != null) {
-        SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
-        this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
-    } else {
-        this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
-    }
-};
-
-Timeline.CompactEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    var commonData = {
-        tooltip: evt.getProperty("tooltip") || evt.getText()
-    };
-
-    var tapeData = {
-        start:          evt.getStart(),
-        end:            evt.getEnd(),
-        isInstant:      false
-    };
-
-    var iconData = {
-        url: evt.getIcon()
-    };
-    if (iconData.url == null) {
-        iconData = null;
-    } else {
-        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
-        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
-    }
-
-    var labelData = {
-        text:       evt.getText(),
-        color:      evt.getTextColor() || evt.getColor(),
-        className:  evt.getClassName()
-    };
-
-    var result = this.paintTapeIconLabel(
-        evt.getLatestStart(),
-        commonData,
-        tapeData, // no tape data
-        iconData,
-        labelData,
-        metrics,
-        theme,
-        highlightIndex
-    );
-
-    var self = this;
-    var clickHandler = iconData != null ?
-        function(elmt, domEvt, target) {
-            return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
-        } :
-        function(elmt, domEvt, target) {
-            return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
-        };
-
-    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
-
-    if (iconData != null) {
-        SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
-        this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
-    } else {
-        this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
-    }
-};
-
-Timeline.CompactEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
-    var commonData = {
-        tooltip: evt.getProperty("tooltip") || evt.getText()
-    };
-
-    var tapeData = {
-        start:          evt.getStart(),
-        end:            evt.getEnd(),
-        latestStart:    evt.getLatestStart(),
-        earliestEnd:    evt.getEarliestEnd(),
-        isInstant:      false
-    };
-
-    var iconData = {
-        url: evt.getIcon()
-    };
-    if (iconData.url == null) {
-        iconData = null;
-    } else {
-        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
-        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
-    }
-
-    var labelData = {
-        text:       evt.getText(),
-        color:      evt.getTextColor() || evt.getColor(),
-        className:  evt.getClassName()
-    };
-
-    var result = this.paintTapeIconLabel(
-        evt.getLatestStart(),
-        commonData,
-        tapeData, // no tape data
-        iconData,
-        labelData,
-        metrics,
-        theme,
-        highlightIndex
-    );
-
-    var self = this;
-    var clickHandler = iconData != null ?
-        function(elmt, domEvt, target) {
-            return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
-        } :
-        function(elmt, domEvt, target) {
-            return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
-        };
-
-    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
-    SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
-
-    if (iconData != null) {
-        SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
-        this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
-    } else {
-        this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
-    }
-};
-
-Timeline.CompactEventPainter.prototype.paintTapeIconLabel = function(
-    anchorDate,
-    commonData,
-    tapeData,
-    iconData,
-    labelData,
-    metrics,
-    theme,
-    highlightIndex
-) {
-    var band = this._band;
-    var getPixelOffset = function(date) {
-        return Math.round(band.dateToPixelOffset(date));
-    };
-
-    var anchorPixel = getPixelOffset(anchorDate);
-    var newTracks = [];
-
-    var tapeHeightOccupied = 0;         // how many pixels (vertically) the tape occupies, including bottom margin
-    var tapeTrackCount = 0;             // how many tracks the tape takes up, usually just 1
-    var tapeLastTrackExtraSpace = 0;    // on the last track that the tape occupies, how many pixels are left (for icon and label to occupy as well)
-    if (tapeData != null) {
-        tapeHeightOccupied = metrics.tapeHeight + metrics.tapeBottomMargin;
-        tapeTrackCount = Math.ceil(metrics.tapeHeight / metrics.trackHeight);
-
-        var tapeEndPixelOffset = getPixelOffset(tapeData.end) - anchorPixel;
-        var tapeStartPixelOffset = getPixelOffset(tapeData.start) - anchorPixel;
-
-        for (var t = 0; t < tapeTrackCount; t++) {
-            newTracks.push({ start: tapeStartPixelOffset, end: tapeEndPixelOffset });
-        }
-
-        tapeLastTrackExtraSpace = metrics.trackHeight - (tapeHeightOccupied % metrics.tapeHeight);
-    }
-
-    var iconStartPixelOffset = 0;        // where the icon starts compared to the anchor pixel;
-                                         // this can be negative if the icon is center-aligned around the anchor
-    var iconHorizontalSpaceOccupied = 0; // how many pixels the icon take up from the anchor pixel,
-                                         // including the gap between the icon and the label
-    if (iconData != null) {
-        if ("iconAlign" in iconData && iconData.iconAlign == "center") {
-            iconStartPixelOffset = -Math.floor(iconData.width / 2);
-        }
-        iconHorizontalSpaceOccupied = iconStartPixelOffset + iconData.width + metrics.iconLabelGap;
-
-        if (tapeTrackCount > 0) {
-            newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, iconHorizontalSpaceOccupied);
-        }
-
-        var iconHeight = iconData.height + metrics.iconBottomMargin + tapeLastTrackExtraSpace;
-        while (iconHeight > 0) {
-            newTracks.push({ start: iconStartPixelOffset, end: iconHorizontalSpaceOccupied });
-            iconHeight -= metrics.trackHeight;
-        }
-    }
-
-    var text = labelData.text;
-    var labelSize = this._frc.computeSize(text);
-    var labelHeight = labelSize.height + metrics.labelBottomMargin + tapeLastTrackExtraSpace;
-    var labelEndPixelOffset = iconHorizontalSpaceOccupied + labelSize.width + metrics.labelRightMargin;
-    if (tapeTrackCount > 0) {
-        newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, labelEndPixelOffset);
-    }
-    for (var i = 0; labelHeight > 0; i++) {
-        if (tapeTrackCount + i < newTracks.length) {
-            var track = newTracks[tapeTrackCount + i];
-            track.end = labelEndPixelOffset;
-        } else {
-            newTracks.push({ start: 0, end: labelEndPixelOffset });
-        }
-        labelHeight -= metrics.trackHeight;
-    }
-
-    /*
-     *  Try to fit the new track on top of the existing tracks, then
-     *  render the various elements.
-     */
-    var firstTrack = this._fitTracks(anchorPixel, newTracks);
-    var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
-    var result = {};
-
-    result.labelElmtData = this._paintEventLabel(
-        commonData,
-        labelData,
-        anchorPixel + iconHorizontalSpaceOccupied,
-        verticalPixelOffset + tapeHeightOccupied,
-        labelSize.width,
-        labelSize.height,
-        theme
-    );
-
-    if (tapeData != null) {
-        if ("latestStart" in tapeData || "earliestEnd" in tapeData) {
-            result.impreciseTapeElmtData = this._paintEventTape(
-                commonData,
-                tapeData,
-                metrics.tapeHeight,
-                verticalPixelOffset,
-                getPixelOffset(tapeData.start),
-                getPixelOffset(tapeData.end),
-                theme.event.duration.impreciseColor,
-                theme.event.duration.impreciseOpacity,
-                metrics,
-                theme
-            );
-        }
-        if (!tapeData.isInstant && "start" in tapeData && "end" in tapeData) {
-            result.tapeElmtData = this._paintEventTape(
-                commonData,
-                tapeData,
-                metrics.tapeHeight,
-                verticalPixelOffset,
-                anchorPixel,
-                getPixelOffset("earliestEnd" in tapeData ? tapeData.earliestEnd : tapeData.end),
-                tapeData.color,
-                100,
-                metrics,
-                theme
-            );
-        }
-    }
-
-    if (iconData != null) {
-        result.iconElmtData = this._paintEventIcon(
-            commonData,
-            iconData,
-            verticalPixelOffset + tapeHeightOccupied,
-            anchorPixel + iconStartPixelOffset,
-            metrics,
-            theme
-        );
-    }
-    //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-
-    return result;
-};
-
-Timeline.CompactEventPainter.prototype._fitTracks = function(anchorPixel, newTracks) {
-    var firstTrack;
-    for (firstTrack = 0; firstTrack < this._tracks.length; firstTrack++) {
-        var fit = true;
-        for (var j = 0; j < newTracks.length && (firstTrack + j) < this._tracks.length; j++) {
-            var existingTrack = this._tracks[firstTrack + j];
-            var newTrack = newTracks[j];
-            if (anchorPixel + newTrack.start < existingTrack) {
-                fit = false;
-                break;
-            }
-        }
-
-        if (fit) {
-            break;
-        }
-    }
-    for (var i = 0; i < newTracks.length; i++) {
-        this._tracks[firstTrack + i] = anchorPixel + newTracks[i].end;
-    }
-
-    return firstTrack;
-};
-
-
-Timeline.CompactEventPainter.prototype._paintEventIcon = function(commonData, iconData, top, left, metrics, theme) {
-    var img = SimileAjax.Graphics.createTranslucentImage(iconData.url);
-    var iconDiv = this._timeline.getDocument().createElement("div");
-    iconDiv.className = 'timeline-event-icon' + ("className" in iconData ? (" " + iconData.className) : "");
-    iconDiv.style.left = left + "px";
-    iconDiv.style.top = top + "px";
-    iconDiv.appendChild(img);
-
-    if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
-        iconDiv.title = commonData.tooltip;
-    }
-
-    this._eventLayer.appendChild(iconDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  metrics.iconWidth,
-        height: metrics.iconHeight,
-        elmt:   iconDiv
-    };
-};
-
-Timeline.CompactEventPainter.prototype._paintEventLabel = function(commonData, labelData, left, top, width, height, theme) {
-    var doc = this._timeline.getDocument();
-
-    var labelDiv = doc.createElement("div");
-    labelDiv.className = 'timeline-event-label';
-
-    labelDiv.style.left = left + "px";
-    labelDiv.style.width = (width + 1) + "px";
-    labelDiv.style.top = top + "px";
-    labelDiv.innerHTML = labelData.text;
-
-    if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
-        labelDiv.title = commonData.tooltip;
-    }
-    if ("color" in labelData && typeof labelData.color == "string") {
-        labelDiv.style.color = labelData.color;
-    }
-    if ("className" in labelData && typeof labelData.className == "string") {
-        labelDiv.className += ' ' + labelData.className;
-    }
-
-    this._eventLayer.appendChild(labelDiv);
-
-    return {
-        left:   left,
-        top:    top,
-        width:  width,
-        height: height,
-        elmt:   labelDiv
-    };
-};
-
-Timeline.CompactEventPainter.prototype._paintEventTape = function(
-    commonData, tapeData, height, top, startPixel, endPixel, color, opacity, metrics, theme) {
-
-    var width = endPixel - startPixel;
-
-    var tapeDiv = this._timeline.getDocument().createElement("div");
-    tapeDiv.className = "timeline-event-tape"
-
-    tapeDiv.style.left = startPixel + "px";
-    tapeDiv.style.top = top + "px";
-    tapeDiv.style.width = width + "px";
-    tapeDiv.style.height = height + "px";
-
-    if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
-        tapeDiv.title = commonData.tooltip;
-    }
-    if (color != null && typeof tapeData.color == "string") {
-        tapeDiv.style.backgroundColor = color;
-    }
-
-    if ("backgroundImage" in tapeData && typeof tapeData.backgroundImage == "string") {
-        tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
-        tapeDiv.style.backgroundRepeat =
-            ("backgroundRepeat" in tapeData && typeof tapeData.backgroundRepeat == "string")
-                ? tapeData.backgroundRepeat : 'repeat';
-    }
-
-    SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
-    if ("className" in tapeData && typeof tapeData.className == "string") {
-        tapeDiv.className += ' ' + tapeData.className;
-    }
-
-    this._eventLayer.appendChild(tapeDiv);
-
-    return {
-        left:   startPixel,
-        top:    top,
-        width:  width,
-        height: height,
-        elmt:   tapeDiv
-    };
-}
-
-Timeline.CompactEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
-    if (highlightIndex >= 0) {
-        var doc = this._timeline.getDocument();
-        var eventTheme = theme.event;
-
-        var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
-
-        var div = doc.createElement("div");
-        div.style.position = "absolute";
-        div.style.overflow = "hidden";
-        div.style.left =    (dimensions.left - 2) + "px";
-        div.style.width =   (dimensions.width + 4) + "px";
-        div.style.top =     (dimensions.top - 2) + "px";
-        div.style.height =  (dimensions.height + 4) + "px";
-//        div.style.background = color;
-
-        this._highlightLayer.appendChild(div);
-    }
-};
-
-Timeline.CompactEventPainter.prototype._onClickMultiplePreciseInstantEvent = function(icon, domEvt, events) {
-    var c = SimileAjax.DOM.getPageCoordinates(icon);
-    this._showBubble(
-        c.left + Math.ceil(icon.offsetWidth / 2),
-        c.top + Math.ceil(icon.offsetHeight / 2),
-        events
-    );
-
-    var ids = [];
-    for (var i = 0; i < events.length; i++) {
-        ids.push(events[i].getID());
-    }
-    this._fireOnSelect(ids);
-
-    domEvt.cancelBubble = true;
-    SimileAjax.DOM.cancelEvent(domEvt);
-
-    return false;
-};
-
-Timeline.CompactEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
-    var c = SimileAjax.DOM.getPageCoordinates(icon);
-    this._showBubble(
-        c.left + Math.ceil(icon.offsetWidth / 2),
-        c.top + Math.ceil(icon.offsetHeight / 2),
-        [evt]
-    );
-    this._fireOnSelect([evt.getID()]);
-
-    domEvt.cancelBubble = true;
-    SimileAjax.DOM.cancelEvent(domEvt);
-    return false;
-};
-
-Timeline.CompactEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
-    if ("pageX" in domEvt) {
-        var x = domEvt.pageX;
-        var y = domEvt.pageY;
-    } else {
-        var c = SimileAjax.DOM.getPageCoordinates(target);
-        var x = domEvt.offsetX + c.left;
-        var y = domEvt.offsetY + c.top;
-    }
-    this._showBubble(x, y, [evt]);
-    this._fireOnSelect([evt.getID()]);
-
-    domEvt.cancelBubble = true;
-    SimileAjax.DOM.cancelEvent(domEvt);
-    return false;
-};
-
-Timeline.CompactEventPainter.prototype.showBubble = function(evt) {
-    var elmt = this._eventIdToElmt[evt.getID()];
-    if (elmt) {
-        var c = SimileAjax.DOM.getPageCoordinates(elmt);
-        this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, [evt]);
-    }
-};
-
-Timeline.CompactEventPainter.prototype._showBubble = function(x, y, evts) {
-    var div = document.createElement("div");
-
-    evts = ("fillInfoBubble" in evts) ? [evts] : evts;
-    for (var i = 0; i < evts.length; i++) {
-        var div2 = document.createElement("div");
-        div.appendChild(div2);
-
-        evts[i].fillInfoBubble(div2, this._params.theme, this._band.getLabeller());
-    }
-
-    SimileAjax.WindowManager.cancelPopups();
-    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, this._params.theme.event.bubble.width);
-};
-
-Timeline.CompactEventPainter.prototype._fireOnSelect = function(eventIDs) {
-    for (var i = 0; i < this._onSelectListeners.length; i++) {
-        this._onSelectListeners[i](eventIDs);
-    }
-};
-/*
- *  Span Highlight Decorator
- *
- */
-
-Timeline.SpanHighlightDecorator = function(params) {
-    // When evaluating params, test against null. Not "p in params". Testing against
-    // null enables caller to explicitly request the default. Testing against "in" means
-    // that the param has to be ommitted to get the default.
-    this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
-    this._startDate = (typeof params.startDate == "string") ?
-        this._unit.parseFromObject(params.startDate) : params.startDate;
-    this._endDate = (typeof params.endDate == "string") ?
-        this._unit.parseFromObject(params.endDate) : params.endDate;
-    this._startLabel = params.startLabel != null ? params.startLabel : ""; // not null!
-    this._endLabel   = params.endLabel   != null ? params.endLabel   : ""; // not null!
-    this._color = params.color;
-    this._cssClass = params.cssClass != null ? params.cssClass : null;
-    this._opacity = params.opacity != null ? params.opacity : 100;
-         // Default z is 10, behind everything but background grid.
-         // If inFront, then place just behind events, in front of everything else
-    this._zIndex = (params.inFront != null && params.inFront) ? 113 : 10;
-};
-
-Timeline.SpanHighlightDecorator.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-
-    this._layerDiv = null;
-};
-
-Timeline.SpanHighlightDecorator.prototype.paint = function() {
-    if (this._layerDiv != null) {
-        this._band.removeLayerDiv(this._layerDiv);
-    }
-    this._layerDiv = this._band.createLayerDiv(this._zIndex);
-    this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
-    this._layerDiv.style.display = "none";
-
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    if (this._unit.compare(this._startDate, maxDate) < 0 &&
-        this._unit.compare(this._endDate, minDate) > 0) {
-
-        minDate = this._unit.later(minDate, this._startDate);
-        maxDate = this._unit.earlier(maxDate, this._endDate);
-
-        var minPixel = this._band.dateToPixelOffset(minDate);
-        var maxPixel = this._band.dateToPixelOffset(maxDate);
-
-        var doc = this._timeline.getDocument();
-
-        var createTable = function() {
-            var table = doc.createElement("table");
-            table.insertRow(0).insertCell(0);
-            return table;
-        };
-
-        var div = doc.createElement("div");
-        div.className='timeline-highlight-decorator'
-        if(this._cssClass) {
-        	  div.className += ' ' + this._cssClass;
-        }
-        if(this._color != null) {
-        	  div.style.backgroundColor = this._color;
-        }
-        if (this._opacity < 100) {
-            SimileAjax.Graphics.setOpacity(div, this._opacity);
-        }
-        this._layerDiv.appendChild(div);
-
-        var tableStartLabel = createTable();
-        tableStartLabel.className = 'timeline-highlight-label timeline-highlight-label-start'
-        var tdStart =  tableStartLabel.rows[0].cells[0]
-        tdStart.innerHTML = this._startLabel;
-        if (this._cssClass) {
-        	  tdStart.className = 'label_' + this._cssClass;
-        }
-        this._layerDiv.appendChild(tableStartLabel);
-
-        var tableEndLabel = createTable();
-        tableEndLabel.className = 'timeline-highlight-label timeline-highlight-label-end'
-        var tdEnd = tableEndLabel.rows[0].cells[0]
-        tdEnd.innerHTML = this._endLabel;
-        if (this._cssClass) {
-        	   tdEnd.className = 'label_' + this._cssClass;
-        }
-        this._layerDiv.appendChild(tableEndLabel);
-
-        if (this._timeline.isHorizontal()){
-            div.style.left = minPixel + "px";
-            div.style.width = (maxPixel - minPixel) + "px";
-
-            tableStartLabel.style.right = (this._band.getTotalViewLength() - minPixel) + "px";
-            tableStartLabel.style.width = (this._startLabel.length) + "em";
-
-            tableEndLabel.style.left = maxPixel + "px";
-            tableEndLabel.style.width = (this._endLabel.length) + "em";
-
-        } else {
-            div.style.top = minPixel + "px";
-            div.style.height = (maxPixel - minPixel) + "px";
-
-            tableStartLabel.style.bottom = minPixel + "px";
-            tableStartLabel.style.height = "1.5px";
-
-            tableEndLabel.style.top = maxPixel + "px";
-            tableEndLabel.style.height = "1.5px";
-        }
-    }
-    this._layerDiv.style.display = "block";
-};
-
-Timeline.SpanHighlightDecorator.prototype.softPaint = function() {
-};
-
-/*
- *  Point Highlight Decorator
- *
- */
-
-Timeline.PointHighlightDecorator = function(params) {
-    this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
-    this._date = (typeof params.date == "string") ?
-        this._unit.parseFromObject(params.date) : params.date;
-    this._width = params.width != null ? params.width : 10;
-      // Since the width is used to calculate placements (see minPixel, below), we
-      // specify width here, not in css.
-    this._color = params.color;
-    this._cssClass = params.cssClass != null ? params.cssClass : '';
-    this._opacity = params.opacity != null ? params.opacity : 100;
-};
-
-Timeline.PointHighlightDecorator.prototype.initialize = function(band, timeline) {
-    this._band = band;
-    this._timeline = timeline;
-    this._layerDiv = null;
-};
-
-Timeline.PointHighlightDecorator.prototype.paint = function() {
-    if (this._layerDiv != null) {
-        this._band.removeLayerDiv(this._layerDiv);
-    }
-    this._layerDiv = this._band.createLayerDiv(10);
-    this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
-    this._layerDiv.style.display = "none";
-
-    var minDate = this._band.getMinDate();
-    var maxDate = this._band.getMaxDate();
-
-    if (this._unit.compare(this._date, maxDate) < 0 &&
-        this._unit.compare(this._date, minDate) > 0) {
-
-        var pixel = this._band.dateToPixelOffset(this._date);
-        var minPixel = pixel - Math.round(this._width / 2);
-
-        var doc = this._timeline.getDocument();
-
-        var div = doc.createElement("div");
-        div.className='timeline-highlight-point-decorator';
-        div.className += ' ' + this._cssClass;
-
-        if(this._color != null) {
-        	  div.style.backgroundColor = this._color;
-        }
-        if (this._opacity < 100) {
-            SimileAjax.Graphics.setOpacity(div, this._opacity);
-        }
-        this._layerDiv.appendChild(div);
-
-        if (this._timeline.isHorizontal()) {
-            div.style.left = minPixel + "px";
-            div.style.width = this._width;
-        } else {
-            div.style.top = minPixel + "px";
-            div.style.height = this._width;
-        }
-    }
-    this._layerDiv.style.display = "block";
-};
-
-Timeline.PointHighlightDecorator.prototype.softPaint = function() {
-};
-/*
- *  Default Unit
- *
- */
-
-Timeline.NativeDateUnit = new Object();
-
-Timeline.NativeDateUnit.createLabeller = function(locale, timeZone) {
-    return new Timeline.GregorianDateLabeller(locale, timeZone);
-};
-
-Timeline.NativeDateUnit.makeDefaultValue = function() {
-    return new Date();
-};
-
-Timeline.NativeDateUnit.cloneValue = function(v) {
-    return new Date(v.getTime());
-};
-
-Timeline.NativeDateUnit.getParser = function(format) {
-    if (typeof format == "string") {
-        format = format.toLowerCase();
-    }
-    return (format == "iso8601" || format == "iso 8601") ?
-        Timeline.DateTime.parseIso8601DateTime : 
-        Timeline.DateTime.parseGregorianDateTime;
-};
-
-Timeline.NativeDateUnit.parseFromObject = function(o) {
-    return Timeline.DateTime.parseGregorianDateTime(o);
-};
-
-Timeline.NativeDateUnit.toNumber = function(v) {
-    return v.getTime();
-};
-
-Timeline.NativeDateUnit.fromNumber = function(n) {
-    return new Date(n);
-};
-
-Timeline.NativeDateUnit.compare = function(v1, v2) {
-    var n1, n2;
-    if (typeof v1 == "object") {
-        n1 = v1.getTime();
-    } else {
-        n1 = Number(v1);
-    }
-    if (typeof v2 == "object") {
-        n2 = v2.getTime();
-    } else {
-        n2 = Number(v2);
-    }
-
-    return n1 - n2;
-};
-
-Timeline.NativeDateUnit.earlier = function(v1, v2) {
-    return Timeline.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
-};
-
-Timeline.NativeDateUnit.later = function(v1, v2) {
-    return Timeline.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
-};
-
-Timeline.NativeDateUnit.change = function(v, n) {
-    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"] = [
-    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
-];
-
-Timeline.GregorianDateLabeller.dayNames["en"] = [
-    "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
-];
--- a/web/data/cubicweb.timeline-ext.js	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-/**
- *  :organization: Logilab
- *  :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
- * 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;
-            };
-        }
-        format = format.toLowerCase();
-    }
-    if (format == "iso8601" || format == "iso 8601") {
-        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 = [];
-
-    this._filterMatcher = null;
-    this._highlightMatcher = null;
-    this._frc = null;
-
-    this._eventIdToElmt = {};
-};
-
-Timeline.CubicWebEventPainter.prototype = new Timeline.OriginalEventPainter();
-
-Timeline.CubicWebEventPainter.prototype._paintEventLabel = function(
-evt, text, left, top, width, height, theme) {
-    var doc = this._timeline.getDocument();
-
-    var labelDiv = doc.createElement("div");
-    labelDiv.className = 'timeline-event-label';
-
-    labelDiv.style.left = left + "px";
-    labelDiv.style.width = width + "px";
-    labelDiv.style.top = top + "px";
-
-    if (evt._obj.onclick) {
-        labelDiv.appendChild(A({
-            'href': evt._obj.onclick
-        },
-        text));
-    } else if (evt._obj.image) {
-        labelDiv.appendChild(IMG({
-            src: evt._obj.image,
-            width: '30px',
-            height: '30px'
-        }));
-    } else {
-        labelDiv.innerHTML = text;
-    }
-
-    if (evt._title != null) labelDiv.title = evt._title;
-
-    var color = evt.getTextColor();
-    if (color == null) {
-        color = evt.getColor();
-    }
-    if (color != null) {
-        labelDiv.style.color = color;
-    }
-    var classname = evt.getClassName();
-    if (classname) labelDiv.className += ' ' + classname;
-
-    this._eventLayer.appendChild(labelDiv);
-
-    return {
-        left: left,
-        top: top,
-        width: width,
-        height: height,
-        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');
-    }
-};
-
--- a/web/data/cubicweb.widgets.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/data/cubicweb.widgets.js	Fri Oct 09 17:52:14 2015 +0200
@@ -347,62 +347,6 @@
 });
 
 /**
- * .. 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);
-    },
-
-    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);
-    },
-
-    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);
-    }
-});
-
-/**
  * .. function:: toggleTree(event)
  *
  * called when the use clicks on a tree node
@@ -428,41 +372,6 @@
     }
 }
 
-/**
- * .. 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)];
-        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) {
--- a/web/data/timeline-bundle.css	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,401 +0,0 @@
-div.simileAjax-bubble-container {
-    margin:     0px;
-    padding:    0px;
-    border:     none;
-    position:   absolute;
-    z-index:    1000;
-}
-
-div.simileAjax-bubble-innerContainer {
-    margin:     0px;
-    padding:    0px;
-    border:     none;
-    position:   relative;
-    width:      100%;
-    height:     100%;
-    overflow:   visible;
-}
-
-div.simileAjax-bubble-contentContainer {
-    margin:     0px;
-    padding:    0px;
-    border:     none;
-    position:   absolute;
-    left:       0px;
-    top:        0px;
-    width:      100%;
-    height:     100%;
-    overflow:   auto;
-    background: white;
-}
-
-div.simileAjax-bubble-border-left {
-    position:   absolute;
-    left:       -50px;
-    top:        0px;
-    width:      50px;
-    height:     100%;
-}
-div.simileAjax-bubble-border-left-pngTranslucent {
-    background: url(../images/bubble-left.png) top right repeat-y;
-}
-
-div.simileAjax-bubble-border-right {
-    position:   absolute;
-    right:      -50px;
-    top:        0px;
-    width:      50px;
-    height:     100%;
-}
-.simileAjax-bubble-border-right-pngTranslucent {
-    background: url(../images/bubble-right.png) top left repeat-y;
-}
-
-div.simileAjax-bubble-border-top {
-    position:   absolute;
-    top:        -50px;
-    left:       0px;
-    width:      100%;
-    height:     50px;
-}
-.simileAjax-bubble-border-top-pngTranslucent {
-    background: url(../images/bubble-top.png) bottom left repeat-x;
-}
-
-div.simileAjax-bubble-border-bottom {
-    position:   absolute;
-    bottom:     -50px;
-    left:       0px;
-    width:      100%;
-    height:     50px;
-}
-.simileAjax-bubble-border-bottom-pngTranslucent {
-    background: url(../images/bubble-bottom.png) top left repeat-x;
-}
-
-div.simileAjax-bubble-border-top-left {
-    position:   absolute;
-    top:        -50px;
-    left:       -50px;
-    width:      50px;
-    height:     50px;
-}
-.simileAjax-bubble-border-top-left-pngTranslucent {
-    background: url(../images/bubble-top-left.png) bottom right no-repeat;
-}
-
-div.simileAjax-bubble-border-top-right {
-    position:   absolute;
-    top:        -50px;
-    right:      -50px;
-    width:      50px;
-    height:     50px;
-}
-.simileAjax-bubble-border-top-right-pngTranslucent {
-    background: url(../images/bubble-top-right.png) bottom left no-repeat;
-}
-
-div.simileAjax-bubble-border-bottom-left {
-    position:   absolute;
-    bottom:     -50px;
-    left:       -50px;
-    width:      50px;
-    height:     50px;
-}
-.simileAjax-bubble-border-bottom-left-pngTranslucent {
-    background: url(../images/bubble-bottom-left.png) top right no-repeat;
-}
-
-div.simileAjax-bubble-border-bottom-right {
-    position:   absolute;
-    bottom:     -50px;
-    right:      -50px;
-    width:      50px;
-    height:     50px;
-}
-.simileAjax-bubble-border-bottom-right-pngTranslucent {
-    background: url(../images/bubble-bottom-right.png) top left no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-left {
-    position:   absolute;
-    left:       -100px;
-    width:      100px;
-    height:     49px;
-}
-.simileAjax-bubble-arrow-point-left-pngTranslucent {
-    background: url(../images/bubble-arrow-point-left.png) center right no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-right {
-    position:   absolute;
-    right:      -100px;
-    width:      100px;
-    height:     49px;
-}
-.simileAjax-bubble-arrow-point-right-pngTranslucent {
-    background: url(../images/bubble-arrow-point-right.png) center left no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-up {
-    position:   absolute;
-    top:        -100px;
-    width:      49px;
-    height:     100px;
-}
-.simileAjax-bubble-arrow-point-up-pngTranslucent {
-    background: url(../images/bubble-arrow-point-up.png) bottom center no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-down {
-    position:   absolute;
-    bottom:     -100px;
-    width:      49px;
-    height:     100px;
-}
-.simileAjax-bubble-arrow-point-down-pngTranslucent {
-    background: url(../images/bubble-arrow-point-down.png) bottom center no-repeat;
-}
-
-
-div.simileAjax-bubble-close {
-    position:   absolute;
-    right:      -10px;
-    top:        -12px;
-    width:      16px;
-    height:     16px;
-    cursor:     pointer;
-}
-.simileAjax-bubble-close-pngTranslucent {
-    background: url(../images/close-button.png) no-repeat;
-}
-.timeline-container {
-    position: relative;
-    overflow: hidden;
-}
-
-.timeline-copyright {
-    position: absolute;
-    bottom: 0px;
-    left: 0px;
-    z-index: 1000;
-    cursor: pointer;
-}
-
-.timeline-message-container {
-    position:   absolute;
-    top:        30%;
-    left:       35%;
-    right:      35%;
-    z-index:    1000;
-    display:    none;
-}
-.timeline-message {
-    font-size:      120%;
-    font-weight:    bold;
-    text-align:     center;
-}
-.timeline-message img {
-    vertical-align: middle;
-}
-
-.timeline-band {
-    position:   absolute;
-    background: #eee;
-    z-index:    10;
-}
-
-.timeline-band-inner {
-    position: relative;
-    width: 100%;
-    height: 100%;
-}
-
-.timeline-band-input {
-    position:   absolute;
-    width:      1em;
-    height:     1em;
-    overflow:   hidden;
-    z-index:    0;
-}
-.timeline-band-input input{
-    width:      0;
-}
-
-.timeline-band-layer {
-    position:   absolute;
-    width:      100%;
-    height:     100%;
-}
-
-.timeline-band-layer-inner {
-    position:   relative;
-    width:      100%;
-    height:     100%;
-}
-
-
-
-/*------------------- Horizontal / Vertical lines ----------------*/
-
-/* style for ethers */
-.timeline-ether-lines{border-color:#666; border-style:dotted; position:absolute;}
-.timeline-horizontal .timeline-ether-lines{border-width:0 0 0 1px; height:100%; top: 0; width: 1px;}
-.timeline-vertical .timeline-ether-lines{border-width:1px 0 0; height:1px; left: 0; width: 100%;}
-
-
-
-/*---------------- Weekends ---------------------------*/
-.timeline-ether-weekends{
-	position:absolute;
-	background-color:#FFFFE0;
-}
-
-.timeline-vertical .timeline-ether-weekends{left:0;width:100%;}
-.timeline-horizontal .timeline-ether-weekends{top:0; height:100%;}
-
-
-/*-------------------------- HIGHLIGHT DECORATORS -------------------*/
-/* Used for decorators, not used for Timeline Highlight              */
-.timeline-highlight-decorator,
-.timeline-highlight-point-decorator{
-	position:absolute;
-	overflow:hidden;
-}
-
-/* Width of horizontal decorators and Height of vertical decorators is
-   set in the decorator function params */
-.timeline-horizontal .timeline-highlight-point-decorator,
-.timeline-horizontal .timeline-highlight-decorator{
-	top:0;
-  height:100%;
-}
-
-.timeline-vertical .timeline-highlight-point-decorator,
-.timeline-vertical .timeline-highlight-decorator{
-	width:100%;
-	left:0;
-}
-
-.timeline-highlight-decorator{background-color:#FFC080;}
-.timeline-highlight-point-decorator{background-color:#ff5;}
-
-
-/*---------------------------- LABELS -------------------------*/
-.timeline-highlight-label {
-  position:absolute; overflow:hidden; font-size:200%;
-  font-weight:bold; color:#999; }
-
-
-/*---------------- VERTICAL LABEL -------------------*/
-.timeline-horizontal .timeline-highlight-label {top:0; height:100%;}
-.timeline-horizontal .timeline-highlight-label td {vertical-align:middle;}
-.timeline-horizontal .timeline-highlight-label-start {text-align:right;}
-.timeline-horizontal .timeline-highlight-label-end {text-align:left;}
-
-
-/*---------------- HORIZONTAL LABEL -------------------*/
-.timeline-vertical .timeline-highlight-label {left:0;width:100%;}
-.timeline-vertical .timeline-highlight-label td {vertical-align:top;}
-.timeline-vertical .timeline-highlight-label-start {text-align:center;}
-.timeline-vertical .timeline-highlight-label-end {text-align:center;}
-
-
-/*-------------------------------- DATE LABELS --------------------------------*/
-.timeline-date-label {
-  position: absolute;
-  border: solid #aaa;
-  color: #aaa;
-  width: 5em;
-  height: 1.5em;}
-.timeline-date-label-em {color: #000;}
-
-/* horizontal */
-.timeline-horizontal .timeline-date-label{padding-left:2px;}
-.timeline-horizontal .timeline-date-label{border-width:0 0 0 1px;}
-.timeline-horizontal .timeline-date-label-em{height:2em}
-
-/* vertical */
-.timeline-vertical .timeline-date-label{padding-top:2px;}
-.timeline-vertical .timeline-date-label{border-width:1px 0 0;}
-.timeline-vertical .timeline-date-label-em{width:7em}
-
-
-/*------------------------------- Ether.highlight -------------------------*/
-.timeline-ether-highlight{position:absolute; background-color:#fff;}
-.timeline-horizontal .timeline-ether-highlight{top:2px;}
-.timeline-vertical .timeline-ether-highlight{left:2px;}
-
-
-/*------------------------------ EVENTS ------------------------------------*/
-.timeline-event-icon, .timeline-event-label,.timeline-event-tape{
-	position:absolute;
-	cursor:pointer;
-}
-
-.timeline-event-tape,
-.timeline-small-event-tape,
-.timeline-small-event-icon{
-	background-color:#58A0DC;
-	overflow:hidden;
-}
-
-.timeline-small-event-tape,
-.timeline-small-event-icon{
-	position:absolute;
-}
-
-.timeline-small-event-icon{width:1px; height:6px;}
-
-  
-/*--------------------------------- TIMELINE-------------------------*/
-.timeline-ether-bg{width:100%; height:100%;}
-.timeline-band-0 .timeline-ether-bg{background-color:#eee}
-.timeline-band-1 .timeline-ether-bg{background-color:#ddd}
-.timeline-band-2 .timeline-ether-bg{background-color:#ccc}
-.timeline-band-3 .timeline-ether-bg{background-color:#aaa}
-.timeline-duration-event {
-    position: absolute;
-    overflow: hidden;
-    border: 1px solid blue;
-}
-
-.timeline-instant-event2 {
-    position: absolute;
-    overflow: hidden;
-    border-left: 1px solid blue;
-    padding-left: 2px;
-}
-
-.timeline-instant-event {
-    position: absolute;
-    overflow: hidden;
-}
-
-.timeline-event-bubble-title {
-    font-weight: bold;
-    border-bottom: 1px solid #888;
-    margin-bottom: 0.5em;
-}
-
-.timeline-event-bubble-body {
-}
-
-.timeline-event-bubble-wiki {
-    margin:     0.5em;
-    text-align: right;
-    color:      #A0A040;
-}
-.timeline-event-bubble-wiki a {
-    color:      #A0A040;
-}
-
-.timeline-event-bubble-time {
-    color: #aaa;
-}
-
-.timeline-event-bubble-image {
-    float: right;
-    padding-left: 5px;
-    padding-bottom: 5px;
-}
\ No newline at end of file
Binary file web/data/timeline/blue-circle.png has changed
Binary file web/data/timeline/bubble-arrows.png has changed
Binary file web/data/timeline/bubble-body-and-arrows.png has changed
Binary file web/data/timeline/bubble-body.png has changed
Binary file web/data/timeline/bubble-bottom-arrow.png has changed
Binary file web/data/timeline/bubble-bottom-left.png has changed
Binary file web/data/timeline/bubble-bottom-right.png has changed
Binary file web/data/timeline/bubble-bottom.png has changed
Binary file web/data/timeline/bubble-left-arrow.png has changed
Binary file web/data/timeline/bubble-left.png has changed
Binary file web/data/timeline/bubble-right-arrow.png has changed
Binary file web/data/timeline/bubble-right.png has changed
Binary file web/data/timeline/bubble-top-arrow.png has changed
Binary file web/data/timeline/bubble-top-left.png has changed
Binary file web/data/timeline/bubble-top-right.png has changed
Binary file web/data/timeline/bubble-top.png has changed
Binary file web/data/timeline/close-button.png has changed
Binary file web/data/timeline/copyright-vertical.png has changed
Binary file web/data/timeline/copyright.png has changed
Binary file web/data/timeline/dark-blue-circle.png has changed
Binary file web/data/timeline/dark-green-circle.png has changed
Binary file web/data/timeline/dark-red-circle.png has changed
Binary file web/data/timeline/dull-blue-circle.png has changed
Binary file web/data/timeline/dull-green-circle.png has changed
Binary file web/data/timeline/dull-red-circle.png has changed
Binary file web/data/timeline/gray-circle.png has changed
Binary file web/data/timeline/green-circle.png has changed
Binary file web/data/timeline/message-bottom-left.png has changed
Binary file web/data/timeline/message-bottom-right.png has changed
Binary file web/data/timeline/message-left.png has changed
Binary file web/data/timeline/message-right.png has changed
Binary file web/data/timeline/message-top-left.png has changed
Binary file web/data/timeline/message-top-right.png has changed
Binary file web/data/timeline/message.png has changed
Binary file web/data/timeline/progress-running.gif has changed
Binary file web/data/timeline/red-circle.png has changed
Binary file web/data/timeline/sundial.png has changed
Binary file web/data/timeline/top-bubble.png has changed
--- a/web/facet.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/facet.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1210,7 +1210,7 @@
 
     The image below display the rendering of the slider:
 
-    .. image:: ../images/facet_range.png
+    .. image:: ../../images/facet_range.png
 
     .. _jquery: http://www.jqueryui.com/
     """
@@ -1309,7 +1309,7 @@
 
     The image below display the rendering of the slider for a date range:
 
-    .. image:: ../images/facet_date_range.png
+    .. image:: ../../images/facet_date_range.png
     """
     target_attr_type = 'Date' # only date types are supported
 
@@ -1440,7 +1440,7 @@
     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
+    .. image:: ../../images/facet_has_image.png
 
     .. sourcecode:: python
 
@@ -1465,15 +1465,17 @@
 
     def add_rql_restrictions(self):
         """add restriction for this facet into the rql syntax tree"""
-        self.select.set_distinct(True) # XXX
         value = self._cw.form.get(self.__regid__)
         if not value: # no value sent for this facet
             return
+        exists = nodes.Exists()
+        self.select.add_restriction(exists)
         var = self.select.make_variable()
         if self.role == 'subject':
-            self.select.add_relation(self.filtered_variable, self.rtype, var)
+            subj, obj = self.filtered_variable, var
         else:
-            self.select.add_relation(var, self.rtype, self.filtered_variable)
+            subj, obj = var, self.filtered_variable
+        exists.add_relation(subj, self.rtype, obj)
 
 
 class BitFieldFacet(AttributeFacet):
--- a/web/formwidgets.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/formwidgets.py	Fri Oct 09 17:52:14 2015 +0200
@@ -712,7 +712,7 @@
 
 
 class JQueryTimePicker(JQueryDatePicker):
-    """Use jquery.timePicker to define a time picker. Will return the time as an
+    """Use jquery.timePicker to define a time picker. Will return the time as a
     unicode string.
     """
     needs_js = ('jquery.timePicker.js',)
--- a/web/htmlwidgets.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/htmlwidgets.py	Fri Oct 09 17:52:14 2015 +0200
@@ -141,7 +141,7 @@
 
 
 class RawBoxItem(HTMLWidget): # XXX deprecated
-    """a simpe box item displaying raw data"""
+    """a simple box item displaying raw data"""
 
     def __init__(self, label, liclass=None):
         self.label = label
--- a/web/httpcache.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/httpcache.py	Fri Oct 09 17:52:14 2015 +0200
@@ -22,10 +22,6 @@
 from time import mktime
 from datetime import datetime
 
-# time delta usable to convert localized time to GMT time
-# XXX this become erroneous after a DST transition!!!
-GMTOFFSET = - (datetime.now() - datetime.utcnow())
-
 class NoHTTPCacheManager(object):
     """default cache manager: set no-cache cache control policy"""
     def __init__(self, view):
--- a/web/propertysheet.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/propertysheet.py	Fri Oct 09 17:52:14 2015 +0200
@@ -63,15 +63,15 @@
                 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._propfile_mtime[fpath] = os.stat(fpath).st_mtime
         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:
+            if os.stat(osp.join(rdirectory, rid)).st_mtime > mtime:
                 del self._cache[rid]
         for fpath, mtime in self._propfile_mtime.iteritems():
-            if os.stat(fpath)[-2] > mtime:
+            if os.stat(fpath).st_mtime > mtime:
                 return True
         return False
 
@@ -109,7 +109,7 @@
                 stream.write(content)
                 stream.close()
                 adirectory = self._cache_directory
-            self._cache[rid] = (adirectory, rdirectory, os.stat(sourcefile)[-2])
+            self._cache[rid] = (adirectory, rdirectory, os.stat(sourcefile).st_mtime)
             return adirectory
 
     def compile(self, content):
--- a/web/request.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/request.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -38,14 +38,14 @@
 from logilab.common.deprecation import deprecated
 from logilab.mtconverter import xml_escape
 
+from cubicweb import AuthenticationError
 from cubicweb.req import RequestSessionBase
-from cubicweb.dbapi import DBAPIRequest
 from cubicweb.uilib import remove_html_tags, js
 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
 from cubicweb.view import TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
-from cubicweb.web.httpcache import GMTOFFSET, get_validators
+from cubicweb.web.httpcache import get_validators
 from cubicweb.web.http_headers import Headers, Cookie, parseDateTime
 
 _MARKER = object()
@@ -155,9 +155,7 @@
         #: shared among various components used to publish the request (views,
         #: controller, application...)
         self.data = {}
-        #:  search state: 'normal' or 'linksearch' (eg searching for an object
-        #:  to create a relation with another)
-        self.search_state = ('normal',)
+        self._search_state = None
         #: page id, set by htmlheader template
         self.pageid = None
         self._set_pageid()
@@ -354,21 +352,36 @@
             self.session.data.pop(self._msgid, u'')
             del self._msgid
 
+    def _load_search_state(self, searchstate):
+        if searchstate is None or searchstate == 'normal':
+            self._search_state = ('normal',)
+        else:
+            self._search_state = ('linksearch', searchstate.split(':'))
+            assert len(self._search_state[-1]) == 4, 'invalid searchstate'
+
+    @property
+    def search_state(self):
+        """search state: 'normal' or 'linksearch' (i.e. searching for an object
+        to create a relation with another)"""
+        if self._search_state is None:
+            searchstate = self.session.data.get('search_state', 'normal')
+            self._load_search_state(searchstate)
+        return self._search_state
+
+    @search_state.setter
+    def search_state(self, searchstate):
+        self._search_state = searchstate
+
     def update_search_state(self):
-        """update the current search state"""
+        """update the current search state if needed"""
         searchstate = self.form.get('__mode')
-        if not searchstate:
-            searchstate = self.session.data.get('search_state', 'normal')
-        self.set_search_state(searchstate)
+        if searchstate:
+            self.set_search_state(searchstate)
 
     def set_search_state(self, searchstate):
         """set a new search state"""
-        if searchstate is None or searchstate == 'normal':
-            self.search_state = (searchstate or 'normal',)
-        else:
-            self.search_state = ('linksearch', searchstate.split(':'))
-            assert len(self.search_state[-1]) == 4
         self.session.data['search_state'] = searchstate
+        self._load_search_state(searchstate)
 
     def match_search_state(self, rset):
         """when searching an entity to create a relation, return True if entities in
@@ -385,7 +398,7 @@
 
     def update_breadcrumbs(self):
         """stores the last visisted page in session data"""
-        searchstate = self.session.data.get('search_state')
+        searchstate = self.search_state[0]
         if searchstate == 'normal':
             breadcrumbs = self.session.data.get('breadcrumbs')
             if breadcrumbs is None:
@@ -403,67 +416,6 @@
             return breadcrumbs.pop()
         return self.base_url()
 
-    @deprecated('[3.19] use a traditional ajaxfunc / controller')
-    def user_rql_callback(self, rqlargs, *args, **kwargs):
-        """register a user callback to execute some rql query, and return a URL
-        to call that callback which can be inserted in an HTML view.
-
-        `rqlargs` should be a tuple containing argument to give to the execute function.
-
-        The first argument following rqlargs must be the message to be
-        displayed after the callback is called.
-
-        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, rqlargs, *args, **kwargs)
-
-    @deprecated('[3.19] use a traditional ajaxfunc / controller')
-    def user_callback(self, cb, cbargs, *args, **kwargs):
-        """register the given user callback and return a URL which can
-        be inserted in an HTML view. When the URL is accessed, the
-        callback function will be called (as 'cb(req, \*cbargs)', and a
-        message will be displayed in the web interface. The third
-        positional argument must be 'msg', containing the message.
-
-        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')
-        jsfunc = kwargs.pop('jsfunc', 'userCallbackThenReloadPage')
-        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)
-
-    @deprecated('[3.19] use a traditional ajaxfunc / controller')
-    def register_onetime_callback(self, func, *args):
-        cbname = build_cb_uid(func.__name__)
-        def _cb(req):
-            try:
-                return func(req, *args)
-            finally:
-                self.unregister_callback(self.pageid, cbname)
-        self.set_page_data(cbname, _cb)
-        return cbname
-
-    @deprecated('[3.19] use a traditional ajaxfunc / controller')
-    def unregister_callback(self, pageid, cbname):
-        assert pageid is not None
-        assert cbname.startswith('cb_')
-        self.info('unregistering callback %s for pageid %s', cbname, pageid)
-        self.del_page_data(cbname)
-
-    @deprecated('[3.19] use a traditional ajaxfunc / controller')
-    def clear_user_callbacks(self):
-        if self.session is not None: # XXX
-            for key in list(self.session.data):
-                if key.startswith('cb_'):
-                    del self.session.data[key]
-
     # web edition helpers #####################################################
 
     @cached # so it's writed only once
@@ -584,8 +536,8 @@
             # we don't want to handle times before the EPOCH (cause bug on
             # windows). Also use > and not >= else expires == 0 and Cookie think
             # that means no expire...
-            assert expires + GMTOFFSET > date(1970, 1, 1)
-            expires = timegm((expires + GMTOFFSET).timetuple())
+            assert expires > date(1970, 1, 1)
+            expires = timegm(expires.timetuple())
         else:
             expires = None
         # make sure cookie is set on the correct path
@@ -859,8 +811,7 @@
         """
         mtime = self.get_header('If-modified-since', raw=False)
         if mtime:
-            # :/ twisted is returned a localized time stamp
-            return datetime.fromtimestamp(mtime) + GMTOFFSET
+            return datetime.utcfromtimestamp(mtime)
         return None
 
     ### outcoming headers
@@ -1005,29 +956,36 @@
         self.set_default_language(vreg)
 
 
-class DBAPICubicWebRequestBase(_CubicWebRequestBase, DBAPIRequest):
-
-    def set_session(self, session):
-        """method called by the session handler when the user is authenticated
-        or an anonymous connection is open
-        """
-        super(CubicWebRequestBase, self).set_session(session)
-        # set request language
-        self.set_user_language(session.user)
-
-
 def _cnx_func(name):
     def proxy(req, *args, **kwargs):
         return getattr(req.cnx, name)(*args, **kwargs)
     return proxy
 
+class _NeedAuthAccessMock(object):
+
+    def __getattribute__(self, attr):
+        raise AuthenticationError()
+
+    def __nonzero__(self):
+        return False
+
+class _MockAnonymousSession(object):
+    sessionid = 'thisisnotarealsession'
+
+    @property
+    def data(self):
+        return {}
+
+    @property
+    def anonymous_session(self):
+        return True
 
 class ConnectionCubicWebRequestBase(_CubicWebRequestBase):
+    cnx = None
+    session = None
 
     def __init__(self, vreg, https=False, form=None, headers={}):
         """"""
-        self.cnx = None
-        self.session = None
         self.vreg = vreg
         try:
             # no vreg or config which doesn't handle translations
@@ -1036,8 +994,7 @@
             self.translations = {}
         super(ConnectionCubicWebRequestBase, self).__init__(vreg, https=https,
                                                        form=form, headers=headers)
-        from cubicweb.dbapi import DBAPISession, _NeedAuthAccessMock
-        self.session = DBAPISession(None)
+        self.session = _MockAnonymousSession()
         self.cnx = self.user = _NeedAuthAccessMock()
 
     @property
@@ -1045,8 +1002,10 @@
         return self.cnx.transaction_data
 
     def set_cnx(self, cnx):
+        if 'ecache' in cnx.transaction_data:
+            del cnx.transaction_data['ecache']
         self.cnx = cnx
-        self.session = cnx._session
+        self.session = cnx.session
         self._set_user(cnx.user)
         self.set_user_language(cnx.user)
 
@@ -1056,7 +1015,6 @@
         return rset
 
     def set_default_language(self, vreg):
-        # XXX copy from dbapi
         try:
             lang = vreg.property_value('ui.language')
         except Exception: # property may not be registered
--- a/web/test/data/bootstrap_cubes	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/data/bootstrap_cubes	Fri Oct 09 17:52:14 2015 +0200
@@ -1,1 +1,1 @@
-file, blog, tag, folder
+file, blog, tag
--- a/web/test/data/schema.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/data/schema.py	Fri Oct 09 17:52:14 2015 +0200
@@ -93,9 +93,9 @@
     title = String(maxsize=32, required=True, fulltextindexed=True)
     concerns = SubjectRelation('Project', composite='object')
 
-# used by windmill for `test_edit_relation`
-from cubes.folder.schema import Folder
-
+class Folder(EntityType):
+    name = String(required=True)
+    filed_under = SubjectRelation('Folder', description=_('parent folder'))
 
 class TreeNode(EntityType):
     name = String(required=True)
--- a/web/test/data/views.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/data/views.py	Fri Oct 09 17:52:14 2015 +0200
@@ -16,7 +16,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.predicates import has_related_entities
 from cubicweb.web.views.ajaxcontroller import ajaxfunc
+from cubicweb.web.views.ibreadcrumbs import IBreadCrumbsAdapter
 
 def _recursive_replace_stream_by_content(tree):
     """ Search for streams (i.e. object that have a 'read' method) in a tree
@@ -46,3 +48,10 @@
     except Exception, ex:
         import traceback as tb
         tb.print_exc(ex)
+
+
+class FolderIBreadCrumbsAdapter(IBreadCrumbsAdapter):
+    __select__ = IBreadCrumbsAdapter.__select__ & has_related_entities('filed_under')
+
+    def parent_entity(self):
+        return self.entity.filed_under[0]
--- a/web/test/jstests/ajax_url1.html	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/jstests/ajax_url1.html	Fri Oct 09 17:52:14 2015 +0200
@@ -1,6 +1,6 @@
 <div id="ajaxroot">
   <div class="ajaxHtmlHead">
-    <script src="http://foo.js" type="text/javascript"> </script>
+    <cubicweb:script src="http://foo.js" type="text/javascript"> </cubicweb:script>
   </div>
   <h1>Hello</h1>
 </div>
--- a/web/test/jstests/ajaxresult.json	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/jstests/ajaxresult.json	Fri Oct 09 17:52:14 2015 +0200
@@ -1,1 +1,1 @@
-['foo', 'bar']
+["foo", "bar"]
--- a/web/test/jstests/test_ajax.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/jstests/test_ajax.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,17 +1,17 @@
 $(document).ready(function() {
 
-    module("ajax", {
+    QUnit.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 = [];
+          cw.loaded_scripts = [];
+          cw.loaded_links = [];
         },
         teardown: function() {
-          $('head script[src]:gt(' + this.scriptsLength + ')').remove();
+          $('head script[src]:lt(' + ($('head script[src]').length - 1 - this.scriptsLength) + ')').remove();
           $('head link[rel=stylesheet]:gt(' + this.cssLength + ')').remove();
         }
       });
@@ -22,66 +22,66 @@
         });
     }
 
-    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() {
+    QUnit.test('test simple h1 inclusion (ajax_url0.html)', function (assert) {
+        assert.expect(3);
+        assert.equal($('#qunit-fixture').children().length, 0);
+        var done = assert.async();
+        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
+        .addCallback(function() {
                 try {
-                    equals(jQuery('#main').children().length, 1);
-                    equals(jQuery('#main h1').html(), 'Hello');
+                    assert.equal($('#qunit-fixture').children().length, 1);
+                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
                 } finally {
-                    start();
+                    done();
                 };
             }
-        });
+        );
     });
 
-    test('test simple html head inclusion (ajax_url1.html)', function() {
-        expect(6);
+    QUnit.test('test simple html head inclusion (ajax_url1.html)', function (assert) {
+        assert.expect(6);
         var scriptsIncluded = jsSources();
-        equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
-        stop();
-        jQuery('#main').loadxhtml('/../ajax_url1.html', {
-            callback: function() {
+        assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        var done = assert.async();
+        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url1.html')
+        .addCallback(function() {
                 try {
                     var origLength = scriptsIncluded.length;
                     scriptsIncluded = jsSources();
-                    // check that foo.js has been *appended* to <head>
-                    equals(scriptsIncluded.length, origLength + 1);
-                    equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+                    // check that foo.js has been prepended to <head>
+                    assert.equal(scriptsIncluded.length, origLength + 1);
+                    assert.equal(scriptsIncluded.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');
+                    assert.equal($('#qunit-fixture').children().length, 1);
+                    assert.equal($('div.ajaxHtmlHead').length, 0);
+                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
                 } finally {
-                    start();
+                    done();
                 };
             }
-        });
+        );
     });
 
-    test('test addCallback', function() {
-        expect(3);
-        equals(jQuery('#main').children().length, 0);
-        stop();
-        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+    QUnit.test('test addCallback', function (assert) {
+        assert.expect(3);
+        assert.equal($('#qunit-fixture').children().length, 0);
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
         d.addCallback(function() {
             try {
-                equals(jQuery('#main').children().length, 1);
-                equals(jQuery('#main h1').html(), 'Hello');
+                assert.equal($('#qunit-fixture').children().length, 1);
+                assert.equal($('#qunit-fixture h1').html(), 'Hello');
             } finally {
-                start();
+                done();
             };
         });
     });
 
-    test('test callback after synchronous request', function() {
-        expect(1);
+    QUnit.test('test callback after synchronous request', function (assert) {
+        assert.expect(1);
         var deferred = new Deferred();
         var result = jQuery.ajax({
-            url: './ajax_url0.html',
+            url: BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html',
             async: false,
             beforeSend: function(xhr) {
                 deferred._req = xhr;
@@ -90,44 +90,44 @@
                 deferred.success(data);
             }
         });
-        stop();
+        var done = assert.async();
         deferred.addCallback(function() {
             try {
                 // add an assertion to ensure the callback is executed
-                ok(true, "callback is executed");
+                assert.ok(true, "callback is executed");
             } finally {
-                start();
+                done();
             };
         });
     });
 
-    test('test addCallback with parameters', function() {
-        expect(3);
-        equals(jQuery('#main').children().length, 0);
-        stop();
-        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+    QUnit.test('test addCallback with parameters', function (assert) {
+        assert.expect(3);
+        assert.equal($('#qunit-fixture').children().length, 0);
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
         d.addCallback(function(data, req, arg1, arg2) {
             try {
-                equals(arg1, 'Hello');
-                equals(arg2, 'world');
+                assert.equal(arg1, 'Hello');
+                assert.equal(arg2, 'world');
             } finally {
-                start();
+                done();
             };
         },
         'Hello', 'world');
     });
 
-    test('test callback after synchronous request with parameters', function() {
-        expect(2);
+    QUnit.test('test callback after synchronous request with parameters', function (assert) {
+        assert.expect(3);
         var deferred = new Deferred();
         deferred.addCallback(function(data, req, arg1, arg2) {
             // add an assertion to ensure the callback is executed
             try {
-                ok(true, "callback is executed");
-                equals(arg1, 'Hello');
-                equals(arg2, 'world');
+                assert.ok(true, "callback is executed");
+                assert.equal(arg1, 'Hello');
+                assert.equal(arg2, 'world');
             } finally {
-                start();
+                done();
             };
         },
         'Hello', 'world');
@@ -136,12 +136,12 @@
             try {
                 throw this._error;
             } finally {
-                start();
+                done();
             };
         });
-        stop();
+        var done = assert.async();
         var result = jQuery.ajax({
-            url: '/../ajax_url0.html',
+            url: BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html',
             async: false,
             beforeSend: function(xhr) {
                 deferred._req = xhr;
@@ -152,138 +152,123 @@
         });
     });
 
-  test('test addErrback', function() {
-        expect(1);
-        stop();
-        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+  QUnit.test('test addErrback', function (assert) {
+        assert.expect(1);
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/nonexistent.html');
         d.addCallback(function() {
-            // throw an exception to start errback chain
-            try {
-                throw new Error();
-            } finally {
-                start();
-            };
+            // should not be executed
+            assert.ok(false, "callback is executed");
         });
         d.addErrback(function() {
             try {
-                ok(true, "errback is executed");
+                assert.ok(true, "errback is executed");
             } finally {
-                start();
+                done();
             };
         });
     });
 
-    test('test callback / errback execution order', function() {
-        expect(4);
+    QUnit.test('test callback execution order', function (assert) {
+        assert.expect(3);
         var counter = 0;
-        stop();
-        var d = jQuery('#main').loadxhtml('/../ajax_url0.html', {
-            callback: function() {
-                try {
-                    equals(++counter, 1); // should be executed first
-                } finally {
-                    start();
-                };
-            }
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
+        d.addCallback(function() {
+            assert.equal(++counter, 1); // should be executed first
         });
         d.addCallback(function() {
-            equals(++counter, 2); // should be executed and break callback chain
-            throw new Error();
+            assert.equal(++counter, 2);
         });
         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);
+            try {
+                assert.equal(++counter, 3);
+            } finally {
+                done();
+            }
         });
     });
 
-    test('test already included resources are ignored (ajax_url1.html)', function() {
-        expect(10);
+    QUnit.test('test already included resources are ignored (ajax_url1.html)', function (assert) {
+        assert.expect(10);
         var scriptsIncluded = jsSources();
         // NOTE:
-        equals(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
-        equals(jQuery('head link').length, 1);
+        assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
+        assert.equal($('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() {
+        assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
+        var done = assert.async();
+        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url1.html')
+        .addCallback(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);
+                    assert.equal(scriptsIncluded.length, origLength + 1);
+                    assert.equal(scriptsIncluded.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');
+                    assert.equal($('#qunit-fixture').children().length, 1);
+                    assert.equal($('div.ajaxHtmlHead').length, 0);
+                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
                     // qunit.css is not added twice
-                    equals(jQuery('head link').length, 1);
+                    assert.equal($('head link').length, 1);
                     /* use endswith because in pytest context we have an absolute path */
-                    ok(jQuery('head link').attr('href').endswith('/qunit.css'));
+                    assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
                 } finally {
-                    start();
+                    done();
                 }
             }
-        });
+        );
     });
 
-    test('test synchronous request loadRemote', function() {
-        var res = loadRemote('/../ajaxresult.json', {},
+    QUnit.test('test synchronous request loadRemote', function (assert) {
+        var res = loadRemote(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajaxresult.json', {},
         'GET', true);
-        same(res, ['foo', 'bar']);
+        assert.deepEqual(res, ['foo', 'bar']);
     });
 
-    test('test event on CubicWeb', function() {
-        expect(1);
-        stop();
+    QUnit.test('test event on CubicWeb', function (assert) {
+        assert.expect(1);
+        var done = assert.async();
         var events = null;
-        jQuery(CubicWeb).bind('server-response', function() {
+        $(CubicWeb).bind('server-response', function() {
             // check that server-response event on CubicWeb is triggered
             events = 'CubicWeb';
         });
-        jQuery('#main').loadxhtml('/../ajax_url0.html', {
-            callback: function() {
+        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
+        .addCallback(function() {
                 try {
-                    equals(events, 'CubicWeb');
+                    assert.equal(events, 'CubicWeb');
                 } finally {
-                    start();
+                    done();
                 };
             }
-        });
+        );
     });
 
-    test('test event on node', function() {
-        expect(3);
-        stop();
+    QUnit.test('test event on node', function (assert) {
+        assert.expect(3);
+        var done = assert.async();
         var nodes = [];
-        jQuery('#main').bind('server-response', function() {
+        $('#qunit-fixture').bind('server-response', function() {
             nodes.push('node');
         });
-        jQuery(CubicWeb).bind('server-response', function() {
+        $(CubicWeb).bind('server-response', function() {
             nodes.push('CubicWeb');
         });
-        jQuery('#main').loadxhtml('/../ajax_url0.html', {
-            callback: function() {
+        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
+        .addCallback(function() {
                 try {
-                    equals(nodes.length, 2);
+                    assert.equal(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');
+                    assert.equal(nodes[0], 'CubicWeb');
+                    assert.equal(nodes[1], 'node');
                 } finally {
-                    start();
+                    done();
                 };
             }
-        });
+        );
     });
 });
 
--- a/web/test/jstests/test_htmlhelpers.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/jstests/test_htmlhelpers.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,35 +1,35 @@
 $(document).ready(function() {
 
-    module("module2", {
+    QUnit.module("module2", {
       setup: function() {
-        $('#main').append('<select id="theselect" multiple="multiple" size="2">' +
+        $('#qunit-fixture').append('<select id="theselect" multiple="multiple" size="2">' +
     			'</select>');
       }
     });
 
-    test("test first selected", function() {
+    QUnit.test("test first selected", function (assert) {
         $('#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');
+        assert.equal(selected.value, 'bar');
     });
 
-    test("test first selected 2", function() {
+    QUnit.test("test first selected 2", function (assert) {
         $('#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);
+        assert.equal(selected, null);
     });
 
-    module("visibilty");
-    test('toggleVisibility', function() {
-        $('#main').append('<div id="foo"></div>');
+    QUnit.module("visibilty");
+    QUnit.test('toggleVisibility', function (assert) {
+        $('#qunit-fixture').append('<div id="foo"></div>');
         toggleVisibility('foo');
-        ok($('#foo').hasClass('hidden'), 'check hidden class is set');
+        assert.ok($('#foo').hasClass('hidden'), 'check hidden class is set');
     });
 
 });
--- a/web/test/jstests/test_utils.js	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/jstests/test_utils.js	Fri Oct 09 17:52:14 2015 +0200
@@ -1,66 +1,66 @@
 $(document).ready(function() {
 
-  module("datetime");
+  QUnit.module("datetime");
 
-  test("test full datetime", function() {
-      equals(cw.utils.toISOTimestamp(new Date(1986, 3, 18, 10, 30, 0, 0)),
+  QUnit.test("test full datetime", function (assert) {
+      assert.equal(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');
+  QUnit.test("test only date", function (assert) {
+      assert.equal(cw.utils.toISOTimestamp(new Date(1986, 3, 18)), '1986-04-18 00:00:00');
   });
 
-  test("test null", function() {
-      equals(cw.utils.toISOTimestamp(null), null);
+  QUnit.test("test null", function (assert) {
+      assert.equal(cw.utils.toISOTimestamp(null), null);
   });
 
-  module("parsing");
-  test("test basic number parsing", function() {
+  QUnit.module("parsing");
+  QUnit.test("test basic number parsing", function (assert) {
       var d = strptime('2008/08/08', '%Y/%m/%d');
-      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
       d = strptime('2008/8/8', '%Y/%m/%d');
-      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
       d = strptime('8/8/8', '%Y/%m/%d');
-      same(datetuple(d), [8, 8, 8, 0, 0]);
+      assert.deepEqual(datetuple(d), [8, 8, 8, 0, 0]);
       d = strptime('0/8/8', '%Y/%m/%d');
-      same(datetuple(d), [0, 8, 8, 0, 0]);
+      assert.deepEqual(datetuple(d), [0, 8, 8, 0, 0]);
       d = strptime('-10/8/8', '%Y/%m/%d');
-      same(datetuple(d), [-10, 8, 8, 0, 0]);
+      assert.deepEqual(datetuple(d), [-10, 8, 8, 0, 0]);
       d = strptime('-35000', '%Y');
-      same(datetuple(d), [-35000, 1, 1, 0, 0]);
+      assert.deepEqual(datetuple(d), [-35000, 1, 1, 0, 0]);
   });
 
-  test("test custom format parsing", function() {
+  QUnit.test("test custom format parsing", function (assert) {
       var d = strptime('2008-08-08', '%Y-%m-%d');
-      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
       d = strptime('2008 - !  08: 08', '%Y - !  %m: %d');
-      same(datetuple(d), [2008, 8, 8, 0, 0]);
+      assert.deepEqual(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]);
+      assert.deepEqual(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]);
+      assert.deepEqual(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]);
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 1, 14]);
   });
 
-  module("sliceList");
-  test("test slicelist", function() {
+  QUnit.module("sliceList");
+  QUnit.test("test slicelist", function (assert) {
       var list = ['a', 'b', 'c', 'd', 'e', 'f'];
-      same(cw.utils.sliceList(list, 2),  ['c', 'd', 'e', 'f']);
-      same(cw.utils.sliceList(list, 2, -2), ['c', 'd']);
-      same(cw.utils.sliceList(list, -3), ['d', 'e', 'f']);
-      same(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
-      same(cw.utils.sliceList(list),  list);
+      assert.deepEqual(cw.utils.sliceList(list, 2),  ['c', 'd', 'e', 'f']);
+      assert.deepEqual(cw.utils.sliceList(list, 2, -2), ['c', 'd']);
+      assert.deepEqual(cw.utils.sliceList(list, -3), ['d', 'e', 'f']);
+      assert.deepEqual(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
+      assert.deepEqual(cw.utils.sliceList(list),  list);
   });
 
-  module("formContents", {
+  QUnit.module("formContents", {
     setup: function() {
-      $('#main').append('<form id="test-form"></form>');
+      $('#qunit-fixture').append('<form id="test-form"></form>');
     }
   });
   // XXX test fckeditor
-  test("test formContents", function() {
+  QUnit.test("test formContents", function (assert) {
       $('#test-form').append('<input name="input-text" ' +
 			     'type="text" value="toto" />');
       $('#test-form').append('<textarea rows="10" cols="30" '+
@@ -83,7 +83,7 @@
 			     'value="one" />');
       $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
 			     'value="two"/>');
-      same(cw.utils.formContents($('#test-form')[0]), [
+      assert.deepEqual(cw.utils.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/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,6 @@
+requests
+webtest
+Twisted
+cubicweb-blog
+cubicweb-file
+cubicweb-tag
--- a/web/test/unittest_application.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_application.py	Fri Oct 09 17:52:14 2015 +0200
@@ -30,7 +30,6 @@
 from cubicweb.web import LogOut, Redirect, INTERNAL_FIELD_VALUE
 from cubicweb.web.views.basecontrollers import ViewController
 from cubicweb.web.application import anonymized_request
-from cubicweb.dbapi import DBAPISession, _NeedAuthAccessMock
 from cubicweb import repoapi
 
 class FakeMapping:
@@ -368,15 +367,11 @@
         req.set_request_header('Cookie', cookie[sessioncookie].OutputString(),
                                raw=True)
         clear_cache(req, 'get_authorization')
-        # reset session as if it was a new incoming request
-        req.session = DBAPISession(None)
-        req.user = req.cnx = _NeedAuthAccessMock
-        
 
     def _test_auth_anon(self, req):
         asession = self.app.get_session(req)
         # important otherwise _reset_cookie will not use the right session
-        req.set_cnx(repoapi.ClientConnection(asession))
+        req.set_cnx(repoapi.Connection(asession))
         self.assertEqual(len(self.open_sessions), 1)
         self.assertEqual(asession.login, 'anon')
         self.assertTrue(asession.anonymous_session)
@@ -386,7 +381,7 @@
         self.assertEqual(1, len(self.open_sessions))
         session = self.app.get_session(req)
         # important otherwise _reset_cookie will not use the right session
-        req.set_cnx(repoapi.ClientConnection(session))
+        req.set_cnx(repoapi.Connection(session))
         self.assertEqual(req.message, 'authentication failure')
         self.assertEqual(req.session.anonymous_session, True)
         self.assertEqual(1, len(self.open_sessions))
--- a/web/test/unittest_facet.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_facet.py	Fri Oct 09 17:52:14 2015 +0200
@@ -129,8 +129,6 @@
             self.assertEqual(f.select.as_string(),
                               'DISTINCT Any  WHERE X is CWUser')
 
-
-
     def test_relationattribute(self):
         with self.admin_access.web_request() as req:
             f, (guests, managers) = self._in_group_facet(req, cls=facet.RelationAttributeFacet)
@@ -150,6 +148,20 @@
             self.assertEqual(f.select.as_string(),
                               "DISTINCT Any  WHERE X is CWUser, X in_group E, E name 'guests'")
 
+    def test_hasrelation(self):
+        with self.admin_access.web_request() as req:
+            rset, rqlst, filtered_variable = self.prepare_rqlst(req)
+            f = facet.HasRelationFacet(req, rset=rset,
+                                       select=rqlst.children[0],
+                                       filtered_variable=filtered_variable)
+            f.__regid__ = 'has_group'
+            f.rtype = 'in_group'
+            f.role = 'subject'
+            f._cw.form[f.__regid__] = 'feed me'
+            f.add_rql_restrictions()
+            self.assertEqual(f.select.as_string(),
+                             'DISTINCT Any  WHERE X is CWUser, EXISTS(X in_group A)')
+
     def test_daterange(self):
         with self.admin_access.web_request() as req:
             rset, rqlst, filtered_variable = self.prepare_rqlst(req)
--- a/web/test/unittest_form.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_form.py	Fri Oct 09 17:52:14 2015 +0200
@@ -21,7 +21,7 @@
 from xml.etree.ElementTree import fromstring
 from lxml import html
 
-from logilab.common.testlib import unittest_main, mock_object
+from logilab.common.testlib import unittest_main
 
 from cubicweb import Binary, ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
@@ -39,7 +39,7 @@
     def test_form_field_format(self):
         with self.admin_access.web_request() as req:
             form = FieldsForm(req, None)
-            self.assertEqual(StringField().format(form), 'text/html')
+            self.assertEqual(StringField().format(form), 'text/plain')
             req.cnx.execute('INSERT CWProperty X: X pkey "ui.default-text-format", X value "text/rest", X for_user U WHERE U login "admin"')
             req.cnx.commit()
             self.assertEqual(StringField().format(form), 'text/rest')
--- a/web/test/unittest_formfields.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_formfields.py	Fri Oct 09 17:52:14 2015 +0200
@@ -150,7 +150,7 @@
             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.assertEqual(description_format_field.value(form), 'text/html')
+            self.assertEqual(description_format_field.value(form), 'text/plain')
             req.cnx.execute('INSERT CWProperty X: X pkey "ui.default-text-format", X value "text/rest", X for_user U WHERE U login "admin"')
             req.cnx.commit()
             self.assertEqual(description_format_field.value(form), 'text/rest')
--- a/web/test/unittest_propertysheet.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_propertysheet.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,23 +1,37 @@
 import os
 from os.path import join, dirname
 from shutil import rmtree
-
-from logilab.common.testlib import TestCase, unittest_main
+import errno
+import tempfile
+from unittest import TestCase, main
 
 from cubicweb.web.propertysheet import PropertySheet, lazystr
 
+
 DATADIR = join(dirname(__file__), 'data')
-CACHEDIR = join(DATADIR, 'uicache')
+
 
 class PropertySheetTC(TestCase):
 
+    def setUp(self):
+        uicache = join(DATADIR, 'uicache')
+        try:
+            os.makedirs(uicache)
+        except OSError as err:
+            if err.errno != errno.EEXIST:
+                raise
+        self.cachedir = tempfile.mkdtemp(dir=uicache)
+
     def tearDown(self):
-        rmtree(CACHEDIR)
+        rmtree(self.cachedir)
+
+    def data(self, filename):
+        return join(DATADIR, filename)
 
     def test(self):
-        ps = PropertySheet(CACHEDIR, datadir_url='http://cwtest.com')
-        ps.load(join(DATADIR, 'sheet1.py'))
-        ps.load(join(DATADIR, 'sheet2.py'))
+        ps = PropertySheet(self.cachedir, datadir_url='http://cwtest.com')
+        ps.load(self.data('sheet1.py'))
+        ps.load(self.data('sheet2.py'))
         # defined by sheet1
         self.assertEqual(ps['logo'], 'http://cwtest.com/logo.png')
         # defined by sheet1, overriden by sheet2
@@ -34,10 +48,10 @@
         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.cachedir)
         self.assertIn('pouet.css', ps._cache)
         self.assertFalse(ps.need_reload())
-        os.utime(join(DATADIR, 'sheet1.py'), None)
+        os.utime(self.data('sheet1.py'), None)
         self.assertIn('pouet.css', ps._cache)
         self.assertTrue(ps.need_reload())
         self.assertIn('pouet.css', ps._cache)
@@ -45,9 +59,10 @@
         self.assertNotIn('pouet.css', ps._cache)
         self.assertFalse(ps.need_reload())
         ps.process_resource(DATADIR, 'pouet.css') # put in cache
-        os.utime(join(DATADIR, 'pouet.css'), None)
+        os.utime(self.data('pouet.css'), None)
         self.assertFalse(ps.need_reload())
         self.assertNotIn('pouet.css', ps._cache)
 
+
 if __name__ == '__main__':
-    unittest_main()
+    main()
--- a/web/test/unittest_reledit.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_reledit.py	Fri Oct 09 17:52:14 2015 +0200
@@ -53,7 +53,7 @@
 
     def test_default_forms(self):
         self.skipTest('Need to check if this test should still run post reledit/doreledit merge')
-        doreledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('invisible')" 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">
+        doreledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('invisible')" 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" 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" />
@@ -82,9 +82,10 @@
 <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>
+<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
 </form><div id="title-subject-%(eid)s" class="editableField invisible"><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="http://testing.fr/cubicweb/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('invisible')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('invisible')" 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">
+                     'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('invisible')" 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" target="eformframe">
 <fieldset>
 <input name="__form_id" type="invisible" value="edition" />
 <input name="__errorurl" type="invisible" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#long_desc-subject-%(eid)s-form" />
@@ -126,9 +127,10 @@
 <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>
+<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
 </form><div id="long_desc-subject-%(eid)s" class="editableField invisible"><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="http://testing.fr/cubicweb/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('invisible')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('invisible')" 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">
+                     'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('invisible')" 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" 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" />
@@ -162,6 +164,7 @@
 <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>
+<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
 </form><div id="manager-subject-%(eid)s" class="editableField invisible"><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="http://testing.fr/cubicweb/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;"""
--- a/web/test/unittest_urlpublisher.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_urlpublisher.py	Fri Oct 09 17:52:14 2015 +0200
@@ -66,8 +66,8 @@
             ctrl, rset = self.process(req, 'CWEType')
             self.assertEqual(ctrl, 'view')
             self.assertEqual(rset.description[0][0], 'CWEType')
-            self.assertEqual("Any X,AA,AB ORDERBY AA WHERE X is_instance_of CWEType, "
-                             "X name AA, X modification_date AB",
+            self.assertEqual("Any X,AA,AB ORDERBY AB WHERE X is_instance_of CWEType, "
+                             "X modification_date AA, X name AB",
                              rset.printable_rql())
 
     def test_rest_path_by_attr(self):
@@ -77,8 +77,8 @@
             self.assertEqual(len(rset), 1)
             self.assertEqual(rset.description[0][0], 'CWUser')
             self.assertEqual('Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
-                             'X login AA, X firstname AB, X surname AC, '
-                             'X modification_date AD, X login "admin"',
+                             'X firstname AA, X login AB, X modification_date AC, '
+                             'X surname AD, X login "admin"',
                              rset.printable_rql())
 
     def test_rest_path_unique_attr(self):
@@ -88,8 +88,8 @@
             self.assertEqual(len(rset), 1)
             self.assertEqual(rset.description[0][0], 'CWUser')
             self.assertEqual('Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
-                             'X login AA, X firstname AB, X surname AC, '
-                             'X modification_date AD, X login "admin"',
+                             'X firstname AA, X login AB, X modification_date AC, '
+                             'X surname AD, X login "admin"',
                              rset.printable_rql())
 
     def test_rest_path_eid(self):
@@ -99,8 +99,8 @@
             self.assertEqual(len(rset), 1)
             self.assertEqual(rset.description[0][0], 'CWUser')
             self.assertEqual('Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
-                             'X login AA, X firstname AB, X surname AC, '
-                             'X modification_date AD, X eid %s' % rset[0][0],
+                             'X firstname AA, X login AB, X modification_date AC, '
+                             'X surname AD, X eid %s' % rset[0][0],
                              rset.printable_rql())
 
     def test_rest_path_non_ascii_paths(self):
@@ -110,8 +110,8 @@
             self.assertEqual(len(rset), 1)
             self.assertEqual(rset.description[0][0], 'CWUser')
             self.assertEqual(u'Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
-                             u'X login AA, X firstname AB, X surname AC, '
-                             u'X modification_date AD, X login "\xffsa\xffe"',
+                             u'X firstname AA, X login AB, X modification_date AC, '
+                             u'X surname AD, X login "\xffsa\xffe"',
                              rset.printable_rql())
 
     def test_rest_path_quoted_paths(self):
@@ -121,7 +121,7 @@
             self.assertEqual(len(rset), 1)
             self.assertEqual(rset.description[0][0], 'BlogEntry')
             self.assertEqual(u'Any X,AA,AB,AC WHERE X is_instance_of BlogEntry, '
-                             'X creation_date AA, X title AB, X modification_date AC, '
+                             'X creation_date AA, X modification_date AB, X title AC, '
                              'X title "hell\'o"',
                              rset.printable_rql())
 
--- a/web/test/unittest_views_basecontrollers.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Fri Oct 09 17:52:14 2015 +0200
@@ -156,9 +156,9 @@
 
     def test_user_can_change_its_password(self):
         with self.admin_access.repo_cnx() as cnx:
-            self.create_user(cnx, 'user')
+            self.create_user(cnx, u'user')
             cnx.commit()
-        with self.new_access('user').web_request() as req:
+        with self.new_access(u'user').web_request() as req:
             eid = unicode(req.user.eid)
             req.form = {
                 'eid': eid, '__maineid' : eid,
@@ -287,7 +287,7 @@
 
     def test_edit_multiple_linked(self):
         with self.admin_access.web_request() as req:
-            peid = unicode(self.create_user(req, 'adim').eid)
+            peid = unicode(self.create_user(req, u'adim').eid)
             req.form = {'eid': [peid, 'Y'], '__maineid': peid,
 
                         '__type:'+peid: u'CWUser',
@@ -548,7 +548,7 @@
             self.assertIn('_cwmsgid', params)
             eid = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
             req.execute('SET X use_email E WHERE E eid %(e)s, X eid %(x)s',
-                        {'x': self.session.user.eid, 'e': eid})
+                        {'x': req.user.eid, 'e': eid})
             req.cnx.commit()
             req.form = {'eid': unicode(eid), '__type:%s'%eid: 'EmailAddress',
                         '__action_delete': ''}
@@ -692,7 +692,7 @@
 
     def test_nonregr_rollback_on_validation_error(self):
         with self.admin_access.web_request() as req:
-            p = self.create_user(req, "doe")
+            p = self.create_user(req, u"doe")
             # do not try to skip 'primary_email' for this test
             old_skips = p.__class__.skip_copy_for
             p.__class__.skip_copy_for = ()
@@ -754,10 +754,10 @@
 class ReportBugControllerTC(CubicWebTC):
 
     def test_usable_by_guest(self):
-        with self.new_access('anon').web_request() as req:
+        with self.new_access(u'anon').web_request() as req:
             self.assertRaises(NoSelectableObject,
                               self.vreg['controllers'].select, 'reportbug', req)
-        with self.new_access('anon').web_request(description='hop') as req:
+        with self.new_access(u'anon').web_request(description='hop') as req:
             self.vreg['controllers'].select('reportbug', req)
 
 
@@ -836,9 +836,9 @@
             deletes = get_pending_deletes(req)
             self.assertEqual(deletes, [])
             inserts = get_pending_inserts(req)
-            self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
+            self.assertCountEqual(inserts, ['12:tags:13', '12:tags:14'])
             inserts = get_pending_inserts(req, 12)
-            self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
+            self.assertCountEqual(inserts, ['12:tags:13', '12:tags:14'])
             inserts = get_pending_inserts(req, 13)
             self.assertEqual(inserts, ['12:tags:13'])
             inserts = get_pending_inserts(req, 14)
@@ -855,9 +855,9 @@
             inserts = get_pending_inserts(req)
             self.assertEqual(inserts, [])
             deletes = get_pending_deletes(req)
-            self.assertEqual(deletes, ['12:tags:13', '12:tags:14'])
+            self.assertCountEqual(deletes, ['12:tags:13', '12:tags:14'])
             deletes = get_pending_deletes(req, 12)
-            self.assertEqual(deletes, ['12:tags:13', '12:tags:14'])
+            self.assertCountEqual(deletes, ['12:tags:13', '12:tags:14'])
             deletes = get_pending_deletes(req, 13)
             self.assertEqual(deletes, ['12:tags:13'])
             deletes = get_pending_deletes(req, 14)
@@ -880,7 +880,7 @@
         with self.remote_calling('add_pending_inserts',
                                  [('12', 'tags', '13'), ('12', 'tags', '14')]) as (_, req):
             inserts = get_pending_inserts(req)
-            self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
+            self.assertCountEqual(inserts, ['12:tags:13', '12:tags:14'])
             req.remove_pending_operations()
 
 
@@ -1016,8 +1016,8 @@
 
     def setup_database(self):
         with self.admin_access.repo_cnx() as cnx:
-            self.toto = self.create_user(cnx, 'toto',
-                                         password='toto',
+            self.toto = self.create_user(cnx, u'toto',
+                                         password=u'toto',
                                          groups=('users',),
                                          commit=False)
             self.txuuid_toto = cnx.commit()
--- a/web/test/unittest_views_editforms.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_views_editforms.py	Fri Oct 09 17:52:14 2015 +0200
@@ -43,7 +43,7 @@
         with self.admin_access.web_request() as req:
             AFFK.tag_subject_of(('CWUser', 'login', '*'),
                                 {'widget': AutoCompletionWidget(autocomplete_initfunc='get_logins')})
-            form = self.vreg['forms'].select('edition', req, entity=self.user(req))
+            form = self.vreg['forms'].select('edition', req, entity=req.user)
             field = form.field_by_name('login', 'subject')
             self.assertIsInstance(field.widget, AutoCompletionWidget)
             AFFK.del_rtag('CWUser', 'login', '*', 'subject')
@@ -54,18 +54,18 @@
             e = self.vreg['etypes'].etype_class('CWUser')(req)
             # see custom configuration in views.cwuser
             self.assertEqual(rbc(e, 'main', 'attributes'),
-                              [('login', 'subject'),
-                               ('upassword', 'subject'),
-                               ('firstname', 'subject'),
-                               ('surname', 'subject'),
-                               ('in_group', 'subject'),
-                               ])
-            self.assertListEqual(rbc(e, 'muledit', 'attributes'),
+                             [('login', 'subject'),
+                              ('upassword', 'subject'),
+                              ('firstname', 'subject'),
+                              ('surname', 'subject'),
+                              ('in_group', 'subject'),
+                              ])
+            self.assertEqual(rbc(e, 'muledit', 'attributes'),
                                   [('login', 'subject'),
                                    ('upassword', 'subject'),
                                    ('in_group', 'subject'),
                                    ])
-            self.assertListEqual(rbc(e, 'main', 'metadata'),
+            self.assertCountEqual(rbc(e, 'main', 'metadata'),
                                   [('last_login_time', 'subject'),
                                    ('cw_source', 'subject'),
                                    ('creation_date', 'subject'),
@@ -77,7 +77,7 @@
             # 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.assertListEqual([x for x in rbc(e, 'main', 'relations')
+            self.assertCountEqual([x for x in rbc(e, 'main', 'relations')
                                    if x != ('tags', 'object')],
                                   [('connait', 'subject'),
                                    ('custom_workflow', 'subject'),
@@ -125,14 +125,14 @@
             self.assertListEqual(rbc(e, 'muledit', 'attributes'),
                                   [('nom', 'subject'),
                                    ])
-            self.assertListEqual(rbc(e, 'main', 'metadata'),
+            self.assertCountEqual(rbc(e, 'main', 'metadata'),
                                   [('cw_source', 'subject'),
                                    ('creation_date', 'subject'),
                                    ('modification_date', 'subject'),
                                    ('created_by', 'subject'),
                                    ('owned_by', 'subject'),
                                    ])
-            self.assertListEqual(rbc(e, 'main', 'relations'),
+            self.assertCountEqual(rbc(e, 'main', 'relations'),
                                   [('travaille', 'subject'),
                                    ('manager', 'object'),
                                    ('connait', 'object'),
@@ -158,15 +158,15 @@
     def test_attribute_add_permissions(self):
         # https://www.cubicweb.org/ticket/4342844
         with self.admin_access.repo_cnx() as cnx:
-            self.create_user(cnx, 'toto')
+            self.create_user(cnx, u'toto')
             cnx.commit()
-        with self.new_access('toto').web_request() as req:
+        with self.new_access(u'toto').web_request() as req:
             e = self.vreg['etypes'].etype_class('Personne')(req)
             cform = self.vreg['forms'].select('edition', req, entity=e)
             self.assertIn('sexe',
                           [rschema.type
                            for rschema, _ in cform.editable_attributes()])
-            with self.new_access('toto').repo_cnx() as cnx:
+            with self.new_access(u'toto').repo_cnx() as cnx:
                 person_eid = cnx.create_entity('Personne', nom=u'Robert').eid
                 cnx.commit()
             person = req.entity_from_eid(person_eid)
--- a/web/test/unittest_views_xmlrss.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_views_xmlrss.py	Fri Oct 09 17:52:14 2015 +0200
@@ -11,13 +11,13 @@
                 req.user.view('xml'),
                 '''\
 <CWUser eid="6" cwuri="http://testing.fr/cubicweb/6" cwsource="system">
+  <creation_date>%(cdate)s</creation_date>
+  <firstname/>
+  <last_login_time/>
   <login>admin</login>
-  <upassword/>
-  <firstname/>
+  <modification_date>%(mdate)s</modification_date>
   <surname/>
-  <last_login_time/>
-  <creation_date>%(cdate)s</creation_date>
-  <modification_date>%(mdate)s</modification_date>
+  <upassword/>
   <tags role="object">
   </tags>
   <in_group role="subject">
--- a/web/test/unittest_viewselector.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_viewselector.py	Fri Oct 09 17:52:14 2015 +0200
@@ -33,7 +33,6 @@
                                 cwproperties, cwsources, xmlrss, rdf, csvexport, json,
                                 undohistory)
 
-from cubes.folder import views as folderviews
 
 USERACTIONS = [actions.UserPreferencesAction,
                actions.UserInfoAction,
@@ -49,7 +48,7 @@
                  debug.SiteInfoAction]
 
 if hasattr(rdf, 'RDFView'): # not available if rdflib not installed
-    RDFVIEWS = [('rdf', rdf.RDFView)]
+    RDFVIEWS = [('rdf', rdf.RDFView), ('n3rdf', rdf.RDFN3View)]
 else:
     RDFVIEWS = []
 
@@ -101,7 +100,6 @@
                                   ('schema', schema.SchemaView),
                                   ('siteinfo', debug.SiteInfoView),
                                   ('systempropertiesform', cwproperties.SystemCWPropertiesForm),
-                                  ('tree', folderviews.FolderTreeView),
                                   ('undohistory', undohistory.UndoHistoryView)])
 
     def test_possible_views_noresult(self):
@@ -117,50 +115,51 @@
     def test_possible_views_one_egroup(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('CWGroup X WHERE X name "managers"')
-            self.assertListEqual(self.pviews(req, rset),
-                                 [('csvexport', csvexport.CSVRsetView),
-                                  ('ecsvexport', csvexport.CSVEntityView),
-                                  ('ejsonexport', json.JsonEntityView),
-                                  ('filetree', treeview.FileTreeView),
-                                  ('jsonexport', json.JsonRsetView),
-                                  ('list', baseviews.ListView),
-                                  ('oneline', baseviews.OneLineView),
-                                  ('owlabox', owl.OWLABOXView),
-                                  ('primary', cwuser.CWGroupPrimaryView)] + \
-                                 RDFVIEWS + \
-                                 [('rsetxml', xmlrss.XMLRsetView),
-                                  ('rss', xmlrss.RSSView),
-                                  ('sameetypelist', baseviews.SameETypeListView),
-                                  ('security', management.SecurityManagementView),
-                                  ('table', tableview.RsetTableView),
-                                  ('text', baseviews.TextView),
-                                  ('treeview', treeview.TreeView),
-                                  ('xbel', xbel.XbelView),
-                                  ('xml', xmlrss.XMLView)])
+            self.assertCountEqual(self.pviews(req, rset),
+                                  RDFVIEWS +
+                                  [('csvexport', csvexport.CSVRsetView),
+                                   ('ecsvexport', csvexport.CSVEntityView),
+                                   ('ejsonexport', json.JsonEntityView),
+                                   ('filetree', treeview.FileTreeView),
+                                   ('jsonexport', json.JsonRsetView),
+                                   ('list', baseviews.ListView),
+                                   ('oneline', baseviews.OneLineView),
+                                   ('owlabox', owl.OWLABOXView),
+                                   ('primary', cwuser.CWGroupPrimaryView),
+                                   ('rsetxml', xmlrss.XMLRsetView),
+                                   ('rss', xmlrss.RSSView),
+                                   ('sameetypelist', baseviews.SameETypeListView),
+                                   ('security', management.SecurityManagementView),
+                                   ('table', tableview.RsetTableView),
+                                   ('text', baseviews.TextView),
+                                   ('treeview', treeview.TreeView),
+                                   ('xbel', xbel.XbelView),
+                                   ('xml', xmlrss.XMLView)])
 
     def test_possible_views_multiple_egroups(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('CWGroup X')
-            self.assertListEqual(self.pviews(req, rset),
-                                 [('csvexport', csvexport.CSVRsetView),
-                                  ('ecsvexport', csvexport.CSVEntityView),
-                                  ('ejsonexport', json.JsonEntityView),
-                                  ('filetree', treeview.FileTreeView),
-                                  ('jsonexport', json.JsonRsetView),
-                                  ('list', baseviews.ListView),
-                                  ('oneline', baseviews.OneLineView),
-                                  ('owlabox', owl.OWLABOXView),
-                                  ('primary', cwuser.CWGroupPrimaryView)] + RDFVIEWS + [
-                                  ('rsetxml', xmlrss.XMLRsetView),
-                                  ('rss', xmlrss.RSSView),
-                                  ('sameetypelist', baseviews.SameETypeListView),
-                                  ('security', management.SecurityManagementView),
-                                  ('table', tableview.RsetTableView),
-                                  ('text', baseviews.TextView),
-                                  ('treeview', treeview.TreeView),
-                                  ('xbel', xbel.XbelView),
-                                  ('xml', xmlrss.XMLView),
-                                  ])
+            self.assertCountEqual(self.pviews(req, rset),
+                                  RDFVIEWS +
+                                  [('csvexport', csvexport.CSVRsetView),
+                                   ('ecsvexport', csvexport.CSVEntityView),
+                                   ('ejsonexport', json.JsonEntityView),
+                                   ('filetree', treeview.FileTreeView),
+                                   ('jsonexport', json.JsonRsetView),
+                                   ('list', baseviews.ListView),
+                                   ('oneline', baseviews.OneLineView),
+                                   ('owlabox', owl.OWLABOXView),
+                                   ('primary', cwuser.CWGroupPrimaryView),
+                                   ('rsetxml', xmlrss.XMLRsetView),
+                                   ('rss', xmlrss.RSSView),
+                                   ('sameetypelist', baseviews.SameETypeListView),
+                                   ('security', management.SecurityManagementView),
+                                   ('table', tableview.RsetTableView),
+                                   ('text', baseviews.TextView),
+                                   ('treeview', treeview.TreeView),
+                                   ('xbel', xbel.XbelView),
+                                   ('xml', xmlrss.XMLView),
+                                   ])
 
     def test_propertiesform_admin(self):
         assert self.vreg['views']['propertiesform']
@@ -172,7 +171,7 @@
             self.assertTrue(self.vreg['views'].select('propertiesform', req, rset=rset2))
 
     def test_propertiesform_anon(self):
-        with self.new_access('anon').web_request() as req:
+        with self.new_access(u'anon').web_request() as req:
             rset1 = req.execute('CWUser X WHERE X login "admin"')
             self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req, rset=None)
             self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req, rset=rset1)
@@ -181,9 +180,9 @@
 
     def test_propertiesform_jdoe(self):
         with self.admin_access.repo_cnx() as cnx:
-            self.create_user(cnx, 'jdoe')
+            self.create_user(cnx, u'jdoe')
             cnx.commit()
-        with self.new_access('jdoe').web_request() as req:
+        with self.new_access(u'jdoe').web_request() as req:
             rset1 = req.execute('CWUser X WHERE X login "admin"')
             rset2 = req.execute('CWUser X WHERE X login "jdoe"')
             self.assertTrue(self.vreg['views'].select('propertiesform', req, rset=None))
@@ -193,24 +192,25 @@
     def test_possible_views_multiple_different_types(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('Any X')
-            self.assertListEqual(self.pviews(req, rset),
-                                 [('csvexport', csvexport.CSVRsetView),
-                                  ('ecsvexport', csvexport.CSVEntityView),
-                                  ('ejsonexport', json.JsonEntityView),
-                                  ('filetree', treeview.FileTreeView),
-                                  ('jsonexport', json.JsonRsetView),
-                                  ('list', baseviews.ListView),
-                                  ('oneline', baseviews.OneLineView),
-                                  ('owlabox', owl.OWLABOXView),
-                                  ('primary', primary.PrimaryView),] + RDFVIEWS + [
-                                  ('rsetxml', xmlrss.XMLRsetView),
-                                  ('rss', xmlrss.RSSView),
-                                  ('security', management.SecurityManagementView),
-                                  ('table', tableview.RsetTableView),
-                                  ('text', baseviews.TextView),
-                                  ('treeview', treeview.TreeView),
-                                  ('xbel', xbel.XbelView),
-                                  ('xml', xmlrss.XMLView),
+            self.assertCountEqual(self.pviews(req, rset),
+                                  RDFVIEWS +
+                                  [('csvexport', csvexport.CSVRsetView),
+                                   ('ecsvexport', csvexport.CSVEntityView),
+                                   ('ejsonexport', json.JsonEntityView),
+                                   ('filetree', treeview.FileTreeView),
+                                   ('jsonexport', json.JsonRsetView),
+                                   ('list', baseviews.ListView),
+                                   ('oneline', baseviews.OneLineView),
+                                   ('owlabox', owl.OWLABOXView),
+                                   ('primary', primary.PrimaryView),
+                                   ('rsetxml', xmlrss.XMLRsetView),
+                                   ('rss', xmlrss.RSSView),
+                                   ('security', management.SecurityManagementView),
+                                   ('table', tableview.RsetTableView),
+                                   ('text', baseviews.TextView),
+                                   ('treeview', treeview.TreeView),
+                                   ('xbel', xbel.XbelView),
+                                   ('xml', xmlrss.XMLView),
                                   ])
 
     def test_possible_views_any_rset(self):
@@ -226,28 +226,29 @@
     def test_possible_views_multiple_eusers(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('CWUser X')
-            self.assertListEqual(self.pviews(req, rset),
-                                 [('csvexport', csvexport.CSVRsetView),
-                                  ('ecsvexport', csvexport.CSVEntityView),
-                                  ('ejsonexport', json.JsonEntityView),
-                                  ('filetree', treeview.FileTreeView),
-                                  ('foaf', cwuser.FoafView),
-                                  ('jsonexport', json.JsonRsetView),
-                                  ('list', baseviews.ListView),
-                                  ('oneline', baseviews.OneLineView),
-                                  ('owlabox', owl.OWLABOXView),
-                                  ('primary', primary.PrimaryView)] + RDFVIEWS + [
-                                  ('rsetxml', xmlrss.XMLRsetView),
-                                  ('rss', xmlrss.RSSView),
-                                  ('sameetypelist', baseviews.SameETypeListView),
-                                  ('security', management.SecurityManagementView),
-                                  ('table', tableview.RsetTableView),
-                                  ('text', baseviews.TextView),
-                                  ('treeview', treeview.TreeView),
-                                  ('vcard', vcard.VCardCWUserView),
-                                  ('xbel', xbel.XbelView),
-                                  ('xml', xmlrss.XMLView),
-                                  ])
+            self.assertCountEqual(self.pviews(req, rset),
+                                  RDFVIEWS +
+                                  [('csvexport', csvexport.CSVRsetView),
+                                   ('ecsvexport', csvexport.CSVEntityView),
+                                   ('ejsonexport', json.JsonEntityView),
+                                   ('filetree', treeview.FileTreeView),
+                                   ('foaf', cwuser.FoafView),
+                                   ('jsonexport', json.JsonRsetView),
+                                   ('list', baseviews.ListView),
+                                   ('oneline', baseviews.OneLineView),
+                                   ('owlabox', owl.OWLABOXView),
+                                   ('primary', primary.PrimaryView),
+                                   ('rsetxml', xmlrss.XMLRsetView),
+                                   ('rss', xmlrss.RSSView),
+                                   ('sameetypelist', baseviews.SameETypeListView),
+                                   ('security', management.SecurityManagementView),
+                                   ('table', tableview.RsetTableView),
+                                   ('text', baseviews.TextView),
+                                   ('treeview', treeview.TreeView),
+                                   ('vcard', vcard.VCardCWUserView),
+                                   ('xbel', xbel.XbelView),
+                                   ('xml', xmlrss.XMLView),
+                                   ])
 
     def test_possible_actions_none_rset(self):
         with self.admin_access.web_request() as req:
--- a/web/test/unittest_web.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/test/unittest_web.py	Fri Oct 09 17:52:14 2015 +0200
@@ -126,17 +126,24 @@
         self.assertIn('HttpOnly', webreq.getheader('set-cookie'))
 
 
-class LogQueriesTC(CubicWebServerTC):
+class MiscOptionsTC(CubicWebServerTC):
     @classmethod
     def init_config(cls, config):
-        super(LogQueriesTC, cls).init_config(config)
+        super(MiscOptionsTC, cls).init_config(config)
         cls.logfile = tempfile.NamedTemporaryFile()
         config.global_set_option('query-log-file', cls.logfile.name)
+        config.global_set_option('datadir-url', '//static.testing.fr/')
+        # call load_configuration again to let the config reset its datadir_url
+        config.load_configuration()
 
     def test_log_queries(self):
         self.web_request()
         self.assertTrue(self.logfile.read())
 
+    def test_datadir_url(self):
+        webreq = self.web_request()
+        self.assertNotIn('/data/', webreq.read())
+
     @classmethod
     def tearDownClass(cls):
         cls.logfile.close()
--- a/web/views/ajaxcontroller.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/ajaxcontroller.py	Fri Oct 09 17:52:14 2015 +0200
@@ -427,26 +427,6 @@
     """returns the URL of the external resource named `resource`"""
     return self._cw.uiprops[resource]
 
-@ajaxfunc(output_type='json', check_pageid=True)
-def user_callback(self, cbname):
-    """execute the previously registered user callback `cbname`.
-
-    If matching callback is not found, return None
-    """
-    page_data = self._cw.session.data.get(self._cw.pageid, {})
-    try:
-        cb = page_data[cbname]
-    except KeyError:
-        self.warning('unable to find user callback %s', cbname)
-        return None
-    return cb(self._cw)
-
-
-@ajaxfunc
-def unregister_user_callback(self, cbname):
-    """unregister user callback `cbname`"""
-    self._cw.unregister_callback(self._cw.pageid, cbname)
-
 @ajaxfunc
 def unload_page_data(self):
     """remove user's session data associated to current pageid"""
--- a/web/views/authentication.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/authentication.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -26,9 +26,8 @@
 
 from cubicweb import AuthenticationError, BadConnectionId
 from cubicweb.view import Component
-from cubicweb.dbapi import _repo_connect, ConnectionProperties
 from cubicweb.web import InvalidSession
-from cubicweb.web.application import AbstractAuthenticationManager
+
 
 class NoAuthInfo(Exception): pass
 
@@ -102,6 +101,36 @@
     '("ie" instead of "ei")')
 
 
+class AbstractAuthenticationManager(Component):
+    """authenticate user associated to a request and check session validity"""
+    __abstract__ = True
+    __regid__ = 'authmanager'
+
+    def __init__(self, repo):
+        self.vreg = repo.vreg
+
+    def validate_session(self, req, session):
+        """check session validity, reconnecting it to the repository if the
+        associated connection expired in the repository side (hence the
+        necessity for this method).
+
+        raise :exc:`InvalidSession` if session is corrupted for a reason or
+        another and should be closed
+        """
+        raise NotImplementedError()
+
+    def authenticate(self, req):
+        """authenticate user using connection information found in the request,
+        and return corresponding a :class:`~cubicweb.dbapi.Connection` instance,
+        as well as login and authentication information dictionary used to open
+        the connection.
+
+        raise :exc:`cubicweb.AuthenticationError` if authentication failed
+        (no authentication info found or wrong user/password)
+        """
+        raise NotImplementedError()
+
+
 class RepositoryAuthenticationManager(AbstractAuthenticationManager):
     """authenticate user associated to a request and check session validity"""
 
--- a/web/views/autoform.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/autoform.py	Fri Oct 09 17:52:14 2015 +0200
@@ -746,15 +746,6 @@
     # action on the form tag
     _default_form_action_path = 'validateform'
 
-    @deprecated('[3.18] you should override form_action()')
-    def set_action(self, action):
-        self._action = action
-
-    @deprecated('[3.18] use form_action()')
-    def get_action(self):
-        return self._action
-
-
     @iclassmethod
     def field_by_name(cls_or_self, name, role=None, eschema=None):
         """return field with the given name and role. If field is not explicitly
--- a/web/views/basecomponents.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/basecomponents.py	Fri Oct 09 17:52:14 2015 +0200
@@ -55,7 +55,7 @@
         else:
             rset = self.cw_rset
         # display multilines query as one line
-        rql = rset is not None and rset.printable_rql(encoded=False) or req.form.get('rql', '')
+        rql = rset is not None and rset.printable_rql() or req.form.get('rql', '')
         rql = rql.replace(u"\n", u" ")
         rql_suggestion_comp = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw)
         if rql_suggestion_comp is not None:
--- a/web/views/basecontrollers.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/basecontrollers.py	Fri Oct 09 17:52:14 2015 +0200
@@ -92,7 +92,7 @@
     def publish(self, rset=None):
         """log in the instance"""
         path = self._cw.form.get('postlogin_path', '')
-        # redirect expect a URL, not a path. Also path may contains a query
+        # Redirect expects a URL, not a path. Also path may contain a query
         # string, hence should not be given to _cw.build_url()
         raise Redirect(self._cw.base_url() + path)
 
--- a/web/views/basetemplates.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/basetemplates.py	Fri Oct 09 17:52:14 2015 +0200
@@ -490,7 +490,7 @@
 
 
 class LogFormView(View):
-    # XXX an awfull lot of hardcoded assumptions there
+    # XXX an awful lot of hardcoded assumptions there
     #     makes it unobvious to reuse/specialize
     __regid__ = 'logform'
     __select__ = match_kwargs('id', 'klass')
@@ -514,10 +514,6 @@
         if config['auth-mode'] != 'http':
             self.login_form(id) # Cookie authentication
         w(u'</div>')
-        if self._cw.https and config.anonymous_user()[0] and config['https-deny-anonymous']:
-            path = xml_escape(config['base-url'] + self._cw.relative_path())
-            w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
-              % (path, self._cw._('No account? Try public access at %s') % path))
         w(u'</div>\n')
 
     def login_form(self, id):
--- a/web/views/cwsources.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/cwsources.py	Fri Oct 09 17:52:14 2015 +0200
@@ -231,6 +231,7 @@
     __regid__ = 'cwsource'
     title = _('data sources')
     category = 'manage'
+    order = 100
 
 
 class CWSourcesManagementView(StartupView):
--- a/web/views/debug.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/debug.py	Fri Oct 09 17:52:14 2015 +0200
@@ -24,7 +24,6 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb import BadConnectionId
 from cubicweb.predicates import none_rset, match_user_groups
 from cubicweb.view import StartupView
 from cubicweb.web.views import actions, tabs
@@ -98,6 +97,13 @@
         w(u'<h3>%s</h3>' % _('resources usage'))
         w(u'<table>')
         stats = self._cw.call_service('repo_stats')
+        stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
+        stats['threads'] = ', '.join(sorted(stats['threads']))
+        for k in stats:
+            if k in ('extid_cache_size', 'type_source_cache_size'):
+                continue
+            if k.endswith('_cache_size'):
+                stats[k] = '%s / %s' % (stats[k]['size'], stats[k]['maxsize'])
         for element in sorted(stats):
             w(u'<tr><th align="left">%s</th><td>%s %s</td></tr>'
                    % (element, xml_escape(unicode(stats[element])),
@@ -126,8 +132,8 @@
         w(u'<tr><th align="left">%s</th><td>%s</td></tr>' % (
             _('data directory url'), req.datadir_url))
         w(u'</table>')
-        if req.user.is_in_group('managers'):
-            from cubicweb.web.application import SESSION_MANAGER
+        from cubicweb.web.application import SESSION_MANAGER
+        if SESSION_MANAGER is not None and req.user.is_in_group('managers'):
             sessions = SESSION_MANAGER.current_sessions()
             w(u'<h3>%s</h3>' % _('opened web sessions'))
             if sessions:
@@ -179,31 +185,13 @@
     cache_max_age = 0
 
     def call(self, **kwargs):
-        from cubicweb._gcdebug import gc_info
-        from rql.stmts import Union
-        from cubicweb.appobject import AppObject
-        from cubicweb.rset import ResultSet
-        from cubicweb.dbapi import Connection, Cursor
-        from cubicweb.web.request import CubicWebRequestBase
-        lookupclasses = (AppObject,
-                         Union, ResultSet,
-                         Connection, Cursor,
-                         CubicWebRequestBase)
-        try:
-            from cubicweb.server.session import Session, InternalSession
-            lookupclasses += (InternalSession, Session)
-        except ImportError:
-            pass # no server part installed
+        stats = self._cw.call_service('repo_gc_stats')
         self.w(u'<h2>%s</h2>' % _('Garbage collection information'))
-        counters, ocounters, garbage = gc_info(lookupclasses,
-                                               viewreferrersclasses=())
         self.w(u'<h3>%s</h3>' % self._cw._('Looked up classes'))
-        values = sorted(counters.iteritems(), key=lambda x: x[1], reverse=True)
-        self.wview('pyvaltable', pyvalue=values)
+        self.wview('pyvaltable', pyvalue=stats['lookupclasses'])
         self.w(u'<h3>%s</h3>' % self._cw._('Most referenced classes'))
-        values = sorted(ocounters.iteritems(), key=lambda x: x[1], reverse=True)
-        self.wview('pyvaltable', pyvalue=values[:self._cw.form.get('nb', 20)])
-        if garbage:
+        self.wview('pyvaltable', pyvalue=stats['referenced'])
+        if stats['unreachable']:
             self.w(u'<h3>%s</h3>' % self._cw._('Unreachable objects'))
-            values = sorted(xml_escape(repr(o)) for o in garbage)
+            values = [xml_escape(val) for val in stats['unreachable']]
             self.wview('pyvallist', pyvalue=values)
--- a/web/views/formrenderers.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/formrenderers.py	Fri Oct 09 17:52:14 2015 +0200
@@ -196,7 +196,7 @@
         if form.cssclass:
             attrs.setdefault('class', form.cssclass)
         if form.cwtarget:
-            attrs.setdefault('cubicweb:target', form.cwtarget)
+            attrs.setdefault('target', form.cwtarget)
         if not form.autocomplete:
             attrs.setdefault('autocomplete', 'off')
         return '<form %s>' % uilib.sgml_attributes(attrs)
@@ -206,7 +206,13 @@
         for form renderers overriding open_form to use something else or more than
         and <form>
         """
-        return u'</form>'
+        out = u'</form>'
+        if form.cwtarget:
+            attrs = {'name': form.cwtarget, 'id': form.cwtarget,
+                     'width': '0px', 'height': '0px',
+                     'src': 'javascript: void(0);'}
+            out =  (u'<iframe %s></iframe>\n' % uilib.sgml_attributes(attrs)) + out
+        return out
 
     def render_fields(self, w, form, values):
         fields = self._render_hidden_fields(w, form)
--- a/web/views/forms.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/forms.py	Fri Oct 09 17:52:14 2015 +0200
@@ -95,7 +95,7 @@
       value for the "style" attribute of the <form> tag
 
     :attr:`cwtarget`
-      value for the "cubicweb:target" attribute of the <form> tag
+      value for the "target" attribute of the <form> tag
 
     :attr:`redirect_path`
       relative to redirect to after submitting the form
@@ -241,10 +241,7 @@
 
     _default_form_action_path = 'edit'
     def form_action(self):
-        try:
-            action = self.get_action() # avoid spurious warning w/ autoform bw compat property
-        except AttributeError:
-            action = self.action
+        action = self.action
         if action is None:
             return self._cw.build_url(self._default_form_action_path)
         return action
--- a/web/views/management.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/management.py	Fri Oct 09 17:52:14 2015 +0200
@@ -187,6 +187,13 @@
 
     def call(self):
         stats = self._cw.call_service('repo_stats')
+        stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
+        stats['threads'] = ', '.join(sorted(stats['threads']))
+        for k in stats:
+            if k in ('extid_cache_size', 'type_source_cache_size'):
+                continue
+            if k.endswith('_cache_size'):
+                stats[k] = '%s / %s' % (stats[k]['size'], stats[k]['maxsize'])
         results = []
         for element in stats:
             results.append(u'%s %s' % (element, stats[element]))
--- a/web/views/rdf.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/rdf.py	Fri Oct 09 17:52:14 2015 +0200
@@ -47,6 +47,8 @@
         __regid__ = 'rdf'
         title = _('rdf export')
         templatable = False
+        binary = True
+        format = 'xml'
         content_type = 'text/xml' # +rdf
 
         def call(self):
@@ -57,7 +59,7 @@
             for i in xrange(self.cw_rset.rowcount):
                 entity = self.cw_rset.complete_entity(i, 0)
                 self.entity2graph(graph, entity)
-            self.w(graph.serialize().decode('utf-8'))
+            self.w(graph.serialize(format=self.format))
 
         def entity_call(self, entity):
             self.call()
@@ -100,3 +102,8 @@
                             else:
                                 add( (URIRef(related.cwuri), CW[rtype], cwuri) )
 
+
+    class RDFN3View(RDFView):
+        __regid__ = 'n3rdf'
+        format = 'n3'
+        content_type = 'text/n3'
--- a/web/views/sessions.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/sessions.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,20 +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/>.
-"""web session component: by dfault the session is actually the db connection
-object :/
-"""
-
+"""web session: by default the session is actually the db connection """
 __docformat__ = "restructuredtext en"
 
 from time import time
 
-from cubicweb import (RepositoryError, Unauthorized, AuthenticationError,
-                      BadConnectionId)
-from cubicweb.web import InvalidSession, Redirect
-from cubicweb.web.application import AbstractSessionManager
-from cubicweb.dbapi import ProgrammingError, DBAPISession
-from cubicweb import repoapi
+from cubicweb import RepositoryError, Unauthorized, BadConnectionId
+from cubicweb.web import InvalidSession, component
+
+
+class AbstractSessionManager(component.Component):
+    """manage session data associated to a session identifier"""
+    __abstract__ = True
+    __regid__ = 'sessionmanager'
+
+    def __init__(self, repo):
+        vreg = repo.vreg
+        self.session_time = vreg.config['http-session-time'] or None
+        self.authmanager = vreg['components'].select('authmanager', repo=repo)
+        interval = (self.session_time or 0) / 2.
+        if vreg.config.anonymous_user()[0] is not None:
+            self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
+            assert self.cleanup_anon_session_time > 0
+            if self.session_time is not None:
+                self.cleanup_anon_session_time = min(self.session_time,
+                                                     self.cleanup_anon_session_time)
+            interval = self.cleanup_anon_session_time / 2.
+        # we don't want to check session more than once every 5 minutes
+        self.clean_sessions_interval = max(5 * 60, interval)
+
+    def clean_sessions(self):
+        """cleanup sessions which has not been unused since a given amount of
+        time. Return the number of sessions which have been closed.
+        """
+        self.debug('cleaning http sessions')
+        session_time = self.session_time
+        closed, total = 0, 0
+        for session in self.current_sessions():
+            total += 1
+            try:
+                last_usage_time = session.cnx.check()
+            except AttributeError:
+                last_usage_time = session.mtime
+            except BadConnectionId:
+                self.close_session(session)
+                closed += 1
+                continue
+
+            no_use_time = (time() - last_usage_time)
+            if session.anonymous_session:
+                if no_use_time >= self.cleanup_anon_session_time:
+                    self.close_session(session)
+                    closed += 1
+            elif session_time is not None and no_use_time >= session_time:
+                self.close_session(session)
+                closed += 1
+        return closed, total - closed
+
+    def current_sessions(self):
+        """return currently open sessions"""
+        raise NotImplementedError()
+
+    def get_session(self, req, sessionid):
+        """return existing session for the given session identifier"""
+        raise NotImplementedError()
+
+    def open_session(self, req):
+        """open and return a new session for the given request.
+
+        raise :exc:`cubicweb.AuthenticationError` if authentication failed
+        (no authentication info found or wrong user/password)
+        """
+        raise NotImplementedError()
+
+    def close_session(self, session):
+        """close session on logout or on invalid session detected (expired out,
+        corrupted...)
+        """
+        raise NotImplementedError()
 
 
 class InMemoryRepositorySessionManager(AbstractSessionManager):
@@ -97,8 +161,7 @@
         # XXX should properly detect missing permission / non writeable source
         # and avoid "except (RepositoryError, Unauthorized)" below
         try:
-            cnx = repoapi.ClientConnection(session)
-            with cnx:
+            with session.new_cnx() as cnx:
                 cnx.execute('SET X last_login_time NOW WHERE X eid %(x)s',
                            {'x' : session.user.eid})
                 cnx.commit()
--- a/web/views/staticcontrollers.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/staticcontrollers.py	Fri Oct 09 17:52:14 2015 +0200
@@ -188,7 +188,6 @@
     def __init__(self, *args, **kwargs):
         super(DataController, self).__init__(*args, **kwargs)
         config = self._cw.vreg.config
-        md5_version = config.instance_md5_version()
         self.base_datapath = config.data_relpath()
         self.data_modconcat_basepath = '%s??' % self.base_datapath
         self.concat_files_registry = ConcatFilesHandler(config)
--- a/web/views/tableview.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/tableview.py	Fri Oct 09 17:52:14 2015 +0200
@@ -992,8 +992,10 @@
 
     @cachedproperty
     def initial_load(self):
-        """We detect a bit heuristically if we are built for the first time of
-        from subsequent calls by the form filter or by the pagination hooks
+        """We detect a bit heuristically if we are built for the first time or
+        from subsequent calls by the form filter or by the pagination
+        hooks.
+
         """
         form = self._cw.form
         return 'fromformfilter' not in form and '__start' not in form
--- a/web/views/timeline.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/timeline.py	Fri Oct 09 17:52:14 2015 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,124 +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/>.
-"""basic support for SIMILE's timeline 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.predicates import adaptable
-from cubicweb.view import EntityView, StartupView
-from cubicweb.utils import json_dumps
-
-_ = unicode
-
-class TimelineJsonView(EntityView):
-    """generates a json file to feed Timeline.loadJSON()
-    NOTE: work in progress (image_url, bubbleUrl and so on
-    should be properties of entity classes or subviews)
-    """
-    __regid__ = 'timeline-json'
-    __select__ = adaptable('ICalendarable')
-
-    binary = True
-    templatable = False
-    content_type = 'application/json'
-
-    date_fmt = '%Y/%m/%d'
-
-    def call(self):
-        events = []
-        for entity in self.cw_rset.entities():
-            event = self.build_event(entity)
-            if event is not None:
-                events.append(event)
-        timeline_data = {'dateTimeFormat': self.date_fmt,
-                         'events': events}
-        self.w(json_dumps(timeline_data))
-
-    # FIXME: those properties should be defined by the entity class
-    def onclick_url(self, entity):
-        return entity.absolute_url()
-
-    def onclick(self, entity):
-        url = self.onclick_url(entity)
-        if url:
-            return u"javascript: document.location.href='%s'" % url
-        return None
+try:
+    from cubes.timeline.views import (
+            TimelineJsonView,
+            TimelineViewMixIn,
+            TimelineView,
+            StaticTimelineView)
 
-    def build_event(self, entity):
-        """converts `entity` into a JSON object
-        {'start': '1891',
-        'end': '1915',
-        'title': 'Portrait of Horace Brodsky',
-        'description': 'by Henri Gaudier-Brzeska, French Sculptor, 1891-1915',
-        'image': 'http://imagecache2.allposters.com/images/BRGPOD/102770_b.jpg',
-        'link': 'http://www.allposters.com/-sp/Portrait-of-Horace-Brodsky-Posters_i1584413_.htm'
-        }
-        """
-        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': ustrftime(start, self.date_fmt),
-                      'title': xml_escape(entity.dc_title()),
-                      'description': entity.dc_description(format='text/html'),
-                      'link': entity.absolute_url(),
-                      }
-        onclick = self.onclick(entity)
-        if onclick:
-            event_data['onclick'] = onclick
-        if stop:
-            event_data['end'] = ustrftime(stop, self.date_fmt)
-        return event_data
-
-
-class TimelineViewMixIn(object):
-    widget_class = 'TimelineWidget'
-    jsfiles = ('cubicweb.timeline-bundle.js', 'cubicweb.widgets.js',
-               'cubicweb.timeline-ext.js', 'cubicweb.ajax.js')
+except ImportError:
+    pass
+else:
+    from logilab.common.deprecation import class_moved
 
-    def render_url(self, loadurl, tlunit=None):
-        tlunit = tlunit or self._cw.form.get('tlunit')
-        self._cw.add_js(self.jsfiles)
-        self._cw.add_css('timeline-bundle.css')
-        if tlunit:
-            additional = u' cubicweb:tlunit="%s"' % tlunit
-        else:
-            additional = u''
-        self.w(u'<div class="widget" cubicweb:wdgtype="%s" '
-               u'cubicweb:loadtype="auto" cubicweb:loadurl="%s" %s >' %
-               (self.widget_class, xml_escape(loadurl),
-                additional))
-        self.w(u'</div>')
-
-
-class TimelineView(TimelineViewMixIn, EntityView):
-    """builds a cubicweb timeline widget node"""
-    __regid__ = 'timeline'
-    title = _('timeline')
-    __select__ = adaptable('ICalendarable')
-    paginable = False
-    def call(self, tlunit=None):
-        self._cw.html_headers.define_var('Timeline_urlPrefix', self._cw.datadir_url)
-        rql = self.cw_rset.printable_rql()
-        loadurl = self._cw.build_url(rql=rql, vid='timeline-json')
-        self.render_url(loadurl, tlunit)
-
-
-class StaticTimelineView(TimelineViewMixIn, StartupView):
-    """similar to `TimelineView` but loads data from a static
-    JSON file instead of one after a RQL query.
-    """
-    __regid__ = 'static-timeline'
-
-    def call(self, loadurl, tlunit=None, wdgclass=None):
-        self.widget_class = wdgclass or self.widget_class
-        self.render_url(loadurl, tlunit)
+    TimelineJsonView = class_moved(TimelineJsonView, 'TimelineJsonView')
+    TimelineViewMixIn = class_moved(TimelineViewMixIn, 'TimelineViewMixIn')
+    TimelineView = class_moved(TimelineView, 'TimelineView')
+    StaticTimelineView = class_moved(StaticTimelineView, 'StaticTimelineView')
--- a/web/views/urlpublishing.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/urlpublishing.py	Fri Oct 09 17:52:14 2015 +0200
@@ -97,7 +97,7 @@
         self.evaluators = sorted(evaluators, key=lambda x: x.priority)
 
     def process(self, req, path):
-        """Given a URL (essentialy caracterized by a path on the
+        """Given a URL (essentially characterized by a path on the
         server, but additional information may be found in the request
         object), return a publishing method identifier
         (e.g. controller) and an optional result set.
--- a/web/views/xmlrss.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/views/xmlrss.py	Fri Oct 09 17:52:14 2015 +0200
@@ -79,7 +79,7 @@
         self.w(u'<%s eid="%s" cwuri="%s" cwsource="%s">\n'
                % (entity.cw_etype, entity.eid, xml_escape(entity.cwuri),
                   xml_escape(source)))
-        for rschema, attrschema in entity.e_schema.attribute_definitions():
+        for rschema, attrschema in sorted(entity.e_schema.attribute_definitions()):
             attr = rschema.type
             if attr in ('eid', 'cwuri'):
                 continue
--- a/web/webconfig.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/webconfig.py	Fri Oct 09 17:52:14 2015 +0200
@@ -49,7 +49,7 @@
       }),
     # user web ui configuration
     ('fckeditor',
-     {'type' : 'yn', 'default': True,
+     {'type' : 'yn', 'default': False,
       'help': _('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.'),
@@ -124,16 +124,13 @@
           'where the cubicweb web server is listening on port 8080.',
           'group': 'main', 'level': 3,
           }),
-        ('https-deny-anonymous',
-         {'type': 'yn',
-          'default': False,
-          'help': 'Prevent anonymous user to browse through https version of '
-                  'the site (https-url). Login form will then be displayed '
-                  'until logged',
+        ('datadir-url',
+         {'type': 'string', 'default': None,
+          'help': ('base url for static data, if different from "${base-url}/data/".  '
+                   'If served from a different domain, that domain should allow '
+                   'cross-origin requests.'),
           'group': 'web',
-          'level': 2
-         }
-          ),
+          }),
         ('auth-mode',
          {'type' : 'choice',
           'choices' : ('cookie', 'http'),
@@ -378,9 +375,9 @@
             if exists(fpath):
                 yield join(fpath)
 
-    def load_configuration(self):
+    def load_configuration(self, **kw):
         """load instance's configuration files"""
-        super(WebConfiguration, self).load_configuration()
+        super(WebConfiguration, self).load_configuration(**kw)
         # load external resources definition
         self._init_base_url()
         self._build_ui_properties()
@@ -392,6 +389,14 @@
             baseurl += '/'
         if not (self.repairing or self.creating):
             self.global_set_option('base-url', baseurl)
+        self.datadir_url = self['datadir-url']
+        if self.datadir_url:
+            if self.datadir_url[-1] != '/':
+                self.datadir_url += '/'
+            if self.mode != 'test':
+                self.datadir_url += '%s/' % self.instance_md5_version()
+            self.https_datadir_url = self.datadir_url
+            return
         httpsurl = self['https-url']
         data_relpath = self.data_relpath()
         if httpsurl:
@@ -477,8 +482,3 @@
     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/web/webctl.py	Fri Oct 09 09:40:08 2015 +0200
+++ b/web/webctl.py	Fri Oct 09 17:52:14 2015 +0200
@@ -46,9 +46,6 @@
         if not automatic:
             print '\n' + underline_title('Generic web configuration')
             config = self.config
-            if config['repository-uri'].startswith('pyro://') or config.pyro_enabled():
-                print '\n' + underline_title('Pyro configuration')
-                config.input_config('pyro', inputlevel)
             config.input_config('web', inputlevel)
             if ASK.confirm('Allow anonymous access ?', False):
                 config.global_set_option('anonymous-user', 'anon')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wsgi/test/requirements.txt	Fri Oct 09 17:52:14 2015 +0200
@@ -0,0 +1,1 @@
+webtest
--- a/zmqclient.py	Fri Oct 09 09:40:08 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Source to query another RQL repository using pyro"""
-
-__docformat__ = "restructuredtext en"
-_ = unicode
-
-from functools import partial
-import zmq
-
-from cubicweb.server.cwzmq import cwproto_to_zmqaddr
-
-# XXX hack to overpass old zmq limitation that force to have
-# only one context per python process
-try:
-    from cubicweb.server.cwzmq import ctx
-except ImportError:
-    ctx = zmq.Context()
-
-class ZMQRepositoryClient(object):
-    """
-    This class delegates the overall repository stuff to a remote source.
-
-    So calling a method of this repository will result on calling the
-    corresponding method of the remote source repository.
-
-    Any raised exception on the remote source is propagated locally.
-
-    ZMQ is used as the transport layer and cPickle is used to serialize data.
-    """
-
-    def __init__(self, zmq_address):
-        """A zmq address provided here will be like
-        `zmqpickle-tcp://127.0.0.1:42000`.  W
-
-        We chop the prefix to get a real zmq address.
-        """
-        self.socket = ctx.socket(zmq.REQ)
-        self.socket.connect(cwproto_to_zmqaddr(zmq_address))
-
-    def __zmqcall__(self, name, *args, **kwargs):
-         self.socket.send_pyobj([name, args, kwargs])
-         result = self.socket.recv_pyobj()
-         if isinstance(result, BaseException):
-             raise result
-         return result
-
-    def __getattr__(self, name):
-        return partial(self.__zmqcall__, name)