merge 3.19.8 into 3.20 branch 3.20.2 centos/3.20.2-1 cubicweb-centos-version-3.20.2-1 cubicweb-debian-version-3.20.2-1 cubicweb-version-3.20.2 debian/3.20.2-1
authorJulien Cristau <julien.cristau@logilab.fr>
Thu, 22 Jan 2015 17:45:06 +0100
changeset 10161 138464fc1c33
parent 10160 b733789cc339 (diff)
parent 10159 5cc8fdba56d5 (current diff)
child 10162 1f68574c3d0b
merge 3.19.8 into 3.20 branch
.hgtags
__pkginfo__.py
cubicweb.spec
debian/changelog
server/test/unittest_security.py
--- a/.hgtags	Thu Jan 22 17:39:07 2015 +0100
+++ b/.hgtags	Thu Jan 22 17:45:06 2015 +0100
@@ -395,3 +395,9 @@
 efc8645ece4300958e3628db81464fef12d5f6e8 cubicweb-version-3.19.8
 efc8645ece4300958e3628db81464fef12d5f6e8 cubicweb-debian-version-3.19.8-1
 efc8645ece4300958e3628db81464fef12d5f6e8 cubicweb-centos-version-3.19.8-1
+7e6b7739afe6128589ad51b0318decb767cbae36 cubicweb-version-3.20.0
+7e6b7739afe6128589ad51b0318decb767cbae36 cubicweb-debian-version-3.20.0-1
+7e6b7739afe6128589ad51b0318decb767cbae36 cubicweb-centos-version-3.20.0-1
+43eef610ef11673d01750459356aec5a96174ca0 cubicweb-version-3.20.1
+43eef610ef11673d01750459356aec5a96174ca0 cubicweb-debian-version-3.20.1-1
+43eef610ef11673d01750459356aec5a96174ca0 cubicweb-centos-version-3.20.1-1
--- a/MANIFEST.in	Thu Jan 22 17:39:07 2015 +0100
+++ b/MANIFEST.in	Thu Jan 22 17:45:06 2015 +0100
@@ -5,7 +5,10 @@
 include bin/cubicweb-*
 include man/cubicweb-ctl.1
 
-recursive-include doc README makefile *.conf *.js *.css *.py *.rst *.txt *.html *.png *.svg *.zargo *.dia
+include doc/*.rst
+recursive-include doc/book *
+recursive-include doc/tools *.py
+recursive-include doc/tutorials *.rst *.py
 
 recursive-include misc *.py *.png *.display
 
@@ -35,3 +38,5 @@
 prune doc/html/_sources/
 prune misc/cwfs
 prune goa
+prune doc/book/en/devweb/js_api
+global-exclude *.pyc
--- a/__init__.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/__init__.py	Thu Jan 22 17:45:06 2015 +0100
@@ -42,12 +42,10 @@
 
 from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods
-from yams.constraints import BASE_CONVERTERS
+from yams.constraints import BASE_CONVERTERS, BASE_CHECKERS
 
-if os.environ.get('APYCOT_ROOT'):
-    logging.basicConfig(level=logging.CRITICAL)
-else:
-    logging.basicConfig()
+# pre python 2.7.2 safety
+logging.basicConfig()
 
 from cubicweb.__pkginfo__ import version as __version__
 
@@ -142,6 +140,10 @@
         return cPickle.loads(zlib.decompress(self.getvalue()))
 
 
+def check_password(eschema, value):
+    return isinstance(value, (str, Binary))
+BASE_CHECKERS['Password'] = check_password
+
 def str_or_binary(value):
     if isinstance(value, Binary):
         return value
--- a/__pkginfo__.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/__pkginfo__.py	Thu Jan 22 17:45:06 2015 +0100
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 19, 8)
+numversion = (3, 20, 2)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -39,18 +39,18 @@
 ]
 
 __depends__ = {
-    'logilab-common': '>= 0.62.0',
+    'logilab-common': '>= 0.63.1',
     'logilab-mtconverter': '>= 0.8.0',
     'rql': '>= 0.31.2',
-    'yams': '>= 0.39.1, < 0.39.99',  # CW 3.19 is not compatible with yams 0.40
+    'yams': '>= 0.40.0',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependencies
     'lxml': '',
-    'Twisted': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '>= 1.12.1',
+    'logilab-database': '>= 1.13.0',
     'passlib': '',
+    'Markdown': ''
     }
 
 __recommends__ = {
@@ -62,6 +62,7 @@
     'vobject': '>= 0.6.0',      # for ical view
     'rdflib': None,             #
     'pyzmq': None,
+    'Twisted': '',
     #'Products.FCKeditor':'',
     #'SimpleTAL':'>= 4.1.6',
     }
@@ -106,7 +107,7 @@
     data_files = [
         # server data
         [join('share', 'cubicweb', 'schemas'),
-         [join('schemas', filename) for filename in listdir('schemas')]],
+         glob.glob(join('schemas', '*.sql'))],
         [join('share', 'cubicweb', 'migration'),
          [join(_server_migration_dir, filename)
           for filename in listdir(_server_migration_dir)]],
@@ -124,7 +125,7 @@
         [join('share', 'cubicweb', 'cubes', 'shared', 'wdoc', 'images'),
          [join(_wdocimages_dir, fname) for fname in listdir(_wdocimages_dir)]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'i18n'),
-         [join(_i18n_dir, fname) for fname in listdir(_i18n_dir)]],
+         glob.glob(join(_i18n_dir, '*.po'))],
         # skeleton
         ]
 except OSError:
--- a/cubicweb.spec	Thu Jan 22 17:39:07 2015 +0100
+++ b/cubicweb.spec	Thu Jan 22 17:45:06 2015 +0100
@@ -7,7 +7,7 @@
 %endif
 
 Name:           cubicweb
-Version:        3.19.8
+Version:        3.20.2
 Release:        logilab.1%{?dist}
 Summary:        CubicWeb is a semantic web application framework
 Source0:        http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz
@@ -20,14 +20,15 @@
 BuildArch:      noarch
 
 Requires:       %{python}
-Requires:       %{python}-logilab-common >= 0.62.0
+Requires:       %{python}-logilab-common >= 0.63.1
 Requires:       %{python}-logilab-mtconverter >= 0.8.0
 Requires:       %{python}-rql >= 0.31.2
-Requires:       %{python}-yams >= 0.39.1
-Requires:       %{python}-logilab-database >= 1.12.1
+Requires:       %{python}-yams >= 0.40.0
+Requires:       %{python}-logilab-database >= 1.13.0
 Requires:       %{python}-passlib
 Requires:       %{python}-lxml
 Requires:       %{python}-twisted-web
+Requires:       %{python}-markdown
 # the schema view uses `dot'; at least on el5, png output requires graphviz-gd
 Requires:       graphviz-gd
 Requires:       gettext
--- a/cwconfig.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/cwconfig.py	Thu Jan 22 17:45:06 2015 +0100
@@ -278,7 +278,7 @@
       }),
     ('default-text-format',
      {'type' : 'choice',
-      'choices': ('text/plain', 'text/rest', 'text/html'),
+      'choices': ('text/plain', 'text/rest', 'text/html', 'text/markdown'),
       'default': 'text/html', # use fckeditor in the web ui
       'help': _('default text format for rich text fields.'),
       'group': 'ui',
@@ -329,6 +329,8 @@
     # nor remove appobjects based on unused interface [???]
     cleanup_unused_appobjects = True
 
+    quick_start = False
+
     if (CWDEV and _forced_mode != 'system'):
         mode = 'user'
         _CUBES_DIR = join(CW_SOFTWARE_ROOT, '../cubes')
@@ -827,13 +829,6 @@
     else:
         _INSTANCES_DIR = join(_INSTALL_PREFIX, 'etc', 'cubicweb.d')
 
-    if os.environ.get('APYCOT_ROOT'):
-        _cubes_init = join(CubicWebNoAppConfiguration.CUBES_DIR, '__init__.py')
-        if not exists(_cubes_init):
-            file(join(_cubes_init), 'w').close()
-        if not exists(_INSTANCES_DIR):
-            os.makedirs(_INSTANCES_DIR)
-
     # set to true during repair (shell, migration) to allow some things which
     # wouldn't be possible otherwise
     repairing = False
--- a/cwctl.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/cwctl.py	Thu Jan 22 17:45:06 2015 +0100
@@ -524,6 +524,15 @@
 
     def start_instance(self, appid):
         """start the instance's server"""
+        try:
+            import twisted  # noqa
+        except ImportError:
+            msg = (
+                "Twisted is required by the 'start' command\n"
+                "Either install it, or use one of the alternative commands:\n"
+                "- '{ctl} wsgi {appid}'\n"
+                "- '{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'])
         init_cmdline_log_threshold(config, self['loglevel'])
         if self['profile']:
@@ -836,6 +845,8 @@
         config = cwcfg.config_for(appid)
         # should not raise error if db versions don't match fs versions
         config.repairing = True
+        # no need to load all appobjects and schema
+        config.quick_start = True
         if hasattr(config, 'set_sources_mode'):
             config.set_sources_mode(('migration',))
         repo = config.migration_handler().repo_connect()
@@ -1042,19 +1053,36 @@
 
 # WSGI #########
 
+WSGI_CHOICES = {}
+from cubicweb.wsgi import server as stdlib_server
+WSGI_CHOICES['stdlib'] = stdlib_server
+try:
+    from cubicweb.wsgi import wz
+except ImportError:
+    pass
+else:
+    WSGI_CHOICES['werkzeug'] = wz
+try:
+    from cubicweb.wsgi import tnd
+except ImportError:
+    pass
+else:
+    WSGI_CHOICES['tornado'] = tnd
+
+
 def wsgichoices():
-    try:
-        from werkzeug import serving
-    except ImportError:
-        return ('stdlib',)
-    return ('stdlib', 'werkzeug')
+    return tuple(WSGI_CHOICES)
+
 
 class WSGIStartHandler(InstanceCommand):
     """Start an interactive wsgi server """
     name = 'wsgi'
     actionverb = 'started'
     arguments = '<instance>'
-    options = (
+
+    @property
+    def options(self):
+        return (
         ("debug",
          {'short': 'D', 'action': 'store_true',
           'default': False,
@@ -1081,10 +1109,7 @@
         init_cmdline_log_threshold(config, self['loglevel'])
         assert config.name == 'all-in-one'
         meth = self['method']
-        if meth == 'stdlib':
-            from cubicweb.wsgi import server
-        else:
-            from cubicweb.wsgi import wz as server
+        server = WSGI_CHOICES[meth]
         return server.run(config)
 
 
--- a/dataimport.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/dataimport.py	Thu Jan 22 17:45:06 2015 +0100
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# 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.
@@ -49,12 +49,7 @@
   GENERATORS.append( (gen_users, CHK) )
 
   # create controller
-  if 'cnx' in globals():
-      ctl = CWImportController(RQLObjectStore(cnx))
-  else:
-      print 'debug mode (not connected)'
-      print 'run through cubicweb-ctl shell to access an instance'
-      ctl = CWImportController(ObjectStore())
+  ctl = CWImportController(RQLObjectStore(cnx))
   ctl.askerror = 1
   ctl.generators = GENERATORS
   ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
@@ -76,7 +71,7 @@
 import inspect
 from collections import defaultdict
 from copy import copy
-from datetime import date, datetime
+from datetime import date, datetime, time
 from time import asctime
 from StringIO import StringIO
 
@@ -104,9 +99,16 @@
     f.seek(0)
     return i+1
 
-def ucsvreader_pb(stream_or_path, encoding='utf-8', separator=',', quote='"',
-                  skipfirst=False, withpb=True, skip_empty=True):
+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)
@@ -118,15 +120,16 @@
         rowcount -= 1
     if withpb:
         pb = shellutils.ProgressBar(rowcount, 50)
-    for urow in ucsvreader(stream, encoding, separator, quote,
+    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', separator=',', quote='"',
-               skipfirst=False, ignore_errors=False, skip_empty=True):
+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
 
@@ -134,7 +137,13 @@
     separators) will be skipped. This is useful for Excel exports which may be
     full of such lines.
     """
-    it = iter(csv.reader(stream, delimiter=separator, quotechar=quote))
+    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()
@@ -370,7 +379,7 @@
                                columns, encoding='utf-8'):
     """ Execute thread with copy from
     """
-    buf = _create_copyfrom_buffer(data, columns, encoding)
+    buf = _create_copyfrom_buffer(data, columns, encoding=encoding)
     if buf is None:
         _execmany_thread_not_copy_from(cu, statement, data)
     else:
@@ -425,16 +434,87 @@
         cnx.commit()
         cu.close()
 
-def _create_copyfrom_buffer(data, columns, encoding='utf-8', replace_sep=None):
+
+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...
+    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 isinstance(data[0], (tuple, list)):
-        columns = range(len(data[0]))
+    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.
@@ -444,43 +524,19 @@
             try:
                 value = row[col]
             except KeyError:
-                warnings.warn(u"Column %s is not accessible in row %s" 
+                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 
+                # 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
-            if value is None:
-                value = 'NULL'
-            elif isinstance(value, (long, int, float)):
-                value = str(value)
-            elif isinstance(value, (str, unicode)):
-                # 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 instead of returning None
-                        # (and thus avoid empty buffer)
-                        if replace_sep:
-                            value = value.replace(_char, replace_sep)
-                        else:
-                            return
-                value = value.replace('\\', r'\\')
-                if value is None:
-                    return
-                if isinstance(value, unicode):
-                    value = value.encode(encoding)
-            elif isinstance(value, (date, datetime)):
-                value = '%04d-%02d-%02d' % (value.year,
-                                            value.month,
-                                            value.day)
-                if isinstance(value, datetime):
-                    value += ' %02d:%02d:%02d' % (value.hour,
-                                                  value.minutes,
-                                                  value.second)
+            for types, converter in _COPYFROM_BUFFER_CONVERTERS:
+                if isinstance(value, types):
+                    value = converter(value, **convert_opts)
+                    break
             else:
-                return None
+                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)
@@ -506,27 +562,15 @@
         self.types = {}
         self.relations = set()
         self.indexes = {}
-        self._rql = None
-        self._commit = None
-
-    def _put(self, type, item):
-        self.items.append(item)
-        return len(self.items) - 1
 
     def create_entity(self, etype, **data):
         data = attrdict(data)
-        data['eid'] = eid = self._put(etype, data)
+        data['eid'] = eid = len(self.items)
+        self.items.append(data)
         self.eids[eid] = data
         self.types.setdefault(etype, []).append(eid)
         return data
 
-    @deprecated("[3.11] add is deprecated, use create_entity instead")
-    def add(self, etype, item):
-        assert isinstance(item, dict), 'item is not a dict but a %s' % type(item)
-        data = self.create_entity(etype, **item)
-        item['eid'] = data['eid']
-        return item
-
     def relate(self, eid_from, rtype, eid_to, **kwargs):
         """Add new relation"""
         relation = eid_from, rtype, eid_to
@@ -534,32 +578,12 @@
         return relation
 
     def commit(self):
-        """this commit method do nothing by default
-
-        This is voluntary to use the frequent autocommit feature in CubicWeb
-        when you are using hooks or another
-
-        If you want override commit method, please set it by the
-        constructor
-        """
-        pass
+        """this commit method does nothing by default"""
+        return
 
     def flush(self):
-        """The method is provided so that all stores share a common API.
-        It just tries to call the commit method.
-        """
-        print 'starting flush'
-        try:
-            self.commit()
-        except:
-            print 'failed to flush'
-        else:
-            print 'flush done'
-
-    def rql(self, *args):
-        if self._rql is not None:
-            return self._rql(*args)
-        return []
+        """The method is provided so that all stores share a common API"""
+        pass
 
     @property
     def nb_inserted_entities(self):
@@ -573,62 +597,47 @@
 
 class RQLObjectStore(ObjectStore):
     """ObjectStore that works with an actual RQL repository (production mode)"""
-    _rql = None # bw compat
 
-    def __init__(self, session=None, commit=None):
-        ObjectStore.__init__(self)
-        if session is None:
-            sys.exit('please provide a session of run this script with cubicweb-ctl shell and pass cnx as session')
-        if not hasattr(session, 'set_cnxset'):
-            if hasattr(session, 'request'):
-                # connection object
-                cnx = session
-                session = session.request()
-            else: # object is already a request
-                cnx = session.cnx
-            session.set_cnxset = lambda : None
-            commit = commit or cnx.commit
-        else:
-            session.set_cnxset()
-        self.session = session
-        self._commit = commit or session.commit
+    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):
-        txuuid = self._commit()
-        self.session.set_cnxset()
-        return txuuid
+        return self._commit()
 
     def rql(self, *args):
-        if self._rql is not None:
-            return self._rql(*args)
-        return self.session.execute(*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.session.create_entity(*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 _put(self, type, item):
-        query = 'INSERT %s X' % type
-        if item:
-            query += ': ' + ', '.join('X %s %%(%s)s' % (k, k)
-                                      for k in item)
-        return self.rql(query, item)[0][0]
-
     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 session.find(*args, **kwargs).entities() instead")
+    @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
     def find_entities(self, *args, **kwargs):
-        return self.session.find(*args, **kwargs).entities()
+        return self._cnx.find(*args, **kwargs).entities()
 
-    @deprecated("[3.19] use session.find(*args, **kwargs).one() instead")
+    @deprecated("[3.19] use cnx.find(*args, **kwargs).one() instead")
     def find_one_entity(self, *args, **kwargs):
-        return self.session.find(*args, **kwargs).one()
+        return self._cnx.find(*args, **kwargs).one()
 
 # the import controller ########################################################
 
@@ -755,7 +764,6 @@
 
 class NoHookRQLObjectStore(RQLObjectStore):
     """ObjectStore that works with an actual RQL repository (production mode)"""
-    _rql = None # bw compat
 
     def __init__(self, session, metagen=None, baseurl=None):
         super(NoHookRQLObjectStore, self).__init__(session)
@@ -768,7 +776,6 @@
         self._nb_inserted_entities = 0
         self._nb_inserted_types = 0
         self._nb_inserted_relations = 0
-        self.rql = session.execute
         # deactivate security
         session.read_security = False
         session.write_security = False
@@ -821,9 +828,6 @@
     def nb_inserted_relations(self):
         return self._nb_inserted_relations
 
-    def _put(self, type, item):
-        raise RuntimeError('use create entity')
-
 
 class MetaGenerator(object):
     META_RELATIONS = (META_RTYPES
@@ -1056,10 +1060,6 @@
                                nb_threads=self.nb_threads_statement,
                                support_copy_from=self.support_copy_from,
                                encoding=self.dbencoding)
-        except:
-            print 'failed to flush'
-        else:
-            print 'flush done'
         finally:
             _entities_sql.clear()
             _relations_sql.clear()
--- a/dbapi.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/dbapi.py	Thu Jan 22 17:45:06 2015 +0100
@@ -447,11 +447,8 @@
 class LogCursor(Cursor):
     """override the standard cursor to log executed queries"""
 
-    def execute(self, operation, parameters=None, eid_key=None, build_descr=True):
+    def execute(self, operation, parameters=None, build_descr=True):
         """override the standard cursor to log executed queries"""
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
         tstart, cstart = time(), clock()
         rset = Cursor.execute(self, operation, parameters, build_descr=build_descr)
         self.connection.executed_queries.append((operation, parameters,
--- a/debian/changelog	Thu Jan 22 17:39:07 2015 +0100
+++ b/debian/changelog	Thu Jan 22 17:45:06 2015 +0100
@@ -1,3 +1,21 @@
+cubicweb (3.20.2-1) unstable; urgency=medium
+
+  * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Thu, 08 Jan 2015 12:20:13 +0100
+
+cubicweb (3.20.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Wed, 07 Jan 2015 15:24:24 +0100
+
+cubicweb (3.20.0-1) unstable; urgency=medium
+
+  * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Tue, 06 Jan 2015 18:11:03 +0100
+
 cubicweb (3.19.8-1) unstable; urgency=medium
 
   * new upstream release
--- a/debian/control	Thu Jan 22 17:39:07 2015 +0100
+++ b/debian/control	Thu Jan 22 17:45:06 2015 +0100
@@ -15,7 +15,7 @@
  python-unittest2 | python (>= 2.7),
  python-logilab-mtconverter,
  python-rql,
- python-yams (>= 0.39.1),
+ python-yams (>= 0.40.0),
  python-lxml,
 Standards-Version: 3.9.1
 Homepage: http://www.cubicweb.org
@@ -35,11 +35,10 @@
 Description: the complete CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
- This package will install all the components you need to run cubicweb on
- a single machine. You can also deploy cubicweb by running the different
- process on different computers, in which case you need to install the
- corresponding packages on the different hosts.
-
+ This metapackage will install all the components you need to run cubicweb on a
+ single machine. You can also deploy cubicweb by running the different process
+ on different computers, in which case you need to install the corresponding
+ packages on the different hosts.
 
 Package: cubicweb-server
 Architecture: all
@@ -52,7 +51,7 @@
  ${python:Depends},
  cubicweb-common (= ${source:Version}),
  cubicweb-ctl (= ${source:Version}),
- python-logilab-database (>= 1.12.1),
+ python-logilab-database (>= 1.13.0),
  cubicweb-postgresql-support
  | cubicweb-mysql-support
  | python-pysqlite2,
@@ -75,6 +74,7 @@
 # postgresql-client packages for backup/restore of non local database
 Depends:
  ${misc:Depends},
+ ${python:Depends},
  python-psycopg2,
  postgresql-client
 Description: postgres support for the CubicWeb framework
@@ -88,6 +88,7 @@
 # mysql-client packages for backup/restore of non local database
 Depends:
  ${misc:Depends},
+ ${python:Depends},
  python-mysqldb,
  mysql-client
 Description: mysql support for the CubicWeb framework
@@ -134,6 +135,7 @@
  python-werkzeug,
 Breaks:
  cubicweb-inlinedit (<< 1.1.1),
+ cubicweb-bootstrap (<< 0.6.6),
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -153,8 +155,9 @@
  graphviz,
  gettext,
  python-logilab-mtconverter (>= 0.8.0),
- python-logilab-common (>= 0.62.0),
- python-yams (>= 0.39.1),
+ python-logilab-common (>= 0.63.1),
+ python-markdown,
+ python-yams (>= 0.40.0),
  python-rql (>= 0.31.2),
  python-lxml
 Recommends:
@@ -213,6 +216,8 @@
 
 Package: cubicweb-documentation
 Architecture: all
+Depends:
+ ${misc:Depends},
 Recommends:
  doc-base
 Description: documentation for the CubicWeb framework
--- a/debian/cubicweb-dev.lintian-overrides	Thu Jan 22 17:39:07 2015 +0100
+++ b/debian/cubicweb-dev.lintian-overrides	Thu Jan 22 17:45:06 2015 +0100
@@ -1,1 +1,1 @@
-missing-dep-for-interpreter make => make | build-essential | dpkg-dev (usr/share/pyshared/cubicweb/skeleton/debian/rules.tmpl)
+missing-dep-for-interpreter make => make | build-essential | dpkg-dev (usr/*/cubicweb/skeleton/debian/rules.tmpl)
--- a/devtools/fake.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/devtools/fake.py	Thu Jan 22 17:45:06 2015 +0100
@@ -65,8 +65,8 @@
         super(FakeRequest, self).__init__(*args, **kwargs)
         self._session_data = {}
 
-    def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
-        super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure)
+    def set_cookie(self, name, value, maxage=300, expires=None, secure=False, httponly=False):
+        super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure, httponly)
         cookie = self.get_response_header('Set-Cookie')
         self._headers_in.setHeader('Cookie', cookie)
 
--- a/devtools/htmlparser.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/devtools/htmlparser.py	Thu Jan 22 17:45:06 2015 +0100
@@ -88,8 +88,6 @@
         try:
             return etree.fromstring(pdata, self.parser)
         except etree.XMLSyntaxError as exc:
-            def save_in(fname=''):
-                file(fname, 'w').write(data)
             new_exc = AssertionError(u'invalid document: %s' % exc)
             new_exc.position = exc.position
             raise new_exc
@@ -176,23 +174,6 @@
         return super(XMLSyntaxValidator, self)._parse(data)
 
 
-class XMLDemotingValidator(XMLValidator):
-    """ some views produce html instead of xhtml, using demote_to_html
-
-    this is typically related to the use of external dependencies
-    which do not produce valid xhtml (google maps, ...)
-    """
-    __metaclass__ = class_deprecated
-    __deprecation_warning__ = '[3.10] this is now handled in testlib.py'
-
-    def preprocess_data(self, data):
-        if data.startswith('<?xml'):
-            self.parser = etree.XMLParser()
-        else:
-            self.parser = etree.HTMLParser()
-        return data
-
-
 class HTMLValidator(Validator):
 
     def __init__(self):
--- a/devtools/testlib.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/devtools/testlib.py	Thu Jan 22 17:45:06 2015 +0100
@@ -32,7 +32,7 @@
 import yams.schema
 
 from logilab.common.testlib import TestCase, InnerTest, Tags
-from logilab.common.pytest import nocoverage, pause_tracing, resume_tracing
+from logilab.common.pytest import nocoverage, pause_trace
 from logilab.common.debugger import Debugger
 from logilab.common.umessage import message_from_string
 from logilab.common.decorators import cached, classproperty, clear_cache, iclassmethod
@@ -513,9 +513,9 @@
         This method will be called by the database handler once the config has
         been properly bootstrapped.
         """
-        source = config.system_source_config
-        cls.admlogin = unicode(source['db-user'])
-        cls.admpassword = source['db-password']
+        admincfg = config.default_admin_config
+        cls.admlogin = unicode(admincfg['login'])
+        cls.admpassword = admincfg['password']
         # uncomment the line below if you want rql queries to be logged
         #config.global_set_option('query-log-file',
         #                         '/tmp/test_rql_log.' + `os.getpid()`)
@@ -572,18 +572,17 @@
     def setUp(self):
         # monkey patch send mail operation so emails are sent synchronously
         self._patch_SendMailOp()
-        pause_tracing()
-        previous_failure = self.__class__.__dict__.get('_repo_init_failed')
-        if previous_failure is not None:
-            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)
-        resume_tracing()
+        with pause_trace():
+            previous_failure = self.__class__.__dict__.get('_repo_init_failed')
+            if previous_failure is not None:
+                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
@@ -601,6 +600,7 @@
         while self._cleanups:
             cleanup, args, kwargs = self._cleanups.pop(-1)
             cleanup(*args, **kwargs)
+        self.repo.turn_repo_off()
 
     def _patch_SendMailOp(self):
         # monkey patch send mail operation so emails are sent synchronously
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.20.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,78 @@
+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/book/en/_themes/cubicweb/layout.html	Thu Jan 22 17:45:06 2015 +0100
@@ -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/book/en/_themes/cubicweb/static/cubicweb.css_t	Thu Jan 22 17:45:06 2015 +0100
@@ -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/book/en/_themes/cubicweb/static/cubicweb.ico	Thu Jan 22 17:45:06 2015 +0100
@@ -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/book/en/_themes/cubicweb/static/logo-cubicweb-small.svg	Thu Jan 22 17:45:06 2015 +0100
@@ -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/book/en/_themes/cubicweb/static/logo-cubicweb.svg	Thu Jan 22 17:45:06 2015 +0100
@@ -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/book/en/_themes/cubicweb/theme.conf	Thu Jan 22 17:45:06 2015 +0100
@@ -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 = 
--- a/doc/book/en/admin/create-instance.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/admin/create-instance.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -12,23 +12,23 @@
   cubicweb-ctl create -c all-in-one mycube myinstance
 
 .. note::
-  Please note that we created a new cube for a demo purpose but
-  you could have use an existing cube available in our standard library
+  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 serie of questions will be prompted to you, the default answer is usually
+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 user/psswd is requested to access the database
-please use the login you create at the time you configured the database
+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/psswd for the database to get the schema and handle low level
+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 others users for your final web instance.
+to use this user to create other users for your final web instance.
 
 
 Instance administration
@@ -49,7 +49,7 @@
 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/psswd you defined when you created the instance.
+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 ::
@@ -68,7 +68,9 @@
 upgrade
 ~~~~~~~
 
-The command is::
+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
 
@@ -93,6 +95,6 @@
   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 question up to the step that failed, and
+  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/setup.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/admin/setup.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -139,7 +139,7 @@
 .. _`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/>`_,
+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.
 
@@ -152,13 +152,15 @@
   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>`_ from this source making
-sure to choose the correct architecture and version of Python.
+<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::
 
--- a/doc/book/en/annexes/faq.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/annexes/faq.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -197,9 +197,6 @@
                 except NoSelectableObject:
                     continue
 
-Don't forget the 'from __future__ import with_statement' at the module
-top-level if you're using python 2.5.
-
 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'>
--- a/doc/book/en/conf.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/conf.py	Thu Jan 22 17:45:06 2015 +0100
@@ -52,8 +52,14 @@
 
 # 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', 'logilab.common.sphinx_ext']
+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 = []
 
@@ -117,8 +123,9 @@
 # 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 = 'standard_theme'
-html_theme_path = ['.']
+
+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
--- a/doc/book/en/devrepo/datamodel/baseschema.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/devrepo/datamodel/baseschema.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -22,7 +22,7 @@
 
 Entity types used to manage workflows
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* _`Workflow`, workflow entity, linked to some entity types which may use this workflow
+* :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
--- a/doc/book/en/devrepo/datamodel/definition.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -1,4 +1,4 @@
- .. -*- coding: utf-8 -*-
+.. -*- coding: utf-8 -*-
 
 .. _datamodel_definition:
 
@@ -92,6 +92,19 @@
 
  .. 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`.
@@ -273,20 +286,8 @@
   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 constraints
-  should be used when UniqueConstraint doesn't fit. Here is a simple example.
-
-.. sourcecode:: python
-
-    # Check that in the same Workflow each state's name is unique.  Using
-    # UniqueConstraint (or unique=True) here would prevent states in different
-    # workflows to have the same name.
-
-    # With: State S, Workflow W, String N ; S state_of W, S name N
-
-    RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N',
-                        mainvars='Y',
-                        msg=_('workflow already has a state of that name'))
+  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
 
@@ -523,6 +524,202 @@
 
 .. _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(ComputedRelationType):
+        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
 -------------------------------
 
--- a/doc/book/en/devrepo/index.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/devrepo/index.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -22,4 +22,4 @@
    migration.rst
    profiling.rst
    fti.rst
-
+   dataimport
--- a/doc/book/en/devweb/views/embedding.rst	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Embedding external pages
-------------------------
-
-(:mod:`cubicweb.web.views.embedding`)
-
-including external content
-
--- a/doc/book/en/devweb/views/idownloadable.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/devweb/views/idownloadable.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -14,7 +14,6 @@
 .. autoclass:: cubicweb.web.views.idownloadable.DownloadView
 .. autoclass:: cubicweb.web.views.idownloadable.DownloadLinkView
 .. autoclass:: cubicweb.web.views.idownloadable.IDownloadablePrimaryView
-.. autoclass:: cubicweb.web.views.idownloadable.IDownloadableLineView
 
 Embedded views
 --------------
--- a/doc/book/en/standard_theme/layout.html	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-{% extends "basic/layout.html" %}
-
-{% block header %}
-<div class="header">
- <a href="http://www.cubicweb.org">
-  <img alt="cubicweb logo" src="{{ pathto('_static/cubicweb.png', 1) }}"/>
- </a>
-</div>
-{% endblock %}
-
-{# puts the sidebar into "sidebar1" block i.e. before the document body #}
-{% block sidebar1 %}{{ sidebar() }}{% endblock %}
-{% block sidebar2 %}{% endblock %}
Binary file doc/book/en/standard_theme/static/contents.png has changed
--- a/doc/book/en/standard_theme/static/lglb-sphinx-doc.css	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,359 +0,0 @@
-/**
- * Sphinx stylesheet -- CubicWeb theme
- * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- *
- * Inspired from sphinxdoc original theme and logilab theme.
- */
-
-@import url("basic.css");
-
-/* -- page layout ----------------------------------------------------------- */
-
-body {
-    font-family: 'Bitstream Vera Sans', 'Lucida Grande', 'Lucida Sans Unicode',
-    'Geneva', 'Verdana', sans-serif;
-    font-size: 14px;
-    line-height: 150%;
-    text-align: center;
-    padding: 0;
-}
-
-div.document {
-    text-align: left;
-}
-
-div.bodywrapper {
-    margin: 0 0 0 230px;
-    border-left: 1px solid #CCBCA7;
-}
-
-div.body {
-    margin: 0;
-    padding: 0.5em 20px 20px 20px;
-}
-
-div.header {
-    text-align: left;
- }
-
-div.related {
-    background-color: #FF7700;
-    color: white;
-    font-weight: bolder;
-    font-size: 1em;
-}
-
-div.related a {
-    color: white;
-}
-
-div.related ul {
-    height: 2em;
-    border-top: 1px solid #CCBCA7;
-    border-bottom: 1px solid #CCBCA7;
-}
-
-div.related ul li {
-    margin: 0;
-    padding: 0;
-    height: 2em;
-    float: left;
-}
-
-div.related ul li.right {
-    float: right;
-    margin-right: 5px;
-}
-
-div.related ul li a {
-    margin: 0;
-    padding: 0 5px 0 5px;
-    line-height: 1.75em;
-}
-
-div.sphinxsidebarwrapper {
-    padding: 0;
-}
-
-div.sphinxsidebar {
-    margin: 0;
-    padding: 5px 10px 5px 10px;
-    width: 210px;
-    float: left;
-    font-size: 1em;
-    text-align: left;
-}
-
-div.sphinxsidebar h3, div.sphinxsidebar h4 {
-    font-size: 1.2em;
-    font-style: italic;
-}
-
-div.sphinxsidebar ul {
-    padding-left: 1.5em;
-    margin-top: 15px;
-    padding: 0;
-    line-height: 130%;
-    font-weight: bold;
-}
-
-div.sphinxsidebar ul ul {
-    margin-left: 20px;
-    font-weight: normal;
-}
-
-div.sphinxsidebar li {
-    margin: 0;
-}
-
-div.sphinxsidebar input {
-    border: 1px solid #CCBCA7;
-    font-family: sans-serif;
-    font-size: 1em;
-}
-
-div.footer {
-    color: orangered;
-    padding: 3px 8px 3px 0;
-    clear: both;
-    font-size: 0.8em;
-    text-align: center;
-}
-
-div.footer a {
-    text-decoration: underline;
-}
-
-/* -- body styles ----------------------------------------------------------- */
-
-p {
-    margin: 0.8em 0 0 0;
-}
-
-ul, ol {
-    margin: 0;
-}
-
-li {
-    margin: 0.2em 0 0 0;
-}
-
-a {
-    color: orangered;
-    text-decoration: none;
-}
-
-div.sphinxsidebar a {
-    color: black;
-    text-decoration: none;
-}
-
-a:hover {
-    text-decoration: underline;
-}
-
-h1 {
-    margin: 0;
-    padding: 0.7em 0 0.3em 0;
-    font-size: 1.5em;
-    border-bottom: 1px dotted;
-}
-
-h2 {
-    margin: 1.3em 0 0.2em 0;
-    font-size: 1.35em;
-    padding: 0;
-    color: #303030;
-}
-
-h3 {
-    margin: 1em 0 -0.3em 0;
-    font-size: 1.2em;
-    color: #202020;
-}
-
-div.body h1 a {
-    color: #404040!important;
-}
-
-div.body h2 a {
-    color: #303030!important;
-}
-
-div.body h3 a {
-    color: #202020!important;
-}
-
-div.body h4 a, div.body h5 a, div.body h6 a {
-    color: #000000!important;
-}
-
-h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor {
-    display: none;
-    margin: 0 0 0 0.3em;
-    padding: 0 0.2em 0 0.2em;
-    color: #AAA!important;
-}
-
-h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor,
-h5:hover a.anchor, h6:hover a.anchor {
-    display: inline;
-}
-
-h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover,
-h5 a.anchor:hover, h6 a.anchor:hover {
-    color: #777;
-    background-color: #EEE;
-}
-
-a.headerlink {
-    color: #C60F0F!important;
-    font-size: 1em;
-    margin-left: 6px;
-    padding: 0 4px 0 4px;
-    text-decoration: none!important;
-}
-
-a.headerlink:hover {
-    background-color: #C0C0C0;
-    color: #FFFFFF!important;
-}
-
-cite, code, tt {
-    font-family: 'Consolas', 'Deja Vu Sans Mono',
-                 'Bitstream Vera Sans Mono', monospace;
-    font-size: 0.95em;
-    letter-spacing: 0.01em;
-}
-
-tt {
-    background-color: #F0F0F0;
-    border-bottom: 1px solid #D0D0D0;
-}
-
-tt.descname, tt.descclassname, tt.xref {
-    background-color: #F0F0F0;
-    font-weight: normal;
-    font-size: 1em;
-    border: 1px solid #D0D0D0;
-    border: 0;
-}
-
-hr {
-    border: 1px solid #CC8B00;
-    margin: 2em;
-}
-
-a tt {
-    border: 0;
-    color: #B45300;
-}
-
-a:hover tt {
-    color: #4BACFF;
-}
-
-pre {
-    font-family: 'Consolas', 'Deja Vu Sans Mono',
-                 'Bitstream Vera Sans Mono', monospace;
-    font-size: 0.95em;
-    letter-spacing: 0.015em;
-    line-height: 120%;
-    padding: 0.5em;
-    border: 1px solid #CCBCA7;
-    background-color: #F0F0F0;
-}
-
-pre a {
-    color: inherit;
-    text-decoration: underline;
-}
-
-td.linenos pre {
-    padding: 0.5em 0;
-}
-
-div.quotebar {
-    background-color: #F8F8F8;
-    max-width: 250px;
-    float: right;
-    padding: 2px 7px;
-    border: 1px solid #C0C0C0;
-}
-
-div.topic {
-    background-color: #F8F8F8;
-}
-
-table {
-    border-collapse: collapse;
-    margin: 0.8em -0.5em 0em -0.5em;
-}
-
-table td, table th {
-    padding: 0.2em 0.5em 0.2em 0.5em;
-}
-
-div.admonition, div.warning {
-    font-size: 0.9em;
-    margin: 1em 0 1em 0;
-    padding: 0;
-}
-
-div.admonition {
-    border: 1px solid #86989B;
-    background-color: #EBEBFF;
-}
-
-div.warning {
-    border: 1px solid #940000;
-    background-color: #FFEBEB;
-}
-
-div.admonition p, div.warning p {
-    margin: 0.5em 1em 0.5em 1em;
-    padding: 0;
-}
-
-div.admonition pre, div.warning pre {
-    margin: 0.4em 1em 0.4em 1em;
-}
-
-div.admonition p.admonition-title,
-div.warning p.admonition-title {
-    margin: 0;
-    padding: 0.1em 0 0.1em 0.5em;
-    color: #FFFFFF;
-    font-weight: bold;
-}
-
-div.admonition p.admonition-title {
-    border-bottom: 1px solid #86989B;
-    background-color: #8C88B5;
-}
-
-div.warning p.admonition-title {
-    background-color: #CF0000;
-    border-bottom: 1px solid #940000;
-}
-
-div.admonition ul, div.admonition ol,
-div.warning ul, div.warning ol {
-    margin: 0.1em 0.5em 0.5em 3em;
-    padding: 0;
-}
-
-div.versioninfo {
-    margin: 1em 0 0 0;
-    border: 1px solid #C0C0C0;
-    background-color: #DDEAF0;
-    padding: 8px;
-    line-height: 1.3em;
-    font-size: 0.9em;
-}
-
-/* TOC trees */
-
-li.toctree-l1 {
-    margin-top: 0.4em;
- }
\ No newline at end of file
Binary file doc/book/en/standard_theme/static/logilab_logo.png has changed
Binary file doc/book/en/standard_theme/static/navigation.png has changed
--- a/doc/book/en/standard_theme/theme.conf	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = lglb-sphinx-doc.css
-pygments_style = friendly
--- a/doc/book/en/tutorials/textreports/index.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/book/en/tutorials/textreports/index.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -8,6 +8,6 @@
 
 Three additional restructuredtext roles are defined by |cubicweb|:
 
-.. autodocfunction:: cubicweb.ext.rest.eid_reference_role
-.. autodocfunction:: cubicweb.ext.rest.rql_role
-.. autodocfunction:: cubicweb.ext.rest.bookmark_role
+.. autofunction:: cubicweb.ext.rest.eid_reference_role
+.. autofunction:: cubicweb.ext.rest.rql_role
+.. autofunction:: cubicweb.ext.rest.bookmark_role
Binary file doc/book/src/cubicweb.zargo has changed
Binary file doc/book/src/cubicweb.zargo~0.14.1 has changed
--- a/doc/refactoring-the-css-with-uiprops.rst	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/refactoring-the-css-with-uiprops.rst	Thu Jan 22 17:45:06 2015 +0100
@@ -40,34 +40,3 @@
 
 Keep in mind that the browser will then interpret the CSSs and apply
 the standard cascading mechanism.
-
-FAQ
-====
-
-- How do I keep the old style?
-
-  Put ``STYLESHEET = [data('cubicweb.old.css')]`` in your uiprops.py
-  file and think about something else.
-
-- What are the changes in cubicweb.css?
-
-  Version 3.9.0 of cubicweb changed the following in the default html
-  markup and css:
-
-  ===============  ==================================
-   old              new
-  ===============  ==================================
-   .navcol          #navColumnLeft, #navColumnRight
-   #contentcol      #contentColumn
-   .footer          #footer
-   .logo	    #logo
-   .simpleMessage   .loginMessage
-   .appMsg	    (styles are removed from css)
-   .searchMessage   (styles are removed from css)
-  ===============  ==================================
-
-  Introduction of the new cubicweb.reset.css based on Eric Meyer's
-  reset css.
-
-  Lots of margin, padding, etc.
-
--- a/doc/tools/pyjsrest.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/doc/tools/pyjsrest.py	Thu Jan 22 17:45:06 2015 +0100
@@ -2,8 +2,6 @@
 """
 Parser for Javascript comments.
 """
-from __future__ import with_statement
-
 import os.path as osp
 import sys, os, getopt, re
 
--- a/entities/__init__.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/entities/__init__.py	Thu Jan 22 17:45:06 2015 +0100
@@ -31,12 +31,11 @@
     instances have access to their issuing cursor
     """
     __regid__ = 'Any'
-    __implements__ = ()
 
     @classproperty
     def cw_etype(cls):
-        """entity type as a string"""
-        return cls.__regid__
+        """entity type as a unicode string"""
+        return unicode(cls.__regid__)
 
     @classmethod
     def cw_create_url(cls, req, **kwargs):
--- a/entities/authobjs.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/entities/authobjs.py	Thu Jan 22 17:45:06 2015 +0100
@@ -74,7 +74,11 @@
         try:
             return self._properties
         except AttributeError:
-            self._properties = dict((p.pkey, p.value) for p in self.reverse_for_user)
+            self._properties = dict(
+                self._cw.execute(
+                    'Any K, V WHERE P for_user U, U eid %(userid)s, '
+                    'P pkey K, P value V',
+                    {'userid': self.eid}))
             return self._properties
 
     def prefered_language(self, language=None):
--- a/entities/test/unittest_wfobjs.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/entities/test/unittest_wfobjs.py	Thu Jan 22 17:45:06 2015 +0100
@@ -50,11 +50,13 @@
             wf = add_wf(shell, 'Company')
             wf.add_state(u'foo', initial=True)
             shell.commit()
-            wf.add_state(u'foo')
             with self.assertRaises(ValidationError) as cm:
-                shell.commit()
-            self.assertEqual({'name-subject': 'workflow already has a state of that name'},
+                wf.add_state(u'foo')
+            self.assertEqual({'name': u'name is part of violated unicity constraint',
+                              'state_of': u'state_of is part of violated unicity constraint',
+                              'unicity constraint': u'some relations violate a unicity constraint'},
                              cm.exception.errors)
+            shell.rollback()
             # no pb if not in the same workflow
             wf2 = add_wf(shell, 'Company')
             foo = wf2.add_state(u'foo', initial=True)
@@ -62,10 +64,12 @@
             # gnark gnark
             bar = wf.add_state(u'bar')
             shell.commit()
-            bar.cw_set(name=u'foo')
             with self.assertRaises(ValidationError) as cm:
-                shell.commit()
-            self.assertEqual({'name-subject': 'workflow already has a state of that name'},
+                bar.cw_set(name=u'foo')
+            shell.rollback()
+            self.assertEqual({'name': u'name is part of violated unicity constraint',
+                              'state_of': u'state_of is part of violated unicity constraint',
+                              'unicity constraint': u'some relations violate a unicity constraint'},
                              cm.exception.errors)
 
     def test_duplicated_transition(self):
@@ -74,10 +78,13 @@
             foo = wf.add_state(u'foo', initial=True)
             bar = wf.add_state(u'bar')
             wf.add_transition(u'baz', (foo,), bar, ('managers',))
-            wf.add_transition(u'baz', (bar,), foo)
             with self.assertRaises(ValidationError) as cm:
-                shell.commit()
-            self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already has a transition of that name'})
+                wf.add_transition(u'baz', (bar,), foo)
+            self.assertEqual({'name': u'name is part of violated unicity constraint',
+                              'transition_of': u'transition_of is part of violated unicity constraint',
+                              'unicity constraint': u'some relations violate a unicity constraint'},
+                             cm.exception.errors)
+            shell.rollback()
             # no pb if not in the same workflow
             wf2 = add_wf(shell, 'Company')
             foo = wf.add_state(u'foo', initial=True)
@@ -87,10 +94,13 @@
             # gnark gnark
             biz = wf.add_transition(u'biz', (bar,), foo)
             shell.commit()
-            biz.cw_set(name=u'baz')
             with self.assertRaises(ValidationError) as cm:
-                shell.commit()
-            self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already has a transition of that name'})
+                biz.cw_set(name=u'baz')
+            shell.rollback()
+            self.assertEqual({'name': u'name is part of violated unicity constraint',
+                              'transition_of': u'transition_of is part of violated unicity constraint',
+                              'unicity constraint': u'some relations violate a unicity constraint'},
+                             cm.exception.errors)
 
 
 class WorkflowTC(CubicWebTC):
--- a/entities/wfobjs.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/entities/wfobjs.py	Thu Jan 22 17:45:06 2015 +0100
@@ -32,6 +32,16 @@
 from cubicweb.view import EntityAdapter
 from cubicweb.predicates import relation_possible
 
+
+try:
+    from cubicweb import server
+except ImportError:
+    # We need to lookup DEBUG from there,
+    # however a pure dbapi client may not have it.
+    class server(object): pass
+    server.DEBUG = False
+
+
 class WorkflowException(Exception): pass
 
 class Workflow(AnyEntity):
@@ -87,7 +97,7 @@
     def transition_by_name(self, trname):
         rset = self._cw.execute('Any T, TN WHERE T name TN, T name %(n)s, '
                                 'T transition_of WF, WF eid %(wf)s',
-                                {'n': trname, 'wf': self.eid})
+                                {'n': unicode(trname), 'wf': self.eid})
         if rset:
             return rset.get_entity(0, 0)
         return None
@@ -201,17 +211,30 @@
 
         `eid` is the eid of the object on which we may fire the transition
         """
+        DBG = False
+        if server.DEBUG & server.DBG_SEC:
+            if 'transition' in server._SECURITY_CAPS:
+                DBG = True
         user = self._cw.user
         # check user is at least in one of the required groups if any
         groups = frozenset(g.name for g in self.require_group)
         if groups:
             matches = user.matching_groups(groups)
             if matches:
+                if DBG:
+                    print 'may_be_fired: %r may fire: user matches %s' % (self.name, groups)
                 return matches
             if 'owners' in groups and user.owns(eid):
+                if DBG:
+                    print 'may_be_fired: %r may fire: user is owner' % self.name
                 return True
         # check one of the rql expression conditions matches if any
         if self.condition:
+            if DBG:
+                print ('my_be_fired: %r: %s' %
+                       (self.name, [(rqlexpr.expression,
+                                    rqlexpr.check_expression(self._cw, eid))
+                                   for rqlexpr in self.condition]))
             for rqlexpr in self.condition:
                 if rqlexpr.check_expression(self._cw, eid):
                     return True
@@ -231,7 +254,7 @@
         for gname in requiredgroups:
             rset = self._cw.execute('SET T require_group G '
                                     'WHERE T eid %(x)s, G name %(gn)s',
-                                    {'x': self.eid, 'gn': gname})
+                                    {'x': self.eid, 'gn': unicode(gname)})
             assert rset, '%s is not a known group' % gname
         if isinstance(conditions, basestring):
             conditions = (conditions,)
@@ -454,7 +477,7 @@
             'Any T,TT, TN WHERE S allowed_transition T, S eid %(x)s, '
             'T type TT, T type %(type)s, '
             'T name TN, T transition_of WF, WF eid %(wfeid)s',
-            {'x': self.current_state.eid, 'type': type,
+            {'x': self.current_state.eid, 'type': unicode(type),
              'wfeid': self.current_workflow.eid})
         for tr in rset.entities():
             if tr.may_be_fired(self.entity.eid):
--- a/entity.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/entity.py	Thu Jan 22 17:45:06 2015 +0100
@@ -22,7 +22,6 @@
 from warnings import warn
 from functools import partial
 
-from logilab.common import interface
 from logilab.common.decorators import cached
 from logilab.common.deprecation import deprecated
 from logilab.common.registry import yes
@@ -520,7 +519,11 @@
             rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
         else:
             rql = 'INSERT %s X' % (cls.__regid__)
-        created = execute(rql, qargs).get_entity(0, 0)
+        try:
+            created = execute(rql, qargs).get_entity(0, 0)
+        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
@@ -1367,100 +1370,6 @@
     def clear_all_caches(self):
         return self.cw_clear_all_caches()
 
-    @property
-    @deprecated('[3.10] use entity.cw_edited')
-    def edited_attributes(self):
-        return self.cw_edited
-
-    @property
-    @deprecated('[3.10] use entity.cw_edited.skip_security')
-    def skip_security_attributes(self):
-        return self.cw_edited.skip_security
-
-    @property
-    @deprecated('[3.10] use entity.cw_edited.skip_security')
-    def _cw_skip_security_attributes(self):
-        return self.cw_edited.skip_security
-
-    @property
-    @deprecated('[3.10] use entity.cw_edited.querier_pending_relations')
-    def querier_pending_relations(self):
-        return self.cw_edited.querier_pending_relations
-
-    @deprecated('[3.10] use key in entity.cw_attr_cache')
-    def __contains__(self, key):
-        return key in self.cw_attr_cache
-
-    @deprecated('[3.10] iter on entity.cw_attr_cache')
-    def __iter__(self):
-        return iter(self.cw_attr_cache)
-
-    @deprecated('[3.10] use entity.cw_attr_cache[attr]')
-    def __getitem__(self, key):
-        return self.cw_attr_cache[key]
-
-    @deprecated('[3.10] use entity.cw_attr_cache.get(attr[, default])')
-    def get(self, key, default=None):
-        return self.cw_attr_cache.get(key, default)
-
-    @deprecated('[3.10] use entity.cw_attr_cache.clear()')
-    def clear(self):
-        self.cw_attr_cache.clear()
-        # XXX clear cw_edited ?
-
-    @deprecated('[3.10] use entity.cw_edited[attr] = value or entity.cw_attr_cache[attr] = value')
-    def __setitem__(self, attr, value):
-        """override __setitem__ to update self.cw_edited.
-
-        Typically, a before_[update|add]_hook could do::
-
-            entity['generated_attr'] = generated_value
-
-        and this way, cw_edited will be updated accordingly. Also, add
-        the attribute to skip_security since we don't want to check security
-        for such attributes set by hooks.
-        """
-        try:
-            self.cw_edited[attr] = value
-        except AttributeError:
-            self.cw_attr_cache[attr] = value
-
-    @deprecated('[3.10] use del entity.cw_edited[attr]')
-    def __delitem__(self, attr):
-        """override __delitem__ to update self.cw_edited on cleanup of
-        undesired changes introduced in the entity's dict. For example, see the
-        code snippet below from the `forge` cube:
-
-        .. sourcecode:: python
-
-            edited = self.entity.cw_edited
-            has_load_left = 'load_left' in edited
-            if 'load' in edited and self.entity.load_left is None:
-                self.entity.load_left = self.entity['load']
-            elif not has_load_left and edited:
-                # cleanup, this may cause undesired changes
-                del self.entity['load_left']
-        """
-        del self.cw_edited[attr]
-
-    @deprecated('[3.10] use entity.cw_edited.setdefault(attr, default)')
-    def setdefault(self, attr, default):
-        """override setdefault to update self.cw_edited"""
-        return self.cw_edited.setdefault(attr, default)
-
-    @deprecated('[3.10] use entity.cw_edited.pop(attr[, default])')
-    def pop(self, attr, *args):
-        """override pop to update self.cw_edited on cleanup of
-        undesired changes introduced in the entity's dict. See `__delitem__`
-        """
-        return self.cw_edited.pop(attr, *args)
-
-    @deprecated('[3.10] use entity.cw_edited.update(values)')
-    def update(self, values):
-        """override update to update self.cw_edited. See `__setitem__`
-        """
-        self.cw_edited.update(values)
-
 
 # attribute and relation descriptors ##########################################
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/markdown.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,27 @@
+from __future__ import absolute_import
+import markdown
+
+import logging
+
+log = logging.getLogger(__name__)
+
+
+def markdown_publish(context, data):
+    """publish a string formatted as MarkDown Text to HTML
+
+    :type context: a cubicweb application object
+
+    :type data: str
+    :param data: some MarkDown text
+
+    :rtype: unicode
+    :return:
+      the data formatted as HTML or the original data if an error occurred
+    """
+    md = markdown.Markdown()
+    try:
+        return md.convert(data)
+    except:
+        import traceback; traceback.print_exc()
+        log.exception("Error while converting Markdown to HTML")
+        return data
--- a/ext/rest.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/ext/rest.py	Thu Jan 22 17:45:06 2015 +0100
@@ -99,9 +99,9 @@
                             **options)], []
 
 def rql_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
-    """:rql:`<rql-expr>` or :rql:`<rql-expr>:<vid>`
+    """``:rql:`<rql-expr>``` or ``:rql:`<rql-expr>:<vid>```
 
-    Example: :rql:`Any X,Y WHERE X is CWUser, X login Y:table`
+    Example: ``:rql:`Any X,Y WHERE X is CWUser, X login Y:table```
 
     Replace the directive with the output of applying the view to the resultset
     returned by the query.
@@ -132,9 +132,9 @@
     return [nodes.raw('', content, format='html')], []
 
 def bookmark_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
-    """:bookmark:`<bookmark-eid>` or :bookmark:`<eid>:<vid>`
+    """``:bookmark:`<bookmark-eid>``` or ``:bookmark:`<eid>:<vid>```
 
-    Example: :bookmark:`1234:table`
+    Example: ``:bookmark:`1234:table```
 
     Replace the directive with the output of applying the view to the resultset
     returned by the query stored in the bookmark. By default, the view is the one
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/synccomputed.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,227 @@
+# 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/>.
+"""Hooks for synchronizing computed attributes"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from collections import defaultdict
+
+from rql import nodes
+
+from cubicweb.server import hook
+
+
+class RecomputeAttributeOperation(hook.DataOperationMixIn, hook.Operation):
+    """Operation to recompute caches of computed attribute at commit time,
+    depending on what's have been modified in the transaction and avoiding to
+    recompute twice the same attribute
+    """
+    containercls = dict
+    def add_data(self, computed_attribute, eid=None):
+        try:
+            self._container[computed_attribute].add(eid)
+        except KeyError:
+            self._container[computed_attribute] = set((eid,))
+
+    def precommit_event(self):
+        for computed_attribute_rdef, eids in self.get_data().iteritems():
+            attr = computed_attribute_rdef.rtype
+            formula  = computed_attribute_rdef.formula
+            rql = formula.replace('Any ', 'Any X, ', 1)
+            kwargs = None
+            # add constraint on X to the formula
+            if None in eids : # recompute for all etype if None is found
+                rql += ', X is %s' % computed_attribute_rdef.subject
+            elif len(eids) == 1:
+                rql += ', X eid %(x)s'
+                kwargs = {'x': eids.pop()}
+            else:
+                rql += ', X eid IN (%s)' % ', '.join((str(eid) for eid in eids))
+            update_rql = 'SET X %s %%(value)s WHERE X eid %%(x)s' % attr
+            for eid, value in self.cnx.execute(rql, kwargs):
+                self.cnx.execute(update_rql, {'value': value, 'x': eid})
+
+
+class EntityWithCACreatedHook(hook.Hook):
+    """When creating an entity that has some computed attribute, those
+    attributes have to be computed.
+
+    Concret class of this hook are generated at registration time by
+    introspecting the schema.
+    """
+    __abstract__ = True
+    events = ('after_add_entity',)
+    # list of computed attribute rdefs that have to be recomputed
+    computed_attributes = None
+
+    def __call__(self):
+        for rdef in self.computed_attributes:
+            RecomputeAttributeOperation.get_instance(self._cw).add_data(
+                rdef, self.entity.eid)
+
+
+class RelationInvolvedInCAModifiedHook(hook.Hook):
+    """When some relation used in a computed attribute is updated, those
+    attributes have to be recomputed.
+
+    Concret class of this hook are generated at registration time by
+    introspecting the schema.
+    """
+    __abstract__ = True
+    events = ('after_add_relation', 'before_delete_relation')
+    # list of (computed attribute rdef, optimize_on) that have to be recomputed
+    optimized_computed_attributes = None
+
+    def __call__(self):
+        for rdef, optimize_on in self.optimized_computed_attributes:
+            if optimize_on is None:
+                eid = None
+            else:
+                eid = getattr(self, optimize_on)
+            RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef, eid)
+
+
+class AttributeInvolvedInCAModifiedHook(hook.Hook):
+    """When some attribute used in a computed attribute is updated, those
+    attributes have to be recomputed.
+
+    Concret class of this hook are generated at registration time by
+    introspecting the schema.
+    """
+    __abstract__ = True
+    events = ('after_update_entity',)
+    # list of (computed attribute rdef, attributes of this entity type involved)
+    # that may have to be recomputed
+    attributes_computed_attributes = None
+
+    def __call__(self):
+        edited_attributes = frozenset(self.entity.cw_edited)
+        for rdef, used_attributes in self.attributes_computed_attributes.iteritems():
+            if edited_attributes.intersection(used_attributes):
+                # XXX optimize if the modified attributes belong to the same
+                # entity as the computed attribute
+                RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef)
+
+
+# code generation at registration time #########################################
+
+def _optimize_on(formula_select, rtype):
+    """Given a formula and some rtype, tells whether on update of the given
+    relation, formula may be recomputed only for rhe relation's subject
+    ('eidfrom' returned), object ('eidto' returned) or None.
+
+    Optimizing is only possible when X is used as direct subject/object of this
+    relation, else we may miss some necessary update.
+    """
+    for rel in formula_select.get_nodes(nodes.Relation):
+        if rel.r_type == rtype:
+            sub = rel.get_variable_parts()[0]
+            obj = rel.get_variable_parts()[1]
+            if sub.name == 'X':
+                return 'eidfrom'
+            elif obj.name == 'X':
+                return 'eidto'
+            else:
+                return None
+
+
+class _FormulaDependenciesMatrix(object):
+    """This class computes and represents the dependencies of computed attributes
+    towards relations and attributes
+    """
+
+    def __init__(self, schema):
+        """Analyzes the schema to compute the dependencies"""
+        # entity types holding some computed attribute {etype: [computed rdefs]}
+        self.computed_attribute_by_etype = defaultdict(list)
+        # depending entity types {dep. etype: {computed rdef: dep. etype attributes}}
+        self.computed_attribute_by_etype_attrs = defaultdict(lambda: defaultdict(set))
+        # depending relations def {dep. rdef: [computed rdefs]
+        self.computed_attribute_by_relation = defaultdict(list) # by rdef
+        # Walk through all attributes definitions
+        for rdef in schema.iter_computed_attributes():
+            self.computed_attribute_by_etype[rdef.subject.type].append(rdef)
+            # extract the relations it depends upon - `rdef.formula_select` is
+            # expected to have been set by finalize_computed_attributes
+            select = rdef.formula_select
+            for rel_node in select.get_nodes(nodes.Relation):
+                rschema = schema.rschema(rel_node.r_type)
+                lhs, rhs = rel_node.get_variable_parts()
+                for sol in select.solutions:
+                    subject_etype = sol[lhs.name]
+                    if isinstance(rhs, nodes.VariableRef):
+                        object_etypes = set(sol[rhs.name] for sol in select.solutions)
+                    else:
+                        object_etypes = rschema.objects(subject_etype)
+                    for object_etype in object_etypes:
+                        if rschema.final:
+                            attr_for_computations = self.computed_attribute_by_etype_attrs[subject_etype]
+                            attr_for_computations[rdef].add(rschema.type)
+                        else:
+                            depend_on_rdef = rschema.rdefs[subject_etype, object_etype]
+                            self.computed_attribute_by_relation[depend_on_rdef].append(rdef)
+
+    def generate_entity_creation_hooks(self):
+        for etype, computed_attributes in self.computed_attribute_by_etype.iteritems():
+            regid = 'computed_attribute.%s_created' % etype
+            selector = hook.is_instance(etype)
+            yield type('%sCreatedHook' % etype,
+                       (EntityWithCACreatedHook,),
+                       {'__regid__': regid,
+                        '__select__':  hook.Hook.__select__ & selector,
+                        'computed_attributes': computed_attributes})
+
+    def generate_relation_change_hooks(self):
+        for rdef, computed_attributes in self.computed_attribute_by_relation.iteritems():
+            regid = 'computed_attribute.%s_modified' % rdef.rtype
+            selector = hook.match_rtype(rdef.rtype.type,
+                                        frometypes=(rdef.subject.type,),
+                                        toetypes=(rdef.object.type,))
+            optimized_computed_attributes = []
+            for computed_rdef in computed_attributes:
+                optimized_computed_attributes.append(
+                    (computed_rdef,
+                     _optimize_on(computed_rdef.formula_select, rdef.rtype))
+                     )
+            yield type('%sModifiedHook' % rdef.rtype,
+                       (RelationInvolvedInCAModifiedHook,),
+                       {'__regid__': regid,
+                        '__select__':  hook.Hook.__select__ & selector,
+                        'optimized_computed_attributes': optimized_computed_attributes})
+
+    def generate_entity_update_hooks(self):
+        for etype, attributes_computed_attributes in self.computed_attribute_by_etype_attrs.iteritems():
+            regid = 'computed_attribute.%s_updated' % etype
+            selector = hook.is_instance(etype)
+            yield type('%sModifiedHook' % etype,
+                       (AttributeInvolvedInCAModifiedHook,),
+                       {'__regid__': regid,
+                        '__select__':  hook.Hook.__select__ & selector,
+                        'attributes_computed_attributes': attributes_computed_attributes})
+
+
+def registration_callback(vreg):
+    vreg.register_all(globals().values(), __name__)
+    dependencies = _FormulaDependenciesMatrix(vreg.schema)
+    for hook_class in dependencies.generate_entity_creation_hooks():
+        vreg.register(hook_class)
+    for hook_class in dependencies.generate_relation_change_hooks():
+        vreg.register(hook_class)
+    for hook_class in dependencies.generate_entity_update_hooks():
+        vreg.register(hook_class)
--- a/hooks/syncschema.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/hooks/syncschema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -27,7 +27,8 @@
 _ = unicode
 
 from copy import copy
-from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
+from yams.schema import (BASE_TYPES, BadSchemaDefinition,
+                         RelationSchema, RelationDefinitionSchema)
 from yams import buildobjs as ybo, schema2sql as y2sql, convert_default_value
 
 from logilab.common.decorators import clear_cache
@@ -38,6 +39,7 @@
                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
 from cubicweb.server import hook, schemaserial as ss
 from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.hooks.synccomputed import RecomputeAttributeOperation
 
 # core entity and relation types which can't be removed
 CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
@@ -70,14 +72,14 @@
     table = SQL_PREFIX + etype
     column = SQL_PREFIX + rtype
     try:
-        cnx.system_sql(str('ALTER TABLE %s ADD %s integer'
-                               % (table, column)), rollback_on_failure=False)
+        cnx.system_sql(str('ALTER TABLE %s ADD %s integer' % (table, column)),
+                       rollback_on_failure=False)
         cnx.info('added column %s to table %s', column, table)
     except Exception:
         # silent exception here, if this error has not been raised because the
         # column already exists, index creation will fail anyway
         cnx.exception('error while adding column %s to table %s',
-                          table, column)
+                      table, column)
     # create index before alter table which may expectingly fail during test
     # (sqlite) while index creation should never fail (test for index existence
     # is done by the dbhelper)
@@ -166,8 +168,8 @@
         # drop index if any
         source.drop_index(cnx, table, column)
         if source.dbhelper.alter_column_support:
-            cnx.system_sql('ALTER TABLE %s DROP COLUMN %s'
-                               % (table, column), rollback_on_failure=False)
+            cnx.system_sql('ALTER TABLE %s DROP COLUMN %s' % (table, column),
+                           rollback_on_failure=False)
             self.info('dropped column %s from table %s', column, table)
         else:
             # not supported by sqlite for instance
@@ -307,7 +309,7 @@
 class CWRTypeUpdateOp(MemSchemaOperation):
     """actually update some properties of a relation definition"""
     rschema = entity = values = None # make pylint happy
-    oldvalus = None
+    oldvalues = None
 
     def precommit_event(self):
         rschema = self.rschema
@@ -388,6 +390,21 @@
         # XXX revert changes on database
 
 
+class CWComputedRTypeUpdateOp(MemSchemaOperation):
+    """actually update some properties of a computed relation definition"""
+    rschema = entity = rule = None # make pylint happy
+    old_rule = None
+
+    def precommit_event(self):
+        # update the in-memory schema first
+        self.old_rule = self.rschema.rule
+        self.rschema.rule = self.rule
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.rschema.rule = self.old_rule
+
+
 class CWAttributeAddOp(MemSchemaOperation):
     """an attribute relation (CWAttribute) has been added:
     * add the necessary column
@@ -407,12 +424,19 @@
             description=entity.description, cardinality=entity.cardinality,
             constraints=get_constraints(self.cnx, entity),
             order=entity.ordernum, eid=entity.eid, **kwargs)
-        self.cnx.vreg.schema.add_relation_def(rdefdef)
+        try:
+            self.cnx.vreg.schema.add_relation_def(rdefdef)
+        except BadSchemaDefinition:
+            # rdef has been infered then explicitly added (current consensus is
+            # not clear at all versus infered relation handling (and much
+            # probably buggy)
+            rdef = self.cnx.vreg.schema.rschema(rdefdef.name).rdefs[rdefdef.subject, rdefdef.object]
+            assert rdef.infered
         self.cnx.execute('SET X ordernum Y+1 '
-                             'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
-                             'X ordernum >= %(order)s, NOT X eid %(x)s',
-                             {'x': entity.eid, 'se': fromentity.eid,
-                              'order': entity.ordernum or 0})
+                         'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
+                         'X ordernum >= %(order)s, NOT X eid %(x)s',
+                         {'x': entity.eid, 'se': fromentity.eid,
+                          'order': entity.ordernum or 0})
         return rdefdef
 
     def precommit_event(self):
@@ -427,6 +451,9 @@
                  'indexed': entity.indexed,
                  'fulltextindexed': entity.fulltextindexed,
                  'internationalizable': entity.internationalizable}
+        # entity.formula may not exist yet if we're migrating to 3.20
+        if hasattr(entity, 'formula'):
+            props['formula'] = entity.formula
         # update the in-memory schema first
         rdefdef = self.init_rdef(**props)
         # then make necessary changes to the system source database
@@ -447,8 +474,8 @@
         column = SQL_PREFIX + rdefdef.name
         try:
             cnx.system_sql(str('ALTER TABLE %s ADD %s %s'
-                                   % (table, column, attrtype)),
-                               rollback_on_failure=False)
+                               % (table, column, attrtype)),
+                           rollback_on_failure=False)
             self.info('added column %s to table %s', column, table)
         except Exception as ex:
             # the column probably already exists. this occurs when
@@ -479,6 +506,12 @@
             default = convert_default_value(self.rdefdef, default)
             cnx.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
                                {'default': default})
+        # if attribute is computed, compute it
+        if getattr(entity, 'formula', None):
+            # add rtype attribute for RelationDefinitionSchema api compat, this
+            # is what RecomputeAttributeOperation expect
+            rdefdef.rtype = rdefdef.name
+            RecomputeAttributeOperation.get_instance(cnx).add_data(rdefdef)
 
     def revertprecommit_event(self):
         # revert changes on in memory schema
@@ -616,6 +649,8 @@
             self.null_allowed_changed = True
         if 'fulltextindexed' in self.values:
             UpdateFTIndexOp.get_instance(cnx).add_data(rdef.subject)
+        if 'formula' in self.values:
+            RecomputeAttributeOperation.get_instance(cnx).add_data(rdef)
 
     def revertprecommit_event(self):
         if self.rdef is None:
@@ -977,7 +1012,26 @@
         MemSchemaCWRTypeDel(self._cw, rtype=name)
 
 
-class AfterAddCWRTypeHook(DelCWRTypeHook):
+class AfterAddCWComputedRTypeHook(SyncSchemaHook):
+    """after a CWComputedRType entity has been added:
+    * register an operation to add the relation type to the instance's
+      schema on commit
+
+    We don't know yet this point if a table is necessary
+    """
+    __regid__ = 'syncaddcwcomputedrtype'
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        rtypedef = ybo.ComputedRelation(name=entity.name,
+                                        eid=entity.eid,
+                                        rule=entity.rule)
+        MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
+
+
+class AfterAddCWRTypeHook(SyncSchemaHook):
     """after a CWRType entity has been added:
     * register an operation to add the relation type to the instance's
       schema on commit
@@ -985,6 +1039,7 @@
     We don't know yet this point if a table is necessary
     """
     __regid__ = 'syncaddcwrtype'
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -997,9 +1052,10 @@
         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
 
 
-class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
+class BeforeUpdateCWRTypeHook(SyncSchemaHook):
     """check name change, handle final"""
     __regid__ = 'syncupdatecwrtype'
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
     events = ('before_update_entity',)
 
     def __call__(self):
@@ -1017,6 +1073,23 @@
                             values=newvalues)
 
 
+class BeforeUpdateCWComputedRTypeHook(SyncSchemaHook):
+    """check name change, handle final"""
+    __regid__ = 'syncupdatecwcomputedrtype'
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+    events = ('before_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        check_valid_changes(self._cw, entity)
+        if 'rule' in entity.cw_edited:
+            old, new = entity.cw_edited.oldnewvalue('rule')
+            if old != new:
+                rschema = self._cw.vreg.schema.rschema(entity.name)
+                CWComputedRTypeUpdateOp(self._cw, rschema=rschema,
+                                        entity=entity, rule=new)
+
+
 class AfterDelRelationTypeHook(SyncSchemaHook):
     """before deleting a CWAttribute or CWRelation entity:
     * if this is a final or inlined relation definition, instantiate an
@@ -1053,6 +1126,24 @@
         RDefDelOp(cnx, rdef=rdef)
 
 
+# CWComputedRType hooks #######################################################
+
+class DelCWComputedRTypeHook(SyncSchemaHook):
+    """before deleting a CWComputedRType entity:
+    * check that we don't remove a core relation type
+    * instantiate an operation to delete the relation type on commit
+    """
+    __regid__ = 'syncdelcwcomputedrtype'
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        name = self.entity.name
+        if name in CORE_TYPES:
+            raise validation_error(self.entity, {None: _("can't be deleted")})
+        MemSchemaCWRTypeDel(self._cw, rtype=name)
+
+
 # CWAttribute / CWRelation hooks ###############################################
 
 class AfterAddCWAttributeHook(SyncSchemaHook):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/data-computed/schema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,31 @@
+# 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/>.
+from yams.buildobjs import EntityType, String, Int, SubjectRelation
+
+THISYEAR = 2014
+
+class Person(EntityType):
+    name = String()
+    salaire = Int()
+    birth_year = Int(required=True)
+    travaille = SubjectRelation('Societe')
+    age = Int(formula='Any %d - D WHERE X birth_year D' % THISYEAR)
+
+class Societe(EntityType):
+    nom = String()
+    salaire_total = Int(formula='Any SUM(SA) GROUPBY X WHERE P travaille X, P salaire SA')
--- a/hooks/test/unittest_hooks.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/hooks/test/unittest_hooks.py	Thu Jan 22 17:45:06 2015 +0100
@@ -77,23 +77,23 @@
             entity = cnx.create_entity('Workflow', name=u'wf1',
                                        description_format=u'text/html',
                                        description=u'yo')
-            self.assertEqual(entity.description, u'yo')
+            self.assertEqual(u'yo', entity.description)
             entity = cnx.create_entity('Workflow', name=u'wf2',
                                        description_format=u'text/html',
                                        description=u'<b>yo')
-            self.assertEqual(entity.description, u'<b>yo</b>')
+            self.assertEqual(u'<b>yo</b>', entity.description)
             entity = cnx.create_entity('Workflow', name=u'wf3',
                                        description_format=u'text/html',
                                        description=u'<b>yo</b>')
-            self.assertEqual(entity.description, u'<b>yo</b>')
+            self.assertEqual(u'<b>yo</b>', entity.description)
             entity = cnx.create_entity('Workflow', name=u'wf4',
                                        description_format=u'text/html',
                                        description=u'<b>R&D</b>')
-            self.assertEqual(entity.description, u'<b>R&amp;D</b>')
+            self.assertEqual(u'<b>R&amp;D</b>', entity.description, )
             entity = cnx.create_entity('Workflow', name=u'wf5',
                                        description_format=u'text/html',
                                        description=u"<div>c&apos;est <b>l'ét&eacute;")
-            self.assertEqual(entity.description, u"<div>c'est <b>l'été</b></div>")
+            self.assertEqual(u"<div>c'est <b>l'été</b></div>", entity.description)
 
     def test_nonregr_html_tidy_hook_no_update(self):
         with self.admin_access.client_cnx() as cnx:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/unittest_synccomputed.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,139 @@
+# 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/>.
+"""unit tests for computed attributes/relations hooks"""
+
+from unittest import TestCase
+
+from yams.buildobjs import EntityType, String, Int, SubjectRelation
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.schema import build_schema_from_namespace
+
+
+class FormulaDependenciesMatrixTC(TestCase):
+
+    def simple_schema(self):
+        THISYEAR = 2014
+
+        class Person(EntityType):
+            name = String()
+            salary = Int()
+            birth_year = Int(required=True)
+            works_for = SubjectRelation('Company')
+            age = Int(formula='Any %d - D WHERE X birth_year D' % THISYEAR)
+
+        class Company(EntityType):
+            name = String()
+            total_salary = Int(formula='Any SUM(SA) GROUPBY X WHERE P works_for X, P salary SA')
+
+        schema = build_schema_from_namespace(vars().items())
+        return schema
+
+    def setUp(self):
+        from cubicweb.hooks.synccomputed import _FormulaDependenciesMatrix
+        self.schema = self.simple_schema()
+        self.dependencies = _FormulaDependenciesMatrix(self.schema)
+
+    def test_computed_attributes_by_etype(self):
+        comp_by_etype = self.dependencies.computed_attribute_by_etype
+        self.assertEqual(len(comp_by_etype), 2)
+        values = comp_by_etype['Person']
+        self.assertEqual(len(values), 1)
+        self.assertEqual(values[0].rtype, 'age')
+        values = comp_by_etype['Company']
+        self.assertEqual(len(values), 1)
+        self.assertEqual(values[0].rtype, 'total_salary')
+
+    def test_computed_attribute_by_relation(self):
+        comp_by_rdef = self.dependencies.computed_attribute_by_relation
+        self.assertEqual(len(comp_by_rdef), 1)
+        key, values = iter(comp_by_rdef.iteritems()).next()
+        self.assertEqual(key.rtype, 'works_for')
+        self.assertEqual(len(values), 1)
+        self.assertEqual(values[0].rtype, 'total_salary')
+
+    def test_computed_attribute_by_etype_attrs(self):
+        comp_by_attr = self.dependencies.computed_attribute_by_etype_attrs
+        self.assertEqual(len(comp_by_attr), 1)
+        values = comp_by_attr['Person']
+        self.assertEqual(len(values), 2)
+        values = set((rdef.formula, tuple(v))
+                     for rdef, v in values.iteritems())
+        self.assertEquals(values,
+                          set((('Any 2014 - D WHERE X birth_year D', tuple(('birth_year',))),
+                               ('Any SUM(SA) GROUPBY X WHERE P works_for X, P salary SA', tuple(('salary',)))))
+                          )
+
+
+class ComputedAttributeTC(CubicWebTC):
+    appid = 'data-computed'
+
+    def setup_entities(self, req):
+        self.societe = req.create_entity('Societe', nom=u'Foo')
+        req.create_entity('Person', name=u'Titi', salaire=1000,
+                          travaille=self.societe, birth_year=2001)
+        self.tata = req.create_entity('Person', name=u'Tata', salaire=2000,
+                                      travaille=self.societe, birth_year=1990)
+
+
+    def test_update_on_add_remove_relation(self):
+        """check the rewriting of a computed attribute"""
+        with self.admin_access.web_request() as req:
+            self.setup_entities(req)
+            req.cnx.commit()
+            rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+            self.assertEqual(rset[0][0], 3000)
+            # Add relation.
+            toto = req.create_entity('Person', name=u'Toto', salaire=1500,
+                                   travaille=self.societe, birth_year=1988)
+            req.cnx.commit()
+            rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+            self.assertEqual(rset[0][0], 4500)
+            # Delete relation.
+            toto.cw_set(travaille=None)
+            req.cnx.commit()
+            rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+            self.assertEqual(rset[0][0], 3000)
+
+    def test_recompute_on_attribute_update(self):
+        """check the modification of an attribute triggers the update of the
+        computed attributes that depend on it"""
+        with self.admin_access.web_request() as req:
+            self.setup_entities(req)
+            req.cnx.commit()
+            rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+            self.assertEqual(rset[0][0], 3000)
+            # Update attribute.
+            self.tata.cw_set(salaire=1000)
+            req.cnx.commit()
+            rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+            self.assertEqual(rset[0][0], 2000)
+
+    def test_init_on_entity_creation(self):
+        """check the computed attribute is initialized on entity creation"""
+        with self.admin_access.web_request() as req:
+            p = req.create_entity('Person', name=u'Tata', salaire=2000,
+                                  birth_year=1990)
+            req.cnx.commit()
+            rset = req.execute('Any A, X WHERE X age A, X name "Tata"')
+            self.assertEqual(rset[0][0], 2014 - 1990)
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/i18n/de.po	Thu Jan 22 17:39:07 2015 +0100
+++ b/i18n/de.po	Thu Jan 22 17:45:06 2015 +0100
@@ -258,9 +258,6 @@
 msgid "BoundaryConstraint"
 msgstr "Rand-einschränkung"
 
-msgid "Browse by category"
-msgstr "nach Kategorien navigieren"
-
 msgid "Browse by entity type"
 msgstr "nach Identitätstyp navigieren"
 
@@ -286,6 +283,12 @@
 msgid "CWCache_plural"
 msgstr "Caches"
 
+msgid "CWComputedRType"
+msgstr ""
+
+msgid "CWComputedRType_plural"
+msgstr ""
+
 msgid "CWConstraint"
 msgstr "Einschränkung"
 
@@ -483,6 +486,12 @@
 msgid "Entities"
 msgstr "Entitäten"
 
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr ""
+
 msgid "Entity and relation supported by this source"
 msgstr ""
 
@@ -564,6 +573,9 @@
 msgid "New CWCache"
 msgstr "Neuer Anwendungs-Cache"
 
+msgid "New CWComputedRType"
+msgstr ""
+
 msgid "New CWConstraint"
 msgstr "Neue Einschränkung"
 
@@ -780,6 +792,9 @@
 msgid "This CWCache"
 msgstr "Dieser Anwendungs-Cache"
 
+msgid "This CWComputedRType"
+msgstr ""
+
 msgid "This CWConstraint"
 msgstr "diese Einschränkung"
 
@@ -1243,9 +1258,6 @@
 msgid "and/or between different values"
 msgstr "und/oder zwischen verschiedenen Werten"
 
-msgid "anonymous"
-msgstr "anonym"
-
 msgid "anyrsetview"
 msgstr ""
 
@@ -2122,6 +2134,9 @@
 msgid "define a schema constraint type"
 msgstr "den Typ einer Schema-Einschränkung definieren"
 
+msgid "define a virtual relation type, used to build the instance schema"
+msgstr ""
+
 msgid "define an entity type, used to build the instance schema"
 msgstr "definieren eines Entitätstyps zur Erstellung des Instanz-Schemas"
 
@@ -2195,6 +2210,10 @@
 msgid "description"
 msgstr "Beschreibung"
 
+msgctxt "CWComputedRType"
+msgid "description"
+msgstr ""
+
 msgctxt "CWEType"
 msgid "description"
 msgstr "Beschreibung"
@@ -2234,6 +2253,10 @@
 msgid "description_format"
 msgstr "Format"
 
+msgctxt "CWComputedRType"
+msgid "description_format"
+msgstr ""
+
 msgctxt "CWEType"
 msgid "description_format"
 msgstr "Format"
@@ -2552,6 +2575,13 @@
 msgid "for_user_object"
 msgstr "verwendet die Eigenschaften"
 
+msgid "formula"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "formula"
+msgstr ""
+
 msgid "friday"
 msgstr "Freitag"
 
@@ -3160,6 +3190,10 @@
 msgid "name"
 msgstr "Name"
 
+msgctxt "CWComputedRType"
+msgid "name"
+msgstr ""
+
 msgctxt "CWConstraintType"
 msgid "name"
 msgstr "Name"
@@ -3656,6 +3690,13 @@
 msgid "rss export"
 msgstr ""
 
+msgid "rule"
+msgstr ""
+
+msgctxt "CWComputedRType"
+msgid "rule"
+msgstr ""
+
 msgid "same_as"
 msgstr "identisch mit"
 
@@ -3988,6 +4029,9 @@
 msgid "text/html"
 msgstr "html"
 
+msgid "text/markdown"
+msgstr ""
+
 msgid "text/plain"
 msgstr "Nur Text"
 
@@ -4480,12 +4524,6 @@
 msgid "workflow"
 msgstr "Workflow"
 
-msgid "workflow already has a state of that name"
-msgstr ""
-
-msgid "workflow already has a transition of that name"
-msgstr ""
-
 #, python-format
 msgid "workflow changed to \"%s\""
 msgstr "Workflow geändert in \"%s\""
@@ -4544,6 +4582,12 @@
 #~ msgid "Any"
 #~ msgstr "irgendein"
 
+#~ msgid "Browse by category"
+#~ msgstr "nach Kategorien navigieren"
+
+#~ msgid "anonymous"
+#~ msgstr "anonym"
+
 #~ msgid "can't connect to source %s, some data may be missing"
 #~ msgstr "Keine Verbindung zu der Quelle %s, einige Daten könnten fehlen"
 
--- a/i18n/en.po	Thu Jan 22 17:39:07 2015 +0100
+++ b/i18n/en.po	Thu Jan 22 17:45:06 2015 +0100
@@ -247,9 +247,6 @@
 msgid "BoundaryConstraint"
 msgstr ""
 
-msgid "Browse by category"
-msgstr ""
-
 msgid "Browse by entity type"
 msgstr ""
 
@@ -275,6 +272,12 @@
 msgid "CWCache_plural"
 msgstr "CubicWeb Caches"
 
+msgid "CWComputedRType"
+msgstr "Virtual relation"
+
+msgid "CWComputedRType_plural"
+msgstr "Virtual relations"
+
 msgid "CWConstraint"
 msgstr "Constraint"
 
@@ -461,6 +464,12 @@
 msgid "Entities"
 msgstr ""
 
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr ""
+
 msgid "Entity and relation supported by this source"
 msgstr ""
 
@@ -542,6 +551,9 @@
 msgid "New CWCache"
 msgstr "New cache"
 
+msgid "New CWComputedRType"
+msgstr "New virtual relation"
+
 msgid "New CWConstraint"
 msgstr "New constraint"
 
@@ -756,6 +768,9 @@
 msgid "This CWCache"
 msgstr "This cache"
 
+msgid "This CWComputedRType"
+msgstr "This virtual relation"
+
 msgid "This CWConstraint"
 msgstr "This constraint"
 
@@ -1202,9 +1217,6 @@
 msgid "and/or between different values"
 msgstr ""
 
-msgid "anonymous"
-msgstr ""
-
 msgid "anyrsetview"
 msgstr "rset views"
 
@@ -2077,6 +2089,9 @@
 msgid "define a schema constraint type"
 msgstr ""
 
+msgid "define a virtual relation type, used to build the instance schema"
+msgstr ""
+
 msgid "define an entity type, used to build the instance schema"
 msgstr ""
 
@@ -2146,6 +2161,10 @@
 msgid "description"
 msgstr "description"
 
+msgctxt "CWComputedRType"
+msgid "description"
+msgstr "description"
+
 msgctxt "CWEType"
 msgid "description"
 msgstr "description"
@@ -2185,6 +2204,10 @@
 msgid "description_format"
 msgstr "format"
 
+msgctxt "CWComputedRType"
+msgid "description_format"
+msgstr "format"
+
 msgctxt "CWEType"
 msgid "description_format"
 msgstr "format"
@@ -2501,6 +2524,13 @@
 msgid "for_user_object"
 msgstr "property of"
 
+msgid "formula"
+msgstr "formula"
+
+msgctxt "CWAttribute"
+msgid "formula"
+msgstr "formula"
+
 msgid "friday"
 msgstr ""
 
@@ -3080,6 +3110,10 @@
 msgid "name"
 msgstr "name"
 
+msgctxt "CWComputedRType"
+msgid "name"
+msgstr "name"
+
 msgctxt "CWConstraintType"
 msgid "name"
 msgstr "name"
@@ -3570,6 +3604,13 @@
 msgid "rss export"
 msgstr "RSS export"
 
+msgid "rule"
+msgstr "rule"
+
+msgctxt "CWComputedRType"
+msgid "rule"
+msgstr "rule"
+
 msgid "same_as"
 msgstr "same as"
 
@@ -3891,6 +3932,9 @@
 msgid "text/html"
 msgstr "html"
 
+msgid "text/markdown"
+msgstr "markdown formatted text"
+
 msgid "text/plain"
 msgstr "plain text"
 
@@ -4369,12 +4413,6 @@
 msgid "workflow"
 msgstr ""
 
-msgid "workflow already has a state of that name"
-msgstr ""
-
-msgid "workflow already has a transition of that name"
-msgstr ""
-
 #, python-format
 msgid "workflow changed to \"%s\""
 msgstr ""
--- a/i18n/es.po	Thu Jan 22 17:39:07 2015 +0100
+++ b/i18n/es.po	Thu Jan 22 17:45:06 2015 +0100
@@ -266,9 +266,6 @@
 msgid "BoundaryConstraint"
 msgstr "Restricción de límite"
 
-msgid "Browse by category"
-msgstr "Busca por categoría"
-
 msgid "Browse by entity type"
 msgstr "Busca por tipo de entidad"
 
@@ -294,6 +291,12 @@
 msgid "CWCache_plural"
 msgstr "Caches"
 
+msgid "CWComputedRType"
+msgstr ""
+
+msgid "CWComputedRType_plural"
+msgstr ""
+
 msgid "CWConstraint"
 msgstr "Restricción"
 
@@ -492,6 +495,12 @@
 msgid "Entities"
 msgstr "Entidades"
 
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr ""
+
 msgid "Entity and relation supported by this source"
 msgstr "Entidades y relaciones aceptadas por esta fuente"
 
@@ -573,6 +582,9 @@
 msgid "New CWCache"
 msgstr "Agregar Caché"
 
+msgid "New CWComputedRType"
+msgstr ""
+
 msgid "New CWConstraint"
 msgstr "Agregar Restricción"
 
@@ -790,6 +802,9 @@
 msgid "This CWCache"
 msgstr "Este Caché"
 
+msgid "This CWComputedRType"
+msgstr ""
+
 msgid "This CWConstraint"
 msgstr "Esta Restricción"
 
@@ -1261,9 +1276,6 @@
 msgid "and/or between different values"
 msgstr "y/o entre los diferentes valores"
 
-msgid "anonymous"
-msgstr "anónimo"
-
 msgid "anyrsetview"
 msgstr "vistas rset"
 
@@ -2171,6 +2183,9 @@
 msgid "define a schema constraint type"
 msgstr "Define un tipo de condición de esquema"
 
+msgid "define a virtual relation type, used to build the instance schema"
+msgstr ""
+
 msgid "define an entity type, used to build the instance schema"
 msgstr ""
 "Define un tipo de entidad, usado para construir el esquema de la instancia."
@@ -2245,6 +2260,10 @@
 msgid "description"
 msgstr "Descripción"
 
+msgctxt "CWComputedRType"
+msgid "description"
+msgstr ""
+
 msgctxt "CWEType"
 msgid "description"
 msgstr "Descripción"
@@ -2284,6 +2303,10 @@
 msgid "description_format"
 msgstr "Formato"
 
+msgctxt "CWComputedRType"
+msgid "description_format"
+msgstr ""
+
 msgctxt "CWEType"
 msgid "description_format"
 msgstr "Formato"
@@ -2608,6 +2631,13 @@
 msgid "for_user_object"
 msgstr "Tiene como preferencia"
 
+msgid "formula"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "formula"
+msgstr ""
+
 msgid "friday"
 msgstr "Viernes"
 
@@ -3213,6 +3243,10 @@
 msgid "name"
 msgstr "Nombre"
 
+msgctxt "CWComputedRType"
+msgid "name"
+msgstr ""
+
 msgctxt "CWConstraintType"
 msgid "name"
 msgstr "Nombre"
@@ -3718,6 +3752,13 @@
 msgid "rss export"
 msgstr "Exportación RSS"
 
+msgid "rule"
+msgstr ""
+
+msgctxt "CWComputedRType"
+msgid "rule"
+msgstr ""
+
 msgid "same_as"
 msgstr "Idéntico a"
 
@@ -4052,6 +4093,9 @@
 msgid "text/html"
 msgstr "Usar HTML"
 
+msgid "text/markdown"
+msgstr ""
+
 msgid "text/plain"
 msgstr "Usar Texto simple"
 
@@ -4543,12 +4587,6 @@
 msgid "workflow"
 msgstr "Workflow"
 
-msgid "workflow already has a state of that name"
-msgstr "el workflow posee ya un estado con ese nombre"
-
-msgid "workflow already has a transition of that name"
-msgstr "El Workflow posee ya una transición con ese nombre"
-
 #, python-format
 msgid "workflow changed to \"%s\""
 msgstr "Workflow cambiado a \"%s\""
@@ -4610,6 +4648,12 @@
 #~ msgid "Any"
 #~ msgstr "Cualquiera"
 
+#~ msgid "Browse by category"
+#~ msgstr "Busca por categoría"
+
+#~ msgid "anonymous"
+#~ msgstr "anónimo"
+
 #~ msgid "attribute/relation can't be mapped, only entity and relation types"
 #~ msgstr ""
 #~ "los atributos y las relaciones no pueden ser mapeados, solamente los "
@@ -4649,6 +4693,12 @@
 #~ msgid "web sessions without CNX"
 #~ msgstr "sesiones web sin conexión asociada"
 
+#~ msgid "workflow already has a state of that name"
+#~ msgstr "el workflow posee ya un estado con ese nombre"
+
+#~ msgid "workflow already has a transition of that name"
+#~ msgstr "El Workflow posee ya una transición con ese nombre"
+
 #~ msgid "you may want to specify something for %s"
 #~ msgstr "usted desea quizás especificar algo para la relación %s"
 
--- a/i18n/fr.po	Thu Jan 22 17:39:07 2015 +0100
+++ b/i18n/fr.po	Thu Jan 22 17:45:06 2015 +0100
@@ -4,7 +4,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2012-02-15 16:08+0100\n"
+"PO-Revision-Date: 2014-06-24 13:29+0200\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
 "Language: \n"
@@ -260,9 +260,6 @@
 msgid "BoundaryConstraint"
 msgstr "contrainte de bornes"
 
-msgid "Browse by category"
-msgstr "Naviguer par catégorie"
-
 msgid "Browse by entity type"
 msgstr "Naviguer par type d'entité"
 
@@ -288,6 +285,12 @@
 msgid "CWCache_plural"
 msgstr "Caches applicatifs"
 
+msgid "CWComputedRType"
+msgstr "Relation virtuelle"
+
+msgid "CWComputedRType_plural"
+msgstr "Relations virtuelles"
+
 msgid "CWConstraint"
 msgstr "Contrainte"
 
@@ -486,6 +489,14 @@
 msgid "Entities"
 msgstr "entités"
 
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr ""
+"L'entité %(eid)s a été modifiée depuis votre demande d'édition. Veuillez "
+"recharger cette page et réappliquer vos changements."
+
 msgid "Entity and relation supported by this source"
 msgstr "Entités et relations supportés par cette source"
 
@@ -567,6 +578,9 @@
 msgid "New CWCache"
 msgstr "Nouveau cache applicatif"
 
+msgid "New CWComputedRType"
+msgstr "Nouvelle relation virtuelle"
+
 msgid "New CWConstraint"
 msgstr "Nouvelle contrainte"
 
@@ -784,6 +798,9 @@
 msgid "This CWCache"
 msgstr "Ce cache applicatif"
 
+msgid "This CWComputedRType"
+msgstr "Cette relation virtuelle"
+
 msgid "This CWConstraint"
 msgstr "Cette contrainte"
 
@@ -1255,9 +1272,6 @@
 msgid "and/or between different values"
 msgstr "et/ou entre les différentes valeurs"
 
-msgid "anonymous"
-msgstr "anonyme"
-
 msgid "anyrsetview"
 msgstr "vues pour tout rset"
 
@@ -2167,6 +2181,9 @@
 msgid "define a schema constraint type"
 msgstr "définit un type de contrainte de schema"
 
+msgid "define a virtual relation type, used to build the instance schema"
+msgstr "définit une relation virtuelle"
+
 msgid "define an entity type, used to build the instance schema"
 msgstr "définit un type d'entité"
 
@@ -2240,6 +2257,10 @@
 msgid "description"
 msgstr "description"
 
+msgctxt "CWComputedRType"
+msgid "description"
+msgstr "description"
+
 msgctxt "CWEType"
 msgid "description"
 msgstr "description"
@@ -2279,6 +2300,10 @@
 msgid "description_format"
 msgstr "format"
 
+msgctxt "CWComputedRType"
+msgid "description_format"
+msgstr "format"
+
 msgctxt "CWEType"
 msgid "description_format"
 msgstr "format"
@@ -2603,6 +2628,13 @@
 msgid "for_user_object"
 msgstr "a pour préférence"
 
+msgid "formula"
+msgstr "formule"
+
+msgctxt "CWAttribute"
+msgid "formula"
+msgstr "formule"
+
 msgid "friday"
 msgstr "vendredi"
 
@@ -3208,6 +3240,10 @@
 msgid "name"
 msgstr "nom"
 
+msgctxt "CWComputedRType"
+msgid "name"
+msgstr "nom"
+
 msgctxt "CWConstraintType"
 msgid "name"
 msgstr "nom"
@@ -3716,6 +3752,13 @@
 msgid "rss export"
 msgstr "export RSS"
 
+msgid "rule"
+msgstr "règle"
+
+msgctxt "CWComputedRType"
+msgid "rule"
+msgstr "règle"
+
 msgid "same_as"
 msgstr "identique à"
 
@@ -4049,6 +4092,9 @@
 msgid "text/html"
 msgstr "html"
 
+msgid "text/markdown"
+msgstr "texte au format markdown"
+
 msgid "text/plain"
 msgstr "texte pur"
 
@@ -4545,12 +4591,6 @@
 msgid "workflow"
 msgstr "workflow"
 
-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"
-
 #, python-format
 msgid "workflow changed to \"%s\""
 msgstr "workflow changé à \"%s\""
@@ -4612,6 +4652,12 @@
 #~ 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 "
@@ -4651,6 +4697,12 @@
 #~ 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"
 
--- a/migration.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/migration.py	Thu Jan 22 17:45:06 2015 +0100
@@ -31,6 +31,7 @@
 from logilab.common.configuration import REQUIRED, read_old_config
 from logilab.common.shellutils import ASK
 from logilab.common.changelog import Version
+from logilab.common.deprecation import deprecated
 
 from cubicweb import ConfigurationError, ExecutionError
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
@@ -247,12 +248,13 @@
         local_ctx = self._create_context()
         try:
             import readline
-            from rlcompleter import Completer
+            from cubicweb.toolsutils import CWShellCompleter
         except ImportError:
             # readline not available
             pass
         else:
-            readline.set_completer(Completer(local_ctx).complete)
+            rql_completer = CWShellCompleter(local_ctx)
+            readline.set_completer(rql_completer.complete)
             readline.parse_and_bind('tab: complete')
             home_key = 'HOME'
             if sys.platform == 'win32':
@@ -406,7 +408,11 @@
             self.config.add_cubes(newcubes)
         return newcubes
 
+    @deprecated('[3.20] use drop_cube() instead of remove_cube()')
     def cmd_remove_cube(self, cube, removedeps=False):
+        return self.cmd_drop_cube(cube, removedeps)
+
+    def cmd_drop_cube(self, cube, removedeps=False):
         if removedeps:
             toremove = self.config.expand_cubes([cube])
         else:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.20.0_Any.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,6 @@
+sync_schema_props_perms('State')
+sync_schema_props_perms('state_of')
+sync_schema_props_perms('BaseTransition')
+sync_schema_props_perms('Transition')
+sync_schema_props_perms('WorkflowTransition')
+sync_schema_props_perms('transition_of')
--- a/misc/migration/bootstrapmigration_repository.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Thu Jan 22 17:45:06 2015 +0100
@@ -63,6 +63,14 @@
 
     replace_eid_sequence_with_eid_numrange(session)
 
+if applcubicwebversion < (3, 20, 0) and cubicwebversion >= (3, 20, 0):
+    ss._IGNORED_PROPS.append('formula')
+    add_attribute('CWAttribute', 'formula', commit=False)
+    ss._IGNORED_PROPS.remove('formula')
+    commit()
+    add_entity_type('CWComputedRType')
+    commit()
+
 if applcubicwebversion < (3, 17, 0) and cubicwebversion >= (3, 17, 0):
     try:
         add_cube('sioc', update_database=False)
@@ -260,3 +268,18 @@
 
 if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0):
     add_cube('card', update_database=False)
+
+def sync_constraint_types():
+    """Make sure the repository knows about all constraint types defined in the code"""
+    from cubicweb.schema import CONSTRAINTS
+    repo_constraints = set(row[0] for row in rql('Any N WHERE X is CWConstraintType, X name N'))
+
+    for cstrtype in set(CONSTRAINTS) - repo_constraints:
+        if cstrtype == 'BoundConstraint':
+            # was renamed to BoundaryConstraint, we don't need the old name
+            continue
+        rql('INSERT CWConstraintType X: X name %(name)s', {'name': cstrtype})
+
+    commit()
+
+sync_constraint_types()
--- a/mttransforms.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/mttransforms.py	Thu Jan 22 17:45:06 2015 +0100
@@ -28,7 +28,7 @@
                                  register_pygments_transforms)
 
 from cubicweb.utils import UStringIO
-from cubicweb.uilib import rest_publish, html_publish
+from cubicweb.uilib import rest_publish, markdown_publish, html_publish
 
 HTML_MIMETYPES = ('text/html', 'text/xhtml', 'application/xhtml+xml')
 
@@ -40,6 +40,12 @@
     def _convert(self, trdata):
         return rest_publish(trdata.appobject, trdata.decode())
 
+class markdown_to_html(Transform):
+    inputs = ('text/markdown', 'text/x-markdown')
+    output = 'text/html'
+    def _convert(self, trdata):
+        return markdown_publish(trdata.appobject, trdata.decode())
+
 class html_to_html(Transform):
     inputs = HTML_MIMETYPES
     output = 'text/html'
@@ -53,6 +59,7 @@
 
 ENGINE = TransformEngine()
 ENGINE.add_transform(rest_to_html())
+ENGINE.add_transform(markdown_to_html())
 ENGINE.add_transform(html_to_html())
 
 try:
--- a/predicates.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/predicates.py	Thu Jan 22 17:45:06 2015 +0100
@@ -188,7 +188,6 @@
 from warnings import warn
 from operator import eq
 
-from logilab.common.interface import implements as implements_iface
 from logilab.common.registry import Predicate, objectify_predicate, yes
 
 from yams.schema import BASE_TYPES, role_name
--- a/repoapi.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/repoapi.py	Thu Jan 22 17:45:06 2015 +0100
@@ -212,10 +212,6 @@
         # Connection object
         rset = self._cnx.execute(*args, **kwargs)
         rset.req = self
-        # XXX keep the same behavior as the old dbapi
-        # otherwise multiple tests break.
-        # The little internet kitten is very sad about this situation.
-        rset._rqlst = None
         return rset
 
     @_open_only
--- a/req.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/req.py	Thu Jan 22 17:45:06 2015 +0100
@@ -485,12 +485,16 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
+    def _base_url(self, secure=None):
+        if secure:
+            return self.vreg.config.get('https-url') or self.vreg.config['base-url']
+        return self.vreg.config['base-url']
+
     def base_url(self, secure=None):
         """return the root url of the instance
         """
-        if secure:
-            return self.vreg.config.get('https-url') or self.vreg.config['base-url']
-        return self.vreg.config['base-url']
+        url = self._base_url(secure=secure)
+        return url if url is None else url.rstrip('/') + '/'
 
     # abstract methods to override according to the web front-end #############
 
--- a/rqlrewrite.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/rqlrewrite.py	Thu Jan 22 17:45:06 2015 +0100
@@ -31,7 +31,7 @@
 from logilab.common.graph import has_path
 
 from cubicweb import Unauthorized
-
+from cubicweb.schema import RRQLExpression
 
 def cleanup_solutions(rqlst, solutions):
     for sol in solutions:
@@ -208,11 +208,21 @@
     because it create an unresolvable query (eg no solutions found)
     """
 
+class VariableFromSubQuery(Exception):
+    """flow control exception to indicate that a variable is coming from a
+    subquery, and let parent act accordingly
+    """
+    def __init__(self, variable):
+        self.variable = variable
+
 
 class RQLRewriter(object):
-    """insert some rql snippets into another rql syntax tree
+    """Insert some rql snippets into another rql syntax tree, for security /
+    relation vocabulary. This implies that it should only restrict results of
+    the original query, not generate new ones. Hence, inserted snippets are
+    inserted under an EXISTS node.
 
-    this class *isn't thread safe*
+    This class *isn't thread safe*.
     """
 
     def __init__(self, session):
@@ -338,7 +348,7 @@
     def rewrite(self, select, snippets, kwargs, existingvars=None):
         """
         snippets: (varmap, list of rql expression)
-                  with varmap a *tuple* (select var, snippet var)
+                  with varmap a *dict* {select var: snippet var}
         """
         self.select = select
         # remove_solutions used below require a copy
@@ -350,7 +360,7 @@
         self.pending_keys = []
         self.existingvars = existingvars
         # we have to annotate the rqlst before inserting snippets, even though
-        # we'll have to redo it latter
+        # we'll have to redo it later
         self.annotate(select)
         self.insert_snippets(snippets)
         if not self.exists_snippet and self.u_varname:
@@ -362,7 +372,7 @@
         assert len(newsolutions) >= len(solutions), (
             'rewritten rql %s has lost some solutions, there is probably '
             'something wrong in your schema permission (for instance using a '
-            'RQLExpression which insert a relation which doesn\'t exists in '
+            'RQLExpression which inserts a relation which doesn\'t exist in '
             'the schema)\nOrig solutions: %s\nnew solutions: %s' % (
             select, solutions, newsolutions))
         if len(newsolutions) > len(solutions):
@@ -382,11 +392,10 @@
                 continue
             self.insert_varmap_snippets(varmap, rqlexprs, varexistsmap)
 
-    def insert_varmap_snippets(self, varmap, rqlexprs, varexistsmap):
+    def init_from_varmap(self, varmap, varexistsmap=None):
         self.varmap = varmap
         self.revvarmap = {}
         self.varinfos = []
-        self._insert_scope = None
         for i, (selectvar, snippetvar) in enumerate(varmap):
             assert snippetvar in 'SOX'
             self.revvarmap[snippetvar] = (selectvar, i)
@@ -399,25 +408,35 @@
                 try:
                     vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
                 except KeyError:
-                    # variable may have been moved to a newly inserted subquery
-                    # we should insert snippet in that subquery
-                    subquery = self.select.aliases[selectvar].query
-                    assert len(subquery.children) == 1
-                    subselect = subquery.children[0]
-                    RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)],
-                                                      self.kwargs)
-                    return
+                    vi['stinfo'] = sti = self._subquery_variable(selectvar)
                 if varexistsmap is None:
                     # build an index for quick access to relations
                     vi['rhs_rels'] = {}
-                    for rel in sti['rhsrelations']:
+                    for rel in sti.get('rhsrelations', []):
                         vi['rhs_rels'].setdefault(rel.r_type, []).append(rel)
                     vi['lhs_rels'] = {}
-                    for rel in sti['relations']:
-                        if not rel in sti['rhsrelations']:
+                    for rel in sti.get('relations', []):
+                        if not rel in sti.get('rhsrelations', []):
                             vi['lhs_rels'].setdefault(rel.r_type, []).append(rel)
                 else:
                     vi['rhs_rels'] = vi['lhs_rels'] = {}
+
+    def _subquery_variable(self, selectvar):
+        raise VariableFromSubQuery(selectvar)
+
+    def insert_varmap_snippets(self, varmap, rqlexprs, varexistsmap):
+        try:
+            self.init_from_varmap(varmap, varexistsmap)
+        except VariableFromSubQuery, ex:
+            # variable may have been moved to a newly inserted subquery
+            # we should insert snippet in that subquery
+            subquery = self.select.aliases[ex.variable].query
+            assert len(subquery.children) == 1, subquery
+            subselect = subquery.children[0]
+            RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)],
+                                              self.kwargs)
+            return
+        self._insert_scope = None
         previous = None
         inserted = False
         for rqlexpr in rqlexprs:
@@ -450,6 +469,11 @@
         finally:
             self.existingvars = existing
 
+    def _inserted_root(self, new):
+        if not isinstance(new, (n.Exists, n.Not)):
+            new = n.Exists(new)
+        return new
+
     def _insert_snippet(self, varmap, previous, new):
         """insert `new` snippet into the syntax tree, which have been rewritten
         using `varmap`. In cases where an action is protected by several rql
@@ -474,8 +498,7 @@
                 self.insert_pending()
                 #self._insert_scope = None
                 return new
-            if not isinstance(new, (n.Exists, n.Not)):
-                new = n.Exists(new)
+            new = self._inserted_root(new)
             if previous is None:
                 insert_scope.add_restriction(new)
             else:
@@ -869,3 +892,40 @@
         if self._insert_scope is None:
             return self.select
         return self._insert_scope.stmt
+
+
+class RQLRelationRewriter(RQLRewriter):
+    """Insert some rql snippets into another rql syntax tree, replacing computed
+    relations by their associated rule.
+
+    This class *isn't thread safe*.
+    """
+    def __init__(self, session):
+        super(RQLRelationRewriter, self).__init__(session)
+        self.rules = {}
+        for rschema in self.schema.iter_computed_relations():
+            self.rules[rschema.type] = RRQLExpression(rschema.rule)
+
+    def rewrite(self, union, kwargs=None):
+        self.kwargs = kwargs
+        self.removing_ambiguity = False
+        self.existingvars = None
+        self.pending_keys = None
+        for relation in union.iget_nodes(n.Relation):
+            if relation.r_type in self.rules:
+                self.select = relation.stmt
+                self.solutions = solutions = self.select.solutions[:]
+                self.current_expr = self.rules[relation.r_type]
+                self._insert_scope = relation.scope
+                self.rewritten = {}
+                lhs, rhs = relation.get_variable_parts()
+                varmap = {lhs.name: 'S', rhs.name: 'O'}
+                self.init_from_varmap(tuple(sorted(varmap.items())))
+                self.insert_snippet(varmap, self.current_expr.snippet_rqlst)
+                self.select.remove_node(relation)
+
+    def _subquery_variable(self, selectvar):
+        return self.select.aliases[selectvar].stinfo
+
+    def _inserted_root(self, new):
+        return new
--- a/rset.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/rset.py	Thu Jan 22 17:45:06 2015 +0100
@@ -19,6 +19,8 @@
 
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
+
 from logilab.common.decorators import cached, clear_cache, copy_cache
 
 from rql import nodes, stmts
@@ -46,6 +48,9 @@
     """
 
     def __init__(self, results, rql, args=None, description=None, rqlst=None):
+        if rqlst is not None:
+            warn('[3.20] rqlst parameter is deprecated',
+                 DeprecationWarning, stacklevel=2)
         self.rows = results
         self.rowcount = results and len(results) or 0
         # original query and arguments
@@ -57,10 +62,6 @@
             self.description = []
         else:
             self.description = description
-        # parsed syntax tree
-        if rqlst is not None:
-            rqlst.schema = None # reset schema in case of pyro transfert
-        self._rqlst = rqlst
         # set to (limit, offset) when a result set is limited using the
         # .limit method
         self.limited = None
@@ -550,18 +551,11 @@
 
     @cached
     def syntax_tree(self):
-        """return the syntax tree (:class:`rql.stmts.Union`) for the originating
-        query. You can expect it to have solutions computed but it won't be
-        annotated (you usually don't need that for simple introspection).
+        """return the syntax tree (:class:`rql.stmts.Union`) for the
+        originating query. You can expect it to have solutions
+        computed and it will be properly annotated.
         """
-        if self._rqlst:
-            rqlst = self._rqlst.copy()
-            # to avoid transport overhead when pyro is used, the schema has been
-            # unset from the syntax tree
-            rqlst.schema = self.req.vreg.schema
-        else:
-            rqlst = self.req.vreg.parse(self.req, self.rql, self.args)
-        return rqlst
+        return self.req.vreg.parse(self.req, self.rql, self.args)
 
     @cached
     def column_types(self, col):
--- a/schema.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/schema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -37,9 +37,11 @@
      RelationDefinitionSchema, PermissionMixIn, role_name
 from yams.constraints import BaseConstraint, FormatConstraint
 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
-                         obsolete as yobsolete, cleanup_sys_modules)
+                         obsolete as yobsolete, cleanup_sys_modules,
+                         fill_schema_from_namespace)
 
 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
+from rql.analyze import ETypeResolver
 
 import cubicweb
 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
@@ -81,7 +83,7 @@
 
 # set of entity and relation types used to build the schema
 SCHEMA_TYPES = set((
-    'CWEType', 'CWRType', 'CWAttribute', 'CWRelation',
+    'CWEType', 'CWRType', 'CWComputedRType', 'CWAttribute', 'CWRelation',
     'CWConstraint', 'CWConstraintType', 'CWUniqueTogetherConstraint',
     'RQLExpression',
     'specializes',
@@ -106,6 +108,11 @@
 ybo.ETYPE_PROPERTIES += ('eid',)
 ybo.RTYPE_PROPERTIES += ('eid',)
 
+def build_schema_from_namespace(items):
+    schema = CubicWebSchema('noname')
+    fill_schema_from_namespace(schema, items, register_base_types=False)
+    return schema
+
 # Bases for manipulating RQL in schema #########################################
 
 def guess_rrqlexpr_mainvars(expression):
@@ -118,7 +125,8 @@
     if 'U' in defined:
         mainvars.add('U')
     if not mainvars:
-        raise Exception('unable to guess selection variables')
+        raise BadSchemaDefinition('unable to guess selection variables in %r'
+                                  % expression)
     return mainvars
 
 def split_expression(rqlstring):
@@ -136,6 +144,44 @@
     return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
 
 
+def _check_valid_formula(rdef, formula_rqlst):
+    """Check the formula is a valid RQL query with some restriction (no union,
+    single selected node, etc.), raise BadSchemaDefinition if not
+    """
+    if len(formula_rqlst.children) != 1:
+        raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+                                  'can not use UNION in formula %(form)r' %
+                                  {'attr' : rdef.rtype,
+                                   'etype' : rdef.subject.type,
+                                   'form' : rdef.formula})
+    select = formula_rqlst.children[0]
+    if len(select.selection) != 1:
+        raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+                                  'can only select one term in formula %(form)r' %
+                                  {'attr' : rdef.rtype,
+                                   'etype' : rdef.subject.type,
+                                   'form' : rdef.formula})
+    term = select.selection[0]
+    types = set(term.get_type(sol) for sol in select.solutions)
+    if len(types) != 1:
+        raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+                                  'multiple possible types (%(types)s) for formula %(form)r' %
+                                  {'attr' : rdef.rtype,
+                                   'etype' : rdef.subject.type,
+                                   'types' : list(types),
+                                   'form' : rdef.formula})
+    computed_type = types.pop()
+    expected_type = rdef.object.type
+    if computed_type != expected_type:
+        raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+                                  'computed attribute type (%(comp_type)s) mismatch with '
+                                  'specified type (%(attr_type)s)' %
+                                  {'attr' : rdef.rtype,
+                                   'etype' : rdef.subject.type,
+                                   'comp_type' : computed_type,
+                                   'attr_type' : expected_type})
+
+
 class RQLExpression(object):
     """Base class for RQL expression used in schema (constraints and
     permissions)
@@ -146,6 +192,7 @@
     # to be defined in concrete classes
     rqlst = None
     predefined_variables = None
+    full_rql = None
 
     def __init__(self, expression, mainvars, eid):
         """
@@ -562,7 +609,7 @@
 PermissionMixIn.set_action_permissions = set_action_permissions
 
 def has_local_role(self, action):
-    """return true if the action *may* be granted locally (eg either rql
+    """return true if the action *may* be granted locally (i.e. either rql
     expressions or the owners group are used in security definition)
 
     XXX this method is only there since we don't know well how to deal with
@@ -1001,6 +1048,59 @@
     def schema_by_eid(self, eid):
         return self._eid_index[eid]
 
+    def iter_computed_attributes(self):
+        for relation in self.relations():
+            for rdef in relation.rdefs.itervalues():
+                if rdef.final and rdef.formula is not None:
+                    yield rdef
+
+    def iter_computed_relations(self):
+        for relation in self.relations():
+            if relation.rule:
+                yield relation
+
+    def finalize(self):
+        super(CubicWebSchema, self).finalize()
+        self.finalize_computed_attributes()
+        self.finalize_computed_relations()
+
+    def finalize_computed_attributes(self):
+        """Check computed attributes validity (if any), else raise
+        `BadSchemaDefinition`
+        """
+        analyzer = ETypeResolver(self)
+        for rdef in self.iter_computed_attributes():
+            rqlst = parse(rdef.formula)
+            select = rqlst.children[0]
+            analyzer.visit(select)
+            _check_valid_formula(rdef, rqlst)
+            rdef.formula_select = select # avoid later recomputation
+
+
+    def finalize_computed_relations(self):
+        """Build relation definitions for computed relations
+
+        The subject and object types are infered using rql analyzer.
+        """
+        analyzer = ETypeResolver(self)
+        for rschema in self.iter_computed_relations():
+            # XXX rule is valid if both S and O are defined and not in an exists
+            rqlexpr = RRQLExpression(rschema.rule)
+            rqlst = rqlexpr.snippet_rqlst
+            analyzer.visit(rqlst)
+            couples = set((sol['S'], sol['O']) for sol in rqlst.solutions)
+            for subjtype, objtype in couples:
+                if self[objtype].final:
+                    raise BadSchemaDefinition('computed relations cannot be final')
+                rdef = ybo.RelationDefinition(
+                    subjtype, rschema.type, objtype)
+                rdef.infered = True
+                self.add_relation_def(rdef)
+
+    def rebuild_infered_relations(self):
+        super(CubicWebSchema, self).rebuild_infered_relations()
+        self.finalize_computed_relations()
+
 
 # additional cw specific constraints ###########################################
 
@@ -1038,16 +1138,16 @@
 class RQLVocabularyConstraint(BaseRQLConstraint):
     """the rql vocabulary constraint:
 
-    limit the proposed values to a set of entities returned by a rql query,
+    limits the proposed values to a set of entities returned by an rql query,
     but this is not enforced at the repository level
 
-     `expression` is additional rql restriction that will be added to
-     a predefined query, where the S and O variables respectivly represent
-     the subject and the object of the relation
+    `expression` is an additional rql restriction that will be added to
+    a predefined query, where the S and O variables respectively represent
+    the subject and the object of the relation
 
-     `mainvars` is a set of variables that should be used as selection variable
-     (eg `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be
-     done to guess it according to variable used in the expression.
+    `mainvars` is a set of variables that should be used as selection variables
+    (i.e. `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be
+    made to guess it based on the variables used in the expression.
     """
 
     def repo_check(self, session, eidfrom, rtype, eidto):
@@ -1263,6 +1363,7 @@
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
 
+
 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
--- a/schemas/bootstrap.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/schemas/bootstrap.py	Thu Jan 22 17:45:06 2015 +0100
@@ -57,6 +57,16 @@
     final = Boolean(description=_('automatic'))
 
 
+class CWComputedRType(EntityType):
+    """define a virtual relation type, used to build the instance schema"""
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+    name = String(required=True, indexed=True, internationalizable=True,
+                  unique=True, maxsize=64)
+    description = RichString(internationalizable=True,
+                             description=_('semantic description of this relation type'))
+    rule = String(required=True)
+
+
 class CWAttribute(EntityType):
     """define a final relation: link a final relation type from a non final
     entity to a final entity type.
@@ -80,6 +90,7 @@
                          description=_('subject/object cardinality'))
     ordernum = Int(description=('control subject entity\'s relations order'), default=0)
 
+    formula = String(maxsize=2048)
     indexed = Boolean(description=_('create an index for quick search on this attribute'))
     fulltextindexed = Boolean(description=_('index this attribute\'s value in the plain text index'))
     internationalizable = Boolean(description=_('is this attribute\'s value translatable'))
--- a/schemas/workflow.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/schemas/workflow.py	Thu Jan 22 17:45:06 2015 +0100
@@ -24,7 +24,7 @@
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
                             SubjectRelation,
                             RichString, String, Int)
-from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
+from cubicweb.schema import RQLConstraint
 from cubicweb.schemas import (PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS,
                               RO_REL_PERMS)
 
@@ -62,11 +62,8 @@
     workflows
     """
     __permissions__ = PUB_SYSTEM_ENTITY_PERMS
-
-    name = String(required=True, indexed=True, internationalizable=True,
-                  maxsize=256,
-                  constraints=[RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N', 'Y',
-                                                   _('workflow already has a state of that name'))])
+    __unique_together__ = [('name', 'state_of')]
+    name = String(required=True, indexed=True, internationalizable=True, maxsize=256)
     description = RichString(default_format='text/rest',
                              description=_('semantic description of this state'))
 
@@ -76,27 +73,21 @@
                                          constraints=[RQLConstraint('S state_of WF, O transition_of WF',
                                                                     msg=_('state and transition don\'t belong the the same workflow'))],
                                          description=_('allowed transitions from this state'))
-    state_of = SubjectRelation('Workflow', cardinality='1*', composite='object',
-                               description=_('workflow to which this state belongs'),
-                               constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N', 'Y',
-                                                                _('workflow already has a state of that name'))])
+    state_of = SubjectRelation('Workflow', cardinality='1*', composite='object', inlined=True,
+                               description=_('workflow to which this state belongs'))
 
 
 class BaseTransition(EntityType):
     """abstract base class for transitions"""
     __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+    __unique_together__ = [('name', 'transition_of')]
 
-    name = String(required=True, indexed=True, internationalizable=True,
-                  maxsize=256,
-                  constraints=[RQLUniqueConstraint('S name N, S transition_of WF, Y transition_of WF, Y name N', 'Y',
-                                                   _('workflow already has a transition of that name'))])
+    name = String(required=True, indexed=True, internationalizable=True, maxsize=256)
     type = String(vocabulary=(_('normal'), _('auto')), default='normal')
     description = RichString(description=_('semantic description of this transition'))
 
-    transition_of = SubjectRelation('Workflow', cardinality='1*', composite='object',
-                                    description=_('workflow to which this transition belongs'),
-                                    constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N', 'Y',
-                                                                     _('workflow already has a transition of that name'))])
+    transition_of = SubjectRelation('Workflow', cardinality='1*', composite='object', inlined=True,
+                                    description=_('workflow to which this transition belongs'))
 
 
 class require_group(RelationDefinition):
--- a/server/__init__.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/__init__.py	Thu Jan 22 17:45:06 2015 +0100
@@ -89,7 +89,7 @@
 DBG_ALL  = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE
 
 _SECURITY_ITEMS = []
-_SECURITY_CAPS = ['read', 'add', 'update', 'delete']
+_SECURITY_CAPS = ['read', 'add', 'update', 'delete', 'transition']
 
 #: current debug mode
 DEBUG = 0
@@ -196,7 +196,7 @@
     user = session.create_entity('CWUser', login=login, upassword=pwd)
     for group in groups:
         session.execute('SET U in_group G WHERE U eid %(u)s, G name %(group)s',
-                        {'u': user.eid, 'group': group})
+                        {'u': user.eid, 'group': unicode(group)})
     return user
 
 def init_repository(config, interactive=True, drop=False, vreg=None,
@@ -331,6 +331,7 @@
         mhandler.cmd_exec_event_script('pre%s' % event, apphome=True)
         # enter instance'schema into the database
         serialize_schema(cnx, schema)
+        cnx.commit()
         # execute cubicweb's post<event> script
         mhandler.cmd_exec_event_script('post%s' % event)
         # execute cubes'post<event> script if any
--- a/server/hook.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/hook.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -415,10 +415,6 @@
 for event in ALL_HOOKS:
     CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
 
-@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)')
-def entity_oldnewvalue(entity, attr):
-    return entity.cw_edited.oldnewvalue(attr)
-
 
 # some hook specific predicates #################################################
 
@@ -763,10 +759,6 @@
 
     def handle_event(self, event):
         """delegate event handling to the opertaion"""
-        if event == 'postcommit_event' and hasattr(self, 'commit_event'):
-            warn('[3.10] %s: commit_event method has been replaced by postcommit_event'
-                 % self.__class__, DeprecationWarning)
-            self.commit_event() # pylint: disable=E1101
         getattr(self, event)()
 
     def precommit_event(self):
@@ -903,58 +895,6 @@
         return self._container
 
 
-@deprecated('[3.10] use opcls.get_instance(cnx, **opkwargs).add_data(value)')
-def set_operation(cnx, datakey, value, opcls, containercls=set, **opkwargs):
-    """Function to ease applying a single operation on a set of data, avoiding
-    to create as many as operation as they are individual modification. You
-    should try to use this instead of creating on operation for each `value`,
-    since handling operations becomes coslty on massive data import.
-
-    Arguments are:
-
-    * `cnx`, the current connection
-
-    * `datakey`, a specially forged key that will be used as key in
-      cnx.transaction_data
-
-    * `value` that is the actual payload of an individual operation
-
-    * `opcls`, the class of the operation. An instance is created on the first
-      call for the given key, and then subsequent calls will simply add the
-      payload to the container (hence `opkwargs` is only used on that first
-      call)
-
-    * `containercls`, the container class that should be instantiated to hold
-      payloads.  An instance is created on the first call for the given key, and
-      then subsequent calls will add the data to the existing container. Default
-      to a set. Give `list` if you want to keep arrival ordering.
-
-    * more optional parameters to give to the operation (here the rtype which do not
-      vary accross operations).
-
-    The body of the operation must then iterate over the values that have been mapped
-    in the transaction_data dictionary to the forged key, e.g.:
-
-    .. sourcecode:: python
-
-           for value in self._cw.transaction_data.pop(datakey):
-               ...
-
-    .. Note::
-       **poping** the key from `transaction_data` is not an option, else you may
-       get unexpected data loss in some case of nested hooks.
-    """
-    try:
-        # Search for cnx.transaction_data[`datakey`] (expected to be a set):
-        # if found, simply append `value`
-        _container_add(cnx.transaction_data[datakey], value)
-    except KeyError:
-        # else, initialize it to containercls([`value`]) and instantiate the given
-        # `opcls` operation class with additional keyword arguments
-        opcls(cnx, **opkwargs)
-        cnx.transaction_data[datakey] = containercls()
-        _container_add(cnx.transaction_data[datakey], value)
-
 
 class LateOperation(Operation):
     """special operation which should be called after all possible (ie non late)
--- a/server/migractions.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/migractions.py	Thu Jan 22 17:45:06 2015 +0100
@@ -112,7 +112,7 @@
             # notify we're starting maintenance (called instead of server_start
             # which is called on regular start
             repo.hm.call_hooks('server_maintenance', repo=repo)
-        if not schema and not getattr(config, 'quick_start', False):
+        if not schema and not config.quick_start:
             insert_lperms = self.repo.get_versions()['cubicweb'] < (3, 14, 0) and 'localperms' in config.available_cubes()
             if insert_lperms:
                 cubes = config._cubes
@@ -320,7 +320,6 @@
         """cached group mapping"""
         return ss.group_mapping(self.cnx)
 
-    @cached
     def cstrtype_mapping(self):
         """cached constraint types mapping"""
         return ss.cstrtype_mapping(self.cnx)
@@ -579,6 +578,9 @@
         """
         subjtype, objtype = str(subjtype), str(objtype)
         rschema = self.fs_schema.rschema(rtype)
+        if rschema.rule:
+            raise ExecutionError('Cannot synchronize a relation definition for a '
+                                 'computed relation (%s)' % rschema)
         reporschema = self.repo.schema.rschema(rschema)
         if (subjtype, rschema, objtype) in self._synchronized:
             return
@@ -687,8 +689,8 @@
                 self.cmd_exec_event_script('postcreate', cube)
                 self.commit()
 
-    def cmd_remove_cube(self, cube, removedeps=False):
-        removedcubes = super(ServerMigrationHelper, self).cmd_remove_cube(
+    def cmd_drop_cube(self, cube, removedeps=False):
+        removedcubes = super(ServerMigrationHelper, self).cmd_drop_cube(
             cube, removedeps)
         if not removedcubes:
             return
@@ -1018,11 +1020,13 @@
         if rtype in reposchema:
             print 'warning: relation type %s is already known, skip addition' % (
                 rtype)
+        elif rschema.rule:
+            ss.execschemarql(execute, rschema, ss.crschema2rql(rschema))
         else:
             # register the relation into CWRType and insert necessary relation
             # definitions
             ss.execschemarql(execute, rschema, ss.rschema2rql(rschema, addrdef=False))
-        if addrdef:
+        if not rschema.rule and addrdef:
             self.commit()
             gmap = self.group_mapping()
             cmap = self.cstrtype_mapping()
@@ -1057,9 +1061,10 @@
 
     def cmd_drop_relation_type(self, rtype, commit=True):
         """unregister an existing relation type"""
-        # unregister the relation from CWRType
         self.rqlexec('DELETE CWRType X WHERE X name %r' % rtype,
                      ask_confirm=self.verbosity>=2)
+        self.rqlexec('DELETE CWComputedRType X WHERE X name %r' % rtype,
+                     ask_confirm=self.verbosity>=2)
         if commit:
             self.commit()
 
@@ -1086,6 +1091,9 @@
         schema definition file
         """
         rschema = self.fs_schema.rschema(rtype)
+        if rschema.rule:
+            raise ExecutionError('Cannot add a relation definition for a '
+                                 'computed relation (%s)' % rschema)
         if not rtype in self.repo.schema:
             self.cmd_add_relation_type(rtype, addrdef=False, commit=True)
         if (subjtype, objtype) in self.repo.schema.rschema(rtype).rdefs:
@@ -1113,6 +1121,9 @@
     def cmd_drop_relation_definition(self, subjtype, rtype, objtype, commit=True):
         """unregister an existing relation definition"""
         rschema = self.repo.schema.rschema(rtype)
+        if rschema.rule:
+            raise ExecutionError('Cannot drop a relation definition for a '
+                                 'computed relation (%s)' % rschema)
         # unregister the definition from CWAttribute or CWRelation
         if rschema.final:
             etype = 'CWAttribute'
@@ -1272,12 +1283,12 @@
                 assert 'wf_info_for' in eschema.objrels, _missing_wf_rel(etype)
             rset = self.rqlexec(
                 'SET X workflow_of ET WHERE X eid %(x)s, ET name %(et)s',
-                {'x': wf.eid, 'et': etype}, ask_confirm=False)
+                {'x': wf.eid, 'et': unicode(etype)}, ask_confirm=False)
             assert rset, 'unexistant entity type %s' % etype
             if default:
                 self.rqlexec(
                     'SET ET default_workflow X WHERE X eid %(x)s, ET name %(et)s',
-                    {'x': wf.eid, 'et': etype}, ask_confirm=False)
+                    {'x': wf.eid, 'et': unicode(etype)}, ask_confirm=False)
         if commit:
             self.commit()
         return wf
@@ -1312,7 +1323,7 @@
         try:
             prop = self.rqlexec(
                 'CWProperty X WHERE X pkey %(k)s, NOT X for_user U',
-                {'k': pkey}, ask_confirm=False).get_entity(0, 0)
+                {'k': unicode(pkey)}, ask_confirm=False).get_entity(0, 0)
         except Exception:
             self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value)
         else:
--- a/server/querier.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/querier.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -28,6 +28,7 @@
 from yams import BASE_TYPES
 
 from cubicweb import ValidationError, Unauthorized, UnknownEid
+from cubicweb.rqlrewrite import RQLRelationRewriter
 from cubicweb import Binary, server
 from cubicweb.rset import ResultSet
 
@@ -72,7 +73,44 @@
     except AttributeError:
         return cnx.entity_metas(term.eval(args))['type']
 
-def check_read_access(cnx, rqlst, solution, args):
+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
+    """
+    # use `term_etype` since we've to deal with rewritten constants here,
+    # when used as an external source by another repository.
+    # XXX what about local read security w/ those rewritten constants...
+    # XXX constants can also happen in some queries generated by req.find()
+    DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
+    schema = cnx.repo.schema
+    user = cnx.user
+    if select.where is not None:
+        for rel in select.where.iget_nodes(Relation):
+            for solution in select.solutions:
+                # XXX has_text may have specific perm ?
+                if rel.r_type in READ_ONLY_RTYPES:
+                    continue
+                rschema = schema.rschema(rel.r_type)
+                if rschema.final:
+                    eschema = schema.eschema(term_etype(cnx, rel.children[0],
+                                             solution, args))
+                    rdef = eschema.rdef(rschema)
+                else:
+                    rdef = rschema.rdef(term_etype(cnx, rel.children[0],
+                                                   solution, args),
+                                        term_etype(cnx, rel.children[1].children[0],
+                                                   solution, args))
+                if not user.matching_groups(rdef.get_groups('read')):
+                    if DBG:
+                        print ('check_read_access: %s %s does not match %s' %
+                               (rdef, user.groups, rdef.get_groups('read')))
+                    # XXX rqlexpr not allowed
+                    raise Unauthorized('read', rel.r_type)
+                if DBG:
+                    print ('check_read_access: %s %s matches %s' %
+                           (rdef, user.groups, rdef.get_groups('read')))
+
+def get_local_checks(cnx, rqlst, solution):
     """Check that the given user has credentials to access data read by the
     query and return a dict defining necessary "local checks" (i.e. rql
     expression in read permission defined in the schema) where no group grants
@@ -80,50 +118,27 @@
 
     Returned dictionary's keys are variable names and values the rql expressions
     for this variable (with the given solution).
+
+    Raise :exc:`Unauthorized` if access is known to be defined, i.e. if there is
+    no matching group and no local permissions.
     """
-    # use `term_etype` since we've to deal with rewritten constants here,
-    # when used as an external source by another repository.
-    # XXX what about local read security w/ those rewritten constants...
     DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
     schema = cnx.repo.schema
-    if rqlst.where is not None:
-        for rel in rqlst.where.iget_nodes(Relation):
-            # XXX has_text may have specific perm ?
-            if rel.r_type in READ_ONLY_RTYPES:
-                continue
-            rschema = schema.rschema(rel.r_type)
-            if rschema.final:
-                eschema = schema.eschema(term_etype(cnx, rel.children[0],
-                                                    solution, args))
-                rdef = eschema.rdef(rschema)
-            else:
-                rdef = rschema.rdef(term_etype(cnx, rel.children[0],
-                                               solution, args),
-                                    term_etype(cnx, rel.children[1].children[0],
-                                               solution, args))
-            if not cnx.user.matching_groups(rdef.get_groups('read')):
-                if DBG:
-                    print ('check_read_access: %s %s does not match %s' %
-                           (rdef, cnx.user.groups, rdef.get_groups('read')))
-                # XXX rqlexpr not allowed
-                raise Unauthorized('read', rel.r_type)
-            if DBG:
-                print ('check_read_access: %s %s matches %s' %
-                       (rdef, cnx.user.groups, rdef.get_groups('read')))
+    user = cnx.user
     localchecks = {}
     # iterate on defined_vars and not on solutions to ignore column aliases
     for varname in rqlst.defined_vars:
         eschema = schema.eschema(solution[varname])
         if eschema.final:
             continue
-        if not cnx.user.matching_groups(eschema.get_groups('read')):
+        if not user.matching_groups(eschema.get_groups('read')):
             erqlexprs = eschema.get_rqlexprs('read')
             if not erqlexprs:
                 ex = Unauthorized('read', solution[varname])
                 ex.var = varname
                 if DBG:
                     print ('check_read_access: %s %s %s %s' %
-                           (varname, eschema, cnx.user.groups, eschema.get_groups('read')))
+                           (varname, eschema, user.groups, eschema.get_groups('read')))
                 raise ex
             # don't insert security on variable only referenced by 'NOT X relation Y' or
             # 'NOT EXISTS(X relation Y)'
@@ -133,7 +148,8 @@
                      if (not schema.rschema(r.r_type).final
                          and ((isinstance(r.parent, Exists) and r.parent.neged(strict=True))
                               or isinstance(r.parent, Not)))])
-                != len(varinfo['relations'])):
+                !=
+                len(varinfo['relations'])):
                 localchecks[varname] = erqlexprs
     return localchecks
 
@@ -258,7 +274,7 @@
         newsolutions = []
         for solution in rqlst.solutions:
             try:
-                localcheck = check_read_access(cnx, rqlst, solution, self.args)
+                localcheck = get_local_checks(cnx, rqlst, solution)
             except Unauthorized as ex:
                 msg = 'remove %s from solutions since %s has no %s access to %s'
                 msg %= (solution, cnx.user.login, ex.args[0], ex.args[1])
@@ -561,7 +577,6 @@
                     cachekey = self._repo.querier_cache_key(cnx, rql, args,
                                                             eidkeys)
             self._rql_cache[cachekey] = rqlst
-        orig_rqlst = rqlst
         if rqlst.TYPE != 'select':
             if cnx.read_security:
                 check_no_password_selected(rqlst)
@@ -573,10 +588,14 @@
             if cnx.read_security:
                 for select in rqlst.children:
                     check_no_password_selected(select)
+                    check_relations_read_access(cnx, select, args)
             # on select query, always copy the cached rqlst so we don't have to
             # bother modifying it. This is not necessary on write queries since
             # a new syntax tree is built from them.
             rqlst = rqlst.copy()
+            # Rewrite computed relations
+            rewriter = RQLRelationRewriter(cnx)
+            rewriter.rewrite(rqlst, args)
             self._annotate(rqlst)
             if args:
                 # different SQL generated when some argument is None or not (IS
@@ -626,7 +645,7 @@
             # FIXME: get number of affected entities / relations on non
             # selection queries ?
         # return a result set object
-        return ResultSet(results, rql, args, descr, orig_rqlst)
+        return ResultSet(results, rql, args, descr)
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
--- a/server/repository.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/repository.py	Thu Jan 22 17:45:06 2015 +0100
@@ -338,8 +338,7 @@
             except Exception as ex:
                 import traceback
                 traceback.print_exc()
-                raise (Exception('Is the database initialised ? (cause: %s)' % ex),
-                       None, sys.exc_info()[-1])
+                raise Exception('Is the database initialised ? (cause: %s)' % ex)
         return appschema
 
     def _prepare_startup(self):
@@ -651,8 +650,8 @@
                                    query_attrs)
             return rset.rows
 
-    def connect(self, login, **kwargs):
-        """open a session for a given user
+    def new_session(self, login, **kwargs):
+        """open a new session for a given user
 
         raise `AuthenticationError` if the authentication failed
         raise `ConnectionError` if we can't open a connection
@@ -678,7 +677,11 @@
             # commit connection at this point in case write operation has been
             # done during `session_open` hooks
             cnx.commit()
-        return session.sessionid
+        return session
+
+    def connect(self, login, **kwargs):
+        """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):
@@ -1161,7 +1164,7 @@
     def glob_add_entity(self, cnx, edited):
         """add an entity to the repository
 
-        the entity eid should originaly be None and a unique eid is assigned to
+        the entity eid should originally be None and a unique eid is assigned to
         the entity instance
         """
         entity = edited.entity
--- a/server/schemaserial.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/schemaserial.py	Thu Jan 22 17:45:06 2015 +0100
@@ -87,6 +87,27 @@
     """
     repo = cnx.repo
     dbhelper = repo.system_source.dbhelper
+
+    # Computed Rtype
+    with cnx.ensure_cnx_set:
+        tables = set(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")
+        has_computed_attributes = True
+    except Exception:
+        cnx.rollback()
+        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'")
@@ -100,6 +121,7 @@
     copiedeids = set()
     permsidx = deserialize_ertype_permissions(cnx)
     schema.reading_from_database = True
+    # load every entity types
     for eid, etype, desc in cnx.execute(
         'Any X, N, D WHERE X is CWEType, X name N, X description D',
         build_descr=False):
@@ -148,6 +170,7 @@
         eschema = schema.add_entity_type(
             ybo.EntityType(name=etype, description=desc, eid=eid))
         set_perms(eschema, permsidx)
+    # load inheritance relations
     for etype, stype in cnx.execute(
         'Any XN, ETN WHERE X is CWEType, X name XN, X specializes ET, ET name ETN',
         build_descr=False):
@@ -155,6 +178,7 @@
         stype = ETYPE_NAME_MAP.get(stype, stype)
         schema.eschema(etype)._specialized_type = stype
         schema.eschema(stype)._specialized_by.append(etype)
+    # 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, '
         'X symmetric S, X inlined I, X fulltext_container FTC', build_descr=False):
@@ -163,6 +187,7 @@
             ybo.RelationType(name=rtype, description=desc,
                              symmetric=bool(sym), inlined=bool(il),
                              fulltext_container=ftc, eid=eid))
+    # remains to load every relation definitions (ie relations and attributes)
     cstrsidx = deserialize_rdef_constraints(cnx)
     pendingrdefs = []
     # closure to factorize common code of attribute/relation rdef addition
@@ -193,29 +218,37 @@
     # Get the type parameters for additional base types.
     try:
         extra_props = dict(cnx.execute('Any X, XTP WHERE X is CWAttribute, '
-                                           'X extra_props XTP'))
+                                       'X extra_props XTP'))
     except Exception:
         cnx.critical('Previous CRITICAL notification about extra_props is not '
-                         'a problem if you are migrating to cubicweb 3.17')
+                     'a problem if you are migrating to cubicweb 3.17')
         extra_props = {} # not yet in the schema (introduced by 3.17 migration)
-    for values in cnx.execute(
-        'Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT WHERE X is CWAttribute,'
-        'X relation_type RT, X cardinality CARD, X ordernum ORD, X indexed IDX,'
-        'X description DESC, X internationalizable I18N, X defaultval DFLT,'
-        'X fulltextindexed FTIDX, X from_entity SE, X to_entity OE',
-        build_descr=False):
-        rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values
-        typeparams = extra_props.get(rdefeid)
-        typeparams = json.load(typeparams) if typeparams else {}
+
+    # load attributes
+    rql = ('Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT%(fm)s '
+           'WHERE X is CWAttribute, X relation_type RT, X cardinality CARD,'
+           '      X ordernum ORD, X indexed IDX, X description DESC, '
+           '      X internationalizable I18N, X defaultval DFLT,%(fmsnip)s'
+           '      X fulltextindexed FTIDX, X from_entity SE, X to_entity OE')
+    if has_computed_attributes:
+        rql = rql % {'fm': ',FM', 'fmsnip': 'X formula FM,'}
+    else:
+        rql = rql % {'fm': '', 'fmsnip': ''}
+    for values in cnx.execute(rql, build_descr=False):
+        attrs = dict(zip(
+            ('rdefeid', 'seid', 'reid', 'oeid', 'cardinality',
+             'order', 'description', 'indexed', 'fulltextindexed',
+             'internationalizable', 'default', 'formula'), values))
+        typeparams = extra_props.get(attrs['rdefeid'])
+        attrs.update(json.load(typeparams) if typeparams else {})
+        default = attrs['default']
         if default is not None:
             if isinstance(default, Binary):
                 # while migrating from 3.17 to 3.18, we still have to
                 # handle String defaults
-                default = default.unzpickle()
-        _add_rdef(rdefeid, seid, reid, oeid,
-                  cardinality=card, description=desc, order=ord,
-                  indexed=idx, fulltextindexed=ftidx, internationalizable=i18n,
-                  default=default, **typeparams)
+                attrs['default'] = default.unzpickle()
+        _add_rdef(**attrs)
+    # load relations
     for values in cnx.execute(
         'Any X,SE,RT,OE,CARD,ORD,DESC,C WHERE X is CWRelation, X relation_type RT,'
         'X cardinality CARD, X ordernum ORD, X description DESC, '
@@ -252,6 +285,7 @@
         eschema._unique_together.append(tuple(sorted(unique_together)))
     schema.infer_specialization_rules()
     cnx.commit()
+    schema.finalize()
     schema.reading_from_database = False
 
 
@@ -309,19 +343,14 @@
     """synchronize schema and permissions in the database according to
     current schema
     """
-    quiet = os.environ.get('APYCOT_ROOT')
-    if not quiet:
-        _title = '-> storing the schema in the database '
-        print _title,
+    _title = '-> storing the schema in the database '
+    print _title,
     execute = cnx.execute
     eschemas = schema.entities()
-    if not quiet:
-        pb_size = (len(eschemas + schema.relations())
-                   + len(CONSTRAINTS)
-                   + len([x for x in eschemas if x.specializes()]))
-        pb = ProgressBar(pb_size, title=_title)
-    else:
-        pb = None
+    pb_size = (len(eschemas + schema.relations())
+               + len(CONSTRAINTS)
+               + len([x for x in eschemas if x.specializes()]))
+    pb = ProgressBar(pb_size, title=_title)
     groupmap = group_mapping(cnx, interactive=False)
     # serialize all entity types, assuring CWEType is serialized first for proper
     # is / is_instance_of insertion
@@ -329,22 +358,23 @@
     eschemas.insert(0, schema.eschema('CWEType'))
     for eschema in eschemas:
         execschemarql(execute, eschema, eschema2rql(eschema, groupmap))
-        if pb is not None:
-            pb.update()
+        pb.update()
     # serialize constraint types
     cstrtypemap = {}
     rql = 'INSERT CWConstraintType X: X name %(ct)s'
     for cstrtype in CONSTRAINTS:
         cstrtypemap[cstrtype] = execute(rql, {'ct': unicode(cstrtype)},
                                         build_descr=False)[0][0]
-        if pb is not None:
-            pb.update()
+        pb.update()
     # serialize relations
     for rschema in schema.relations():
         # skip virtual relations such as eid, has_text and identity
         if rschema in VIRTUAL_RTYPES:
-            if pb is not None:
-                pb.update()
+            pb.update()
+            continue
+        if rschema.rule:
+            execschemarql(execute, rschema, crschema2rql(rschema))
+            pb.update()
             continue
         execschemarql(execute, rschema, rschema2rql(rschema, addrdef=False))
         if rschema.symmetric:
@@ -355,8 +385,7 @@
         for rdef in rdefs:
             execschemarql(execute, rdef,
                           rdef2rql(rdef, cstrtypemap, groupmap))
-        if pb is not None:
-            pb.update()
+        pb.update()
     # serialize unique_together constraints
     for eschema in eschemas:
         if eschema._unique_together:
@@ -364,10 +393,8 @@
     # serialize yams inheritance relationships
     for rql, kwargs in specialize2rql(schema):
         execute(rql, kwargs, build_descr=False)
-        if pb is not None:
-            pb.update()
-    if not quiet:
-        print
+        pb.update()
+    print
 
 
 # high level serialization functions
@@ -441,7 +468,7 @@
     for i, name in enumerate(unique_together):
         rschema = eschema.schema.rschema(name)
         rtype = 'T%d' % i
-        substs[rtype] = rschema.type
+        substs[rtype] = unicode(rschema.type)
         relations.append('C relations %s' % rtype)
         restrictions.append('%(rtype)s name %%(%(rtype)s)s' % {'rtype': rtype})
     relations = ', '.join(relations)
@@ -469,7 +496,7 @@
 # rtype serialization
 
 def rschema2rql(rschema, cstrtypemap=None, addrdef=True, groupmap=None):
-    """return a list of rql insert statements to enter a relation schema
+    """generate rql insert statements to enter a relation schema
     in the database as an CWRType entity
     """
     if rschema.type == 'has_text':
@@ -496,10 +523,22 @@
     relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
     return relations, values
 
+def crschema2rql(crschema):
+    relations, values = crschema_relations_values(crschema)
+    yield 'INSERT CWComputedRType X: %s' % ','.join(relations), values
+
+def crschema_relations_values(crschema):
+    values = _ervalues(crschema)
+    values['rule'] = unicode(crschema.rule)
+    # XXX why oh why?
+    del values['final']
+    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
+    return relations, values
+
 # rdef serialization
 
 def rdef2rql(rdef, cstrtypemap, groupmap=None):
-    # don't serialize infered relations
+    # don't serialize inferred relations
     if rdef.infered:
         return
     relations, values = _rdef_values(rdef)
@@ -518,12 +557,14 @@
         for rql, args in _erperms2rql(rdef, groupmap):
             yield rql, args
 
+_IGNORED_PROPS = ['eid', 'constraints', 'uid', 'infered', 'permissions']
+
 def _rdef_values(rdef):
     amap = {'order': 'ordernum', 'default': 'defaultval'}
     values = {}
     extra = {}
     for prop in rdef.rproperty_defs(rdef.object):
-        if prop in ('eid', 'constraints', 'uid', 'infered', 'permissions'):
+        if prop in _IGNORED_PROPS:
             continue
         value = getattr(rdef, prop)
         if prop not in KNOWN_RPROPERTIES:
@@ -592,9 +633,13 @@
     yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
 
 def updaterschema2rql(rschema, eid):
-    relations, values = rschema_relations_values(rschema)
-    values['x'] = eid
-    yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
+    if rschema.rule:
+        yield ('SET X rule %(r)s WHERE X eid %(x)s',
+               {'x': eid, 'r': unicode(rschema.rule)})
+    else:
+        relations, values = rschema_relations_values(rschema)
+        values['x'] = eid
+        yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
 
 def updaterdef2rql(rdef, eid):
     relations, values = _rdef_values(rdef)
--- a/server/serverctl.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/serverctl.py	Thu Jan 22 17:45:06 2015 +0100
@@ -24,6 +24,7 @@
 # completion). So import locally in command helpers.
 import sys
 import os
+from contextlib import contextmanager
 import logging
 import subprocess
 
@@ -31,6 +32,8 @@
 from logilab.common.configuration import Configuration, merge_options
 from logilab.common.shellutils import ASK, generate_password
 
+from logilab.database import get_db_helper, get_connection
+
 from cubicweb import AuthenticationError, ExecutionError, ConfigurationError
 from cubicweb.toolsutils import Command, CommandHandler, underline_title
 from cubicweb.cwctl import CWCTL, check_options_consistency, ConfigureInstanceCommand
@@ -47,7 +50,6 @@
     given server.serverconfig
     """
     from getpass import getpass
-    from logilab.database import get_connection, get_db_helper
     dbhost = source.get('db-host')
     if dbname is None:
         dbname = source['db-name']
@@ -86,6 +88,7 @@
     extra = extra_args and {'extra_args': extra_args} or {}
     cnx = get_connection(driver, dbhost, dbname, user, password=password,
                          port=source.get('db-port'),
+                         schema=source.get('db-namespace'),
                          **extra)
     try:
         cnx.logged_user = user
@@ -104,7 +107,6 @@
     create/drop the instance database)
     """
     if dbms_system_base:
-        from logilab.database import get_db_helper
         system_db = get_db_helper(source['db-driver']).system_database()
         return source_cnx(source, system_db, special_privs=special_privs,
                           interactive=interactive)
@@ -116,7 +118,6 @@
     database)
     """
     import logilab.common as lgp
-    from logilab.database import get_db_helper
     lgp.USE_MX_DATETIME = False
     driver = source['db-driver']
     helper = get_db_helper(driver)
@@ -205,56 +206,99 @@
             print ('-> nevermind, you can do it later with '
                    '"cubicweb-ctl db-create %s".' % self.config.appid)
 
-ERROR = nullobject()
 
-def confirm_on_error_or_die(msg, func, *args, **kwargs):
+@contextmanager
+def db_transaction(source, privilege):
+    """Open a transaction to the instance database"""
+    cnx = system_source_cnx(source, special_privs=privilege)
+    cursor = cnx.cursor()
     try:
-        return func(*args, **kwargs)
-    except Exception as ex:
-        print 'ERROR', ex
-        if not ASK.confirm('An error occurred while %s. Continue anyway?' % msg):
-            raise ExecutionError(str(ex))
-    return ERROR
+        yield cursor
+    except:
+        cnx.rollback()
+        cnx.close()
+        raise
+    else:
+        cnx.commit()
+        cnx.close()
+
+
+@contextmanager
+def db_sys_transaction(source, privilege):
+    """Open a transaction to the system database"""
+    cnx = _db_sys_cnx(source, privilege)
+    cursor = cnx.cursor()
+    try:
+        yield cursor
+    except:
+        cnx.rollback()
+        cnx.close()
+        raise
+    else:
+        cnx.commit()
+        cnx.close()
+
 
 class RepositoryDeleteHandler(CommandHandler):
     cmdname = 'delete'
     cfgname = 'repository'
 
+    def _drop_namespace(self, source):
+        db_namespace = source.get('db-namespace')
+        with db_transaction(source, privilege='DROP SCHEMA') as cursor:
+            helper = get_db_helper(source['db-driver'])
+            helper.drop_schema(cursor, db_namespace)
+            print '-> database schema %s dropped' % db_namespace
+
+    def _drop_database(self, source):
+        dbname = source['db-name']
+        if source['db-driver'] == 'sqlite':
+            print 'deleting database file %(db-name)s' % source
+            os.unlink(source['db-name'])
+            print '-> database %(db-name)s dropped.' % source
+        else:
+            helper = get_db_helper(source['db-driver'])
+            with db_sys_transaction(source, privilege='DROP DATABASE') as cursor:
+                print 'dropping database %(db-name)s' % source
+                cursor.execute('DROP DATABASE "%(db-name)s"' % source)
+                print '-> database %(db-name)s dropped.' % source
+
+    def _drop_user(self, source):
+        user = source['db-user'] or None
+        if user is not None:
+            with db_sys_transaction(source, privilege='DROP USER') as cursor:
+                print 'dropping user %s' % user
+                cursor.execute('DROP USER %s' % user)
+
+    def _cleanup_steps(self, source):
+        # 1/ delete namespace if used
+        db_namespace = source.get('db-namespace')
+        if db_namespace:
+            yield ('Delete database namespace "%s"' % db_namespace,
+                   self._drop_namespace, True)
+        # 2/ delete database
+        yield ('Delete database "%(db-name)s"' % source,
+               self._drop_database, True)
+        # 3/ delete user
+        helper = get_db_helper(source['db-driver'])
+        if source['db-user'] and helper.users_support:
+            # XXX should check we are not connected as user
+            yield ('Delete user "%(db-user)s"' % source,
+                   self._drop_user, False)
+
     def cleanup(self):
         """remove instance's configuration and database"""
-        from logilab.database import get_db_helper
         source = self.config.system_source_config
-        dbname = source['db-name']
-        helper = get_db_helper(source['db-driver'])
-        if ASK.confirm('Delete database %s ?' % dbname):
-            if source['db-driver'] == 'sqlite':
-                if confirm_on_error_or_die(
-                    'deleting database file %s' % dbname,
-                    os.unlink, source['db-name']) is not ERROR:
-                    print '-> database %s dropped.' % dbname
-                return
-            user = source['db-user'] or None
-            cnx = confirm_on_error_or_die('connecting to database %s' % dbname,
-                                          _db_sys_cnx, source, 'DROP DATABASE')
-            if cnx is ERROR:
-                return
-            cursor = cnx.cursor()
-            try:
-                if confirm_on_error_or_die(
-                    'dropping database %s' % dbname,
-                    cursor.execute, 'DROP DATABASE "%s"' % dbname) is not ERROR:
-                    print '-> database %s dropped.' % dbname
-                # XXX should check we are not connected as user
-                if user and helper.users_support and \
-                       ASK.confirm('Delete user %s ?' % user, default_is_yes=False):
-                    if confirm_on_error_or_die(
-                        'dropping user %s' % user,
-                        cursor.execute, 'DROP USER %s' % user) is not ERROR:
-                        print '-> user %s dropped.' % user
-                cnx.commit()
-            except BaseException:
-                cnx.rollback()
-                raise
+        for msg, step, default in self._cleanup_steps(source):
+            if ASK.confirm(msg, default_is_yes=default):
+                try:
+                    step(source)
+                except Exception as exc:
+                    print 'ERROR', exc
+                    if ASK.confirm('An error occurred. Continue anyway?',
+                                   default_is_yes=False):
+                        continue
+                    raise ExecutionError(str(exc))
 
 
 class RepositoryStartHandler(CommandHandler):
@@ -294,6 +338,7 @@
         helper.create_database(cursor, source['db-name'],
                                dbencoding=source['db-encoding'], **kwargs)
 
+
 class CreateInstanceDBCommand(Command):
     """Create the system database of an instance (run after 'create').
 
@@ -331,7 +376,6 @@
 
     def run(self, args):
         """run the command with its specific arguments"""
-        from logilab.database import get_db_helper
         check_options_consistency(self.config)
         automatic = self.get('automatic')
         appid = args.pop()
@@ -371,10 +415,14 @@
             except BaseException:
                 dbcnx.rollback()
                 raise
-        cnx = system_source_cnx(source, special_privs='CREATE LANGUAGE',
+        cnx = system_source_cnx(source, special_privs='CREATE LANGUAGE/SCHEMA',
                                 interactive=not automatic)
         cursor = cnx.cursor()
         helper.init_fti_extensions(cursor)
+        namespace = source.get('db-namespace')
+        if namespace and ASK.confirm('Create schema %s in database %s ?'
+                                     % (namespace, dbname)):
+            helper.create_schema(cursor, namespace)
         cnx.commit()
         # postgres specific stuff
         if driver == 'postgres':
@@ -439,7 +487,6 @@
         check_options_consistency(self.config)
         print '\n'+underline_title('Initializing the system database')
         from cubicweb.server import init_repository
-        from logilab.database import get_connection
         appid = args[0]
         config = ServerConfiguration.config_for(appid)
         try:
@@ -450,7 +497,7 @@
                 system['db-driver'], database=system['db-name'],
                 host=system.get('db-host'), port=system.get('db-port'),
                 user=system.get('db-user') or '', password=system.get('db-password') or '',
-                **extra)
+                schema=system.get('db-namespace'), **extra)
         except Exception as ex:
             raise ConfigurationError(
                 'You seem to have provided wrong connection information in '\
@@ -482,7 +529,6 @@
     def run(self, args):
         appid = args[0]
         config = ServerConfiguration.config_for(appid)
-        config.quick_start = True
         repo, cnx = repo_cnx(config)
         with cnx:
             used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
@@ -505,6 +551,12 @@
                         continue
                 break
             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:
@@ -515,11 +567,13 @@
                         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)
+            cnx.create_entity('CWSource', name=sourceuri, type=unicode(type),
+                              config=cfgstr, parser=unicode(parser), url=unicode(url))
             cnx.commit()
 
 
@@ -596,7 +650,6 @@
             sys.exit(1)
         cnx = source_cnx(sourcescfg['system'])
         driver = sourcescfg['system']['db-driver']
-        from logilab.database import get_db_helper
         dbhelper = get_db_helper(driver)
         cursor = cnx.cursor()
         # check admin exists
@@ -1098,9 +1151,14 @@
 
 db_options = (
     ('db',
-     {'short': 'd', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
+     {'short': 'd', 'type' : 'named', 'metavar' : '[section1.]key1:value1,[section2.]key2:value2',
       'default': None,
-      'help': 'set <key> to <value> in "source" configuration file.',
+      'help': '''set <key> in <section> to <value> in "source" configuration file. If <section> is not specified, it defaults to "system".
+
+Beware that changing admin.login or admin.password using this command
+will NOT update the database with new admin credentials.  Use the
+reset-admin-pwd command instead.
+''',
       }),
     )
 
@@ -1114,10 +1172,14 @@
         appcfg = ServerConfiguration.config_for(appid)
         srccfg = appcfg.read_sources_file()
         for key, value in self.config.db.iteritems():
+            if '.' in key:
+                section, key = key.split('.', 1)
+            else:
+                section = 'system'
             try:
-                srccfg['system'][key] = value
+                srccfg[section][key] = value
             except KeyError:
-                raise ConfigurationError('unknown configuration key "%s" for source' % key)
+                raise ConfigurationError('unknown configuration key "%s" in section "%s" for source' % (key, section))
         admcfg = Configuration(options=USER_OPTIONS)
         admcfg['login'] = srccfg['admin']['login']
         admcfg['password'] = srccfg['admin']['password']
--- a/server/session.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/session.py	Thu Jan 22 17:45:06 2015 +0100
@@ -1010,15 +1010,12 @@
 
     @_with_cnx_set
     @_open_only
-    def execute(self, rql, kwargs=None, eid_key=None, build_descr=True):
+    def execute(self, rql, kwargs=None, build_descr=True):
         """db-api like method directly linked to the querier execute method.
 
         See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
         """
         self._session_timestamp.touch()
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
         rset = self._execute(self, rql, kwargs, build_descr)
         rset.req = self
         self._session_timestamp.touch()
--- a/server/sources/__init__.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/sources/__init__.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -19,10 +19,7 @@
 
 __docformat__ = "restructuredtext en"
 
-import itertools
-from os.path import join, splitext
 from time import time
-from datetime import datetime, timedelta
 from logging import getLogger
 
 from logilab.common import configuration
@@ -31,8 +28,7 @@
 from yams.schema import role_name
 
 from cubicweb import ValidationError, set_log_methods, server
-from cubicweb.schema import VIRTUAL_RTYPES
-from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.server import SOURCE_TYPES
 from cubicweb.server.edition import EditedEntity
 
 
@@ -105,7 +101,7 @@
         self.support_relations['identity'] = False
         self.eid = eid
         self.public_config = source_config.copy()
-        self.public_config.setdefault('use-cwuri-as-url', self.use_cwuri_as_url)
+        self.public_config['use-cwuri-as-url'] = self.use_cwuri_as_url
         self.remove_sensitive_information(self.public_config)
         self.uri = source_config.pop('uri')
         set_log_methods(self, getLogger('cubicweb.sources.'+self.uri))
@@ -311,23 +307,15 @@
         """
         pass
 
-    def _load_mapping(self, session=None, **kwargs):
+    def _load_mapping(self, cnx, **kwargs):
         if not 'CWSourceSchemaConfig' in self.schema:
             self.warning('instance is not mapping ready')
             return
-        if session is None:
-            _session = self.repo.internal_session()
-        else:
-            _session = session
-        try:
-            for schemacfg in _session.execute(
-                'Any CFG,CFGO,S WHERE '
-                'CFG options CFGO, CFG cw_schema S, '
-                'CFG cw_for_source X, X eid %(x)s', {'x': self.eid}).entities():
-                self.add_schema_config(schemacfg, **kwargs)
-        finally:
-            if session is None:
-                _session.close()
+        for schemacfg in cnx.execute(
+            'Any CFG,CFGO,S WHERE '
+            'CFG options CFGO, CFG cw_schema S, '
+            'CFG cw_for_source X, X eid %(x)s', {'x': self.eid}).entities():
+            self.add_schema_config(schemacfg, **kwargs)
 
     def add_schema_config(self, schemacfg, checkonly=False):
         """added CWSourceSchemaConfig, modify mapping accordingly"""
@@ -372,33 +360,33 @@
         """return the external id for the given newly inserted entity"""
         raise NotImplementedError(self)
 
-    def add_entity(self, session, entity):
+    def add_entity(self, cnx, entity):
         """add a new entity to the source"""
         raise NotImplementedError(self)
 
-    def update_entity(self, session, entity):
+    def update_entity(self, cnx, entity):
         """update an entity in the source"""
         raise NotImplementedError(self)
 
-    def delete_entities(self, session, entities):
+    def delete_entities(self, cnx, entities):
         """delete several entities from the source"""
         for entity in entities:
-            self.delete_entity(session, entity)
+            self.delete_entity(cnx, entity)
 
-    def delete_entity(self, session, entity):
+    def delete_entity(self, cnx, entity):
         """delete an entity from the source"""
         raise NotImplementedError(self)
 
-    def add_relation(self, session, subject, rtype, object):
+    def add_relation(self, cnx, subject, rtype, object):
         """add a relation to the source"""
         raise NotImplementedError(self)
 
-    def add_relations(self, session,  rtype, subj_obj_list):
+    def add_relations(self, cnx,  rtype, subj_obj_list):
         """add a relations to the source"""
         # override in derived classes if you feel you can
         # optimize
         for subject, object in subj_obj_list:
-            self.add_relation(session, subject, rtype, object)
+            self.add_relation(cnx, subject, rtype, object)
 
     def delete_relation(self, session, subject, rtype, object):
         """delete a relation from the source"""
@@ -406,57 +394,56 @@
 
     # system source interface #################################################
 
-    def eid_type_source(self, session, eid):
+    def eid_type_source(self, cnx, eid):
         """return a tuple (type, source, extid) for the entity with id <eid>"""
         raise NotImplementedError(self)
 
-    def create_eid(self, session):
+    def create_eid(self, cnx):
         raise NotImplementedError(self)
 
-    def add_info(self, session, entity, source, extid):
+    def add_info(self, cnx, entity, source, extid):
         """add type and source info for an eid into the system table"""
         raise NotImplementedError(self)
 
-    def update_info(self, session, entity, need_fti_update):
+    def update_info(self, cnx, entity, need_fti_update):
         """mark entity as being modified, fulltext reindex if needed"""
         raise NotImplementedError(self)
 
-    def index_entity(self, session, entity):
+    def index_entity(self, cnx, entity):
         """create an operation to [re]index textual content of the given entity
         on commit
         """
         raise NotImplementedError(self)
 
-    def fti_unindex_entities(self, session, entities):
+    def fti_unindex_entities(self, cnx, entities):
         """remove text content for entities from the full text index
         """
         raise NotImplementedError(self)
 
-    def fti_index_entities(self, session, entities):
+    def fti_index_entities(self, cnx, entities):
         """add text content of created/modified entities to the full text index
         """
         raise NotImplementedError(self)
 
     # sql system source interface #############################################
 
-    def sqlexec(self, session, sql, args=None):
+    def sqlexec(self, cnx, sql, args=None):
         """execute the query and return its result"""
         raise NotImplementedError(self)
 
-    def create_index(self, session, table, column, unique=False):
+    def create_index(self, cnx, table, column, unique=False):
         raise NotImplementedError(self)
 
-    def drop_index(self, session, table, column, unique=False):
+    def drop_index(self, cnx, table, column, unique=False):
         raise NotImplementedError(self)
 
 
-    @deprecated('[3.13] use extid2eid(source, value, etype, session, **kwargs)')
-    def extid2eid(self, value, etype, session, **kwargs):
-        return self.repo.extid2eid(self, value, etype, session, **kwargs)
+    @deprecated('[3.13] use extid2eid(source, value, etype, cnx, **kwargs)')
+    def extid2eid(self, value, etype, cnx, **kwargs):
+        return self.repo.extid2eid(self, value, etype, cnx, **kwargs)
 
 
 
-from cubicweb.server import SOURCE_TYPES
 
 def source_adapter(source_type):
     try:
--- a/server/sources/datafeed.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/sources/datafeed.py	Thu Jan 22 17:45:06 2015 +0100
@@ -83,6 +83,13 @@
           'help': ('Timeout of HTTP GET requests, when synchronizing a source.'),
           'group': 'datafeed-source', 'level': 2,
           }),
+        ('use-cwuri-as-url',
+         {'type': 'yn',
+          'default': None, # explicitly unset
+          'help': ('Use cwuri (i.e. external URL) for link to the entity '
+                   'instead of its local URL.'),
+          'group': 'datafeed-source', 'level': 1,
+          }),
         )
 
     def check_config(self, source_entity):
@@ -107,6 +114,12 @@
         self.synchro_interval = timedelta(seconds=typed_config['synchronization-interval'])
         self.max_lock_lifetime = timedelta(seconds=typed_config['max-lock-lifetime'])
         self.http_timeout = typed_config['http-timeout']
+        # if typed_config['use-cwuri-as-url'] is set, we have to update
+        # use_cwuri_as_url attribute and public configuration dictionary
+        # accordingly
+        if typed_config['use-cwuri-as-url'] is not None:
+            self.use_cwuri_as_url = typed_config['use-cwuri-as-url']
+            self.public_config['use-cwuri-as-url'] = self.use_cwuri_as_url
 
     def init(self, activated, source_entity):
         super(DataFeedSource, self).init(activated, source_entity)
@@ -285,12 +298,41 @@
         self.stats = {'created': set(), 'updated': set(), 'checked': set()}
 
     def normalize_url(self, url):
-        from cubicweb.sobjects import URL_MAPPING # available after registration
+        """Normalize an url by looking if there is a replacement for it in
+        `cubicweb.sobjects.URL_MAPPING`.
+
+        This dictionary allow to redirect from one host to another, which may be
+        useful for example in case of test instance using production data, while
+        you don't want to load the external source nor to hack your `/etc/hosts`
+        file.
+        """
+        # local import mandatory, it's available after registration
+        from cubicweb.sobjects import URL_MAPPING
         for mappedurl in URL_MAPPING:
             if url.startswith(mappedurl):
                 return url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
         return url
 
+    def retrieve_url(self, url, data=None, headers=None):
+        """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
+        """
+        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)
+            return _OPENER.open(req, timeout=self.source.http_timeout)
+        if url.startswith('file://'):
+            return URLLibResponseAdapter(open(url[7:]), url)
+        return URLLibResponseAdapter(StringIO.StringIO(url), url)
+
     def add_schema_config(self, schemacfg, checkonly=False):
         """added CWSourceSchemaConfig, modify mapping accordingly"""
         msg = schemacfg._cw._("this parser doesn't use a mapping")
@@ -302,9 +344,13 @@
         raise ValidationError(schemacfg.eid, {None: msg})
 
     def extid2entity(self, uri, etype, **sourceparams):
-        """return an entity for the given uri. May return None if it should be
-        skipped
+        """Return an entity for the given uri. May return None if it should be
+        skipped.
+
+        If a `raise_on_error` keyword parameter is passed, a ValidationError
+        exception may be raised.
         """
+        raise_on_error = sourceparams.pop('raise_on_error', False)
         cnx = self._cw
         # if cwsource is specified and repository has a source with the same
         # name, call extid2eid on that source so entity will be properly seen as
@@ -321,8 +367,8 @@
             eid = cnx.repo.extid2eid(source, str(uri), etype, cnx,
                                          sourceparams=sourceparams)
         except ValidationError as ex:
-            # XXX use critical so they are seen during tests. Should consider
-            # raise_on_error instead?
+            if raise_on_error:
+                raise
             self.source.critical('error while creating %s: %s', etype, ex)
             self.import_log.record_error('error while creating %s: %s'
                                          % (etype, ex))
@@ -413,7 +459,7 @@
         rollback = self._cw.rollback
         for args in parsed:
             try:
-                self.process_item(*args)
+                self.process_item(*args, raise_on_error=raise_on_error)
                 # commit+set_cnxset instead of commit(free_cnxset=False) to let
                 # other a chance to get our connections set
                 commit()
@@ -427,20 +473,13 @@
         return error
 
     def parse(self, url):
-        if url.startswith('http'):
-            url = self.normalize_url(url)
-            self.source.info('GET %s', url)
-            stream = _OPENER.open(url, timeout=self.source.http_timeout)
-        elif url.startswith('file://'):
-            stream = open(url[7:])
-        else:
-            stream = StringIO.StringIO(url)
+        stream = self.retrieve_url(url)
         return self.parse_etree(etree.parse(stream).getroot())
 
     def parse_etree(self, document):
         return [(document,)]
 
-    def process_item(self, *args):
+    def process_item(self, *args, **kwargs):
         raise NotImplementedError
 
     def is_deleted(self, extid, etype, eid):
@@ -455,6 +494,27 @@
             return exists(extid[7:])
         return False
 
+
+class URLLibResponseAdapter(object):
+    """Thin wrapper to be used to fake a value returned by urllib2.urlopen"""
+    def __init__(self, stream, url, code=200):
+        self._stream = stream
+        self._url = url
+        self.code = code
+
+    def read(self, *args):
+        return self._stream.read(*args)
+
+    def geturl(self):
+        return self._url
+
+    def getcode(self):
+        return self.code
+
+    def info(self):
+        from mimetools import Message
+        return Message(StringIO.StringIO())
+
 # use a cookie enabled opener to use session cookie if any
 _OPENER = urllib2.build_opener()
 try:
--- a/server/sources/ldapfeed.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/sources/ldapfeed.py	Thu Jan 22 17:45:06 2015 +0100
@@ -126,7 +126,7 @@
           }),
         ('user-attrs-map',
          {'type' : 'named',
-          'default': {'uid': 'login', 'gecos': 'email', 'userPassword': 'upassword'},
+          'default': {'uid': 'login'},
           'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
           'group': 'ldap-source', 'level': 1,
           }),
--- a/server/sources/native.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/sources/native.py	Thu Jan 22 17:45:06 2015 +0100
@@ -293,6 +293,12 @@
           'help': 'database name',
           'group': 'native-source', 'level': 0,
           }),
+        ('db-namespace',
+         {'type' : 'string',
+          'default': '',
+          'help': 'database namespace (schema) name',
+          'group': 'native-source', 'level': 1,
+          }),
         ('db-user',
          {'type' : 'string',
           'default': CubicWebNoAppConfiguration.mode == 'user' and getlogin() or 'cubicweb',
@@ -318,10 +324,16 @@
                   'want trusted authentication for the database connection',
           'group': 'native-source', 'level': 2,
           }),
+        ('db-statement-timeout',
+         {'type': 'int',
+          'default': 0,
+          'help': 'sql statement timeout, in milliseconds (postgres only)',
+          'group': 'native-source', 'level': 2,
+          }),
     )
 
     def __init__(self, repo, source_config, *args, **kwargs):
-        SQLAdapterMixIn.__init__(self, source_config)
+        SQLAdapterMixIn.__init__(self, source_config, repairing=repo.config.repairing)
         self.authentifiers = [LoginPasswordAuthentifier(self)]
         if repo.config['allow-email-login']:
             self.authentifiers.insert(0, EmailPasswordAuthentifier(self))
@@ -440,10 +452,10 @@
 
     # XXX deprecates [un]map_attribute?
     def map_attribute(self, etype, attr, cb, sourcedb=True):
-        self._rql_sqlgen.attr_map['%s.%s' % (etype, attr)] = (cb, sourcedb)
+        self._rql_sqlgen.attr_map[u'%s.%s' % (etype, attr)] = (cb, sourcedb)
 
     def unmap_attribute(self, etype, attr):
-        self._rql_sqlgen.attr_map.pop('%s.%s' % (etype, attr), None)
+        self._rql_sqlgen.attr_map.pop(u'%s.%s' % (etype, attr), None)
 
     def set_storage(self, etype, attr, storage):
         storage_dict = self._storages.setdefault(etype, {})
@@ -560,7 +572,7 @@
                 cursor = self.doexec(cnx, sql, args)
             else:
                 raise
-        results = self.process_result(cursor, cbs, session=cnx)
+        results = self.process_result(cursor, cnx, cbs)
         assert dbg_results(results)
         return results
 
@@ -885,8 +897,8 @@
         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}
+        attrs = {'type': 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
         try:
@@ -1464,7 +1476,7 @@
 
 class LoginPasswordAuthentifier(BaseAuthentifier):
     passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
-    auth_rql = ('Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
+    auth_rql = (u'Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
                 'X cw_source S, S name "system"')
     _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
 
--- a/server/sqlutils.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/sqlutils.py	Thu Jan 22 17:45:06 2015 +0100
@@ -47,7 +47,7 @@
     return subprocess.call(cmd)
 
 
-def sqlexec(sqlstmts, cursor_or_execute, withpb=not os.environ.get('APYCOT_ROOT'),
+def sqlexec(sqlstmts, cursor_or_execute, withpb=True,
             pbtitle='', delimiter=';', cnx=None):
     """execute sql statements ignoring DROP/ CREATE GROUP or USER statements
     error.
@@ -299,7 +299,7 @@
     """
     cnx_wrap = ConnectionWrapper
 
-    def __init__(self, source_config):
+    def __init__(self, source_config, repairing=False):
         try:
             self.dbdriver = source_config['db-driver'].lower()
             dbname = source_config['db-name']
@@ -312,10 +312,11 @@
         dbpassword = source_config.get('db-password')
         dbencoding = source_config.get('db-encoding', 'UTF-8')
         dbextraargs = source_config.get('db-extra-arguments')
+        dbnamespace = source_config.get('db-namespace')
         self.dbhelper = db.get_db_helper(self.dbdriver)
         self.dbhelper.record_connection_info(dbname, dbhost, dbport, dbuser,
                                              dbpassword, dbextraargs,
-                                             dbencoding)
+                                             dbencoding, dbnamespace)
         self.sqlgen = SQLGenerator()
         # copy back some commonly accessed attributes
         dbapi_module = self.dbhelper.dbapi_module
@@ -328,6 +329,14 @@
         if self.dbdriver == 'sqlite':
             self.cnx_wrap = SqliteConnectionWrapper
             self.dbhelper.dbname = abspath(self.dbhelper.dbname)
+        if not repairing:
+            statement_timeout = int(source_config.get('db-statement-timeout', 0))
+            if statement_timeout > 0:
+                def set_postgres_timeout(cnx):
+                    cnx.cursor().execute('SET statement_timeout to %d' % statement_timeout)
+                    cnx.commit()
+                postgres_hooks = SQL_CONNECT_HOOKS['postgres']
+                postgres_hooks.append(set_postgres_timeout)
 
     def wrapped_connection(self):
         """open and return a connection to the database, wrapped into a class
@@ -367,12 +376,12 @@
             return newargs
         return query_args
 
-    def process_result(self, cursor, column_callbacks=None, session=None):
+    def process_result(self, cursor, cnx=None, column_callbacks=None):
         """return a list of CubicWeb compliant values from data in the given cursor
         """
-        return list(self.iter_process_result(cursor, column_callbacks, session))
+        return list(self.iter_process_result(cursor, cnx, column_callbacks))
 
-    def iter_process_result(self, cursor, column_callbacks=None, session=None):
+    def iter_process_result(self, cursor, cnx, column_callbacks=None):
         """return a iterator on tuples of CubicWeb compliant values from data
         in the given cursor
         """
@@ -382,10 +391,10 @@
         if not column_callbacks:
             return self.dbhelper.dbapi_module.process_cursor(cursor, self._dbencoding,
                                                              Binary)
-        assert session
-        return self._cb_process_result(cursor, column_callbacks, session)
+        assert cnx
+        return self._cb_process_result(cursor, column_callbacks, cnx)
 
-    def _cb_process_result(self, cursor, column_callbacks, session):
+    def _cb_process_result(self, cursor, column_callbacks, cnx):
         # begin bind to locals for optimization
         descr = cursor.description
         encoding = self._dbencoding
@@ -408,7 +417,7 @@
                         value = process_value(value, descr[col], encoding, binary)
                     else:
                         for cb in cbstack:
-                            value = cb(self, session, value)
+                            value = cb(self, cnx, value)
                     result.append(value)
                 yield result
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-cwep002/schema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,35 @@
+# 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/>.
+
+from yams.buildobjs import EntityType, RelationDefinition, Int, ComputedRelation
+
+class Person(EntityType):
+    salary = Int()
+
+class works_for(RelationDefinition):
+    subject = 'Person'
+    object  = 'Company'
+    cardinality = '?*'
+
+class Company(EntityType):
+    total_salary = Int(formula='Any SUM(SA) GROUPBY X WHERE '
+                       'P works_for X, P salary SA')
+
+class has_employee(ComputedRelation):
+    rule = 'O works_for S'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/datacomputed/migratedapp/schema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,57 @@
+# 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/>.
+
+from yams.buildobjs import (EntityType, RelationDefinition, ComputedRelation,
+                            Int, Float)
+
+
+class Employee(EntityType):
+    pass
+
+
+class employees(RelationDefinition):
+    subject = 'Company'
+    object = 'Employee'
+
+
+class associates(RelationDefinition):
+    subject = 'Company'
+    object = 'Employee'
+
+
+class works_for(ComputedRelation):
+    rule = 'O employees S, NOT EXISTS (O associates S)'
+
+
+class Company(EntityType):
+    score = Float(formula='Any AVG(NN) WHERE X employees E, N concerns E, N note NN')
+    score100 = Float(formula='Any AVG(NN) WHERE X employees E, N concerns E, N note100 NN')
+
+
+class Note(EntityType):
+    note = Int()
+    note100 = Int(formula='Any N*100 WHERE X note N')
+
+
+class concerns(RelationDefinition):
+    subject = 'Note'
+    object = 'Employee'
+
+
+class whatever(ComputedRelation):
+    rule = 'S employees E, O associates E'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/datacomputed/schema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,54 @@
+# 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/>.
+
+from yams.buildobjs import EntityType, RelationDefinition, ComputedRelation, Int, Float
+
+
+class Employee(EntityType):
+    pass
+
+
+class employees(RelationDefinition):
+    subject = 'Company'
+    object = 'Employee'
+
+
+class associates(RelationDefinition):
+    subject = 'Company'
+    object = 'Employee'
+
+
+class Company(EntityType):
+    score100 = Float(formula='Any AVG(NN) WHERE X employees E, N concerns E, N note100 NN')
+
+class Note(EntityType):
+    note = Int()
+    note20 = Int(formula='Any N*20 WHERE X note N')
+    note100 = Int(formula='Any N*20 WHERE X note N')
+
+class concerns(RelationDefinition):
+    subject = 'Note'
+    object = 'Employee'
+
+
+class notes(ComputedRelation):
+    rule = 'S employees E, O concerns E'
+
+
+class whatever(ComputedRelation):
+    rule = 'S employees E, O concerns E'
--- a/server/test/unittest_datafeed.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/test/unittest_datafeed.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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/>.
 
+import mimetools
 from datetime import timedelta
+from contextlib import contextmanager
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sources import datafeed
@@ -25,32 +27,45 @@
 class DataFeedTC(CubicWebTC):
     def setup_database(self):
         with self.admin_access.repo_cnx() as cnx:
-            cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
-                              parser=u'testparser', url=u'ignored',
-                              config=u'synchronization-interval=1min')
-            cnx.commit()
+            with self.base_parser(cnx):
+                cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+                                  parser=u'testparser', url=u'ignored',
+                                  config=u'synchronization-interval=1min')
+                cnx.commit()
 
-    def test(self):
-        self.assertIn('myfeed', self.repo.sources_by_uri)
-        dfsource = self.repo.sources_by_uri['myfeed']
-        self.assertEqual(dfsource.latest_retrieval, None)
-        self.assertEqual(dfsource.synchro_interval, timedelta(seconds=60))
-        self.assertFalse(dfsource.fresh())
-
-
+    @contextmanager
+    def base_parser(self, session):
         class AParser(datafeed.DataFeedParser):
             __regid__ = 'testparser'
             def process(self, url, raise_on_error=False):
                 entity = self.extid2entity('http://www.cubicweb.org/', 'Card',
                                            item={'title': u'cubicweb.org',
-                                                 'content': u'the cw web site'})
+                                                 'content': u'the cw web site'},
+                                           raise_on_error=raise_on_error)
                 if not self.created_during_pull(entity):
                     self.notify_updated(entity)
             def before_entity_copy(self, entity, sourceparams):
                 entity.cw_edited.update(sourceparams['item'])
 
         with self.temporary_appobjects(AParser):
-            with self.repo.internal_cnx() as cnx:
+            if 'myfeed' in self.repo.sources_by_uri:
+                yield self.repo.sources_by_uri['myfeed']._get_parser(session)
+            else:
+                yield
+
+    def test(self):
+        self.assertIn('myfeed', self.repo.sources_by_uri)
+        dfsource = self.repo.sources_by_uri['myfeed']
+        self.assertNotIn('use_cwuri_as_url', dfsource.__dict__)
+        self.assertEqual({'type': u'datafeed', 'uri': u'myfeed', 'use-cwuri-as-url': True},
+                         dfsource.public_config)
+        self.assertEqual(dfsource.use_cwuri_as_url, True)
+        self.assertEqual(dfsource.latest_retrieval, None)
+        self.assertEqual(dfsource.synchro_interval, timedelta(seconds=60))
+        self.assertFalse(dfsource.fresh())
+
+        with self.repo.internal_cnx() as cnx:
+            with self.base_parser(cnx):
                 stats = dfsource.pull_data(cnx, force=True)
                 cnx.commit()
                 # test import stats
@@ -119,6 +134,28 @@
             self.assertFalse(cnx.execute('Card X WHERE X title "cubicweb.org"'))
             self.assertFalse(cnx.execute('Any X WHERE X has_text "cubicweb.org"'))
 
+    def test_parser_retrieve_url_local(self):
+        with self.admin_access.repo_cnx() as cnx:
+            with self.base_parser(cnx) as parser:
+                value = parser.retrieve_url('a string')
+                self.assertEqual(200, value.getcode())
+                self.assertEqual('a string', value.geturl())
+                self.assertIsInstance(value.info(), mimetools.Message)
+
+
+class DataFeedConfigTC(CubicWebTC):
+
+    def test_use_cwuri_as_url_override(self):
+        with self.admin_access.client_cnx() as cnx:
+            cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+                              parser=u'testparser', url=u'ignored',
+                              config=u'use-cwuri-as-url=no')
+            cnx.commit()
+        dfsource = self.repo.sources_by_uri['myfeed']
+        self.assertEqual(dfsource.use_cwuri_as_url, False)
+        self.assertEqual({'type': u'datafeed', 'uri': u'myfeed', 'use-cwuri-as-url': False},
+                         dfsource.public_config)
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/server/test/unittest_migractions.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/test/unittest_migractions.py	Thu Jan 22 17:45:06 2015 +0100
@@ -18,46 +18,50 @@
 """unit tests for module cubicweb.server.migractions"""
 
 from datetime import date
-from os.path import join
+import os.path as osp
 from contextlib import contextmanager
 
 from logilab.common.testlib import unittest_main, Tags, tag
 
 from yams.constraints import UniqueConstraint
 
-from cubicweb import ConfigurationError, ValidationError
+from cubicweb import ConfigurationError, ValidationError, ExecutionError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.migractions import ServerMigrationHelper
 
 import cubicweb.devtools
 
+
+HERE = osp.dirname(osp.abspath(__file__))
+
 migrschema = None
 def tearDownModule(*args):
     global migrschema
     del migrschema
     if hasattr(MigrationCommandsTC, 'origschema'):
         del MigrationCommandsTC.origschema
+    if hasattr(MigrationCommandsComputedTC, 'origschema'):
+        del MigrationCommandsComputedTC.origschema
 
-class MigrationCommandsTC(CubicWebTC):
+class MigrationTC(CubicWebTC):
 
     configcls = cubicweb.devtools.TestServerConfiguration
 
     tags = CubicWebTC.tags | Tags(('server', 'migration', 'migractions'))
 
     def _init_repo(self):
-        super(MigrationCommandsTC, self)._init_repo()
+        super(MigrationTC, self)._init_repo()
         # we have to read schema from the database to get eid for schema entities
         self.repo.set_schema(self.repo.deserialize_schema(), resetvreg=False)
         # hack to read the schema from data/migrschema
         config = self.config
-        config.appid = join('data', 'migratedapp')
-        config._apphome = self.datapath('migratedapp')
+        config.appid = osp.join(self.appid, 'migratedapp')
+        config._apphome = osp.join(HERE, config.appid)
         global migrschema
         migrschema = config.load_schema()
-        config.appid = 'data'
-        config._apphome = self.datadir
-        assert 'Folder' in migrschema
+        config.appid = self.appid
+        config._apphome = osp.join(HERE, self.appid)
 
     def setUp(self):
         CubicWebTC.setUp(self)
@@ -73,6 +77,26 @@
                                              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})
+        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(','))
+
+
+class MigrationCommandsTC(MigrationTC):
+
+    def _init_repo(self):
+        super(MigrationCommandsTC, self)._init_repo()
+        assert 'Folder' in migrschema
+
     def test_add_attribute_bool(self):
         with self.mh() as (cnx, mh):
             self.assertNotIn('yesno', self.schema)
@@ -135,8 +159,7 @@
             self.assertEqual(self.schema['shortpara'].subjects(), ('Note', ))
             self.assertEqual(self.schema['shortpara'].objects(), ('String', ))
             # test created column is actually a varchar(64)
-            notesql = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' and name='%sNote'" % SQL_PREFIX)[0][0]
-            fields = dict(x.strip().split()[:2] for x in notesql.split('(', 1)[1].rsplit(')', 1)[0].split(','))
+            fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
             self.assertEqual(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
             # test default value set on existing entities
             self.assertEqual(cnx.execute('Note X').get_entity(0, 0).shortpara, 'hop')
@@ -286,6 +309,8 @@
             self.assertEqual(self.schema['filed_under2'].objects(), ('Folder2',))
             mh.cmd_drop_relation_type('filed_under2')
             self.assertNotIn('filed_under2', self.schema)
+            # this should not crash
+            mh.cmd_drop_relation_type('filed_under2')
 
     def test_add_relation_definition_nortype(self):
         with self.mh() as (cnx, mh):
@@ -531,7 +556,7 @@
                 mh.cmd_set_size_constraint('CWEType', 'description', None)
 
     @tag('longrun')
-    def test_add_remove_cube_and_deps(self):
+    def test_add_drop_cube_and_deps(self):
         with self.mh() as (cnx, mh):
             schema = self.repo.schema
             self.assertEqual(sorted((str(s), str(o)) for s, o in schema['see_also'].rdefs.iterkeys()),
@@ -539,7 +564,7 @@
                                      ('Bookmark', 'Bookmark'), ('Bookmark', 'Note'),
                                      ('Note', 'Note'), ('Note', 'Bookmark')]))
             try:
-                mh.cmd_remove_cube('email', removedeps=True)
+                mh.cmd_drop_cube('email', 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())
@@ -590,12 +615,12 @@
 
 
     @tag('longrun')
-    def test_add_remove_cube_no_deps(self):
+    def test_add_drop_cube_no_deps(self):
         with self.mh() as (cnx, mh):
             cubes = set(self.config.cubes())
             schema = self.repo.schema
             try:
-                mh.cmd_remove_cube('email')
+                mh.cmd_drop_cube('email')
                 cubes.remove('email')
                 self.assertNotIn('email', self.config.cubes())
                 self.assertIn('file', self.config.cubes())
@@ -612,10 +637,10 @@
                 # next test may fail complaining of missing tables
                 cnx.commit()
 
-    def test_remove_dep_cube(self):
+    def test_drop_dep_cube(self):
         with self.mh() as (cnx, mh):
             with self.assertRaises(ConfigurationError) as cm:
-                mh.cmd_remove_cube('file')
+                mh.cmd_drop_cube('file')
             self.assertEqual(str(cm.exception), "can't remove cube file, used as a dependency")
 
     @tag('longrun')
@@ -656,16 +681,167 @@
             self.assertEqual(self.schema['Note'].specializes(), None)
             self.assertEqual(self.schema['Text'].specializes(), None)
 
-
     def test_add_symmetric_relation_type(self):
         with self.mh() as (cnx, mh):
-            same_as_sql = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
-                                     "and name='same_as_relation'")
-            self.assertFalse(same_as_sql)
+            self.assertFalse(self.table_sql(mh, 'same_as_relation'))
             mh.cmd_add_relation_type('same_as')
-            same_as_sql = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
-                                     "and name='same_as_relation'")
-            self.assertTrue(same_as_sql)
+            self.assertTrue(self.table_sql(mh, 'same_as_relation'))
+
+
+class MigrationCommandsComputedTC(MigrationTC):
+    """ Unit tests for computed relations and attributes
+    """
+    appid = 'datacomputed'
+
+    def setUp(self):
+        MigrationTC.setUp(self)
+        # ensure vregistry is reloaded, needed by generated hooks for computed
+        # attributes
+        self.repo.vreg.set_schema(self.repo.schema)
+
+    def test_computed_relation_add_relation_definition(self):
+        self.assertNotIn('works_for', self.schema)
+        with self.mh() as (cnx, mh):
+            with self.assertRaises(ExecutionError) as exc:
+                mh.cmd_add_relation_definition('Employee', 'works_for',
+                                                    'Company')
+        self.assertEqual(str(exc.exception),
+                         'Cannot add a relation definition for a computed '
+                         'relation (works_for)')
+
+    def test_computed_relation_drop_relation_definition(self):
+        self.assertIn('notes', self.schema)
+        with self.mh() as (cnx, mh):
+            with self.assertRaises(ExecutionError) as exc:
+                mh.cmd_drop_relation_definition('Company', 'notes', 'Note')
+        self.assertEqual(str(exc.exception),
+                         'Cannot drop a relation definition for a computed '
+                         'relation (notes)')
+
+    def test_computed_relation_add_relation_type(self):
+        self.assertNotIn('works_for', self.schema)
+        with self.mh() as (cnx, mh):
+            mh.cmd_add_relation_type('works_for')
+            self.assertIn('works_for', self.schema)
+            self.assertEqual(self.schema['works_for'].rule,
+                             'O employees S, NOT EXISTS (O associates S)')
+            self.assertEqual(self.schema['works_for'].objects(), ('Company',))
+            self.assertEqual(self.schema['works_for'].subjects(), ('Employee',))
+            self.assertFalse(self.table_sql(mh, 'works_for_relation'))
+            e = cnx.create_entity('Employee')
+            a = cnx.create_entity('Employee')
+            cnx.create_entity('Company', employees=e, associates=a)
+            cnx.commit()
+            company = cnx.execute('Company X').get_entity(0, 0)
+            self.assertEqual([e.eid],
+                             [x.eid for x in company.reverse_works_for])
+            mh.rollback()
+
+    def test_computed_relation_drop_relation_type(self):
+        self.assertIn('notes', self.schema)
+        with self.mh() as (cnx, mh):
+            mh.cmd_drop_relation_type('notes')
+        self.assertNotIn('notes', self.schema)
+
+    def test_computed_relation_sync_schema_props_perms(self):
+        self.assertIn('whatever', self.schema)
+        with self.mh() as (cnx, mh):
+            mh.cmd_sync_schema_props_perms('whatever')
+            self.assertEqual(self.schema['whatever'].rule,
+                             'S employees E, O associates E')
+            self.assertEqual(self.schema['whatever'].objects(), ('Company',))
+            self.assertEqual(self.schema['whatever'].subjects(), ('Company',))
+            self.assertFalse(self.table_sql(mh, 'whatever_relation'))
+
+    def test_computed_relation_sync_schema_props_perms_on_rdef(self):
+        self.assertIn('whatever', self.schema)
+        with self.mh() as (cnx, mh):
+            with self.assertRaises(ExecutionError) as exc:
+                mh.cmd_sync_schema_props_perms(
+                    ('Company', 'whatever', 'Person'))
+        self.assertEqual(str(exc.exception),
+                         'Cannot synchronize a relation definition for a computed '
+                         'relation (whatever)')
+
+    # computed attributes migration ############################################
+
+    def setup_add_score(self):
+        with self.admin_access.client_cnx() as cnx:
+            assert not cnx.execute('Company X')
+            c = cnx.create_entity('Company')
+            e1 = cnx.create_entity('Employee', reverse_employees=c)
+            n1 = cnx.create_entity('Note', note=2, concerns=e1)
+            e2 = cnx.create_entity('Employee', reverse_employees=c)
+            n2 = cnx.create_entity('Note', note=4, concerns=e2)
+            cnx.commit()
+
+    def assert_score_initialized(self, mh):
+        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([[3.0]],
+                         mh.rqlexec('Any CS WHERE C score CS, C is Company').rows)
+
+    def test_computed_attribute_add_relation_type(self):
+        self.assertNotIn('score', self.schema)
+        self.setup_add_score()
+        with self.mh() as (cnx, mh):
+            mh.cmd_add_relation_type('score')
+            self.assertIn('score', self.schema)
+            self.assertEqual(self.schema['score'].objects(), ('Float',))
+            self.assertEqual(self.schema['score'].subjects(), ('Company',))
+            self.assert_score_initialized(mh)
+
+    def test_computed_attribute_add_attribute(self):
+        self.assertNotIn('score', self.schema)
+        self.setup_add_score()
+        with self.mh() as (cnx, mh):
+            mh.cmd_add_attribute('Company', 'score')
+            self.assertIn('score', self.schema)
+            self.assert_score_initialized(mh)
+
+    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)
+
+    def test_computed_attribute_drop_type(self):
+        self.assertIn('note20', self.schema)
+        with self.mh() as (cnx, mh):
+            mh.cmd_drop_relation_type('note20')
+        self.assert_computed_attribute_dropped()
+
+    def test_computed_attribute_drop_relation_definition(self):
+        self.assertIn('note20', self.schema)
+        with self.mh() as (cnx, mh):
+            mh.cmd_drop_relation_definition('Note', 'note20', 'Int')
+        self.assert_computed_attribute_dropped()
+
+    def test_computed_attribute_drop_attribute(self):
+        self.assertIn('note20', self.schema)
+        with self.mh() as (cnx, mh):
+            mh.cmd_drop_attribute('Note', 'note20')
+        self.assert_computed_attribute_dropped()
+
+    def test_computed_attribute_sync_schema_props_perms_rtype(self):
+        self.assertIn('note100', self.schema)
+        with self.mh() as (cnx, mh):
+            mh.cmd_sync_schema_props_perms('note100')
+        self.assertEqual(self.schema['note100'].rdefs['Note', 'Int'].formula,
+                         'Any N*100 WHERE X note N')
+
+    def test_computed_attribute_sync_schema_props_perms_rdef(self):
+        self.setup_add_score()
+        with self.mh() as (cnx, mh):
+            mh.cmd_sync_schema_props_perms(('Note', 'note100', 'Int'))
+            self.assertEqual([[200], [400]],
+                             cnx.execute('Any N ORDERBY N WHERE X note100 N').rows)
+            self.assertEqual([[300]],
+                             cnx.execute('Any CS WHERE C score100 CS, C is Company').rows)
+
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_querier.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/test/unittest_querier.py	Thu Jan 22 17:45:06 2015 +0100
@@ -173,11 +173,11 @@
                                            'ET': 'CWEType', 'ETN': 'String'}])
             rql, solutions = partrqls[1]
             self.assertRQLEqual(rql,  'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, '
-                                'X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, '
-                                '        CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, '
-                                '        CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, Comment, '
-                                '        Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Frozable, '
-                                '        Note, Old, Personne, RQLExpression, Societe, State, SubDivision, '
+                                'X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWComputedRType, '
+                                '        CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, '
+                                '        CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, '
+                                '        Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, '
+                                '        Frozable, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, '
                                 '        SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
             self.assertListEqual(sorted(solutions),
                                   sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
@@ -186,6 +186,7 @@
                                           {'X': 'Comment', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'Division', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'CWCache', 'ETN': 'String', 'ET': 'CWEType'},
+                                          {'X': 'CWComputedRType', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'CWConstraint', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'CWConstraintType', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'CWEType', 'ETN': 'String', 'ET': 'CWEType'},
@@ -603,18 +604,18 @@
                             'WHERE RT name N, RDEF relation_type RT '
                             'HAVING COUNT(RDEF) > 10')
         self.assertListEqual(rset.rows,
-                              [[u'description_format', 12],
-                               [u'description', 13],
-                               [u'name', 18],
-                               [u'created_by', 44],
-                               [u'creation_date', 44],
-                               [u'cw_source', 44],
-                               [u'cwuri', 44],
-                               [u'in_basket', 44],
-                               [u'is', 44],
-                               [u'is_instance_of', 44],
-                               [u'modification_date', 44],
-                               [u'owned_by', 44]])
+                              [[u'description_format', 13],
+                               [u'description', 14],
+                               [u'name', 19],
+                               [u'created_by', 45],
+                               [u'creation_date', 45],
+                               [u'cw_source', 45],
+                               [u'cwuri', 45],
+                               [u'in_basket', 45],
+                               [u'is', 45],
+                               [u'is_instance_of', 45],
+                               [u'modification_date', 45],
+                               [u'owned_by', 45]])
 
     def test_select_aggregat_having_dumb(self):
         # dumb but should not raise an error
--- a/server/test/unittest_repository.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/test/unittest_repository.py	Thu Jan 22 17:45:06 2015 +0100
@@ -280,7 +280,7 @@
         self.assertListEqual(['relation_type',
                               'from_entity', 'to_entity',
                               'constrained_by',
-                              'cardinality', 'ordernum',
+                              'cardinality', 'ordernum', 'formula',
                               'indexed', 'fulltextindexed', 'internationalizable',
                               'defaultval', 'extra_props',
                               'description', 'description_format'],
--- a/server/test/unittest_schemaserial.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/test/unittest_schemaserial.py	Thu Jan 22 17:45:06 2015 +0100
@@ -25,6 +25,7 @@
 from cubicweb import Binary
 from cubicweb.schema import CubicWebSchemaLoader
 from cubicweb.devtools import TestServerConfiguration
+from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb.server.schemaserial import (updateeschema2rql, updaterschema2rql, rschema2rql,
                                           eschema2rql, rdef2rql, specialize2rql,
@@ -221,7 +222,7 @@
               'inlined': False}),
 
             ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,'
-             'X description %(description)s,X fulltextindexed %(fulltextindexed)s,'
+             'X description %(description)s,X formula %(formula)s,X fulltextindexed %(fulltextindexed)s,'
              'X indexed %(indexed)s,X internationalizable %(internationalizable)s,'
              'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,'
              'X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
@@ -234,6 +235,7 @@
               'ordernum': 5,
               'defaultval': None,
               'indexed': False,
+              'formula': None,
               'cardinality': u'?1'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
              'WHERE CT eid %(ct)s, EDEF eid %(x)s',
@@ -247,7 +249,7 @@
               'value': u"u'?1', u'11'"}),
 
             ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,'
-             'X description %(description)s,X fulltextindexed %(fulltextindexed)s,'
+             'X description %(description)s,X formula %(formula)s,X fulltextindexed %(fulltextindexed)s,'
              'X indexed %(indexed)s,X internationalizable %(internationalizable)s,'
              'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE '
              'WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
@@ -260,6 +262,7 @@
               'ordernum': 5,
               'defaultval': None,
               'indexed': False,
+              'formula': None,
               'cardinality': u'?1'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
              'WHERE CT eid %(ct)s, EDEF eid %(x)s',
@@ -272,7 +275,7 @@
               'ct': u'StaticVocabularyConstraint_eid',
               'value': (u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', "
                         "u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'")})],
-                             list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)))
+              list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)))
 
     def test_rschema2rql_custom_type(self):
         expected = [('INSERT CWRType X: X description %(description)s,X final %(final)s,'
@@ -286,13 +289,14 @@
                       'symmetric': False}),
                      ('INSERT CWAttribute X: X cardinality %(cardinality)s,'
                       'X defaultval %(defaultval)s,X description %(description)s,'
-                      'X extra_props %(extra_props)s,X indexed %(indexed)s,'
+                      'X extra_props %(extra_props)s,X formula %(formula)s,X indexed %(indexed)s,'
                       'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,'
                       'X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
                       {'cardinality': u'?1',
                        'defaultval': None,
                        'description': u'',
                        'extra_props': '{"jungle_speed": 42}',
+                       'formula': None,
                        'indexed': False,
                        'oe': None,
                        'ordernum': 4,
@@ -312,7 +316,7 @@
     def test_rdef2rql(self):
         self.assertListEqual([
             ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,'
-             'X description %(description)s,X fulltextindexed %(fulltextindexed)s,'
+             'X description %(description)s,X formula %(formula)s,X fulltextindexed %(fulltextindexed)s,'
              'X indexed %(indexed)s,X internationalizable %(internationalizable)s,'
              'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,'
              'X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
@@ -325,6 +329,7 @@
               'ordernum': 3,
               'defaultval': Binary.zpickle(u'text/plain'),
               'indexed': False,
+              'formula': None,
               'cardinality': u'?1'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
              'WHERE CT eid %(ct)s, EDEF eid %(x)s',
@@ -424,7 +429,19 @@
     #    self.assertListEqual(perms2rql(schema, self.GROUP_MAPPING),
     #                         ['INSERT CWEType X: X name 'Societe', X final FALSE'])
 
+class ComputedAttributeAndRelationTC(CubicWebTC):
+    appid = 'data-cwep002'
 
+    def test(self):
+        # force to read schema from the database
+        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('O works_for S',
+                         schema['has_employee'].rule)
+        self.assertEqual([('Company', 'Int')], list(schema['total_salary'].rdefs))
+        self.assertEqual('Any SUM(SA) GROUPBY X WHERE P works_for X, P salary SA',
+                         schema['total_salary'].rdefs['Company', 'Int'].formula)
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_security.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/test/unittest_security.py	Thu Jan 22 17:45:06 2015 +0100
@@ -22,7 +22,7 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import Unauthorized, ValidationError, QueryError, Binary
 from cubicweb.schema import ERQLExpression
-from cubicweb.server.querier import check_read_access
+from cubicweb.server.querier import get_local_checks, check_relations_read_access
 from cubicweb.server.utils import _CRYPTO_CTX
 
 
@@ -37,18 +37,33 @@
 
 class LowLevelSecurityFunctionTC(BaseSecurityTC):
 
-    def test_check_read_access(self):
-        rql = u'Personne U where U nom "managers"'
+    def test_check_relation_read_access(self):
+        rql = u'Personne U WHERE U nom "managers"'
+        rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
+        nom = self.repo.schema['Personne'].rdef('nom')
+        with self.temporary_permissions((nom, {'read': ('users', 'managers')})):
+            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:
+                self.assertRaises(Unauthorized,
+                                  check_relations_read_access,
+                                  cnx, rqlst, {})
+                self.assertRaises(Unauthorized, cnx.execute, rql)
+
+    def test_get_local_checks(self):
+        rql = u'Personne U WHERE U nom "managers"'
         rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
         with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
             with self.admin_access.repo_cnx() as cnx:
                 self.repo.vreg.solutions(cnx, rqlst, None)
                 solution = rqlst.solutions[0]
-                check_read_access(cnx, rqlst, solution, {})
+                localchecks = get_local_checks(cnx, rqlst, solution)
+                self.assertEqual({}, localchecks)
             with self.new_access('anon').repo_cnx() as cnx:
                 self.assertRaises(Unauthorized,
-                                  check_read_access,
-                                  cnx, rqlst, solution, {})
+                                  get_local_checks,
+                                  cnx, rqlst, solution)
                 self.assertRaises(Unauthorized, cnx.execute, rql)
 
     def test_upassword_not_selectable(self):
--- a/server/test/unittest_undo.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/test/unittest_undo.py	Thu Jan 22 17:45:06 2015 +0100
@@ -106,13 +106,20 @@
             self.assertEqual(a4.eid_from, self.totoeid)
             self.assertEqual(a4.eid_to, self.toto(cnx).in_group[0].eid)
             self.assertEqual(a4.order, 4)
-            for i, rtype in ((1, 'owned_by'), (2, 'owned_by'),
-                             (4, 'in_state'), (5, 'created_by')):
+            for i, rtype in ((1, 'owned_by'), (2, 'owned_by')):
                 a = actions[i]
                 self.assertEqual(a.action, 'A')
                 self.assertEqual(a.eid_from, self.totoeid)
                 self.assertEqual(a.rtype, rtype)
                 self.assertEqual(a.order, i+1)
+            self.assertEqual(set((actions[4].rtype, actions[5].rtype)),
+                             set(('in_state', 'created_by')))
+            for i in (4, 5):
+                a = actions[i]
+                self.assertEqual(a.action, 'A')
+                self.assertEqual(a.eid_from, self.totoeid)
+                self.assertEqual(a.order, i+1)
+
             # test undoable_transactions
             txs = cnx.undoable_transactions()
             self.assertEqual(len(txs), 1)
--- a/server/utils.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/server/utils.py	Thu Jan 22 17:45:06 2015 +0100
@@ -81,7 +81,7 @@
     if eschema.eid is None:
         eschema.eid = cnx.execute(
             'Any X WHERE X is CWEType, X name %(name)s',
-            {'name': str(eschema)})[0][0]
+            {'name': unicode(eschema)})[0][0]
     return eschema.eid
 
 
--- a/skeleton/debian/control.tmpl	Thu Jan 22 17:39:07 2015 +0100
+++ b/skeleton/debian/control.tmpl	Thu Jan 22 17:45:06 2015 +0100
@@ -2,9 +2,11 @@
 Section: web
 Priority: optional
 Maintainer: %(author)s <%(author-email)s>
-Build-Depends: debhelper (>= 7), python (>= 2.6), python-support
+Build-Depends:
+ debhelper (>= 7),
+ python (>= 2.6.5),
 Standards-Version: 3.9.3
-XS-Python-Version: >= 2.6
+X-Python-Version: >= 2.6
 
 Package: %(distname)s
 Architecture: all
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/skeleton/debian/rules	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,14 @@
+#!/usr/bin/make -f
+
+export NO_SETUPTOOLS=1
+
+%:
+	dh $@ --with python2
+
+override_dh_auto_install:
+	dh_auto_install
+	# remove generated .egg-info file
+	rm -rf debian/*/usr/lib/python*
+
+override_dh_python2:
+	dh_python2 -i /usr/share/cubicweb
--- a/skeleton/debian/rules.tmpl	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-#!/usr/bin/make -f
-# Sample debian/rules that uses debhelper.
-# GNU copyright 1997 to 1999 by Joey Hess.
-
-# Uncomment this to turn on verbose mode.
-#export DH_VERBOSE=1
-build: build-arch build-indep
-build-arch:
-	# Nothing to do
-build-indep: build-stamp
-build-stamp:
-	dh_testdir
-	NO_SETUPTOOLS=1 python setup.py -q build
-	touch build-stamp
-
-clean:
-	dh_testdir
-	rm -f build-stamp configure-stamp
-	rm -rf build
-	find . -name "*.pyc" -delete
-	dh_clean
-
-install: build
-	dh_testdir
-	dh_testroot
-	dh_clean -k
-	dh_installdirs -i
-	NO_SETUPTOOLS=1 python setup.py -q install --no-compile --prefix=debian/%(distname)s/usr/
-	# remove generated .egg-info file
-	rm -rf debian/%(distname)s/usr/lib/python*
-
-
-# Build architecture-independent files here.
-binary-indep: build install
-	dh_testdir
-	dh_testroot
-	dh_install -i
-	dh_installchangelogs -i
-	dh_installexamples -i
-	dh_installdocs -i README
-	dh_installman -i
-	dh_pysupport -i /usr/share/cubicweb
-	dh_link -i
-	dh_compress -i -X.py -X.ini -X.xml -Xtest
-	dh_fixperms -i
-	dh_installdeb -i
-	dh_gencontrol -i
-	dh_md5sums -i
-	dh_builddeb -i
-
-
-# Build architecture-dependent files here.
-binary-arch:
-
-binary: binary-indep
-.PHONY: build clean binary-arch binary-indep binary
--- a/sobjects/cwxmlparser.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/sobjects/cwxmlparser.py	Thu Jan 22 17:45:06 2015 +0100
@@ -195,7 +195,7 @@
                 parser=self)
             yield builder.build_item()
 
-    def process_item(self, item, rels):
+    def process_item(self, item, rels, raise_on_error=False):
         """
         item and rels are what's returned by the item builder `build_item` method:
 
@@ -204,7 +204,8 @@
            {role: {relation: [(related item, related rels)...]}
         """
         entity = self.extid2entity(str(item['cwuri']), item['cwtype'],
-                                   cwsource=item['cwsource'], item=item)
+                                   cwsource=item['cwsource'], item=item,
+                                   raise_on_error=raise_on_error)
         if entity is None:
             return None
         if entity.eid in self._processed_entities:
--- a/sobjects/ldapparser.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/sobjects/ldapparser.py	Thu Jan 22 17:45:06 2015 +0100
@@ -70,10 +70,11 @@
                                                         attrs))
         return {}
 
-    def _process(self, etype, sdict):
+    def _process(self, etype, sdict, raise_on_error=False):
         self.debug('fetched %s %s', etype, sdict)
         extid = sdict['dn']
-        entity = self.extid2entity(extid, etype, **sdict)
+        entity = self.extid2entity(extid, etype,
+                                   raise_on_error=raise_on_error, **sdict)
         if entity is not None and not self.created_during_pull(entity):
             self.notify_updated(entity)
             attrs = self.ldap2cwattrs(sdict, etype)
@@ -90,7 +91,7 @@
             self._process('CWUser', userdict)
         self.debug('processing ldapfeed source %s %s', self.source, self.searchgroupfilterstr)
         for groupdict in self.group_source_entities_by_extid.itervalues():
-            self._process('CWGroup', groupdict)
+            self._process('CWGroup', groupdict, raise_on_error=raise_on_error)
 
     def handle_deletion(self, config, cnx, myuris):
         if config['delete-entities']:
--- a/sobjects/notification.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/sobjects/notification.py	Thu Jan 22 17:45:06 2015 +0100
@@ -80,15 +80,8 @@
 
     # this is usually the method to call
     def render_and_send(self, **kwargs):
-        """generate and send an email message for this view"""
-        delayed = kwargs.pop('delay_to_commit', None)
-        for recipients, msg in self.render_emails(**kwargs):
-            if delayed is None:
-                self.send(recipients, msg)
-            elif delayed:
-                self.send_on_commit(recipients, msg)
-            else:
-                self.send_now(recipients, msg)
+        """generate and send email messages for this view"""
+        self._cw.vreg.config.sendmails(self.render_emails(**kwargs))
 
     def cell_call(self, row, col=0, **kwargs):
         self.w(self._cw._(self.content) % self.context(**kwargs))
@@ -146,16 +139,11 @@
                         continue
                     msg = format_mail(self.user_data, [emailaddr], content, subject,
                                       config=self._cw.vreg.config, msgid=msgid, references=refs)
-                    yield [emailaddr], msg
+                    yield msg, [emailaddr]
                 finally:
-                    # ensure we have a cnxset since commit will fail if there is
-                    # some operation but no cnxset. This may occurs in this very
-                    # specific case (eg SendMailOp)
-                    with cnx.ensure_cnx_set:
-                        cnx.commit()
                     self._cw = req
 
-    # recipients / email sending ###############################################
+    # recipients handling ######################################################
 
     def recipients(self):
         """return a list of either 2-uple (email, language) or user entity to
@@ -166,13 +154,6 @@
             row=self.cw_row or 0, col=self.cw_col or 0)
         return finder.recipients()
 
-    def send_now(self, recipients, msg):
-        self._cw.vreg.config.sendmails([(msg, recipients)])
-
-    def send_on_commit(self, recipients, msg):
-        SendMailOp(self._cw, recipients=recipients, msg=msg)
-    send = send_on_commit
-
     # email generation helpers #################################################
 
     def construct_message_id(self, eid):
--- a/test/data/rewrite/schema.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/test/data/rewrite/schema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -15,9 +15,15 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from yams.buildobjs import EntityType, RelationDefinition, String, SubjectRelation
+from yams.buildobjs import (EntityType, RelationDefinition, String, SubjectRelation,
+                            ComputedRelation, Int)
 from cubicweb.schema import ERQLExpression
 
+
+class Person(EntityType):
+    name = String()
+
+
 class Affaire(EntityType):
     __permissions__ = {
         'read':   ('managers',
@@ -82,3 +88,37 @@
     object = 'CWUser'
     inlined = True
     cardinality = '1*'
+
+class Contribution(EntityType):
+    code = Int()
+
+class ArtWork(EntityType):
+    name = String()
+
+class Role(EntityType):
+    name = String()
+
+class contributor(RelationDefinition):
+    subject = 'Contribution'
+    object = 'Person'
+    cardinality = '1*'
+    inlined = True
+
+class manifestation(RelationDefinition):
+    subject = 'Contribution'
+    object = 'ArtWork'
+
+class role(RelationDefinition):
+    subject = 'Contribution'
+    object = 'Role'
+
+class illustrator_of(ComputedRelation):
+    rule = ('C is Contribution, C contributor S, C manifestation O, '
+            'C role R, R name "illustrator"')
+
+class participated_in(ComputedRelation):
+    rule = 'S contributor O'
+
+class match(RelationDefinition):
+    subject = 'ArtWork'
+    object = 'Note'
--- a/test/unittest_dataimport.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/test/unittest_dataimport.py	Thu Jan 22 17:45:06 2015 +0100
@@ -1,6 +1,88 @@
+# -*- 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):
--- a/test/unittest_dbapi.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/test/unittest_dbapi.py	Thu Jan 22 17:45:06 2015 +0100
@@ -78,7 +78,7 @@
         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.base_url(), 'http://perdu.com/')
             self.assertEqual(req.from_controller(), 'view')
             self.assertEqual(req.relative_path(), '')
             req.ajax_replace_url('domid') # don't crash
--- a/test/unittest_entity.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/test/unittest_entity.py	Thu Jan 22 17:45:06 2015 +0100
@@ -587,6 +587,16 @@
             # should be default groups but owners, i.e. managers, users, guests
             self.assertEqual(len(unrelated), 3)
 
+    def test_markdown_printable_value_string(self):
+        with self.admin_access.web_request() as req:
+            e = req.create_entity('Card', title=u'rest markdown',
+                                  content=u'This is [an example](http://example.com/ "Title") inline link`',
+                                  content_format=u'text/markdown')
+            self.assertEqual(
+                u'<p>This is <a href="http://example.com/" '
+                u'title="Title">an example</a> inline link`</p>',
+                e.printable_value('content'))
+
     def test_printable_value_string(self):
         with self.admin_access.web_request() as req:
             e = req.create_entity('Card', title=u'rest test',
--- a/test/unittest_rqlrewrite.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/test/unittest_rqlrewrite.py	Thu Jan 22 17:45:06 2015 +0100
@@ -19,6 +19,7 @@
 from logilab.common.testlib import unittest_main, TestCase
 from logilab.common.testlib import mock_object
 from yams import BadSchemaDefinition
+from yams.buildobjs import RelationDefinition
 from rql import parse, nodes, RQLHelper
 
 from cubicweb import Unauthorized, rqlrewrite
@@ -31,10 +32,8 @@
     config = TestServerConfiguration(RQLRewriteTC.datapath('rewrite'))
     config.bootstrap_cubes()
     schema = config.load_schema()
-    from yams.buildobjs import RelationDefinition
     schema.add_relation_def(RelationDefinition(subject='Card', name='in_state',
                                                object='State', cardinality='1*'))
-
     rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
                                                      'has_text': 'fti'})
     repotest.do_monkey_patch()
@@ -49,11 +48,11 @@
             2: 'Card',
             3: 'Affaire'}[eid]
 
-def rewrite(rqlst, snippets_map, kwargs, existingvars=None):
+def _prepare_rewriter(rewriter_cls, kwargs):
     class FakeVReg:
         schema = schema
         @staticmethod
-        def solutions(sqlcursor, mainrqlst, kwargs):
+        def solutions(sqlcursor, rqlst, kwargs):
             rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs)
         class rqlhelper:
             @staticmethod
@@ -62,8 +61,10 @@
             @staticmethod
             def simplify(mainrqlst, needcopy=False):
                 rqlhelper.simplify(rqlst, needcopy)
-    rewriter = rqlrewrite.RQLRewriter(
-        mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
+    return rewriter_cls(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
+
+def rewrite(rqlst, snippets_map, kwargs, existingvars=None):
+    rewriter = _prepare_rewriter(rqlrewrite.RQLRewriter, kwargs)
     snippets = []
     for v, exprs in sorted(snippets_map.items()):
         rqlexprs = [isinstance(snippet, basestring)
@@ -87,7 +88,7 @@
         except KeyError:
             vrefmaps[stmt] = {vref.name: set( (vref,) )}
             selects.append(stmt)
-    assert node in selects
+    assert node in selects, (node, selects)
     for stmt in selects:
         for var in stmt.defined_vars.itervalues():
             assert var.stinfo['references']
@@ -591,5 +592,223 @@
         finally:
             RQLRewriter.insert_snippets = orig_insert_snippets
 
+
+class RQLRelationRewriterTC(TestCase):
+    # XXX valid rules: S and O specified, not in a SET, INSERT, DELETE scope
+    #     valid uses: no outer join
+
+    # Basic tests
+    def test_base_rule(self):
+        rules = {'participated_in': 'S contributor O'}
+        rqlst = rqlhelper.parse('Any X WHERE X participated_in S')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any X WHERE X contributor S',
+                         rqlst.as_string())
+
+    def test_complex_rule_1(self):
+        rules = {'illustrator_of': ('C is Contribution, C contributor S, '
+                                    'C manifestation O, C role R, '
+                                    'R name "illustrator"')}
+        rqlst = rqlhelper.parse('Any A,B WHERE A illustrator_of B')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE C is Contribution, '
+                         'C contributor A, C manifestation B, '
+                         'C role D, D name "illustrator"',
+                         rqlst.as_string())
+
+    def test_complex_rule_2(self):
+        rules = {'illustrator_of': ('C is Contribution, C contributor S, '
+                                    'C manifestation O, C role R, '
+                                    'R name "illustrator"')}
+        rqlst = rqlhelper.parse('Any A WHERE EXISTS(A illustrator_of B)')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A WHERE EXISTS(C is Contribution, '
+                         'C contributor A, C manifestation B, '
+                         'C role D, D name "illustrator")',
+                         rqlst.as_string())
+
+
+    def test_rewrite2(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('Any A,B WHERE A illustrator_of B, C require_permission R, S'
+                                'require_state O')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE C require_permission R, S require_state O, '
+                         'D is Contribution, D contributor A, D manifestation B, D role E, '
+                         'E name "illustrator"',
+                          rqlst.as_string())
+
+    def test_rewrite3(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('Any A,B WHERE E require_permission T, A illustrator_of B')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE E require_permission T, '
+                         'C is Contribution, C contributor A, C manifestation B, '
+                         'C role D, D name "illustrator"',
+                         rqlst.as_string())
+
+    def test_rewrite4(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('Any A,B WHERE C require_permission R, A illustrator_of B')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE C require_permission R, '
+                         'D is Contribution, D contributor A, D manifestation B, '
+                         'D role E, E name "illustrator"',
+                         rqlst.as_string())
+
+    def test_rewrite5(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('Any A,B WHERE C require_permission R, A illustrator_of B, '
+                                'S require_state O')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE C require_permission R, S require_state O, '
+                         'D is Contribution, D contributor A, D manifestation B, D role E, '
+                         'E name "illustrator"',
+                         rqlst.as_string())
+
+    # Tests for the with clause
+    def test_rewrite_with(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('Any A,B WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WITH A,B BEING '
+                         '(Any X,Y WHERE A is Contribution, A contributor X, '
+                         'A manifestation Y, A role B, B name "illustrator")',
+                         rqlst.as_string())
+
+    def test_rewrite_with2(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('Any A,B WHERE T require_permission C WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE T require_permission C '
+                         'WITH A,B BEING (Any X,Y WHERE A is Contribution, '
+                         'A contributor X, A manifestation Y, A role B, B name "illustrator")',
+                         rqlst.as_string())
+
+    def test_rewrite_with3(self):
+        rules = {'participated_in': 'S contributor O'}
+        rqlst = rqlhelper.parse('Any A,B WHERE A participated_in B '
+                                'WITH A, B BEING(Any X,Y WHERE X contributor Y)')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE A contributor B WITH A,B BEING '
+                         '(Any X,Y WHERE X contributor Y)',
+                         rqlst.as_string())
+
+    def test_rewrite_with4(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('Any A,B WHERE A illustrator_of B '
+                               'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE C is Contribution, '
+                         'C contributor A, C manifestation B, C role D, '
+                         'D name "illustrator" WITH A,B BEING '
+                         '(Any X,Y WHERE A is Contribution, A contributor X, '
+                         'A manifestation Y, A role B, B name "illustrator")',
+                          rqlst.as_string())
+
+    # Tests for the union
+    def test_rewrite_union(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('(Any A,B WHERE A illustrator_of B) UNION'
+                                '(Any X,Y WHERE X is CWUser, Z manifestation Y)')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('(Any A,B WHERE C is Contribution, '
+                         'C contributor A, C manifestation B, C role D, '
+                         'D name "illustrator") UNION (Any X,Y WHERE X is CWUser, Z manifestation Y)',
+                         rqlst.as_string())
+
+    def test_rewrite_union2(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('(Any Y WHERE Y match W) UNION '
+                                '(Any A WHERE A illustrator_of B) UNION '
+                                '(Any Y WHERE Y is ArtWork)')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('(Any Y WHERE Y match W) '
+                         'UNION (Any A WHERE C is Contribution, C contributor A, '
+                         'C manifestation B, C role D, D name "illustrator") '
+                         'UNION (Any Y WHERE Y is ArtWork)',
+                         rqlst.as_string())
+
+    # Tests for the exists clause
+    def test_rewrite_exists(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('(Any A,B WHERE A illustrator_of B, '
+                     'EXISTS(B is ArtWork))')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE EXISTS(B is ArtWork), '
+                         'C is Contribution, C contributor A, C manifestation B, C role D, '
+                         'D name "illustrator"',
+                         rqlst.as_string())
+
+    def test_rewrite_exists2(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('(Any A,B WHERE B contributor A, EXISTS(A illustrator_of W))')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE B contributor A, '
+                         'EXISTS(C is Contribution, C contributor A, C manifestation W, '
+                         'C role D, D name "illustrator")',
+                         rqlst.as_string())
+
+    def test_rewrite_exists3(self):
+        rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+                'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse('(Any A,B WHERE A illustrator_of B, EXISTS(A illustrator_of W))')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any A,B WHERE EXISTS(C is Contribution, C contributor A, '
+                         'C manifestation W, C role D, D name "illustrator"), '
+                         'E is Contribution, E contributor A, E manifestation B, E role F, '
+                         'F name "illustrator"',
+                         rqlst.as_string())
+
+    # Test for GROUPBY
+    def test_rewrite_groupby(self):
+        rules = {'participated_in': 'S contributor O'}
+        rqlst = rqlhelper.parse('Any SUM(SA) GROUPBY S WHERE P participated_in S, P manifestation SA')
+        rule_rewrite(rqlst, rules)
+        self.assertEqual('Any SUM(SA) GROUPBY S WHERE P manifestation SA, P contributor S',
+                         rqlst.as_string())
+
+
+class RQLRelationRewriterTC(CubicWebTC):
+
+    appid = 'data/rewrite'
+
+    def test_base_rule(self):
+        with self.admin_access.client_cnx() as cnx:
+            art = cnx.create_entity('ArtWork', name=u'Les travailleurs de la Mer')
+            role = cnx.create_entity('Role', name=u'illustrator')
+            vic = cnx.create_entity('Person', name=u'Victor Hugo')
+            contrib = cnx.create_entity('Contribution', code=96, contributor=vic,
+                                        manifestation=art, role=role)
+            rset = cnx.execute('Any X WHERE X illustrator_of S')
+            self.assertEqual([u'Victor Hugo'],
+                             [result.name for result in rset.entities()])
+            rset = cnx.execute('Any S WHERE X illustrator_of S, X eid %(x)s',
+                               {'x': vic.eid})
+            self.assertEqual([u'Les travailleurs de la Mer'],
+                             [result.name for result in rset.entities()])
+
+
+def rule_rewrite(rqlst, kwargs=None):
+    rewriter = _prepare_rewriter(rqlrewrite.RQLRelationRewriter, kwargs)
+    rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map},
+                                kwargs=kwargs)
+    rewriter.rewrite(rqlst)
+    for select in rqlst.children:
+        test_vrefs(select)
+    return rewriter.rewritten
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_rset.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/test/unittest_rset.py	Thu Jan 22 17:45:06 2015 +0100
@@ -100,7 +100,7 @@
 
     def test_pickle(self):
         del self.rset.req
-        self.assertEqual(len(pickle.dumps(self.rset)), 392)
+        self.assertEqual(len(pickle.dumps(self.rset)), 376)
 
     def test_build_url(self):
         with self.admin_access.web_request() as req:
--- a/test/unittest_schema.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/test/unittest_schema.py	Thu Jan 22 17:45:06 2015 +0100
@@ -26,14 +26,16 @@
 
 from yams import ValidationError, BadSchemaDefinition
 from yams.constraints import SizeConstraint, StaticVocabularyConstraint
-from yams.buildobjs import RelationDefinition, EntityType, RelationType
+from yams.buildobjs import (RelationDefinition, EntityType, RelationType,
+                            Int, String, SubjectRelation, ComputedRelation)
 from yams.reader import fill_schema
 
 from cubicweb.schema import (
     CubicWebSchema, CubicWebEntitySchema, CubicWebSchemaLoader,
     RQLConstraint, RQLUniqueConstraint, RQLVocabularyConstraint,
     RQLExpression, ERQLExpression, RRQLExpression,
-    normalize_expression, order_eschemas, guess_rrqlexpr_mainvars)
+    normalize_expression, order_eschemas, guess_rrqlexpr_mainvars,
+    build_schema_from_namespace)
 from cubicweb.devtools import TestServerConfiguration as TestConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -161,9 +163,10 @@
         entities = sorted([str(e) for e in schema.entities()])
         expected_entities = ['Ami', 'BaseTransition', 'BigInt', 'Bookmark', 'Boolean', 'Bytes', 'Card',
                              'Date', 'Datetime', 'Decimal',
-                             'CWCache', 'CWConstraint', 'CWConstraintType', 'CWDataImport',
-                             'CWEType', 'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation',
-                             'CWPermission', 'CWProperty', 'CWRType',
+                             'CWCache', 'CWComputedRType', 'CWConstraint',
+                             'CWConstraintType', 'CWDataImport', 'CWEType',
+                             'CWAttribute', 'CWGroup', 'EmailAddress',
+                             'CWRelation', 'CWPermission', 'CWProperty', 'CWRType',
                              'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig',
                              'CWUniqueTogetherConstraint', 'CWUser',
                              'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
@@ -190,7 +193,7 @@
 
                               'ean', 'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype', 'extra_props',
 
-                              'fabrique_par', 'final', 'firstname', 'for_user', 'fournit',
+                              'fabrique_par', 'final', 'firstname', 'for_user', 'formula', 'fournit',
                               'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed',
 
                               'has_group_permission', 'has_text',
@@ -207,7 +210,7 @@
 
                               'parser', 'path', 'pkey', 'prefered_form', 'prenom', 'primary_email',
 
-                              'read_permission', 'relation_type', 'relations', 'require_group',
+                              'read_permission', 'relation_type', 'relations', 'require_group', 'rule',
 
                               'specializes', 'start_timestamp', 'state_of', 'status', 'subworkflow', 'subworkflow_exit', 'subworkflow_state', 'surname', 'symmetric', 'synopsis',
 
@@ -281,6 +284,88 @@
                           'add': ('managers',),
                           'delete': ('managers',)})
 
+    def test_computed_attribute(self):
+        """Check schema finalization for computed attributes."""
+        class Person(EntityType):
+            salary = Int()
+
+        class works_for(RelationDefinition):
+            subject = 'Person'
+            object  = 'Company'
+            cardinality = '?*'
+
+        class Company(EntityType):
+            total_salary = Int(formula='Any SUM(SA) GROUPBY X WHERE '
+                                       'P works_for X, P salary SA')
+        good_schema = build_schema_from_namespace(vars().items())
+
+        class Company(EntityType):
+            total_salary = String(formula='Any SUM(SA) GROUPBY X WHERE '
+                                          'P works_for X, P salary SA')
+
+        with self.assertRaises(BadSchemaDefinition) as exc:
+            bad_schema = build_schema_from_namespace(vars().items())
+
+        self.assertEqual(str(exc.exception),
+                         'computed attribute total_salary on Company: '
+                         'computed attribute type (Int) mismatch with '
+                         'specified type (String)')
+
+
+class SchemaReaderComputedRelationAndAttributesTest(TestCase):
+
+    def test_infer_computed_relation(self):
+        class Person(EntityType):
+            name = String()
+
+        class Company(EntityType):
+            name  = String()
+
+        class Service(EntityType):
+            name = String()
+
+        class works_for(RelationDefinition):
+            subject = 'Person'
+            object = 'Company'
+
+        class produce(RelationDefinition):
+            subject = ('Person', 'Company')
+            object = 'Service'
+
+        class achete(RelationDefinition):
+            subject = 'Person'
+            object = 'Service'
+
+        class produces_and_buys(ComputedRelation):
+            rule = 'S produce O, S achete O'
+
+        class produces_and_buys2(ComputedRelation):
+            rule = 'S works_for SO, SO produce O'
+
+        class reproduce(ComputedRelation):
+            rule = 'S produce O'
+
+        schema = build_schema_from_namespace(vars().items())
+
+        # check object/subject type
+        self.assertEqual([('Person','Service')],
+                         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())
+        # check relations as marked infered
+        self.assertTrue(
+            schema['produces_and_buys'].rdefs[('Person','Service')].infered)
+
+        del schema
+        class autoname(ComputedRelation):
+            rule = 'S produce X, X name O'
+
+        with self.assertRaises(BadSchemaDefinition) as cm:
+            build_schema_from_namespace(vars().items())
+        self.assertEqual(str(cm.exception), 'computed relations cannot be final')
+
 
 class BadSchemaTC(TestCase):
     def setUp(self):
@@ -395,6 +480,7 @@
                      ('cw_source', 'Bookmark', 'CWSource', 'object'),
                      ('cw_source', 'CWAttribute', 'CWSource', 'object'),
                      ('cw_source', 'CWCache', 'CWSource', 'object'),
+                     ('cw_source', 'CWComputedRType', 'CWSource', 'object'),
                      ('cw_source', 'CWConstraint', 'CWSource', 'object'),
                      ('cw_source', 'CWConstraintType', 'CWSource', 'object'),
                      ('cw_source', 'CWDataImport', 'CWSource', 'object'),
@@ -454,5 +540,6 @@
                              sorted([(r.rtype.type, r.subject.type, r.object.type, role)
                                      for r, role in sorted(schema[etype].composite_rdef_roles)])
 
+
 if __name__ == '__main__':
     unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_toolsutils.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,57 @@
+# 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/>.
+
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.toolsutils import RQLExecuteMatcher
+
+
+class RQLExecuteMatcherTests(TestCase):
+    def matched_query(self, text):
+        match = RQLExecuteMatcher.match(text)
+        if match is None:
+            return None
+        return match['rql_query']
+
+    def test_unknown_function_dont_match(self):
+        self.assertIsNone(self.matched_query('foo'))
+        self.assertIsNone(self.matched_query('rql('))
+        self.assertIsNone(self.matched_query('hell("")'))
+        self.assertIsNone(self.matched_query('eval("rql(\'bla\''))
+
+    def test_rql_other_parameters_dont_match(self):
+        self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s")'))
+        self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s", {'))
+        self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s")'))
+        self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s", {'))
+
+    def test_rql_function_match(self):
+        for func_expr in ('rql', 'session.execute'):
+            query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+            self.assertEqual(query, 'Any X WHERE X is ')
+
+    def test_offseted_rql_function_match(self):
+        """check indentation is allowed"""
+        for func_expr in ('  rql', '  session.execute'):
+            query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+            self.assertEqual(query, 'Any X WHERE X is ')
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/toolsutils.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/toolsutils.py	Thu Jan 22 17:45:06 2015 +0100
@@ -25,7 +25,12 @@
 import subprocess
 from os import listdir, makedirs, environ, chmod, walk, remove
 from os.path import exists, join, abspath, normpath
-
+import re
+from rlcompleter import Completer
+try:
+    import readline
+except ImportError: # readline not available, no completion
+    pass
 try:
     from os import symlink
 except ImportError:
@@ -263,3 +268,155 @@
         password = getpass('password: ')
     return connect(login=user, password=password, host=optconfig.host, database=appid)
 
+
+## cwshell helpers #############################################################
+
+class AbstractMatcher(object):
+    """Abstract class for CWShellCompleter's matchers.
+
+    A matcher should implement a ``possible_matches`` method. This
+    method has to return the list of possible completions for user's input.
+    Because of the python / readline interaction, each completion should
+    be a superset of the user's input.
+
+    NOTE: readline tokenizes user's input and only passes last token to
+    completers.
+    """
+
+    def possible_matches(self, text):
+        """return possible completions for user's input.
+
+        Parameters:
+            text: the user's input
+
+        Return:
+            a list of completions. Each completion includes the original input.
+        """
+        raise NotImplementedError()
+
+
+class RQLExecuteMatcher(AbstractMatcher):
+    """Custom matcher for rql queries.
+
+    If user's input starts with ``rql(`` or ``session.execute(`` and
+    the corresponding rql query is incomplete, suggest some valid completions.
+    """
+    query_match_rgx = re.compile(
+        r'(?P<func_prefix>\s*(?:rql)'  # match rql, possibly indented
+        r'|'                           # or
+        r'\s*(?:\w+\.execute))'        # match .execute, possibly indented
+        # end of <func_prefix>
+        r'\('                          # followed by a parenthesis
+        r'(?P<quote_delim>["\'])'      # a quote or double quote
+        r'(?P<parameters>.*)')         # and some content
+
+    def __init__(self, local_ctx, req):
+        self.local_ctx = local_ctx
+        self.req = req
+        self.schema = req.vreg.schema
+        self.rsb = req.vreg['components'].select('rql.suggestions', req)
+
+    @staticmethod
+    def match(text):
+        """check if ``text`` looks like a call to ``rql`` or ``session.execute``
+
+        Parameters:
+            text: the user's input
+
+        Returns:
+            None if it doesn't match, the query structure otherwise.
+        """
+        query_match = RQLExecuteMatcher.query_match_rgx.match(text)
+        if query_match is None:
+            return None
+        parameters_text = query_match.group('parameters')
+        quote_delim = query_match.group('quote_delim')
+        # first parameter is fully specified, no completion needed
+        if re.match(r"(.*?)%s" % quote_delim, parameters_text) is not None:
+            return None
+        func_prefix = query_match.group('func_prefix')
+        return {
+            # user's input
+            'text': text,
+            # rql( or session.execute(
+            'func_prefix': func_prefix,
+            # offset of rql query
+            'rql_offset': len(func_prefix) + 2,
+            # incomplete rql query
+            'rql_query': parameters_text,
+            }
+
+    def possible_matches(self, text):
+        """call ``rql.suggestions`` component to complete user's input.
+        """
+        # readline will only send last token, but we need the entire user's input
+        user_input = readline.get_line_buffer()
+        query_struct = self.match(user_input)
+        if query_struct is None:
+            return []
+        else:
+            # we must only send completions of the last token => compute where it
+            # starts relatively to the rql query itself.
+            completion_offset = readline.get_begidx() - query_struct['rql_offset']
+            rql_query = query_struct['rql_query']
+            return [suggestion[completion_offset:]
+                    for suggestion in self.rsb.build_suggestions(rql_query)]
+
+
+class DefaultMatcher(AbstractMatcher):
+    """Default matcher: delegate to standard's `rlcompleter.Completer`` class
+    """
+    def __init__(self, local_ctx):
+        self.completer = Completer(local_ctx)
+
+    def possible_matches(self, text):
+        if "." in text:
+            return self.completer.attr_matches(text)
+        else:
+            return self.completer.global_matches(text)
+
+
+class CWShellCompleter(object):
+    """Custom auto-completion helper for cubicweb-ctl shell.
+
+    ``CWShellCompleter`` provides a ``complete`` method suitable for
+    ``readline.set_completer``.
+
+    Attributes:
+        matchers: the list of ``AbstractMatcher`` instances that will suggest
+                  possible completions
+
+    The completion process is the following:
+
+    - readline calls the ``complete`` method with user's input,
+    - the ``complete`` method asks for each known matchers if
+      it can suggest completions for user's input.
+    """
+
+    def __init__(self, local_ctx):
+        # list of matchers to ask for possible matches on completion
+        self.matchers = [DefaultMatcher(local_ctx)]
+        self.matchers.insert(0, RQLExecuteMatcher(local_ctx, local_ctx['session']))
+
+    def complete(self, text, state):
+        """readline's completer method
+
+        cf http://docs.python.org/2/library/readline.html#readline.set_completer
+        for more details.
+
+        Implementation inspired by `rlcompleter.Completer`
+        """
+        if state == 0:
+            # reset self.matches
+            self.matches = []
+            for matcher in self.matchers:
+                matches = matcher.possible_matches(text)
+                if matches:
+                    self.matches = matches
+                    break
+            else:
+                return None # no matcher able to handle `text`
+        try:
+            return self.matches[state]
+        except IndexError:
+            return None
--- a/uilib.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/uilib.py	Thu Jan 22 17:45:06 2015 +0100
@@ -163,6 +163,8 @@
 
 # text publishing #############################################################
 
+from cubicweb.ext.markdown import markdown_publish # pylint: disable=W0611
+
 try:
     from cubicweb.ext.rest import rest_publish # pylint: disable=W0611
 except ImportError:
@@ -170,6 +172,7 @@
         """default behaviour if docutils was not found"""
         return xml_escape(data)
 
+
 TAG_PROG = re.compile(r'</?.*?>', re.U)
 def remove_html_tags(text):
     """Removes HTML tags from text
--- a/utils.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/utils.py	Thu Jan 22 17:45:06 2015 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 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,7 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Some utilities for CubicWeb server/clients."""
 
-from __future__ import division, with_statement
+from __future__ import division
 
 __docformat__ = "restructuredtext en"
 
@@ -47,6 +47,20 @@
 # initialize random seed from current time
 random.seed()
 
+def admincnx(appid):
+    from cubicweb.cwconfig import CubicWebConfiguration
+    from cubicweb.server.repository import Repository
+    from cubicweb.server.utils import TasksManager
+    config = CubicWebConfiguration.config_for(appid)
+
+    login = config.default_admin_config['login']
+    password = config.default_admin_config['password']
+
+    repo = Repository(config, TasksManager())
+    session = repo.new_session(login, password=password)
+    return session.new_cnx()
+
+
 def make_uid(key=None):
     """Return a unique identifier string.
 
@@ -206,12 +220,23 @@
     specifed in the constructor
     """
 
+    def __init__(self, tracewrites=False, *args, **kwargs):
+        self.tracewrites = tracewrites
+        super(UStringIO, self).__init__(*args, **kwargs)
+
     def __nonzero__(self):
         return True
 
     def write(self, value):
         assert isinstance(value, unicode), u"unicode required not %s : %s"\
                                      % (type(value).__name__, repr(value))
+        if self.tracewrites:
+            from traceback import format_stack
+            stack = format_stack(None)[:-1]
+            escaped_stack = xml_escape(json_dumps(u'\n'.join(stack)))
+            escaped_html = xml_escape(value).replace('\n', '<br/>\n')
+            tpl = u'<span onclick="alert(%s)">%s</span>'
+            value = tpl % (escaped_stack, escaped_html)
         self.append(value)
 
     def getvalue(self):
@@ -234,8 +259,8 @@
     script_opening = u'<script type="text/javascript">\n'
     script_closing = u'\n</script>'
 
-    def __init__(self, req):
-        super(HTMLHead, self).__init__()
+    def __init__(self, req, *args, **kwargs):
+        super(HTMLHead, self).__init__(*args, **kwargs)
         self.jsvars = []
         self.jsfiles = []
         self.cssfiles = []
@@ -399,10 +424,15 @@
                 w(self.script_opening)
                 w(u'\n\n'.join(self.post_inlined_scripts))
                 w(self.script_closing)
-        header = super(HTMLHead, self).getvalue()
-        if skiphead:
-            return header
-        return u'<head>\n%s</head>\n' % header
+        # at the start of this function, the parent UStringIO may already have
+        # data in it, so we can't w(u'<head>\n') at the top. Instead, we create
+        # a temporary UStringIO to get the same debugging output formatting
+        # if debugging is enabled.
+        headtag = UStringIO(tracewrites=self.tracewrites)
+        if not skiphead:
+            headtag.write(u'<head>\n')
+            w(u'</head>\n')
+        return headtag.getvalue() + super(HTMLHead, self).getvalue()
 
 
 class HTMLStream(object):
@@ -416,10 +446,13 @@
     """
 
     def __init__(self, req):
+        self.tracehtml = req.tracehtml
         # stream for <head>
         self.head = req.html_headers
         # main stream
-        self.body = UStringIO()
+        self.body = UStringIO(tracewrites=req.tracehtml)
+        # this method will be assigned to self.w in views
+        self.write = self.body.write
         self.doctype = u''
         self._htmlattrs = [('lang', req.lang)]
         # keep main_stream's reference on req for easier text/html demoting
@@ -445,11 +478,6 @@
             warn('[3.17] xhtml is no more supported',
                  DeprecationWarning, stacklevel=2)
 
-    def write(self, data):
-        """StringIO interface: this method will be assigned to self.w
-        """
-        self.body.write(data)
-
     @property
     def htmltag(self):
         attrs = ' '.join('%s="%s"' % (attr, xml_escape(value))
@@ -460,6 +488,26 @@
 
     def getvalue(self):
         """writes HTML headers, closes </head> tag and writes HTML body"""
+        if self.tracehtml:
+            css = u'\n'.join((u'span {',
+                              u'  font-family: monospace;',
+                              u'  word-break: break-all;',
+                              u'  word-wrap: break-word;',
+                              u'}',
+                              u'span:hover {',
+                              u'  color: red;',
+                              u'  text-decoration: underline;',
+                              u'}'))
+            style = u'<style type="text/css">\n%s\n</style>\n' % css
+            return (u'<!DOCTYPE html>\n'
+                    + u'<html>\n<head>\n%s\n</head>\n' % style
+                    + u'<body>\n'
+                    + u'<span>' + xml_escape(self.doctype) + u'</span><br/>'
+                    + u'<span>' + xml_escape(self.htmltag) + u'</span><br/>'
+                    + self.head.getvalue()
+                    + self.body.getvalue()
+                    + u'<span>' + xml_escape(u'</html>') + u'</span>'
+                    + u'</body>\n</html>')
         return u'%s\n%s\n%s\n%s\n</html>' % (self.doctype,
                                              self.htmltag,
                                              self.head.getvalue(),
--- a/view.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/view.py	Thu Jan 22 17:45:06 2015 +0100
@@ -20,7 +20,6 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-import types, new
 from cStringIO import StringIO
 from warnings import warn
 from functools import partial
@@ -290,12 +289,6 @@
             clabel = vtitle
         return u'%s (%s)' % (clabel, self._cw.property_value('ui.site-title'))
 
-    @deprecated('[3.10] use vreg["etypes"].etype_class(etype).cw_create_url(req)')
-    def create_url(self, etype, **kwargs):
-        """ return the url of the entity creation form for a given entity type"""
-        return self._cw.vreg["etypes"].etype_class(etype).cw_create_url(
-            self._cw, **kwargs)
-
     def field(self, label, value, row=True, show_label=True, w=None, tr=True,
               table=False):
         """read-only field"""
@@ -501,36 +494,10 @@
 class ReloadableMixIn(object):
     """simple mixin for reloadable parts of UI"""
 
-    def user_callback(self, cb, args, msg=None, nonify=False):
-        """register the given user callback and return a URL to call it ready to be
-        inserted in html
-        """
-        self._cw.add_js('cubicweb.ajax.js')
-        if nonify:
-            _cb = cb
-            def cb(*args):
-                _cb(*args)
-        cbname = self._cw.register_onetime_callback(cb, *args)
-        return self.build_js(cbname, xml_escape(msg or ''))
-
-    def build_update_js_call(self, cbname, msg):
-        rql = self.cw_rset.printable_rql()
-        return "javascript: %s" % js.userCallbackThenUpdateUI(
-            cbname, self.__regid__, rql, msg, self.__registry__, self.domid)
-
-    def build_reload_js_call(self, cbname, msg):
-        return "javascript: %s" % js.userCallbackThenReloadPage(cbname, msg)
-
-    build_js = build_update_js_call # expect updatable component by default
-
     @property
     def domid(self):
         return domid(self.__regid__)
 
-    @deprecated('[3.10] use .domid property')
-    def div_id(self):
-        return self.domid
-
 
 class Component(ReloadableMixIn, View):
     """base class for components"""
@@ -548,10 +515,6 @@
     def domid(self):
         return '%sComponent' % domid(self.__regid__)
 
-    @deprecated('[3.10] use .cssclass property')
-    def div_class(self):
-        return self.cssclass
-
 
 class Adapter(AppObject):
     """base class for adapters"""
--- a/web/application.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/application.py	Thu Jan 22 17:45:06 2015 +0100
@@ -23,6 +23,7 @@
 from time import clock, time
 from contextlib import contextmanager
 from warnings import warn
+import json
 
 import httplib
 
@@ -223,7 +224,7 @@
         sessioncookie = self.session_cookie(req)
         secure = req.https and req.base_url().startswith('https://')
         req.set_cookie(sessioncookie, session.sessionid,
-                       maxage=None, secure=secure)
+                       maxage=None, secure=secure, httponly=True)
         if not session.anonymous_session:
             self.session_manager.postlogin(req, session)
         return session
@@ -589,8 +590,10 @@
         status = httplib.INTERNAL_SERVER_ERROR
         if isinstance(ex, PublishException) and ex.status is not None:
             status = ex.status
-        req.status_out = status
-        json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+        if req.status_out < 400:
+            # don't overwrite it if it's already set
+            req.status_out = status
+        json_dumper = getattr(ex, 'dumps', lambda : json.dumps({'reason': unicode(ex)}))
         return json_dumper()
 
     # special case handling
--- a/web/component.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/component.py	Thu Jan 22 17:45:06 2015 +0100
@@ -218,6 +218,10 @@
     def render(self, w):
         w(tags.a(self.label, href=self.href, **self.attrs))
 
+    def __repr__(self):
+        return '<%s: href=%r label=%r %r>' % (self.__class__.__name__,
+                                              self.href, self.label, self.attrs)
+
 
 class Separator(object):
     """a menu separator.
@@ -270,7 +274,7 @@
     layout_id = None # to be defined in concret class
     layout_args = {}
 
-    def layout_render(self, w):
+    def layout_render(self, w, **kwargs):
         getlayout = self._cw.vreg['components'].select
         layout = getlayout(self.layout_id, self._cw, **self.layout_select_args())
         layout.render(w)
@@ -331,19 +335,8 @@
     title = None
     layout_id = 'component_layout'
 
-    # XXX support kwargs for compat with old boxes which gets the view as
-    # argument
     def render(self, w, **kwargs):
-        if hasattr(self, 'call'):
-            warn('[3.10] should not anymore implement call on %s, see new CtxComponent api'
-                 % self.__class__, DeprecationWarning)
-            self.w = w
-            def wview(__vid, rset=None, __fallback_vid=None, **kwargs):
-                self._cw.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
-            self.wview = wview
-            self.call(**kwargs) # pylint: disable=E1101
-            return
-        self.layout_render(w)
+        self.layout_render(w, **kwargs)
 
     def layout_select_args(self):
         args = super(CtxComponent, self).layout_select_args()
@@ -410,19 +403,6 @@
     def separator(self):
         return Separator()
 
-    @deprecated('[3.10] use action_link() / link()')
-    def box_action(self, action): # XXX action_link
-        return self.build_link(self._cw._(action.title), action.url())
-
-    @deprecated('[3.10] use action_link() / link()')
-    def build_link(self, title, url, **kwargs):
-        if self._cw.selected(url):
-            try:
-                kwargs['klass'] += ' selected'
-            except KeyError:
-                kwargs['klass'] = 'selected'
-        return tags.a(title, href=url, **kwargs)
-
 
 class EntityCtxComponent(CtxComponent):
     """base class for boxes related to a single entity"""
@@ -618,27 +598,41 @@
         w(self.rdef.rtype.display_name(self._cw, self.role,
                                        context=self.entity.cw_etype))
 
+    def add_js_css(self):
+        self._cw.add_js(('jquery.ui.js', 'cubicweb.widgets.js'))
+        self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
+        self._cw.add_css('jquery.ui.css')
+        return True
+
     def render_body(self, w):
         req = self._cw
         entity = self.entity
         related = entity.related(self.rtype, self.role)
         if self.role == 'subject':
             mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
-            maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid)
         else:
             mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
-            maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid)
-        if mayadd or maydel:
-            req.add_js(('jquery.ui.js', 'cubicweb.widgets.js'))
-            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
-            req.add_css('jquery.ui.css')
+        js_css_added = False
+        if mayadd:
+            js_css_added = self.add_js_css()
         _ = req._
         if related:
+            maydel = None
             w(u'<table class="ajaxEditRelationTable">')
             for rentity in related.entities():
+                if maydel is None:
+                    # Only check permission for the first related.
+                    if self.role == 'subject':
+                        fromeid, toeid = entity.eid, rentity.eid
+                    else:
+                        fromeid, toeid = rentity.eid, entity.eid
+                    maydel = self.rdef.has_perm(
+                            req, 'delete', fromeid=fromeid, toeid=toeid)
                 # for each related entity, provide a link to remove the relation
                 subview = rentity.view(self.item_vid)
                 if maydel:
+                    if not js_css_added:
+                        js_css_added = self.add_js_css()
                     jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
                         self.__regid__, entity.eid, rentity.eid,
                         self.fname_remove,
@@ -725,7 +719,6 @@
     def entity_call(self, entity, view=None):
         raise NotImplementedError()
 
-
 class RelatedObjectsVComponent(EntityVComponent):
     """a section to display some related entities"""
     __select__ = EntityVComponent.__select__ & partial_has_related_entities()
--- a/web/cors.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/cors.py	Thu Jan 22 17:45:06 2015 +0100
@@ -109,6 +109,6 @@
              '%s != %s' % (host, myhost))
         raise CORSFailed('Host header and hostname do not match')
     # include "Vary: Origin" header (see 6.4)
-    req.set_header('Vary', 'Origin')
+    req.headers_out.addHeader('Vary', 'Origin')
     return origin
 
--- a/web/data/cubicweb.ajax.js	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/cubicweb.ajax.js	Thu Jan 22 17:45:06 2015 +0100
@@ -88,8 +88,8 @@
 });
 
 var AJAX_PREFIX_URL = 'ajax';
-var JSON_BASE_URL = baseuri() + 'json?';
-var AJAX_BASE_URL = baseuri() + AJAX_PREFIX_URL + '?';
+var JSON_BASE_URL = BASE_URL + 'json?';
+var AJAX_BASE_URL = BASE_URL + AJAX_PREFIX_URL + '?';
 
 
 jQuery.extend(cw.ajax, {
@@ -122,9 +122,7 @@
      * (e.g. http://..../data??resource1.js,resource2.js)
      */
     _modconcatLikeUrl: function(url) {
-        var base = baseuri();
-        if (!base.endswith('/')) { base += '/'; }
-        var modconcat_rgx = new RegExp('(' + base + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
+        var modconcat_rgx = new RegExp('(' + BASE_URL + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
         return modconcat_rgx.exec(url);
     },
 
@@ -285,8 +283,6 @@
         setFormsTarget(node);
     }
     _loadDynamicFragments(node);
-    // XXX [3.7] jQuery.one is now used instead jQuery.bind,
-    // jquery.treeview.js can be unpatched accordingly.
     jQuery(cw).trigger('server-response', [true, node]);
     jQuery(node).trigger('server-response', [true, node]);
 }
@@ -379,8 +375,8 @@
  * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
  */
 function loadRemote(url, form, reqtype, sync) {
-    if (!url.toLowerCase().startswith(baseuri().toLowerCase())) {
-        url = baseuri() + url;
+    if (!url.toLowerCase().startswith(BASE_URL.toLowerCase())) {
+        url = BASE_URL + url;
     }
     if (!sync) {
         var deferred = new Deferred();
@@ -609,7 +605,7 @@
                 var fck = new FCKeditor(this.id);
                 fck.Config['CustomConfigurationsPath'] = fckconfigpath;
                 fck.Config['DefaultLanguage'] = fcklang;
-                fck.BasePath = baseuri() + "fckeditor/";
+                fck.BasePath = BASE_URL + "fckeditor/";
                 fck.ReplaceTextarea();
             } else {
                 cw.log('fckeditor could not be found.');
--- a/web/data/cubicweb.css	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/cubicweb.css	Thu Jan 22 17:45:06 2015 +0100
@@ -7,49 +7,52 @@
 /***************************************/
 /* xhtml tags                          */
 /***************************************/
+* {
+  margin: 0px;
+  padding: 0px;
+}
 
-/* scale and rhythm cf http://lamb.cc/typograph/ */
+html, body {
+  background: #e2e2e2;
+}
+
 body {
-  font-family:  %(defaultFontFamily)s;
-  font-size: %(defaultSize)s;
-  line-height: %(defaultLineHeight)s;
-  color: %(defaultColor)s;
+  font-size: 69%;
+  font-weight: normal;
+  font-family: Verdana, sans-serif;
 }
-h1, h2, h3 { margin-top:0; margin-bottom:0; }
 
 h1,
 .vtitle {
   font-size: %(h1FontSize)s;
-  border-bottom: %(h1BorderBottomStyle)s;
-  padding: %(h1Padding)s;
-  margin: %(h1Margin)s;
-  color: %(h1Color)s;
+  margin: 0.2em 0px 0.5em;
+  border-bottom: 1px solid #000;
+}
+
+h2, h3 {
+  margin-top: 0.2em;
+  margin-bottom: 0.5em;
 }
 
 h2 {
   font-size: %(h2FontSize)s;
-  padding: %(h2Padding)s;
 }
 
 h3 {
   font-size: %(h3FontSize)s;
-  padding: %(h3Padding)s;
 }
 
-
 h4 {
   font-size: %(h4FontSize)s;
+  margin: 0.2em 0px;
 }
 
-
-div.tabbedprimary + h1,
-h1.plain {
- border-bottom: none;
+h5 {
+  font-size:110%;
 }
 
-
-html, body {
-  background: %(pageBgColor)s;
+h6{
+  font-size:105%;
 }
 
 /* more specific selectors to override jQueryUI's braindamaged CSS rules */
@@ -62,65 +65,18 @@
   text-decoration: none;
 }
 
-a:hover {
+a:hover{
   text-decoration: underline;
 }
 
-table {
-  border: none;
-}
-
-table th, table td {
-  vertical-align: top;
-}
-
-label, .label {
-  font-weight: bold;
-}
-
-pre {
-  clear: both;
-  font-family: 'Courier New', monospace;
-  letter-spacing: 0.015em;
-  padding: 0.6em;
-  margin: 0 2em 1.7em;
-  background-color: %(listingHighlightedBgColor)s;
-  border: 1px solid %(listingBorderColor)s;
-}
-
-p {
-  text-align: justify;
-  margin-bottom: %(defaultLineHeightEm)s;
+a img{
+  text-align: center;
 }
 
-ul {
-  margin-bottom: %(defaultLineHeightEm)s;
-}
-
-ol {
-  list-style-type: decimal;
- /* margin-bottom: %(defaultLineHeightEm)s; */
-}
-
-ol ol,
-ul ul {
-  margin-left: 8px;
-  margin-bottom : 0px;
-}
-
-/* p + ul { */
-/*   margin-top: -%(defaultLineHeightEm)s; */
-/* } */
-
-li {
-  margin-left: 1.5em;
-}
-
-img {
+img{
   border: none;
 }
 
-
 img.prevnext {
   width: 22px;
   height: 22px;
@@ -133,42 +89,111 @@
   opacity:.25;
 }
 
+p {
+  margin: 0em 0px 0.5em;
+  padding-top: 2px;
+}
+
+table, td, input, select{
+  font-size: 100%;
+}
+
+table {
+  border-collapse: collapse;
+  border: none;
+}
+
+table th, table td {
+  vertical-align: top;
+}
+
+table td img {
+  vertical-align: middle;
+  margin-right: 10px;
+}
+
+ol {
+  margin: 1px 0px 1px 16px;
+}
+
+ul{
+  margin: 1px 0px 1px 4px;
+  list-style-type: none;
+}
+
+ul > li {
+  margin-top: 2px;
+  padding: 0px 0px 2px 8px;
+  background: url("bullet_orange.png") 0% 6px no-repeat;
+}
+
+dt {
+  font-size:1.17em;
+  font-weight:600;
+}
+
+dd {
+  margin: 0.6em 0 1.5em 2em;
+}
+
 fieldset {
   border: none;
 }
 
-h1 a, h1 a:active, h1 a:visited, h1 a:link,
-h2 a, h2 a:active, h2 a:visited, h2 a:link,
-h3 a, h3 a:active, h3 a:visited, h3 a:link {
-  color: inherit;
-  text-decoration: none;
+legend {
+  padding: 0px 2px;
+  font: bold 1em Verdana, sans-serif;
 }
 
 input, textarea {
-  padding: 0.1em 0.2em;
-  vertical-align: bottom;
-  border: 1px solid %(pageContentBorderColor)s;
-
+  padding: 0.2em;
+  vertical-align: middle;
+  border: 1px solid #ccc;
 }
 
 input:focus {
-  border: 1px inset %(headerBgColor)s;
+  border: 1px inset #ff7700;
+}
+
+label, .label {
+  font-weight: bold;
+}
+
+iframe {
+  border: 0px;
 }
 
-hr {
-  border: none;
-  border-bottom: 1px solid %(defaultColor)s;
-  height: 1px;
+pre {
+  font-family: Courier, "Courier New", Monaco, monospace;
+  font-size: 100%;
+  color: #000;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+  margin: 10px 0;
+  padding-bottom: 12px;
+  padding-left: 5px;
+}
+
+code {
+  font-size: 120%;
+  color: #000;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
+}
+
+blockquote {
+  font-family: Courier, "Courier New", serif;
+  font-size: 120%;
+  margin: 5px 0px;
+  padding: 0.8em;
+  background-color: #f2f2f2;
+  border: 1px solid #ccc;
 }
 
 /***************************************/
 /* generic classes                     */
 /***************************************/
 
-h1 a:hover {
- text-decoration: none;
-}
-
 .odd {
   background-color: #f7f6f1;
 }
@@ -178,14 +203,8 @@
 }
 
 .hr {
-  border-bottom: 1px dotted %(pageContentBorderColor)s;
-  height: 17px;
-}
-
-hr.boxSeparator{
-  border: none;
-  border-bottom: 1px solid %(listingBorderColor)s;
-  height: 1px;
+  border-bottom: 1px dotted #ccc;
+  margin: 1em 0px;
 }
 
 .left {
@@ -217,11 +236,30 @@
 }
 
 .caption {
-    font-weight: bold;
+  font-weight: bold;
 }
 
 .legend{
-    font-style: italic;
+  font-style: italic;
+}
+
+/* rest related image classes generated with align: directive */
+
+img.align-right {
+  margin-left: auto;
+  display:block;
+}
+
+img.align-left {
+  margin-right: auto;
+  display:block;
+}
+
+img.align-center{
+  text-align: center;
+  margin-left: auto;
+  margin-right: auto;
+  display:block;
 }
 
 
@@ -232,125 +270,159 @@
 /* header */
 
 table#header {
-  background: %(headerBg)s;
+  background-image: linear-gradient(white, #e2e2e2);
   width: 100%;
+  border-bottom: 1px solid #bbb;
+  text-shadow: 1px 1px 0 #f5f5f5;
 }
 
 table#header td {
   vertical-align: middle;
 }
 
-table#header a {
-  color: %(defaultColor)s;
-}
-
-table#header td#header-right {
-  padding-top: 1em;
-  white-space: nowrap;
-}
-
-table#header img#logo{
-  vertical-align: middle;
+table#header, table#header a {
+  color: #444;
 }
 
 table#header td#headtext {
   white-space: nowrap;
+  padding: 0 10px;
+  width: 10%;
 }
 
+#logo{
+  width: 150px;
+  height: 42px;
+  background-image: url(logo-cubicweb.svg);
+  background-repeat: no-repeat;
+  background-position: center center;
+  background-size: contain;
+  float: left;
+}
+
+table#header td#header-right {
+  white-space: nowrap;
+  width: 10%;
+}
 table#header td#header-center{
- width: 100%;
+  border-bottom-left-radius: 10px;
+  border-top-left-radius: 10px;
+  padding-left: 1em;
 }
 
 span#appliName {
   font-weight: bold;
-  color: %(defaultColor)s;
   white-space: nowrap;
 }
 
+/* FIXME appear with 4px width in IE6 */
+div#stateheader{
+  min-width: 66%;
+}
+
 /* Popup on login box and userActionBox */
+
+.popupWrapper{
+  position:relative;
+}
+
 div.popup {
   position: absolute;
   background: #fff;
-  border: 1px solid %(listingBorderColor)s;
-  border-top: none;
+  border: 1px solid black;
   text-align: left;
   z-index: 400;
 }
 
 div.popup ul li a {
   text-decoration: none;
-  color: #000;
+  color: black;
 }
 
 /* main zone */
 
 div#page {
-  margin: %(defaultLayoutMargin)s;
+  background: #e2e2e2;
+  position: relative;
+  min-height: 800px;
 }
 
-table#mainLayout td#navColumnLeft {
-  width: 16em;
-  padding-right: %(defaultLayoutMargin)s;
-
-}
-
-table#mainLayout td#navColumnRight {
-  width: 16em;
-  padding-left: %(defaultLayoutMargin)s;
+table#mainLayout{
+ padding: 0px 3px;
 }
 
-div#pageContent {
-  clear: both;
-  background: %(pageContentBgColor)s;
-  border: 1px solid %(pageContentBorderColor)s;
-  padding: 0 %(pageContentPadding)s %(pageContentPadding)s;
+table#mainLayout td#contentColumn {
+  padding: 8px 10px 5px;
 }
 
-div#pageContent #contentmain .pagination {
-  margin-top: 0;
+table#mainLayout td#navColumnLeft,
+table#mainLayout td#navColumnRight {
+  width: 150px;
 }
 
-div#pageContent .pagination{
-  margin-top: 1.5em;
-}
-
-div#contentmain{
-  margin-top: %(pageContentPadding)s;
-}
-
-/*FIXME */
 #contentheader {
   margin: 0px;
   padding: 0.2em 0.5em 0.5em 0.5em;
 }
 
 #contentheader a {
-  color: %(defaultColor)s;
+  color: #000;
+}
+
+div#pageContent {
+  clear: both;
+  padding: 10px 1em 2em;
+  background: #ffffff;
+  border-radius: 3px;
+  border: 1px solid #ccc;
 }
 
-/* XXX old boxes, deprecated */
+/* rql bar */
+
+div#rqlinput {
+  border: 1px solid #cfceb7;
+  margin-bottom: 8px;
+  padding: 1px;
+  background: #cfceb7;
+  width: 100%;
+}
+
+input#rql {
+  width: 99%;
+}
+
+input.rqlsubmit{
+  display: block;
+  width: 20px;
+  height: 20px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
+  vertical-align: bottom;
+}
+/* old boxes, deprecated */
 
 div.boxFrame {
   width: 100%;
 }
 
 div.boxTitle {
+  padding-top: 0px;
+  padding-bottom: 0.2em;
   color: #fff;
-  background: %(contextualBoxTitleBgColor)s;
+  background: #ff9900 url("search.png") left bottom repeat-x;
 }
 
 div.boxTitle span,
 div.sideBoxTitle span {
-  padding: 0px 0.5em;
+  padding: 0px 5px;
   white-space: nowrap;
 }
 
 div.sideBoxTitle span {
-  color: %(defaultColor)s;
+  color: #222211;
 }
 
 .boxFrame a {
-  color: %(defaultColor)s;
+  color: #000;
 }
 
 div.boxContent {
@@ -364,36 +436,37 @@
 }
 
 div.sideBoxTitle {
-  background: %(incontextBoxBodyBg)s;
+  background: #cfceb7;
   display: block;
-  font-weight: bold;
-  border-top-left-radius: 6px;
-  border-top-right-radius: 6px;
-}
-
-div.sideBox {
-  margin-bottom: 1em;
+  font: bold 100% Georgia;
   border-top-left-radius: 6px;
   border-top-right-radius: 6px;
 }
 
-ul.sideBox,
-ul.sideBox ul {
-  margin-bottom: 0px;
+#navColumnLeft div.boxTitle {
+  border-top-left-radius: 0px;
+}
+
+div.sideBox {
+  padding: 0 0 0.2em;
+  margin-bottom: 0.5em;
 }
 
 ul.sideBox li {
+ list-style: none;
+ background: none;
  padding: 0px 0px 1px 1px;
- margin: 1px 0 1px 4px;
-}
+ }
 
 div.sideBoxBody {
   padding: 0.2em 5px;
-  background: %(incontextBoxBodyBg)s;
+  background: #eeedd9;
+  border-bottom-left-radius: 6px;
+  border-bottom-right-radius: 6px;
 }
 
 div.sideBoxBody a {
-  color: %(incontextBoxBodyColor)s;
+  color:#555544;
 }
 
 div.sideBoxBody a:hover {
@@ -406,6 +479,10 @@
 
 /* boxes */
 
+div.navboxes {
+  padding-top: 8px;
+}
+
 div.boxTitle {
   overflow: hidden;
   font-weight: bold;
@@ -419,9 +496,11 @@
 }
 
 div.boxBody {
-  padding: 5px;
+  padding: 3px 3px;
   border-top: none;
   background-color: %(leftrightBoxBodyBgColor)s;
+  border-bottom-left-radius: 2px;
+  border-bottom-right-radius: 2px;
 }
 
 div.boxBody a {
@@ -445,6 +524,9 @@
   background: %(contextFreeBoxTitleBg)s;
   color: %(contextFreeBoxTitleColor)s;
 }
+.contextFreeBox div.boxTitle span {
+  text-shadow: 0px 1px 0 #ccc;
+}
 
 .contextualBox div.boxTitle {
   background: %(contextualBoxTitleBg)s;
@@ -477,7 +559,11 @@
   height: 14px;
 }
 
-.boxBody, .boxTitle, #pageContent, #appMsg {
+.navboxes {
+  padding: 0px;
+}
+
+.boxBody, .boxTitle, #appMsg {
     box-shadow: 1px 1px 3px Gray;
 }
 
@@ -494,7 +580,7 @@
 
 ul.boxListing a {
   color: %(defaultColor)s;
-  padding: 1px 9px 1px 3px;
+  padding: 1px 3px;
   display: block; /* necessary to get links across all width available (see on mouse over) */
 }
 
@@ -515,7 +601,7 @@
 
 ul.boxListing ul li {
   margin: 0px;
-  padding-left: 8px;
+  padding-left: 1em;
 }
 
 ul.boxListing ul li a {
@@ -556,6 +642,7 @@
   padding-left: 2em;
 }
 
+
 /* custom boxes */
 
 .search_box div.boxBody {
@@ -563,8 +650,8 @@
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
 }
 
-.bookmarks_box ul.boxListing div a:hover {
-  border-bottom: 1px solid #000;
+.bookmarks_box ul.boxListing div {
+  padding-bottom: 0.3em;
 }
 
 .download_box div.boxTitle {
@@ -573,30 +660,7 @@
 
 .download_box div.boxBody {
   background : #eefed9;
-}
-
-/* search box and rql bar */
-
-div#rqlinput {
-  margin-bottom: %(defaultLayoutMargin)s;
-}
-
-input#rql{
-  padding: 0.25em 0.3em;
-  width: 99%;
-}
-
-input.rqlsubmit{
-  display: block;
-  width: 20px;
-  height: 20px;
-  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
-  vertical-align: bottom;
-}
-
-input.norql{
-  width:155px;
-  margin-right: 2px;
+  vertical-align: center;
 }
 
 /* user actions menu */
@@ -606,12 +670,14 @@
 }
 
 div#userActionsBox {
-  width: 15em;
+  width: 14em;
   text-align: right;
+  display: inline-block;
+  padding-right: 10px;
 }
 
 div#userActionsBox a.popupMenu {
-  color: #000;
+  color: black;
   text-decoration: underline;
   padding-right: 2em;
 }
@@ -621,22 +687,21 @@
 /**************/
 div#etyperestriction {
   margin-bottom: 1ex;
-  border-bottom: 1px solid %(pageContentBorderColor)s;
+  border-bottom: 1px solid #ccc;
 }
 
-/* pagination */
-
 div.pagination{
   margin: 0.5em 0;
 }
+
 span.slice a:visited,
 span.slice a:hover{
-  color: %(helperColor)s;
+  color: #555544;
 }
 
 span.selectedSlice a:visited,
 span.selectedSlice a {
-  color: %(defaultColor)s;
+  background-color: #EBE8D9;
 }
 
 /* FIXME should be moved to cubes/folder */
@@ -651,13 +716,19 @@
 }
 
 div.prevnext a {
-  color: %(defaultColor)s;
+  color: #000;
 }
 
 /***************************************/
 /* entity views                        */
 /***************************************/
 
+.mainInfo  {
+  margin-right: 1em;
+  padding: 0.2em;
+}
+
+
 div.mainRelated {
   border: none;
   margin-right: 1em;
@@ -665,17 +736,18 @@
 }
 
 div.primaryRight{
-  margin-left: %(defaultLayoutMargin)s;
-}
+ }
 
 div.metadata {
   font-size: 90%;
   margin: 5px 0px 3px;
-  color: %(helperColor)s;
+  color: #666;
+  font-style: italic;
   text-align: right;
 }
 
 div.section {
+  margin-top: 0.5em;
   width:100%;
 }
 
@@ -698,7 +770,6 @@
   float: right;
   padding-left: 24px;
   position: relative;
-  z-index: 10;
 }
 div.toolbarButton {
   display: inline;
@@ -710,50 +781,56 @@
 
 .warning,
 .message,
-.errorMessage{
-  padding: 0.2em;
+.errorMessage ,
+.searchMessage{
+  padding: 0.3em 0.3em 0.3em 1em;
   font-weight: bold;
 }
 
-.searchMessage{
- margin-top: %(defaultLayoutMargin)s;
-}
-
 .loginMessage {
   margin: 4px 0px;
   font-weight: bold;
-  color: %(aColor)s;
+  color: #ff7700;
 }
 
-div#appMsg {
-  margin-bottom: %(defaultLayoutMargin)s;
-  border: 1px solid %(incontextBoxTitleBgColor)s;
+div#appMsg, div.appMsg{
+  border: 1px solid #cfceb7;
+  margin-bottom: 8px;
+  padding: 3px;
+  background: #f8f8ee;
 }
 
 .message {
-  background: %(msgBgColor)s %(infoMsgBgImg)s;
+  margin: 0px;
+  background: #f8f8ee url("information.png") 5px center no-repeat;
   padding-left: 15px;
 }
 
 .errorMessage {
   margin: 10px 0px;
   padding-left: 25px;
-  background: %(msgBgColor)s url("critical.png") 2px center no-repeat;
-  color: %(errorMsgColor)s;
-  border: 1px solid %(incontextBoxTitleBgColor)s;
+  background: #f7f6f1 url("critical.png") 2px center no-repeat;
+  color: #ed0d0d;
+  border: 1px solid #cfceb7;
 }
 
-/* search-associate message */
+.searchMessage {
+  margin-top: 0.5em;
+  border-top: 1px solid #cfceb7;
+  background: #eeedd9 url("information.png") 0% 50% no-repeat; /*dcdbc7*/
+}
+
 .stateMessage {
-  border: 1px solid %(pageContentBorderColor)s;
-  background: %(msgBgColor)s %(infoMsgBgImg)s;
-  padding: 0.1em 0 0.1em 20px;
+  border: 1px solid #ccc;
+  background: #f8f8ee url("information.png") 10px 50% no-repeat;
+  padding:4px 0px 4px 20px;
+  border-width: 1px 0px 1px 0px;
 }
 
 /* warning messages like "There are too many results ..." */
 .warning {
   padding-left: 25px;
-  background: %(msgBgColor)s url("critical.png") 3px 50% no-repeat;
+  background: #f2f2f2 url("critical.png") 3px 50% no-repeat;
 }
 
 /* label shown in the top-right hand corner during form validation */
@@ -761,8 +838,8 @@
   position: fixed;
   right: 5px;
   top: 0px;
-  background: %(defaultColor)s;
-  color: #fff;
+  background: #222211;
+  color: white;
   font-weight: bold;
   display: none;
 }
@@ -772,67 +849,69 @@
 /***************************************/
 
 table.listing {
-  width: 100%;
-  font-size: 0.9167em;
-  padding: 10px 0em;
-  color: %(defaultColor)s;
-  border: 1px solid %(listingBorderColor)s;
-  margin-bottom: 1em;
+ padding: 10px 0em;
+ color: #000;
+ width: 100%;
+ border-right: 1px solid #dfdfdf;
 }
 
-table.listing th {
-  font-weight: bold;
-  font-size: 8pt;
-  background: %(listingHeaderBgColor)s;
-  padding: 2px 4px;
-  border: 1px solid %(listingBorderColor)s;
-  border-right:none;
- /* white-space: nowrap; */
-}
 
 table.listing thead th.over {
-  background-color: %(listingHeaderBgColor)s;
+  background-color: #746B6B;
   cursor: pointer;
 }
 
+table.listing tr th {
+  border: 1px solid #dfdfdf;
+  border-right:none;
+  font-size: 8pt;
+  padding: 4px;
+}
+
 table.listing tr .header {
-  border-right: 1px solid %(listingBorderColor)s;
+  border-right: 1px solid #dfdfdf;
   cursor: pointer;
 }
 
 table.listing td {
-  padding: 3px;
+  color: #3D3D3D;
+  padding: 4px;
+  background-color: #FFF;
   vertical-align: top;
-  border: 1px solid %(listingBorderColor)s;
+}
+
+table.listing th,
+table.listing td {
+  padding: 3px 0px 3px 5px;
+  border: 1px solid #dfdfdf;
   border-right: none;
-  background-color: #fff;
+}
+
+table.listing th {
+  font-weight: bold;
+  background: %(listingHeaderBgColor)s;
 }
 
 table.listing td a,
 table.listing td a:visited {
-  color: %(defaultColor)s;
+  color: #666;
 }
 
 table.listing a:hover,
 table.listing tr.highlighted td a {
-  color:%(defaultColor)s;
+  color:#000;
 }
 
 table.listing td.top {
-  border: 1px solid #fff;
+  border: 1px solid white;
   border-bottom: none;
   text-align: right ! important;
-  /* insane IE row bug workraound */
+  /* insane IE row bug workaround */
   position: relative;
   left: -1px;
   top: -1px;
 }
 
-table.listing input,
-table.listing textarea {
- background: %(listingHighlightedBgColor)s;
-}
-
 table.htableForm label, table.oneRowTableForm label {
   vertical-align: middle;
 }
@@ -858,7 +937,6 @@
   margin: 0 0 0 1em ;
 }
 
-
 table.ajaxEditRelationTable{
   margin-bottom: 0.5em;
 }
@@ -882,26 +960,27 @@
   color: #ff0000;
 }
 
+
 /***************************************/
 /* addcombobox                         */
 /***************************************/
 
-input#newopt {
-  display: block;
-  float: left;
-  width: 120px;
-}
+input#newopt{
+ width:120px ;
+ display:block;
+ float:left;
+ }
 
 div#newvalue{
-  margin-top: 2px;
-}
+ margin-top:2px;
+ }
 
-#add_newopt {
-  display: block;
-  float: left;
-  width: 20px;
-  line-height: 20px;
-  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
+#add_newopt{
+ background: #fffff8 url("go.png") 50% 50% no-repeat;
+ width: 20px;
+ line-height: 20px;
+ display:block;
+ float:left;
 }
 
 /***************************************/
@@ -910,8 +989,9 @@
 
 input.button{
   margin: 1em 1em 0px 0px;
-  border: 1px solid %(buttonBorderColor)s;
-  border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s;
+  border: 1px solid #edecd2;
+  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
+  background: #fffff8 url("button.png") bottom left repeat-x;
 }
 
 /* FileItemInnerView  jquery.treeview.css */
@@ -927,40 +1007,6 @@
 }
 
 /***************************************/
-/* lists                               */
-/***************************************/
-
-ul.section,
-ul.startup {
-  margin-bottom: 0px;
-}
-
-ul.startup li,
-ul.section li {
-  margin-left: 0px;
-}
-
-ul.simple li,
-.popupWrapper ul li {
-  background: transparent url("bullet_orange.png") no-repeat 0% 6px;
-}
-
-ul.simple li {
-  padding-left: 8px;
-}
-
-.popupWrapper ul {
-  padding: 0.2em 0.3em;
-  margin-bottom: 0px;
-}
-
-.popupWrapper ul li {
-  padding-left: 8px;
-  margin-left: 0px;
-  white-space: nowrap;
-}
-
-/***************************************/
 /* footer                              */
 /***************************************/
 
@@ -968,10 +1014,11 @@
   text-align: center;
 }
 div#footer a {
-  color: %(defaultColor)s;
+  color: #000;
   text-decoration: none;
 }
 
+
 /****************************************/
 /* FIXME must by managed by cubes       */
 /****************************************/
@@ -980,11 +1027,21 @@
   color: gray;
 }
 
+
+/***************************************/
+/* FIXME : Deprecated ? entity view ?  */
+/***************************************/
+.title {
+  text-align: left;
+  font-size:  large;
+  font-weight: bold;
+}
+
 .validateButton {
   margin: 1em 1em 0px 0px;
-  border: 1px solid %(buttonBorderColor)s;
-  border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s;
-  background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
+  border: 1px solid #edecd2;
+  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
+  background: #fffff8 url("button.png") bottom left repeat-x;
 }
 
 /********************************/
@@ -995,26 +1052,6 @@
   float: right;
 }
 
-/********************************/
-/* rest related classes         */
-/********************************/
-
-img.align-right {
-  margin-left: auto;
-  display:block;
-}
-
-img.align-left {
-  margin-right: auto;
-  display:block;
-}
-
-img.align-center{
-  text-align: center;
-  margin-left: auto;
-  margin-right: auto;
-  display:block;
-}
 
 /******************************/
 /* reledit                    */
@@ -1037,8 +1074,6 @@
   background-image: none;
 }
 
-/* jquery-ui tabs */
-
 div.ui-tabs.ui-widget-content {
   background:none;
   border:none;
@@ -1052,14 +1087,15 @@
 div.ui-tabs ul.ui-tabs-nav a {
   color:#27537A;
   padding: 0.3em 0.6em;
+  outline:0;
 }
 
 div.ui-tabs ul.ui-tabs-nav li.ui-tabs-selected a {
   color:black;
 }
 
-div.ui-tabs ul.ui-tabs-nav li.ui-state-hover {
-  background:none;
+div.ui-tabs ul.ui-tabs-nav li.ui-state-hover, div.ui-tabs ul.ui-tabs-nav li.ui-state-focus {
+  background:white;
 }
 
 div.ui-tabs .ui-widget-header {
@@ -1074,19 +1110,10 @@
 div.ui-tabs .ui-tabs-panel {
   border-top:1px solid #97A5B0;
   padding-left:0.5em;
+  padding-right: 2px;
   color:inherit;
 }
 
-div.ui-tabs .ui-tabs-nav, div.ui-tabs .ui-tabs-panel {
-  font-family: %(defaultFontFamily)s;
-  font-size: %(defaultSize)s;
-}
-
-img.ui-datepicker-trigger {
-  margin-left: 0.5em;
-  vertical-align: bottom;
-}
-
 /* cubicweb.views.undohistory uses :
  *   - span.undo around undo link
  *   - ul.undo-transactions to list transaction
--- a/web/data/cubicweb.edition.js	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/cubicweb.edition.js	Thu Jan 22 17:45:06 2015 +0100
@@ -67,7 +67,7 @@
                 rql: rql_for_eid(eid),
                 '__notemplate': 1
             };
-            var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
+            var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(BASE_URL + 'view', args, 'post', 'append');
             d.addCallback(function() {
                 _showMatchingSelect(eid, jQuery('#' + divId));
             });
--- a/web/data/cubicweb.facets.js	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/cubicweb.facets.js	Thu Jan 22 17:45:06 2015 +0100
@@ -69,7 +69,7 @@
         }
         var $focusLink = $('#focusLink');
         if ($focusLink.length) {
-            var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql);
+            var url = BASE_URL + 'view?rql=' + encodeURIComponent(rql);
             if (vid) {
                 url += '&vid=' + encodeURIComponent(vid);
             }
--- a/web/data/cubicweb.htmlhelpers.js	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/cubicweb.htmlhelpers.js	Thu Jan 22 17:45:06 2015 +0100
@@ -12,20 +12,13 @@
 /**
  * .. function:: baseuri()
  *
- * returns the document's baseURI. (baseuri() uses document.baseURI if
- * available and inspects the <base> tag manually otherwise.)
+ * returns the document's baseURI.
  */
-function baseuri() {
-    if (typeof BASE_URL === 'undefined') {
-        // backward compatibility, BASE_URL might be undefined
-        var uri = document.baseURI;
-        if (uri) { // some browsers don't define baseURI
-            return uri.toLowerCase();
-        }
-        return jQuery('base').attr('href').toLowerCase();
-    }
-    return BASE_URL;
-}
+baseuri = cw.utils.deprecatedFunction(
+    "[3.20] baseuri() is deprecated, use BASE_URL instead",
+    function () {
+        return BASE_URL;
+    });
 
 /**
  * .. function:: setProgressCursor()
@@ -107,18 +100,6 @@
 }
 
 /**
- * .. function:: popupLoginBox()
- *
- * toggles visibility of login popup div
- */
-// XXX used exactly ONCE in basecomponents
-popupLoginBox = cw.utils.deprecatedFunction(
-    function() {
-        $('#popupLoginBox').toggleClass('hidden');
-        jQuery('#__login:visible').focus();
-});
-
-/**
  * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
  *
  * returns the list of elements in the document matching the tag name
--- a/web/data/cubicweb.js	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/cubicweb.js	Thu Jan 22 17:45:06 2015 +0100
@@ -208,91 +208,40 @@
     },
 
     /**
-     * .. function:: formContents(elem \/* = document.body *\/)
+     * .. function:: formContents(elem)
      *
-     * this implementation comes from MochiKit
+     * cannot use jQuery.serializeArray() directly because of FCKeditor
      */
-    formContents: function (elem /* = document.body */ ) {
-        var names = [];
-        var values = [];
-        if (typeof(elem) == "undefined" || elem === null) {
-            elem = document.body;
-        } else {
-            elem = cw.getNode(elem);
-        }
-        cw.utils.nodeWalkDepthFirst(elem, function (elem) {
-            var name = elem.name;
-            if (name && name.length) {
-                if (elem.disabled) {
-                    return null;
-                }
-                var tagName = elem.tagName.toUpperCase();
-                if (tagName === "INPUT" && (elem.type == "radio" || elem.type == "checkbox") && !elem.checked) {
-                    return null;
-                }
-                if (tagName === "SELECT") {
-                    if (elem.type == "select-one") {
-                        if (elem.selectedIndex >= 0) {
-                            var opt = elem.options[elem.selectedIndex];
-                            var v = opt.value;
-                            if (!v) {
-                                var h = opt.outerHTML;
-                                // internet explorer sure does suck.
-                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                    v = opt.text;
-                                }
-                            }
-                            names.push(name);
-                            values.push(v);
+    formContents: function (elem) {
+        var $elem, array, names, values;
+        $elem = cw.jqNode(elem);
+        array = $elem.serializeArray();
+
+        if (typeof FCKeditor !== 'undefined') {
+            $elem.find('textarea').each(function (idx, textarea) {
+                var fck = FCKeditorAPI.GetInstance(textarea.id);
+                if (fck) {
+                    array = jQuery.map(array, function (dict) {
+                        if (dict.name === textarea.name) {
+                            // filter out the textarea's - likely empty - value ...
                             return null;
                         }
-                        // no form elements?
-                        names.push(name);
-                        values.push("");
-                        return null;
-                    } else {
-                        var opts = elem.options;
-                        if (!opts.length) {
-                            names.push(name);
-                            values.push("");
-                            return null;
-                        }
-                        for (var i = 0; i < opts.length; i++) {
-                            var opt = opts[i];
-                            if (!opt.selected) {
-                                continue;
-                            }
-                            var v = opt.value;
-                            if (!v) {
-                                var h = opt.outerHTML;
-                                // internet explorer sure does suck.
-                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                    v = opt.text;
-                                }
-                            }
-                            names.push(name);
-                            values.push(v);
-                        }
-                        return null;
-                    }
+                        return dict;
+                    });
+                    // ... so we can put the HTML coming from FCKeditor instead.
+                    array.push({
+                        name: textarea.name,
+                        value: fck.GetHTML()
+                    });
                 }
-                if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" || tagName === "DIV") {
-                    return elem.childNodes;
-                }
-		var value = elem.value;
-		if (tagName === "TEXTAREA") {
-		    if (typeof(FCKeditor) != 'undefined') {
-			var fck = FCKeditorAPI.GetInstance(elem.id);
-			if (fck) {
-			    value = fck.GetHTML();
-			}
-		    }
-		}
-                names.push(name);
-                values.push(value || '');
-                return null;
-            }
-            return elem.childNodes;
+            });
+        }
+
+        names = [];
+        values = [];
+        jQuery.each(array, function (idx, dict) {
+            names.push(dict.name);
+            values.push(dict.value);
         });
         return [names, values];
     },
--- a/web/data/cubicweb.old.css	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1123 +0,0 @@
-/*
- *  :organization: Logilab
- *  :copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
- *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
- */
-
-/***************************************/
-/* xhtml tags                          */
-/***************************************/
-* {
-  margin: 0px;
-  padding: 0px;
-}
-
-html, body {
-  background: #e2e2e2;
-}
-
-body {
-  font-size: 69%;
-  font-weight: normal;
-  font-family: Verdana, sans-serif;
-}
-
-h1,
-.vtitle {
-  font-size: %(h1FontSize)s;
-  margin: 0.2em 0px 0.3em;
-  border-bottom: 1px solid #000;
-}
-
-h2, h3 {
-  margin-top: 0.2em;
-  margin-bottom: 0.3em;
-}
-
-h2 {
-  font-size: %(h2FontSize)s;
-}
-
-h3 {
-  font-size: %(h3FontSize)s;
-}
-
-h4 {
-  font-size: %(h4FontSize)s;
-  margin: 0.2em 0px;
-}
-
-h5 {
-  font-size:110%;
-}
-
-h6{
-  font-size:105%;
-}
-
-/* more specific selectors to override jQueryUI's braindamaged CSS rules */
-#pageContent .ui-tabs-panel a,
-#pageContent .ui-tabs-panel a:active,
-#pageContent .ui-tabs-panel a:visited,
-#pageContent .ui-tabs-panel a:link,
-a, a:active, a:visited, a:link {
-  color: %(aColor)s;
-  text-decoration: none;
-}
-
-a:hover{
-  text-decoration: underline;
-}
-
-a img{
-  text-align: center;
-}
-
-img{
-  border: none;
-}
-
-img.prevnext {
-  width: 22px;
-  height: 22px;
-}
-
-img.prevnext_nogo {
-  width: 22px;
-  height: 22px;
-  filter:alpha(opacity=25); /* IE */
-  opacity:.25;
-}
-
-p {
-  margin: 0em 0px 0.2em;
-  padding-top: 2px;
-}
-
-table, td, input, select{
-  font-size: 100%;
-}
-
-table {
-  border-collapse: collapse;
-  border: none;
-}
-
-table th, table td {
-  vertical-align: top;
-}
-
-table td img {
-  vertical-align: middle;
-  margin-right: 10px;
-}
-
-ol {
-  margin: 1px 0px 1px 16px;
-}
-
-ul{
-  margin: 1px 0px 1px 4px;
-  list-style-type: none;
-}
-
-ul > li {
-  margin-top: 2px;
-  padding: 0px 0px 2px 8px;
-  background: url("bullet_orange.png") 0% 6px no-repeat;
-}
-
-dt {
-  font-size:1.17em;
-  font-weight:600;
-}
-
-dd {
-  margin: 0.6em 0 1.5em 2em;
-}
-
-fieldset {
-  border: none;
-}
-
-legend {
-  padding: 0px 2px;
-  font: bold 1em Verdana, sans-serif;
-}
-
-input, textarea {
-  padding: 0.2em;
-  vertical-align: middle;
-  border: 1px solid #ccc;
-}
-
-input:focus {
-  border: 1px inset #ff7700;
-}
-
-label, .label {
-  font-weight: bold;
-}
-
-iframe {
-  border: 0px;
-}
-
-pre {
-  font-family: Courier, "Courier New", Monaco, monospace;
-  font-size: 100%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
-  margin: 10px 0;
-  padding-bottom: 12px;
-  padding-left: 5px;
-}
-
-code {
-  font-size: 120%;
-  color: #000;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
-}
-
-blockquote {
-  font-family: Courier, "Courier New", serif;
-  font-size: 120%;
-  margin: 5px 0px;
-  padding: 0.8em;
-  background-color: #f2f2f2;
-  border: 1px solid #ccc;
-}
-
-/***************************************/
-/* generic classes                     */
-/***************************************/
-
-.odd {
-  background-color: #f7f6f1;
-}
-
-.even {
-  background-color: transparent;
-}
-
-.hr {
-  border-bottom: 1px dotted #ccc;
-  margin: 1em 0px;
-}
-
-.left {
-  float: left;
-}
-
-.right {
-  float: right;
-}
-
-.clear {
-  clear: both;
-}
-
-.hidden {
-  display: none;
-  visibility: hidden;
-}
-
-/* copied verbatim from bootstrap 3.0 */
-.invisible {
-  visibility: hidden;
-}
-
-/* copied verbatim from bootstrap 3.0 */
-.list-unstyled {
-  padding-left: 0;
-  list-style: none;
-}
-
-.caption {
-  font-weight: bold;
-}
-
-.legend{
-  font-style: italic;
-}
-
-/* rest related image classes generated with align: directive */
-
-img.align-right {
-  margin-left: auto;
-  display:block;
-}
-
-img.align-left {
-  margin-right: auto;
-  display:block;
-}
-
-img.align-center{
-  text-align: center;
-  margin-left: auto;
-  margin-right: auto;
-  display:block;
-}
-
-
-/***************************************/
-/*   LAYOUT                            */
-/***************************************/
-
-/* header */
-
-table#header {
-  background-image: linear-gradient(white, #e2e2e2);
-  width: 100%;
-  border-bottom: 1px solid #bbb;
-  text-shadow: 1px 1px 0 #f5f5f5;
-}
-
-table#header td {
-  vertical-align: middle;
-}
-
-table#header, table#header a {
-  color: #444;
-}
-
-table#header td#headtext {
-  white-space: nowrap;
-  padding: 0 10px;
-  width: 10%;
-}
-
-#logo{
-  width: 150px;
-  height: 42px;
-  background-image: url(logo-cubicweb.svg);
-  background-repeat: no-repeat;
-  background-position: center center;
-  background-size: contain;
-  float: left;
-}
-
-table#header td#header-right {
-  white-space: nowrap;
-  width: 10%;
-}
-table#header td#header-center{
-  border-bottom-left-radius: 10px;
-  border-top-left-radius: 10px;
-  padding-left: 1em;
-}
-
-span#appliName {
-  font-weight: bold;
-  white-space: nowrap;
-}
-
-/* FIXME appear with 4px width in IE6 */
-div#stateheader{
-  min-width: 66%;
-}
-
-/* Popup on login box and userActionBox */
-
-.popupWrapper{
-  position:relative;
-}
-
-div.popup {
-  position: absolute;
-  background: #fff;
-  border: 1px solid black;
-  text-align: left;
-  z-index: 400;
-}
-
-div.popup ul li a {
-  text-decoration: none;
-  color: black;
-}
-
-/* main zone */
-
-div#page {
-  background: #e2e2e2;
-  position: relative;
-  min-height: 800px;
-}
-
-table#mainLayout{
- padding: 0px 3px;
-}
-
-table#mainLayout td#contentColumn {
-  padding: 8px 10px 5px;
-}
-
-table#mainLayout td#navColumnLeft,
-table#mainLayout td#navColumnRight {
-  width: 16em;
-}
-
-#contentheader {
-  margin: 0px;
-  padding: 0.2em 0.5em 0.5em 0.5em;
-}
-
-#contentheader a {
-  color: #000;
-}
-
-div#pageContent {
-  clear: both;
-  padding: 10px 1em 2em;
-  background: #ffffff;
-  border: 1px solid #ccc;
-}
-
-/* rql bar */
-
-div#rqlinput {
-  border: 1px solid #cfceb7;
-  margin-bottom: 8px;
-  padding: 1px;
-  background: #cfceb7;
-  width: 100%;
-}
-
-input#rql {
-  width: 99%;
-}
-
-input.rqlsubmit{
-  display: block;
-  width: 20px;
-  height: 20px;
-  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
-  vertical-align: bottom;
-}
-/* old boxes, deprecated */
-
-div.boxFrame {
-  width: 100%;
-}
-
-div.boxTitle {
-  padding-top: 0px;
-  padding-bottom: 0.2em;
-  font: bold 100% Georgia;
-  color: #fff;
-  background: #ff9900 url("search.png") left bottom repeat-x;
-}
-
-div.boxTitle span,
-div.sideBoxTitle span {
-  padding: 0px 5px;
-  white-space: nowrap;
-}
-
-div.sideBoxTitle span {
-  color: #222211;
-}
-
-.boxFrame a {
-  color: #000;
-}
-
-div.boxContent {
-  padding: 3px 0px;
-  background: #fff;
-  border-top: none;
-}
-
-div.shadow{
-  height: 14px;
-}
-
-div.sideBoxTitle {
-  background: #cfceb7;
-  display: block;
-  font: bold 100% Georgia;
-  border-top-left-radius: 6px;
-  border-top-right-radius: 6px;
-}
-
-div.sideBox {
-  padding: 0 0 0.2em;
-  margin-bottom: 0.5em;
-}
-
-ul.sideBox li {
- list-style: none;
- background: none;
- padding: 0px 0px 1px 1px;
- }
-
-div.sideBoxBody {
-  padding: 0.2em 5px;
-  background: #eeedd9;
-  border-bottom-left-radius: 6px;
-  border-bottom-right-radius: 6px;
-}
-
-div.sideBoxBody a {
-  color:#555544;
-}
-
-div.sideBoxBody a:hover {
-  text-decoration: underline;
-}
-
-div.sideBox table td {
-  padding-right: 1em;
-}
-
-/* boxes */
-
-div.navboxes {
-  padding-top: 0.5em;
-}
-
-div.boxTitle {
-  overflow: hidden;
-  font-weight: bold;
-  border-top-left-radius: 6px;
-  border-top-right-radius: 6px;
-}
-
-div.boxTitle span {
-  padding: 0px 0.5em;
-  white-space: nowrap;
-}
-
-div.boxBody {
-  padding: 3px 3px;
-  border-top: none;
-  background-color: %(leftrightBoxBodyBgColor)s;
-}
-
-div.boxBody a {
-  color: %(leftrightBoxBodyColor)s;
-}
-
-div.boxBody a:hover {
-  text-decoration: none;
-  cursor: pointer;
-  background-color: %(leftrightBoxBodyHoverBgColor)s;
-}
-
-hr.boxSeparator {
-  margin-top: 0.5em;
-  margin-bottom: 0.5em;
-}
-
-/* boxes contextual customization */
-
-.contextFreeBox div.boxTitle {
-  background: %(contextFreeBoxTitleBg)s;
-  color: %(contextFreeBoxTitleColor)s;
-}
-
-.contextualBox div.boxTitle {
-  background: %(contextualBoxTitleBg)s;
-  color: %(contextualBoxTitleColor)s;
-}
-
-.primaryRight div.boxTitle {
-  background: %(incontextBoxTitleBg)s;
-  color: %(incontextBoxTitleColor)s;
-}
-
-.primaryRight div.boxBody {
-  padding: 0.2em 5px;
-  background: %(incontextBoxBodyBgColor)s;
-}
-
-.primaryRight div.boxBody a {
-  color: %(incontextBoxBodyColor)s;
-}
-
-.primaryRight div.boxBody a:hover {
-  background-color: %(incontextBoxBodyHoverBgColor)s;
-}
-
-.primaryRight div.boxFooter {
-  margin-bottom: 1em;
-}
-
-#navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{
-  height: 14px;
-}
-
-.navboxes {
-  padding: 2px;
-}
-
-.boxBody, .boxTitle, #pageContent, #appMsg {
-    box-shadow: 1px 1px 3px Gray;
-}
-
-/* boxes lists and menus */
-
-ul.boxListing {
-  margin: 0;
-  padding: 0;
-}
-
-ul.boxListing ul {
-  padding: 1px 3px;
-}
-
-ul.boxListing a {
-  color: %(defaultColor)s;
-  padding: 1px 3px;
-  display: block; /* necessary to get links across all width available (see on mouse over) */
-}
-
-ul.boxListing a.action {
-  padding: 0;
-  display: inline;
-}
-
-ul.boxListing a.action + a{
-  display: inline;
-}
-
-ul.boxListing li {
-  margin: 0px;
-  padding: 0px;
-  background-image: none;
-}
-
-ul.boxListing ul li {
-  margin: 0px;
-  padding-left: 1em;
-}
-
-ul.boxListing ul li a {
-  padding-left: 10px;
-  background-image: url("bullet_orange.png");
-  background-repeat: no-repeat;
-  background-position: 0 6px;
-}
-
-ul.boxListing .selected {
-  color: %(aColor)s;
-  font-weight: bold;
-}
-
-ul.boxListing a.boxMenu:hover {
-  border-top: medium none;
-  background: %(leftrightBoxBodyHoverBgColor)s;
-}
-
-a.boxMenu,
-ul.boxListing a.boxMenu {
-  display: block;
-  padding: 1px 3px;
-  background: transparent %(bulletDownImg)s;
-}
-
-ul.boxListing a.boxMenu:hover {
-  border-top: medium none;
-  background: %(leftrightBoxBodyHoverBgColor)s %(bulletDownImg)s;
-}
-
-a.boxMenu:hover {
-  cursor: pointer;
-}
-
-a.popupMenu {
-  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
-  padding-left: 2em;
-}
-
-
-/* custom boxes */
-
-.search_box div.boxBody {
-  padding: 4px 4px 3px;
-  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
-}
-
-.bookmarks_box ul.boxListing div {
-  padding-bottom: 0.3em;
-}
-
-.download_box div.boxTitle {
-  background : #8fbc8f !important;
-}
-
-.download_box div.boxBody {
-  background : #eefed9;
-  vertical-align: center;
-}
-
-/* user actions menu */
-a.logout, a.logout:visited, a.logout:hover{
-  color: #fff;
-  text-decoration: none;
-}
-
-div#userActionsBox {
-  width: 14em;
-  text-align: right;
-  display: inline-block;
-  padding-right: 10px;
-}
-
-div#userActionsBox a.popupMenu {
-  color: black;
-  text-decoration: underline;
-  padding-right: 2em;
-}
-
-/**************/
-/* navigation */
-/**************/
-div#etyperestriction {
-  margin-bottom: 1ex;
-  border-bottom: 1px solid #ccc;
-}
-
-div.pagination{
-  margin: 0.5em 0;
-}
-
-span.slice a:visited,
-span.slice a:hover{
-  color: #555544;
-}
-
-span.selectedSlice a:visited,
-span.selectedSlice a {
-  background-color: #EBE8D9;
-}
-
-/* FIXME should be moved to cubes/folder */
-div.navigation a {
-  text-align: center;
-  text-decoration: none;
-}
-
-div.prevnext {
-  width: 100%;
-  margin-bottom: 1em;
-}
-
-div.prevnext a {
-  color: #000;
-}
-
-/***************************************/
-/* entity views                        */
-/***************************************/
-
-.mainInfo  {
-  margin-right: 1em;
-  padding: 0.2em;
-}
-
-
-div.mainRelated {
-  border: none;
-  margin-right: 1em;
-  padding: 0.5em 0.2em 0.2em;
-}
-
-div.primaryRight{
- }
-
-div.metadata {
-  font-size: 90%;
-  margin: 5px 0px 3px;
-  color: #666;
-  font-style: italic;
-  text-align: right;
-}
-
-div.section {
-  margin-top: 0.5em;
-  width:100%;
-}
-
-div.section a:hover {
-  text-decoration: none;
-}
-
-/* basic entity view */
-
-tr.entityfield th {
-  text-align: left;
-  padding-right: 0.5em;
-}
-
-div.field {
-  display: inline;
-}
-
-div.ctxtoolbar {
-  float: right;
-  padding-left: 24px;
-  position: relative;
-}
-div.toolbarButton {
-  display: inline;
-}
-
-/***************************************/
-/* messages                            */
-/***************************************/
-
-.warning,
-.message,
-.errorMessage ,
-.searchMessage{
-  padding: 0.3em 0.3em 0.3em 1em;
-  font-weight: bold;
-}
-
-.loginMessage {
-  margin: 4px 0px;
-  font-weight: bold;
-  color: #ff7700;
-}
-
-div#appMsg, div.appMsg{
-  border: 1px solid #cfceb7;
-  margin-bottom: 8px;
-  padding: 3px;
-  background: #f8f8ee;
-}
-
-.message {
-  margin: 0px;
-  background: #f8f8ee url("information.png") 5px center no-repeat;
-  padding-left: 15px;
-}
-
-.errorMessage {
-  margin: 10px 0px;
-  padding-left: 25px;
-  background: #f7f6f1 url("critical.png") 2px center no-repeat;
-  color: #ed0d0d;
-  border: 1px solid #cfceb7;
-}
-
-.searchMessage {
-  margin-top: 0.5em;
-  border-top: 1px solid #cfceb7;
-  background: #eeedd9 url("information.png") 0% 50% no-repeat; /*dcdbc7*/
-}
-
-.stateMessage {
-  border: 1px solid #ccc;
-  background: #f8f8ee url("information.png") 10px 50% no-repeat;
-  padding:4px 0px 4px 20px;
-  border-width: 1px 0px 1px 0px;
-}
-
-/* warning messages like "There are too many results ..." */
-.warning {
-  padding-left: 25px;
-  background: #f2f2f2 url("critical.png") 3px 50% no-repeat;
-}
-
-/* label shown in the top-right hand corner during form validation */
-div#progress {
-  position: fixed;
-  right: 5px;
-  top: 0px;
-  background: #222211;
-  color: white;
-  font-weight: bold;
-  display: none;
-}
-
-/***************************************/
-/* listing table                       */
-/***************************************/
-
-table.listing {
- padding: 10px 0em;
- color: #000;
- width: 100%;
- border-right: 1px solid #dfdfdf;
-}
-
-
-table.listing thead th.over {
-  background-color: #746B6B;
-  cursor: pointer;
-}
-
-table.listing tr th {
-  border: 1px solid #dfdfdf;
-  border-right:none;
-  font-size: 8pt;
-  padding: 4px;
-}
-
-table.listing tr .header {
-  border-right: 1px solid #dfdfdf;
-  cursor: pointer;
-}
-
-table.listing td {
-  color: #3D3D3D;
-  padding: 4px;
-  background-color: #FFF;
-  vertical-align: top;
-}
-
-table.listing th,
-table.listing td {
-  padding: 3px 0px 3px 5px;
-  border: 1px solid #dfdfdf;
-  border-right: none;
-}
-
-table.listing th {
-  font-weight: bold;
-  background: %(listingHeaderBgColor)s;
-}
-
-table.listing td a,
-table.listing td a:visited {
-  color: #666;
-}
-
-table.listing a:hover,
-table.listing tr.highlighted td a {
-  color:#000;
-}
-
-table.listing td.top {
-  border: 1px solid white;
-  border-bottom: none;
-  text-align: right ! important;
-  /* insane IE row bug workaround */
-  position: relative;
-  left: -1px;
-  top: -1px;
-}
-
-table.htableForm label, table.oneRowTableForm label {
-  vertical-align: middle;
-}
-table.htableForm td {
-  padding-left: 1em;
-  padding-top: 0.5em;
-}
-table.htableForm th {
-  padding-left: 1em;
-}
-table.htableForm .validateButton {
-  margin-right: 0.2em;
-  margin-bottom: 0.2em;
-}
-
-table.oneRowTableForm td {
-  padding-left: 0.5em;
-}
-table.oneRowTableForm th {
-  padding-left: 1em;
-}
-table.oneRowTableForm .validateButton {
-  margin: 0 0 0 1em ;
-}
-
-table.ajaxEditRelationTable{
-  margin-bottom: 0.5em;
-}
-table.ajaxEditRelationTable td.entity{
-  padding-left: 0.5em;
-}
-
-/***************************************/
-/* error view (views/management.py)    */
-/***************************************/
-
-div.pycontext { /* html traceback */
-  font-family: Verdana, sans-serif;
-  font-size: 80%;
-  padding: 1em;
-  margin: 10px 0px 5px 20px;
-  background-color: #dee7ec;
-}
-
-div.pycontext span.name {
-  color: #ff0000;
-}
-
-
-/***************************************/
-/* addcombobox                         */
-/***************************************/
-
-input#newopt{
- width:120px ;
- display:block;
- float:left;
- }
-
-div#newvalue{
- margin-top:2px;
- }
-
-#add_newopt{
- background: #fffff8 url("go.png") 50% 50% no-repeat;
- width: 20px;
- line-height: 20px;
- display:block;
- float:left;
-}
-
-/***************************************/
-/* buttons                             */
-/***************************************/
-
-input.button{
-  margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
-}
-
-/* FileItemInnerView  jquery.treeview.css */
-.folder {
-  /* disable odd/even under folder class */
-  background-color: transparent;
-}
-
-a.addButton {
-  margin-left: 0.5em;
-  padding-left: 16px;
-  background: transparent url("add_button.png") 0% 50% no-repeat;
-}
-
-/***************************************/
-/* footer                              */
-/***************************************/
-
-div#footer {
-  text-align: center;
-}
-div#footer a {
-  color: #000;
-  text-decoration: none;
-}
-
-
-/****************************************/
-/* FIXME must by managed by cubes       */
-/****************************************/
-.needsvalidation {
-  font-style: italic;
-  color: gray;
-}
-
-
-/***************************************/
-/* FIXME : Deprecated ? entity view ?  */
-/***************************************/
-.title {
-  text-align: left;
-  font-size:  large;
-  font-weight: bold;
-}
-
-.validateButton {
-  margin: 1em 1em 0px 0px;
-  border: 1px solid #edecd2;
-  border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
-  background: #fffff8 url("button.png") bottom left repeat-x;
-}
-
-/********************************/
-/* placement of alt. view icons */
-/********************************/
-
-.otherView {
-  float: right;
-}
-
-
-/******************************/
-/* reledit                    */
-/******************************/
-
-.releditField {
-    display: inline;
-}
-
-.releditForm {
- display:none;
-}
-
-/********************************/
-/* overwite other css here      */
-/********************************/
-
-.ui-menu li.ui-menu-item {
-  /* remove background image (orange bullet) for autocomplete suggestions */
-  background-image: none;
-}
-
-div.ui-tabs.ui-widget-content {
-  background:none;
-  border:none;
-  color:inherit;
-}
-
-div.ui-tabs ul.ui-tabs-nav {
-  padding-left: 0.5em;
-}
-
-div.ui-tabs ul.ui-tabs-nav a {
-  color:#27537A;
-  padding: 0.3em 0.6em;
-  outline:0;
-}
-
-div.ui-tabs ul.ui-tabs-nav li.ui-tabs-selected a {
-  color:black;
-}
-
-div.ui-tabs ul.ui-tabs-nav li.ui-state-hover, div.ui-tabs ul.ui-tabs-nav li.ui-state-focus {
-  background:white;
-}
-
-div.ui-tabs .ui-widget-header {
-  background:none;
-  border:none;
-}
-
-div.ui-tabs .ui-widget-header li {
-  border-color:#333333;
-}
-
-div.ui-tabs .ui-tabs-panel {
-  border-top:1px solid #97A5B0;
-  padding-left:0.5em;
-  color:inherit;
-}
-
-/* cubicweb.views.undohistory uses :
- *   - span.undo around undo link
- *   - ul.undo-transactions to list transaction
- *   - ol.undo-actions to list actions in a transaction
- */
-
-span.undo {
-  border: 1pt;
-}
-
-ol.undo-actions > li {
-  margin-left: 2em;
-  margin-top: 2px;
-  padding: 0px 0px 2px 0px;
-  background-image: none;
-}
-
--- a/web/data/cubicweb.reset.css	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/* http://meyerweb.com/eric/tools/css/reset/ 
-   v2.0 | 20110126
-   License: none (public domain)
-*/
-
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed, 
-figure, figcaption, footer, header, hgroup, 
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
-	margin: 0;
-	padding: 0;
-	border: 0;
-	font-size: 100%;
-	font: inherit;
-	vertical-align: baseline;
-}
-/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure, 
-footer, header, hgroup, menu, nav, section {
-	display: block;
-}
-body {
-	line-height: 1;
-}
-ol, ul {
-	list-style: none;
-}
-blockquote, q {
-	quotes: none;
-}
-blockquote:before, blockquote:after,
-q:before, q:after {
-	content: '';
-	content: none;
-}
-table {
-	border-collapse: collapse;
-	border-spacing: 0;
-}
\ No newline at end of file
--- a/web/data/cubicweb.timeline-bundle.js	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/cubicweb.timeline-bundle.js	Thu Jan 22 17:45:06 2015 +0100
@@ -3,8 +3,8 @@
  *  :organization: Logilab
  */
 
-var SimileAjax_urlPrefix = baseuri() + 'data/';
-var Timeline_urlPrefix = baseuri() + 'data/';
+var SimileAjax_urlPrefix = BASE_URL + 'data/';
+var Timeline_urlPrefix = BASE_URL + 'data/';
 
 /*
  *  Simile Ajax API
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.treeview.css	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,2 @@
+/* override settings in jquery.treeview.css */
+ul.placeholder { display: none; }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery-treeview/changelog.md	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,36 @@
+1.4.1
+-----
+* Fix for #2360
+* Added option cookieOptions: Passed through to $.cookie to set path, domain etc.
+* Tested with jQuery 1.2.x and 1.4.3
+* Fixed combination of persist: "location" and prerendered: true
+
+1.4
+---
+
+* Added changelog (this file)
+* Fixed tree control to search only for anchors, allowing images or other elements inside the controls, while keeping the control usable with the keyboard
+* Restructured folder layout: root contains plugin resources, lib contains script dependencies, demo contains demos and related files
+* Added prerendered option: If set to true, assumes all hitarea divs and classes already rendered, speeding up initialization for big trees, but more obtrusive
+* Added jquery.treeview.async.js for ajax-lazy-loading trees, see async.html demo
+* Exposed $.fn.treeview.classes for custom classes if necessary
+* Show treecontrol only when JavaScript is enabled
+* Completely reworked themeing via CSS sprites, resulting in only two files per theme
+  * updated dotted, black, gray and red theme
+  * added famfamfam theme (no lines)
+* Improved cookie persistence to allow multiple persisted trees per page via cookieId option
+* Improved location persistence by making it case-insensitive
+* Improved swapClass and replaceClass plugin implementations
+* Added folder-closed.gif to filetree example
+
+1.3
+---
+
+* Fixes for all outstanding bugs
+* Added persistence features
+      * location based: click on a link in the treeview and reopen that link after the page loaded
+      * cookie based: save the state of the tree in a cookie on each click and load that on reload
+* smoothed animations, fixing flickering in both IE and Opera
+* Tested in Firefox 2, IE 6 & 7, Opera 9, Safari 3
+* Moved documentation to jQuery wiki
+* Requires jQuery 1.2+
Binary file web/data/jquery-treeview/images/ajax-loader.gif has changed
Binary file web/data/jquery-treeview/images/file.gif has changed
Binary file web/data/jquery-treeview/images/folder-closed.gif has changed
Binary file web/data/jquery-treeview/images/folder.gif has changed
Binary file web/data/jquery-treeview/images/minus.gif has changed
Binary file web/data/jquery-treeview/images/plus.gif has changed
Binary file web/data/jquery-treeview/images/treeview-black-line.gif has changed
Binary file web/data/jquery-treeview/images/treeview-black.gif has changed
Binary file web/data/jquery-treeview/images/treeview-default-line.gif has changed
Binary file web/data/jquery-treeview/images/treeview-default.gif has changed
Binary file web/data/jquery-treeview/images/treeview-famfamfam-line.gif has changed
Binary file web/data/jquery-treeview/images/treeview-famfamfam.gif has changed
Binary file web/data/jquery-treeview/images/treeview-gray-line.gif has changed
Binary file web/data/jquery-treeview/images/treeview-gray.gif has changed
Binary file web/data/jquery-treeview/images/treeview-red-line.gif has changed
Binary file web/data/jquery-treeview/images/treeview-red.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery-treeview/jquery.treeview.async.js	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,108 @@
+/*
+ * Async Treeview 0.1 - Lazy-loading extension for Treeview
+ *
+ * http://bassistance.de/jquery-plugins/jquery-plugin-treeview/
+ *
+ * Copyright 2010 Jörn Zaefferer
+ * Released under the MIT license:
+ *   http://www.opensource.org/licenses/mit-license.php
+ */
+
+;(function($) {
+
+function load(settings, root, child, container) {
+	function createNode(parent) {
+		var current = $("<li/>").attr("id", this.id || "").html("<span>" + this.text + "</span>").appendTo(parent);
+		if (this.classes) {
+			current.children("span").addClass(this.classes);
+		}
+		if (this.expanded) {
+			current.addClass("open");
+		}
+		if (this.hasChildren || this.children && this.children.length) {
+			var branch = $("<ul/>").appendTo(current);
+			if (this.hasChildren) {
+				current.addClass("hasChildren");
+				createNode.call({
+					classes: "placeholder",
+					text: "&nbsp;",
+					children:[]
+				}, branch);
+			}
+			if (this.children && this.children.length) {
+				$.each(this.children, createNode, [branch])
+			}
+		}
+	}
+	$.ajax($.extend(true, {
+		url: settings.url,
+		dataType: "json",
+		data: {
+			root: root
+		},
+		success: function(response) {
+			child.empty();
+			$.each(response, createNode, [child]);
+	        $(container).treeview({add: child});
+	    }
+	}, settings.ajax));
+	/*
+	$.getJSON(settings.url, {root: root}, function(response) {
+		function createNode(parent) {
+			var current = $("<li/>").attr("id", this.id || "").html("<span>" + this.text + "</span>").appendTo(parent);
+			if (this.classes) {
+				current.children("span").addClass(this.classes);
+			}
+			if (this.expanded) {
+				current.addClass("open");
+			}
+			if (this.hasChildren || this.children && this.children.length) {
+				var branch = $("<ul/>").appendTo(current);
+				if (this.hasChildren) {
+					current.addClass("hasChildren");
+					createNode.call({
+						classes: "placeholder",
+						text: "&nbsp;",
+						children:[]
+					}, branch);
+				}
+				if (this.children && this.children.length) {
+					$.each(this.children, createNode, [branch])
+				}
+			}
+		}
+		child.empty();
+		$.each(response, createNode, [child]);
+        $(container).treeview({add: child});
+    });
+    */
+}
+
+var proxied = $.fn.treeview;
+$.fn.treeview = function(settings) {
+	if (!settings.url) {
+		return proxied.apply(this, arguments);
+	}
+	if (!settings.root) {
+		settings.root = "source";
+	}
+	var container = this;
+	if (!container.children().size())
+		load(settings, settings.root, this, container);
+	var userToggle = settings.toggle;
+	return proxied.call(this, $.extend({}, settings, {
+		collapsed: true,
+		toggle: function() {
+			var $this = $(this);
+			if ($this.hasClass("hasChildren")) {
+				var childList = $this.removeClass("hasChildren").find("ul");
+				load(settings, this.id, childList, container);
+			}
+			if (userToggle) {
+				userToggle.apply(this, arguments);
+			}
+		}
+	}));
+};
+
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery-treeview/jquery.treeview.css	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,74 @@
+.treeview, .treeview ul {
+	padding: 0;
+	margin: 0;
+	list-style: none;
+}
+
+.treeview ul {
+	background-color: white;
+	margin-top: 4px;
+}
+
+.treeview .hitarea {
+	background: url(images/treeview-default.gif) -64px -25px no-repeat;
+	height: 16px;
+	width: 16px;
+	margin-left: -16px;
+	float: left;
+	cursor: pointer;
+}
+/* fix for IE6 */
+* html .hitarea {
+	display: inline;
+	float:none;
+}
+
+.treeview li {
+	margin: 0;
+	padding: 3px 0pt 3px 16px;
+}
+
+.treeview a.selected {
+	background-color: #eee;
+}
+
+#treecontrol { margin: 1em 0; display: none; }
+
+.treeview .hover { color: red; cursor: pointer; }
+
+.treeview li { background: url(images/treeview-default-line.gif) 0 0 no-repeat; }
+.treeview li.collapsable, .treeview li.expandable { background-position: 0 -176px; }
+
+.treeview .expandable-hitarea { background-position: -80px -3px; }
+
+.treeview li.last { background-position: 0 -1766px }
+.treeview li.lastCollapsable, .treeview li.lastExpandable { background-image: url(images/treeview-default.gif); }
+.treeview li.lastCollapsable { background-position: 0 -111px }
+.treeview li.lastExpandable { background-position: -32px -67px }
+
+.treeview div.lastCollapsable-hitarea, .treeview div.lastExpandable-hitarea { background-position: 0; }
+
+.treeview-red li { background-image: url(images/treeview-red-line.gif); }
+.treeview-red .hitarea, .treeview-red li.lastCollapsable, .treeview-red li.lastExpandable { background-image: url(images/treeview-red.gif); }
+
+.treeview-black li { background-image: url(images/treeview-black-line.gif); }
+.treeview-black .hitarea, .treeview-black li.lastCollapsable, .treeview-black li.lastExpandable { background-image: url(images/treeview-black.gif); }
+
+.treeview-gray li { background-image: url(images/treeview-gray-line.gif); }
+.treeview-gray .hitarea, .treeview-gray li.lastCollapsable, .treeview-gray li.lastExpandable { background-image: url(images/treeview-gray.gif); }
+
+.treeview-famfamfam li { background-image: url(images/treeview-famfamfam-line.gif); }
+.treeview-famfamfam .hitarea, .treeview-famfamfam li.lastCollapsable, .treeview-famfamfam li.lastExpandable { background-image: url(images/treeview-famfamfam.gif); }
+
+.treeview .placeholder {
+	background: url(images/ajax-loader.gif) 0 0 no-repeat;
+	height: 16px;
+	width: 16px;
+	display: block;
+}
+
+.filetree li { padding: 3px 0 2px 16px; }
+.filetree span.folder, .filetree span.file { padding: 1px 0 1px 16px; display: block; }
+.filetree span.folder { background: url(images/folder.gif) 0 0 no-repeat; }
+.filetree li.expandable span.folder { background: url(images/folder-closed.gif) 0 0 no-repeat; }
+.filetree span.file { background: url(images/file.gif) 0 0 no-repeat; }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery-treeview/jquery.treeview.edit.js	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,37 @@
+(function($) {
+	var CLASSES = $.treeview.classes;
+	var proxied = $.fn.treeview;
+	$.fn.treeview = function(settings) {
+		settings = $.extend({}, settings);
+		if (settings.add) {
+			return this.trigger("add", [settings.add]);
+		}
+		if (settings.remove) {
+			return this.trigger("remove", [settings.remove]);
+		}
+		return proxied.apply(this, arguments).bind("add", function(event, branches) {
+			$(branches).prev()
+				.removeClass(CLASSES.last)
+				.removeClass(CLASSES.lastCollapsable)
+				.removeClass(CLASSES.lastExpandable)
+			.find(">.hitarea")
+				.removeClass(CLASSES.lastCollapsableHitarea)
+				.removeClass(CLASSES.lastExpandableHitarea);
+			$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings, $(this).data("toggler"));
+		}).bind("remove", function(event, branches) {
+			var prev = $(branches).prev();
+			var parent = $(branches).parent();
+			$(branches).remove();
+			prev.filter(":last-child").addClass(CLASSES.last)
+				.filter("." + CLASSES.expandable).replaceClass(CLASSES.last, CLASSES.lastExpandable).end()
+				.find(">.hitarea").replaceClass(CLASSES.expandableHitarea, CLASSES.lastExpandableHitarea).end()
+				.filter("." + CLASSES.collapsable).replaceClass(CLASSES.last, CLASSES.lastCollapsable).end()
+				.find(">.hitarea").replaceClass(CLASSES.collapsableHitarea, CLASSES.lastCollapsableHitarea);
+			if (parent.is(":not(:has(>))") && parent[0] != this) {
+				parent.parent().removeClass(CLASSES.collapsable).removeClass(CLASSES.expandable)
+				parent.siblings(".hitarea").andSelf().remove();
+			}
+		});
+	};
+
+})(jQuery);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery-treeview/jquery.treeview.js	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,251 @@
+/*
+ * Treeview 1.5pre - jQuery plugin to hide and show branches of a tree
+ *
+ * http://bassistance.de/jquery-plugins/jquery-plugin-treeview/
+ * http://docs.jquery.com/Plugins/Treeview
+ *
+ * Copyright 2010 Jörn Zaefferer
+ * Released under the MIT license:
+ *   http://www.opensource.org/licenses/mit-license.php
+ */
+
+;(function($) {
+
+	// TODO rewrite as a widget, removing all the extra plugins
+	$.extend($.fn, {
+		swapClass: function(c1, c2) {
+			var c1Elements = this.filter('.' + c1);
+			this.filter('.' + c2).removeClass(c2).addClass(c1);
+			c1Elements.removeClass(c1).addClass(c2);
+			return this;
+		},
+		replaceClass: function(c1, c2) {
+			return this.filter('.' + c1).removeClass(c1).addClass(c2).end();
+		},
+		hoverClass: function(className) {
+			className = className || "hover";
+			return this.hover(function() {
+				$(this).addClass(className);
+			}, function() {
+				$(this).removeClass(className);
+			});
+		},
+		heightToggle: function(animated, callback) {
+			animated ?
+				this.animate({ height: "toggle" }, animated, callback) :
+				this.each(function(){
+					jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ]();
+					if(callback)
+						callback.apply(this, arguments);
+				});
+		},
+		heightHide: function(animated, callback) {
+			if (animated) {
+				this.animate({ height: "hide" }, animated, callback);
+			} else {
+				this.hide();
+				if (callback)
+					this.each(callback);
+			}
+		},
+		prepareBranches: function(settings) {
+			if (!settings.prerendered) {
+				// mark last tree items
+				this.filter(":last-child:not(ul)").addClass(CLASSES.last);
+				// collapse whole tree, or only those marked as closed, anyway except those marked as open
+				this.filter((settings.collapsed ? "" : "." + CLASSES.closed) + ":not(." + CLASSES.open + ")").find(">ul").hide();
+			}
+			// return all items with sublists
+			return this.filter(":has(>ul)");
+		},
+		applyClasses: function(settings, toggler) {
+			// TODO use event delegation
+			this.filter(":has(>ul):not(:has(>a))").find(">span").unbind("click.treeview").bind("click.treeview", function(event) {
+				// don't handle click events on children, eg. checkboxes
+				if ( this == event.target )
+					toggler.apply($(this).next());
+			}).add( $("a", this) ).hoverClass();
+
+			if (!settings.prerendered) {
+				// handle closed ones first
+				this.filter(":has(>ul:hidden)")
+						.addClass(CLASSES.expandable)
+						.replaceClass(CLASSES.last, CLASSES.lastExpandable);
+
+				// handle open ones
+				this.not(":has(>ul:hidden)")
+						.addClass(CLASSES.collapsable)
+						.replaceClass(CLASSES.last, CLASSES.lastCollapsable);
+
+	            // create hitarea if not present
+				var hitarea = this.find("div." + CLASSES.hitarea);
+				if (!hitarea.length)
+					hitarea = this.prepend("<div class=\"" + CLASSES.hitarea + "\"/>").find("div." + CLASSES.hitarea);
+				hitarea.removeClass().addClass(CLASSES.hitarea).each(function() {
+					var classes = "";
+					$.each($(this).parent().attr("class").split(" "), function() {
+						classes += this + "-hitarea ";
+					});
+					$(this).addClass( classes );
+				})
+			}
+
+			// apply event to hitarea
+			this.find("div." + CLASSES.hitarea).click( toggler );
+		},
+		treeview: function(settings) {
+
+			settings = $.extend({
+				cookieId: "treeview"
+			}, settings);
+
+			if ( settings.toggle ) {
+				var callback = settings.toggle;
+				settings.toggle = function() {
+					return callback.apply($(this).parent()[0], arguments);
+				};
+			}
+
+			// factory for treecontroller
+			function treeController(tree, control) {
+				// factory for click handlers
+				function handler(filter) {
+					return function() {
+						// reuse toggle event handler, applying the elements to toggle
+						// start searching for all hitareas
+						toggler.apply( $("div." + CLASSES.hitarea, tree).filter(function() {
+							// for plain toggle, no filter is provided, otherwise we need to check the parent element
+							return filter ? $(this).parent("." + filter).length : true;
+						}) );
+						return false;
+					};
+				}
+				// click on first element to collapse tree
+				$("a:eq(0)", control).click( handler(CLASSES.collapsable) );
+				// click on second to expand tree
+				$("a:eq(1)", control).click( handler(CLASSES.expandable) );
+				// click on third to toggle tree
+				$("a:eq(2)", control).click( handler() );
+			}
+
+			// handle toggle event
+			function toggler() {
+				$(this)
+					.parent()
+					// swap classes for hitarea
+					.find(">.hitarea")
+						.swapClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
+						.swapClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea )
+					.end()
+					// swap classes for parent li
+					.swapClass( CLASSES.collapsable, CLASSES.expandable )
+					.swapClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
+					// find child lists
+					.find( ">ul" )
+					// toggle them
+					.heightToggle( settings.animated, settings.toggle );
+				if ( settings.unique ) {
+					$(this).parent()
+						.siblings()
+						// swap classes for hitarea
+						.find(">.hitarea")
+							.replaceClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
+							.replaceClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea )
+						.end()
+						.replaceClass( CLASSES.collapsable, CLASSES.expandable )
+						.replaceClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
+						.find( ">ul" )
+						.heightHide( settings.animated, settings.toggle );
+				}
+			}
+			this.data("toggler", toggler);
+
+			function serialize() {
+				function binary(arg) {
+					return arg ? 1 : 0;
+				}
+				var data = [];
+				branches.each(function(i, e) {
+					data[i] = $(e).is(":has(>ul:visible)") ? 1 : 0;
+				});
+				$.cookie(settings.cookieId, data.join(""), settings.cookieOptions );
+			}
+
+			function deserialize() {
+				var stored = $.cookie(settings.cookieId);
+				if ( stored ) {
+					var data = stored.split("");
+					branches.each(function(i, e) {
+						$(e).find(">ul")[ parseInt(data[i]) ? "show" : "hide" ]();
+					});
+				}
+			}
+
+			// add treeview class to activate styles
+			this.addClass("treeview");
+
+			// prepare branches and find all tree items with child lists
+			var branches = this.find("li").prepareBranches(settings);
+
+			switch(settings.persist) {
+			case "cookie":
+				var toggleCallback = settings.toggle;
+				settings.toggle = function() {
+					serialize();
+					if (toggleCallback) {
+						toggleCallback.apply(this, arguments);
+					}
+				};
+				deserialize();
+				break;
+			case "location":
+				var current = this.find("a").filter(function() {
+					return location.href.toLowerCase().indexOf(this.href.toLowerCase()) == 0;
+				});
+				if ( current.length ) {
+					// TODO update the open/closed classes
+					var items = current.addClass("selected").parents("ul, li").add( current.next() ).show();
+					if (settings.prerendered) {
+						// if prerendered is on, replicate the basic class swapping
+						items.filter("li")
+							.swapClass( CLASSES.collapsable, CLASSES.expandable )
+							.swapClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
+							.find(">.hitarea")
+								.swapClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
+								.swapClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea );
+					}
+				}
+				break;
+			}
+
+			branches.applyClasses(settings, toggler);
+
+			// if control option is set, create the treecontroller and show it
+			if ( settings.control ) {
+				treeController(this, settings.control);
+				$(settings.control).show();
+			}
+
+			return this;
+		}
+	});
+
+	// classes used by the plugin
+	// need to be styled via external stylesheet, see first example
+	$.treeview = {};
+	var CLASSES = ($.treeview.classes = {
+		open: "open",
+		closed: "closed",
+		expandable: "expandable",
+		expandableHitarea: "expandable-hitarea",
+		lastExpandableHitarea: "lastExpandable-hitarea",
+		collapsable: "collapsable",
+		collapsableHitarea: "collapsable-hitarea",
+		lastCollapsableHitarea: "lastCollapsable-hitarea",
+		lastCollapsable: "lastCollapsable",
+		lastExpandable: "lastExpandable",
+		last: "last",
+		hitarea: "hitarea"
+	});
+
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery-treeview/jquery.treeview.sortable.js	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,378 @@
+/*
+ * jQuery UI Sortable
+ *
+ * Copyright (c) 2008 Paul Bakaus
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Sortables
+ *
+ * Depends:
+ *   ui.base.js
+ *
+ * Revision: $Id: ui.sortable.js 5262 2008-04-17 13:13:51Z paul.bakaus $
+ */
+;(function($) {
+
+	if (window.Node && Node.prototype && !Node.prototype.contains) {
+		Node.prototype.contains = function (arg) {
+			return !!(this.compareDocumentPosition(arg) & 16);
+		};
+	}
+
+
+	$.widget("ui.sortableTree", $.extend($.ui.mouse, {
+		init: function() {
+
+			//Initialize needed constants
+			var self = this, o = this.options;
+			this.containerCache = {};
+			this.element.addClass("ui-sortableTree");
+
+			//Get the items
+			this.refresh();
+
+			//Let's determine the parent's offset
+			if(!(/(relative|absolute|fixed)/).test(this.element.css('position'))) this.element.css('position', 'relative');
+			this.offset = this.element.offset();
+
+			//Initialize mouse events for interaction
+			this.mouseInit();
+
+			//Prepare cursorAt
+			if(o.cursorAt && o.cursorAt.constructor == Array)
+				o.cursorAt = { left: o.cursorAt[0], top: o.cursorAt[1] };
+
+		},
+		plugins: {},
+		ui: function(inst) {
+			return {
+				helper: (inst || this)["helper"],
+				position: (inst || this)["position"].current,
+				absolutePosition: (inst || this)["position"].absolute,
+				instance: this,
+				options: this.options,
+				element: this.element,
+				item: (inst || this)["currentItem"],
+				sender: inst ? inst.element : null
+			};
+		},
+		propagate: function(n,e,inst) {
+			$.ui.plugin.call(this, n, [e, this.ui(inst)]);
+			this.element.triggerHandler(n == "sort" ? n : "sort"+n, [e, this.ui(inst)], this.options[n]);
+		},
+		serialize: function(o) {
+
+			var items = $(this.options.items, this.element).not('.ui-sortableTree-helper'); //Only the items of the sortable itself
+			var str = []; o = o || {};
+
+			items.each(function() {
+				var res = ($(this).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/));
+				if(res) str.push((o.key || res[1])+'[]='+(o.key ? res[1] : res[2]));
+			});
+
+			return str.join('&');
+
+		},
+		toArray: function(attr) {
+			var items = $(this.options.items, this.element).not('.ui-sortableTree-helper'); //Only the items of the sortable itself
+			var ret = [];
+
+			items.each(function() { ret.push($(this).attr(attr || 'id')); });
+			return ret;
+		},
+		enable: function() {
+			this.element.removeClass("ui-sortableTree-disabled");
+			this.options.disabled = false;
+		},
+		disable: function() {
+			this.element.addClass("ui-sortableTree-disabled");
+			this.options.disabled = true;
+		},
+		/* Be careful with the following core functions */
+		intersectsWith: function(item) {
+
+			var x1 = this.position.absolute.left - 10, x2 = x1 + 10,
+			    y1 = this.position.absolute.top - 10, y2 = y1 + 10;
+			var l = item.left, r = l + item.width,
+			    t = item.top,  b = t + item.height;
+
+			return (   l < x1 + (this.helperProportions.width  / 2)    // Right Half
+				&&     x2 - (this.helperProportions.width  / 2) < r    // Left Half
+				&& t < y1 + (this.helperProportions.height / 2)        // Bottom Half
+				&&     y2 - (this.helperProportions.height / 2) < b ); // Top Half
+
+		},
+		intersectsWithEdge: function(item) {
+			var y1 = this.position.absolute.top - 10, y2 = y1 + 10;
+			var t = item.top,  b = t + item.height;
+
+			if(!this.intersectsWith(item.item.parents(".ui-sortableTree").data("sortableTree").containerCache)) return false;
+
+			if (!( t < y1 + (this.helperProportions.height / 2)        // Bottom Half
+				&&     y2 - (this.helperProportions.height / 2) < b )) return false; // Top Half
+
+			if(y2 > t && y1 < t) return 1; //Crosses top edge
+			if(y1 < b && y2 > b) return 2; //Crosses bottom edge
+
+			return false;
+
+		},
+		refresh: function() {
+			this.refreshItems();
+			this.refreshPositions();
+		},
+		refreshItems: function() {
+
+			this.items = [];
+			this.containers = [this];
+			var items = this.items;
+			var queries = [$(this.options.items, this.element)];
+
+			if(this.options.connectWith) {
+				for (var i = this.options.connectWith.length - 1; i >= 0; i--){
+					var cur = $(this.options.connectWith[i]);
+					for (var j = cur.length - 1; j >= 0; j--){
+						var inst = $.data(cur[j], 'sortableTree');
+						if(inst && !inst.options.disabled) {
+							queries.push($(inst.options.items, inst.element));
+							this.containers.push(inst);
+						}
+					};
+				};
+			}
+
+			for (var i = queries.length - 1; i >= 0; i--){
+				queries[i].each(function() {
+					$.data(this, 'sortableTree-item', true); // Data for target checking (mouse manager)
+					items.push({
+						item: $(this),
+						width: 0, height: 0,
+						left: 0, top: 0
+					});
+				});
+			};
+
+		},
+		refreshPositions: function(fast) {
+			for (var i = this.items.length - 1; i >= 0; i--){
+				if(!fast) this.items[i].height 			= this.items[i].item.outerHeight();
+				this.items[i].top 						= this.items[i].item.offset().top;
+			};
+			for (var i = this.containers.length - 1; i >= 0; i--){
+				var p =this.containers[i].element.offset();
+				this.containers[i].containerCache.left 	= p.left;
+				this.containers[i].containerCache.top 	= p.top;
+				this.containers[i].containerCache.width	= this.containers[i].element.outerWidth();
+				this.containers[i].containerCache.height= this.containers[i].element.outerHeight();
+			};
+		},
+		destroy: function() {
+
+			this.element
+				.removeClass("ui-sortableTree ui-sortableTree-disabled")
+				.removeData("sortableTree")
+				.unbind(".sortableTree");
+			this.mouseDestroy();
+
+			for ( var i = this.items.length - 1; i >= 0; i-- )
+				this.items[i].item.removeData("sortableTree-item");
+
+		},
+		contactContainers: function(e) {
+			for (var i = this.containers.length - 1; i >= 0; i--){
+
+				if(this.intersectsWith(this.containers[i].containerCache)) {
+					if(!this.containers[i].containerCache.over) {
+
+						if(this.currentContainer != this.containers[i]) {
+
+							//When entering a new container, we will find the item with the least distance and append our item near it
+							var dist = 10000; var itemWithLeastDistance = null; var base = this.position.absolute.top;
+							for (var j = this.items.length - 1; j >= 0; j--) {
+								if(!this.containers[i].element[0].contains(this.items[j].item[0])) continue;
+								var cur = this.items[j].top;
+								if(Math.abs(cur - base) < dist) {
+									dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j];
+								}
+							}
+
+							itemWithLeastDistance ? this.rearrange(e, itemWithLeastDistance) : this.rearrange(e, null, this.containers[i].element);
+							this.propagate("change", e); //Call plugins and callbacks
+							this.containers[i].propagate("change", e, this); //Call plugins and callbacks
+							this.currentContainer = this.containers[i];
+
+						}
+
+						this.containers[i].propagate("over", e, this);
+						this.containers[i].containerCache.over = 1;
+					}
+				} else {
+					if(this.containers[i].containerCache.over) {
+						this.containers[i].propagate("out", e, this);
+						this.containers[i].containerCache.over = 0;
+					}
+				}
+
+			};
+		},
+		mouseStart: function(e,el) {
+
+			if(this.options.disabled || this.options.type == 'static') return false;
+
+			//Find out if the clicked node (or one of its parents) is a actual item in this.items
+			var currentItem = null, nodes = $(e.target).parents().each(function() {
+				if($.data(this, 'sortableTree-item')) {
+					currentItem = $(this);
+					return false;
+				}
+			});
+			if($.data(e.target, 'sortableTree-item')) currentItem = $(e.target);
+
+			if(!currentItem) return false;
+			if(this.options.handle) {
+				var validHandle = false;
+				$(this.options.handle, currentItem).each(function() { if(this == e.target) validHandle = true; });
+				if(!validHandle) return false;
+			}
+
+			this.currentItem = currentItem;
+
+			var o = this.options;
+			this.currentContainer = this;
+			this.refresh();
+
+			//Create and append the visible helper
+			this.helper = typeof o.helper == 'function' ? $(o.helper.apply(this.element[0], [e, this.currentItem])) : this.currentItem.clone();
+			if(!this.helper.parents('body').length) this.helper.appendTo("body"); //Add the helper to the DOM if that didn't happen already
+			this.helper.css({ position: 'absolute', clear: 'both' }).addClass('ui-sortableTree-helper'); //Position it absolutely and add a helper class
+
+			//Prepare variables for position generation
+			$.extend(this, {
+				offsetParent: this.helper.offsetParent(),
+				offsets: { absolute: this.currentItem.offset() }
+			});
+
+			//Save the first time position
+			$.extend(this, {
+				position: {
+					current: { left: e.pageX, top: e.pageY },
+					absolute: { left: e.pageX, top: e.pageY },
+					dom: this.currentItem.prev()[0]
+				},
+				clickOffset: { left: -5, top: -5 }
+			});
+
+			this.propagate("start", e); //Call plugins and callbacks
+			this.helperProportions = { width: this.helper.outerWidth(), height: this.helper.outerHeight() }; //Save and store the helper proportions
+
+			for (var i = this.containers.length - 1; i >= 0; i--) {
+				this.containers[i].propagate("activate", e, this);
+			} //Post 'activate' events to possible containers
+
+			//Prepare possible droppables
+			if($.ui.ddmanager) $.ui.ddmanager.current = this;
+			if ($.ui.ddmanager && !o.dropBehaviour) $.ui.ddmanager.prepareOffsets(this, e);
+
+			this.dragging = true;
+			return true;
+
+		},
+		mouseStop: function(e) {
+
+			if(this.newPositionAt) this.options.sortIndication.remove.call(this.currentItem, this.newPositionAt); //remove sort indicator
+			this.propagate("stop", e); //Call plugins and trigger callbacks
+
+			//If we are using droppables, inform the manager about the drop
+			var dropped = ($.ui.ddmanager && !this.options.dropBehaviour) ? $.ui.ddmanager.drop(this, e) : false;
+			if(!dropped && this.newPositionAt) this.newPositionAt[this.direction == 'down' ? 'before' : 'after'](this.currentItem); //Append to element to its new position
+
+			if(this.position.dom != this.currentItem.prev()[0]) this.propagate("update", e); //Trigger update callback if the DOM position has changed
+			if(!this.element[0].contains(this.currentItem[0])) { //Node was moved out of the current element
+				this.propagate("remove", e);
+				for (var i = this.containers.length - 1; i >= 0; i--){
+					if(this.containers[i].element[0].contains(this.currentItem[0])) {
+						this.containers[i].propagate("update", e, this);
+						this.containers[i].propagate("receive", e, this);
+					}
+				};
+			};
+
+			//Post events to containers
+			for (var i = this.containers.length - 1; i >= 0; i--){
+				this.containers[i].propagate("deactivate", e, this);
+				if(this.containers[i].containerCache.over) {
+					this.containers[i].propagate("out", e, this);
+					this.containers[i].containerCache.over = 0;
+				}
+			}
+
+			this.dragging = false;
+			if(this.cancelHelperRemoval) return false;
+			this.helper.remove();
+
+			return false;
+
+		},
+		mouseDrag: function(e) {
+
+			//Compute the helpers position
+			this.position.current = { top: e.pageY + 5, left: e.pageX + 5 };
+			this.position.absolute = { left: e.pageX + 5, top: e.pageY + 5 };
+
+			//Interconnect with droppables
+			if($.ui.ddmanager) $.ui.ddmanager.drag(this, e);
+			var intersectsWithDroppable = false;
+			$.each($.ui.ddmanager.droppables, function() {
+				if(this.isover) intersectsWithDroppable = true;
+			});
+
+			//Rearrange
+			if(intersectsWithDroppable) {
+				if(this.newPositionAt) this.options.sortIndication.remove.call(this.currentItem, this.newPositionAt);
+			} else {
+				for (var i = this.items.length - 1; i >= 0; i--) {
+
+					if(this.currentItem[0].contains(this.items[i].item[0])) continue;
+
+					var intersection = this.intersectsWithEdge(this.items[i]);
+					if(!intersection) continue;
+
+					this.direction = intersection == 1 ? "down" : "up";
+					this.rearrange(e, this.items[i]);
+					this.propagate("change", e); //Call plugins and callbacks
+					break;
+				}
+			}
+
+			//Post events to containers
+			this.contactContainers(e);
+
+			this.propagate("sort", e); //Call plugins and callbacks
+			this.helper.css({ left: this.position.current.left+'px', top: this.position.current.top+'px' }); // Stick the helper to the cursor
+			return false;
+
+		},
+		rearrange: function(e, i, a) {
+			if(i) {
+				if(this.newPositionAt) this.options.sortIndication.remove.call(this.currentItem, this.newPositionAt);
+				this.newPositionAt = i.item;
+				this.options.sortIndication[this.direction].call(this.currentItem, this.newPositionAt);
+			} else {
+				//Append
+			}
+		}
+	}));
+
+	$.extend($.ui.sortableTree, {
+		defaults: {
+			items: '> *',
+			zIndex: 1000,
+			distance: 1
+		},
+		getter: "serialize toArray"
+	});
+
+
+
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery-treeview/readme.md	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,47 @@
+# jQuery Treeview
+
+Lightweight and flexible transformation of an unordered list into an expandable and collapsable tree, great for unobtrusive navigation enhancements. Supports both location and cookie based persistence.
+
+Provides some options for customizing, an async-tree extension and an experimental sortable extension.
+
+![screenshot](https://raw.github.com/jzaefferer/jquery-treeview/master/screenshot.png)
+
+### Note that this project is not actively maintained anymore.  
+Check out [jqTree](http://mbraak.github.com/jqTree/) for a more up to date plugin.
+
+---
+
+#### [Demo](http://jquery.bassistance.de/treeview/demo/)
+
+#### [Download](https://github.com/jzaefferer/jquery-treeview/zipball/1.4.1)
+
+#### [Changelog](https://raw.github.com/jzaefferer/jquery-treeview/master/changelog.md)
+
+
+## Todo
+
+### 1.5
+- Add classes and rules for root items
+- Lazy-loading: render the complete tree, but only apply hitzones and hiding of children to the first level on load
+- Async treeview
+  - Support animations
+  - Support persist options
+
+
+## Documentation
+
+```javascript
+.treeview( options )
+```
+
+Takes an unordered list and makes all branches collapsable. The "treeview" class is added if not already present. To hide branches on first display, mark their li elements with the class "closed". If the "collapsed" option is used, mark initially open branches with class "open".
+
+
+## License
+
+Copyright (c) 2007 Jörn Zaefferer
+
+Dual licensed under the MIT and GPL licenses:
+
+- http://www.opensource.org/licenses/mit-license.php
+- http://www.gnu.org/licenses/gpl.html
\ No newline at end of file
Binary file web/data/jquery-treeview/screenshot.png has changed
--- a/web/data/jquery.flot.js	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/jquery.flot.js	Thu Jan 22 17:45:06 2015 +0100
@@ -1,1 +1,2119 @@
-(function(){jQuery.color={};jQuery.color.make=function(G,H,J,I){var A={};A.r=G||0;A.g=H||0;A.b=J||0;A.a=I!=null?I:1;A.add=function(C,D){for(var E=0;E<C.length;++E){A[C.charAt(E)]+=D}return A.normalize()};A.scale=function(C,D){for(var E=0;E<C.length;++E){A[C.charAt(E)]*=D}return A.normalize()};A.toString=function(){if(A.a>=1){return"rgb("+[A.r,A.g,A.b].join(",")+")"}else{return"rgba("+[A.r,A.g,A.b,A.a].join(",")+")"}};A.normalize=function(){function C(E,D,F){return D<E?E:(D>F?F:D)}A.r=C(0,parseInt(A.r),255);A.g=C(0,parseInt(A.g),255);A.b=C(0,parseInt(A.b),255);A.a=C(0,A.a,1);return A};A.clone=function(){return jQuery.color.make(A.r,A.b,A.g,A.a)};return A.normalize()};jQuery.color.extract=function(E,F){var A;do{A=E.css(F).toLowerCase();if(A!=""&&A!="transparent"){break}E=E.parent()}while(!jQuery.nodeName(E.get(0),"body"));if(A=="rgba(0, 0, 0, 0)"){A="transparent"}return jQuery.color.parse(A)};jQuery.color.parse=function(A){var F,H=jQuery.color.make;if(F=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(A)){return H(parseInt(F[1],10),parseInt(F[2],10),parseInt(F[3],10))}if(F=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(A)){return H(parseInt(F[1],10),parseInt(F[2],10),parseInt(F[3],10),parseFloat(F[4]))}if(F=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(A)){return H(parseFloat(F[1])*2.55,parseFloat(F[2])*2.55,parseFloat(F[3])*2.55)}if(F=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(A)){return H(parseFloat(F[1])*2.55,parseFloat(F[2])*2.55,parseFloat(F[3])*2.55,parseFloat(F[4]))}if(F=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(A)){return H(parseInt(F[1],16),parseInt(F[2],16),parseInt(F[3],16))}if(F=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(A)){return H(parseInt(F[1]+F[1],16),parseInt(F[2]+F[2],16),parseInt(F[3]+F[3],16))}var G=jQuery.trim(A).toLowerCase();if(G=="transparent"){return H(255,255,255,0)}else{F=B[G];return H(F[0],F[1],F[2])}};var B={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();(function(C){function B(l,W,X,E){var O=[],g={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{mode:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,tickDecimals:null,tickSize:null,minTickSize:null,monthNames:null,timeformat:null,twelveHourClock:false},yaxis:{autoscaleMargin:0.02},x2axis:{autoscaleMargin:null},y2axis:{autoscaleMargin:0.02},series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false},shadowSize:3},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,tickColor:"rgba(0,0,0,0.15)",labelMargin:5,borderWidth:2,borderColor:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},hooks:{}},P=null,AC=null,AD=null,Y=null,AJ=null,s={xaxis:{},yaxis:{},x2axis:{},y2axis:{}},e={left:0,right:0,top:0,bottom:0},y=0,Q=0,I=0,t=0,L={processOptions:[],processRawData:[],processDatapoints:[],draw:[],bindEvents:[],drawOverlay:[]},G=this;G.setData=f;G.setupGrid=k;G.draw=AH;G.getPlaceholder=function(){return l};G.getCanvas=function(){return P};G.getPlotOffset=function(){return e};G.width=function(){return I};G.height=function(){return t};G.offset=function(){var AK=AD.offset();AK.left+=e.left;AK.top+=e.top;return AK};G.getData=function(){return O};G.getAxes=function(){return s};G.getOptions=function(){return g};G.highlight=AE;G.unhighlight=x;G.triggerRedrawOverlay=q;G.pointOffset=function(AK){return{left:parseInt(T(AK,"xaxis").p2c(+AK.x)+e.left),top:parseInt(T(AK,"yaxis").p2c(+AK.y)+e.top)}};G.hooks=L;b(G);r(X);c();f(W);k();AH();AG();function Z(AM,AK){AK=[G].concat(AK);for(var AL=0;AL<AM.length;++AL){AM[AL].apply(this,AK)}}function b(){for(var AK=0;AK<E.length;++AK){var AL=E[AK];AL.init(G);if(AL.options){C.extend(true,g,AL.options)}}}function r(AK){C.extend(true,g,AK);if(g.grid.borderColor==null){g.grid.borderColor=g.grid.color}if(g.xaxis.noTicks&&g.xaxis.ticks==null){g.xaxis.ticks=g.xaxis.noTicks}if(g.yaxis.noTicks&&g.yaxis.ticks==null){g.yaxis.ticks=g.yaxis.noTicks}if(g.grid.coloredAreas){g.grid.markings=g.grid.coloredAreas}if(g.grid.coloredAreasColor){g.grid.markingsColor=g.grid.coloredAreasColor}if(g.lines){C.extend(true,g.series.lines,g.lines)}if(g.points){C.extend(true,g.series.points,g.points)}if(g.bars){C.extend(true,g.series.bars,g.bars)}if(g.shadowSize){g.series.shadowSize=g.shadowSize}for(var AL in L){if(g.hooks[AL]&&g.hooks[AL].length){L[AL]=L[AL].concat(g.hooks[AL])}}Z(L.processOptions,[g])}function f(AK){O=M(AK);U();m()}function M(AN){var AL=[];for(var AK=0;AK<AN.length;++AK){var AM=C.extend(true,{},g.series);if(AN[AK].data){AM.data=AN[AK].data;delete AN[AK].data;C.extend(true,AM,AN[AK]);AN[AK].data=AM.data}else{AM.data=AN[AK]}AL.push(AM)}return AL}function T(AM,AK){var AL=AM[AK];if(!AL||AL==1){return s[AK]}if(typeof AL=="number"){return s[AK.charAt(0)+AL+AK.slice(1)]}return AL}function U(){var AP;var AV=O.length,AK=[],AN=[];for(AP=0;AP<O.length;++AP){var AS=O[AP].color;if(AS!=null){--AV;if(typeof AS=="number"){AN.push(AS)}else{AK.push(C.color.parse(O[AP].color))}}}for(AP=0;AP<AN.length;++AP){AV=Math.max(AV,AN[AP]+1)}var AL=[],AO=0;AP=0;while(AL.length<AV){var AR;if(g.colors.length==AP){AR=C.color.make(100,100,100)}else{AR=C.color.parse(g.colors[AP])}var AM=AO%2==1?-1:1;AR.scale("rgb",1+AM*Math.ceil(AO/2)*0.2);AL.push(AR);++AP;if(AP>=g.colors.length){AP=0;++AO}}var AQ=0,AW;for(AP=0;AP<O.length;++AP){AW=O[AP];if(AW.color==null){AW.color=AL[AQ].toString();++AQ}else{if(typeof AW.color=="number"){AW.color=AL[AW.color].toString()}}if(AW.lines.show==null){var AU,AT=true;for(AU in AW){if(AW[AU].show){AT=false;break}}if(AT){AW.lines.show=true}}AW.xaxis=T(AW,"xaxis");AW.yaxis=T(AW,"yaxis")}}function m(){var AW=Number.POSITIVE_INFINITY,AQ=Number.NEGATIVE_INFINITY,Ac,Aa,AZ,AV,AL,AR,Ab,AX,AP,AO,AK,Ai,Af,AT;for(AK in s){s[AK].datamin=AW;s[AK].datamax=AQ;s[AK].used=false}function AN(Al,Ak,Aj){if(Ak<Al.datamin){Al.datamin=Ak}if(Aj>Al.datamax){Al.datamax=Aj}}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];AR.datapoints={points:[]};Z(L.processRawData,[AR,AR.data,AR.datapoints])}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];var Ah=AR.data,Ae=AR.datapoints.format;if(!Ae){Ae=[];Ae.push({x:true,number:true,required:true});Ae.push({y:true,number:true,required:true});if(AR.bars.show){Ae.push({y:true,number:true,required:false,defaultValue:0})}AR.datapoints.format=Ae}if(AR.datapoints.pointsize!=null){continue}if(AR.datapoints.pointsize==null){AR.datapoints.pointsize=Ae.length}AX=AR.datapoints.pointsize;Ab=AR.datapoints.points;insertSteps=AR.lines.show&&AR.lines.steps;AR.xaxis.used=AR.yaxis.used=true;for(Aa=AZ=0;Aa<Ah.length;++Aa,AZ+=AX){AT=Ah[Aa];var AM=AT==null;if(!AM){for(AV=0;AV<AX;++AV){Ai=AT[AV];Af=Ae[AV];if(Af){if(Af.number&&Ai!=null){Ai=+Ai;if(isNaN(Ai)){Ai=null}}if(Ai==null){if(Af.required){AM=true}if(Af.defaultValue!=null){Ai=Af.defaultValue}}}Ab[AZ+AV]=Ai}}if(AM){for(AV=0;AV<AX;++AV){Ai=Ab[AZ+AV];if(Ai!=null){Af=Ae[AV];if(Af.x){AN(AR.xaxis,Ai,Ai)}if(Af.y){AN(AR.yaxis,Ai,Ai)}}Ab[AZ+AV]=null}}else{if(insertSteps&&AZ>0&&Ab[AZ-AX]!=null&&Ab[AZ-AX]!=Ab[AZ]&&Ab[AZ-AX+1]!=Ab[AZ+1]){for(AV=0;AV<AX;++AV){Ab[AZ+AX+AV]=Ab[AZ+AV]}Ab[AZ+1]=Ab[AZ-AX+1];AZ+=AX}}}}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];Z(L.processDatapoints,[AR,AR.datapoints])}for(Ac=0;Ac<O.length;++Ac){AR=O[Ac];Ab=AR.datapoints.points,AX=AR.datapoints.pointsize;var AS=AW,AY=AW,AU=AQ,Ad=AQ;for(Aa=0;Aa<Ab.length;Aa+=AX){if(Ab[Aa]==null){continue}for(AV=0;AV<AX;++AV){Ai=Ab[Aa+AV];Af=Ae[AV];if(!Af){continue}if(Af.x){if(Ai<AS){AS=Ai}if(Ai>AU){AU=Ai}}if(Af.y){if(Ai<AY){AY=Ai}if(Ai>Ad){Ad=Ai}}}}if(AR.bars.show){var Ag=AR.bars.align=="left"?0:-AR.bars.barWidth/2;if(AR.bars.horizontal){AY+=Ag;Ad+=Ag+AR.bars.barWidth}else{AS+=Ag;AU+=Ag+AR.bars.barWidth}}AN(AR.xaxis,AS,AU);AN(AR.yaxis,AY,Ad)}for(AK in s){if(s[AK].datamin==AW){s[AK].datamin=null}if(s[AK].datamax==AQ){s[AK].datamax=null}}}function c(){function AK(AM,AL){var AN=document.createElement("canvas");AN.width=AM;AN.height=AL;if(C.browser.msie){AN=window.G_vmlCanvasManager.initElement(AN)}return AN}y=l.width();Q=l.height();l.html("");if(l.css("position")=="static"){l.css("position","relative")}if(y<=0||Q<=0){throw"Invalid dimensions for plot, width = "+y+", height = "+Q}if(C.browser.msie){window.G_vmlCanvasManager.init_(document)}P=C(AK(y,Q)).appendTo(l).get(0);Y=P.getContext("2d");AC=C(AK(y,Q)).css({position:"absolute",left:0,top:0}).appendTo(l).get(0);AJ=AC.getContext("2d");AJ.stroke()}function AG(){AD=C([AC,P]);if(g.grid.hoverable){AD.mousemove(D)}if(g.grid.clickable){AD.click(d)}Z(L.bindEvents,[AD])}function k(){function AL(AT,AU){function AP(AV){return AV}var AS,AO,AQ=AU.transform||AP,AR=AU.inverseTransform;if(AT==s.xaxis||AT==s.x2axis){AS=AT.scale=I/(AQ(AT.max)-AQ(AT.min));AO=AQ(AT.min);if(AQ==AP){AT.p2c=function(AV){return(AV-AO)*AS}}else{AT.p2c=function(AV){return(AQ(AV)-AO)*AS}}if(!AR){AT.c2p=function(AV){return AO+AV/AS}}else{AT.c2p=function(AV){return AR(AO+AV/AS)}}}else{AS=AT.scale=t/(AQ(AT.max)-AQ(AT.min));AO=AQ(AT.max);if(AQ==AP){AT.p2c=function(AV){return(AO-AV)*AS}}else{AT.p2c=function(AV){return(AO-AQ(AV))*AS}}if(!AR){AT.c2p=function(AV){return AO-AV/AS}}else{AT.c2p=function(AV){return AR(AO-AV/AS)}}}}function AN(AR,AT){var AQ,AS=[],AP;AR.labelWidth=AT.labelWidth;AR.labelHeight=AT.labelHeight;if(AR==s.xaxis||AR==s.x2axis){if(AR.labelWidth==null){AR.labelWidth=y/(AR.ticks.length>0?AR.ticks.length:1)}if(AR.labelHeight==null){AS=[];for(AQ=0;AQ<AR.ticks.length;++AQ){AP=AR.ticks[AQ].label;if(AP){AS.push('<div class="tickLabel" style="float:left;width:'+AR.labelWidth+'px">'+AP+"</div>")}}if(AS.length>0){var AO=C('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">'+AS.join("")+'<div style="clear:left"></div></div>').appendTo(l);AR.labelHeight=AO.height();AO.remove()}}}else{if(AR.labelWidth==null||AR.labelHeight==null){for(AQ=0;AQ<AR.ticks.length;++AQ){AP=AR.ticks[AQ].label;if(AP){AS.push('<div class="tickLabel">'+AP+"</div>")}}if(AS.length>0){var AO=C('<div style="position:absolute;top:-10000px;font-size:smaller">'+AS.join("")+"</div>").appendTo(l);if(AR.labelWidth==null){AR.labelWidth=AO.width()}if(AR.labelHeight==null){AR.labelHeight=AO.find("div").height()}AO.remove()}}}if(AR.labelWidth==null){AR.labelWidth=0}if(AR.labelHeight==null){AR.labelHeight=0}}function AM(){var AP=g.grid.borderWidth;for(i=0;i<O.length;++i){AP=Math.max(AP,2*(O[i].points.radius+O[i].points.lineWidth/2))}e.left=e.right=e.top=e.bottom=AP;var AO=g.grid.labelMargin+g.grid.borderWidth;if(s.xaxis.labelHeight>0){e.bottom=Math.max(AP,s.xaxis.labelHeight+AO)}if(s.yaxis.labelWidth>0){e.left=Math.max(AP,s.yaxis.labelWidth+AO)}if(s.x2axis.labelHeight>0){e.top=Math.max(AP,s.x2axis.labelHeight+AO)}if(s.y2axis.labelWidth>0){e.right=Math.max(AP,s.y2axis.labelWidth+AO)}I=y-e.left-e.right;t=Q-e.bottom-e.top}var AK;for(AK in s){K(s[AK],g[AK])}if(g.grid.show){for(AK in s){F(s[AK],g[AK]);p(s[AK],g[AK]);AN(s[AK],g[AK])}AM()}else{e.left=e.right=e.top=e.bottom=0;I=y;t=Q}for(AK in s){AL(s[AK],g[AK])}if(g.grid.show){h()}AI()}function K(AN,AQ){var AM=+(AQ.min!=null?AQ.min:AN.datamin),AK=+(AQ.max!=null?AQ.max:AN.datamax),AP=AK-AM;if(AP==0){var AL=AK==0?1:0.01;if(AQ.min==null){AM-=AL}if(AQ.max==null||AQ.min!=null){AK+=AL}}else{var AO=AQ.autoscaleMargin;if(AO!=null){if(AQ.min==null){AM-=AP*AO;if(AM<0&&AN.datamin!=null&&AN.datamin>=0){AM=0}}if(AQ.max==null){AK+=AP*AO;if(AK>0&&AN.datamax!=null&&AN.datamax<=0){AK=0}}}}AN.min=AM;AN.max=AK}function F(AP,AS){var AO;if(typeof AS.ticks=="number"&&AS.ticks>0){AO=AS.ticks}else{if(AP==s.xaxis||AP==s.x2axis){AO=0.3*Math.sqrt(y)}else{AO=0.3*Math.sqrt(Q)}}var AX=(AP.max-AP.min)/AO,AZ,AT,AV,AW,AR,AM,AL;if(AS.mode=="time"){var AU={second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000};var AY=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]];var AN=0;if(AS.minTickSize!=null){if(typeof AS.tickSize=="number"){AN=AS.tickSize}else{AN=AS.minTickSize[0]*AU[AS.minTickSize[1]]}}for(AR=0;AR<AY.length-1;++AR){if(AX<(AY[AR][0]*AU[AY[AR][1]]+AY[AR+1][0]*AU[AY[AR+1][1]])/2&&AY[AR][0]*AU[AY[AR][1]]>=AN){break}}AZ=AY[AR][0];AV=AY[AR][1];if(AV=="year"){AM=Math.pow(10,Math.floor(Math.log(AX/AU.year)/Math.LN10));AL=(AX/AU.year)/AM;if(AL<1.5){AZ=1}else{if(AL<3){AZ=2}else{if(AL<7.5){AZ=5}else{AZ=10}}}AZ*=AM}if(AS.tickSize){AZ=AS.tickSize[0];AV=AS.tickSize[1]}AT=function(Ac){var Ah=[],Af=Ac.tickSize[0],Ai=Ac.tickSize[1],Ag=new Date(Ac.min);var Ab=Af*AU[Ai];if(Ai=="second"){Ag.setUTCSeconds(A(Ag.getUTCSeconds(),Af))}if(Ai=="minute"){Ag.setUTCMinutes(A(Ag.getUTCMinutes(),Af))}if(Ai=="hour"){Ag.setUTCHours(A(Ag.getUTCHours(),Af))}if(Ai=="month"){Ag.setUTCMonth(A(Ag.getUTCMonth(),Af))}if(Ai=="year"){Ag.setUTCFullYear(A(Ag.getUTCFullYear(),Af))}Ag.setUTCMilliseconds(0);if(Ab>=AU.minute){Ag.setUTCSeconds(0)}if(Ab>=AU.hour){Ag.setUTCMinutes(0)}if(Ab>=AU.day){Ag.setUTCHours(0)}if(Ab>=AU.day*4){Ag.setUTCDate(1)}if(Ab>=AU.year){Ag.setUTCMonth(0)}var Ak=0,Aj=Number.NaN,Ad;do{Ad=Aj;Aj=Ag.getTime();Ah.push({v:Aj,label:Ac.tickFormatter(Aj,Ac)});if(Ai=="month"){if(Af<1){Ag.setUTCDate(1);var Aa=Ag.getTime();Ag.setUTCMonth(Ag.getUTCMonth()+1);var Ae=Ag.getTime();Ag.setTime(Aj+Ak*AU.hour+(Ae-Aa)*Af);Ak=Ag.getUTCHours();Ag.setUTCHours(0)}else{Ag.setUTCMonth(Ag.getUTCMonth()+Af)}}else{if(Ai=="year"){Ag.setUTCFullYear(Ag.getUTCFullYear()+Af)}else{Ag.setTime(Aj+Ab)}}}while(Aj<Ac.max&&Aj!=Ad);return Ah};AW=function(Aa,Ad){var Af=new Date(Aa);if(AS.timeformat!=null){return C.plot.formatDate(Af,AS.timeformat,AS.monthNames)}var Ab=Ad.tickSize[0]*AU[Ad.tickSize[1]];var Ac=Ad.max-Ad.min;var Ae=(AS.twelveHourClock)?" %p":"";if(Ab<AU.minute){fmt="%h:%M:%S"+Ae}else{if(Ab<AU.day){if(Ac<2*AU.day){fmt="%h:%M"+Ae}else{fmt="%b %d %h:%M"+Ae}}else{if(Ab<AU.month){fmt="%b %d"}else{if(Ab<AU.year){if(Ac<AU.year){fmt="%b"}else{fmt="%b %y"}}else{fmt="%y"}}}}return C.plot.formatDate(Af,fmt,AS.monthNames)}}else{var AK=AS.tickDecimals;var AQ=-Math.floor(Math.log(AX)/Math.LN10);if(AK!=null&&AQ>AK){AQ=AK}AM=Math.pow(10,-AQ);AL=AX/AM;if(AL<1.5){AZ=1}else{if(AL<3){AZ=2;if(AL>2.25&&(AK==null||AQ+1<=AK)){AZ=2.5;++AQ}}else{if(AL<7.5){AZ=5}else{AZ=10}}}AZ*=AM;if(AS.minTickSize!=null&&AZ<AS.minTickSize){AZ=AS.minTickSize}if(AS.tickSize!=null){AZ=AS.tickSize}AP.tickDecimals=Math.max(0,(AK!=null)?AK:AQ);AT=function(Ac){var Ae=[];var Af=A(Ac.min,Ac.tickSize),Ab=0,Aa=Number.NaN,Ad;do{Ad=Aa;Aa=Af+Ab*Ac.tickSize;Ae.push({v:Aa,label:Ac.tickFormatter(Aa,Ac)});++Ab}while(Aa<Ac.max&&Aa!=Ad);return Ae};AW=function(Aa,Ab){return Aa.toFixed(Ab.tickDecimals)}}AP.tickSize=AV?[AZ,AV]:AZ;AP.tickGenerator=AT;if(C.isFunction(AS.tickFormatter)){AP.tickFormatter=function(Aa,Ab){return""+AS.tickFormatter(Aa,Ab)}}else{AP.tickFormatter=AW}}function p(AO,AQ){AO.ticks=[];if(!AO.used){return }if(AQ.ticks==null){AO.ticks=AO.tickGenerator(AO)}else{if(typeof AQ.ticks=="number"){if(AQ.ticks>0){AO.ticks=AO.tickGenerator(AO)}}else{if(AQ.ticks){var AP=AQ.ticks;if(C.isFunction(AP)){AP=AP({min:AO.min,max:AO.max})}var AN,AK;for(AN=0;AN<AP.length;++AN){var AL=null;var AM=AP[AN];if(typeof AM=="object"){AK=AM[0];if(AM.length>1){AL=AM[1]}}else{AK=AM}if(AL==null){AL=AO.tickFormatter(AK,AO)}AO.ticks[AN]={v:AK,label:AL}}}}}if(AQ.autoscaleMargin!=null&&AO.ticks.length>0){if(AQ.min==null){AO.min=Math.min(AO.min,AO.ticks[0].v)}if(AQ.max==null&&AO.ticks.length>1){AO.max=Math.max(AO.max,AO.ticks[AO.ticks.length-1].v)}}}function AH(){Y.clearRect(0,0,y,Q);var AL=g.grid;if(AL.show&&!AL.aboveData){S()}for(var AK=0;AK<O.length;++AK){AA(O[AK])}Z(L.draw,[Y]);if(AL.show&&AL.aboveData){S()}}function N(AL,AR){var AO=AR+"axis",AK=AR+"2axis",AN,AQ,AP,AM;if(AL[AO]){AN=s[AO];AQ=AL[AO].from;AP=AL[AO].to}else{if(AL[AK]){AN=s[AK];AQ=AL[AK].from;AP=AL[AK].to}else{AN=s[AO];AQ=AL[AR+"1"];AP=AL[AR+"2"]}}if(AQ!=null&&AP!=null&&AQ>AP){return{from:AP,to:AQ,axis:AN}}return{from:AQ,to:AP,axis:AN}}function S(){var AO;Y.save();Y.translate(e.left,e.top);if(g.grid.backgroundColor){Y.fillStyle=R(g.grid.backgroundColor,t,0,"rgba(255, 255, 255, 0)");Y.fillRect(0,0,I,t)}var AL=g.grid.markings;if(AL){if(C.isFunction(AL)){AL=AL({xmin:s.xaxis.min,xmax:s.xaxis.max,ymin:s.yaxis.min,ymax:s.yaxis.max,xaxis:s.xaxis,yaxis:s.yaxis,x2axis:s.x2axis,y2axis:s.y2axis})}for(AO=0;AO<AL.length;++AO){var AK=AL[AO],AQ=N(AK,"x"),AN=N(AK,"y");if(AQ.from==null){AQ.from=AQ.axis.min}if(AQ.to==null){AQ.to=AQ.axis.max}if(AN.from==null){AN.from=AN.axis.min}if(AN.to==null){AN.to=AN.axis.max}if(AQ.to<AQ.axis.min||AQ.from>AQ.axis.max||AN.to<AN.axis.min||AN.from>AN.axis.max){continue}AQ.from=Math.max(AQ.from,AQ.axis.min);AQ.to=Math.min(AQ.to,AQ.axis.max);AN.from=Math.max(AN.from,AN.axis.min);AN.to=Math.min(AN.to,AN.axis.max);if(AQ.from==AQ.to&&AN.from==AN.to){continue}AQ.from=AQ.axis.p2c(AQ.from);AQ.to=AQ.axis.p2c(AQ.to);AN.from=AN.axis.p2c(AN.from);AN.to=AN.axis.p2c(AN.to);if(AQ.from==AQ.to||AN.from==AN.to){Y.beginPath();Y.strokeStyle=AK.color||g.grid.markingsColor;Y.lineWidth=AK.lineWidth||g.grid.markingsLineWidth;Y.moveTo(AQ.from,AN.from);Y.lineTo(AQ.to,AN.to);Y.stroke()}else{Y.fillStyle=AK.color||g.grid.markingsColor;Y.fillRect(AQ.from,AN.to,AQ.to-AQ.from,AN.from-AN.to)}}}Y.lineWidth=1;Y.strokeStyle=g.grid.tickColor;Y.beginPath();var AM,AP=s.xaxis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=s.xaxis.max){continue}Y.moveTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,0);Y.lineTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,t)}AP=s.yaxis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=AP.max){continue}Y.moveTo(0,Math.floor(AP.p2c(AM))+Y.lineWidth/2);Y.lineTo(I,Math.floor(AP.p2c(AM))+Y.lineWidth/2)}AP=s.x2axis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=AP.max){continue}Y.moveTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,-5);Y.lineTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,5)}AP=s.y2axis;for(AO=0;AO<AP.ticks.length;++AO){AM=AP.ticks[AO].v;if(AM<=AP.min||AM>=AP.max){continue}Y.moveTo(I-5,Math.floor(AP.p2c(AM))+Y.lineWidth/2);Y.lineTo(I+5,Math.floor(AP.p2c(AM))+Y.lineWidth/2)}Y.stroke();if(g.grid.borderWidth){var AR=g.grid.borderWidth;Y.lineWidth=AR;Y.strokeStyle=g.grid.borderColor;Y.strokeRect(-AR/2,-AR/2,I+AR,t+AR)}Y.restore()}function h(){l.find(".tickLabels").remove();var AK=['<div class="tickLabels" style="font-size:smaller;color:'+g.grid.color+'">'];function AM(AP,AQ){for(var AO=0;AO<AP.ticks.length;++AO){var AN=AP.ticks[AO];if(!AN.label||AN.v<AP.min||AN.v>AP.max){continue}AK.push(AQ(AN,AP))}}var AL=g.grid.labelMargin+g.grid.borderWidth;AM(s.xaxis,function(AN,AO){return'<div style="position:absolute;top:'+(e.top+t+AL)+"px;left:"+Math.round(e.left+AO.p2c(AN.v)-AO.labelWidth/2)+"px;width:"+AO.labelWidth+'px;text-align:center" class="tickLabel">'+AN.label+"</div>"});AM(s.yaxis,function(AN,AO){return'<div style="position:absolute;top:'+Math.round(e.top+AO.p2c(AN.v)-AO.labelHeight/2)+"px;right:"+(e.right+I+AL)+"px;width:"+AO.labelWidth+'px;text-align:right" class="tickLabel">'+AN.label+"</div>"});AM(s.x2axis,function(AN,AO){return'<div style="position:absolute;bottom:'+(e.bottom+t+AL)+"px;left:"+Math.round(e.left+AO.p2c(AN.v)-AO.labelWidth/2)+"px;width:"+AO.labelWidth+'px;text-align:center" class="tickLabel">'+AN.label+"</div>"});AM(s.y2axis,function(AN,AO){return'<div style="position:absolute;top:'+Math.round(e.top+AO.p2c(AN.v)-AO.labelHeight/2)+"px;left:"+(e.left+I+AL)+"px;width:"+AO.labelWidth+'px;text-align:left" class="tickLabel">'+AN.label+"</div>"});AK.push("</div>");l.append(AK.join(""))}function AA(AK){if(AK.lines.show){a(AK)}if(AK.bars.show){n(AK)}if(AK.points.show){o(AK)}}function a(AN){function AM(AY,AZ,AR,Ad,Ac){var Ae=AY.points,AS=AY.pointsize,AW=null,AV=null;Y.beginPath();for(var AX=AS;AX<Ae.length;AX+=AS){var AU=Ae[AX-AS],Ab=Ae[AX-AS+1],AT=Ae[AX],Aa=Ae[AX+1];if(AU==null||AT==null){continue}if(Ab<=Aa&&Ab<Ac.min){if(Aa<Ac.min){continue}AU=(Ac.min-Ab)/(Aa-Ab)*(AT-AU)+AU;Ab=Ac.min}else{if(Aa<=Ab&&Aa<Ac.min){if(Ab<Ac.min){continue}AT=(Ac.min-Ab)/(Aa-Ab)*(AT-AU)+AU;Aa=Ac.min}}if(Ab>=Aa&&Ab>Ac.max){if(Aa>Ac.max){continue}AU=(Ac.max-Ab)/(Aa-Ab)*(AT-AU)+AU;Ab=Ac.max}else{if(Aa>=Ab&&Aa>Ac.max){if(Ab>Ac.max){continue}AT=(Ac.max-Ab)/(Aa-Ab)*(AT-AU)+AU;Aa=Ac.max}}if(AU<=AT&&AU<Ad.min){if(AT<Ad.min){continue}Ab=(Ad.min-AU)/(AT-AU)*(Aa-Ab)+Ab;AU=Ad.min}else{if(AT<=AU&&AT<Ad.min){if(AU<Ad.min){continue}Aa=(Ad.min-AU)/(AT-AU)*(Aa-Ab)+Ab;AT=Ad.min}}if(AU>=AT&&AU>Ad.max){if(AT>Ad.max){continue}Ab=(Ad.max-AU)/(AT-AU)*(Aa-Ab)+Ab;AU=Ad.max}else{if(AT>=AU&&AT>Ad.max){if(AU>Ad.max){continue}Aa=(Ad.max-AU)/(AT-AU)*(Aa-Ab)+Ab;AT=Ad.max}}if(AU!=AW||Ab!=AV){Y.moveTo(Ad.p2c(AU)+AZ,Ac.p2c(Ab)+AR)}AW=AT;AV=Aa;Y.lineTo(Ad.p2c(AT)+AZ,Ac.p2c(Aa)+AR)}Y.stroke()}function AO(AX,Ae,Ac){var Af=AX.points,AR=AX.pointsize,AS=Math.min(Math.max(0,Ac.min),Ac.max),Aa,AV=0,Ad=false;for(var AW=AR;AW<Af.length;AW+=AR){var AU=Af[AW-AR],Ab=Af[AW-AR+1],AT=Af[AW],AZ=Af[AW+1];if(Ad&&AU!=null&&AT==null){Y.lineTo(Ae.p2c(AV),Ac.p2c(AS));Y.fill();Ad=false;continue}if(AU==null||AT==null){continue}if(AU<=AT&&AU<Ae.min){if(AT<Ae.min){continue}Ab=(Ae.min-AU)/(AT-AU)*(AZ-Ab)+Ab;AU=Ae.min}else{if(AT<=AU&&AT<Ae.min){if(AU<Ae.min){continue}AZ=(Ae.min-AU)/(AT-AU)*(AZ-Ab)+Ab;AT=Ae.min}}if(AU>=AT&&AU>Ae.max){if(AT>Ae.max){continue}Ab=(Ae.max-AU)/(AT-AU)*(AZ-Ab)+Ab;AU=Ae.max}else{if(AT>=AU&&AT>Ae.max){if(AU>Ae.max){continue}AZ=(Ae.max-AU)/(AT-AU)*(AZ-Ab)+Ab;AT=Ae.max}}if(!Ad){Y.beginPath();Y.moveTo(Ae.p2c(AU),Ac.p2c(AS));Ad=true}if(Ab>=Ac.max&&AZ>=Ac.max){Y.lineTo(Ae.p2c(AU),Ac.p2c(Ac.max));Y.lineTo(Ae.p2c(AT),Ac.p2c(Ac.max));AV=AT;continue}else{if(Ab<=Ac.min&&AZ<=Ac.min){Y.lineTo(Ae.p2c(AU),Ac.p2c(Ac.min));Y.lineTo(Ae.p2c(AT),Ac.p2c(Ac.min));AV=AT;continue}}var Ag=AU,AY=AT;if(Ab<=AZ&&Ab<Ac.min&&AZ>=Ac.min){AU=(Ac.min-Ab)/(AZ-Ab)*(AT-AU)+AU;Ab=Ac.min}else{if(AZ<=Ab&&AZ<Ac.min&&Ab>=Ac.min){AT=(Ac.min-Ab)/(AZ-Ab)*(AT-AU)+AU;AZ=Ac.min}}if(Ab>=AZ&&Ab>Ac.max&&AZ<=Ac.max){AU=(Ac.max-Ab)/(AZ-Ab)*(AT-AU)+AU;Ab=Ac.max}else{if(AZ>=Ab&&AZ>Ac.max&&Ab<=Ac.max){AT=(Ac.max-Ab)/(AZ-Ab)*(AT-AU)+AU;AZ=Ac.max}}if(AU!=Ag){if(Ab<=Ac.min){Aa=Ac.min}else{Aa=Ac.max}Y.lineTo(Ae.p2c(Ag),Ac.p2c(Aa));Y.lineTo(Ae.p2c(AU),Ac.p2c(Aa))}Y.lineTo(Ae.p2c(AU),Ac.p2c(Ab));Y.lineTo(Ae.p2c(AT),Ac.p2c(AZ));if(AT!=AY){if(AZ<=Ac.min){Aa=Ac.min}else{Aa=Ac.max}Y.lineTo(Ae.p2c(AT),Ac.p2c(Aa));Y.lineTo(Ae.p2c(AY),Ac.p2c(Aa))}AV=Math.max(AT,AY)}if(Ad){Y.lineTo(Ae.p2c(AV),Ac.p2c(AS));Y.fill()}}Y.save();Y.translate(e.left,e.top);Y.lineJoin="round";var AP=AN.lines.lineWidth,AK=AN.shadowSize;if(AP>0&&AK>0){Y.lineWidth=AK;Y.strokeStyle="rgba(0,0,0,0.1)";var AQ=Math.PI/18;AM(AN.datapoints,Math.sin(AQ)*(AP/2+AK/2),Math.cos(AQ)*(AP/2+AK/2),AN.xaxis,AN.yaxis);Y.lineWidth=AK/2;AM(AN.datapoints,Math.sin(AQ)*(AP/2+AK/4),Math.cos(AQ)*(AP/2+AK/4),AN.xaxis,AN.yaxis)}Y.lineWidth=AP;Y.strokeStyle=AN.color;var AL=V(AN.lines,AN.color,0,t);if(AL){Y.fillStyle=AL;AO(AN.datapoints,AN.xaxis,AN.yaxis)}if(AP>0){AM(AN.datapoints,0,0,AN.xaxis,AN.yaxis)}Y.restore()}function o(AN){function AP(AU,AT,Ab,AR,AV,AZ,AY){var Aa=AU.points,AQ=AU.pointsize;for(var AS=0;AS<Aa.length;AS+=AQ){var AX=Aa[AS],AW=Aa[AS+1];if(AX==null||AX<AZ.min||AX>AZ.max||AW<AY.min||AW>AY.max){continue}Y.beginPath();Y.arc(AZ.p2c(AX),AY.p2c(AW)+AR,AT,0,AV,false);if(Ab){Y.fillStyle=Ab;Y.fill()}Y.stroke()}}Y.save();Y.translate(e.left,e.top);var AO=AN.lines.lineWidth,AL=AN.shadowSize,AK=AN.points.radius;if(AO>0&&AL>0){var AM=AL/2;Y.lineWidth=AM;Y.strokeStyle="rgba(0,0,0,0.1)";AP(AN.datapoints,AK,null,AM+AM/2,Math.PI,AN.xaxis,AN.yaxis);Y.strokeStyle="rgba(0,0,0,0.2)";AP(AN.datapoints,AK,null,AM/2,Math.PI,AN.xaxis,AN.yaxis)}Y.lineWidth=AO;Y.strokeStyle=AN.color;AP(AN.datapoints,AK,V(AN.points,AN.color),0,2*Math.PI,AN.xaxis,AN.yaxis);Y.restore()}function AB(AV,AU,Ad,AQ,AY,AN,AL,AT,AS,Ac,AZ){var AM,Ab,AR,AX,AO,AK,AW,AP,Aa;if(AZ){AP=AK=AW=true;AO=false;AM=Ad;Ab=AV;AX=AU+AQ;AR=AU+AY;if(Ab<AM){Aa=Ab;Ab=AM;AM=Aa;AO=true;AK=false}}else{AO=AK=AW=true;AP=false;AM=AV+AQ;Ab=AV+AY;AR=Ad;AX=AU;if(AX<AR){Aa=AX;AX=AR;AR=Aa;AP=true;AW=false}}if(Ab<AT.min||AM>AT.max||AX<AS.min||AR>AS.max){return }if(AM<AT.min){AM=AT.min;AO=false}if(Ab>AT.max){Ab=AT.max;AK=false}if(AR<AS.min){AR=AS.min;AP=false}if(AX>AS.max){AX=AS.max;AW=false}AM=AT.p2c(AM);AR=AS.p2c(AR);Ab=AT.p2c(Ab);AX=AS.p2c(AX);if(AL){Ac.beginPath();Ac.moveTo(AM,AR);Ac.lineTo(AM,AX);Ac.lineTo(Ab,AX);Ac.lineTo(Ab,AR);Ac.fillStyle=AL(AR,AX);Ac.fill()}if(AO||AK||AW||AP){Ac.beginPath();Ac.moveTo(AM,AR+AN);if(AO){Ac.lineTo(AM,AX+AN)}else{Ac.moveTo(AM,AX+AN)}if(AW){Ac.lineTo(Ab,AX+AN)}else{Ac.moveTo(Ab,AX+AN)}if(AK){Ac.lineTo(Ab,AR+AN)}else{Ac.moveTo(Ab,AR+AN)}if(AP){Ac.lineTo(AM,AR+AN)}else{Ac.moveTo(AM,AR+AN)}Ac.stroke()}}function n(AM){function AL(AS,AR,AU,AP,AT,AW,AV){var AX=AS.points,AO=AS.pointsize;for(var AQ=0;AQ<AX.length;AQ+=AO){if(AX[AQ]==null){continue}AB(AX[AQ],AX[AQ+1],AX[AQ+2],AR,AU,AP,AT,AW,AV,Y,AM.bars.horizontal)}}Y.save();Y.translate(e.left,e.top);Y.lineWidth=AM.bars.lineWidth;Y.strokeStyle=AM.color;var AK=AM.bars.align=="left"?0:-AM.bars.barWidth/2;var AN=AM.bars.fill?function(AO,AP){return V(AM.bars,AM.color,AO,AP)}:null;AL(AM.datapoints,AK,AK+AM.bars.barWidth,0,AN,AM.xaxis,AM.yaxis);Y.restore()}function V(AM,AK,AL,AO){var AN=AM.fill;if(!AN){return null}if(AM.fillColor){return R(AM.fillColor,AL,AO,AK)}var AP=C.color.parse(AK);AP.a=typeof AN=="number"?AN:0.4;AP.normalize();return AP.toString()}function AI(){l.find(".legend").remove();if(!g.legend.show){return }var AP=[],AN=false,AV=g.legend.labelFormatter,AU,AR;for(i=0;i<O.length;++i){AU=O[i];AR=AU.label;if(!AR){continue}if(i%g.legend.noColumns==0){if(AN){AP.push("</tr>")}AP.push("<tr>");AN=true}if(AV){AR=AV(AR,AU)}AP.push('<td class="legendColorBox"><div style="border:1px solid '+g.legend.labelBoxBorderColor+';padding:1px"><div style="width:4px;height:0;border:5px solid '+AU.color+';overflow:hidden"></div></div></td><td class="legendLabel">'+AR+"</td>")}if(AN){AP.push("</tr>")}if(AP.length==0){return }var AT='<table style="font-size:smaller;color:'+g.grid.color+'">'+AP.join("")+"</table>";if(g.legend.container!=null){C(g.legend.container).html(AT)}else{var AQ="",AL=g.legend.position,AM=g.legend.margin;if(AM[0]==null){AM=[AM,AM]}if(AL.charAt(0)=="n"){AQ+="top:"+(AM[1]+e.top)+"px;"}else{if(AL.charAt(0)=="s"){AQ+="bottom:"+(AM[1]+e.bottom)+"px;"}}if(AL.charAt(1)=="e"){AQ+="right:"+(AM[0]+e.right)+"px;"}else{if(AL.charAt(1)=="w"){AQ+="left:"+(AM[0]+e.left)+"px;"}}var AS=C('<div class="legend">'+AT.replace('style="','style="position:absolute;'+AQ+";")+"</div>").appendTo(l);if(g.legend.backgroundOpacity!=0){var AO=g.legend.backgroundColor;if(AO==null){AO=g.grid.backgroundColor;if(AO&&typeof AO=="string"){AO=C.color.parse(AO)}else{AO=C.color.extract(AS,"background-color")}AO.a=1;AO=AO.toString()}var AK=AS.children();C('<div style="position:absolute;width:'+AK.width()+"px;height:"+AK.height()+"px;"+AQ+"background-color:"+AO+';"> </div>').prependTo(AS).css("opacity",g.legend.backgroundOpacity)}}}var w=[],J=null;function AF(AR,AP,AM){var AX=g.grid.mouseActiveRadius,Aj=AX*AX+1,Ah=null,Aa=false,Af,Ad;for(Af=0;Af<O.length;++Af){if(!AM(O[Af])){continue}var AY=O[Af],AQ=AY.xaxis,AO=AY.yaxis,Ae=AY.datapoints.points,Ac=AY.datapoints.pointsize,AZ=AQ.c2p(AR),AW=AO.c2p(AP),AL=AX/AQ.scale,AK=AX/AO.scale;if(AY.lines.show||AY.points.show){for(Ad=0;Ad<Ae.length;Ad+=Ac){var AT=Ae[Ad],AS=Ae[Ad+1];if(AT==null){continue}if(AT-AZ>AL||AT-AZ<-AL||AS-AW>AK||AS-AW<-AK){continue}var AV=Math.abs(AQ.p2c(AT)-AR),AU=Math.abs(AO.p2c(AS)-AP),Ab=AV*AV+AU*AU;if(Ab<=Aj){Aj=Ab;Ah=[Af,Ad/Ac]}}}if(AY.bars.show&&!Ah){var AN=AY.bars.align=="left"?0:-AY.bars.barWidth/2,Ag=AN+AY.bars.barWidth;for(Ad=0;Ad<Ae.length;Ad+=Ac){var AT=Ae[Ad],AS=Ae[Ad+1],Ai=Ae[Ad+2];if(AT==null){continue}if(O[Af].bars.horizontal?(AZ<=Math.max(Ai,AT)&&AZ>=Math.min(Ai,AT)&&AW>=AS+AN&&AW<=AS+Ag):(AZ>=AT+AN&&AZ<=AT+Ag&&AW>=Math.min(Ai,AS)&&AW<=Math.max(Ai,AS))){Ah=[Af,Ad/Ac]}}}}if(Ah){Af=Ah[0];Ad=Ah[1];Ac=O[Af].datapoints.pointsize;return{datapoint:O[Af].datapoints.points.slice(Ad*Ac,(Ad+1)*Ac),dataIndex:Ad,series:O[Af],seriesIndex:Af}}return null}function D(AK){if(g.grid.hoverable){H("plothover",AK,function(AL){return AL.hoverable!=false})}}function d(AK){H("plotclick",AK,function(AL){return AL.clickable!=false})}function H(AL,AK,AM){var AN=AD.offset(),AS={pageX:AK.pageX,pageY:AK.pageY},AQ=AK.pageX-AN.left-e.left,AO=AK.pageY-AN.top-e.top;if(s.xaxis.used){AS.x=s.xaxis.c2p(AQ)}if(s.yaxis.used){AS.y=s.yaxis.c2p(AO)}if(s.x2axis.used){AS.x2=s.x2axis.c2p(AQ)}if(s.y2axis.used){AS.y2=s.y2axis.c2p(AO)}var AT=AF(AQ,AO,AM);if(AT){AT.pageX=parseInt(AT.series.xaxis.p2c(AT.datapoint[0])+AN.left+e.left);AT.pageY=parseInt(AT.series.yaxis.p2c(AT.datapoint[1])+AN.top+e.top)}if(g.grid.autoHighlight){for(var AP=0;AP<w.length;++AP){var AR=w[AP];if(AR.auto==AL&&!(AT&&AR.series==AT.series&&AR.point==AT.datapoint)){x(AR.series,AR.point)}}if(AT){AE(AT.series,AT.datapoint,AL)}}l.trigger(AL,[AS,AT])}function q(){if(!J){J=setTimeout(v,30)}}function v(){J=null;AJ.save();AJ.clearRect(0,0,y,Q);AJ.translate(e.left,e.top);var AL,AK;for(AL=0;AL<w.length;++AL){AK=w[AL];if(AK.series.bars.show){z(AK.series,AK.point)}else{u(AK.series,AK.point)}}AJ.restore();Z(L.drawOverlay,[AJ])}function AE(AM,AK,AN){if(typeof AM=="number"){AM=O[AM]}if(typeof AK=="number"){AK=AM.data[AK]}var AL=j(AM,AK);if(AL==-1){w.push({series:AM,point:AK,auto:AN});q()}else{if(!AN){w[AL].auto=false}}}function x(AM,AK){if(AM==null&&AK==null){w=[];q()}if(typeof AM=="number"){AM=O[AM]}if(typeof AK=="number"){AK=AM.data[AK]}var AL=j(AM,AK);if(AL!=-1){w.splice(AL,1);q()}}function j(AM,AN){for(var AK=0;AK<w.length;++AK){var AL=w[AK];if(AL.series==AM&&AL.point[0]==AN[0]&&AL.point[1]==AN[1]){return AK}}return -1}function u(AN,AM){var AL=AM[0],AR=AM[1],AQ=AN.xaxis,AP=AN.yaxis;if(AL<AQ.min||AL>AQ.max||AR<AP.min||AR>AP.max){return }var AO=AN.points.radius+AN.points.lineWidth/2;AJ.lineWidth=AO;AJ.strokeStyle=C.color.parse(AN.color).scale("a",0.5).toString();var AK=1.5*AO;AJ.beginPath();AJ.arc(AQ.p2c(AL),AP.p2c(AR),AK,0,2*Math.PI,false);AJ.stroke()}function z(AN,AK){AJ.lineWidth=AN.bars.lineWidth;AJ.strokeStyle=C.color.parse(AN.color).scale("a",0.5).toString();var AM=C.color.parse(AN.color).scale("a",0.5).toString();var AL=AN.bars.align=="left"?0:-AN.bars.barWidth/2;AB(AK[0],AK[1],AK[2]||0,AL,AL+AN.bars.barWidth,0,function(){return AM},AN.xaxis,AN.yaxis,AJ,AN.bars.horizontal)}function R(AM,AL,AQ,AO){if(typeof AM=="string"){return AM}else{var AP=Y.createLinearGradient(0,AQ,0,AL);for(var AN=0,AK=AM.colors.length;AN<AK;++AN){var AR=AM.colors[AN];if(typeof AR!="string"){AR=C.color.parse(AO).scale("rgb",AR.brightness);AR.a*=AR.opacity;AR=AR.toString()}AP.addColorStop(AN/(AK-1),AR)}return AP}}}C.plot=function(G,E,D){var F=new B(C(G),E,D,C.plot.plugins);return F};C.plot.plugins=[];C.plot.formatDate=function(H,E,G){var L=function(N){N=""+N;return N.length==1?"0"+N:N};var D=[];var M=false;var K=H.getUTCHours();var I=K<12;if(G==null){G=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}if(E.search(/%p|%P/)!=-1){if(K>12){K=K-12}else{if(K==0){K=12}}}for(var F=0;F<E.length;++F){var J=E.charAt(F);if(M){switch(J){case"h":J=""+K;break;case"H":J=L(K);break;case"M":J=L(H.getUTCMinutes());break;case"S":J=L(H.getUTCSeconds());break;case"d":J=""+H.getUTCDate();break;case"m":J=""+(H.getUTCMonth()+1);break;case"y":J=""+H.getUTCFullYear();break;case"b":J=""+G[H.getUTCMonth()];break;case"p":J=(I)?("am"):("pm");break;case"P":J=(I)?("AM"):("PM");break}D.push(J);M=false}else{if(J=="%"){M=true}else{D.push(J)}}}return D.join("")};function A(E,D){return D*Math.floor(E/D)}})(jQuery);
+/* Javascript plotting library for jQuery, v. 0.6.
+ *
+ * Released under the MIT license by IOLA, December 2007.
+ *
+ */
+
+// first an inline dependency, jquery.colorhelpers.js, we inline it here
+// for convenience
+
+/* Plugin for jQuery for working with colors.
+ * 
+ * Version 1.0.
+ * 
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ *   $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ *   var c = $.color.extract($("#mydiv"), 'background-color');
+ *   console.log(c.r, c.g, c.b, c.a);
+ *   $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() work in-place instead of returning
+ * new objects.
+ */ 
+(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();
+
+// the actual Flot code
+(function($) {
+    function Plot(placeholder, data_, options_, plugins) {
+        // data is on the form:
+        //   [ series1, series2 ... ]
+        // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
+        // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
+        
+        var series = [],
+            options = {
+                // the color theme used for graphs
+                colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
+                legend: {
+                    show: true,
+                    noColumns: 1, // number of colums in legend table
+                    labelFormatter: null, // fn: string -> string
+                    labelBoxBorderColor: "#ccc", // border color for the little label boxes
+                    container: null, // container (as jQuery object) to put legend in, null means default on top of graph
+                    position: "ne", // position of default legend container within plot
+                    margin: 5, // distance from grid edge to default legend container within plot
+                    backgroundColor: null, // null means auto-detect
+                    backgroundOpacity: 0.85 // set to 0 to avoid background
+                },
+                xaxis: {
+                    mode: null, // null or "time"
+                    transform: null, // null or f: number -> number to transform axis
+                    inverseTransform: null, // if transform is set, this should be the inverse function
+                    min: null, // min. value to show, null means set automatically
+                    max: null, // max. value to show, null means set automatically
+                    autoscaleMargin: null, // margin in % to add if auto-setting min/max
+                    ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
+                    tickFormatter: null, // fn: number -> string
+                    labelWidth: null, // size of tick labels in pixels
+                    labelHeight: null,
+                    
+                    // mode specific options
+                    tickDecimals: null, // no. of decimals, null means auto
+                    tickSize: null, // number or [number, "unit"]
+                    minTickSize: null, // number or [number, "unit"]
+                    monthNames: null, // list of names of months
+                    timeformat: null, // format string to use
+                    twelveHourClock: false // 12 or 24 time in time mode
+                },
+                yaxis: {
+                    autoscaleMargin: 0.02
+                },
+                x2axis: {
+                    autoscaleMargin: null
+                },
+                y2axis: {
+                    autoscaleMargin: 0.02
+                },
+                series: {
+                    points: {
+                        show: false,
+                        radius: 3,
+                        lineWidth: 2, // in pixels
+                        fill: true,
+                        fillColor: "#ffffff"
+                    },
+                    lines: {
+                        // we don't put in show: false so we can see
+                        // whether lines were actively disabled 
+                        lineWidth: 2, // in pixels
+                        fill: false,
+                        fillColor: null,
+                        steps: false
+                    },
+                    bars: {
+                        show: false,
+                        lineWidth: 2, // in pixels
+                        barWidth: 1, // in units of the x axis
+                        fill: true,
+                        fillColor: null,
+                        align: "left", // or "center" 
+                        horizontal: false // when horizontal, left is now top
+                    },
+                    shadowSize: 3
+                },
+                grid: {
+                    show: true,
+                    aboveData: false,
+                    color: "#545454", // primary color used for outline and labels
+                    backgroundColor: null, // null for transparent, else color
+                    tickColor: "rgba(0,0,0,0.15)", // color used for the ticks
+                    labelMargin: 5, // in pixels
+                    borderWidth: 2, // in pixels
+                    borderColor: null, // set if different from the grid color
+                    markings: null, // array of ranges or fn: axes -> array of ranges
+                    markingsColor: "#f4f4f4",
+                    markingsLineWidth: 2,
+                    // interactive stuff
+                    clickable: false,
+                    hoverable: false,
+                    autoHighlight: true, // highlight in case mouse is near
+                    mouseActiveRadius: 10 // how far the mouse can be away to activate an item
+                },
+                hooks: {}
+            },
+        canvas = null,      // the canvas for the plot itself
+        overlay = null,     // canvas for interactive stuff on top of plot
+        eventHolder = null, // jQuery object that events should be bound to
+        ctx = null, octx = null,
+        axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} },
+        plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
+        canvasWidth = 0, canvasHeight = 0,
+        plotWidth = 0, plotHeight = 0,
+        hooks = {
+            processOptions: [],
+            processRawData: [],
+            processDatapoints: [],
+            draw: [],
+            bindEvents: [],
+            drawOverlay: []
+        },
+        plot = this;
+
+        // public functions
+        plot.setData = setData;
+        plot.setupGrid = setupGrid;
+        plot.draw = draw;
+        plot.getPlaceholder = function() { return placeholder; };
+        plot.getCanvas = function() { return canvas; };
+        plot.getPlotOffset = function() { return plotOffset; };
+        plot.width = function () { return plotWidth; };
+        plot.height = function () { return plotHeight; };
+        plot.offset = function () {
+            var o = eventHolder.offset();
+            o.left += plotOffset.left;
+            o.top += plotOffset.top;
+            return o;
+        };
+        plot.getData = function() { return series; };
+        plot.getAxes = function() { return axes; };
+        plot.getOptions = function() { return options; };
+        plot.highlight = highlight;
+        plot.unhighlight = unhighlight;
+        plot.triggerRedrawOverlay = triggerRedrawOverlay;
+        plot.pointOffset = function(point) {
+            return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left),
+                     top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) };
+        };
+        
+
+        // public attributes
+        plot.hooks = hooks;
+        
+        // initialize
+        initPlugins(plot);
+        parseOptions(options_);
+        constructCanvas();
+        setData(data_);
+        setupGrid();
+        draw();
+        bindEvents();
+
+
+        function executeHooks(hook, args) {
+            args = [plot].concat(args);
+            for (var i = 0; i < hook.length; ++i)
+                hook[i].apply(this, args);
+        }
+
+        function initPlugins() {
+            for (var i = 0; i < plugins.length; ++i) {
+                var p = plugins[i];
+                p.init(plot);
+                if (p.options)
+                    $.extend(true, options, p.options);
+            }
+        }
+        
+        function parseOptions(opts) {
+            $.extend(true, options, opts);
+            if (options.grid.borderColor == null)
+                options.grid.borderColor = options.grid.color;
+            // backwards compatibility, to be removed in future
+            if (options.xaxis.noTicks && options.xaxis.ticks == null)
+                options.xaxis.ticks = options.xaxis.noTicks;
+            if (options.yaxis.noTicks && options.yaxis.ticks == null)
+                options.yaxis.ticks = options.yaxis.noTicks;
+            if (options.grid.coloredAreas)
+                options.grid.markings = options.grid.coloredAreas;
+            if (options.grid.coloredAreasColor)
+                options.grid.markingsColor = options.grid.coloredAreasColor;
+            if (options.lines)
+                $.extend(true, options.series.lines, options.lines);
+            if (options.points)
+                $.extend(true, options.series.points, options.points);
+            if (options.bars)
+                $.extend(true, options.series.bars, options.bars);
+            if (options.shadowSize)
+                options.series.shadowSize = options.shadowSize;
+
+            for (var n in hooks)
+                if (options.hooks[n] && options.hooks[n].length)
+                    hooks[n] = hooks[n].concat(options.hooks[n]);
+
+            executeHooks(hooks.processOptions, [options]);
+        }
+
+        function setData(d) {
+            series = parseData(d);
+            fillInSeriesOptions();
+            processData();
+        }
+        
+        function parseData(d) {
+            var res = [];
+            for (var i = 0; i < d.length; ++i) {
+                var s = $.extend(true, {}, options.series);
+
+                if (d[i].data) {
+                    s.data = d[i].data; // move the data instead of deep-copy
+                    delete d[i].data;
+
+                    $.extend(true, s, d[i]);
+
+                    d[i].data = s.data;
+                }
+                else
+                    s.data = d[i];
+                res.push(s);
+            }
+
+            return res;
+        }
+        
+        function axisSpecToRealAxis(obj, attr) {
+            var a = obj[attr];
+            if (!a || a == 1)
+                return axes[attr];
+            if (typeof a == "number")
+                return axes[attr.charAt(0) + a + attr.slice(1)];
+            return a; // assume it's OK
+        }
+        
+        function fillInSeriesOptions() {
+            var i;
+            
+            // collect what we already got of colors
+            var neededColors = series.length,
+                usedColors = [],
+                assignedColors = [];
+            for (i = 0; i < series.length; ++i) {
+                var sc = series[i].color;
+                if (sc != null) {
+                    --neededColors;
+                    if (typeof sc == "number")
+                        assignedColors.push(sc);
+                    else
+                        usedColors.push($.color.parse(series[i].color));
+                }
+            }
+            
+            // we might need to generate more colors if higher indices
+            // are assigned
+            for (i = 0; i < assignedColors.length; ++i) {
+                neededColors = Math.max(neededColors, assignedColors[i] + 1);
+            }
+
+            // produce colors as needed
+            var colors = [], variation = 0;
+            i = 0;
+            while (colors.length < neededColors) {
+                var c;
+                if (options.colors.length == i) // check degenerate case
+                    c = $.color.make(100, 100, 100);
+                else
+                    c = $.color.parse(options.colors[i]);
+
+                // vary color if needed
+                var sign = variation % 2 == 1 ? -1 : 1;
+                c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
+
+                // FIXME: if we're getting to close to something else,
+                // we should probably skip this one
+                colors.push(c);
+                
+                ++i;
+                if (i >= options.colors.length) {
+                    i = 0;
+                    ++variation;
+                }
+            }
+
+            // fill in the options
+            var colori = 0, s;
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                // assign colors
+                if (s.color == null) {
+                    s.color = colors[colori].toString();
+                    ++colori;
+                }
+                else if (typeof s.color == "number")
+                    s.color = colors[s.color].toString();
+
+                // turn on lines automatically in case nothing is set
+                if (s.lines.show == null) {
+                    var v, show = true;
+                    for (v in s)
+                        if (s[v].show) {
+                            show = false;
+                            break;
+                        }
+                    if (show)
+                        s.lines.show = true;
+                }
+
+                // setup axes
+                s.xaxis = axisSpecToRealAxis(s, "xaxis");
+                s.yaxis = axisSpecToRealAxis(s, "yaxis");
+            }
+        }
+        
+        function processData() {
+            var topSentry = Number.POSITIVE_INFINITY,
+                bottomSentry = Number.NEGATIVE_INFINITY,
+                i, j, k, m, length,
+                s, points, ps, x, y, axis, val, f, p;
+
+            for (axis in axes) {
+                axes[axis].datamin = topSentry;
+                axes[axis].datamax = bottomSentry;
+                axes[axis].used = false;
+            }
+
+            function updateAxis(axis, min, max) {
+                if (min < axis.datamin)
+                    axis.datamin = min;
+                if (max > axis.datamax)
+                    axis.datamax = max;
+            }
+
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                s.datapoints = { points: [] };
+                
+                executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
+            }
+            
+            // first pass: clean and copy data
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+
+                var data = s.data, format = s.datapoints.format;
+
+                if (!format) {
+                    format = [];
+                    // find out how to copy
+                    format.push({ x: true, number: true, required: true });
+                    format.push({ y: true, number: true, required: true });
+
+                    if (s.bars.show)
+                        format.push({ y: true, number: true, required: false, defaultValue: 0 });
+                    
+                    s.datapoints.format = format;
+                }
+
+                if (s.datapoints.pointsize != null)
+                    continue; // already filled in
+
+                if (s.datapoints.pointsize == null)
+                    s.datapoints.pointsize = format.length;
+                
+                ps = s.datapoints.pointsize;
+                points = s.datapoints.points;
+
+                insertSteps = s.lines.show && s.lines.steps;
+                s.xaxis.used = s.yaxis.used = true;
+                
+                for (j = k = 0; j < data.length; ++j, k += ps) {
+                    p = data[j];
+
+                    var nullify = p == null;
+                    if (!nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = p[m];
+                            f = format[m];
+
+                            if (f) {
+                                if (f.number && val != null) {
+                                    val = +val; // convert to number
+                                    if (isNaN(val))
+                                        val = null;
+                                }
+
+                                if (val == null) {
+                                    if (f.required)
+                                        nullify = true;
+                                    
+                                    if (f.defaultValue != null)
+                                        val = f.defaultValue;
+                                }
+                            }
+                            
+                            points[k + m] = val;
+                        }
+                    }
+                    
+                    if (nullify) {
+                        for (m = 0; m < ps; ++m) {
+                            val = points[k + m];
+                            if (val != null) {
+                                f = format[m];
+                                // extract min/max info
+                                if (f.x)
+                                    updateAxis(s.xaxis, val, val);
+                                if (f.y)
+                                    updateAxis(s.yaxis, val, val);
+                            }
+                            points[k + m] = null;
+                        }
+                    }
+                    else {
+                        // a little bit of line specific stuff that
+                        // perhaps shouldn't be here, but lacking
+                        // better means...
+                        if (insertSteps && k > 0
+                            && points[k - ps] != null
+                            && points[k - ps] != points[k]
+                            && points[k - ps + 1] != points[k + 1]) {
+                            // copy the point to make room for a middle point
+                            for (m = 0; m < ps; ++m)
+                                points[k + ps + m] = points[k + m];
+
+                            // middle point has same y
+                            points[k + 1] = points[k - ps + 1];
+
+                            // we've added a point, better reflect that
+                            k += ps;
+                        }
+                    }
+                }
+            }
+
+            // give the hooks a chance to run
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                
+                executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
+            }
+
+            // second pass: find datamax/datamin for auto-scaling
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                points = s.datapoints.points,
+                ps = s.datapoints.pointsize;
+
+                var xmin = topSentry, ymin = topSentry,
+                    xmax = bottomSentry, ymax = bottomSentry;
+                
+                for (j = 0; j < points.length; j += ps) {
+                    if (points[j] == null)
+                        continue;
+
+                    for (m = 0; m < ps; ++m) {
+                        val = points[j + m];
+                        f = format[m];
+                        if (!f)
+                            continue;
+                        
+                        if (f.x) {
+                            if (val < xmin)
+                                xmin = val;
+                            if (val > xmax)
+                                xmax = val;
+                        }
+                        if (f.y) {
+                            if (val < ymin)
+                                ymin = val;
+                            if (val > ymax)
+                                ymax = val;
+                        }
+                    }
+                }
+                
+                if (s.bars.show) {
+                    // make sure we got room for the bar on the dancing floor
+                    var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
+                    if (s.bars.horizontal) {
+                        ymin += delta;
+                        ymax += delta + s.bars.barWidth;
+                    }
+                    else {
+                        xmin += delta;
+                        xmax += delta + s.bars.barWidth;
+                    }
+                }
+                
+                updateAxis(s.xaxis, xmin, xmax);
+                updateAxis(s.yaxis, ymin, ymax);
+            }
+
+            for (axis in axes) {
+                if (axes[axis].datamin == topSentry)
+                    axes[axis].datamin = null;
+                if (axes[axis].datamax == bottomSentry)
+                    axes[axis].datamax = null;
+            }
+        }
+
+        function constructCanvas() {
+            function makeCanvas(width, height) {
+                var c = document.createElement('canvas');
+                c.width = width;
+                c.height = height;
+                if ($.browser.msie) // excanvas hack
+                    c = window.G_vmlCanvasManager.initElement(c);
+                return c;
+            }
+            
+            canvasWidth = placeholder.width();
+            canvasHeight = placeholder.height();
+            placeholder.html(""); // clear placeholder
+            if (placeholder.css("position") == 'static')
+                placeholder.css("position", "relative"); // for positioning labels and overlay
+
+            if (canvasWidth <= 0 || canvasHeight <= 0)
+                throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
+
+            if ($.browser.msie) // excanvas hack
+                window.G_vmlCanvasManager.init_(document); // make sure everything is setup
+            
+            // the canvas
+            canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0);
+            ctx = canvas.getContext("2d");
+
+            // overlay canvas for interactive features
+            overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0);
+            octx = overlay.getContext("2d");
+            octx.stroke();
+        }
+
+        function bindEvents() {
+            // we include the canvas in the event holder too, because IE 7
+            // sometimes has trouble with the stacking order
+            eventHolder = $([overlay, canvas]);
+
+            // bind events
+            if (options.grid.hoverable)
+                eventHolder.mousemove(onMouseMove);
+
+            if (options.grid.clickable)
+                eventHolder.click(onClick);
+
+            executeHooks(hooks.bindEvents, [eventHolder]);
+        }
+
+        function setupGrid() {
+            function setTransformationHelpers(axis, o) {
+                function identity(x) { return x; }
+                
+                var s, m, t = o.transform || identity,
+                    it = o.inverseTransform;
+                    
+                // add transformation helpers
+                if (axis == axes.xaxis || axis == axes.x2axis) {
+                    // precompute how much the axis is scaling a point
+                    // in canvas space
+                    s = axis.scale = plotWidth / (t(axis.max) - t(axis.min));
+                    m = t(axis.min);
+
+                    // data point to canvas coordinate
+                    if (t == identity) // slight optimization
+                        axis.p2c = function (p) { return (p - m) * s; };
+                    else
+                        axis.p2c = function (p) { return (t(p) - m) * s; };
+                    // canvas coordinate to data point
+                    if (!it)
+                        axis.c2p = function (c) { return m + c / s; };
+                    else
+                        axis.c2p = function (c) { return it(m + c / s); };
+                }
+                else {
+                    s = axis.scale = plotHeight / (t(axis.max) - t(axis.min));
+                    m = t(axis.max);
+                    
+                    if (t == identity)
+                        axis.p2c = function (p) { return (m - p) * s; };
+                    else
+                        axis.p2c = function (p) { return (m - t(p)) * s; };
+                    if (!it)
+                        axis.c2p = function (c) { return m - c / s; };
+                    else
+                        axis.c2p = function (c) { return it(m - c / s); };
+                }
+            }
+
+            function measureLabels(axis, axisOptions) {
+                var i, labels = [], l;
+                
+                axis.labelWidth = axisOptions.labelWidth;
+                axis.labelHeight = axisOptions.labelHeight;
+
+                if (axis == axes.xaxis || axis == axes.x2axis) {
+                    // to avoid measuring the widths of the labels, we
+                    // construct fixed-size boxes and put the labels inside
+                    // them, we don't need the exact figures and the
+                    // fixed-size box content is easy to center
+                    if (axis.labelWidth == null)
+                        axis.labelWidth = canvasWidth / (axis.ticks.length > 0 ? axis.ticks.length : 1);
+
+                    // measure x label heights
+                    if (axis.labelHeight == null) {
+                        labels = [];
+                        for (i = 0; i < axis.ticks.length; ++i) {
+                            l = axis.ticks[i].label;
+                            if (l)
+                                labels.push('<div class="tickLabel" style="float:left;width:' + axis.labelWidth + 'px">' + l + '</div>');
+                        }
+                        
+                        if (labels.length > 0) {
+                            var dummyDiv = $('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">'
+                                             + labels.join("") + '<div style="clear:left"></div></div>').appendTo(placeholder);
+                            axis.labelHeight = dummyDiv.height();
+                            dummyDiv.remove();
+                        }
+                    }
+                }
+                else if (axis.labelWidth == null || axis.labelHeight == null) {
+                    // calculate y label dimensions
+                    for (i = 0; i < axis.ticks.length; ++i) {
+                        l = axis.ticks[i].label;
+                        if (l)
+                            labels.push('<div class="tickLabel">' + l + '</div>');
+                    }
+                    
+                    if (labels.length > 0) {
+                        var dummyDiv = $('<div style="position:absolute;top:-10000px;font-size:smaller">'
+                                         + labels.join("") + '</div>').appendTo(placeholder);
+                        if (axis.labelWidth == null)
+                            axis.labelWidth = dummyDiv.width();
+                        if (axis.labelHeight == null)
+                            axis.labelHeight = dummyDiv.find("div").height();
+                        dummyDiv.remove();
+                    }
+                    
+                }
+
+                if (axis.labelWidth == null)
+                    axis.labelWidth = 0;
+                if (axis.labelHeight == null)
+                    axis.labelHeight = 0;
+            }
+            
+            function setGridSpacing() {
+                // get the most space needed around the grid for things
+                // that may stick out
+                var maxOutset = options.grid.borderWidth;
+                for (i = 0; i < series.length; ++i)
+                    maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
+                
+                plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset;
+                
+                var margin = options.grid.labelMargin + options.grid.borderWidth;
+                
+                if (axes.xaxis.labelHeight > 0)
+                    plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin);
+                if (axes.yaxis.labelWidth > 0)
+                    plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin);
+                if (axes.x2axis.labelHeight > 0)
+                    plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin);
+                if (axes.y2axis.labelWidth > 0)
+                    plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin);
+            
+                plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
+                plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
+            }
+            
+            var axis;
+            for (axis in axes)
+                setRange(axes[axis], options[axis]);
+            
+            if (options.grid.show) {
+                for (axis in axes) {
+                    prepareTickGeneration(axes[axis], options[axis]);
+                    setTicks(axes[axis], options[axis]);
+                    measureLabels(axes[axis], options[axis]);
+                }
+
+                setGridSpacing();
+            }
+            else {
+                plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
+                plotWidth = canvasWidth;
+                plotHeight = canvasHeight;
+            }
+            
+            for (axis in axes)
+                setTransformationHelpers(axes[axis], options[axis]);
+
+            if (options.grid.show)
+                insertLabels();
+            
+            insertLegend();
+        }
+        
+        function setRange(axis, axisOptions) {
+            var min = +(axisOptions.min != null ? axisOptions.min : axis.datamin),
+                max = +(axisOptions.max != null ? axisOptions.max : axis.datamax),
+                delta = max - min;
+
+            if (delta == 0.0) {
+                // degenerate case
+                var widen = max == 0 ? 1 : 0.01;
+
+                if (axisOptions.min == null)
+                    min -= widen;
+                // alway widen max if we couldn't widen min to ensure we
+                // don't fall into min == max which doesn't work
+                if (axisOptions.max == null || axisOptions.min != null)
+                    max += widen;
+            }
+            else {
+                // consider autoscaling
+                var margin = axisOptions.autoscaleMargin;
+                if (margin != null) {
+                    if (axisOptions.min == null) {
+                        min -= delta * margin;
+                        // make sure we don't go below zero if all values
+                        // are positive
+                        if (min < 0 && axis.datamin != null && axis.datamin >= 0)
+                            min = 0;
+                    }
+                    if (axisOptions.max == null) {
+                        max += delta * margin;
+                        if (max > 0 && axis.datamax != null && axis.datamax <= 0)
+                            max = 0;
+                    }
+                }
+            }
+            axis.min = min;
+            axis.max = max;
+        }
+
+        function prepareTickGeneration(axis, axisOptions) {
+            // estimate number of ticks
+            var noTicks;
+            if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0)
+                noTicks = axisOptions.ticks;
+            else if (axis == axes.xaxis || axis == axes.x2axis)
+                 // heuristic based on the model a*sqrt(x) fitted to
+                 // some reasonable data points
+                noTicks = 0.3 * Math.sqrt(canvasWidth);
+            else
+                noTicks = 0.3 * Math.sqrt(canvasHeight);
+            
+            var delta = (axis.max - axis.min) / noTicks,
+                size, generator, unit, formatter, i, magn, norm;
+
+            if (axisOptions.mode == "time") {
+                // pretty handling of time
+                
+                // map of app. size of time units in milliseconds
+                var timeUnitSize = {
+                    "second": 1000,
+                    "minute": 60 * 1000,
+                    "hour": 60 * 60 * 1000,
+                    "day": 24 * 60 * 60 * 1000,
+                    "month": 30 * 24 * 60 * 60 * 1000,
+                    "year": 365.2425 * 24 * 60 * 60 * 1000
+                };
+
+
+                // the allowed tick sizes, after 1 year we use
+                // an integer algorithm
+                var spec = [
+                    [1, "second"], [2, "second"], [5, "second"], [10, "second"],
+                    [30, "second"], 
+                    [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
+                    [30, "minute"], 
+                    [1, "hour"], [2, "hour"], [4, "hour"],
+                    [8, "hour"], [12, "hour"],
+                    [1, "day"], [2, "day"], [3, "day"],
+                    [0.25, "month"], [0.5, "month"], [1, "month"],
+                    [2, "month"], [3, "month"], [6, "month"],
+                    [1, "year"]
+                ];
+
+                var minSize = 0;
+                if (axisOptions.minTickSize != null) {
+                    if (typeof axisOptions.tickSize == "number")
+                        minSize = axisOptions.tickSize;
+                    else
+                        minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
+                }
+
+                for (i = 0; i < spec.length - 1; ++i)
+                    if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+                                 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
+                       && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
+                        break;
+                size = spec[i][0];
+                unit = spec[i][1];
+                
+                // special-case the possibility of several years
+                if (unit == "year") {
+                    magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
+                    norm = (delta / timeUnitSize.year) / magn;
+                    if (norm < 1.5)
+                        size = 1;
+                    else if (norm < 3)
+                        size = 2;
+                    else if (norm < 7.5)
+                        size = 5;
+                    else
+                        size = 10;
+
+                    size *= magn;
+                }
+
+                if (axisOptions.tickSize) {
+                    size = axisOptions.tickSize[0];
+                    unit = axisOptions.tickSize[1];
+                }
+                
+                generator = function(axis) {
+                    var ticks = [],
+                        tickSize = axis.tickSize[0], unit = axis.tickSize[1],
+                        d = new Date(axis.min);
+                    
+                    var step = tickSize * timeUnitSize[unit];
+
+                    if (unit == "second")
+                        d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
+                    if (unit == "minute")
+                        d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
+                    if (unit == "hour")
+                        d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
+                    if (unit == "month")
+                        d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
+                    if (unit == "year")
+                        d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
+                    
+                    // reset smaller components
+                    d.setUTCMilliseconds(0);
+                    if (step >= timeUnitSize.minute)
+                        d.setUTCSeconds(0);
+                    if (step >= timeUnitSize.hour)
+                        d.setUTCMinutes(0);
+                    if (step >= timeUnitSize.day)
+                        d.setUTCHours(0);
+                    if (step >= timeUnitSize.day * 4)
+                        d.setUTCDate(1);
+                    if (step >= timeUnitSize.year)
+                        d.setUTCMonth(0);
+
+
+                    var carry = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = d.getTime();
+                        ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
+                        if (unit == "month") {
+                            if (tickSize < 1) {
+                                // a bit complicated - we'll divide the month
+                                // up but we need to take care of fractions
+                                // so we don't end up in the middle of a day
+                                d.setUTCDate(1);
+                                var start = d.getTime();
+                                d.setUTCMonth(d.getUTCMonth() + 1);
+                                var end = d.getTime();
+                                d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
+                                carry = d.getUTCHours();
+                                d.setUTCHours(0);
+                            }
+                            else
+                                d.setUTCMonth(d.getUTCMonth() + tickSize);
+                        }
+                        else if (unit == "year") {
+                            d.setUTCFullYear(d.getUTCFullYear() + tickSize);
+                        }
+                        else
+                            d.setTime(v + step);
+                    } while (v < axis.max && v != prev);
+
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    var d = new Date(v);
+
+                    // first check global format
+                    if (axisOptions.timeformat != null)
+                        return $.plot.formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
+                    
+                    var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
+                    var span = axis.max - axis.min;
+                    var suffix = (axisOptions.twelveHourClock) ? " %p" : "";
+                    
+                    if (t < timeUnitSize.minute)
+                        fmt = "%h:%M:%S" + suffix;
+                    else if (t < timeUnitSize.day) {
+                        if (span < 2 * timeUnitSize.day)
+                            fmt = "%h:%M" + suffix;
+                        else
+                            fmt = "%b %d %h:%M" + suffix;
+                    }
+                    else if (t < timeUnitSize.month)
+                        fmt = "%b %d";
+                    else if (t < timeUnitSize.year) {
+                        if (span < timeUnitSize.year)
+                            fmt = "%b";
+                        else
+                            fmt = "%b %y";
+                    }
+                    else
+                        fmt = "%y";
+                    
+                    return $.plot.formatDate(d, fmt, axisOptions.monthNames);
+                };
+            }
+            else {
+                // pretty rounding of base-10 numbers
+                var maxDec = axisOptions.tickDecimals;
+                var dec = -Math.floor(Math.log(delta) / Math.LN10);
+                if (maxDec != null && dec > maxDec)
+                    dec = maxDec;
+
+                magn = Math.pow(10, -dec);
+                norm = delta / magn; // norm is between 1.0 and 10.0
+                
+                if (norm < 1.5)
+                    size = 1;
+                else if (norm < 3) {
+                    size = 2;
+                    // special case for 2.5, requires an extra decimal
+                    if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
+                        size = 2.5;
+                        ++dec;
+                    }
+                }
+                else if (norm < 7.5)
+                    size = 5;
+                else
+                    size = 10;
+
+                size *= magn;
+                
+                if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
+                    size = axisOptions.minTickSize;
+
+                if (axisOptions.tickSize != null)
+                    size = axisOptions.tickSize;
+
+                axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
+
+                generator = function (axis) {
+                    var ticks = [];
+
+                    // spew out all possible ticks
+                    var start = floorInBase(axis.min, axis.tickSize),
+                        i = 0, v = Number.NaN, prev;
+                    do {
+                        prev = v;
+                        v = start + i * axis.tickSize;
+                        ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
+                        ++i;
+                    } while (v < axis.max && v != prev);
+                    return ticks;
+                };
+
+                formatter = function (v, axis) {
+                    return v.toFixed(axis.tickDecimals);
+                };
+            }
+
+            axis.tickSize = unit ? [size, unit] : size;
+            axis.tickGenerator = generator;
+            if ($.isFunction(axisOptions.tickFormatter))
+                axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
+            else
+                axis.tickFormatter = formatter;
+        }
+        
+        function setTicks(axis, axisOptions) {
+            axis.ticks = [];
+
+            if (!axis.used)
+                return;
+            
+            if (axisOptions.ticks == null)
+                axis.ticks = axis.tickGenerator(axis);
+            else if (typeof axisOptions.ticks == "number") {
+                if (axisOptions.ticks > 0)
+                    axis.ticks = axis.tickGenerator(axis);
+            }
+            else if (axisOptions.ticks) {
+                var ticks = axisOptions.ticks;
+
+                if ($.isFunction(ticks))
+                    // generate the ticks
+                    ticks = ticks({ min: axis.min, max: axis.max });
+                
+                // clean up the user-supplied ticks, copy them over
+                var i, v;
+                for (i = 0; i < ticks.length; ++i) {
+                    var label = null;
+                    var t = ticks[i];
+                    if (typeof t == "object") {
+                        v = t[0];
+                        if (t.length > 1)
+                            label = t[1];
+                    }
+                    else
+                        v = t;
+                    if (label == null)
+                        label = axis.tickFormatter(v, axis);
+                    axis.ticks[i] = { v: v, label: label };
+                }
+            }
+
+            if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) {
+                // snap to ticks
+                if (axisOptions.min == null)
+                    axis.min = Math.min(axis.min, axis.ticks[0].v);
+                if (axisOptions.max == null && axis.ticks.length > 1)
+                    axis.max = Math.max(axis.max, axis.ticks[axis.ticks.length - 1].v);
+            }
+        }
+      
+        function draw() {
+            ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+            var grid = options.grid;
+            
+            if (grid.show && !grid.aboveData)
+                drawGrid();
+
+            for (var i = 0; i < series.length; ++i)
+                drawSeries(series[i]);
+
+            executeHooks(hooks.draw, [ctx]);
+            
+            if (grid.show && grid.aboveData)
+                drawGrid();
+        }
+
+        function extractRange(ranges, coord) {
+            var firstAxis = coord + "axis",
+                secondaryAxis = coord + "2axis",
+                axis, from, to, reverse;
+
+            if (ranges[firstAxis]) {
+                axis = axes[firstAxis];
+                from = ranges[firstAxis].from;
+                to = ranges[firstAxis].to;
+            }
+            else if (ranges[secondaryAxis]) {
+                axis = axes[secondaryAxis];
+                from = ranges[secondaryAxis].from;
+                to = ranges[secondaryAxis].to;
+            }
+            else {
+                // backwards-compat stuff - to be removed in future
+                axis = axes[firstAxis];
+                from = ranges[coord + "1"];
+                to = ranges[coord + "2"];
+            }
+
+            // auto-reverse as an added bonus
+            if (from != null && to != null && from > to)
+                return { from: to, to: from, axis: axis };
+            
+            return { from: from, to: to, axis: axis };
+        }
+        
+        function drawGrid() {
+            var i;
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // draw background, if any
+            if (options.grid.backgroundColor) {
+                ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
+                ctx.fillRect(0, 0, plotWidth, plotHeight);
+            }
+
+            // draw markings
+            var markings = options.grid.markings;
+            if (markings) {
+                if ($.isFunction(markings))
+                    // xmin etc. are backwards-compatible, to be removed in future
+                    markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis });
+
+                for (i = 0; i < markings.length; ++i) {
+                    var m = markings[i],
+                        xrange = extractRange(m, "x"),
+                        yrange = extractRange(m, "y");
+
+                    // fill in missing
+                    if (xrange.from == null)
+                        xrange.from = xrange.axis.min;
+                    if (xrange.to == null)
+                        xrange.to = xrange.axis.max;
+                    if (yrange.from == null)
+                        yrange.from = yrange.axis.min;
+                    if (yrange.to == null)
+                        yrange.to = yrange.axis.max;
+
+                    // clip
+                    if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
+                        yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
+                        continue;
+
+                    xrange.from = Math.max(xrange.from, xrange.axis.min);
+                    xrange.to = Math.min(xrange.to, xrange.axis.max);
+                    yrange.from = Math.max(yrange.from, yrange.axis.min);
+                    yrange.to = Math.min(yrange.to, yrange.axis.max);
+
+                    if (xrange.from == xrange.to && yrange.from == yrange.to)
+                        continue;
+
+                    // then draw
+                    xrange.from = xrange.axis.p2c(xrange.from);
+                    xrange.to = xrange.axis.p2c(xrange.to);
+                    yrange.from = yrange.axis.p2c(yrange.from);
+                    yrange.to = yrange.axis.p2c(yrange.to);
+                    
+                    if (xrange.from == xrange.to || yrange.from == yrange.to) {
+                        // draw line
+                        ctx.beginPath();
+                        ctx.strokeStyle = m.color || options.grid.markingsColor;
+                        ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
+                        //ctx.moveTo(Math.floor(xrange.from), yrange.from);
+                        //ctx.lineTo(Math.floor(xrange.to), yrange.to);
+                        ctx.moveTo(xrange.from, yrange.from);
+                        ctx.lineTo(xrange.to, yrange.to);
+                        ctx.stroke();
+                    }
+                    else {
+                        // fill area
+                        ctx.fillStyle = m.color || options.grid.markingsColor;
+                        ctx.fillRect(xrange.from, yrange.to,
+                                     xrange.to - xrange.from,
+                                     yrange.from - yrange.to);
+                    }
+                }
+            }
+            
+            // draw the inner grid
+            ctx.lineWidth = 1;
+            ctx.strokeStyle = options.grid.tickColor;
+            ctx.beginPath();
+            var v, axis = axes.xaxis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axes.xaxis.max)
+                    continue;   // skip those lying on the axes
+
+                ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0);
+                ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight);
+            }
+
+            axis = axes.yaxis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axis.max)
+                    continue;
+
+                ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+                ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+            }
+
+            axis = axes.x2axis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axis.max)
+                    continue;
+    
+                ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5);
+                ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5);
+            }
+
+            axis = axes.y2axis;
+            for (i = 0; i < axis.ticks.length; ++i) {
+                v = axis.ticks[i].v;
+                if (v <= axis.min || v >= axis.max)
+                    continue;
+
+                ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+                ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
+            }
+            
+            ctx.stroke();
+            
+            if (options.grid.borderWidth) {
+                // draw border
+                var bw = options.grid.borderWidth;
+                ctx.lineWidth = bw;
+                ctx.strokeStyle = options.grid.borderColor;
+                ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
+            }
+
+            ctx.restore();
+        }
+
+        function insertLabels() {
+            placeholder.find(".tickLabels").remove();
+            
+            var html = ['<div class="tickLabels" style="font-size:smaller;color:' + options.grid.color + '">'];
+
+            function addLabels(axis, labelGenerator) {
+                for (var i = 0; i < axis.ticks.length; ++i) {
+                    var tick = axis.ticks[i];
+                    if (!tick.label || tick.v < axis.min || tick.v > axis.max)
+                        continue;
+                    html.push(labelGenerator(tick, axis));
+                }
+            }
+
+            var margin = options.grid.labelMargin + options.grid.borderWidth;
+            
+            addLabels(axes.xaxis, function (tick, axis) {
+                return '<div style="position:absolute;top:' + (plotOffset.top + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
+            });
+            
+            
+            addLabels(axes.yaxis, function (tick, axis) {
+                return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;right:' + (plotOffset.right + plotWidth + margin) + 'px;width:' + axis.labelWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>";
+            });
+            
+            addLabels(axes.x2axis, function (tick, axis) {
+                return '<div style="position:absolute;bottom:' + (plotOffset.bottom + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
+            });
+            
+            addLabels(axes.y2axis, function (tick, axis) {
+                return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;left:' + (plotOffset.left + plotWidth + margin) +'px;width:' + axis.labelWidth + 'px;text-align:left" class="tickLabel">' + tick.label + "</div>";
+            });
+
+            html.push('</div>');
+            
+            placeholder.append(html.join(""));
+        }
+
+        function drawSeries(series) {
+            if (series.lines.show)
+                drawSeriesLines(series);
+            if (series.bars.show)
+                drawSeriesBars(series);
+            if (series.points.show)
+                drawSeriesPoints(series);
+        }
+        
+        function drawSeriesLines(series) {
+            function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    prevx = null, prevy = null;
+                
+                ctx.beginPath();
+                for (var i = ps; i < points.length; i += ps) {
+                    var x1 = points[i - ps], y1 = points[i - ps + 1],
+                        x2 = points[i], y2 = points[i + 1];
+                    
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min) {
+                        if (y2 < axisy.min)
+                            continue;   // line segment is outside
+                        // compute new intersection point
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min) {
+                        if (y1 < axisy.min)
+                            continue;
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max) {
+                        if (y2 > axisy.max)
+                            continue;
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max) {
+                        if (y1 > axisy.max)
+                            continue;
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (x1 != prevx || y1 != prevy)
+                        ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
+                    
+                    prevx = x2;
+                    prevy = y2;
+                    ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
+                }
+                ctx.stroke();
+            }
+
+            function plotLineArea(datapoints, axisx, axisy) {
+                var points = datapoints.points,
+                    ps = datapoints.pointsize,
+                    bottom = Math.min(Math.max(0, axisy.min), axisy.max),
+                    top, lastX = 0, areaOpen = false;
+                
+                for (var i = ps; i < points.length; i += ps) {
+                    var x1 = points[i - ps], y1 = points[i - ps + 1],
+                        x2 = points[i], y2 = points[i + 1];
+                    
+                    if (areaOpen && x1 != null && x2 == null) {
+                        // close area
+                        ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
+                        ctx.fill();
+                        areaOpen = false;
+                        continue;
+                    }
+
+                    if (x1 == null || x2 == null)
+                        continue;
+
+                    // clip x values
+                    
+                    // clip with xmin
+                    if (x1 <= x2 && x1 < axisx.min) {
+                        if (x2 < axisx.min)
+                            continue;
+                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.min;
+                    }
+                    else if (x2 <= x1 && x2 < axisx.min) {
+                        if (x1 < axisx.min)
+                            continue;
+                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.min;
+                    }
+
+                    // clip with xmax
+                    if (x1 >= x2 && x1 > axisx.max) {
+                        if (x2 > axisx.max)
+                            continue;
+                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x1 = axisx.max;
+                    }
+                    else if (x2 >= x1 && x2 > axisx.max) {
+                        if (x1 > axisx.max)
+                            continue;
+                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+                        x2 = axisx.max;
+                    }
+
+                    if (!areaOpen) {
+                        // open area
+                        ctx.beginPath();
+                        ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
+                        areaOpen = true;
+                    }
+                    
+                    // now first check the case where both is outside
+                    if (y1 >= axisy.max && y2 >= axisy.max) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
+                        lastX = x2;
+                        continue;
+                    }
+                    else if (y1 <= axisy.min && y2 <= axisy.min) {
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
+                        lastX = x2;
+                        continue;
+                    }
+                    
+                    // else it's a bit more complicated, there might
+                    // be two rectangles and two triangles we need to fill
+                    // in; to find these keep track of the current x values
+                    var x1old = x1, x2old = x2;
+
+                    // and clip the y values, without shortcutting
+                    
+                    // clip with ymin
+                    if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
+                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.min;
+                    }
+                    else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
+                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.min;
+                    }
+
+                    // clip with ymax
+                    if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
+                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y1 = axisy.max;
+                    }
+                    else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
+                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+                        y2 = axisy.max;
+                    }
+
+
+                    // if the x value was changed we got a rectangle
+                    // to fill
+                    if (x1 != x1old) {
+                        if (y1 <= axisy.min)
+                            top = axisy.min;
+                        else
+                            top = axisy.max;
+                        
+                        ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top));
+                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(top));
+                    }
+                    
+                    // fill the triangles
+                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
+                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+
+                    // fill the other rectangle if it's there
+                    if (x2 != x2old) {
+                        if (y2 <= axisy.min)
+                            top = axisy.min;
+                        else
+                            top = axisy.max;
+                        
+                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(top));
+                        ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top));
+                    }
+
+                    lastX = Math.max(x2, x2old);
+                }
+
+                if (areaOpen) {
+                    ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
+                    ctx.fill();
+                }
+            }
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+            ctx.lineJoin = "round";
+
+            var lw = series.lines.lineWidth,
+                sw = series.shadowSize;
+            // FIXME: consider another form of shadow when filling is turned on
+            if (lw > 0 && sw > 0) {
+                // draw shadow as a thick and thin line with transparency
+                ctx.lineWidth = sw;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                // position shadow at angle from the mid of line
+                var angle = Math.PI/18;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
+                ctx.lineWidth = sw/2;
+                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
+            if (fillStyle) {
+                ctx.fillStyle = fillStyle;
+                plotLineArea(series.datapoints, series.xaxis, series.yaxis);
+            }
+
+            if (lw > 0)
+                plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function drawSeriesPoints(series) {
+            function plotPoints(datapoints, radius, fillStyle, offset, circumference, axisx, axisy) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+                
+                for (var i = 0; i < points.length; i += ps) {
+                    var x = points[i], y = points[i + 1];
+                    if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                        continue;
+                    
+                    ctx.beginPath();
+                    ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, false);
+                    if (fillStyle) {
+                        ctx.fillStyle = fillStyle;
+                        ctx.fill();
+                    }
+                    ctx.stroke();
+                }
+            }
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            var lw = series.lines.lineWidth,
+                sw = series.shadowSize,
+                radius = series.points.radius;
+            if (lw > 0 && sw > 0) {
+                // draw shadow in two steps
+                var w = sw / 2;
+                ctx.lineWidth = w;
+                ctx.strokeStyle = "rgba(0,0,0,0.1)";
+                plotPoints(series.datapoints, radius, null, w + w/2, Math.PI,
+                           series.xaxis, series.yaxis);
+
+                ctx.strokeStyle = "rgba(0,0,0,0.2)";
+                plotPoints(series.datapoints, radius, null, w/2, Math.PI,
+                           series.xaxis, series.yaxis);
+            }
+
+            ctx.lineWidth = lw;
+            ctx.strokeStyle = series.color;
+            plotPoints(series.datapoints, radius,
+                       getFillStyle(series.points, series.color), 0, 2 * Math.PI,
+                       series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal) {
+            var left, right, bottom, top,
+                drawLeft, drawRight, drawTop, drawBottom,
+                tmp;
+
+            if (horizontal) {
+                drawBottom = drawRight = drawTop = true;
+                drawLeft = false;
+                left = b;
+                right = x;
+                top = y + barLeft;
+                bottom = y + barRight;
+
+                // account for negative bars
+                if (right < left) {
+                    tmp = right;
+                    right = left;
+                    left = tmp;
+                    drawLeft = true;
+                    drawRight = false;
+                }
+            }
+            else {
+                drawLeft = drawRight = drawTop = true;
+                drawBottom = false;
+                left = x + barLeft;
+                right = x + barRight;
+                bottom = b;
+                top = y;
+
+                // account for negative bars
+                if (top < bottom) {
+                    tmp = top;
+                    top = bottom;
+                    bottom = tmp;
+                    drawBottom = true;
+                    drawTop = false;
+                }
+            }
+           
+            // clip
+            if (right < axisx.min || left > axisx.max ||
+                top < axisy.min || bottom > axisy.max)
+                return;
+            
+            if (left < axisx.min) {
+                left = axisx.min;
+                drawLeft = false;
+            }
+
+            if (right > axisx.max) {
+                right = axisx.max;
+                drawRight = false;
+            }
+
+            if (bottom < axisy.min) {
+                bottom = axisy.min;
+                drawBottom = false;
+            }
+            
+            if (top > axisy.max) {
+                top = axisy.max;
+                drawTop = false;
+            }
+
+            left = axisx.p2c(left);
+            bottom = axisy.p2c(bottom);
+            right = axisx.p2c(right);
+            top = axisy.p2c(top);
+            
+            // fill the bar
+            if (fillStyleCallback) {
+                c.beginPath();
+                c.moveTo(left, bottom);
+                c.lineTo(left, top);
+                c.lineTo(right, top);
+                c.lineTo(right, bottom);
+                c.fillStyle = fillStyleCallback(bottom, top);
+                c.fill();
+            }
+
+            // draw outline
+            if (drawLeft || drawRight || drawTop || drawBottom) {
+                c.beginPath();
+
+                // FIXME: inline moveTo is buggy with excanvas
+                c.moveTo(left, bottom + offset);
+                if (drawLeft)
+                    c.lineTo(left, top + offset);
+                else
+                    c.moveTo(left, top + offset);
+                if (drawTop)
+                    c.lineTo(right, top + offset);
+                else
+                    c.moveTo(right, top + offset);
+                if (drawRight)
+                    c.lineTo(right, bottom + offset);
+                else
+                    c.moveTo(right, bottom + offset);
+                if (drawBottom)
+                    c.lineTo(left, bottom + offset);
+                else
+                    c.moveTo(left, bottom + offset);
+                c.stroke();
+            }
+        }
+        
+        function drawSeriesBars(series) {
+            function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
+                var points = datapoints.points, ps = datapoints.pointsize;
+                
+                for (var i = 0; i < points.length; i += ps) {
+                    if (points[i] == null)
+                        continue;
+                    drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal);
+                }
+            }
+
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            // FIXME: figure out a way to add shadows (for instance along the right edge)
+            ctx.lineWidth = series.bars.lineWidth;
+            ctx.strokeStyle = series.color;
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
+            plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
+            ctx.restore();
+        }
+
+        function getFillStyle(filloptions, seriesColor, bottom, top) {
+            var fill = filloptions.fill;
+            if (!fill)
+                return null;
+
+            if (filloptions.fillColor)
+                return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
+            
+            var c = $.color.parse(seriesColor);
+            c.a = typeof fill == "number" ? fill : 0.4;
+            c.normalize();
+            return c.toString();
+        }
+        
+        function insertLegend() {
+            placeholder.find(".legend").remove();
+
+            if (!options.legend.show)
+                return;
+            
+            var fragments = [], rowStarted = false,
+                lf = options.legend.labelFormatter, s, label;
+            for (i = 0; i < series.length; ++i) {
+                s = series[i];
+                label = s.label;
+                if (!label)
+                    continue;
+                
+                if (i % options.legend.noColumns == 0) {
+                    if (rowStarted)
+                        fragments.push('</tr>');
+                    fragments.push('<tr>');
+                    rowStarted = true;
+                }
+
+                if (lf)
+                    label = lf(label, s);
+                
+                fragments.push(
+                    '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
+                    '<td class="legendLabel">' + label + '</td>');
+            }
+            if (rowStarted)
+                fragments.push('</tr>');
+            
+            if (fragments.length == 0)
+                return;
+
+            var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
+            if (options.legend.container != null)
+                $(options.legend.container).html(table);
+            else {
+                var pos = "",
+                    p = options.legend.position,
+                    m = options.legend.margin;
+                if (m[0] == null)
+                    m = [m, m];
+                if (p.charAt(0) == "n")
+                    pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
+                else if (p.charAt(0) == "s")
+                    pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
+                if (p.charAt(1) == "e")
+                    pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
+                else if (p.charAt(1) == "w")
+                    pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
+                var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
+                if (options.legend.backgroundOpacity != 0.0) {
+                    // put in the transparent background
+                    // separately to avoid blended labels and
+                    // label boxes
+                    var c = options.legend.backgroundColor;
+                    if (c == null) {
+                        c = options.grid.backgroundColor;
+                        if (c && typeof c == "string")
+                            c = $.color.parse(c);
+                        else
+                            c = $.color.extract(legend, 'background-color');
+                        c.a = 1;
+                        c = c.toString();
+                    }
+                    var div = legend.children();
+                    $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
+                }
+            }
+        }
+
+
+        // interactive features
+        
+        var highlights = [],
+            redrawTimeout = null;
+        
+        // returns the data item the mouse is over, or null if none is found
+        function findNearbyItem(mouseX, mouseY, seriesFilter) {
+            var maxDistance = options.grid.mouseActiveRadius,
+                smallestDistance = maxDistance * maxDistance + 1,
+                item = null, foundPoint = false, i, j;
+
+            for (i = 0; i < series.length; ++i) {
+                if (!seriesFilter(series[i]))
+                    continue;
+                
+                var s = series[i],
+                    axisx = s.xaxis,
+                    axisy = s.yaxis,
+                    points = s.datapoints.points,
+                    ps = s.datapoints.pointsize,
+                    mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
+                    my = axisy.c2p(mouseY),
+                    maxx = maxDistance / axisx.scale,
+                    maxy = maxDistance / axisy.scale;
+
+                if (s.lines.show || s.points.show) {
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1];
+                        if (x == null)
+                            continue;
+                        
+                        // For points and lines, the cursor must be within a
+                        // certain distance to the data point
+                        if (x - mx > maxx || x - mx < -maxx ||
+                            y - my > maxy || y - my < -maxy)
+                            continue;
+
+                        // We have to calculate distances in pixels, not in
+                        // data units, because the scales of the axes may be different
+                        var dx = Math.abs(axisx.p2c(x) - mouseX),
+                            dy = Math.abs(axisy.p2c(y) - mouseY),
+                            dist = dx * dx + dy * dy; // we save the sqrt
+
+                        // use <= to ensure last point takes precedence
+                        // (last generally means on top of)
+                        if (dist <= smallestDistance) {
+                            smallestDistance = dist;
+                            item = [i, j / ps];
+                        }
+                    }
+                }
+                    
+                if (s.bars.show && !item) { // no other point can be nearby
+                    var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
+                        barRight = barLeft + s.bars.barWidth;
+                    
+                    for (j = 0; j < points.length; j += ps) {
+                        var x = points[j], y = points[j + 1], b = points[j + 2];
+                        if (x == null)
+                            continue;
+  
+                        // for a bar graph, the cursor must be inside the bar
+                        if (series[i].bars.horizontal ? 
+                            (mx <= Math.max(b, x) && mx >= Math.min(b, x) && 
+                             my >= y + barLeft && my <= y + barRight) :
+                            (mx >= x + barLeft && mx <= x + barRight &&
+                             my >= Math.min(b, y) && my <= Math.max(b, y)))
+                                item = [i, j / ps];
+                    }
+                }
+            }
+
+            if (item) {
+                i = item[0];
+                j = item[1];
+                ps = series[i].datapoints.pointsize;
+                
+                return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
+                         dataIndex: j,
+                         series: series[i],
+                         seriesIndex: i };
+            }
+            
+            return null;
+        }
+
+        function onMouseMove(e) {
+            if (options.grid.hoverable)
+                triggerClickHoverEvent("plothover", e,
+                                       function (s) { return s["hoverable"] != false; });
+        }
+        
+        function onClick(e) {
+            triggerClickHoverEvent("plotclick", e,
+                                   function (s) { return s["clickable"] != false; });
+        }
+
+        // trigger click or hover event (they send the same parameters
+        // so we share their code)
+        function triggerClickHoverEvent(eventname, event, seriesFilter) {
+            var offset = eventHolder.offset(),
+                pos = { pageX: event.pageX, pageY: event.pageY },
+                canvasX = event.pageX - offset.left - plotOffset.left,
+                canvasY = event.pageY - offset.top - plotOffset.top;
+
+            if (axes.xaxis.used)
+                pos.x = axes.xaxis.c2p(canvasX);
+            if (axes.yaxis.used)
+                pos.y = axes.yaxis.c2p(canvasY);
+            if (axes.x2axis.used)
+                pos.x2 = axes.x2axis.c2p(canvasX);
+            if (axes.y2axis.used)
+                pos.y2 = axes.y2axis.c2p(canvasY);
+
+            var item = findNearbyItem(canvasX, canvasY, seriesFilter);
+
+            if (item) {
+                // fill in mouse pos for any listeners out there
+                item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
+                item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
+            }
+
+            if (options.grid.autoHighlight) {
+                // clear auto-highlights
+                for (var i = 0; i < highlights.length; ++i) {
+                    var h = highlights[i];
+                    if (h.auto == eventname &&
+                        !(item && h.series == item.series && h.point == item.datapoint))
+                        unhighlight(h.series, h.point);
+                }
+                
+                if (item)
+                    highlight(item.series, item.datapoint, eventname);
+            }
+            
+            placeholder.trigger(eventname, [ pos, item ]);
+        }
+
+        function triggerRedrawOverlay() {
+            if (!redrawTimeout)
+                redrawTimeout = setTimeout(drawOverlay, 30);
+        }
+
+        function drawOverlay() {
+            redrawTimeout = null;
+
+            // draw highlights
+            octx.save();
+            octx.clearRect(0, 0, canvasWidth, canvasHeight);
+            octx.translate(plotOffset.left, plotOffset.top);
+            
+            var i, hi;
+            for (i = 0; i < highlights.length; ++i) {
+                hi = highlights[i];
+
+                if (hi.series.bars.show)
+                    drawBarHighlight(hi.series, hi.point);
+                else
+                    drawPointHighlight(hi.series, hi.point);
+            }
+            octx.restore();
+            
+            executeHooks(hooks.drawOverlay, [octx]);
+        }
+        
+        function highlight(s, point, auto) {
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number")
+                point = s.data[point];
+
+            var i = indexOfHighlight(s, point);
+            if (i == -1) {
+                highlights.push({ series: s, point: point, auto: auto });
+
+                triggerRedrawOverlay();
+            }
+            else if (!auto)
+                highlights[i].auto = false;
+        }
+            
+        function unhighlight(s, point) {
+            if (s == null && point == null) {
+                highlights = [];
+                triggerRedrawOverlay();
+            }
+            
+            if (typeof s == "number")
+                s = series[s];
+
+            if (typeof point == "number")
+                point = s.data[point];
+
+            var i = indexOfHighlight(s, point);
+            if (i != -1) {
+                highlights.splice(i, 1);
+
+                triggerRedrawOverlay();
+            }
+        }
+        
+        function indexOfHighlight(s, p) {
+            for (var i = 0; i < highlights.length; ++i) {
+                var h = highlights[i];
+                if (h.series == s && h.point[0] == p[0]
+                    && h.point[1] == p[1])
+                    return i;
+            }
+            return -1;
+        }
+        
+        function drawPointHighlight(series, point) {
+            var x = point[0], y = point[1],
+                axisx = series.xaxis, axisy = series.yaxis;
+            
+            if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+                return;
+            
+            var pointRadius = series.points.radius + series.points.lineWidth / 2;
+            octx.lineWidth = pointRadius;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var radius = 1.5 * pointRadius;
+            octx.beginPath();
+            octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, false);
+            octx.stroke();
+        }
+
+        function drawBarHighlight(series, point) {
+            octx.lineWidth = series.bars.lineWidth;
+            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+            drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
+                    0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal);
+        }
+
+        function getColorOrGradient(spec, bottom, top, defaultColor) {
+            if (typeof spec == "string")
+                return spec;
+            else {
+                // assume this is a gradient spec; IE currently only
+                // supports a simple vertical gradient properly, so that's
+                // what we support too
+                var gradient = ctx.createLinearGradient(0, top, 0, bottom);
+                
+                for (var i = 0, l = spec.colors.length; i < l; ++i) {
+                    var c = spec.colors[i];
+                    if (typeof c != "string") {
+                        c = $.color.parse(defaultColor).scale('rgb', c.brightness);
+                        c.a *= c.opacity;
+                        c = c.toString();
+                    }
+                    gradient.addColorStop(i / (l - 1), c);
+                }
+                
+                return gradient;
+            }
+        }
+    }
+
+    $.plot = function(placeholder, data, options) {
+        var plot = new Plot($(placeholder), data, options, $.plot.plugins);
+        /*var t0 = new Date();
+        var t1 = new Date();
+        var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime())
+        if (window.console)
+            console.log(tstr);
+        else
+            alert(tstr);*/
+        return plot;
+    };
+
+    $.plot.plugins = [];
+
+    // returns a string with the date d formatted according to fmt
+    $.plot.formatDate = function(d, fmt, monthNames) {
+        var leftPad = function(n) {
+            n = "" + n;
+            return n.length == 1 ? "0" + n : n;
+        };
+        
+        var r = [];
+        var escape = false;
+        var hours = d.getUTCHours();
+        var isAM = hours < 12;
+        if (monthNames == null)
+            monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+        if (fmt.search(/%p|%P/) != -1) {
+            if (hours > 12) {
+                hours = hours - 12;
+            } else if (hours == 0) {
+                hours = 12;
+            }
+        }
+        for (var i = 0; i < fmt.length; ++i) {
+            var c = fmt.charAt(i);
+            
+            if (escape) {
+                switch (c) {
+                case 'h': c = "" + hours; break;
+                case 'H': c = leftPad(hours); break;
+                case 'M': c = leftPad(d.getUTCMinutes()); break;
+                case 'S': c = leftPad(d.getUTCSeconds()); break;
+                case 'd': c = "" + d.getUTCDate(); break;
+                case 'm': c = "" + (d.getUTCMonth() + 1); break;
+                case 'y': c = "" + d.getUTCFullYear(); break;
+                case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
+                case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
+                case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
+                }
+                r.push(c);
+                escape = false;
+            }
+            else {
+                if (c == "%")
+                    escape = true;
+                else
+                    r.push(c);
+            }
+        }
+        return r.join("");
+    };
+    
+    // round to nearby lower multiple of base
+    function floorInBase(n, base) {
+        return base * Math.floor(n / base);
+    }
+    
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery.qtip.js	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,2149 @@
+/*!
+ * jquery.qtip. The jQuery tooltip plugin
+ *
+ * Copyright (c) 2009 Craig Thompson
+ * http://craigsworks.com
+ *
+ * Licensed under MIT
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * Launch  : February 2009
+ * Version : 1.0.0-rc3
+ * Released: Tuesday 12th May, 2009 - 00:00
+ * Debug: jquery.qtip.debug.js
+ */
+(function($)
+{
+   // Implementation
+   $.fn.qtip = function(options, blanket)
+   {
+      var i, id, interfaces, opts, obj, command, config, api;
+
+      // Return API / Interfaces if requested
+      if(typeof options == 'string')
+      {
+         // Make sure API data exists if requested
+         if(typeof $(this).data('qtip') !== 'object')
+            $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.NO_TOOLTIP_PRESENT, false);
+
+         // Return requested object
+         if(options == 'api')
+            return $(this).data('qtip').interfaces[ $(this).data('qtip').current ];
+         else if(options == 'interfaces')
+            return $(this).data('qtip').interfaces;
+      }
+
+      // Validate provided options
+      else
+      {
+         // Set null options object if no options are provided
+         if(!options) options = {};
+
+         // Sanitize option data
+         if(typeof options.content !== 'object' || (options.content.jquery && options.content.length > 0)) options.content = { text: options.content };
+         if(typeof options.content.title !== 'object') options.content.title = { text: options.content.title };
+         if(typeof options.position !== 'object') options.position = { corner: options.position };
+         if(typeof options.position.corner !== 'object') options.position.corner = { target: options.position.corner, tooltip: options.position.corner };
+         if(typeof options.show !== 'object') options.show = { when: options.show };
+         if(typeof options.show.when !== 'object') options.show.when = { event: options.show.when };
+         if(typeof options.show.effect !== 'object') options.show.effect = { type: options.show.effect };
+         if(typeof options.hide !== 'object') options.hide = { when: options.hide };
+         if(typeof options.hide.when !== 'object') options.hide.when = { event: options.hide.when };
+         if(typeof options.hide.effect !== 'object') options.hide.effect = { type: options.hide.effect };
+         if(typeof options.style !== 'object') options.style = { name: options.style };
+         options.style = sanitizeStyle(options.style);
+
+         // Build main options object
+         opts = $.extend(true, {}, $.fn.qtip.defaults, options);
+
+         // Inherit all style properties into one syle object and include original options
+         opts.style = buildStyle.call({ options: opts }, opts.style);
+         opts.user = $.extend(true, {}, options);
+      };
+
+      // Iterate each matched element
+      return $(this).each(function() // Return original elements as per jQuery guidelines
+      {
+         // Check for API commands
+         if(typeof options == 'string')
+         {
+            command = options.toLowerCase();
+            interfaces = $(this).qtip('interfaces');
+
+            // Make sure API data exists$('.qtip').qtip('destroy')
+            if(typeof interfaces == 'object')
+            {
+               // Check if API call is a BLANKET DESTROY command
+               if(blanket === true && command == 'destroy')
+                  while(interfaces.length > 0) interfaces[interfaces.length-1].destroy();
+
+               // API call is not a BLANKET DESTROY command
+               else
+               {
+                  // Check if supplied command effects this tooltip only (NOT BLANKET)
+                  if(blanket !== true) interfaces = [ $(this).qtip('api') ];
+
+                  // Execute command on chosen qTips
+                  for(i = 0; i < interfaces.length; i++)
+                  {
+                     // Destroy command doesn't require tooltip to be rendered
+                     if(command == 'destroy') interfaces[i].destroy();
+
+                     // Only call API if tooltip is rendered and it wasn't a destroy call
+                     else if(interfaces[i].status.rendered === true)
+                     {
+                        if(command == 'show') interfaces[i].show();
+                        else if(command == 'hide') interfaces[i].hide();
+                        else if(command == 'focus') interfaces[i].focus();
+                        else if(command == 'disable') interfaces[i].disable(true);
+                        else if(command == 'enable') interfaces[i].disable(false);
+                     };
+                  };
+               };
+            };
+         }
+
+         // No API commands, continue with qTip creation
+         else
+         {
+            // Create unique configuration object
+            config = $.extend(true, {}, opts);
+            config.hide.effect.length = opts.hide.effect.length;
+            config.show.effect.length = opts.show.effect.length;
+
+            // Sanitize target options
+            if(config.position.container === false) config.position.container = $(document.body);
+            if(config.position.target === false) config.position.target = $(this);
+            if(config.show.when.target === false) config.show.when.target = $(this);
+            if(config.hide.when.target === false) config.hide.when.target = $(this);
+
+            // Determine tooltip ID (Reuse array slots if possible)
+            id = $.fn.qtip.interfaces.length;
+            for(i = 0; i < id; i++)
+            {
+               if(typeof $.fn.qtip.interfaces[i] == 'undefined'){ id = i; break; };
+            };
+
+            // Instantiate the tooltip
+            obj = new qTip($(this), config, id);
+
+            // Add API references
+            $.fn.qtip.interfaces[id] = obj;
+
+            // Check if element already has qTip data assigned
+            if(typeof $(this).data('qtip') == 'object')
+            {
+               // Set new current interface id
+               if(typeof $(this).attr('qtip') === 'undefined')
+                  $(this).data('qtip').current = $(this).data('qtip').interfaces.length;
+
+               // Push new API interface onto interfaces array
+               $(this).data('qtip').interfaces.push(obj);
+            }
+
+            // No qTip data is present, create now
+            else $(this).data('qtip', { current: 0, interfaces: [obj] });
+
+            // If prerendering is disabled, create tooltip on showEvent
+            if(config.content.prerender === false && config.show.when.event !== false && config.show.ready !== true)
+            {
+               config.show.when.target.bind(config.show.when.event+'.qtip-'+id+'-create', { qtip: id }, function(event)
+               {
+                  // Retrieve API interface via passed qTip Id
+                  api = $.fn.qtip.interfaces[ event.data.qtip ];
+
+                  // Unbind show event and cache mouse coords
+                  api.options.show.when.target.unbind(api.options.show.when.event+'.qtip-'+event.data.qtip+'-create');
+                  api.cache.mouse = { x: event.pageX, y: event.pageY };
+
+                  // Render tooltip and start the event sequence
+                  construct.call( api );
+                  api.options.show.when.target.trigger(api.options.show.when.event);
+               });
+            }
+
+            // Prerendering is enabled, create tooltip now
+            else
+            {
+               // Set mouse position cache to top left of the element
+               obj.cache.mouse = {
+                  x: config.show.when.target.offset().left,
+                  y: config.show.when.target.offset().top
+               };
+
+               // Construct the tooltip
+               construct.call(obj);
+            }
+         };
+      });
+   };
+
+   // Instantiator
+   function qTip(target, options, id)
+   {
+      // Declare this reference
+      var self = this;
+
+      // Setup class attributes
+      self.id = id;
+      self.options = options;
+      self.status = {
+         animated: false,
+         rendered: false,
+         disabled: false,
+         focused: false
+      };
+      self.elements = {
+         target: target.addClass(self.options.style.classes.target),
+         tooltip: null,
+         wrapper: null,
+         content: null,
+         contentWrapper: null,
+         title: null,
+         button: null,
+         tip: null,
+         bgiframe: null
+      };
+      self.cache = {
+         mouse: {},
+         position: {},
+         toggle: 0
+      };
+      self.timers = {};
+
+      // Define exposed API methods
+      $.extend(self, self.options.api,
+      {
+         show: function(event)
+         {
+            var returned, solo;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'show');
+
+            // Only continue if element is visible
+            if(self.elements.tooltip.css('display') !== 'none') return self;
+
+            // Clear animation queue
+            self.elements.tooltip.stop(true, false);
+
+            // Call API method and if return value is false, halt
+            returned = self.beforeShow.call(self, event);
+            if(returned === false) return self;
+
+            // Define afterShow callback method
+            function afterShow()
+            {
+               // Call API method and focus if it isn't static
+               if(self.options.position.type !== 'static') self.focus();
+               self.onShow.call(self, event);
+
+               // Prevent antialias from disappearing in IE7 by removing filter attribute
+               if($.browser.msie) self.elements.tooltip.get(0).style.removeAttribute('filter');
+            };
+
+            // Maintain toggle functionality if enabled
+            self.cache.toggle = 1;
+
+            // Update tooltip position if it isn't static
+            if(self.options.position.type !== 'static')
+               self.updatePosition(event, (self.options.show.effect.length > 0));
+
+            // Hide other tooltips if tooltip is solo
+            if(typeof self.options.show.solo == 'object') solo = $(self.options.show.solo);
+            else if(self.options.show.solo === true) solo = $('div.qtip').not(self.elements.tooltip);
+            if(solo) solo.each(function(){ if($(this).qtip('api').status.rendered === true) $(this).qtip('api').hide(); });
+
+            // Show tooltip
+            if(typeof self.options.show.effect.type == 'function')
+            {
+               self.options.show.effect.type.call(self.elements.tooltip, self.options.show.effect.length);
+               self.elements.tooltip.queue(function(){ afterShow(); $(this).dequeue(); });
+            }
+            else
+            {
+               switch(self.options.show.effect.type.toLowerCase())
+               {
+                  case 'fade':
+                     self.elements.tooltip.fadeIn(self.options.show.effect.length, afterShow);
+                     break;
+                  case 'slide':
+                     self.elements.tooltip.slideDown(self.options.show.effect.length, function()
+                     {
+                        afterShow();
+                        if(self.options.position.type !== 'static') self.updatePosition(event, true);
+                     });
+                     break;
+                  case 'grow':
+                     self.elements.tooltip.show(self.options.show.effect.length, afterShow);
+                     break;
+                  default:
+                     self.elements.tooltip.show(null, afterShow);
+                     break;
+               };
+
+               // Add active class to tooltip
+               self.elements.tooltip.addClass(self.options.style.classes.active);
+            };
+
+            // Log event and return
+            return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_SHOWN, 'show');
+         },
+
+         hide: function(event)
+         {
+            var returned;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'hide');
+
+            // Only continue if element is visible
+            else if(self.elements.tooltip.css('display') === 'none') return self;
+
+            // Stop show timer and animation queue
+            clearTimeout(self.timers.show);
+            self.elements.tooltip.stop(true, false);
+
+            // Call API method and if return value is false, halt
+            returned = self.beforeHide.call(self, event);
+            if(returned === false) return self;
+
+            // Define afterHide callback method
+            function afterHide(){ self.onHide.call(self, event); };
+
+            // Maintain toggle functionality if enabled
+            self.cache.toggle = 0;
+
+            // Hide tooltip
+            if(typeof self.options.hide.effect.type == 'function')
+            {
+               self.options.hide.effect.type.call(self.elements.tooltip, self.options.hide.effect.length);
+               self.elements.tooltip.queue(function(){ afterHide(); $(this).dequeue(); });
+            }
+            else
+            {
+               switch(self.options.hide.effect.type.toLowerCase())
+               {
+                  case 'fade':
+                     self.elements.tooltip.fadeOut(self.options.hide.effect.length, afterHide);
+                     break;
+                  case 'slide':
+                     self.elements.tooltip.slideUp(self.options.hide.effect.length, afterHide);
+                     break;
+                  case 'grow':
+                     self.elements.tooltip.hide(self.options.hide.effect.length, afterHide);
+                     break;
+                  default:
+                     self.elements.tooltip.hide(null, afterHide);
+                     break;
+               };
+
+               // Remove active class to tooltip
+               self.elements.tooltip.removeClass(self.options.style.classes.active);
+            };
+
+            // Log event and return
+            return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_HIDDEN, 'hide');
+         },
+
+         updatePosition: function(event, animate)
+         {
+            var i, target, tooltip, coords, mapName, imagePos, newPosition, ieAdjust, ie6Adjust, borderAdjust, mouseAdjust, offset, curPosition, returned
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'updatePosition');
+
+            // If tooltip is static, return
+            else if(self.options.position.type == 'static')
+               return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.CANNOT_POSITION_STATIC, 'updatePosition');
+
+            // Define property objects
+            target = {
+               position: { left: 0, top: 0 },
+               dimensions: { height: 0, width: 0 },
+               corner: self.options.position.corner.target
+            };
+            tooltip = {
+               position: self.getPosition(),
+               dimensions: self.getDimensions(),
+               corner: self.options.position.corner.tooltip
+            };
+
+            // Target is an HTML element
+            if(self.options.position.target !== 'mouse')
+            {
+               // If the HTML element is AREA, calculate position manually
+               if(self.options.position.target.get(0).nodeName.toLowerCase() == 'area')
+               {
+                  // Retrieve coordinates from coords attribute and parse into integers
+                  coords = self.options.position.target.attr('coords').split(',');
+                  for(i = 0; i < coords.length; i++) coords[i] = parseInt(coords[i]);
+
+                  // Setup target position object
+                  mapName = self.options.position.target.parent('map').attr('name');
+                  imagePos = $('img[usemap="#'+mapName+'"]:first').offset();
+                  target.position = {
+                     left: Math.floor(imagePos.left + coords[0]),
+                     top: Math.floor(imagePos.top + coords[1])
+                  };
+
+                  // Determine width and height of the area
+                  switch(self.options.position.target.attr('shape').toLowerCase())
+                  {
+                     case 'rect':
+                        target.dimensions = {
+                           width: Math.ceil(Math.abs(coords[2] - coords[0])),
+                           height: Math.ceil(Math.abs(coords[3] - coords[1]))
+                        };
+                        break;
+
+                     case 'circle':
+                        target.dimensions = {
+                           width: coords[2] + 1,
+                           height: coords[2] + 1
+                        };
+                        break;
+
+                     case 'poly':
+                        target.dimensions = {
+                           width: coords[0],
+                           height: coords[1]
+                        };
+
+                        for(i = 0; i < coords.length; i++)
+                        {
+                           if(i % 2 == 0)
+                           {
+                              if(coords[i] > target.dimensions.width)
+                                 target.dimensions.width = coords[i];
+                              if(coords[i] < coords[0])
+                                 target.position.left = Math.floor(imagePos.left + coords[i]);
+                           }
+                           else
+                           {
+                              if(coords[i] > target.dimensions.height)
+                                 target.dimensions.height = coords[i];
+                              if(coords[i] < coords[1])
+                                 target.position.top = Math.floor(imagePos.top + coords[i]);
+                           };
+                        };
+
+                        target.dimensions.width = target.dimensions.width - (target.position.left - imagePos.left);
+                        target.dimensions.height = target.dimensions.height - (target.position.top - imagePos.top);
+                        break;
+
+                     default:
+                        return $.fn.qtip.log.error.call(self, 4, $.fn.qtip.constants.INVALID_AREA_SHAPE, 'updatePosition');
+                        break;
+                  };
+
+                  // Adjust position by 2 pixels (Positioning bug?)
+                  target.dimensions.width -= 2; target.dimensions.height -= 2;
+               }
+
+               // Target is the document
+               else if(self.options.position.target.add(document.body).length === 1)
+               {
+                  target.position = { left: $(document).scrollLeft(), top: $(document).scrollTop() };
+                  target.dimensions = { height: $(window).height(), width: $(window).width() };
+               }
+
+               // Target is a regular HTML element, find position normally
+               else
+               {
+                  // Check if the target is another tooltip. If its animated, retrieve position from newPosition data
+                  if(typeof self.options.position.target.attr('qtip') !== 'undefined')
+                     target.position = self.options.position.target.qtip('api').cache.position;
+                  else
+                     target.position = self.options.position.target.offset();
+
+                  // Setup dimensions objects
+                  target.dimensions = {
+                     height: self.options.position.target.outerHeight(),
+                     width: self.options.position.target.outerWidth()
+                  };
+               };
+
+               // Calculate correct target corner position
+               newPosition = $.extend({}, target.position);
+               if(target.corner.search(/right/i) !== -1)
+                  newPosition.left += target.dimensions.width;
+
+               if(target.corner.search(/bottom/i) !== -1)
+                  newPosition.top += target.dimensions.height;
+
+               if(target.corner.search(/((top|bottom)Middle)|center/) !== -1)
+                  newPosition.left += (target.dimensions.width / 2);
+
+               if(target.corner.search(/((left|right)Middle)|center/) !== -1)
+                  newPosition.top += (target.dimensions.height / 2);
+            }
+
+            // Mouse is the target, set position to current mouse coordinates
+            else
+            {
+               // Setup target position and dimensions objects
+               target.position = newPosition = { left: self.cache.mouse.x, top: self.cache.mouse.y };
+               target.dimensions = { height: 1, width: 1 };
+            };
+
+            // Calculate correct target corner position
+            if(tooltip.corner.search(/right/i) !== -1)
+               newPosition.left -= tooltip.dimensions.width;
+
+            if(tooltip.corner.search(/bottom/i) !== -1)
+               newPosition.top -= tooltip.dimensions.height;
+
+            if(tooltip.corner.search(/((top|bottom)Middle)|center/) !== -1)
+               newPosition.left -= (tooltip.dimensions.width / 2);
+
+            if(tooltip.corner.search(/((left|right)Middle)|center/) !== -1)
+               newPosition.top -= (tooltip.dimensions.height / 2);
+
+            // Setup IE adjustment variables (Pixel gap bugs)
+            ieAdjust = ($.browser.msie) ? 1 : 0; // And this is why I hate IE...
+            ie6Adjust = ($.browser.msie && parseInt($.browser.version.charAt(0)) === 6) ? 1 : 0; // ...and even more so IE6!
+
+            // Adjust for border radius
+            if(self.options.style.border.radius > 0)
+            {
+               if(tooltip.corner.search(/Left/) !== -1)
+                  newPosition.left -= self.options.style.border.radius;
+               else if(tooltip.corner.search(/Right/) !== -1)
+                  newPosition.left += self.options.style.border.radius;
+
+               if(tooltip.corner.search(/Top/) !== -1)
+                  newPosition.top -= self.options.style.border.radius;
+               else if(tooltip.corner.search(/Bottom/) !== -1)
+                  newPosition.top += self.options.style.border.radius;
+            };
+
+            // IE only adjustments (Pixel perfect!)
+            if(ieAdjust)
+            {
+               if(tooltip.corner.search(/top/) !== -1)
+                  newPosition.top -= ieAdjust
+               else if(tooltip.corner.search(/bottom/) !== -1)
+                  newPosition.top += ieAdjust
+
+               if(tooltip.corner.search(/left/) !== -1)
+                  newPosition.left -= ieAdjust
+               else if(tooltip.corner.search(/right/) !== -1)
+                  newPosition.left += ieAdjust
+
+               if(tooltip.corner.search(/leftMiddle|rightMiddle/) !== -1)
+                  newPosition.top -= 1
+            };
+
+            // If screen adjustment is enabled, apply adjustments
+            if(self.options.position.adjust.screen === true)
+               newPosition = screenAdjust.call(self, newPosition, target, tooltip);
+
+            // If mouse is the target, prevent tooltip appearing directly under the mouse
+            if(self.options.position.target === 'mouse' && self.options.position.adjust.mouse === true)
+            {
+               if(self.options.position.adjust.screen === true && self.elements.tip)
+                  mouseAdjust = self.elements.tip.attr('rel');
+               else
+                  mouseAdjust = self.options.position.corner.tooltip;
+
+               newPosition.left += (mouseAdjust.search(/right/i) !== -1) ? -6 : 6;
+               newPosition.top += (mouseAdjust.search(/bottom/i) !== -1) ? -6 : 6;
+            }
+
+            // Initiate bgiframe plugin in IE6 if tooltip overlaps a select box or object element
+            if(!self.elements.bgiframe && $.browser.msie && parseInt($.browser.version.charAt(0)) == 6)
+            {
+               $('select, object').each(function()
+               {
+                  offset = $(this).offset();
+                  offset.bottom = offset.top + $(this).height();
+                  offset.right = offset.left + $(this).width();
+
+                  if(newPosition.top + tooltip.dimensions.height >= offset.top
+                  && newPosition.left + tooltip.dimensions.width >= offset.left)
+                     bgiframe.call(self);
+               });
+            };
+
+            // Add user xy adjustments
+            newPosition.left += self.options.position.adjust.x;
+            newPosition.top += self.options.position.adjust.y;
+
+            // Set new tooltip position if its moved, animate if enabled
+            curPosition = self.getPosition();
+            if(newPosition.left != curPosition.left || newPosition.top != curPosition.top)
+            {
+               // Call API method and if return value is false, halt
+               returned = self.beforePositionUpdate.call(self, event);
+               if(returned === false) return self;
+
+               // Cache new position
+               self.cache.position = newPosition;
+
+               // Check if animation is enabled
+               if(animate === true)
+               {
+                  // Set animated status
+                  self.status.animated = true;
+
+                  // Animate and reset animated status on animation end
+                  self.elements.tooltip.animate(newPosition, 200, 'swing', function(){ self.status.animated = false });
+               }
+
+               // Set new position via CSS
+               else self.elements.tooltip.css(newPosition);
+
+               // Call API method and log event if its not a mouse move
+               self.onPositionUpdate.call(self, event);
+               if(typeof event !== 'undefined' && event.type && event.type !== 'mousemove')
+                  $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_POSITION_UPDATED, 'updatePosition');
+            };
+
+            return self;
+         },
+
+         updateWidth: function(newWidth)
+         {
+            var hidden;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'updateWidth');
+
+            // Make sure supplied width is a number and if not, return
+            else if(newWidth && typeof newWidth !== 'number')
+               return $.fn.qtip.log.error.call(self, 2, 'newWidth must be of type number', 'updateWidth');
+
+            // Setup elements which must be hidden during width update
+            hidden = self.elements.contentWrapper.siblings().add(self.elements.tip).add(self.elements.button);
+
+            // Calculate the new width if one is not supplied
+            if(!newWidth)
+            {
+               // Explicit width is set
+               if(typeof self.options.style.width.value == 'number')
+                  newWidth = self.options.style.width.value;
+
+               // No width is set, proceed with auto detection
+               else
+               {
+                  // Set width to auto initally to determine new width and hide other elements
+                  self.elements.tooltip.css({ width: 'auto' });
+                  hidden.hide();
+
+                  // Set position and zoom to defaults to prevent IE hasLayout bug
+                  if($.browser.msie)
+                     self.elements.wrapper.add(self.elements.contentWrapper.children()).css({ zoom: 'normal' });
+
+                  // Set the new width
+                  newWidth = self.getDimensions().width + 1;
+
+                  // Make sure its within the maximum and minimum width boundries
+                  if(!self.options.style.width.value)
+                  {
+                     if(newWidth > self.options.style.width.max) newWidth = self.options.style.width.max
+                     if(newWidth < self.options.style.width.min) newWidth = self.options.style.width.min
+                  };
+               };
+            };
+
+            // Adjust newWidth by 1px if width is odd (IE6 rounding bug fix)
+            if(newWidth % 2 !== 0) newWidth -= 1;
+
+            // Set the new calculated width and unhide other elements
+            self.elements.tooltip.width(newWidth);
+            hidden.show();
+
+            // Set the border width, if enabled
+            if(self.options.style.border.radius)
+            {
+               self.elements.tooltip.find('.qtip-betweenCorners').each(function(i)
+               {
+                  $(this).width(newWidth - (self.options.style.border.radius * 2));
+               })
+            };
+
+            // IE only adjustments
+            if($.browser.msie)
+            {
+               // Reset position and zoom to give the wrapper layout (IE hasLayout bug)
+               self.elements.wrapper.add(self.elements.contentWrapper.children()).css({ zoom: '1' });
+
+               // Set the new width
+               self.elements.wrapper.width(newWidth);
+
+               // Adjust BGIframe height and width if enabled
+               if(self.elements.bgiframe) self.elements.bgiframe.width(newWidth).height(self.getDimensions.height);
+            };
+
+            // Log event and return
+            return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_WIDTH_UPDATED, 'updateWidth');
+         },
+
+         updateStyle: function(name)
+         {
+            var tip, borders, context, corner, coordinates;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'updateStyle');
+
+            // Return if style is not defined or name is not a string
+            else if(typeof name !== 'string' || !$.fn.qtip.styles[name])
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.STYLE_NOT_DEFINED, 'updateStyle');
+
+            // Set the new style object
+            self.options.style = buildStyle.call(self, $.fn.qtip.styles[name], self.options.user.style);
+
+            // Update initial styles of content and title elements
+            self.elements.content.css( jQueryStyle(self.options.style) );
+            if(self.options.content.title.text !== false)
+               self.elements.title.css( jQueryStyle(self.options.style.title, true) );
+
+            // Update CSS border colour
+            self.elements.contentWrapper.css({ borderColor: self.options.style.border.color });
+
+            // Update tip color if enabled
+            if(self.options.style.tip.corner !== false)
+            {
+               if($('<canvas>').get(0).getContext)
+               {
+                  // Retrieve canvas context and clear
+                  tip = self.elements.tooltip.find('.qtip-tip canvas:first');
+                  context = tip.get(0).getContext('2d');
+                  context.clearRect(0,0,300,300);
+
+                  // Draw new tip
+                  corner = tip.parent('div[rel]:first').attr('rel');
+                  coordinates = calculateTip(corner, self.options.style.tip.size.width, self.options.style.tip.size.height);
+                  drawTip.call(self, tip, coordinates, self.options.style.tip.color || self.options.style.border.color);
+               }
+               else if($.browser.msie)
+               {
+                  // Set new fillcolor attribute
+                  tip = self.elements.tooltip.find('.qtip-tip [nodeName="shape"]');
+                  tip.attr('fillcolor', self.options.style.tip.color || self.options.style.border.color);
+               };
+            };
+
+            // Update border colors if enabled
+            if(self.options.style.border.radius > 0)
+            {
+               self.elements.tooltip.find('.qtip-betweenCorners').css({ backgroundColor: self.options.style.border.color });
+
+               if($('<canvas>').get(0).getContext)
+               {
+                  borders = calculateBorders(self.options.style.border.radius)
+                  self.elements.tooltip.find('.qtip-wrapper canvas').each(function()
+                  {
+                     // Retrieve canvas context and clear
+                     context = $(this).get(0).getContext('2d');
+                     context.clearRect(0,0,300,300);
+
+                     // Draw new border
+                     corner = $(this).parent('div[rel]:first').attr('rel')
+                     drawBorder.call(self, $(this), borders[corner],
+                        self.options.style.border.radius, self.options.style.border.color);
+                  });
+               }
+               else if($.browser.msie)
+               {
+                  // Set new fillcolor attribute on each border corner
+                  self.elements.tooltip.find('.qtip-wrapper [nodeName="arc"]').each(function()
+                  {
+                     $(this).attr('fillcolor', self.options.style.border.color)
+                  });
+               };
+            };
+
+            // Log event and return
+            return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_STYLE_UPDATED, 'updateStyle');
+         },
+
+         updateContent: function(content, reposition)
+         {
+            var parsedContent, images, loadedImages;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'updateContent');
+
+            // Make sure content is defined before update
+            else if(!content)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.NO_CONTENT_PROVIDED, 'updateContent');
+
+            // Call API method and set new content if a string is returned
+            parsedContent = self.beforeContentUpdate.call(self, content);
+            if(typeof parsedContent == 'string') content = parsedContent;
+            else if(parsedContent === false) return;
+
+            // Set position and zoom to defaults to prevent IE hasLayout bug
+            if($.browser.msie) self.elements.contentWrapper.children().css({ zoom: 'normal' });
+
+            // Append new content if its a DOM array and show it if hidden
+            if(content.jquery && content.length > 0)
+               content.clone(true).appendTo(self.elements.content).show();
+
+            // Content is a regular string, insert the new content
+            else self.elements.content.html(content);
+
+            // Check if images need to be loaded before position is updated to prevent mis-positioning
+            images = self.elements.content.find('img[complete=false]');
+            if(images.length > 0)
+            {
+               loadedImages = 0;
+               images.each(function(i)
+               {
+                  $('<img src="'+ $(this).attr('src') +'" />')
+                     .load(function(){ if(++loadedImages == images.length) afterLoad(); });
+               });
+            }
+            else afterLoad();
+
+            function afterLoad()
+            {
+               // Update the tooltip width
+               self.updateWidth();
+
+               // If repositioning is enabled, update positions
+               if(reposition !== false)
+               {
+                  // Update position if tooltip isn't static
+                  if(self.options.position.type !== 'static')
+                     self.updatePosition(self.elements.tooltip.is(':visible'), true);
+
+                  // Reposition the tip if enabled
+                  if(self.options.style.tip.corner !== false)
+                     positionTip.call(self);
+               };
+            };
+
+            // Call API method and log event
+            self.onContentUpdate.call(self);
+            return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_CONTENT_UPDATED, 'loadContent');
+         },
+
+         loadContent: function(url, data, method)
+         {
+            var returned;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'loadContent');
+
+            // Call API method and if return value is false, halt
+            returned = self.beforeContentLoad.call(self);
+            if(returned === false) return self;
+
+            // Load content using specified request type
+            if(method == 'post')
+               $.post(url, data, setupContent);
+            else
+               $.get(url, data, setupContent);
+
+            function setupContent(content)
+            {
+               // Call API method and log event
+               self.onContentLoad.call(self);
+               $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_CONTENT_LOADED, 'loadContent');
+
+               // Update the content
+               self.updateContent(content);
+            };
+
+            return self;
+         },
+
+         updateTitle: function(content)
+         {
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'updateTitle');
+
+            // Make sure content is defined before update
+            else if(!content)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.NO_CONTENT_PROVIDED, 'updateTitle');
+
+            // Call API method and if return value is false, halt
+            returned = self.beforeTitleUpdate.call(self);
+            if(returned === false) return self;
+
+            // Set the new content and reappend the button if enabled
+            if(self.elements.button) self.elements.button = self.elements.button.clone(true);
+            self.elements.title.html(content)
+            if(self.elements.button) self.elements.title.prepend(self.elements.button);
+
+            // Call API method and log event
+            self.onTitleUpdate.call(self);
+            return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_TITLE_UPDATED, 'updateTitle');
+         },
+
+         focus: function(event)
+         {
+            var curIndex, newIndex, elemIndex, returned;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'focus');
+
+            else if(self.options.position.type == 'static')
+               return $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.CANNOT_FOCUS_STATIC, 'focus');
+
+            // Set z-index variables
+            curIndex = parseInt( self.elements.tooltip.css('z-index') );
+            newIndex = 6000 + $('div.qtip[qtip]').length - 1;
+
+            // Only update the z-index if it has changed and tooltip is not already focused
+            if(!self.status.focused && curIndex !== newIndex)
+            {
+               // Call API method and if return value is false, halt
+               returned = self.beforeFocus.call(self, event);
+               if(returned === false) return self;
+
+               // Loop through all other tooltips
+               $('div.qtip[qtip]').not(self.elements.tooltip).each(function()
+               {
+                  if($(this).qtip('api').status.rendered === true)
+                  {
+                     elemIndex = parseInt($(this).css('z-index'));
+
+                     // Reduce all other tooltip z-index by 1
+                     if(typeof elemIndex == 'number' && elemIndex > -1)
+                        $(this).css({ zIndex: parseInt( $(this).css('z-index') ) - 1 });
+
+                     // Set focused status to false
+                     $(this).qtip('api').status.focused = false;
+                  }
+               })
+
+               // Set the new z-index and set focus status to true
+               self.elements.tooltip.css({ zIndex: newIndex });
+               self.status.focused = true;
+
+               // Call API method and log event
+               self.onFocus.call(self, event);
+               $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_FOCUSED, 'focus');
+            };
+
+            return self;
+         },
+
+         disable: function(state)
+         {
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'disable');
+
+            if(state)
+            {
+               // Tooltip is not already disabled, proceed
+               if(!self.status.disabled)
+               {
+                  // Set the disabled flag and log event
+                  self.status.disabled = true;
+                  $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_DISABLED, 'disable');
+               }
+
+               // Tooltip is already disabled, inform user via log
+               else  $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.TOOLTIP_ALREADY_DISABLED, 'disable');
+            }
+            else
+            {
+               // Tooltip is not already enabled, proceed
+               if(self.status.disabled)
+               {
+                  // Reassign events, set disable status and log
+                  self.status.disabled = false;
+                  $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_ENABLED, 'disable');
+               }
+
+               // Tooltip is already enabled, inform the user via log
+               else $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.TOOLTIP_ALREADY_ENABLED, 'disable');
+            };
+
+            return self;
+         },
+
+         destroy: function()
+         {
+            var i, returned, interfaces;
+
+            // Call API method and if return value is false, halt
+            returned = self.beforeDestroy.call(self);
+            if(returned === false) return self;
+
+            // Check if tooltip is rendered
+            if(self.status.rendered)
+            {
+               // Remove event handlers and remove element
+               self.options.show.when.target.unbind('mousemove.qtip', self.updatePosition);
+               self.options.show.when.target.unbind('mouseout.qtip', self.hide);
+               self.options.show.when.target.unbind(self.options.show.when.event + '.qtip');
+               self.options.hide.when.target.unbind(self.options.hide.when.event + '.qtip');
+               self.elements.tooltip.unbind(self.options.hide.when.event + '.qtip');
+               self.elements.tooltip.unbind('mouseover.qtip', self.focus);
+               self.elements.tooltip.remove();
+            }
+
+            // Tooltip isn't yet rendered, remove render event
+            else self.options.show.when.target.unbind(self.options.show.when.event+'.qtip-create');
+
+            // Check to make sure qTip data is present on target element
+            if(typeof self.elements.target.data('qtip') == 'object')
+            {
+               // Remove API references from interfaces object
+               interfaces = self.elements.target.data('qtip').interfaces;
+               if(typeof interfaces == 'object' && interfaces.length > 0)
+               {
+                  // Remove API from interfaces array
+                  for(i = 0; i < interfaces.length - 1; i++)
+                     if(interfaces[i].id == self.id) interfaces.splice(i, 1)
+               }
+            }
+            delete $.fn.qtip.interfaces[self.id];
+
+            // Set qTip current id to previous tooltips API if available
+            if(typeof interfaces == 'object' && interfaces.length > 0)
+               self.elements.target.data('qtip').current = interfaces.length -1;
+            else
+               self.elements.target.removeData('qtip');
+
+            // Call API method and log destroy
+            self.onDestroy.call(self);
+            $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_DESTROYED, 'destroy');
+
+            return self.elements.target
+         },
+
+         getPosition: function()
+         {
+            var show, offset;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'getPosition');
+
+            show = (self.elements.tooltip.css('display') !== 'none') ? false : true;
+
+            // Show and hide tooltip to make sure coordinates are returned
+            if(show) self.elements.tooltip.css({ visiblity: 'hidden' }).show();
+            offset = self.elements.tooltip.offset();
+            if(show) self.elements.tooltip.css({ visiblity: 'visible' }).hide();
+
+            return offset;
+         },
+
+         getDimensions: function()
+         {
+            var show, dimensions;
+
+            // Make sure tooltip is rendered and if not, return
+            if(!self.status.rendered)
+               return $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.TOOLTIP_NOT_RENDERED, 'getDimensions');
+
+            show = (!self.elements.tooltip.is(':visible')) ? true : false;
+
+            // Show and hide tooltip to make sure dimensions are returned
+            if(show) self.elements.tooltip.css({ visiblity: 'hidden' }).show();
+            dimensions = {
+               height: self.elements.tooltip.outerHeight(),
+               width: self.elements.tooltip.outerWidth()
+            };
+            if(show) self.elements.tooltip.css({ visiblity: 'visible' }).hide();
+
+            return dimensions;
+         }
+      });
+   };
+
+   // Define priamry construct function
+   function construct()
+   {
+      var self, adjust, content, url, data, method, tempLength;
+      self = this;
+
+      // Call API method
+      self.beforeRender.call(self);
+
+      // Set rendered status to true
+      self.status.rendered = true;
+
+      // Create initial tooltip elements
+      self.elements.tooltip =  '<div qtip="'+self.id+'" ' +
+         'class="qtip '+(self.options.style.classes.tooltip || self.options.style)+'"' +
+         'style="display:none; -moz-border-radius:0; -webkit-border-radius:0; border-radius:0;' +
+         'position:'+self.options.position.type+';">' +
+         '  <div class="qtip-wrapper" style="position:relative; overflow:hidden; text-align:left;">' +
+         '    <div class="qtip-contentWrapper" style="overflow:hidden;">' +
+         '       <div class="qtip-content '+self.options.style.classes.content+'"></div>' +
+         '</div></div></div>';
+
+      // Append to container element
+      self.elements.tooltip = $(self.elements.tooltip);
+      self.elements.tooltip.appendTo(self.options.position.container)
+
+      // Setup tooltip qTip data
+      self.elements.tooltip.data('qtip', { current: 0, interfaces: [self] });
+
+      // Setup element references
+      self.elements.wrapper = self.elements.tooltip.children('div:first');
+      self.elements.contentWrapper = self.elements.wrapper.children('div:first').css({ background: self.options.style.background });
+      self.elements.content = self.elements.contentWrapper.children('div:first').css( jQueryStyle(self.options.style) );
+
+      // Apply IE hasLayout fix to wrapper and content elements
+      if($.browser.msie) self.elements.wrapper.add(self.elements.content).css({ zoom: 1 });
+
+      // Setup tooltip attributes
+      if(self.options.hide.when.event == 'unfocus') self.elements.tooltip.attr('unfocus', true);
+
+      // If an explicit width is set, updateWidth prior to setting content to prevent dirty rendering
+      if(typeof self.options.style.width.value == 'number') self.updateWidth();
+
+      // Create borders and tips if supported by the browser
+      if($('<canvas>').get(0).getContext || $.browser.msie)
+      {
+         // Create border
+         if(self.options.style.border.radius > 0)
+            createBorder.call(self);
+         else
+            self.elements.contentWrapper.css({ border: self.options.style.border.width+'px solid '+self.options.style.border.color  });
+
+         // Create tip if enabled
+         if(self.options.style.tip.corner !== false)
+            createTip.call(self);
+      }
+
+      // Neither canvas or VML is supported, tips and borders cannot be drawn!
+      else
+      {
+         // Set defined border width
+         self.elements.contentWrapper.css({ border: self.options.style.border.width+'px solid '+self.options.style.border.color  });
+
+         // Reset border radius and tip
+         self.options.style.border.radius = 0;
+         self.options.style.tip.corner = false;
+
+         // Inform via log
+         $.fn.qtip.log.error.call(self, 2, $.fn.qtip.constants.CANVAS_VML_NOT_SUPPORTED, 'render');
+      };
+
+      // Use the provided content string or DOM array
+      if((typeof self.options.content.text == 'string' && self.options.content.text.length > 0)
+      || (self.options.content.text.jquery && self.options.content.text.length > 0))
+         content = self.options.content.text;
+
+      // Use title string for content if present
+      else if(typeof self.elements.target.attr('title') == 'string' && self.elements.target.attr('title').length > 0)
+      {
+         content = self.elements.target.attr('title').replace("\\n", '<br />');
+         self.elements.target.attr('title', ''); // Remove title attribute to prevent default tooltip showing
+      }
+
+      // No title is present, use alt attribute instead
+      else if(typeof self.elements.target.attr('alt') == 'string' && self.elements.target.attr('alt').length > 0)
+      {
+         content = self.elements.target.attr('alt').replace("\\n", '<br />');
+         self.elements.target.attr('alt', ''); // Remove alt attribute to prevent default tooltip showing
+      }
+
+      // No valid content was provided, inform via log
+      else
+      {
+         content = ' ';
+         $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.NO_VALID_CONTENT, 'render');
+      };
+
+      // Set the tooltips content and create title if enabled
+      if(self.options.content.title.text !== false) createTitle.call(self);
+      self.updateContent(content);
+
+      // Assign events and toggle tooltip with focus
+      assignEvents.call(self);
+      if(self.options.show.ready === true) self.show();
+
+      // Retrieve ajax content if provided
+      if(self.options.content.url !== false)
+      {
+         url = self.options.content.url;
+         data = self.options.content.data;
+         method = self.options.content.method || 'get';
+         self.loadContent(url, data, method);
+      };
+
+      // Call API method and log event
+      self.onRender.call(self);
+      $.fn.qtip.log.error.call(self, 1, $.fn.qtip.constants.EVENT_RENDERED, 'render');
+   };
+
+   // Create borders using canvas and VML
+   function createBorder()
+   {
+      var self, i, width, radius, color, coordinates, containers, size, betweenWidth, betweenCorners, borderTop, borderBottom, borderCoord, sideWidth, vertWidth;
+      self = this;
+
+      // Destroy previous border elements, if present
+      self.elements.wrapper.find('.qtip-borderBottom, .qtip-borderTop').remove();
+
+      // Setup local variables
+      width = self.options.style.border.width;
+      radius = self.options.style.border.radius;
+      color = self.options.style.border.color || self.options.style.tip.color;
+
+      // Calculate border coordinates
+      coordinates = calculateBorders(radius);
+
+      // Create containers for the border shapes
+      containers = {};
+      for(i in coordinates)
+      {
+         // Create shape container
+         containers[i] = '<div rel="'+i+'" style="'+((i.search(/Left/) !== -1) ? 'left' : 'right') + ':0; ' +
+            'position:absolute; height:'+radius+'px; width:'+radius+'px; overflow:hidden; line-height:0.1px; font-size:1px">';
+
+         // Canvas is supported
+         if($('<canvas>').get(0).getContext)
+            containers[i] += '<canvas height="'+radius+'" width="'+radius+'" style="vertical-align: top"></canvas>';
+
+         // No canvas, but if it's IE use VML
+         else if($.browser.msie)
+         {
+            size = radius * 2 + 3;
+            containers[i] += '<v:arc stroked="false" fillcolor="'+color+'" startangle="'+coordinates[i][0]+'" endangle="'+coordinates[i][1]+'" ' +
+               'style="width:'+size+'px; height:'+size+'px; margin-top:'+((i.search(/bottom/) !== -1) ? -2 : -1)+'px; ' +
+               'margin-left:'+((i.search(/Right/) !== -1) ? coordinates[i][2] - 3.5 : -1)+'px; ' +
+               'vertical-align:top; display:inline-block; behavior:url(#default#VML)"></v:arc>';
+
+         };
+
+         containers[i] += '</div>';
+      };
+
+      // Create between corners elements
+      betweenWidth = self.getDimensions().width - (Math.max(width, radius) * 2);
+      betweenCorners = '<div class="qtip-betweenCorners" style="height:'+radius+'px; width:'+betweenWidth+'px; ' +
+         'overflow:hidden; background-color:'+color+'; line-height:0.1px; font-size:1px;">';
+
+      // Create top border container
+      borderTop = '<div class="qtip-borderTop" dir="ltr" style="height:'+radius+'px; ' +
+         'margin-left:'+radius+'px; line-height:0.1px; font-size:1px; padding:0;">' +
+         containers['topLeft'] + containers['topRight'] + betweenCorners;
+      self.elements.wrapper.prepend(borderTop);
+
+      // Create bottom border container
+      borderBottom = '<div class="qtip-borderBottom" dir="ltr" style="height:'+radius+'px; ' +
+         'margin-left:'+radius+'px; line-height:0.1px; font-size:1px; padding:0;">' +
+         containers['bottomLeft'] + containers['bottomRight'] + betweenCorners;
+      self.elements.wrapper.append(borderBottom);
+
+      // Draw the borders if canvas were used (Delayed til after DOM creation)
+      if($('<canvas>').get(0).getContext)
+      {
+         self.elements.wrapper.find('canvas').each(function()
+         {
+            borderCoord = coordinates[ $(this).parent('[rel]:first').attr('rel') ];
+            drawBorder.call(self, $(this), borderCoord, radius, color);
+         })
+      }
+
+      // Create a phantom VML element (IE won't show the last created VML element otherwise)
+      else if($.browser.msie) self.elements.tooltip.append('<v:image style="behavior:url(#default#VML);"></v:image>');
+
+      // Setup contentWrapper border
+      sideWidth = Math.max(radius, (radius + (width - radius)) )
+      vertWidth = Math.max(width - radius, 0);
+      self.elements.contentWrapper.css({
+         border: '0px solid ' + color,
+         borderWidth: vertWidth + 'px ' + sideWidth + 'px'
+      })
+   };
+
+   // Border canvas draw method
+   function drawBorder(canvas, coordinates, radius, color)
+   {
+      // Create corner
+      var context = canvas.get(0).getContext('2d');
+      context.fillStyle = color;
+      context.beginPath();
+      context.arc(coordinates[0], coordinates[1], radius, 0, Math.PI * 2, false);
+      context.fill();
+   };
+
+   // Create tip using canvas and VML
+   function createTip(corner)
+   {
+      var self, color, coordinates, coordsize, path;
+      self = this;
+
+      // Destroy previous tip, if there is one
+      if(self.elements.tip !== null) self.elements.tip.remove();
+
+      // Setup color and corner values
+      color = self.options.style.tip.color || self.options.style.border.color;
+      if(self.options.style.tip.corner === false) return;
+      else if(!corner) corner = self.options.style.tip.corner;
+
+      // Calculate tip coordinates
+      coordinates = calculateTip(corner, self.options.style.tip.size.width, self.options.style.tip.size.height);
+
+      // Create tip element
+      self.elements.tip =  '<div class="'+self.options.style.classes.tip+'" dir="ltr" rel="'+corner+'" style="position:absolute; ' +
+         'height:'+self.options.style.tip.size.height+'px; width:'+self.options.style.tip.size.width+'px; ' +
+         'margin:0 auto; line-height:0.1px; font-size:1px;">';
+
+      // Use canvas element if supported
+      if($('<canvas>').get(0).getContext)
+          self.elements.tip += '<canvas height="'+self.options.style.tip.size.height+'" width="'+self.options.style.tip.size.width+'"></canvas>';
+
+      // Canvas not supported - Use VML (IE)
+      else if($.browser.msie)
+      {
+         // Create coordize and tip path using tip coordinates
+         coordsize = self.options.style.tip.size.width + ',' + self.options.style.tip.size.height;
+         path = 'm' + coordinates[0][0] + ',' + coordinates[0][1];
+         path += ' l' + coordinates[1][0] + ',' + coordinates[1][1];
+         path += ' ' + coordinates[2][0] + ',' + coordinates[2][1];
+         path += ' xe';
+
+         // Create VML element
+         self.elements.tip += '<v:shape fillcolor="'+color+'" stroked="false" filled="true" path="'+path+'" coordsize="'+coordsize+'" ' +
+            'style="width:'+self.options.style.tip.size.width+'px; height:'+self.options.style.tip.size.height+'px; ' +
+            'line-height:0.1px; display:inline-block; behavior:url(#default#VML); ' +
+            'vertical-align:'+((corner.search(/top/) !== -1) ? 'bottom' : 'top')+'"></v:shape>';
+
+         // Create a phantom VML element (IE won't show the last created VML element otherwise)
+         self.elements.tip += '<v:image style="behavior:url(#default#VML);"></v:image>';
+
+         // Prevent tooltip appearing above the content (IE z-index bug)
+         self.elements.contentWrapper.css('position', 'relative');
+      };
+
+      // Attach new tip to tooltip element
+      self.elements.tooltip.prepend(self.elements.tip + '</div>');
+
+      // Create element reference and draw the canvas tip (Delayed til after DOM creation)
+      self.elements.tip = self.elements.tooltip.find('.'+self.options.style.classes.tip).eq(0);
+      if($('<canvas>').get(0).getContext)
+         drawTip.call(self, self.elements.tip.find('canvas:first'), coordinates, color);
+
+      // Fix IE small tip bug
+      if(corner.search(/top/) !== -1 && $.browser.msie && parseInt($.browser.version.charAt(0)) === 6)
+         self.elements.tip.css({ marginTop: -4 });
+
+      // Set the tip position
+      positionTip.call(self, corner);
+   };
+
+   // Canvas tip drawing method
+   function drawTip(canvas, coordinates, color)
+   {
+      // Setup properties
+      var context = canvas.get(0).getContext('2d');
+      context.fillStyle = color;
+
+      // Create tip
+      context.beginPath();
+      context.moveTo(coordinates[0][0], coordinates[0][1]);
+      context.lineTo(coordinates[1][0], coordinates[1][1]);
+      context.lineTo(coordinates[2][0], coordinates[2][1]);
+      context.fill();
+   };
+
+   function positionTip(corner)
+   {
+      var self, ieAdjust, paddingCorner, paddingSize, newMargin;
+      self = this;
+
+      // Return if tips are disabled or tip is not yet rendered
+      if(self.options.style.tip.corner === false || !self.elements.tip) return;
+      if(!corner) corner = self.elements.tip.attr('rel');
+
+      // Setup adjustment variables
+      ieAdjust = positionAdjust = ($.browser.msie) ? 1 : 0;
+
+      // Set initial position
+      self.elements.tip.css(corner.match(/left|right|top|bottom/)[0], 0);
+
+      // Set position of tip to correct side
+      if(corner.search(/top|bottom/) !== -1)
+      {
+         // Adjustments for IE6 - 0.5px border gap bug
+         if($.browser.msie)
+         {
+            if(parseInt($.browser.version.charAt(0)) === 6)
+               positionAdjust = (corner.search(/top/) !== -1) ? -3 : 1;
+            else
+               positionAdjust = (corner.search(/top/) !== -1) ? 1 : 2;
+         };
+
+         if(corner.search(/Middle/) !== -1)
+            self.elements.tip.css({ left: '50%', marginLeft: -(self.options.style.tip.size.width / 2) });
+
+         else if(corner.search(/Left/) !== -1)
+            self.elements.tip.css({ left: self.options.style.border.radius - ieAdjust });
+
+         else if(corner.search(/Right/) !== -1)
+            self.elements.tip.css({ right: self.options.style.border.radius + ieAdjust });
+
+         if(corner.search(/top/) !== -1)
+            self.elements.tip.css({ top: -positionAdjust });
+         else
+            self.elements.tip.css({ bottom: positionAdjust });
+
+      }
+      else if(corner.search(/left|right/) !== -1)
+      {
+         // Adjustments for IE6 - 0.5px border gap bug
+         if($.browser.msie)
+            positionAdjust = (parseInt($.browser.version.charAt(0)) === 6) ? 1 : ((corner.search(/left/) !== -1) ? 1 : 2);
+
+         if(corner.search(/Middle/) !== -1)
+            self.elements.tip.css({ top: '50%', marginTop: -(self.options.style.tip.size.height / 2) });
+
+         else if(corner.search(/Top/) !== -1)
+            self.elements.tip.css({ top: self.options.style.border.radius - ieAdjust });
+
+         else if(corner.search(/Bottom/) !== -1)
+            self.elements.tip.css({ bottom: self.options.style.border.radius + ieAdjust });
+
+         if(corner.search(/left/) !== -1)
+            self.elements.tip.css({ left: -positionAdjust });
+         else
+            self.elements.tip.css({ right: positionAdjust });
+      };
+
+      // Adjust tooltip padding to compensate for tip
+      paddingCorner = 'padding-' + corner.match(/left|right|top|bottom/)[0];
+      paddingSize = self.options.style.tip.size[ (paddingCorner.search(/left|right/) !== -1) ? 'width' : 'height' ];
+      self.elements.tooltip.css('padding', 0);
+      self.elements.tooltip.css(paddingCorner, paddingSize);
+
+      // Match content margin to prevent gap bug in IE6 ONLY
+      if($.browser.msie && parseInt($.browser.version.charAt(0)) == 6)
+      {
+         newMargin = parseInt(self.elements.tip.css('margin-top')) || 0;
+         newMargin += parseInt(self.elements.content.css('margin-top')) || 0;
+
+         self.elements.tip.css({ marginTop: newMargin });
+      };
+   };
+
+   // Create title bar for content
+   function createTitle()
+   {
+      var self = this;
+
+      // Destroy previous title element, if present
+      if(self.elements.title !== null) self.elements.title.remove();
+
+      // Create title element
+      self.elements.title = $('<div class="'+self.options.style.classes.title+'">')
+         .css( jQueryStyle(self.options.style.title, true) )
+         .css({ zoom: ($.browser.msie) ? 1 : 0 })
+         .prependTo(self.elements.contentWrapper);
+
+      // Update title with contents if enabled
+      if(self.options.content.title.text) self.updateTitle.call(self, self.options.content.title.text);
+
+      // Create title close buttons if enabled
+      if(self.options.content.title.button !== false
+      && typeof self.options.content.title.button == 'string')
+      {
+         self.elements.button = $('<a class="'+self.options.style.classes.button+'" style="float:right; position: relative"></a>')
+            .css( jQueryStyle(self.options.style.button, true) )
+            .html(self.options.content.title.button)
+            .prependTo(self.elements.title)
+            .click(function(event){ if(!self.status.disabled) self.hide(event) });
+      };
+   };
+
+   // Assign hide and show events
+   function assignEvents()
+   {
+      var self, showTarget, hideTarget, inactiveEvents;
+      self = this;
+
+      // Setup event target variables
+      showTarget = self.options.show.when.target;
+      hideTarget = self.options.hide.when.target;
+
+      // Add tooltip as a hideTarget is its fixed
+      if(self.options.hide.fixed) hideTarget = hideTarget.add(self.elements.tooltip);
+
+      // Check if the hide event is special 'inactive' type
+      if(self.options.hide.when.event == 'inactive')
+      {
+         // Define events which reset the 'inactive' event handler
+         inactiveEvents = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove',
+         'mouseout', 'mouseenter', 'mouseleave', 'mouseover' ];
+
+         // Define 'inactive' event timer method
+         function inactiveMethod(event)
+         {
+            if(self.status.disabled === true) return;
+
+            //Clear and reset the timer
+            clearTimeout(self.timers.inactive);
+            self.timers.inactive = setTimeout(function()
+            {
+               // Unassign 'inactive' events
+               $(inactiveEvents).each(function()
+               {
+                  hideTarget.unbind(this+'.qtip-inactive');
+                  self.elements.content.unbind(this+'.qtip-inactive');
+               });
+
+               // Hide the tooltip
+               self.hide(event);
+            }
+            , self.options.hide.delay);
+         };
+      }
+
+      // Check if the tooltip is 'fixed'
+      else if(self.options.hide.fixed === true)
+      {
+         self.elements.tooltip.bind('mouseover.qtip', function()
+         {
+            if(self.status.disabled === true) return;
+
+            // Reset the hide timer
+            clearTimeout(self.timers.hide);
+         });
+      };
+
+      // Define show event method
+      function showMethod(event)
+      {
+         if(self.status.disabled === true) return;
+
+         // If set, hide tooltip when inactive for delay period
+         if(self.options.hide.when.event == 'inactive')
+         {
+            // Assign each reset event
+            $(inactiveEvents).each(function()
+            {
+               hideTarget.bind(this+'.qtip-inactive', inactiveMethod);
+               self.elements.content.bind(this+'.qtip-inactive', inactiveMethod);
+            });
+
+            // Start the inactive timer
+            inactiveMethod();
+         };
+
+         // Clear hide timers
+         clearTimeout(self.timers.show);
+         clearTimeout(self.timers.hide);
+
+         // Start show timer
+         self.timers.show = setTimeout(function(){ self.show(event); }, self.options.show.delay);
+      };
+
+      // Define hide event method
+      function hideMethod(event)
+      {
+         if(self.status.disabled === true) return;
+
+         // Prevent hiding if tooltip is fixed and event target is the tooltip
+         if(self.options.hide.fixed === true
+         && self.options.hide.when.event.search(/mouse(out|leave)/i) !== -1
+         && $(event.relatedTarget).parents('div.qtip[qtip]').length > 0)
+         {
+            // Prevent default and popagation
+            event.stopPropagation();
+            event.preventDefault();
+
+            // Reset the hide timer
+            clearTimeout(self.timers.hide);
+            return false;
+         };
+
+         // Clear timers and stop animation queue
+         clearTimeout(self.timers.show);
+         clearTimeout(self.timers.hide);
+         self.elements.tooltip.stop(true, true);
+
+         // If tooltip has displayed, start hide timer
+         self.timers.hide = setTimeout(function(){ self.hide(event); }, self.options.hide.delay);
+      };
+
+      // Both events and targets are identical, apply events using a toggle
+      if((self.options.show.when.target.add(self.options.hide.when.target).length === 1
+      && self.options.show.when.event == self.options.hide.when.event
+      && self.options.hide.when.event !== 'inactive')
+      || self.options.hide.when.event == 'unfocus')
+      {
+         self.cache.toggle = 0;
+         // Use a toggle to prevent hide/show conflicts
+         showTarget.bind(self.options.show.when.event + '.qtip', function(event)
+         {
+            if(self.cache.toggle == 0) showMethod(event);
+            else hideMethod(event);
+         });
+      }
+
+      // Events are not identical, bind normally
+      else
+      {
+         showTarget.bind(self.options.show.when.event + '.qtip', showMethod);
+
+         // If the hide event is not 'inactive', bind the hide method
+         if(self.options.hide.when.event !== 'inactive')
+            hideTarget.bind(self.options.hide.when.event + '.qtip', hideMethod);
+      };
+
+      // Focus the tooltip on mouseover
+      if(self.options.position.type.search(/(fixed|absolute)/) !== -1)
+         self.elements.tooltip.bind('mouseover.qtip', self.focus);
+
+      // If mouse is the target, update tooltip position on mousemove
+      if(self.options.position.target === 'mouse' && self.options.position.type !== 'static')
+      {
+         showTarget.bind('mousemove.qtip', function(event)
+         {
+            // Set the new mouse positions if adjustment is enabled
+            self.cache.mouse = { x: event.pageX, y: event.pageY };
+
+            // Update the tooltip position only if the tooltip is visible and adjustment is enabled
+            if(self.status.disabled === false
+            && self.options.position.adjust.mouse === true
+            && self.options.position.type !== 'static'
+            && self.elements.tooltip.css('display') !== 'none')
+               self.updatePosition(event);
+         });
+      };
+   };
+
+   // Screen position adjustment
+   function screenAdjust(position, target, tooltip)
+   {
+      var self, adjustedPosition, adjust, newCorner, overflow, corner;
+      self = this;
+
+      // Setup corner and adjustment variable
+      if(tooltip.corner == 'center') return target.position // TODO: 'center' corner adjustment
+      adjustedPosition = $.extend({}, position);
+      newCorner = { x: false, y: false };
+
+      // Define overflow properties
+      overflow = {
+         left: (adjustedPosition.left < $.fn.qtip.cache.screen.scroll.left),
+         right: (adjustedPosition.left + tooltip.dimensions.width + 2 >= $.fn.qtip.cache.screen.width + $.fn.qtip.cache.screen.scroll.left),
+         top: (adjustedPosition.top < $.fn.qtip.cache.screen.scroll.top),
+         bottom: (adjustedPosition.top + tooltip.dimensions.height + 2 >= $.fn.qtip.cache.screen.height + $.fn.qtip.cache.screen.scroll.top)
+      };
+
+      // Determine new positioning properties
+      adjust = {
+         left: (overflow.left && (tooltip.corner.search(/right/i) != -1 || (tooltip.corner.search(/right/i) == -1 && !overflow.right))),
+         right: (overflow.right && (tooltip.corner.search(/left/i) != -1 || (tooltip.corner.search(/left/i) == -1 && !overflow.left))),
+         top: (overflow.top && tooltip.corner.search(/top/i) == -1),
+         bottom: (overflow.bottom && tooltip.corner.search(/bottom/i) == -1)
+      };
+
+      // Tooltip overflows off the left side of the screen
+      if(adjust.left)
+      {
+         if(self.options.position.target !== 'mouse')
+            adjustedPosition.left = target.position.left + target.dimensions.width;
+         else
+            adjustedPosition.left = self.cache.mouse.x
+
+         newCorner.x = 'Left';
+      }
+
+      // Tooltip overflows off the right side of the screen
+      else if(adjust.right)
+      {
+         if(self.options.position.target !== 'mouse')
+            adjustedPosition.left = target.position.left - tooltip.dimensions.width;
+         else
+            adjustedPosition.left = self.cache.mouse.x - tooltip.dimensions.width;
+
+         newCorner.x = 'Right';
+      };
+
+      // Tooltip overflows off the top of the screen
+      if(adjust.top)
+      {
+         if(self.options.position.target !== 'mouse')
+            adjustedPosition.top = target.position.top + target.dimensions.height;
+         else
+            adjustedPosition.top = self.cache.mouse.y
+
+         newCorner.y = 'top';
+      }
+
+      // Tooltip overflows off the bottom of the screen
+      else if(adjust.bottom)
+      {
+         if(self.options.position.target !== 'mouse')
+            adjustedPosition.top = target.position.top - tooltip.dimensions.height;
+         else
+            adjustedPosition.top = self.cache.mouse.y - tooltip.dimensions.height;
+
+         newCorner.y = 'bottom';
+      };
+
+      // Don't adjust if resulting position is negative
+      if(adjustedPosition.left < 0)
+      {
+         adjustedPosition.left = position.left;
+         newCorner.x = false;
+      };
+      if(adjustedPosition.top < 0)
+      {
+         adjustedPosition.top = position.top;
+         newCorner.y = false;
+      };
+
+      // Change tip corner if positioning has changed and tips are enabled
+      if(self.options.style.tip.corner !== false)
+      {
+         // Determine new corner properties
+         adjustedPosition.corner = new String(tooltip.corner);
+         if(newCorner.x !== false) adjustedPosition.corner = adjustedPosition.corner.replace(/Left|Right|Middle/, newCorner.x);
+         if(newCorner.y !== false) adjustedPosition.corner = adjustedPosition.corner.replace(/top|bottom/, newCorner.y);
+
+         // Adjust tip if position has changed and tips are enabled
+         if(adjustedPosition.corner !== self.elements.tip.attr('rel'))
+            createTip.call(self, adjustedPosition.corner);
+      };
+
+      return adjustedPosition;
+   };
+
+   // Build a jQuery style object from supplied style object
+   function jQueryStyle(style, sub)
+   {
+      var styleObj, i;
+
+      styleObj = $.extend(true, {}, style);
+      for(i in styleObj)
+      {
+         if(sub === true && i.search(/(tip|classes)/i) !== -1)
+            delete styleObj[i];
+         else if(!sub && i.search(/(width|border|tip|title|classes|user)/i) !== -1)
+            delete styleObj[i];
+      };
+
+      return styleObj;
+   };
+
+   // Sanitize styles
+   function sanitizeStyle(style)
+   {
+      if(typeof style.tip !== 'object') style.tip = { corner: style.tip };
+      if(typeof style.tip.size !== 'object') style.tip.size = { width: style.tip.size, height: style.tip.size };
+      if(typeof style.border !== 'object') style.border = { width: style.border };
+      if(typeof style.width !== 'object') style.width = { value: style.width };
+      if(typeof style.width.max == 'string') style.width.max = parseInt(style.width.max.replace(/([0-9]+)/i, "$1"));
+      if(typeof style.width.min == 'string') style.width.min = parseInt(style.width.min.replace(/([0-9]+)/i, "$1"));
+
+      // Convert deprecated x and y tip values to width/height
+      if(typeof style.tip.size.x == 'number')
+      {
+         style.tip.size.width = style.tip.size.x;
+         delete style.tip.size.x;
+      };
+      if(typeof style.tip.size.y == 'number')
+      {
+         style.tip.size.height = style.tip.size.y;
+         delete style.tip.size.y;
+      };
+
+      return style;
+   };
+
+   // Build styles recursively with inheritance
+   function buildStyle()
+   {
+      var self, i, styleArray, styleExtend, finalStyle, ieAdjust;
+      self = this;
+
+      // Build style options from supplied arguments
+      styleArray = [true, {}];
+      for(i = 0; i < arguments.length; i++)
+         styleArray.push(arguments[i]);
+      styleExtend = [ $.extend.apply($, styleArray) ];
+
+      // Loop through each named style inheritance
+      while(typeof styleExtend[0].name == 'string')
+      {
+         // Sanitize style data and append to extend array
+         styleExtend.unshift( sanitizeStyle($.fn.qtip.styles[ styleExtend[0].name ]) );
+      };
+
+      // Make sure resulting tooltip className represents final style
+      styleExtend.unshift(true, {classes:{ tooltip: 'qtip-' + (arguments[0].name || 'defaults') }}, $.fn.qtip.styles.defaults);
+
+      // Extend into a single style object
+      finalStyle = $.extend.apply($, styleExtend);
+
+      // Adjust tip size if needed (IE 1px adjustment bug fix)
+      ieAdjust = ($.browser.msie) ? 1 : 0;
+      finalStyle.tip.size.width += ieAdjust;
+      finalStyle.tip.size.height += ieAdjust;
+
+      // Force even numbers for pixel precision
+      if(finalStyle.tip.size.width % 2 > 0) finalStyle.tip.size.width += 1;
+      if(finalStyle.tip.size.height % 2 > 0) finalStyle.tip.size.height += 1;
+
+      // Sanitize final styles tip corner value
+      if(finalStyle.tip.corner === true)
+         finalStyle.tip.corner = (self.options.position.corner.tooltip === 'center') ? false : self.options.position.corner.tooltip;
+
+      return finalStyle;
+   };
+
+   // Tip coordinates calculator
+   function calculateTip(corner, width, height)
+   {
+      // Define tip coordinates in terms of height and width values
+      var tips = {
+         bottomRight:   [[0,0],              [width,height],      [width,0]],
+         bottomLeft:    [[0,0],              [width,0],           [0,height]],
+         topRight:      [[0,height],         [width,0],           [width,height]],
+         topLeft:       [[0,0],              [0,height],          [width,height]],
+         topMiddle:     [[0,height],         [width / 2,0],       [width,height]],
+         bottomMiddle:  [[0,0],              [width,0],           [width / 2,height]],
+         rightMiddle:   [[0,0],              [width,height / 2],  [0,height]],
+         leftMiddle:    [[width,0],          [width,height],      [0,height / 2]]
+      };
+      tips.leftTop = tips.bottomRight;
+      tips.rightTop = tips.bottomLeft;
+      tips.leftBottom = tips.topRight;
+      tips.rightBottom = tips.topLeft;
+
+      return tips[corner];
+   };
+
+   // Border coordinates calculator
+   function calculateBorders(radius)
+   {
+      var borders;
+
+      // Use canvas element if supported
+      if($('<canvas>').get(0).getContext)
+      {
+         borders = {
+            topLeft: [radius,radius], topRight: [0,radius],
+            bottomLeft: [radius,0], bottomRight: [0,0]
+         };
+      }
+
+      // Canvas not supported - Use VML (IE)
+      else if($.browser.msie)
+      {
+         borders = {
+            topLeft: [-90,90,0], topRight: [-90,90,-radius],
+            bottomLeft: [90,270,0], bottomRight: [90, 270,-radius]
+         };
+      };
+
+      return borders;
+   };
+
+   // BGIFRAME JQUERY PLUGIN ADAPTION
+   //   Special thanks to Brandon Aaron for this plugin
+   //   http://plugins.jquery.com/project/bgiframe
+   function bgiframe()
+   {
+      var self, html, dimensions;
+      self = this;
+      dimensions = self.getDimensions();
+
+      // Setup iframe HTML string
+      html = '<iframe class="qtip-bgiframe" frameborder="0" tabindex="-1" src="javascript:false" '+
+         'style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=\'0\'); border: 1px solid red; ' +
+         'height:'+dimensions.height+'px; width:'+dimensions.width+'px" />';
+
+      // Append the new HTML and setup element reference
+      self.elements.bgiframe = self.elements.wrapper.prepend(html).children('.qtip-bgiframe:first');
+   };
+
+   // Assign cache and event initialisation on document load
+   $(document).ready(function()
+   {
+      // Setup library cache with window scroll and dimensions of document
+      $.fn.qtip.cache = {
+         screen: {
+            scroll: { left: $(window).scrollLeft(), top: $(window).scrollTop() },
+            width: $(window).width(),
+            height: $(window).height()
+         }
+      };
+
+      // Adjust positions of the tooltips on window resize or scroll if enabled
+      var adjustTimer;
+      $(window).bind('resize scroll', function(event)
+      {
+         clearTimeout(adjustTimer);
+         adjustTimer = setTimeout(function()
+         {
+            // Readjust cached screen values
+            if(event.type === 'scroll')
+               $.fn.qtip.cache.screen.scroll = { left: $(window).scrollLeft(), top: $(window).scrollTop() };
+            else
+            {
+               $.fn.qtip.cache.screen.width = $(window).width();
+               $.fn.qtip.cache.screen.height = $(window).height();
+            };
+
+            for(i = 0; i < $.fn.qtip.interfaces.length; i++)
+            {
+               // Access current elements API
+               var api = $.fn.qtip.interfaces[i];
+
+               // Update position if resize or scroll adjustments are enabled
+               if(api.status.rendered === true
+               && (api.options.position.type !== 'static'
+               || api.options.position.adjust.scroll && event.type === 'scroll'
+               || api.options.position.adjust.resize && event.type === 'resize'))
+               {
+                  // Queue the animation so positions are updated correctly
+                  api.updatePosition(event, true);
+               }
+            };
+         }
+         , 100);
+      })
+
+      // Hide unfocus toolipts on document mousedown
+      $(document).bind('mousedown.qtip', function(event)
+      {
+         if($(event.target).parents('div.qtip').length === 0)
+         {
+            $('.qtip[unfocus]').each(function()
+            {
+               var api = $(this).qtip("api");
+
+               // Only hide if its visible and not the tooltips target
+               if($(this).is(':visible') && !api.status.disabled
+               && $(event.target).add(api.elements.target).length > 1)
+                  api.hide(event);
+            })
+         };
+      })
+   });
+
+   // Define qTip API interfaces array
+   $.fn.qtip.interfaces = []
+
+   // Define log and constant place holders
+   $.fn.qtip.log = { error: function(){ return this; } };
+   $.fn.qtip.constants = {};
+
+   // Define configuration defaults
+   $.fn.qtip.defaults = {
+      // Content
+      content: {
+         prerender: false,
+         text: false,
+         url: false,
+         data: null,
+         title: {
+            text: false,
+            button: false
+         }
+      },
+      // Position
+      position: {
+         target: false,
+         corner: {
+            target: 'bottomRight',
+            tooltip: 'topLeft'
+         },
+         adjust: {
+            x: 0, y: 0,
+            mouse: true,
+            screen: false,
+            scroll: true,
+            resize: true
+         },
+         type: 'absolute',
+         container: false
+      },
+      // Effects
+      show: {
+         when: {
+            target: false,
+            event: 'mouseover'
+         },
+         effect: {
+            type: 'fade',
+            length: 100
+         },
+         delay: 140,
+         solo: false,
+         ready: false
+      },
+      hide: {
+         when: {
+            target: false,
+            event: 'mouseout'
+         },
+         effect: {
+            type: 'fade',
+            length: 100
+         },
+         delay: 0,
+         fixed: false
+      },
+      // Callbacks
+      api: {
+         beforeRender: function(){},
+         onRender: function(){},
+         beforePositionUpdate: function(){},
+         onPositionUpdate: function(){},
+         beforeShow: function(){},
+         onShow: function(){},
+         beforeHide: function(){},
+         onHide: function(){},
+         beforeContentUpdate: function(){},
+         onContentUpdate: function(){},
+         beforeContentLoad: function(){},
+         onContentLoad: function(){},
+         beforeTitleUpdate: function(){},
+         onTitleUpdate: function(){},
+         beforeDestroy: function(){},
+         onDestroy: function(){},
+         beforeFocus: function(){},
+         onFocus: function(){}
+      }
+   };
+
+   $.fn.qtip.styles = {
+      defaults: {
+         background: 'white',
+         color: '#111',
+         overflow: 'hidden',
+         textAlign: 'left',
+         width: {
+            min: 0,
+            max: 250
+         },
+         padding: '5px 9px',
+         border: {
+            width: 1,
+            radius: 0,
+            color: '#d3d3d3'
+         },
+         tip: {
+            corner: false,
+            color: false,
+            size: { width: 13, height: 13 },
+            opacity: 1
+         },
+         title: {
+            background: '#e1e1e1',
+            fontWeight: 'bold',
+            padding: '7px 12px'
+         },
+         button: {
+            cursor: 'pointer'
+         },
+         classes: {
+            target: '',
+            tip: 'qtip-tip',
+            title: 'qtip-title',
+            button: 'qtip-button',
+            content: 'qtip-content',
+            active: 'qtip-active'
+         }
+      },
+      cream: {
+         border: {
+            width: 3,
+            radius: 0,
+            color: '#F9E98E'
+         },
+         title: {
+            background: '#F0DE7D',
+            color: '#A27D35'
+         },
+         background: '#FBF7AA',
+         color: '#A27D35',
+
+         classes: { tooltip: 'qtip-cream' }
+      },
+      light: {
+         border: {
+            width: 3,
+            radius: 0,
+            color: '#E2E2E2'
+         },
+         title: {
+            background: '#f1f1f1',
+            color: '#454545'
+         },
+         background: 'white',
+         color: '#454545',
+
+         classes: { tooltip: 'qtip-light' }
+      },
+      dark: {
+         border: {
+            width: 3,
+            radius: 0,
+            color: '#303030'
+         },
+         title: {
+            background: '#404040',
+            color: '#f3f3f3'
+         },
+         background: '#505050',
+         color: '#f3f3f3',
+
+         classes: { tooltip: 'qtip-dark' }
+      },
+      red: {
+         border: {
+            width: 3,
+            radius: 0,
+            color: '#CE6F6F'
+         },
+         title: {
+            background: '#f28279',
+            color: '#9C2F2F'
+         },
+         background: '#F79992',
+         color: '#9C2F2F',
+
+         classes: { tooltip: 'qtip-red' }
+      },
+      green: {
+         border: {
+            width: 3,
+            radius: 0,
+            color: '#A9DB66'
+         },
+         title: {
+            background: '#b9db8c',
+            color: '#58792E'
+         },
+         background: '#CDE6AC',
+         color: '#58792E',
+
+         classes: { tooltip: 'qtip-green' }
+      },
+      blue: {
+         border: {
+            width: 3,
+            radius: 0,
+            color: '#ADD9ED'
+         },
+         title: {
+            background: '#D0E9F5',
+            color: '#5E99BD'
+         },
+         background: '#E5F6FE',
+         color: '#4D9FBF',
+
+         classes: { tooltip: 'qtip-blue' }
+      }
+   };
+})(jQuery);
\ No newline at end of file
--- a/web/data/jquery.treeview.css	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-.treeview, .treeview ul { 
-  padding: 0;
-  margin: 0;
-  list-style: none;
-}
-
-.treeview ul {
-  background-color: white;
-  margin-top: 4px;
-}
-
-.treeview .hitarea {
-  background: url(treeview-default.gif) -64px -25px no-repeat;
-  height: 16px;
-  width: 16px;
-  margin-left: -16px; 
-  float: left;
-  cursor: pointer;
-}
-/* fix for IE6 */
-* html .hitarea {
-  display: inline;
-  float:none;
-}
-
-.treeview li { 
-  margin: 0;
-  padding: 3px 0pt 3px 16px;
-}
-
-.treeview a.selected {
-  background-color: #eee;
-}
-
-#treecontrol { margin: 1em 0; display: none; }
-
-.treeview .hover { color: red; cursor: pointer; }
-
-.treeview li { background: url(treeview-default-line.gif) 0 0 no-repeat; }
-.treeview li.collapsable, .treeview li.expandable { background-position: 0 -176px; }
-
-.treeview .expandable-hitarea { background-position: -80px -3px; }
-
-.treeview li.last { background-position: 0 -1766px }
-.treeview li.lastCollapsable, .treeview li.lastExpandable { background-image: url(treeview-default.gif); }  
-.treeview li.lastCollapsable { background-position: 0 -111px }
-.treeview li.lastExpandable { background-position: -32px -67px }
-
-.treeview div.lastCollapsable-hitarea, .treeview div.lastExpandable-hitarea { background-position: 0; }
-
-.treeview-red li { background-image: url(treeview-red-line.gif); }
-.treeview-red .hitarea, .treeview-red li.lastCollapsable, .treeview-red li.lastExpandable { background-image: url(treeview-red.gif); } 
-
-.treeview-black li { background-image: url(treeview-black-line.gif); }
-.treeview-black .hitarea, .treeview-black li.lastCollapsable, .treeview-black li.lastExpandable { background-image: url(treeview-black.gif); }  
-
-.treeview-gray li { background-image: url(treeview-gray-line.gif); }
-.treeview-gray .hitarea, .treeview-gray li.lastCollapsable, .treeview-gray li.lastExpandable { background-image: url(treeview-gray.gif); } 
-
-.treeview-famfamfam li { background-image: url(treeview-famfamfam-line.gif); }
-.treeview-famfamfam .hitarea, .treeview-famfamfam li.lastCollapsable, .treeview-famfamfam li.lastExpandable { background-image: url(treeview-famfamfam.gif); } 
-
-
-.filetree li { padding: 3px 0 2px 16px; }
-.filetree span.folder, .filetree span.file { padding: 1px 0 1px 16px; }
-.filetree span.folder { background: url(folder.gif) 0 0 no-repeat; }
-.filetree li.expandable span.folder { background: url(folder-closed.gif) 0 0 no-repeat; }
-.filetree span.file { background: url(file.gif) 0 0 no-repeat; }
-
-/* added by adim */
-ul.placeholder { display: none; }
\ No newline at end of file
--- a/web/data/jquery.treeview.js	Thu Jan 22 17:39:07 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-/*
- * Treeview 1.4 - jQuery plugin to hide and show branches of a tree
- *
- * http://bassistance.de/jquery-plugins/jquery-plugin-treeview/
- * http://docs.jquery.com/Plugins/Treeview
- *
- * Copyright (c) 2007 Jörn Zaefferer
- *
- * Dual licensed under the MIT and GPL licenses:
- *   http://www.opensource.org/licenses/mit-license.php
- *   http://www.gnu.org/licenses/gpl.html
- *
- * Revision: $Id: jquery.treeview.js 4684 2010-02-07 19:08:06Z joern.zaefferer $
- * updated by Aurelien Campeas, 2010-09-01, to handle top-level ajax loads
- *
- */;(function($){$.extend($.fn,{swapClass:function(c1,c2){var c1Elements=this.filter('.'+c1);this.filter('.'+c2).removeClass(c2).addClass(c1);c1Elements.removeClass(c1).addClass(c2);return this;},replaceClass:function(c1,c2){return this.filter('.'+c1).removeClass(c1).addClass(c2).end();},hoverClass:function(className){className=className||"hover";return this.hover(function(){$(this).addClass(className);},function(){$(this).removeClass(className);});},heightToggle:function(animated,callback){animated?this.animate({height:"toggle"},animated,callback):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();if(callback)callback.apply(this,arguments);});},heightHide:function(animated,callback){if(animated){this.animate({height:"hide"},animated,callback);}else{this.hide();if(callback)this.each(callback);}},prepareBranches:function(settings){if(!settings.prerendered){this.filter(":last-child:not(ul)").addClass(CLASSES.last);this.filter((settings.collapsed?"":"."+CLASSES.closed)+":not(."+CLASSES.open+")").find(">ul").hide();}return this.filter(":has(>ul)");},applyClasses:function(settings,toggler){this.filter(":has(>ul):not(:has(>a))").find(">span").click(function(event){toggler.apply($(this).next());}).add($("a",this)).hoverClass();if(!settings.prerendered){this.filter(":has(>ul:hidden)").addClass(CLASSES.expandable).replaceClass(CLASSES.last,CLASSES.lastExpandable);this.not(":has(>ul:hidden)").addClass(CLASSES.collapsable).replaceClass(CLASSES.last,CLASSES.lastCollapsable);this.prepend("<div class=\""+CLASSES.hitarea+"\"/>").find("div."+CLASSES.hitarea).each(function(){var classes="";$.each($(this).parent().attr("class").split(" "),function(){classes+=this+"-hitarea ";});$(this).addClass(classes);});}this.find("div."+CLASSES.hitarea).click(toggler);},treeview:function(settings){if(this.attr('cubicweb:type')=='prepared-treeview'){return this;}this.attr('cubicweb:type','prepared-treeview');settings=$.extend({cookieId:"treeview"},settings);if(settings.add){return this.trigger("add",[settings.add]);}if(settings.toggle){var callback=settings.toggle;settings.toggle=function(){return callback.apply($(this).parent()[0],arguments);};}function treeController(tree,control){function handler(filter){return function(){toggler.apply($("div."+CLASSES.hitarea,tree).filter(function(){return filter?$(this).parent("."+filter).length:true;}));return false;};}$("a:eq(0)",control).click(handler(CLASSES.collapsable));$("a:eq(1)",control).click(handler(CLASSES.expandable));$("a:eq(2)",control).click(handler());}function toggler(){$(this).parent().find(">.hitarea").swapClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).swapClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().swapClass(CLASSES.collapsable,CLASSES.expandable).swapClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightToggle(settings.animated,settings.toggle);if(settings.unique){$(this).parent().siblings().find(">.hitarea").replaceClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).replaceClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().replaceClass(CLASSES.collapsable,CLASSES.expandable).replaceClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightHide(settings.animated,settings.toggle);}}function serialize(){function binary(arg){return arg?1:0;}var data=[];branches.each(function(i,e){data[i]=$(e).is(":has(>ul:visible)")?1:0;});$.cookie(settings.cookieId,data.join(""));}function deserialize(){var stored=$.cookie(settings.cookieId);if(stored){var data=stored.split("");branches.each(function(i,e){$(e).find(">ul")[parseInt(data[i])?"show":"hide"]();});}}this.addClass("treeview");var branches=this.find("li").prepareBranches(settings);switch(settings.persist){case"cookie":var toggleCallback=settings.toggle;settings.toggle=function(){serialize();if(toggleCallback){toggleCallback.apply(this,arguments);}};deserialize();break;case"location":var current=this.find("a").filter(function(){return this.href.toLowerCase()==location.href.toLowerCase();});if(current.length){current.addClass("selected").parents("ul, li").add(current.next()).show();}break;}branches.applyClasses(settings,toggler);if(settings.control){treeController(this,settings.control);$(settings.control).show();}return this.bind("add",function(event,branches){$(branches).prev().removeClass(CLASSES.last).removeClass(CLASSES.lastCollapsable).removeClass(CLASSES.lastExpandable).find(">.hitarea").removeClass(CLASSES.lastCollapsableHitarea).removeClass(CLASSES.lastExpandableHitarea);$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings,toggler);});}});var CLASSES=$.fn.treeview.classes={open:"open",closed:"closed",expandable:"expandable",expandableHitarea:"expandable-hitarea",lastExpandableHitarea:"lastExpandable-hitarea",collapsable:"collapsable",collapsableHitarea:"collapsable-hitarea",lastCollapsableHitarea:"lastCollapsable-hitarea",lastCollapsable:"lastCollapsable",lastExpandable:"lastExpandable",last:"last",hitarea:"hitarea"};$.fn.Treeview=$.fn.treeview;})(jQuery);
\ No newline at end of file
Binary file web/data/logo.xcf has changed
Binary file web/data/nomail.xcf has changed
Binary file web/data/treeview-black-line.gif has changed
Binary file web/data/treeview-black.gif has changed
Binary file web/data/treeview-default-line.gif has changed
Binary file web/data/treeview-default.gif has changed
Binary file web/data/treeview-famfamfam-line.gif has changed
Binary file web/data/treeview-famfamfam.gif has changed
Binary file web/data/treeview-gray-line.gif has changed
Binary file web/data/treeview-gray.gif has changed
Binary file web/data/treeview-red-line.gif has changed
Binary file web/data/treeview-red.gif has changed
--- a/web/data/uiprops.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/data/uiprops.py	Thu Jan 22 17:45:06 2015 +0100
@@ -2,8 +2,7 @@
 
 # CSS stylesheets to include systematically in HTML headers
 # use the following line if you *need* to keep the old stylesheet
-STYLESHEETS =       [data('cubicweb.reset.css'),
-                     data('cubicweb.css'), ]
+STYLESHEETS =       [data('cubicweb.css'), ]
 STYLESHEETS_IE =    [data('cubicweb.ie.css')]
 STYLESHEETS_PRINT = [data('cubicweb.print.css')]
 
--- a/web/form.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/form.py	Thu Jan 22 17:45:06 2015 +0100
@@ -83,7 +83,7 @@
     domid = 'form'
     copy_nav_params = False
     control_fields = set( ('__form_id', '__errorurl', '__domid',
-                           '__redirectpath', '_cwmsgid', '__message',
+                           '__redirectpath', '_cwmsgid',
                            ) )
 
     def __init__(self, req, rset=None, row=None, col=None,
--- a/web/formfields.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/formfields.py	Thu Jan 22 17:45:06 2015 +0100
@@ -349,13 +349,7 @@
     def initial_typed_value(self, form, load_bytes):
         if self.value is not _MARKER:
             if callable(self.value):
-                # pylint: disable=E1102
-                if support_args(self.value, 'form', 'field'):
-                    return self.value(form, self)
-                else:
-                    warn("[3.10] field's value callback must now take form and "
-                         "field as argument (%s)" % self, DeprecationWarning)
-                    return self.value(form)
+                return self.value(form, self)
             return self.value
         formattr = '%s_%s_default' % (self.role, self.name)
         if self.eidparam and self.role is not None:
@@ -529,6 +523,7 @@
     """
     widget = fw.TextArea
     size = 45
+    placeholder = None
 
     def __init__(self, name=None, max_length=None, **kwargs):
         self.max_length = max_length # must be set before super call
@@ -547,6 +542,9 @@
         elif isinstance(self.widget, fw.TextInput):
             self.init_text_input(self.widget)
 
+        if self.placeholder:
+            self.widget.attrs.setdefault('placeholder', self.placeholder)
+
     def init_text_input(self, widget):
         if self.max_length:
             widget.attrs.setdefault('size', min(self.size, self.max_length))
@@ -557,6 +555,11 @@
             widget.attrs.setdefault('cols', 60)
             widget.attrs.setdefault('rows', 5)
 
+    def set_placeholder(self, placeholder):
+        self.placeholder = placeholder
+        if self.widget and self.placeholder:
+            self.widget.attrs.setdefault('placeholder', self.placeholder)
+
 
 class PasswordField(StringField):
     """Use this field to edit password (`Password` yams type, encoded python
@@ -776,11 +779,13 @@
     actually contains some text.
 
     If the stream format is one of text/plain, text/html, text/rest,
+    text/markdown
     then a :class:`~cubicweb.web.formwidgets.TextArea` will be additionaly
     displayed, allowing to directly the file's content when desired, instead
     of choosing a file from user's file system.
     """
-    editable_formats = ('text/plain', 'text/html', 'text/rest')
+    editable_formats = (
+        'text/plain', 'text/html', 'text/rest', 'text/markdown')
 
     def render(self, form, renderer):
         wdgs = [super(EditableFileField, self).render(form, renderer)]
--- a/web/formwidgets.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/formwidgets.py	Thu Jan 22 17:45:06 2015 +0100
@@ -210,6 +210,8 @@
             attrs['id'] = field.dom_id(form, self.suffix)
         if self.settabindex and not 'tabindex' in attrs:
             attrs['tabindex'] = form._cw.next_tabindex()
+        if 'placeholder' in attrs:
+            attrs['placeholder'] = form._cw._(attrs['placeholder'])
         return attrs
 
     def values(self, form, field):
--- a/web/http_headers.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/http_headers.py	Thu Jan 22 17:45:06 2015 +0100
@@ -2,8 +2,6 @@
 # http://twistedmatrix.com/trac/wiki/TwistedWeb2
 
 
-from __future__ import generators
-
 import types, time
 from calendar import timegm
 import base64
@@ -934,9 +932,13 @@
 
 #### Cookies. Blech!
 class Cookie(object):
-    # __slots__ = ['name', 'value', 'path', 'domain', 'ports', 'expires', 'discard', 'secure', 'comment', 'commenturl', 'version']
+    # __slots__ = ['name', 'value', 'path', 'domain', 'ports', 'expires',
+    #              'discard', 'secure', 'httponly', 'comment', 'commenturl',
+    #              'version']
 
-    def __init__(self, name, value, path=None, domain=None, ports=None, expires=None, discard=False, secure=False, comment=None, commenturl=None, version=0):
+    def __init__(self, name, value, path=None, domain=None, ports=None,
+                 expires=None, discard=False, secure=False, httponly=False,
+                 comment=None, commenturl=None, version=0):
         self.name = name
         self.value = value
         self.path = path
@@ -945,6 +947,7 @@
         self.expires = expires
         self.discard = discard
         self.secure = secure
+        self.httponly = httponly
         self.comment = comment
         self.commenturl = commenturl
         self.version = version
@@ -955,7 +958,8 @@
         if self.domain is not None: s+=", domain=%r" % (self.domain,)
         if self.ports is not None: s+=", ports=%r" % (self.ports,)
         if self.expires is not None: s+=", expires=%r" % (self.expires,)
-        if self.secure is not False: s+=", secure=%r" % (self.secure,)
+        if self.secure: s+=", secure"
+        if self.httponly: s+=", HttpOnly"
         if self.comment is not None: s+=", comment=%r" % (self.comment,)
         if self.commenturl is not None: s+=", commenturl=%r" % (self.commenturl,)
         if self.version != 0: s+=", version=%r" % (self.version,)
@@ -1213,6 +1217,8 @@
             out.append("domain=%s" % cookie.domain)
         if cookie.secure:
             out.append("secure")
+        if cookie.httponly:
+            out.append("HttpOnly")
 
         setCookies.append('; '.join(out))
     return setCookies
@@ -1240,6 +1246,8 @@
                 out.append("Port=%s" % quoteString(",".join([str(x) for x in cookie.ports])))
         if cookie.secure:
             out.append("Secure")
+        if cookie.httponly:
+            out.append("HttpOnly")
         out.append('Version="1"')
         setCookies.append('; '.join(out))
     return setCookies
@@ -1339,6 +1347,9 @@
         h = self._headers.get(name, None)
         r = self.handler.generate(name, h)
         if r is not None:
+            assert isinstance(r, list)
+            for v in r:
+                assert isinstance(v, str)
             self._raw_headers[name] = r
         return r
 
@@ -1377,6 +1388,9 @@
         Value should be a list of strings, each being one header of the
         given name.
         """
+        assert isinstance(value, list)
+        for v in value:
+            assert isinstance(v, str)
         name = name.lower()
         self._raw_headers[name] = value
         self._headers[name] = _RecalcNeeded
--- a/web/request.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/request.py	Thu Jan 22 17:45:06 2015 +0100
@@ -128,8 +128,12 @@
             self.datadir_url = vreg.config.https_datadir_url
         else:
             self.datadir_url = vreg.config.datadir_url
+        #: enable UStringIO's write tracing
+        self.tracehtml = False
+        if vreg.config.debugmode:
+            self.tracehtml = bool(form.pop('_cwtracehtml', False))
         #: raw html headers that can be added from any view
-        self.html_headers = HTMLHead(self)
+        self.html_headers = HTMLHead(self, tracewrites=self.tracehtml)
         #: received headers
         self._headers_in = Headers()
         if headers is not None:
@@ -139,6 +143,11 @@
         self.setup_params(form)
         #: received body
         self.content = StringIO()
+        # prepare output header
+        #: Header used for the final response
+        self.headers_out = Headers()
+        #: HTTP status use by the final response
+        self.status_out  = 200
         # set up language based on request headers or site default (we don't
         # have a user yet, and might not get one)
         self.set_user_language(None)
@@ -152,11 +161,6 @@
         #: page id, set by htmlheader template
         self.pageid = None
         self._set_pageid()
-        # prepare output header
-        #: Header used for the final response
-        self.headers_out = Headers()
-        #: HTTP status use by the final response
-        self.status_out  = 200
 
     def _set_pageid(self):
         """initialize self.pageid
@@ -179,7 +183,7 @@
         self.ajax_request = value
     json_request = property(_get_json_request, _set_json_request)
 
-    def base_url(self, secure=None):
+    def _base_url(self, secure=None):
         """return the root url of the instance
 
         secure = False -> base-url
@@ -192,7 +196,7 @@
         if secure:
             base_url = self.vreg.config.get('https-url')
         if base_url is None:
-            base_url = super(_CubicWebRequestBase, self).base_url()
+            base_url = super(_CubicWebRequestBase, self)._base_url()
         return base_url
 
     @property
@@ -237,7 +241,6 @@
     no_script_form_params = set(('vid',
                                  'etype',
                                  'vtitle', 'title',
-                                 '__message',
                                  '__redirectvid', '__redirectrql'))
 
     def setup_params(self, params):
@@ -260,14 +263,6 @@
                 val = self.no_script_form_param(param, val)
             if param == '_cwmsgid':
                 self.set_message_id(val)
-            elif param == '__message':
-                warn('[3.13] __message in request parameter is deprecated (may '
-                     'only be given to .build_url). Seeing this message usualy '
-                     'means your application hold some <form> where you should '
-                     'replace use of __message hidden input by form.set_message, '
-                     'so new _cwmsgid mechanism is properly used',
-                     DeprecationWarning)
-                self.set_message(val)
             else:
                 self.form[param] = val
 
@@ -408,6 +403,7 @@
             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.
@@ -439,14 +435,11 @@
         """
         self.add_js('cubicweb.ajax.js')
         jsfunc = kwargs.pop('jsfunc', 'userCallbackThenReloadPage')
-        if 'msg' in kwargs:
-            warn('[3.10] msg should be given as positional argument',
-                 DeprecationWarning, stacklevel=2)
-            args = (kwargs.pop('msg'),) + args
         assert not kwargs, 'dunno what to do with remaining kwargs: %s' % kwargs
         cbname = self.register_onetime_callback(cb, *cbargs)
         return "javascript: %s" % getattr(js, jsfunc)(cbname, *args)
 
+    @deprecated('[3.19] use a traditional ajaxfunc / controller')
     def register_onetime_callback(self, func, *args):
         cbname = build_cb_uid(func.__name__)
         def _cb(req):
@@ -457,12 +450,14 @@
         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):
@@ -569,7 +564,7 @@
         except KeyError:
             return SimpleCookie()
 
-    def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
+    def set_cookie(self, name, value, maxage=300, expires=None, secure=False, httponly=False):
         """set / update a cookie
 
         by default, cookie will be available for the next 5 minutes.
@@ -595,7 +590,7 @@
             expires = None
         # make sure cookie is set on the correct path
         cookie = Cookie(str(name), str(value), self.base_url_path(),
-                        expires=expires, secure=secure)
+                        expires=expires, secure=secure, httponly=httponly)
         self.headers_out.addHeader('Set-cookie', cookie)
 
     def remove_cookie(self, name, bwcompat=None):
@@ -786,10 +781,6 @@
             if 'Expires' not in self.headers_out:
                 # Expires header seems to be required by IE7 -- Are you sure ?
                 self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-            if self.http_method() == 'HEAD':
-                self.status_out = 200
-                # XXX replace by True once validate_cache bw compat method is dropped
-                return 200
             # /!\ no raise, the function returns and we keep processing the request
         else:
             # overwrite headers_out to forge a brand new not-modified response
@@ -1005,6 +996,7 @@
                 pass
         if vreg.config.get('language-negociation', False):
             # 2. http accept-language
+            self.headers_out.addHeader('Vary', 'Accept-Language')
             for lang in self.header_accept_language():
                 if lang in self.translations:
                     self.set_language(lang)
@@ -1082,6 +1074,10 @@
     set_shared_data = _cnx_func('set_shared_data')
     describe = _cnx_func('describe') # deprecated XXX
 
+    # security #################################################################
+
+    security_enabled = _cnx_func('security_enabled')
+
     # server-side service call #################################################
 
     def call_service(self, regid, **kwargs):
--- a/web/test/unittest_application.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/test/unittest_application.py	Thu Jan 22 17:45:06 2015 +0100
@@ -281,8 +281,7 @@
         """test against current script injection"""
         injected = '<i>toto</i>'
         cleaned = 'toto'
-        for kwargs in ({'__message': injected},
-                       {'vid': injected},
+        for kwargs in ({'vid': injected},
                        {'vtitle': injected},
                        ):
             yield self._test_cleaned, kwargs, injected, cleaned
--- a/web/test/unittest_form.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/test/unittest_form.py	Thu Jan 22 17:45:06 2015 +0100
@@ -16,7 +16,10 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
+import time
+
 from xml.etree.ElementTree import fromstring
+from lxml import html
 
 from logilab.common.testlib import unittest_main, mock_object
 
@@ -104,7 +107,9 @@
             req.form['__linkto'] = 'in_group:%s:subject' % geid
             form = self.vreg['forms'].select('edition', req, entity=e)
             form.content_type = 'text/html'
-            pageinfo = self._check_html(form.render(), form, template=None)
+            data = []
+            form.render(w=data.append)
+            pageinfo = self._check_html(u'\n'.join(data), form, template=None)
             inputs = pageinfo.find_tag('select', False)
             ok = False
             for selectnode in pageinfo.matching_nodes('select', name='from_in_group-subject:A'):
@@ -124,6 +129,24 @@
             self.assertIn('content_format', data)
 
 
+    def test_form_generation_time(self):
+        with self.admin_access.web_request() as req:
+            e = req.create_entity('BlogEntry', title=u'cubicweb.org', content=u"hop")
+            expected_field_name = '__form_generation_time:%d' % e.eid
+
+            ts_before = time.time()
+            form = self.vreg['forms'].select('edition', req, entity=e)
+            ts_after = time.time()
+
+            data = []
+            form.render(action='edit', w=data.append)
+            html_form = html.fromstring(''.join(data)).forms[0]
+            fields = dict(html_form.form_values())
+            self.assertIn(expected_field_name, fields)
+            ts = float(fields[expected_field_name])
+            self.assertTrue(ts_before < ts  < ts_after)
+
+
     # form tests ##############################################################
 
     def test_form_inheritance(self):
@@ -133,14 +156,18 @@
                 creation_date = DateTimeField(widget=DateTimePicker)
             form = CustomChangeStateForm(req, redirect_path='perdu.com',
                                          entity=req.user)
-            form.render(formvalues=dict(state=123, trcomment=u'',
+            data = []
+            form.render(w=data.append,
+                        formvalues=dict(state=123, trcomment=u'',
                                         trcomment_format=u'text/plain'))
 
     def test_change_state_form(self):
         with self.admin_access.web_request() as req:
             form = ChangeStateForm(req, redirect_path='perdu.com',
                                    entity=req.user)
-            form.render(formvalues=dict(state=123, trcomment=u'',
+            data = []
+            form.render(w=data.append,
+                        formvalues=dict(state=123, trcomment=u'',
                                         trcomment_format=u'text/plain'))
 
     # fields tests ############################################################
@@ -168,6 +195,7 @@
             self._test_richtextfield(req, '''<select id="description_format-subject:%(eid)s" name="description_format-subject:%(eid)s" size="1" style="display: block" tabindex="1">
 <option value="text/cubicweb-page-template">text/cubicweb-page-template</option>
 <option selected="selected" value="text/html">text/html</option>
+<option value="text/markdown">text/markdown</option>
 <option value="text/plain">text/plain</option>
 <option value="text/rest">text/rest</option>
 </select><textarea cols="80" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="2"></textarea>''')
--- a/web/test/unittest_http.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/test/unittest_http.py	Thu Jan 22 17:45:06 2015 +0100
@@ -227,7 +227,7 @@
         hout = [('etag', 'rhino/really-not-babar'),
                ]
         req = _test_cache(hin, hout, method='HEAD')
-        self.assertCache(200, req.status_out, 'modifier HEAD verb')
+        self.assertCache(None, req.status_out, 'modifier HEAD verb')
         # not modified
         hin  = [('if-none-match', 'babar'),
                ]
--- a/web/test/unittest_views_basecontrollers.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Thu Jan 22 17:45:06 2015 +0100
@@ -23,10 +23,15 @@
     from urlparse import parse_qs as url_parse_query
 except ImportError:
     from cgi import parse_qs as url_parse_query
+
+import lxml
+
 from logilab.common.testlib import unittest_main
+
 from logilab.common.decorators import monkeypatch
 
 from cubicweb import Binary, NoSelectableObject, ValidationError
+from cubicweb.schema import RRQLExpression
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.utils import json_dumps
 from cubicweb.uilib import rql_for_eid
@@ -81,6 +86,49 @@
                 self.assertEqual({'login-subject': 'the value "admin" is already used, use another one'},
                                  cm.exception.errors)
 
+    def test_simultaneous_edition_only_one_commit(self):
+        """ Allow two simultaneous edit view of the same entity as long as only one commits
+        """
+        with self.admin_access.web_request() as req:
+            e = req.create_entity('BlogEntry', title=u'cubicweb.org', content=u"hop")
+            expected_path = e.rest_path()
+            req.cnx.commit()
+            form = self.vreg['views'].select('edition', req, rset=e.as_rset(), row=0)
+            html_form = lxml.html.fromstring(form.render(w=None, action='edit')).forms[0]
+
+        with self.admin_access.web_request() as req2:
+            form2 = self.vreg['views'].select('edition', req, rset=e.as_rset(), row=0)
+
+        with self.admin_access.web_request(**dict(html_form.form_values())) as req:
+            path, args = self.expect_redirect_handle_request(req, path='edit')
+            self.assertEqual(path, expected_path)
+
+    def test_simultaneous_edition_refuse_second_commit(self):
+        """ Disallow committing changes to an entity edited in between """
+        with self.admin_access.web_request() as req:
+            e = req.create_entity('BlogEntry', title=u'cubicweb.org', content=u"hop")
+            eid = e.eid
+            req.cnx.commit()
+            form = self.vreg['views'].select('edition', req, rset=e.as_rset(), row=0)
+            html_form = lxml.html.fromstring(form.render(w=None, action='edit')).forms[0]
+
+        with self.admin_access.web_request() as req2:
+            e = req2.entity_from_eid(eid)
+            e.cw_set(content = u"hip")
+            req2.cnx.commit()
+
+        form_field_name = "content-subject:%d" % eid
+        form_values = dict(html_form.form_values())
+        assert form_field_name in form_values
+        form_values[form_field_name] = u'yep'
+        with self.admin_access.web_request(**form_values) as req:
+            with self.assertRaises(ValidationError) as cm:
+                self.ctrl_publish(req)
+            reported_eid, dict_info = cm.exception.args
+            self.assertEqual(reported_eid, eid)
+            self.assertIn(None, dict_info)
+            self.assertIn("has changed since you started to edit it.", dict_info[None])
+
     def test_user_editing_itself(self):
         """checking that a manager user can edit itself
         """
@@ -761,6 +809,23 @@
                 req.execute('Any N WHERE T tags P, P is CWUser, T name N').rows,
                 [['javascript']])
 
+    def test_maydel_perms(self):
+        """Check that AjaxEditRelationCtxComponent calls rdef.check with a
+        sufficient context"""
+        with self.remote_calling('tag_entity', self.john.eid, ['python']) as (_, req):
+            req.cnx.commit()
+        with self.temporary_permissions(
+                (self.schema['tags'].rdefs['Tag', 'CWUser'],
+                 {'delete': (RRQLExpression('S owned_by U'), )}, )):
+            with self.admin_access.web_request(rql='CWUser P WHERE P login "John"',
+                                   pageid='123', fname='view') as req:
+                ctrl = self.ctrl(req)
+                rset = self.john.as_rset()
+                rset.req = req
+                source = ctrl.publish()
+                # maydel jscall
+                self.assertIn('ajaxBoxRemoveLinkedEntity', source)
+
     def test_pending_insertion(self):
         with self.remote_calling('add_pending_inserts', [['12', 'tags', '13']]) as (_, req):
             deletes = get_pending_deletes(req)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_forms.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,46 @@
+# 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/>.
+
+from cubicweb.devtools.testlib import CubicWebTC
+
+class InlinedFormTC(CubicWebTC):
+
+    def test_linked_to(self):
+        with self.admin_access.web_request() as req:
+            formview = req.vreg['views'].select(
+                'inline-creation', req,
+                etype='File', rtype='described_by_test', role='subject',
+                peid=123,
+                petype='Salesterm')
+            self.assertEqual({('described_by_test', 'object'): [123]},
+                             formview.form.linked_to)
+
+    def test_linked_to_parent_being_created(self):
+        with self.admin_access.web_request() as req:
+            formview = req.vreg['views'].select(
+                'inline-creation', req,
+                etype='File', rtype='described_by_test', role='subject',
+                peid='A',
+                petype='Salesterm')
+            self.assertEqual(formview.form.linked_to, {})
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
+
--- a/web/test/unittest_views_json.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/test/unittest_views_json.py	Thu Jan 22 17:45:06 2015 +0100
@@ -70,6 +70,11 @@
             self.assertEqual(data[0]['name'], 'guests')
             self.assertEqual(data[1]['name'], 'managers')
 
+            rset = req.execute('Any G WHERE G is CWGroup, G name "foo"')
+            data = self.view('ejsonexport', rset, req=req)
+            self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json'])
+            self.assertEqual(data, [])
+
 
 class NotAnonymousJsonViewsTC(JsonViewsTC):
     anonymize = False
--- a/web/test/unittest_viewselector.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/test/unittest_viewselector.py	Thu Jan 22 17:45:06 2015 +0100
@@ -110,6 +110,7 @@
             self.assertListEqual(self.pviews(req, rset),
                     [('csvexport', csvexport.CSVRsetView),
                      ('ecsvexport', csvexport.CSVEntityView),
+                     ('ejsonexport', json.JsonEntityView),
                      ('jsonexport', json.JsonRsetView),
                      ])
 
--- a/web/test/unittest_web.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/test/unittest_web.py	Thu Jan 22 17:45:06 2015 +0100
@@ -94,15 +94,37 @@
         self.assertEqual(webreq.status_code, 200)
         self.assertDictEqual(expect, loads(webreq.content))
 
+
 class LanguageTC(CubicWebServerTC):
 
     def test_language_neg(self):
         headers = {'Accept-Language': 'fr'}
         webreq = self.web_request(headers=headers)
         self.assertIn('lang="fr"', webreq.read())
+        vary = [h.lower().strip() for h in webreq.getheader('Vary').split(',')]
+        self.assertIn('accept-language', vary)
         headers = {'Accept-Language': 'en'}
         webreq = self.web_request(headers=headers)
         self.assertIn('lang="en"', webreq.read())
+        vary = [h.lower().strip() for h in webreq.getheader('Vary').split(',')]
+        self.assertIn('accept-language', vary)
+
+    def test_response_codes(self):
+        with self.admin_access.client_cnx() as cnx:
+            admin_eid = cnx.user.eid
+        # guest can't see admin
+        webreq = self.web_request('/%d' % admin_eid)
+        self.assertEqual(webreq.status, 403)
+
+        # but admin can
+        self.web_login()
+        webreq = self.web_request('/%d' % admin_eid)
+        self.assertEqual(webreq.status, 200)
+
+    def test_session_cookie_httponly(self):
+        webreq = self.web_request()
+        self.assertIn('HttpOnly', webreq.getheader('set-cookie'))
+
 
 class LogQueriesTC(CubicWebServerTC):
     @classmethod
--- a/web/views/ajaxedit.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/ajaxedit.py	Thu Jan 22 17:45:06 2015 +0100
@@ -36,8 +36,6 @@
     cw_property_defs = {} # don't want to inherit this from Box
     expected_kwargs = form_params = ('rtype', 'target')
 
-    build_js = component.EditRelationMixIn.build_reload_js_call
-
     def cell_call(self, row, col, rtype=None, target=None, etype=None):
         self.rtype = rtype or self._cw.form['rtype']
         self.target = target or self._cw.form['target']
--- a/web/views/authentication.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/authentication.py	Thu Jan 22 17:45:06 2015 +0100
@@ -148,11 +148,13 @@
         raise :exc:`cubicweb.AuthenticationError` if authentication failed
         (no authentication info found or wrong user/password)
         """
+        has_auth = False
         for retriever in self.authinforetrievers:
             try:
                 login, authinfo = retriever.authentication_information(req)
             except NoAuthInfo:
                 continue
+            has_auth = True
             try:
                 session = self._authenticate(login, authinfo)
             except AuthenticationError:
@@ -161,9 +163,9 @@
             for retriever_ in self.authinforetrievers:
                 retriever_.authenticated(retriever, req, session, login, authinfo)
             return session, login
-        # false if no authentication info found, eg this is not an
+        # false if no authentication info found, i.e. this is not an
         # authentication failure
-        if 'login' in locals():
+        if has_auth:
             req.set_message(req._('authentication failure'))
         login, authinfo = self.anoninfo
         if login:
--- a/web/views/basecomponents.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/basecomponents.py	Thu Jan 22 17:45:06 2015 +0100
@@ -119,8 +119,8 @@
                   & configuration_values('auth-mode', 'cookie'))
     context = 'header-right'
     loginboxid = 'popupLoginBox'
-    _html = u"""[<a class="logout" title="%s" href="javascript:
-cw.htmlhelpers.popupLoginBox('%s', '__login');">%s</a>]"""
+    _html = u"""<a class="logout icon-login" title="%s" href="javascript:
+cw.htmlhelpers.popupLoginBox('%s', '__login');">%s</a>"""
 
     def render(self, w):
         # XXX bw compat, though should warn about subclasses redefining call
@@ -128,6 +128,7 @@
         self.call()
 
     def call(self):
+        self._cw.add_css('cubicweb.pictograms.css')
         self.w(self._html % (self._cw._('login / password'),
                              self.loginboxid, self._cw._('i18n_login_popup')))
         self._cw.view('logform', rset=self.cw_rset, id=self.loginboxid,
@@ -160,17 +161,17 @@
     order = HeaderComponent.order - 10
 
     def render(self, w):
-        w(u'<span class="caption">%s</span>' % self._cw._('anonymous'))
-
+        pass
 
 class AuthenticatedUserStatus(AnonUserStatusLink):
     __select__ = authenticated_user()
 
     def render(self, w):
         # display useractions and siteactions
+        self._cw.add_css('cubicweb.pictograms.css')
         actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset)
         box = MenuWidget('', 'userActionsBox', _class='', islist=False)
-        menu = PopupBoxMenu(self._cw.user.login, isitem=False)
+        menu = PopupBoxMenu(self._cw.user.login, isitem=False, link_class='icon-user')
         box.append(menu)
         for action in actions.get('useractions', ()):
             menu.append(self.action_link(action))
@@ -182,8 +183,8 @@
 
 
 class ApplicationMessage(component.Component):
-    """display messages given using the __message parameter into a special div
-    section
+    """display messages given using the __message/_cwmsgid parameter into a
+    special div section
     """
     __select__ = yes()
     __regid__ = 'applmessages'
--- a/web/views/editcontroller.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/editcontroller.py	Thu Jan 22 17:45:06 2015 +0100
@@ -22,6 +22,8 @@
 from warnings import warn
 from collections import defaultdict
 
+from datetime import datetime
+
 from logilab.common.deprecation import deprecated
 from logilab.common.graph import ordered_nodes
 
@@ -266,6 +268,7 @@
         if eid is None: # creation or copy
             entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery)
         elif rqlquery.edited: # edition of an existant entity
+            self.check_concurrent_edition(formparams, eid)
             self._update_entity(eid, rqlquery)
         if is_main_entity:
             self.notify_edited(entity)
@@ -362,6 +365,23 @@
         else:
             self._cw.set_message(self._cw._('entity deleted'))
 
+
+    def check_concurrent_edition(self, formparams, eid):
+        req = self._cw
+        try:
+            form_ts = datetime.fromtimestamp(float(formparams['__form_generation_time']))
+        except KeyError:
+            # Backward and tests compatibility : if no timestamp consider edition OK
+            return
+        if req.execute("Any X WHERE X modification_date > %(fts)s, X eid %(eid)s",
+                       {'eid': eid, 'fts': form_ts}):
+            # We only mark the message for translation but the actual
+            # translation will be handled by the Validation mechanism...
+            msg = _("Entity %(eid)s has changed since you started to edit it."
+                    " Reload the page and reapply your changes.")
+            # ... this is why we pass the formats' dict as a third argument.
+            raise ValidationError(eid, {None: msg}, {'eid' : eid})
+
     def _action_apply(self):
         self._default_publish()
         self.reset()
--- a/web/views/formrenderers.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/formrenderers.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -142,12 +142,7 @@
         help = []
         descr = field.help
         if callable(descr):
-            if support_args(descr, 'form', 'field'):
-                descr = descr(form, field)
-            else:
-                warn("[3.10] field's help callback must now take form and field as argument (%s)"
-                     % field, DeprecationWarning)
-                descr = descr(form)
+            descr = descr(form, field)
         if descr:
             help.append('<div class="helper">%s</div>' % self._cw._(descr))
         example = field.example_format(self._cw)
--- a/web/views/forms.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/forms.py	Thu Jan 22 17:45:06 2015 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 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.
@@ -41,16 +41,20 @@
 
 but you'll use this one rarely.
 """
+
 __docformat__ = "restructuredtext en"
 
+
 from warnings import warn
 
+import time
+
 from logilab.common import dictattr, tempattr
 from logilab.common.decorators import iclassmethod, cached
 from logilab.common.textutils import splitstrip
 from logilab.common.deprecation import deprecated
 
-from cubicweb import ValidationError
+from cubicweb import ValidationError, neg_role
 from cubicweb.utils import support_args
 from cubicweb.predicates import non_final_entity, match_kwargs, one_line_rset
 from cubicweb.web import RequestError, ProcessFormError
@@ -196,24 +200,10 @@
         Extra keyword arguments will be given to renderer's :meth:`render` method.
         """
         w = kwargs.pop('w', None)
-        if w is None:
-            warn('[3.10] you should specify "w" to form.render() named arguments',
-                 DeprecationWarning, stacklevel=2)
-            data = []
-            w = data.append
-        else:
-            data = None
         self.build_context(formvalues)
         if renderer is None:
             renderer = self.default_renderer()
-        if support_args(renderer.render, 'w'):
-            renderer.render(w, self, kwargs)
-        else:
-            warn('[3.10] you should add "w" as first argument o %s.render()'
-                 % renderer.__class__, DeprecationWarning)
-            w(renderer.render(self, kwargs))
-        if data is not None:
-            return '\n'.join(data)
+        renderer.render(w, self, kwargs)
 
     def default_renderer(self):
         return self._cw.vreg['formrenderers'].select(
@@ -362,7 +352,9 @@
         self.uicfg_affk = self._cw.vreg['uicfg'].select(
             'autoform_field_kwargs', self._cw, entity=self.edited_entity)
         self.add_hidden('__type', self.edited_entity.cw_etype, eidparam=True)
+
         self.add_hidden('eid', self.edited_entity.eid)
+        self.add_generation_time()
         # mainform default to true in parent, hence default to True
         if kwargs.get('mainform', True) or kwargs.get('mainentity', False):
             self.add_hidden(u'__maineid', self.edited_entity.eid)
@@ -376,6 +368,11 @@
             msgid = self._cw.set_redirect_message(msg)
             self.add_hidden('_cwmsgid', msgid)
 
+    def add_generation_time(self):
+        # NB repr is critical to avoid truncation of the timestamp
+        self.add_hidden('__form_generation_time', repr(time.time()),
+                        eidparam=True)
+
     def add_linkto_hidden(self):
         """add the __linkto hidden field used to directly attach the new object
         to an existing other one when the relation between those two is not
@@ -399,12 +396,23 @@
     @property
     @cached
     def linked_to(self):
-        # if current form is not the main form, exit immediately
+        linked_to = {}
+        # case where this is an embeded creation form
+        try:
+            eid = int(self.cw_extra_kwargs['peid'])
+        except (KeyError, ValueError):
+            # When parent is being created, its eid is not numeric (e.g. 'A')
+            # hence ValueError.
+            pass
+        else:
+            ltrtype = self.cw_extra_kwargs['rtype']
+            ltrole = neg_role(self.cw_extra_kwargs['role'])
+            linked_to[(ltrtype, ltrole)] = [eid]
+        # now consider __linkto if the current form is the main form
         try:
             self.field_by_name('__maineid')
         except form.FieldNotFound:
-            return {}
-        linked_to = {}
+            return linked_to
         for linkto in self._cw.list_form_param('__linkto'):
             ltrtype, eid, ltrole = linkto.split(':')
             linked_to.setdefault((ltrtype, ltrole), []).append(int(eid))
--- a/web/views/ibreadcrumbs.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/ibreadcrumbs.py	Thu Jan 22 17:45:06 2015 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 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.
@@ -64,16 +64,9 @@
         """
         parent = self.parent_entity()
         if parent is not None:
-            if recurs is True:
-                _recurs = set()
-                warn('[3.10] recurs argument should be a set() or None',
-                     DeprecationWarning, stacklevel=2)
-            elif recurs:
+            if recurs:
                 _recurs = recurs
             else:
-                if recurs is False:
-                    warn('[3.10] recurs argument should be a set() or None',
-                         DeprecationWarning, stacklevel=2)
                 _recurs = set()
             if _recurs and parent.eid in _recurs:
                 self.error('cycle in breadcrumbs for entity %s' % self.entity)
--- a/web/views/idownloadable.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/idownloadable.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -34,23 +34,6 @@
 from cubicweb.web.views import primary, baseviews
 
 
-@deprecated('[3.10] use a custom IDownloadable adapter instead')
-def download_box(w, entity, title=None, label=None, footer=u''):
-    req = entity._cw
-    w(u'<div class="sideBox">')
-    if title is None:
-        title = req._('download')
-    w(u'<div class="sideBoxTitle downloadBoxTitle"><span>%s</span></div>'
-      % xml_escape(title))
-    w(u'<div class="sideBox downloadBox"><div class="sideBoxBody">')
-    w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
-      % (xml_escape(entity.cw_adapt_to('IDownloadable').download_url()),
-         req.uiprops['DOWNLOAD_ICON'],
-         req._('download icon'), xml_escape(label or entity.dc_title())))
-    w(u'%s</div>' % footer)
-    w(u'</div></div>\n')
-
-
 class DownloadBox(component.EntityCtxComponent):
     """add download box"""
     __regid__ = 'download_box'    # no download box for images
@@ -175,10 +158,6 @@
         self.w(u'<a href="%s">%s</a> [<a href="%s">%s</a>]' %
                (url, name, durl, self._cw._('download')))
 
-IDownloadableLineView = class_renamed(
-    'IDownloadableLineView', IDownloadableOneLineView,
-    '[3.10] IDownloadableLineView is deprecated, use IDownloadableOneLineView')
-
 
 class AbstractEmbeddedView(EntityView):
     __abstract__ = True
--- a/web/views/json.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/json.py	Thu Jan 22 17:45:06 2015 +0100
@@ -21,7 +21,7 @@
 _ = unicode
 
 from cubicweb.utils import json_dumps
-from cubicweb.predicates import any_rset
+from cubicweb.predicates import any_rset, empty_rset
 from cubicweb.view import EntityView, AnyRsetView
 from cubicweb.web.application import anonymized_request
 from cubicweb.web.views import basecontrollers
@@ -106,6 +106,7 @@
     - ``__cwetype__`` : entity type
     """
     __regid__ = 'ejsonexport'
+    __select__ = EntityView.__select__ | empty_rset()
     title = _('json-entities-export-view')
 
     def call(self):
--- a/web/views/primary.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/primary.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -131,16 +131,7 @@
             boxes = None
         if boxes or hasattr(self, 'render_side_related'):
             self.w(u'<table width="100%"><tr><td style="width: 75%">')
-        if hasattr(self, 'render_entity_summary'):
-            warn('[3.10] render_entity_summary method is deprecated (%s)' % self,
-                 DeprecationWarning)
-            self.render_entity_summary(entity) # pylint: disable=E1101
 
-        summary = self.summary(entity)
-        if summary:
-            warn('[3.10] summary method is deprecated (%s)' % self,
-                 DeprecationWarning)
-            self.w(u'<div class="summary">%s</div>' % summary)
         self.w(u'<div class="mainInfo">')
         self.content_navigation_components('navcontenttop')
         self.render_entity_attributes(entity)
@@ -189,10 +180,6 @@
     def render_entity_toolbox(self, entity):
         self.content_navigation_components('ctxtoolbar')
 
-    def summary(self, entity):
-        """default implementation return an empty string"""
-        return u''
-
     def render_entity_attributes(self, entity):
         """Renders all attributes and relations in the 'attributes' section. 
         """
@@ -263,23 +250,10 @@
         explicit box appobjects selectable in this context.
         """
         for box in boxes:
-            if isinstance(box, tuple):
-                try:
-                    label, rset, vid, dispctrl  = box
-                except ValueError:
-                    label, rset, vid = box
-                    dispctrl = {}
-                warn('[3.10] box views should now be a RsetBox instance, '
-                     'please update %s' % self.__class__.__name__,
-                     DeprecationWarning)
-                self.w(u'<div class="sideBox">')
-                self.wview(vid, rset, title=label, initargs={'dispctrl': dispctrl})
-                self.w(u'</div>')
-            else:
-                 try:
-                     box.render(w=self.w, row=self.cw_row)
-                 except TypeError:
-                     box.render(w=self.w)
+            try:
+                box.render(w=self.w, row=self.cw_row)
+            except TypeError:
+                box.render(w=self.w)
 
     def _prepare_side_boxes(self, entity):
         sideboxes = []
--- a/web/views/startup.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/startup.py	Thu Jan 22 17:45:06 2015 +0100
@@ -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.
@@ -156,14 +156,6 @@
         return u'[<a href="%s" title="%s">+</a>]' % (
             xml_escape(url), self._cw.__('New %s' % etype))
 
-    @deprecated('[3.11] display_folders method is deprecated, backport it if needed')
-    def display_folders(self):
-        return False
-
-    @deprecated('[3.11] folders method is deprecated, backport it if needed')
-    def folders(self):
-        self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by category'))
-        self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1)
 
 
 class IndexView(ManageView):
--- a/web/views/treeview.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/treeview.py	Thu Jan 22 17:45:06 2015 +0100
@@ -145,9 +145,9 @@
         toplevel = toplevel_thru_ajax or (initial_load and not form.get('fname'))
         return subvid, treeid, toplevel_thru_ajax, toplevel
 
-    def _init_headers(self, treeid, toplevel_thru_ajax):
-        self._cw.add_css('jquery.treeview.css')
-        self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery.treeview.js'))
+    def _init_headers(self, treeid):
+        self._cw.add_css(('jquery-treeview/jquery.treeview.css', 'cubicweb.treeview.css'))
+        self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery-treeview/jquery.treeview.js'))
         self._cw.html_headers.add_onload(u"""
 jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid)
 
@@ -157,7 +157,7 @@
             subvid, treeid, initial_load, initial_thru_ajax, morekwargs)
         ulid = ' '
         if toplevel:
-            self._init_headers(treeid, toplevel_thru_ajax)
+            self._init_headers(treeid)
             ulid = ' id="tree-%s"' % treeid
         self.w(u'<ul%s class="%s">' % (ulid, self.cssclass))
         # XXX force sorting on x.sortvalue() (which return dc_title by default)
--- a/web/views/workflow.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/views/workflow.py	Thu Jan 22 17:45:06 2015 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 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.
@@ -169,14 +169,7 @@
     title = _('Workflow history')
 
     def render_body(self, w):
-        if hasattr(self, 'cell_call'):
-            warn('[3.10] %s should now implement render_body instead of cell_call'
-                 % self.__class__, DeprecationWarning)
-            self.w = w
-            # pylint: disable=E1101
-            self.cell_call(self.entity.cw_row, self.entity.cw_col)
-        else:
-            self.entity.view('wfhistory', w=w, title=None)
+        self.entity.view('wfhistory', w=w, title=None)
 
 
 class InContextWithStateView(EntityView):
--- a/web/webconfig.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/web/webconfig.py	Thu Jan 22 17:45:06 2015 +0100
@@ -209,12 +209,6 @@
           'group': 'web', 'level': 3,
           }),
 
-        ('use-old-css',
-         {'type' : 'yn',
-          'default': True,
-          'help': 'use cubicweb.old.css instead of 3.9 cubicweb.css',
-          'group': 'web', 'level': 2,
-          }),
         ('concat-resources',
          {'type' : 'yn',
           'default': False,
@@ -368,9 +362,11 @@
         if directory is None:
             return None, None
         if rdirectory == 'data' and rid.endswith('.css'):
-            if self['use-old-css'] and rid == 'cubicweb.css':
+            if rid == 'cubicweb.old.css':
                 # @import('cubicweb.css') in css
-                rid = 'cubicweb.old.css'
+                warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
+                     DeprecationWarning)
+                rid = 'cubicweb.css'
             return self.uiprops.process_resource(join(directory, rdirectory), rid), rid
         return join(directory, rdirectory), rid
 
@@ -437,13 +433,14 @@
             self._load_ui_properties_file(uiprops, path)
         self._load_ui_properties_file(uiprops, self.apphome)
         datadir_url = uiprops.context['datadir_url']
-        # pre 3.9 css compat, however the old css still rules
-        if self['use-old-css']:
-            if (datadir_url+'/cubicweb.css') in uiprops['STYLESHEETS']:
-                idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.css')
-                uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.old.css'
-            if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
-                uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
+        if (datadir_url+'/cubicweb.old.css') in uiprops['STYLESHEETS']:
+            warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
+                 DeprecationWarning)
+            idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.old.css')
+            uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.css'
+        if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
+            warn('[3.20] cubicweb.reset.css is obsolete', DeprecationWarning)
+            uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
         cubicweb_js_url = datadir_url + '/cubicweb.js'
         if cubicweb_js_url not in uiprops['JAVASCRIPTS']:
             uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
--- a/wsgi/request.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/wsgi/request.py	Thu Jan 22 17:45:06 2015 +0100
@@ -70,7 +70,7 @@
                           if k.startswith('HTTP_'))
         if 'CONTENT_TYPE' in environ:
             headers_in['Content-Type'] = environ['CONTENT_TYPE']
-        https = environ["wsgi.url_scheme"] == 'https'
+        https = self.is_secure()
         if self.path.startswith('/https/'):
             self.path = self.path[6:]
             self.environ['PATH_INFO'] = self.path
@@ -118,32 +118,8 @@
 
     ## wsgi request helpers ###################################################
 
-    def instance_uri(self):
-        """Return the instance's base URI (no PATH_INFO or QUERY_STRING)
-
-        see python2.5's wsgiref.util.instance_uri code
-        """
-        environ = self.environ
-        url = environ['wsgi.url_scheme'] + '://'
-        if environ.get('HTTP_HOST'):
-            url += environ['HTTP_HOST']
-        else:
-            url += environ['SERVER_NAME']
-            if environ['wsgi.url_scheme'] == 'https':
-                if environ['SERVER_PORT'] != '443':
-                    url += ':' + environ['SERVER_PORT']
-            else:
-                if environ['SERVER_PORT'] != '80':
-                    url += ':' + environ['SERVER_PORT']
-        url += quote(environ.get('SCRIPT_NAME') or '/')
-        return url
-
-    def get_full_path(self):
-        return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + self.environ.get('QUERY_STRING', '')) or '')
-
     def is_secure(self):
-        return 'wsgi.url_scheme' in self.environ \
-            and self.environ['wsgi.url_scheme'] == 'https'
+        return self.environ['wsgi.url_scheme'] == 'https'
 
     def get_posted_data(self):
         # The WSGI spec says 'QUERY_STRING' may be absent.
@@ -186,13 +162,5 @@
                 val = self.no_script_form_param(param, val)
             if param == '_cwmsgid':
                 self.set_message_id(val)
-            elif param == '__message':
-                warn('[3.13] __message in request parameter is deprecated (may '
-                     'only be given to .build_url). Seeing this message usualy '
-                     'means your application hold some <form> where you should '
-                     'replace use of __message hidden input by form.set_message, '
-                     'so new _cwmsgid mechanism is properly used',
-                     DeprecationWarning)
-                self.set_message(val)
             else:
                 self.form[param] = val
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wsgi/tnd.py	Thu Jan 22 17:45:06 2015 +0100
@@ -0,0 +1,47 @@
+# 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/>.
+"""tornado wsgi server for CubicWeb web instances"""
+
+__docformat__ = "restructuredtext en"
+
+
+from cubicweb.wsgi.handler import CubicWebWSGIApplication
+from cubicweb import ConfigurationError
+from tornado import wsgi, httpserver, ioloop
+
+from logging import getLogger
+LOGGER = getLogger('cubicweb')
+
+
+def run(config):
+    config.check_writeable_uid_directory(config.appdatahome)
+
+    port = config['port'] or 8080
+    interface = config['interface']
+
+    app = CubicWebWSGIApplication(config)
+    container = wsgi.WSGIContainer(app)
+    http_server = httpserver.HTTPServer(container)
+    http_server.listen(port, interface)
+    repo = app.appli.repo
+    try:
+        repo.start_looping_tasks()
+        LOGGER.info('starting http server on %s', config['base-url'])
+        ioloop.IOLoop.instance().start()
+    finally:
+        repo.shutdown()
--- a/wsgi/wz.py	Thu Jan 22 17:39:07 2015 +0100
+++ b/wsgi/wz.py	Thu Jan 22 17:45:06 2015 +0100
@@ -39,6 +39,7 @@
     repo = app.appli.repo
     try:
         repo.start_looping_tasks()
+        LOGGER.info('starting http server on %s', config['base-url'])
         run_simple(interface, port, app,
                    threaded=True,
                    use_debugger=True,