merge 3.19.6 into 3.20 branch
authorJulien Cristau <julien.cristau@logilab.fr>
Mon, 01 Dec 2014 11:13:10 +0100
changeset 10074 ab956b780d4e
parent 10068 1b0cb3c6c95e (diff)
parent 10073 c53062c49d53 (current diff)
child 10075 136b5f995f8e
merge 3.19.6 into 3.20 branch
__pkginfo__.py
cubicweb.spec
hooks/syncschema.py
migration.py
misc/migration/bootstrapmigration_repository.py
server/__init__.py
server/schemaserial.py
server/session.py
server/sources/native.py
web/data/cubicweb.ajax.js
web/data/cubicweb.css
web/data/cubicweb.facets.js
web/data/cubicweb.old.css
web/facet.py
web/test/unittest_viewselector.py
--- a/__init__.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/__init__.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/__pkginfo__.py	Mon Dec 01 11:13:10 2014 +0100
@@ -42,7 +42,7 @@
     'logilab-common': '>= 0.62.0',
     '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': '',
@@ -51,6 +51,7 @@
     # server dependencies
     'logilab-database': '>= 1.12.1',
     'passlib': '',
+    'Markdown': ''
     }
 
 __recommends__ = {
--- a/cubicweb.spec	Sun Nov 30 21:24:36 2014 +0100
+++ b/cubicweb.spec	Mon Dec 01 11:13:10 2014 +0100
@@ -23,11 +23,12 @@
 Requires:       %{python}-logilab-common >= 0.62.0
 Requires:       %{python}-logilab-mtconverter >= 0.8.0
 Requires:       %{python}-rql >= 0.31.2
-Requires:       %{python}-yams >= 0.39.1
+Requires:       %{python}-yams >= 0.40.0
 Requires:       %{python}-logilab-database >= 1.12.1
 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	Sun Nov 30 21:24:36 2014 +0100
+++ b/cwconfig.py	Mon Dec 01 11:13:10 2014 +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',
@@ -827,13 +827,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/cwctl.py	Mon Dec 01 11:13:10 2014 +0100
@@ -836,6 +836,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()
--- a/dataimport.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/dataimport.py	Mon Dec 01 11:13:10 2014 +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
 
@@ -425,16 +420,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 +510,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 +548,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 +564,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 +583,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 +750,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 +762,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 +814,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 +1046,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/dbapi.py	Mon Dec 01 11:13:10 2014 +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/control	Sun Nov 30 21:24:36 2014 +0100
+++ b/debian/control	Mon Dec 01 11:13:10 2014 +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
@@ -152,7 +152,8 @@
  gettext,
  python-logilab-mtconverter (>= 0.8.0),
  python-logilab-common (>= 0.62.0),
- python-yams (>= 0.39.1),
+ python-markdown,
+ python-yams (>= 0.40.0),
  python-rql (>= 0.31.2),
  python-lxml
 Recommends:
--- a/devtools/fake.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/devtools/fake.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/devtools/htmlparser.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/devtools/testlib.py	Mon Dec 01 11:13:10 2014 +0100
@@ -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()`)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.20.rst	Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,22 @@
+What's new in CubicWeb 3.20
+===========================
+
+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	Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,63 @@
+{% extends "basic/layout.html" %}
+
+{%- block extrahead %}
+<link rel="stylesheet" href="{{ pathto('_static/font-nobile.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
+<link rel="stylesheet" href="{{ pathto('_static/font-neuton.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
+<!--[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	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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/setup.rst	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/admin/setup.rst	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/annexes/faq.rst	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/conf.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/devrepo/datamodel/baseschema.rst	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Mon Dec 01 11:13:10 2014 +0100
@@ -1,4 +1,4 @@
- .. -*- coding: utf-8 -*-
+.. -*- coding: utf-8 -*-
 
 .. _datamodel_definition:
 
@@ -523,6 +523,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/devrepo/index.rst	Mon Dec 01 11:13:10 2014 +0100
@@ -22,4 +22,4 @@
    migration.rst
    profiling.rst
    fti.rst
-
+   dataimport
--- a/doc/book/en/devweb/views/embedding.rst	Sun Nov 30 21:24:36 2014 +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/standard_theme/layout.html	Sun Nov 30 21:24:36 2014 +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	Sun Nov 30 21:24:36 2014 +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	Sun Nov 30 21:24:36 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/tutorials/textreports/index.rst	Mon Dec 01 11:13:10 2014 +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
--- a/doc/tools/pyjsrest.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/tools/pyjsrest.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/entities/__init__.py	Mon Dec 01 11:13:10 2014 +0100
@@ -31,7 +31,6 @@
     instances have access to their issuing cursor
     """
     __regid__ = 'Any'
-    __implements__ = ()
 
     @classproperty
     def cw_etype(cls):
--- a/entity.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/entity.py	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/ext/rest.py	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/hooks/syncschema.py	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/unittest_synccomputed.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/de.po	Mon Dec 01 11:13:10 2014 +0100
@@ -483,6 +483,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 ""
 
--- a/i18n/en.po	Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/en.po	Mon Dec 01 11:13:10 2014 +0100
@@ -461,6 +461,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 ""
 
--- a/i18n/es.po	Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/es.po	Mon Dec 01 11:13:10 2014 +0100
@@ -492,6 +492,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"
 
--- a/i18n/fr.po	Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/fr.po	Mon Dec 01 11:13:10 2014 +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"
@@ -486,6 +486,12 @@
 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"
 
--- a/migration.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/migration.py	Mon Dec 01 11:13:10 2014 +0100
@@ -247,12 +247,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':
--- a/misc/migration/bootstrapmigration_repository.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/mttransforms.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/predicates.py	Mon Dec 01 11:13:10 2014 +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/req.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/req.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/rqlrewrite.py	Mon Dec 01 11:13:10 2014 +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/schema.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/schema.py	Mon Dec 01 11:13:10 2014 +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):
         """
@@ -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 ###########################################
 
@@ -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	Sun Nov 30 21:24:36 2014 +0100
+++ b/schemas/bootstrap.py	Mon Dec 01 11:13:10 2014 +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/server/__init__.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/__init__.py	Mon Dec 01 11:13:10 2014 +0100
@@ -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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/hook.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/migractions.py	Mon Dec 01 11:13:10 2014 +0100
@@ -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
@@ -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,8 +1061,12 @@
 
     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,
+        rschema = self.repo.schema[rtype]
+        if rschema.rule:
+            etype = 'CWComputedRType'
+        else:
+            etype = 'CWRType'
+        self.rqlexec('DELETE %s X WHERE X name %r' % (etype, rtype),
                      ask_confirm=self.verbosity>=2)
         if commit:
             self.commit()
@@ -1086,6 +1094,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 +1124,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'
--- a/server/querier.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/querier.py	Mon Dec 01 11:13:10 2014 +0100
@@ -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])
@@ -573,10 +589,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
--- a/server/repository.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/repository.py	Mon Dec 01 11:13:10 2014 +0100
@@ -651,8 +651,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 +678,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 +1165,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/schemaserial.py	Mon Dec 01 11:13:10 2014 +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
@@ -346,6 +375,11 @@
             if pb is not None:
                 pb.update()
             continue
+        if rschema.rule:
+            execschemarql(execute, rschema, crschema2rql(rschema))
+            if pb is not None:
+                pb.update()
+            continue
         execschemarql(execute, rschema, rschema2rql(rschema, addrdef=False))
         if rschema.symmetric:
             rdefs = [rdef for k, rdef in rschema.rdefs.iteritems()
@@ -366,8 +400,7 @@
         execute(rql, kwargs, build_descr=False)
         if pb is not None:
             pb.update()
-    if not quiet:
-        print
+    print
 
 
 # high level serialization functions
@@ -469,7 +502,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 +529,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 +563,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 +639,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/serverctl.py	Mon Dec 01 11:13:10 2014 +0100
@@ -482,7 +482,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 +504,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 +520,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()
 
 
--- a/server/session.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/session.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/__init__.py	Mon Dec 01 11:13:10 2014 +0100
@@ -105,7 +105,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))
--- a/server/sources/datafeed.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/datafeed.py	Mon Dec 01 11:13:10 2014 +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,39 @@
         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 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")
@@ -427,14 +467,7 @@
         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):
@@ -455,6 +488,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/ldapfeed.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/native.py	Mon Dec 01 11:13:10 2014 +0100
@@ -318,10 +318,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))
--- a/server/sqlutils.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sqlutils.py	Mon Dec 01 11:13:10 2014 +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']
@@ -328,6 +328,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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-cwep002/schema.py	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_datafeed.py	Mon Dec 01 11:13:10 2014 +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,19 +27,14 @@
 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):
@@ -50,7 +47,24 @@
                 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 +133,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_migractions.py	Mon Dec 01 11:13:10 2014 +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')
@@ -656,16 +679,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_querier.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_repository.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_schemaserial.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_security.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_undo.py	Mon Dec 01 11:13:10 2014 +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/sobjects/notification.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/sobjects/notification.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/test/data/rewrite/schema.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_dataimport.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_dbapi.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_entity.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_rqlrewrite.py	Mon Dec 01 11:13:10 2014 +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_schema.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_schema.py	Mon Dec 01 11:13:10 2014 +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	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/toolsutils.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/uilib.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/utils.py	Mon Dec 01 11:13:10 2014 +0100
@@ -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"
 
@@ -420,6 +420,8 @@
         self.head = req.html_headers
         # main stream
         self.body = UStringIO()
+        # 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 +447,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))
--- a/view.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/view.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/application.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/component.py	Mon Dec 01 11:13:10 2014 +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"""
@@ -725,7 +705,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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/cors.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.ajax.js	Mon Dec 01 11:13:10 2014 +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.edition.js	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.edition.js	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.facets.js	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.htmlhelpers.js	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.js	Mon Dec 01 11:13:10 2014 +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.timeline-bundle.js	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.timeline-bundle.js	Mon Dec 01 11:13:10 2014 +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
--- a/web/formfields.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/formfields.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/formwidgets.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/http_headers.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/request.py	Mon Dec 01 11:13:10 2014 +0100
@@ -139,6 +139,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 +157,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 +179,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 +192,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
@@ -439,10 +439,6 @@
         """
         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)
@@ -569,7 +565,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 +591,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 +782,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 +997,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)
--- a/web/test/unittest_form.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_form.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_http.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Mon Dec 01 11:13:10 2014 +0100
@@ -23,7 +23,11 @@
     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
@@ -81,6 +85,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
         """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_forms.py	Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,36 @@
+# 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):
+        req = self.request()
+        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)
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
+
--- a/web/test/unittest_views_json.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_views_json.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_viewselector.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_web.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/ajaxedit.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/authentication.py	Mon Dec 01 11:13:10 2014 +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/editcontroller.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/editcontroller.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/formrenderers.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/forms.py	Mon Dec 01 11:13:10 2014 +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,21 @@
     @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:
+            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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/ibreadcrumbs.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/idownloadable.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/json.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/primary.py	Mon Dec 01 11:13:10 2014 +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	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/startup.py	Mon Dec 01 11:13:10 2014 +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/workflow.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/workflow.py	Mon Dec 01 11:13:10 2014 +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/wsgi/request.py	Sun Nov 30 21:24:36 2014 +0100
+++ b/wsgi/request.py	Mon Dec 01 11:13:10 2014 +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.