backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 13 Sep 2010 16:47:03 +0200
changeset 6227 82d4011f54c1
parent 6225 a176e68b7d0d (diff)
parent 6226 dbb7ad04b963 (current diff)
child 6228 469ebd0d6fb7
backport stable
misc/migration/3.6.0_Any.py
--- a/cwvreg.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/cwvreg.py	Mon Sep 13 16:47:03 2010 +0200
@@ -421,6 +421,44 @@
 VRegistry.REGISTRY_FACTORY['actions'] = ActionsRegistry
 
 
+class CtxComponentsRegistry(CWRegistry):
+    def poss_visible_objects(self, *args, **kwargs):
+        """return an ordered list of possible components"""
+        context = kwargs.pop('context')
+        if kwargs.get('rset') is None:
+            cache = args[0]
+        else:
+            cache = kwargs['rset']
+        try:
+            cached = cache.__components_cache
+        except AttributeError:
+            ctxcomps = super(CtxComponentsRegistry, self).poss_visible_objects(
+                *args, **kwargs)
+            cached = cache.__components_cache = {}
+            for component in ctxcomps:
+                cached.setdefault(component.cw_propval('context'), []).append(component)
+        thisctxcomps = cached.get(context, ())
+        # XXX set context for bw compat (should now be taken by comp.render())
+        for component in thisctxcomps:
+            component.cw_extra_kwargs['context'] = context
+        return thisctxcomps
+
+VRegistry.REGISTRY_FACTORY['ctxcomponents'] = CtxComponentsRegistry
+
+
+class BwCompatCWRegistry(object):
+    def __init__(self, vreg, oldreg, redirecttoreg):
+        self.vreg = vreg
+        self.oldreg = oldreg
+        self.redirecto = redirecttoreg
+
+    def __getattr__(self, attr):
+        warn('[3.10] you should now use the %s registry instead of the %s registry'
+             % (self.redirecto, self.oldreg), DeprecationWarning, stacklevel=2)
+        return getattr(self.vreg[self.redirecto], attr)
+
+    def clear(self): pass
+    def initialization_completed(self): pass
 
 class CubicWebVRegistry(VRegistry):
     """Central registry for the cubicweb instance, extending the generic
@@ -433,15 +471,23 @@
     stored objects. Currently we have the following registries of objects known
     by the web instance (library may use some others additional registries):
 
-    * etypes
-    * views
-    * components
-    * actions
-    * forms
-    * formrenderers
-    * controllers, which are directly plugged into the application
-      object to handle request publishing XXX to merge with views
-    * contentnavigation XXX to merge with components? to kill?
+    * 'etypes', entity type classes
+
+    * 'views', views and templates (e.g. layout views)
+
+    * 'components', non contextual components, like magic search, url evaluators
+
+    * 'ctxcomponents', contextual components like boxes and dynamic section
+
+    * 'actions', contextual actions, eg links to display in predefined places in
+      the ui
+
+    * 'forms', describing logic of HTML form
+
+    * 'formrenderers', rendering forms to html
+
+    * 'controllers', primary objects to handle request publishing, directly
+      plugged into the application
     """
 
     def __init__(self, config, initlog=True):
@@ -456,6 +502,8 @@
             # don't clear rtags during test, this may cause breakage with
             # manually imported appobject modules
             CW_EVENT_MANAGER.bind('before-registry-reload', clear_rtag_objects)
+        self['boxes'] = BwCompatCWRegistry(self, 'boxes', 'ctxcomponents')
+        self['contentnavigation'] = BwCompatCWRegistry(self, 'contentnavigation', 'ctxcomponents')
 
     def setdefault(self, regid):
         try:
@@ -713,7 +761,7 @@
         vocab = pdef['vocabulary']
         if vocab is not None:
             if callable(vocab):
-                vocab = vocab(key, None) # XXX need a req object
+                vocab = vocab(None) # XXX need a req object
             if not value in vocab:
                 raise ValueError(_('unauthorized value'))
         return value
@@ -751,7 +799,7 @@
     def possible_actions(self, req, rset=None, **kwargs):
         return self["actions"].possible_actions(req, rest=rset, **kwargs)
 
-    @deprecated('[3.4] use vreg["boxes"].select_object(...)')
+    @deprecated('[3.4] use vreg["ctxcomponents"].select_object(...)')
     def select_box(self, oid, *args, **kwargs):
         return self['boxes'].select_object(oid, *args, **kwargs)
 
--- a/dataimport.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/dataimport.py	Mon Sep 13 16:47:03 2010 +0200
@@ -81,6 +81,7 @@
 from logilab.common.deprecation import deprecated
 
 from cubicweb.server.utils import eschema_eid
+from cubicweb.server.ssplanner import EditedEntity
 
 def count_lines(stream_or_filename):
     if isinstance(stream_or_filename, basestring):
@@ -612,8 +613,7 @@
         entity = copy(entity)
         entity.cw_clear_relation_cache()
         self.metagen.init_entity(entity)
-        entity.update(kwargs)
-        entity.edited_attributes = set(entity)
+        entity.cw_edited.update(kwargs, skipsec=False)
         session = self.session
         self.source.add_entity(session, entity)
         self.source.add_info(session, entity, self.source, None, complete=False)
@@ -686,8 +686,9 @@
         entity = self.session.vreg['etypes'].etype_class(etype)(self.session)
         # entity are "surface" copied, avoid shared dict between copies
         del entity.cw_extra_kwargs
+        entity.cw_edited = EditedEntity(entity)
         for attr in self.etype_attrs:
-            entity[attr] = self.generate(entity, attr)
+            entity.cw_edited.attribute_edited(attr, self.generate(entity, attr))
         rels = {}
         for rel in self.etype_rels:
             rels[rel] = self.generate(entity, rel)
@@ -696,7 +697,7 @@
     def init_entity(self, entity):
         entity.eid = self.source.create_eid(self.session)
         for attr in self.entity_attrs:
-            entity[attr] = self.generate(entity, attr)
+            entity.cw_edited.attribute_edited(attr, self.generate(entity, attr))
 
     def generate(self, entity, rtype):
         return getattr(self, 'gen_%s' % rtype)(entity)
--- a/dbapi.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/dbapi.py	Mon Sep 13 16:47:03 2010 +0200
@@ -313,19 +313,17 @@
 
     # low level session data management #######################################
 
-    def get_shared_data(self, key, default=None, pop=False):
-        """return value associated to `key` in shared data"""
-        return self.cnx.get_shared_data(key, default, pop)
-
-    def set_shared_data(self, key, value, querydata=False):
-        """set value associated to `key` in shared data
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
+        """see :meth:`Connection.get_shared_data`"""
+        return self.cnx.get_shared_data(key, default, pop, txdata)
 
-        if `querydata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction, and won't be available through the connexion, only on the
-        repository side.
-        """
-        return self.cnx.set_shared_data(key, value, querydata)
+    def set_shared_data(self, key, value, txdata=False, querydata=None):
+        """see :meth:`Connection.set_shared_data`"""
+        if querydata is not None:
+            txdata = querydata
+            warn('[3.10] querydata argument has been renamed to txdata',
+                 DeprecationWarning, stacklevel=2)
+        return self.cnx.set_shared_data(key, value, txdata)
 
     # server session compat layer #############################################
 
@@ -507,10 +505,12 @@
         return DBAPIRequest(self.vreg, DBAPISession(self))
 
     def check(self):
-        """raise `BadConnectionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid, else
+        return its latest activity timestamp.
+        """
         if self._closed is not None:
             raise ProgrammingError('Closed connection')
-        self._repo.check_session(self.sessionid)
+        return self._repo.check_session(self.sessionid)
 
     def set_session_props(self, **props):
         """raise `BadConnectionId` if the connection is no more valid"""
@@ -518,23 +518,29 @@
             raise ProgrammingError('Closed connection')
         self._repo.set_session_props(self.sessionid, props)
 
-    def get_shared_data(self, key, default=None, pop=False):
-        """return value associated to `key` in shared data"""
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.get_shared_data(self.sessionid, key, default, pop)
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
+        """return value associated to key in the session's data dictionary or
+        session's transaction's data if `txdata` is true.
 
-    def set_shared_data(self, key, value, querydata=False):
-        """set value associated to `key` in shared data
+        If pop is True, value will be removed from the dictionnary.
 
-        if `querydata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction, and won't be available through the connexion, only on the
-        repository side.
+        If key isn't defined in the dictionnary, value specified by the
+        `default` argument will be returned.
         """
         if self._closed is not None:
             raise ProgrammingError('Closed connection')
-        return self._repo.set_shared_data(self.sessionid, key, value, querydata)
+        return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
+
+    def set_shared_data(self, key, value, txdata=False):
+        """set value associated to `key` in shared data
+
+        if `txdata` is true, the value will be added to the repository session's
+        transaction's data which are cleared on commit/rollback of the current
+        transaction.
+        """
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
+        return self._repo.set_shared_data(self.sessionid, key, value, txdata)
 
     def get_schema(self):
         """Return the schema currently used by the repository.
@@ -625,7 +631,7 @@
         else:
             from cubicweb.entity import Entity
             user = Entity(req, rset, row=0)
-        user['login'] = login # cache login
+        user.cw_attr_cache['login'] = login # cache login
         return user
 
     def __del__(self):
--- a/devtools/testlib.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/devtools/testlib.py	Mon Sep 13 16:47:03 2010 +0200
@@ -499,7 +499,7 @@
     def list_boxes_for(self, rset):
         """returns the list of boxes that can be applied on `rset`"""
         req = rset.req
-        for box in self.vreg['boxes'].possible_objects(req, rset=rset):
+        for box in self.vreg['ctxcomponents'].possible_objects(req, rset=rset):
             yield box
 
     def list_startup_views(self):
@@ -951,7 +951,8 @@
         for action in self.list_actions_for(rset):
             yield InnerTest(self._testname(rset, action.__regid__, 'action'), self._test_action, action)
         for box in self.list_boxes_for(rset):
-            yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render)
+            w = [].append
+            yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w)
 
     @staticmethod
     def _testname(rset, objid, objtype):
--- a/doc/book/en/devrepo/devcore/cwconfig.rst	Mon Sep 13 16:46:52 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-Configuration
--------------
-
-.. automodule:: cubicweb.cwconfig
-      :members:
--- a/doc/book/en/devrepo/devcore/index.rst	Mon Sep 13 16:46:52 2010 +0200
+++ b/doc/book/en/devrepo/devcore/index.rst	Mon Sep 13 16:47:03 2010 +0200
@@ -6,5 +6,4 @@
 
    dbapi.rst
    reqbase.rst
-   cwconfig.rst
 
--- a/doc/book/en/devrepo/repo/hooks.rst	Mon Sep 13 16:46:52 2010 +0200
+++ b/doc/book/en/devrepo/repo/hooks.rst	Mon Sep 13 16:47:03 2010 +0200
@@ -1,158 +1,26 @@
 .. -*- coding: utf-8 -*-
-
 .. _hooks:
 
 Hooks and Operations
 ====================
 
-Generalities
-------------
-
-Paraphrasing the `emacs`_ documentation, let us say that hooks are an
-important mechanism for customizing an application. A hook is
-basically a list of functions to be called on some well-defined
-occasion (this is called `running the hook`).
-
-.. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
-
-In CubicWeb, hooks are subclasses of the Hook class in
-`server/hook.py`, implementing their own `call` method, and selected
-over a set of pre-defined `events` (and possibly more conditions,
-hooks being selectable AppObjects like views and components).
-
-There are two families of events: data events and server events. In a
-typical application, most of the Hooks are defined over data
-events.
-
-The purpose of data hooks is to complement the data model as defined
-in the schema.py, which is static by nature, with dynamic or value
-driven behaviours. It is functionally equivalent to a `database
-trigger`_, except that database triggers definition languages are not
-standardized, hence not portable (for instance, PL/SQL works with
-Oracle and PostgreSQL but not SqlServer nor Sqlite).
-
-.. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
-
-Data hooks can serve the following purposes:
-
-* enforcing constraints that the static schema cannot express
-  (spanning several entities/relations, exotic value ranges and
-  cardinalities, etc.)
-
-* implement computed attributes
-
-Operations are Hook-like objects that may be created by Hooks and
-scheduled to happen just before (or after) the `commit` event. Hooks
-being fired immediately on data operations, it is sometime necessary
-to delay the actual work down to a time where all other Hooks have
-run, for instance a validation check which needs that all relations be
-already set on an entity. Also while the order of execution of Hooks
-is data dependant (and thus hard to predict), it is possible to force
-an order on Operations.
-
-Operations also may be used to process various side effects associated
-with a transaction such as filesystem udpates, mail notifications,
-etc.
-
-Operations are subclasses of the Operation class in `server/hook.py`,
-implementing `precommit_event` and other standard methods (wholly
-described in :ref:`operations_api`).
-
-Events
-------
-
-Hooks are mostly defined and used to handle `dataflow`_ operations. It
-means as data gets in (entities added, updated, relations set or
-unset), specific events are issued and the Hooks matching these events
-are called.
-
-.. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow
-
-Below comes a list of the dataflow events related to entities operations:
-
-* before_add_entity
-
-* before_update_entity
+.. autodocstring:: cubicweb.server.hook
 
-* before_delete_entity
-
-* after_add_entity
-
-* after_update_entity
-
-* after_delete_entity
-
-These define ENTTIES HOOKS. RELATIONS HOOKS are defined
-over the following events:
-
-* after_add_relation
-
-* after_delete_relation
-
-* before_add_relation
-
-* before_delete_relation
-
-This is an occasion to remind us that relations support the add/delete
-operation, but no update.
-
-Non data events also exist. These are called SYSTEM HOOKS.
-
-* server_startup
-
-* server_shutdown
-
-* server_maintenance
-
-* server_backup
-
-* server_restore
+Example using dataflow hooks
+----------------------------
 
-* session_open
-
-* session_close
-
-
-Using dataflow Hooks
---------------------
-
-Dataflow hooks either automate data operations or maintain the
-consistency of the data model. In the later case, we must use a
-specific exception named ValidationError
-
-Validation Errors
-~~~~~~~~~~~~~~~~~
-
-When a condition is not met in a Hook/Operation, it must raise a
-`ValidationError`. Raising anything but a (subclass of)
-ValidationError is a programming error. Raising a ValidationError
-entails aborting the current transaction.
-
-The ValidationError exception is used to convey enough information up
-to the user interface. Hence its constructor is different from the
-default Exception constructor. It accepts, positionally:
-
-* an entity eid,
-
-* a dict whose keys represent attribute (or relation) names and values
-  an end-user facing message (hence properly translated) relating the
-  problem.
-
-An entity hook
-~~~~~~~~~~~~~~
-
-We will use a very simple example to show hooks usage. Let us start
-with the following schema.
+We will use a very simple example to show hooks usage. Let us start with the
+following schema.
 
 .. sourcecode:: python
 
    class Person(EntityType):
        age = Int(required=True)
 
-We would like to add a range constraint over a person's age. Let's
-write an hook. It shall be placed into mycube/hooks.py. If this file
-were to grow too much, we can easily have a mycube/hooks/... package
-containing hooks in various modules.
+We would like to add a range constraint over a person's age. Let's write an hook
+(supposing yams can not handle this nativly, which is wrong). It shall be placed
+into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
+`mycube/hooks/... package` containing hooks in various modules.
 
 .. sourcecode:: python
 
@@ -166,64 +34,26 @@
         __select__ = Hook.__select__ & is_instance('Person')
 
         def __call__(self):
-            if 0 >= self.entity.age <= 120:
-               return
-            msg = self._cw._('age must be between 0 and 120')
-            raise ValidationError(self.entity.eid, {'age': msg})
-
-Hooks being AppObjects like views, they have a __regid__ and a
-__select__ class attribute. The base __select__ is augmented with an
-`is_instance` selector matching the desired entity type. The `events`
-tuple is used by the Hook.__select__ base selector to dispatch the
-hook on the right events. In an entity hook, it is possible to
-dispatch on any entity event (e.g. 'before_add_entity',
-'before_update_entity') at once if needed.
+	    if 'age' in self.entity.cw_edited:
+                if 0 <= self.entity.age <= 120:
+                   return
+		msg = self._cw._('age must be between 0 and 120')
+		raise ValidationError(self.entity.eid, {'age': msg})
 
-Like all appobjects, hooks have the `self._cw` attribute which
-represents the current session. In entity hooks, a `self.entity`
-attribute is also present.
-
-
-A relation hook
-~~~~~~~~~~~~~~~
-
-Let us add another entity type with a relation to person (in
-mycube/schema.py).
-
-.. sourcecode:: python
+In our example the base `__select__` is augmented with an `is_instance` selector
+matching the desired entity type.
 
-   class Company(EntityType):
-        name = String(required=True)
-        boss = SubjectRelation('Person', cardinality='1*')
+The `events` tuple is used specify that our hook should be called before the
+entity is added or updated.
 
-We would like to constrain the company's bosses to have a minimum
-(legal) age. Let's write an hook for this, which will be fired when
-the `boss` relation is established.
-
-.. sourcecode:: python
-
-   class CompanyBossLegalAge(Hook):
-        __regid__ = 'company_boss_legal_age'
-        events = ('before_add_relation',)
-        __select__ = Hook.__select__ & match_rtype('boss')
+Then in the hook's `__call__` method, we:
 
-        def __call__(self):
-            boss = self._cw.entity_from_eid(self.eidto)
-            if boss.age < 18:
-                msg = self._cw._('the minimum age for a boss is 18')
-                raise ValidationError(self.eidfrom, {'boss': msg})
-
-We use the `match_rtype` selector to select the proper relation type.
+* check if the 'age' attribute is edited
+* if so, check the value is in the range
+* if not, raise a validation error properly
 
-The essential difference with respect to an entity hook is that there
-is no self.entity, but `self.eidfrom` and `self.eidto` hook attributes
-which represent the subject and object eid of the relation.
-
-
-Using Operations
-----------------
-
-Let's augment our example with a new `subsidiary_of` relation on Company.
+Now Let's augment our schema with new `Company` entity type with some relation to
+`Person` (in 'mycube/schema.py').
 
 .. sourcecode:: python
 
@@ -232,12 +62,37 @@
         boss = SubjectRelation('Person', cardinality='1*')
         subsidiary_of = SubjectRelation('Company', cardinality='*?')
 
-Base example
-~~~~~~~~~~~~
+
+We would like to constrain the company's bosses to have a minimum (legal)
+age. Let's write an hook for this, which will be fired when the `boss` relation
+is established (still supposing we could not specify that kind of thing in the
+schema).
+
+.. sourcecode:: python
+
+   class CompanyBossLegalAge(Hook):
+        __regid__ = 'company_boss_legal_age'
+        __select__ = Hook.__select__ & match_rtype('boss')
+        events = ('before_add_relation',)
 
-We would like to check that there is no cycle by the `subsidiary_of`
-relation. This is best achieved in an Operation since all relations
-are likely to be set at commit time.
+        def __call__(self):
+            boss = self._cw.entity_from_eid(self.eidto)
+            if boss.age < 18:
+                msg = self._cw._('the minimum age for a boss is 18')
+                raise ValidationError(self.eidfrom, {'boss': msg})
+
+.. Note::
+
+    We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
+    proper relation type.
+
+    The essential difference with respect to an entity hook is that there is no
+    self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
+    represent the subject and object **eid** of the relation.
+
+Suppose we want to check that there is no cycle by the `subsidiary_of`
+relation. This is best achieved in an operation since all relations are likely to
+be set at commit time.
 
 .. sourcecode:: python
 
@@ -251,6 +106,7 @@
                 raise ValidationError(eid, {rtype: msg})
             parents.add(parent.eid)
 
+
     class CheckSubsidiaryCycleOp(Operation):
 
         def precommit_event(self):
@@ -259,30 +115,20 @@
 
     class CheckSubsidiaryCycleHook(Hook):
         __regid__ = 'check_no_subsidiary_cycle'
+        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
         events = ('after_add_relation',)
-        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
 
         def __call__(self):
             CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
 
-The operation is instantiated in the Hook.__call__ method.
 
-An operation always takes a session object as first argument
-(accessible as `.session` from the operation instance), and optionally
-all keyword arguments needed by the operation. These keyword arguments
-will be accessible as attributes from the operation instance.
+Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
+exceptions are usually programming errors.
 
-Like in Hooks, ValidationError can be raised in Operations. Other
-exceptions are programming errors.
-
-Notice how our hook will instantiate an operation each time the Hook
-is called, i.e. each time the `subsidiary_of` relation is set.
-
-Using set_operation
-~~~~~~~~~~~~~~~~~~~
-
-There is an alternative method to schedule an Operation from a Hook,
-using the `set_operation` function.
+In the above example, our hook will instantiate an operation each time the hook
+is called, i.e. each time the `subsidiary_of` relation is set. There is an
+alternative method to schedule an operation from a hook, using the
+:func:`set_operation` function.
 
 .. sourcecode:: python
 
@@ -295,142 +141,91 @@
 
        def __call__(self):
            set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto,
-                         CheckSubsidiaryCycleOp, rtype=self.rtype)
+                         CheckSubsidiaryCycleOp)
 
    class CheckSubsidiaryCycleOp(Operation):
 
        def precommit_event(self):
-           for eid in self._cw.transaction_data['subsidiary_cycle_detection']:
-               check_cycle(self.session, eid, self.rtype)
+           for eid in self._cw.transaction_data.pop('subsidiary_cycle_detection'):
+               check_cycle(self.session, eid, 'subsidiary_of')
 
-Here, we call set_operation with a session object, a specially forged
-key, a value that is the actual payload of an individual operation (in
-our case, the object of the subsidiary_of relation) , the class of the
-Operation, and 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.
-
-This mechanism is especially useful on two occasions (not shown in our
-example):
-
-* massive data import (reduced memory consumption within a large
-  transaction)
-
-* when several hooks need to instantiate the same operation (e.g. an
-  entity and a relation hook).
+Here, we call :func:`set_operation` so that we will simply accumulate eids of
+entities to check at the end in a single CheckSubsidiaryCycleOp operation.  Value
+are stored in a set associated to the 'subsidiary_cycle_detection' transaction
+data key. The set initialization and operation creation are handled nicely by
+:func:set_operation.
 
-.. note::
-
-  A more realistic example can be found in the advanced tutorial
-  chapter :ref:`adv_tuto_security_propagation`.
+A more realistic example can be found in the advanced tutorial chapter
+:ref:`adv_tuto_security_propagation`.
 
-.. _operations_api:
 
-Operation: a small API overview
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Hooks writing tips
+------------------
 
-.. autoclass:: cubicweb.server.hook.Operation
-.. autoclass:: cubicweb.server.hook.LateOperation
-.. autofunction:: cubicweb.server.hook.set_operation
+Reminder
+~~~~~~~~
 
-Hooks writing rules
--------------------
+Never, ever use the `entity.foo = 42` notation to update an entity. It will not
+work.To updating an entity attribute or relation, uses :meth:`set_attributes` and
+:meth:`set_relations` methods.
 
-Remainder
-~~~~~~~~~
-
-Never, ever use the `entity.foo = 42` notation to update an entity. It
-will not work.
 
 How to choose between a before and an after event ?
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Before hooks give you access to the old attribute (or relation)
-values. By definition the database is not yet updated in a before
-hook.
-
-To access old and new values in an before_update_entity hook, one can
-use the `server.hook.entity_oldnewvalue` function which returns a
-tuple of the old and new values. This function takes an entity and an
-attribute name as parameters.
-
-In a 'before_add|update_entity' hook the self.entity contains the new
-values. One is allowed to further modify them before database
-operations, using the dictionary notation.
-
-.. sourcecode:: python
-
-   self.entity['age'] = 42
+'before_*' hooks give you access to the old attribute (or relation)
+values. You can also hi-jack actually edited stuff in the case of entity
+modification. Needing one of this will definitly guide your choice.
 
-This is because using self.entity.set_attributes(age=42) will
-immediately update the database (which does not make sense in a
-pre-database hook), and will trigger any existing
-before_add|update_entity hook, thus leading to infinite hook loops or
-such awkward situations.
-
-Beyond these specific cases, updating an entity attribute or relation
-must *always* be done using `set_attributes` and `set_relations`
-methods.
+Else the question is: should I need to do things before or after the actual
+modification. If the answer is "it doesn't matter", use an 'after' event.
 
-(Of course, ValidationError will always abort the current transaction,
-whetever the event).
 
-Peculiarities of inlined relations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Some relations are defined in the schema as `inlined` (see
-:ref:`RelationType` for details). In this case, they are inserted in
-the database at the same time as entity attributes.
-
-Hence in the case of before_add_relation, such relations already exist
-in the database.
-
-Edited attributes
+Validation Errors
 ~~~~~~~~~~~~~~~~~
 
-On udpates, it is possible to ask the `entity.edited_attributes`
-variable whether one attribute has been updated.
+When a hook is responsible to maintain the consistency of the data model detect
+an error, it must use a specific exception named
+:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
+:exc:`~cubicweb.ValidationError` is a programming error. Raising a it entails
+aborting the current transaction.
 
-.. sourcecode:: python
+This exception is used to convey enough information up to the user
+interface. Hence its constructor is different from the default Exception
+constructor. It accepts, positionally:
+
+* an entity eid,
 
-  if 'age' not in entity.edited_attribute:
-      return
+* a dict whose keys represent attribute (or relation) names and values
+  an end-user facing message (hence properly translated) relating the
+  problem.
+
+
+Checking for object created/deleted in the current transaction
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Deleted in transaction
-~~~~~~~~~~~~~~~~~~~~~~
+In hooks, you can use the
+:meth:`~cubicweb.server.session.Session.added_in_transaction` or
+:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
+object to check if an eid has been created or deleted during the hook's
+transaction.
 
-The session object has a deleted_in_transaction method, which can help
-writing deletion Hooks.
+This is useful to enable or disable some stuff if some entity is being added or
+deleted.
 
 .. sourcecode:: python
 
    if self._cw.deleted_in_transaction(self.eidto):
       return
 
-Given this predicate, we can avoid scheduling an operation.
 
-Disabling hooks
-~~~~~~~~~~~~~~~
-
-It is sometimes convenient to disable some hooks. For instance to
-avoid infinite Hook loops. One uses the `hooks_control` context
-manager.
-
-This can be controlled more finely through the `category` Hook class
-attribute, which is a string.
+Peculiarities of inlined relations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. sourcecode:: python
-
-   with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, <category>):
-       # ... do stuff
-
-.. autoclass:: cubicweb.server.session.hooks_control
-
-The existing categories are: ``email``, ``syncsession``,
-``syncschema``, ``bookmark``, ``security``, ``worfklow``,
-``metadata``, ``notification``, ``integrity``, ``activeintegrity``.
-
-Nothing precludes one to invent new categories and use the
-hooks_control context manager to filter them (in or out).
+Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
+for details) are inserted in the database at the same time as entity attributes.
+This may have some side effect, for instance when creating entity and setting an
+inlined relation in the same rql query, when 'before_add_relation' for that
+relation will be run, the relation will already exist in the database (it's
+usually not the case).
--- a/entities/__init__.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/entities/__init__.py	Mon Sep 13 16:47:03 2010 +0200
@@ -35,6 +35,11 @@
     __regid__ = 'Any'
     __implements__ = ()
 
+    @classmethod
+    def cw_create_url(cls, req, **kwargs):
+        """ return the url of the entity creation form for this entity type"""
+        return req.build_url('add/%s' % cls.__regid__, **kwargs)
+
     # meta data api ###########################################################
 
     def dc_title(self):
--- a/entity.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/entity.py	Mon Sep 13 16:47:03 2010 +0200
@@ -19,7 +19,6 @@
 
 __docformat__ = "restructuredtext en"
 
-from copy import copy
 from warnings import warn
 
 from logilab.common import interface
@@ -312,6 +311,9 @@
         return '<Entity %s %s %s at %s>' % (
             self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
 
+    def __cmp__(self, other):
+        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+
     def __json_encode__(self):
         """custom json dumps hook to dump the entity's eid
         which is not part of dict structure itself
@@ -320,107 +322,6 @@
         dumpable['eid'] = self.eid
         return dumpable
 
-    def __nonzero__(self):
-        return True
-
-    def __hash__(self):
-        return id(self)
-
-    def __cmp__(self, other):
-        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
-
-    def __contains__(self, key):
-        return key in self.cw_attr_cache
-
-    def __iter__(self):
-        return iter(self.cw_attr_cache)
-
-    def __getitem__(self, key):
-        if key == 'eid':
-            warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
-                 DeprecationWarning, stacklevel=2)
-            return self.eid
-        return self.cw_attr_cache[key]
-
-    def __setitem__(self, attr, value):
-        """override __setitem__ to update self.edited_attributes.
-
-        Typically, a before_[update|add]_hook could do::
-
-            entity['generated_attr'] = generated_value
-
-        and this way, edited_attributes 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.
-        """
-        if attr == 'eid':
-            warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
-                 DeprecationWarning, stacklevel=2)
-            self.eid = value
-        else:
-            self.cw_attr_cache[attr] = value
-            # don't add attribute into skip_security if already in edited
-            # attributes, else we may accidentaly skip a desired security check
-            if hasattr(self, 'edited_attributes') and \
-                   attr not in self.edited_attributes:
-                self.edited_attributes.add(attr)
-                self._cw_skip_security_attributes.add(attr)
-
-    def __delitem__(self, attr):
-        """override __delitem__ to update self.edited_attributes 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.edited_attributes
-            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_attr_cache[attr]
-        if hasattr(self, 'edited_attributes'):
-            self.edited_attributes.remove(attr)
-
-    def clear(self):
-        self.cw_attr_cache.clear()
-
-    def get(self, key, default=None):
-        return self.cw_attr_cache.get(key, default)
-
-    def setdefault(self, attr, default):
-        """override setdefault to update self.edited_attributes"""
-        value = self.cw_attr_cache.setdefault(attr, default)
-        # don't add attribute into skip_security if already in edited
-        # attributes, else we may accidentaly skip a desired security check
-        if hasattr(self, 'edited_attributes') and \
-               attr not in self.edited_attributes:
-            self.edited_attributes.add(attr)
-            self._cw_skip_security_attributes.add(attr)
-        return value
-
-    def pop(self, attr, default=_marker):
-        """override pop to update self.edited_attributes on cleanup of
-        undesired changes introduced in the entity's dict. See `__delitem__`
-        """
-        if default is _marker:
-            value = self.cw_attr_cache.pop(attr)
-        else:
-            value = self.cw_attr_cache.pop(attr, default)
-        if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
-            self.edited_attributes.remove(attr)
-        return value
-
-    def update(self, values):
-        """override update to update self.edited_attributes. See `__setitem__`
-        """
-        for attr, value in values.items():
-            self[attr] = value # use self.__setitem__ implementation
-
     def cw_adapt_to(self, interface):
         """return an adapter the entity to the given interface name.
 
@@ -590,12 +491,6 @@
 
     # entity cloning ##########################################################
 
-    def cw_copy(self):
-        thecopy = copy(self)
-        thecopy.cw_attr_cache = copy(self.cw_attr_cache)
-        thecopy._cw_related_cache = {}
-        return thecopy
-
     def copy_relations(self, ceid): # XXX cw_copy_relations
         """copy relations of the object with the given eid on this
         object (this method is called on the newly created copy, and
@@ -680,7 +575,7 @@
             rdef = rschema.rdef(self.e_schema, attrschema)
             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
                    or (attrschema.type == 'Password' and skip_pwd):
-                self[attr] = None
+                self.cw_attr_cache[attr] = None
                 continue
             yield attr
 
@@ -739,7 +634,7 @@
             rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
             # handle attributes
             for i in xrange(1, lastattr):
-                self[str(selected[i-1][0])] = rset[i]
+                self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
             # handle relations
             for i in xrange(lastattr, len(rset)):
                 rtype, role = selected[i-1][0]
@@ -759,7 +654,7 @@
         :param name: name of the attribute to get
         """
         try:
-            value = self.cw_attr_cache[name]
+            return self.cw_attr_cache[name]
         except KeyError:
             if not self.cw_is_saved():
                 return None
@@ -767,21 +662,20 @@
             try:
                 rset = self._cw.execute(rql, {'x': self.eid})
             except Unauthorized:
-                self[name] = value = None
+                self.cw_attr_cache[name] = value = None
             else:
                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
                 try:
-                    self[name] = value = rset.rows[0][0]
+                    self.cw_attr_cache[name] = value = rset.rows[0][0]
                 except IndexError:
                     # probably a multisource error
                     self.critical("can't get value for attribute %s of entity with eid %s",
                                   name, self.eid)
                     if self.e_schema.destination(name) == 'String':
-                        # XXX (syt) imo emtpy string is better
-                        self[name] = value = self._cw._('unaccessible')
+                        self.cw_attr_cache[name] = value = self._cw._('unaccessible')
                     else:
-                        self[name] = value = None
-        return value
+                        self.cw_attr_cache[name] = value = None
+            return value
 
     def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
         """returns a resultset of related entities
@@ -985,7 +879,6 @@
         you should override this method to clear them as well.
         """
         # clear attributes cache
-        haseid = 'eid' in self
         self._cw_completed = False
         self.cw_attr_cache.clear()
         # clear relations cache
@@ -1012,9 +905,9 @@
                          kwargs)
         kwargs.pop('x')
         # update current local object _after_ the rql query to avoid
-        # interferences between the query execution itself and the
-        # edited_attributes / skip_security_attributes machinery
-        self.update(kwargs)
+        # interferences between the query execution itself and the cw_edited /
+        # skip_security machinery
+        self.cw_attr_cache.update(kwargs)
 
     def set_relations(self, **kwargs): # XXX cw_set_relations
         """add relations to the given object. To set a relation where this entity
@@ -1045,58 +938,13 @@
         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
                          {'x': self.eid}, **kwargs)
 
-    # server side utilities ###################################################
-
-    def _cw_rql_set_value(self, attr, value):
-        """call by rql execution plan when some attribute is modified
-
-        don't use dict api in such case since we don't want attribute to be
-        added to skip_security_attributes.
-
-        This method is for internal use, you should not use it.
-        """
-        self.cw_attr_cache[attr] = value
+    # server side utilities ####################################################
 
     def _cw_clear_local_perm_cache(self, action):
         for rqlexpr in self.e_schema.get_rqlexprs(action):
             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
 
-    @property
-    def _cw_skip_security_attributes(self):
-        try:
-            return self.__cw_skip_security_attributes
-        except:
-            self.__cw_skip_security_attributes = set()
-            return self.__cw_skip_security_attributes
-
-    def _cw_set_defaults(self):
-        """set default values according to the schema"""
-        for attr, value in self.e_schema.defaults():
-            if not self.cw_attr_cache.has_key(attr):
-                self[str(attr)] = value
-
-    def _cw_check(self, creation=False):
-        """check this entity against its schema. Only final relation
-        are checked here, constraint on actual relations are checked in hooks
-        """
-        # necessary since eid is handled specifically and yams require it to be
-        # in the dictionary
-        if self._cw is None:
-            _ = unicode
-        else:
-            _ = self._cw._
-        if creation:
-            # on creations, we want to check all relations, especially
-            # required attributes
-            relations = [rschema for rschema in self.e_schema.subject_relations()
-                         if rschema.final and rschema.type != 'eid']
-        elif hasattr(self, 'edited_attributes'):
-            relations = [self._cw.vreg.schema.rschema(rtype)
-                         for rtype in self.edited_attributes]
-        else:
-            relations = None
-        self.e_schema.check(self, creation=creation, _=_,
-                            relations=relations)
+    # deprecated stuff #########################################################
 
     @deprecated('[3.9] use entity.cw_attr_value(attr)')
     def get_value(self, name):
@@ -1126,6 +974,109 @@
     def related_rql(self, rtype, role='subject', targettypes=None):
         return self.cw_related_rql(rtype, role, targettypes)
 
+    @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.skip_security')
+    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):
+        if key == 'eid':
+            warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
+                 DeprecationWarning, stacklevel=2)
+            return self.eid
+        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.
+        """
+        if attr == 'eid':
+            warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
+                 DeprecationWarning, stacklevel=2)
+            self.eid = value
+        else:
+            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 ##########################################
 
@@ -1141,8 +1092,9 @@
             return self
         return eobj.cw_attr_value(self._attrname)
 
+    @deprecated('[3.10] use entity.cw_attr_cache[attr] = value')
     def __set__(self, eobj, value):
-        eobj[self._attrname] = value
+        eobj.cw_attr_cache[self._attrname] = value
 
 
 class Relation(object):
--- a/etwist/twconfig.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/etwist/twconfig.py	Mon Sep 13 16:47:03 2010 +0200
@@ -76,12 +76,6 @@
 the repository rather than the user running the command',
           'group': 'main', 'level': WebConfiguration.mode == 'system'
           }),
-        ('session-time',
-         {'type' : 'time',
-          'default': '30min',
-          'help': 'session expiration time, default to 30 minutes',
-          'group': 'main', 'level': 1,
-          }),
         ('pyro-server',
          {'type' : 'yn',
           # pyro is only a recommends by default, so don't activate it here
--- a/goa/appobjects/components.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/goa/appobjects/components.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""overrides some base views for cubicweb on google appengine
+"""overrides some base views for cubicweb on google appengine"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
@@ -88,7 +87,8 @@
         view = self.vreg.select('views', 'list', req, req.etype_rset(etype))
         url = view.url()
         etypelink = u'&#160;<a href="%s">%s</a>' % (xml_escape(url), label)
-        yield (label, etypelink, self.add_entity_link(eschema, req))
+        if eschema.has_perm(req, 'add'):
+            yield (label, etypelink, self.add_entity_link(etype))
 
 ManageView.entity_types = entity_types_no_count
 
--- a/goa/gaesource.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/goa/gaesource.py	Mon Sep 13 16:47:03 2010 +0200
@@ -118,7 +118,7 @@
             Put(gaeentity)
         modified.clear()
 
-    def commit_event(self):
+    def postcommit_event(self):
         self._put_entities()
 
     def precommit_event(self):
--- a/hooks/integrity.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/integrity.py	Mon Sep 13 16:47:03 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Core hooks: check for data integrity according to the instance'schema
 validity
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from threading import Lock
@@ -64,8 +64,6 @@
         _UNIQUE_CONSTRAINTS_LOCK.release()
 
 class _ReleaseUniqueConstraintsOperation(hook.Operation):
-    def commit_event(self):
-        pass
     def postcommit_event(self):
         _release_unique_cstr_lock(self.session)
     def rollback_event(self):
@@ -185,9 +183,6 @@
                     self.critical('can\'t check constraint %s, not supported',
                                   constraint)
 
-    def commit_event(self):
-        pass
-
 
 class CheckConstraintHook(IntegrityHook):
     """check the relation satisfy its constraints
@@ -219,7 +214,7 @@
 
     def __call__(self):
         eschema = self.entity.e_schema
-        for attr in self.entity.edited_attributes:
+        for attr in self.entity.cw_edited:
             if eschema.subjrels[attr].final:
                 constraints = [c for c in eschema.rdef(attr).constraints
                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
@@ -236,9 +231,8 @@
     def __call__(self):
         entity = self.entity
         eschema = entity.e_schema
-        for attr in entity.edited_attributes:
+        for attr, val in entity.cw_edited.iteritems():
             if eschema.subjrels[attr].final and eschema.has_unique_values(attr):
-                val = entity[attr]
                 if val is None:
                     continue
                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
@@ -257,18 +251,17 @@
     events = ('before_delete_entity', 'before_update_entity')
 
     def __call__(self):
-        if self.event == 'before_delete_entity' and self.entity.name == 'owners':
+        entity = self.entity
+        if self.event == 'before_delete_entity' and entity.name == 'owners':
             msg = self._cw._('can\'t be deleted')
-            raise ValidationError(self.entity.eid, {None: msg})
-        elif self.event == 'before_update_entity' and \
-                 'name' in self.entity.edited_attributes:
-            newname = self.entity.pop('name')
-            oldname = self.entity.name
+            raise ValidationError(entity.eid, {None: msg})
+        elif self.event == 'before_update_entity' \
+                 and 'name' in entity.cw_edited:
+            oldname, newname = entity.cw_edited.oldnewvalue('name')
             if oldname == 'owners' and newname != oldname:
                 qname = role_name('name', 'subject')
                 msg = self._cw._('can\'t be changed')
-                raise ValidationError(self.entity.eid, {qname: msg})
-            self.entity['name'] = newname
+                raise ValidationError(entity.eid, {qname: msg})
 
 
 class TidyHtmlFields(IntegrityHook):
@@ -279,15 +272,16 @@
     def __call__(self):
         entity = self.entity
         metaattrs = entity.e_schema.meta_attributes()
+        edited = entity.cw_edited
         for metaattr, (metadata, attr) in metaattrs.iteritems():
-            if metadata == 'format' and attr in entity.edited_attributes:
+            if metadata == 'format' and attr in edited:
                 try:
-                    value = entity[attr]
+                    value = edited[attr]
                 except KeyError:
                     continue # no text to tidy
                 if isinstance(value, unicode): # filter out None and Binary
                     if getattr(entity, str(metaattr)) == 'text/html':
-                        entity[attr] = soup2xhtml(value, self._cw.encoding)
+                        edited[attr] = soup2xhtml(value, self._cw.encoding)
 
 
 class StripCWUserLoginHook(IntegrityHook):
@@ -297,9 +291,9 @@
     events = ('before_add_entity', 'before_update_entity',)
 
     def __call__(self):
-        user = self.entity
-        if 'login' in user.edited_attributes and user.login:
-            user.login = user.login.strip()
+        login = self.entity.cw_edited.get('login')
+        if login:
+            self.entity.cw_edited['login'] = login.strip()
 
 
 # 'active' integrity hooks: you usually don't want to deactivate them, they are
--- a/hooks/metadata.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/metadata.py	Mon Sep 13 16:47:03 2010 +0200
@@ -41,11 +41,12 @@
 
     def __call__(self):
         timestamp = datetime.now()
-        self.entity.setdefault('creation_date', timestamp)
-        self.entity.setdefault('modification_date', timestamp)
+        edited = self.entity.cw_edited
+        edited.setdefault('creation_date', timestamp)
+        edited.setdefault('modification_date', timestamp)
         if not self._cw.get_shared_data('do-not-insert-cwuri'):
             cwuri = u'%seid/%s' % (self._cw.base_url(), self.entity.eid)
-            self.entity.setdefault('cwuri', cwuri)
+            edited.setdefault('cwuri', cwuri)
 
 
 class UpdateMetaAttrsHook(MetaDataHook):
@@ -60,7 +61,7 @@
         # XXX to be really clean, we should turn off modification_date update
         # explicitly on each command where we do not want that behaviour.
         if not self._cw.vreg.config.repairing:
-            self.entity.setdefault('modification_date', datetime.now())
+            self.entity.cw_edited.setdefault('modification_date', datetime.now())
 
 
 class _SetCreatorOp(hook.Operation):
--- a/hooks/notification.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/notification.py	Mon Sep 13 16:47:03 2010 +0200
@@ -125,7 +125,7 @@
         if session.added_in_transaction(self.entity.eid):
             return # entity is being created
         # then compute changes
-        attrs = [k for k in self.entity.edited_attributes
+        attrs = [k for k in self.entity.cw_edited
                  if not k in self.skip_attrs]
         if not attrs:
             return
@@ -168,8 +168,9 @@
             if self._cw.added_in_transaction(self.entity.eid):
                 return False
             if self.entity.e_schema == 'CWUser':
-                if not (self.entity.edited_attributes - frozenset(('eid', 'modification_date',
-                                                                   'last_login_time'))):
+                if not (frozenset(self.entity.cw_edited)
+                        - frozenset(('eid', 'modification_date',
+                                     'last_login_time'))):
                     # don't record last_login_time update which are done
                     # automatically at login time
                     return False
--- a/hooks/security.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/security.py	Mon Sep 13 16:47:03 2010 +0200
@@ -31,12 +31,9 @@
     eschema = entity.e_schema
     # ._cw_skip_security_attributes is there to bypass security for attributes
     # set by hooks by modifying the entity's dictionnary
-    dontcheck = entity._cw_skip_security_attributes
     if editedattrs is None:
-        try:
-            editedattrs = entity.edited_attributes
-        except AttributeError:
-            editedattrs = entity # XXX unexpected
+        editedattrs = entity.cw_edited
+    dontcheck = editedattrs.skip_security
     for attr in editedattrs:
         if attr in dontcheck:
             continue
@@ -46,10 +43,6 @@
             if creation and not rdef.permissions.get('update'):
                 continue
             rdef.check_perm(session, 'update', eid=eid)
-    # don't update dontcheck until everything went fine: see usage in
-    # after_update_entity, where if we got an Unauthorized at hook time, we will
-    # retry and commit time
-    dontcheck |= frozenset(editedattrs)
 
 
 class _CheckEntityPermissionOp(hook.LateOperation):
@@ -57,15 +50,12 @@
         #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
         session = self.session
         for values in session.transaction_data.pop('check_entity_perm_op'):
-            entity = session.entity_from_eid(values[0])
-            action = values[1]
+            eid, action, edited = values
+            entity = session.entity_from_eid(eid)
             entity.cw_check_perm(action)
-            check_entity_attributes(session, entity, values[2:],
+            check_entity_attributes(session, entity, edited,
                                     creation=self.creation)
 
-    def commit_event(self):
-        pass
-
 
 class _CheckRelationPermissionOp(hook.LateOperation):
     def precommit_event(self):
@@ -76,9 +66,6 @@
                                 session.describe(eidto)[0])
             rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto)
 
-    def commit_event(self):
-        pass
-
 
 @objectify_selector
 @lltrace
@@ -99,7 +86,7 @@
 
     def __call__(self):
         hook.set_operation(self._cw, 'check_entity_perm_op',
-                           (self.entity.eid, 'add') + tuple(self.entity.edited_attributes),
+                           (self.entity.eid, 'add', self.entity.cw_edited),
                            _CheckEntityPermissionOp, creation=True)
 
 
@@ -115,10 +102,10 @@
         except Unauthorized:
             self.entity._cw_clear_local_perm_cache('update')
             # save back editedattrs in case the entity is reedited later in the
-            # same transaction, which will lead to edited_attributes being
+            # same transaction, which will lead to cw_edited being
             # overwritten
             hook.set_operation(self._cw, 'check_entity_perm_op',
-                               (self.entity.eid, 'update') + tuple(self.entity.edited_attributes),
+                               (self.entity.eid, 'update', self.entity.cw_edited),
                                _CheckEntityPermissionOp, creation=False)
 
 
--- a/hooks/syncschema.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/syncschema.py	Mon Sep 13 16:47:03 2010 +0200
@@ -128,14 +128,12 @@
 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
     errors = {}
     # don't use getattr(entity, attr), we would get the modified value if any
-    for attr in entity.edited_attributes:
+    for attr in entity.cw_edited:
         if attr in ro_attrs:
-            newval = entity.pop(attr)
-            origval = getattr(entity, attr)
+            origval, newval = entity.cw_edited.oldnewvalue(attr)
             if newval != origval:
                 errors[attr] = session._("can't change the %s attribute") % \
                                display_name(session, attr)
-            entity[attr] = newval
     if errors:
         raise ValidationError(entity.eid, errors)
 
@@ -907,7 +905,7 @@
 
     def __call__(self):
         entity = self.entity
-        if entity.get('final'):
+        if entity.cw_edited.get('final'):
             return
         CWETypeAddOp(self._cw, entity=entity)
 
@@ -921,8 +919,8 @@
         entity = self.entity
         check_valid_changes(self._cw, entity, ro_attrs=('final',))
         # don't use getattr(entity, attr), we would get the modified value if any
-        if 'name' in entity.edited_attributes:
-            oldname, newname = hook.entity_oldnewvalue(entity, 'name')
+        if 'name' in entity.cw_edited:
+            oldname, newname = entity.cw_edited.oldnewvalue('name')
             if newname.lower() != oldname.lower():
                 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
 
@@ -965,8 +963,8 @@
         entity = self.entity
         rtypedef = ybo.RelationType(name=entity.name,
                                     description=entity.description,
-                                    inlined=entity.get('inlined', False),
-                                    symmetric=entity.get('symmetric', False),
+                                    inlined=entity.cw_edited.get('inlined', False),
+                                    symmetric=entity.cw_edited.get('symmetric', False),
                                     eid=entity.eid)
         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
 
@@ -981,10 +979,10 @@
         check_valid_changes(self._cw, entity)
         newvalues = {}
         for prop in ('symmetric', 'inlined', 'fulltext_container'):
-            if prop in entity.edited_attributes:
-                old, new = hook.entity_oldnewvalue(entity, prop)
+            if prop in entity.cw_edited:
+                old, new = entity.cw_edited.oldnewvalue(prop)
                 if old != new:
-                    newvalues[prop] = entity[prop]
+                    newvalues[prop] = new
         if newvalues:
             rschema = self._cw.vreg.schema.rschema(entity.name)
             CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
@@ -1069,8 +1067,8 @@
                 attr = 'ordernum'
             else:
                 attr = prop
-            if attr in entity.edited_attributes:
-                old, new = hook.entity_oldnewvalue(entity, attr)
+            if attr in entity.cw_edited:
+                old, new = entity.cw_edited.oldnewvalue(attr)
                 if old != new:
                     newvalues[prop] = new
         if newvalues:
--- a/hooks/syncsession.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/syncsession.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Core hooks: synchronize living session on persistent data changes
+"""Core hooks: synchronize living session on persistent data changes"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from yams.schema import role_name
@@ -56,26 +55,25 @@
 
 class _DeleteGroupOp(_GroupOperation):
     """synchronize user when a in_group relation has been deleted"""
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         groups = self.cnxuser.groups
         try:
             groups.remove(self.group)
         except KeyError:
             self.error('user %s not in group %s',  self.cnxuser, self.group)
-            return
 
 
 class _AddGroupOp(_GroupOperation):
     """synchronize user when a in_group relation has been added"""
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         groups = self.cnxuser.groups
         if self.group in groups:
             self.warning('user %s already in group %s', self.cnxuser,
                          self.group)
-            return
-        groups.add(self.group)
+        else:
+            groups.add(self.group)
 
 
 class SyncInGroupHook(SyncSessionHook):
@@ -98,7 +96,7 @@
         self.cnxid = cnxid
         hook.Operation.__init__(self, session)
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         try:
             self.session.repo.close(self.cnxid)
@@ -123,7 +121,7 @@
 class _DelCWPropertyOp(hook.Operation):
     """a user's custom properties has been deleted"""
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         try:
             del self.cwpropdict[self.key]
@@ -134,7 +132,7 @@
 class _ChangeCWPropertyOp(hook.Operation):
     """a user's custom properties has been added/changed"""
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         self.cwpropdict[self.key] = self.value
 
@@ -142,7 +140,7 @@
 class _AddCWPropertyOp(hook.Operation):
     """a user's custom properties has been added/changed"""
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         cwprop = self.cwprop
         if not cwprop.for_user:
@@ -180,8 +178,8 @@
 
     def __call__(self):
         entity = self.entity
-        if not ('pkey' in entity.edited_attributes or
-                'value' in entity.edited_attributes):
+        if not ('pkey' in entity.cw_edited or
+                'value' in entity.cw_edited):
             return
         key, value = entity.pkey, entity.value
         session = self._cw
--- a/hooks/test/unittest_hooks.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/test/unittest_hooks.py	Mon Sep 13 16:47:03 2010 +0200
@@ -143,7 +143,7 @@
         entity.set_attributes(name=u'wf2')
         self.assertEquals(entity.description, u'yo')
         entity.set_attributes(description=u'R&D<p>yo')
-        entity.pop('description')
+        entity.cw_attr_cache.pop('description')
         self.assertEquals(entity.description, u'R&amp;D<p>yo</p>')
 
 
--- a/hooks/workflow.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/hooks/workflow.py	Mon Sep 13 16:47:03 2010 +0200
@@ -135,7 +135,7 @@
             qname = role_name('to_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
             raise ValidationError(self.trinfo.eid, {'to_state': msg})
-        tostate = wftr.get_exit_point(forentity, trinfo['to_state'])
+        tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
         if tostate is not None:
             # reached an exit point
             msg = session._('exiting from subworkflow %s')
@@ -185,7 +185,7 @@
         entity = self.entity
         # first retreive entity to which the state change apply
         try:
-            foreid = entity['wf_info_for']
+            foreid = entity.cw_attr_cache['wf_info_for']
         except KeyError:
             qname = role_name('wf_info_for', 'subject')
             msg = session._('mandatory relation')
@@ -213,7 +213,7 @@
                      or not session.write_security)
         # no investigate the requested state change...
         try:
-            treid = entity['by_transition']
+            treid = entity.cw_attr_cache['by_transition']
         except KeyError:
             # no transition set, check user is a manager and destination state
             # is specified (and valid)
@@ -221,7 +221,7 @@
                 qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
                 raise ValidationError(entity.eid, {qname: msg})
-            deststateeid = entity.get('to_state')
+            deststateeid = entity.cw_attr_cache.get('to_state')
             if not deststateeid:
                 qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
@@ -247,8 +247,8 @@
                 if not tr.may_be_fired(foreid):
                     msg = session._("transition may not be fired")
                     raise ValidationError(entity.eid, {qname: msg})
-            if entity.get('to_state'):
-                deststateeid = entity['to_state']
+            deststateeid = entity.cw_attr_cache.get('to_state')
+            if deststateeid is not None:
                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
                     qname = role_name('by_transition', 'subject')
                     msg = session._("transition isn't allowed")
@@ -262,8 +262,8 @@
             else:
                 deststateeid = tr.destination(forentity).eid
         # everything is ok, add missing information on the trinfo entity
-        entity['from_state'] = fromstate.eid
-        entity['to_state'] = deststateeid
+        entity.cw_edited['from_state'] = fromstate.eid
+        entity.cw_edited['to_state'] = deststateeid
         nocheck = session.transaction_data.setdefault('skip-security', set())
         nocheck.add((entity.eid, 'from_state', fromstate.eid))
         nocheck.add((entity.eid, 'to_state', deststateeid))
@@ -278,11 +278,12 @@
 
     def __call__(self):
         trinfo = self.entity
-        _change_state(self._cw, trinfo['wf_info_for'],
-                      trinfo['from_state'], trinfo['to_state'])
-        forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
+        rcache = trinfo.cw_attr_cache
+        _change_state(self._cw, rcache['wf_info_for'], rcache['from_state'],
+                      rcache['to_state'])
+        forentity = self._cw.entity_from_eid(rcache['wf_info_for'])
         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
-        assert iworkflowable.current_state.eid == trinfo['to_state']
+        assert iworkflowable.current_state.eid == rcache['to_state']
         if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
             _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
 
--- a/i18n/en.po	Mon Sep 13 16:46:52 2010 +0200
+++ b/i18n/en.po	Mon Sep 13 16:47:03 2010 +0200
@@ -1124,52 +1124,52 @@
 msgid "boxes"
 msgstr ""
 
-msgid "boxes_bookmarks_box"
+msgid "ctxcomponents_bookmarks_box"
 msgstr "bookmarks box"
 
-msgid "boxes_bookmarks_box_description"
+msgid "ctxcomponents_bookmarks_box_description"
 msgstr "box listing the user's bookmarks"
 
-msgid "boxes_download_box"
+msgid "ctxcomponents_download_box"
 msgstr "download box"
 
-msgid "boxes_download_box_description"
-msgstr ""
-
-msgid "boxes_edit_box"
+msgid "ctxcomponents_download_box_description"
+msgstr ""
+
+msgid "ctxcomponents_edit_box"
 msgstr "actions box"
 
-msgid "boxes_edit_box_description"
+msgid "ctxcomponents_edit_box_description"
 msgstr "box listing the applicable actions on the displayed data"
 
-msgid "boxes_filter_box"
+msgid "ctxcomponents_filter_box"
 msgstr "filter"
 
-msgid "boxes_filter_box_description"
+msgid "ctxcomponents_filter_box_description"
 msgstr "box providing filter within current search results functionality"
 
-msgid "boxes_possible_views_box"
+msgid "ctxcomponents_possible_views_box"
 msgstr "possible views box"
 
-msgid "boxes_possible_views_box_description"
+msgid "ctxcomponents_possible_views_box_description"
 msgstr "box listing the possible views for the displayed data"
 
-msgid "boxes_rss"
+msgid "ctxcomponents_rss"
 msgstr "rss box"
 
-msgid "boxes_rss_description"
+msgid "ctxcomponents_rss_description"
 msgstr "RSS icon to get displayed data as a RSS thread"
 
-msgid "boxes_search_box"
+msgid "ctxcomponents_search_box"
 msgstr "search box"
 
-msgid "boxes_search_box_description"
+msgid "ctxcomponents_search_box_description"
 msgstr "search box"
 
-msgid "boxes_startup_views_box"
+msgid "ctxcomponents_startup_views_box"
 msgstr "startup views box"
 
-msgid "boxes_startup_views_box_description"
+msgid "ctxcomponents_startup_views_box_description"
 msgstr "box listing the possible start pages"
 
 msgid "bug report sent"
@@ -1445,41 +1445,41 @@
 msgid "content type"
 msgstr ""
 
-msgid "contentnavigation"
+msgid "ctxcomponents"
 msgstr "contextual components"
 
-msgid "contentnavigation_breadcrumbs"
+msgid "ctxcomponents_breadcrumbs"
 msgstr "breadcrumb"
 
-msgid "contentnavigation_breadcrumbs_description"
+msgid "ctxcomponents_breadcrumbs_description"
 msgstr "breadcrumbs bar that display a path locating the page in the site"
 
-msgid "contentnavigation_metadata"
+msgid "ctxcomponents_metadata"
 msgstr "entity's metadata"
 
-msgid "contentnavigation_metadata_description"
-msgstr ""
-
-msgid "contentnavigation_prevnext"
+msgid "ctxcomponents_metadata_description"
+msgstr ""
+
+msgid "ctxcomponents_prevnext"
 msgstr "previous / next entity"
 
-msgid "contentnavigation_prevnext_description"
+msgid "ctxcomponents_prevnext_description"
 msgstr ""
 "display link to go from one entity to another on entities implementing the "
 "\"previous/next\" interface."
 
-msgid "contentnavigation_seealso"
+msgid "ctxcomponents_seealso"
 msgstr "see also"
 
-msgid "contentnavigation_seealso_description"
+msgid "ctxcomponents_seealso_description"
 msgstr ""
 "section containing entities related by the \"see also\" relation on entities "
 "supporting it."
 
-msgid "contentnavigation_wfhistory"
+msgid "ctxcomponents_wfhistory"
 msgstr "workflow history"
 
-msgid "contentnavigation_wfhistory_description"
+msgid "ctxcomponents_wfhistory_description"
 msgstr "show the workflow's history."
 
 msgid "context"
--- a/i18n/es.po	Mon Sep 13 16:46:52 2010 +0200
+++ b/i18n/es.po	Mon Sep 13 16:47:03 2010 +0200
@@ -1170,54 +1170,54 @@
 msgid "boxes"
 msgstr "Cajas"
 
-msgid "boxes_bookmarks_box"
+msgid "ctxcomponents_bookmarks_box"
 msgstr "Caja de Favoritos"
 
-msgid "boxes_bookmarks_box_description"
+msgid "ctxcomponents_bookmarks_box_description"
 msgstr "Muestra y permite administrar los favoritos del usuario"
 
-msgid "boxes_download_box"
+msgid "ctxcomponents_download_box"
 msgstr "Configuración de caja de descargas"
 
-msgid "boxes_download_box_description"
+msgid "ctxcomponents_download_box_description"
 msgstr "Caja que contiene los elementos descargados"
 
-msgid "boxes_edit_box"
+msgid "ctxcomponents_edit_box"
 msgstr "Caja de Acciones"
 
-msgid "boxes_edit_box_description"
+msgid "ctxcomponents_edit_box_description"
 msgstr "Muestra las acciones posibles a ejecutar para los datos seleccionados"
 
-msgid "boxes_filter_box"
+msgid "ctxcomponents_filter_box"
 msgstr "Filtros"
 
-msgid "boxes_filter_box_description"
+msgid "ctxcomponents_filter_box_description"
 msgstr "Muestra los filtros aplicables a una búsqueda realizada"
 
-msgid "boxes_possible_views_box"
+msgid "ctxcomponents_possible_views_box"
 msgstr "Caja de Vistas Posibles"
 
-msgid "boxes_possible_views_box_description"
+msgid "ctxcomponents_possible_views_box_description"
 msgstr "Muestra las vistas posibles a aplicar a los datos seleccionados"
 
-msgid "boxes_rss"
+msgid "ctxcomponents_rss"
 msgstr "Ãcono RSS"
 
-msgid "boxes_rss_description"
+msgid "ctxcomponents_rss_description"
 msgstr "Muestra el ícono RSS para vistas RSS"
 
-msgid "boxes_search_box"
+msgid "ctxcomponents_search_box"
 msgstr "Caja de búsqueda"
 
-msgid "boxes_search_box_description"
+msgid "ctxcomponents_search_box_description"
 msgstr ""
 "Permite realizar una búsqueda simple para cualquier tipo de dato en la "
 "aplicación"
 
-msgid "boxes_startup_views_box"
+msgid "ctxcomponents_startup_views_box"
 msgstr "Caja Vistas de inicio"
 
-msgid "boxes_startup_views_box_description"
+msgid "ctxcomponents_startup_views_box_description"
 msgstr "Muestra las vistas de inicio de la aplicación"
 
 msgid "bug report sent"
@@ -1499,38 +1499,38 @@
 msgid "contentnavigation"
 msgstr "Componentes contextuales"
 
-msgid "contentnavigation_breadcrumbs"
+msgid "ctxcomponents_breadcrumbs"
 msgstr "Ruta de Navegación"
 
-msgid "contentnavigation_breadcrumbs_description"
+msgid "ctxcomponents_breadcrumbs_description"
 msgstr "Muestra la ruta que permite localizar la página actual en el Sistema"
 
-msgid "contentnavigation_metadata"
+msgid "ctxcomponents_metadata"
 msgstr "Metadatos de la Entidad"
 
-msgid "contentnavigation_metadata_description"
+msgid "ctxcomponents_metadata_description"
 msgstr ""
 
-msgid "contentnavigation_prevnext"
+msgid "ctxcomponents_prevnext"
 msgstr "Elemento anterior / siguiente"
 
-msgid "contentnavigation_prevnext_description"
+msgid "ctxcomponents_prevnext_description"
 msgstr ""
 "Muestra las ligas que permiten pasar de una entidad a otra en las entidades "
 "que implementan la interface \"anterior/siguiente\"."
 
-msgid "contentnavigation_seealso"
+msgid "ctxcomponents_seealso"
 msgstr "Vea también"
 
-msgid "contentnavigation_seealso_description"
+msgid "ctxcomponents_seealso_description"
 msgstr ""
 "sección que muestra las entidades relacionadas por la relación \"vea también"
 "\" , si la entidad soporta esta relación."
 
-msgid "contentnavigation_wfhistory"
+msgid "ctxcomponents_wfhistory"
 msgstr "Histórico del workflow."
 
-msgid "contentnavigation_wfhistory_description"
+msgid "ctxcomponents_wfhistory_description"
 msgstr ""
 "Sección que muestra el reporte histórico de las transiciones del workflow. "
 "Aplica solo en entidades con workflow."
--- a/i18n/fr.po	Mon Sep 13 16:46:52 2010 +0200
+++ b/i18n/fr.po	Mon Sep 13 16:47:03 2010 +0200
@@ -1169,53 +1169,53 @@
 msgid "boxes"
 msgstr "boîtes"
 
-msgid "boxes_bookmarks_box"
+msgid "ctxcomponents_bookmarks_box"
 msgstr "boîte signets"
 
-msgid "boxes_bookmarks_box_description"
+msgid "ctxcomponents_bookmarks_box_description"
 msgstr "boîte contenant les signets de l'utilisateur"
 
-msgid "boxes_download_box"
+msgid "ctxcomponents_download_box"
 msgstr "boîte de téléchargement"
 
-msgid "boxes_download_box_description"
+msgid "ctxcomponents_download_box_description"
 msgstr "boîte contenant un lien permettant de télécharger la ressource"
 
-msgid "boxes_edit_box"
+msgid "ctxcomponents_edit_box"
 msgstr "boîte d'actions"
 
-msgid "boxes_edit_box_description"
+msgid "ctxcomponents_edit_box_description"
 msgstr ""
 "boîte affichant les différentes actions possibles sur les données affichées"
 
-msgid "boxes_filter_box"
+msgid "ctxcomponents_filter_box"
 msgstr "filtrer"
 
-msgid "boxes_filter_box_description"
+msgid "ctxcomponents_filter_box_description"
 msgstr "boîte permettant de filtrer parmi les résultats d'une recherche"
 
-msgid "boxes_possible_views_box"
+msgid "ctxcomponents_possible_views_box"
 msgstr "boîte des vues possibles"
 
-msgid "boxes_possible_views_box_description"
+msgid "ctxcomponents_possible_views_box_description"
 msgstr "boîte affichant les vues possibles pour les données courantes"
 
-msgid "boxes_rss"
+msgid "ctxcomponents_rss"
 msgstr "icône RSS"
 
-msgid "boxes_rss_description"
+msgid "ctxcomponents_rss_description"
 msgstr "l'icône RSS permettant de récupérer la vue RSS des données affichées"
 
-msgid "boxes_search_box"
+msgid "ctxcomponents_search_box"
 msgstr "boîte de recherche"
 
-msgid "boxes_search_box_description"
+msgid "ctxcomponents_search_box_description"
 msgstr "boîte avec un champ de recherche simple"
 
-msgid "boxes_startup_views_box"
+msgid "ctxcomponents_startup_views_box"
 msgstr "boîte des vues de départs"
 
-msgid "boxes_startup_views_box_description"
+msgid "ctxcomponents_startup_views_box_description"
 msgstr "boîte affichant les vues de départs de l'application"
 
 msgid "bug report sent"
@@ -1496,42 +1496,42 @@
 msgid "content type"
 msgstr "type MIME"
 
-msgid "contentnavigation"
+msgid "ctxcomponents"
 msgstr "composants contextuels"
 
-msgid "contentnavigation_breadcrumbs"
+msgid "ctxcomponents_breadcrumbs"
 msgstr "fil d'ariane"
 
-msgid "contentnavigation_breadcrumbs_description"
+msgid "ctxcomponents_breadcrumbs_description"
 msgstr ""
 "affiche un chemin permettant de localiser la page courante dans le site"
 
-msgid "contentnavigation_metadata"
+msgid "ctxcomponents_metadata"
 msgstr "méta-données de l'entité"
 
-msgid "contentnavigation_metadata_description"
+msgid "ctxcomponents_metadata_description"
 msgstr ""
 
-msgid "contentnavigation_prevnext"
+msgid "ctxcomponents_prevnext"
 msgstr "élément précedent / suivant"
 
-msgid "contentnavigation_prevnext_description"
+msgid "ctxcomponents_prevnext_description"
 msgstr ""
 "affiche des liens permettant de passer d'une entité à une autre sur les "
 "entités implémentant l'interface \"précédent/suivant\"."
 
-msgid "contentnavigation_seealso"
+msgid "ctxcomponents_seealso"
 msgstr "voir aussi"
 
-msgid "contentnavigation_seealso_description"
+msgid "ctxcomponents_seealso_description"
 msgstr ""
 "section affichant les entités liées par la relation \"voir aussi\" si "
 "l'entité supporte cette relation."
 
-msgid "contentnavigation_wfhistory"
+msgid "ctxcomponents_wfhistory"
 msgstr "historique du workflow."
 
-msgid "contentnavigation_wfhistory_description"
+msgid "ctxcomponents_wfhistory_description"
 msgstr ""
 "section affichant l'historique du workflow pour les entités ayant un "
 "workflow."
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.10.0_Any.py	Mon Sep 13 16:47:03 2010 +0200
@@ -0,0 +1,5 @@
+# rename cwprops for boxes/contentnavigation
+for x in rql('Any X,XK WHERE X pkey XK, '
+             'X pkey ~= "boxes.%s" OR '
+             'X pkey ~= "contentnavigation.%s"').entities():
+    x.set_attributes(pkey=u'ctxcomponents.' + x.pkey.split('.',1))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.10.0_common.py	Mon Sep 13 16:47:03 2010 +0200
@@ -0,0 +1,1 @@
+option_group_changed('cleanup-session-time', 'web', 'main')
--- a/req.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/req.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Base class for request/session
+"""Base class for request/session"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
--- a/rset.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/rset.py	Mon Sep 13 16:47:03 2010 +0200
@@ -484,7 +484,7 @@
                         if attr == 'eid':
                             entity.eid = rowvalues[outerselidx]
                         else:
-                            entity[attr] = rowvalues[outerselidx]
+                            entity.cw_attr_cache[attr] = rowvalues[outerselidx]
                         continue
                 else:
                     rschema = eschema.objrels[attr]
--- a/selectors.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/selectors.py	Mon Sep 13 16:47:03 2010 +0200
@@ -60,9 +60,9 @@
 
 .. sourcecode:: python
 
-  class RSSIconBox(ExtResourcesBoxTemplate):
+  class RSSIconBox(box.Box):
     ''' just display the RSS icon on uniform result set '''
-    __select__ = ExtResourcesBoxTemplate.__select__ & non_final_entity()
+    __select__ = box.Box.__select__ & non_final_entity()
 
 It takes into account:
 
@@ -1203,6 +1203,7 @@
 
 # Web request selectors ########################################################
 
+# XXX deprecate
 @objectify_selector
 @lltrace
 def primary_view(cls, req, view=None, **kwargs):
@@ -1220,6 +1221,15 @@
     return 1
 
 
+@objectify_selector
+@lltrace
+def contextual(cls, req, view=None, **kwargs):
+    """Return 1 if view's contextual property is true"""
+    if view is not None and view.contextual:
+        return 1
+    return 0
+
+
 class match_view(ExpectedValueSelector):
     """Return 1 if a view is specified an as its registry id is in one of the
     expected view id given to the initializer.
@@ -1231,6 +1241,19 @@
         return 1
 
 
+class match_context(ExpectedValueSelector):
+
+    @lltrace
+    def __call__(self, cls, req, context=None, **kwargs):
+        try:
+            if not context in self.expected:
+                return 0
+        except AttributeError:
+            return 1 # class doesn't care about search state, accept it
+        return 1
+
+
+# XXX deprecate
 @objectify_selector
 @lltrace
 def match_context_prop(cls, req, context=None, **kwargs):
@@ -1251,8 +1274,6 @@
         return 1
     propval = req.property_value('%s.%s.context' % (cls.__registry__,
                                                     cls.__regid__))
-    if not propval:
-        propval = cls.context
     if propval and context != propval:
         return 0
     return 1
--- a/server/hook.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/hook.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,37 +15,228 @@
 #
 # 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 management
+"""
+Generalities
+------------
+
+Paraphrasing the `emacs`_ documentation, let us say that hooks are an important
+mechanism for customizing an application. A hook is basically a list of
+functions to be called on some well-defined occasion (this is called `running
+the hook`).
+
+.. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
+
+Hooks
+~~~~~
+
+In |cubicweb|, hooks are subclasses of the :class:`~cubicweb.server.hook.Hook`
+class. They are selected over a set of pre-defined `events` (and possibly more
+conditions, hooks being selectable appobjects like views and components).  They
+should implement a :meth:`~cubicweb.server.hook.Hook.__call__` method that will
+be called when the hook is triggered.
 
-This module defined the `Hook` class and registry and a set of abstract classes
-for operations.
+There are two families of events: data events (before / after any individual
+update of an entity / or a relation in the repository) and server events (such
+as server startup or shutdown).  In a typical application, most of the hooks are
+defined over data events.
+
+Also, some :class:`~cubicweb.server.hook.Operation` may be registered by hooks,
+which will be fired when the transaction is commited or rollbacked.
+
+The purpose of data event hooks is usually to complement the data model as
+defined in the schema, which is static by nature and only provide a restricted
+builtin set of dynamic constraints, with dynamic or value driven behaviours.
+For instance they can serve the following purposes:
+
+* enforcing constraints that the static schema cannot express (spanning several
+  entities/relations, exotic value ranges and cardinalities, etc.)
+
+* implement computed attributes
+
+It is functionally equivalent to a `database trigger`_, except that database
+triggers definition languages are not standardized, hence not portable (for
+instance, PL/SQL works with Oracle and PostgreSQL but not SqlServer nor Sqlite).
+
+.. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
 
 
-Hooks are called before / after any individual update of entities / relations
-in the repository and on special events such as server startup or shutdown.
+Operations
+~~~~~~~~~~
+
+Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class
+that may be created by hooks and scheduled to happen just before (or after) the
+`precommit`, `postcommit` or `rollback` event. Hooks are being fired immediately
+on data operations, and it is sometime necessary to delay the actual work down
+to a time where all other hooks have run. Also while the order of execution of
+hooks is data dependant (and thus hard to predict), it is possible to force an
+order on operations.
+
+Operations may be used to:
+
+* implements a validation check which needs that all relations be already set on
+  an entity
+
+* process various side effects associated with a transaction such as filesystem
+  udpates, mail notifications, etc.
 
 
-Operations may be registered by hooks during a transaction, which will  be
-fired when the pool is commited or rollbacked.
+Events
+------
+
+Hooks are mostly defined and used to handle `dataflow`_ operations. It
+means as data gets in (entities added, updated, relations set or
+unset), specific events are issued and the Hooks matching these events
+are called.
+
+You can get the event that triggered a hook by accessing its :attr:event
+attribute.
+
+.. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow
 
 
-Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,
-after_update_entity, before_delete_entity, after_delete_entity) all have an
-`entity` attribute
+Entity modification related events
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When called for one of these events, hook will have an `entity` attribute
+containing the entity instance.
+
+* 'before_add_entity', 'before_update_entity':
+
+  - on those events, you can check what attributes of the entity are modified in
+    `entity.cw_edited` (by definition the database is not yet updated in a before
+    event)
+
+  - you are allowed to further modify the entity before database operations,
+    using the dictionary notation. By doing this, you'll avoid the need for a
+    whole new rql query processing, the only difference is that the underlying
+    backend query (eg usually sql) will contains the additional data. For
+    example:
+
+    .. sourcecode:: python
+
+       self.entity.set_attributes(age=42)
+
+    will set the `age` attribute of the entity to 42. But to do so, it will
+    generate a rql query that will have to be processed, then trigger some
+    hooks, and so one (potentially leading to infinite hook loops or such
+    awkward situations..) You can avoid this by doing the modification that way:
+
+    .. sourcecode:: python
+
+       self.entity.cw_edited['age'] = 42
+
+    Here the attribute will simply be edited in the same query that the
+    one that triggered the hook.
 
-Relation (eg before_add_relation, after_add_relation, before_delete_relation,
-after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
+    Similarly, removing an attribute from `cw_edited` will cancel its
+    modification.
+
+  - on 'before_update_entity' event, you can access to old and new values in
+    this hook, by using `entity.cw_edited.oldnewvalue(attr)`
+
+
+* 'after_add_entity', 'after_update_entity'
+
+  - on those events, you can still check what attributes of the entity are
+    modified in `entity.cw_edited` but you can't get anymore the old value, nor
+    modify it.
+
+* 'before_delete_entity', 'after_delete_entity'
+
+  - on those events, the entity has no `cw_edited` set.
+
+
+Relation modification related events
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When called for one of these events, hook will have `eidfrom`, `rtype`, `eidto`
+attributes containing respectivly the eid of the subject entity, the relation
+type and the eid of the object entity.
+
+* 'before_add_relation', 'before_delete_relation'
+
+  - on those events, you can still get original relation by issuing a rql query
+
+* 'after_add_relation', 'after_delete_relation'
+
+This is an occasion to remind us that relations support the add / delete
+operation, but no update.
+
+
+Non data events
+~~~~~~~~~~~~~~~
 
-Server start/maintenance/stop hooks (eg server_startup, server_maintenance,
-server_shutdown) have a `repo` attribute, but *their `_cw` attribute is None*.
-The `server_startup` is called on regular startup, while `server_maintenance`
-is called on cubicweb-ctl upgrade or shell commands. `server_shutdown` is
-called anyway.
+Hooks called on server start/maintenance/stop event (eg 'server_startup',
+'server_maintenance', 'server_shutdown') have a `repo` attribute, but *their
+`_cw` attribute is None*.  The `server_startup` is called on regular startup,
+while `server_maintenance` is called on cubicweb-ctl upgrade or shell
+commands. `server_shutdown` is called anyway.
+
+Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have
+a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*.
+
+Hooks called on session event (eg 'session_open', 'session_close') have no
+special attribute.
+
+
+API
+---
+
+Hooks control
+~~~~~~~~~~~~~
+
+It is sometimes convenient to explicitly enable or disable some hooks. For
+instance if you want to disable some integrity checking hook.  This can be
+controlled more finely through the `category` class attribute, which is a string
+giving a category name.  One can then uses the
+:class:`~cubicweb.server.session.hooks_control` context manager to explicitly
+enable or disable some categories.
+
+.. autoclass:: cubicweb.server.session.hooks_control
+
+
+The existing categories are:
+
+* ``security``, security checking hooks
+
+* ``worfklow``, workflow handling hooks
 
-Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
-`timestamp` attributes, but *their `_cw` attribute is None*.
+* ``metadata``, hooks setting meta-data on newly created entities
+
+* ``notification``, email notification hooks
+
+* ``integrity``, data integrity checking hooks
+
+* ``activeintegrity``, data integrity consistency hooks, that you should *never*
+  want to disable
+
+* ``syncsession``, hooks synchronizing existing sessions
+
+* ``syncschema``, hooks synchronizing instance schema (including the physical database)
+
+* ``email``, email address handling hooks
+
+* ``bookmark``, bookmark entities handling hooks
+
 
-Session hooks (eg session_open, session_close) have no special attribute.
+Nothing precludes one to invent new categories and use the
+:class:`~cubicweb.server.session.hooks_control` context manager to filter them
+in or out.
+
+
+Hooks specific selector
+~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: cubicweb.server.hook.match_rtype
+.. autoclass:: cubicweb.server.hook.match_rtype_sets
+
+
+Hooks and operations classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: cubicweb.server.hook.Hook
+.. autoclass:: cubicweb.server.hook.Operation
+.. autoclass:: cubicweb.server.hook.LateOperation
+.. autofunction:: cubicweb.server.hook.set_operation
+
 """
 
 from __future__ import with_statement
@@ -61,6 +252,7 @@
 from logilab.common.logging_ext import set_log_methods
 
 from cubicweb import RegistryNotFound
+from cubicweb.vregistry import classid
 from cubicweb.cwvreg import CWRegistry, VRegistry
 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
                                 is_instance)
@@ -83,7 +275,7 @@
         for appobjects in self.values():
             for cls in appobjects:
                 if not cls.enabled:
-                    warn('[3.6] %s: enabled is deprecated' % cls)
+                    warn('[3.6] %s: enabled is deprecated' % classid(cls))
                     self.unregister(cls)
 
     def register(self, obj, **kwargs):
@@ -119,21 +311,9 @@
 for event in ALL_HOOKS:
     VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
 
-_MARKER = object()
+@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)')
 def entity_oldnewvalue(entity, attr):
-    """returns the couple (old attr value, new attr value)
-
-    NOTE: will only work in a before_update_entity hook
-    """
-    # get new value and remove from local dict to force a db query to
-    # fetch old value
-    newvalue = entity.pop(attr, _MARKER)
-    oldvalue = getattr(entity, attr)
-    if newvalue is not _MARKER:
-        entity[attr] = newvalue
-    else:
-        newvalue = oldvalue
-    return oldvalue, newvalue
+    return entity.cw_edited.oldnewvalue(attr)
 
 
 # some hook specific selectors #################################################
@@ -203,6 +383,29 @@
 # base class for hook ##########################################################
 
 class Hook(AppObject):
+    """Base class for hook.
+
+    Hooks being appobjects like views, they have a `__regid__` and a `__select__`
+    class attribute. Like all appobjects, hooks have the `self._cw` attribute which
+    represents the current session. In entity hooks, a `self.entity` attribute is
+    also present.
+
+    The `events` tuple is used by the base class selector to dispatch the hook
+    on the right events. It is possible to dispatch on multiple events at once
+    if needed (though take care as hook attribute may vary as described above).
+
+    .. Note::
+
+      Do not forget to extend the base class selectors as in ::
+
+      .. sourcecode:: python
+
+          class MyHook(Hook):
+            __regid__ = 'whatever'
+            __select__ = Hook.__select__ & implements('Person')
+
+      else your hooks will be called madly, whatever the event.
+    """
     __select__ = enabled_category()
     # set this in derivated classes
     events = None
@@ -231,16 +434,16 @@
 
     @classproperty
     def __regid__(cls):
-        warn('[3.6] %s.%s: please specify an id for your hook'
-             % (cls.__module__, cls.__name__), DeprecationWarning)
+        warn('[3.6] %s: please specify an id for your hook' % classid(cls),
+             DeprecationWarning)
         return str(id(cls))
 
     @classmethod
     def __registered__(cls, reg):
         super(Hook, cls).__registered__(reg)
         if getattr(cls, 'accepts', None):
-            warn('[3.6] %s.%s: accepts is deprecated, define proper __select__'
-                 % (cls.__module__, cls.__name__), DeprecationWarning)
+            warn('[3.6] %s: accepts is deprecated, define proper __select__'
+                 % classid(cls), DeprecationWarning)
             rtypes = []
             for ertype in cls.accepts:
                 if ertype.islower():
@@ -261,9 +464,8 @@
 
     def __call__(self):
         if hasattr(self, 'call'):
-            cls = self.__class__
-            warn('[3.6] %s.%s: call is deprecated, implement __call__'
-                 % (cls.__module__, cls.__name__), DeprecationWarning)
+            warn('[3.6] %s: call is deprecated, implement __call__'
+                 % classid(self.__class__), DeprecationWarning)
             if self.event.endswith('_relation'):
                 self.call(self._cw, self.eidfrom, self.rtype, self.eidto)
             elif 'delete' in self.event:
@@ -365,40 +567,53 @@
 # abstract classes for operation ###############################################
 
 class Operation(object):
-    """an operation is triggered on connections pool events related to
+    """Base class for operations.
+
+    Operation may be instantiated in the hooks' `__call__` method. It always
+    takes a session object as first argument (accessible as `.session` from the
+    operation instance), and optionally all keyword arguments needed by the
+    operation. These keyword arguments will be accessible as attributes from the
+    operation instance.
+
+    An operation is triggered on connections pool events related to
     commit / rollback transations. Possible events are:
 
-    precommit:
-      the pool is preparing to commit. You shouldn't do anything which
-      has to be reverted if the commit fails at this point, but you can freely
-      do any heavy computation or raise an exception if the commit can't go.
-      You can add some new operations during this phase but their precommit
-      event won't be triggered
+    * 'precommit':
 
-    commit:
-      the pool is preparing to commit. You should avoid to do to expensive
-      stuff or something that may cause an exception in this event
+      the transaction is being prepared for commit. You can freely do any heavy
+      computation, raise an exception if the commit can't go. or even add some
+      new operations during this phase. If you do anything which has to be
+      reverted if the commit fails afterwards (eg altering the file system for
+      instance), you'll have to support the 'revertprecommit' event to revert
+      things by yourself
 
-    revertcommit:
-      if an operation failed while commited, this event is triggered for
-      all operations which had their commit event already to let them
-      revert things (including the operation which made fail the commit)
+    * 'revertprecommit':
+
+      if an operation failed while being pre-commited, this event is triggered
+      for all operations which had their 'precommit' event already fired to let
+      them revert things (including the operation which made the commit fail)
+
+    * 'rollback':
 
-    rollback:
       the transaction has been either rollbacked either:
+
        * intentionaly
-       * a precommit event failed, all operations are rollbacked
-       * a commit event failed, all operations which are not been triggered for
-         commit are rollbacked
+       * a 'precommit' event failed, in which case all operations are rollbacked
+         once 'revertprecommit'' has been called
+
+    * 'postcommit':
 
-    postcommit:
-      The transaction is over. All the ORM entities are
-      invalid. If you need to work on the database, you need to stard
-      a new transaction, for instance using a new internal_session,
-      which you will need to commit (and close!).
+      the transaction is over. All the ORM entities accessed by the earlier
+      transaction are invalid. If you need to work on the database, you need to
+      start a new transaction, for instance using a new internal session, which
+      you will need to commit (and close!).
 
-    order of operations may be important, and is controlled according to
-    the insert_index's method output
+    For an operation to support an event, one has to implement the `<event
+    name>_event` method with no arguments.
+
+    Notice order of operations may be important, and is controlled according to
+    the insert_index's method output (whose implementation vary according to the
+    base hook class used).
     """
 
     def __init__(self, session, **kwargs):
@@ -428,6 +643,10 @@
 
     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'
+                 % classid(self.__class__), DeprecationWarning)
+            self.commit_event()
         getattr(self, event)()
 
     def precommit_event(self):
@@ -440,16 +659,6 @@
         been all considered if it's this operation which failed
         """
 
-    def commit_event(self):
-        """the observed connections pool is commiting"""
-
-    def revertcommit_event(self):
-        """an error went when commiting this operation or a later one
-
-        should revert commit's changes but take care, they may have not
-        been all considered if it's this operation which failed
-        """
-
     def rollback_event(self):
         """the observed connections pool has been rollbacked
 
@@ -486,20 +695,55 @@
     {set: set.add, list: list.append}[container.__class__](container, value)
 
 def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs):
-    """Search for session.transaction_data[`datakey`] (expected to be a set):
+    """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:
+
+    * the `session` object
 
-    * if found, simply append `value`
+    * `datakey`, a specially forged key that will be used as key in
+      session.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)
 
-    * else, initialize it to containercls([`value`]) and instantiate the given
-      `opcls` operation class with additional keyword arguments. `containercls`
-      is a set by default. Give `list` if you want to keep arrival ordering.
+    * `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
 
-    You should use this instead of creating on operation for each `value`,
-    since handling operations becomes coslty on massive data import.
+           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 session.transaction_data[`datakey`] (expected to be a set):
+        # if found, simply append `value`
         _container_add(session.transaction_data[datakey], value)
     except KeyError:
+        # else, initialize it to containercls([`value`]) and instantiate the given
+        # `opcls` operation class with additional keyword arguments
         opcls(session, **opkwargs)
         session.transaction_data[datakey] = containercls()
         _container_add(session.transaction_data[datakey], value)
@@ -524,8 +768,12 @@
         return -(i + 1)
 
 
-class SingleOperation(Operation):
-    """special operation which should be called once"""
+
+class SingleLastOperation(Operation):
+    """special operation which should be called once and after all other
+    operations
+    """
+
     def register(self, session):
         """override register to handle cases where this operation has already
         been added
@@ -546,11 +794,6 @@
                 return -(i+1)
         return None
 
-
-class SingleLastOperation(SingleOperation):
-    """special operation which should be called once and after all other
-    operations
-    """
     def insert_index(self):
         return None
 
@@ -572,7 +815,7 @@
         if previous:
             self.to_send = previous.to_send + self.to_send
 
-    def commit_event(self):
+    def postcommit_event(self):
         self.session.repo.threaded_task(self.sendmails)
 
     def sendmails(self):
@@ -612,7 +855,7 @@
     type/source cache eids of entities deleted in that transaction.
     """
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been rollbacked,
         remove inserted eid from repository type/source cache
         """
--- a/server/pool.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/pool.py	Mon Sep 13 16:47:03 2010 +0200
@@ -144,11 +144,9 @@
         self._cursors.pop(source.uri, None)
 
 
-from cubicweb.server.hook import (Operation, LateOperation, SingleOperation,
-                                  SingleLastOperation)
+from cubicweb.server.hook import Operation, LateOperation, SingleLastOperation
 from logilab.common.deprecation import class_moved, class_renamed
 Operation = class_moved(Operation)
 PreCommitOperation = class_renamed('PreCommitOperation', Operation)
 LateOperation = class_moved(LateOperation)
-SingleOperation = class_moved(SingleOperation)
 SingleLastOperation = class_moved(SingleLastOperation)
--- a/server/querier.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/querier.py	Mon Sep 13 16:47:03 2010 +0200
@@ -38,7 +38,7 @@
 
 from cubicweb.server.utils import cleanup_solutions
 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
-from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
+from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction, EditedEntity
 from cubicweb.server.session import security_enabled
 
 def empty_rset(rql, args, rqlst=None):
@@ -450,7 +450,7 @@
         # save originaly selected variable, we may modify this
         # dictionary for substitution (query parameters)
         self.selected = rqlst.selection
-        # list of new or updated entities definition (utils.Entity)
+        # list of rows of entities definition (ssplanner.EditedEntity)
         self.e_defs = [[]]
         # list of new relation definition (3-uple (from_eid, r_type, to_eid)
         self.r_defs = set()
@@ -461,7 +461,6 @@
 
     def add_entity_def(self, edef):
         """add an entity definition to build"""
-        edef.querier_pending_relations = {}
         self.e_defs[-1].append(edef)
 
     def add_relation_def(self, rdef):
@@ -493,8 +492,9 @@
             self.e_defs[i][colidx] = edefs[0]
             samplerow = self.e_defs[i]
             for edef_ in edefs[1:]:
-                row = samplerow[:]
-                row[colidx] = edef_
+                row = [ed.clone() for i, ed in enumerate(samplerow)
+                       if i != colidx]
+                row.insert(colidx, edef_)
                 self.e_defs.append(row)
         # now, see if this entity def is referenced as subject in some relation
         # definition
@@ -560,15 +560,16 @@
             if isinstance(subj, basestring):
                 subj = typed_eid(subj)
             elif not isinstance(subj, (int, long)):
-                subj = subj.eid
+                subj = subj.entity.eid
             if isinstance(obj, basestring):
                 obj = typed_eid(obj)
             elif not isinstance(obj, (int, long)):
-                obj = obj.eid
+                obj = obj.entity.eid
             if repo.schema.rschema(rtype).inlined:
                 entity = session.entity_from_eid(subj)
-                entity[rtype] = obj
-                repo.glob_update_entity(session, entity, set((rtype,)))
+                edited = EditedEntity(entity)
+                edited.edited_attribute(rtype, obj)
+                repo.glob_update_entity(session, edited)
             else:
                 repo.glob_add_relation(session, subj, rtype, obj)
 
--- a/server/repository.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/repository.py	Mon Sep 13 16:47:03 2010 +0200
@@ -55,7 +55,7 @@
 from cubicweb.server import utils, hook, pool, querier, sources
 from cubicweb.server.session import Session, InternalSession, InternalManager, \
      security_enabled
-_ = unicode
+from cubicweb.server.ssplanner import EditedEntity
 
 def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
     """delete existing relation when adding a new one if card is 1 or ?
@@ -270,7 +270,10 @@
             # call instance level initialisation hooks
             self.hm.call_hooks('server_startup', repo=self)
             # register a task to cleanup expired session
-            self.looping_task(self.config['session-time']/3., self.clean_sessions)
+            self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24
+            assert self.cleanup_session_time > 0
+            cleanup_session_interval = min(60*60, self.cleanup_session_time / 3)
+            self.looping_task(cleanup_session_interval, self.clean_sessions)
         assert isinstance(self._looping_tasks, list), 'already started'
         for i, (interval, func, args) in enumerate(self._looping_tasks):
             self._looping_tasks[i] = task = utils.LoopTask(interval, func, args)
@@ -533,8 +536,7 @@
                 password = password.encode('UTF8')
             kwargs['login'] = login
             kwargs['upassword'] = password
-            user.update(kwargs)
-            self.glob_add_entity(session, user)
+            self.glob_add_entity(session, EditedEntity(user, **kwargs))
             session.execute('SET X in_group G WHERE X eid %(x)s, G name "users"',
                             {'x': user.eid})
             if email or '@' in login:
@@ -625,24 +627,32 @@
             session.reset_pool()
 
     def check_session(self, sessionid):
-        """raise `BadConnectionId` if the connection is no more valid"""
-        self._get_session(sessionid, setpool=False)
+        """raise `BadConnectionId` if the connection is no more valid, else
+        return its latest activity timestamp.
+        """
+        return self._get_session(sessionid, setpool=False).timestamp
+
+    def get_shared_data(self, sessionid, key, default=None, pop=False, txdata=False):
+        """return value associated to key in the session's data dictionary or
+        session's transaction's data if `txdata` is true.
 
-    def get_shared_data(self, sessionid, key, default=None, pop=False):
-        """return the session's data dictionary"""
+        If pop is True, value will be removed from the dictionnary.
+
+        If key isn't defined in the dictionnary, value specified by the
+        `default` argument will be returned.
+        """
         session = self._get_session(sessionid, setpool=False)
-        return session.get_shared_data(key, default, pop)
+        return session.get_shared_data(key, default, pop, txdata)
 
-    def set_shared_data(self, sessionid, key, value, querydata=False):
+    def set_shared_data(self, sessionid, key, value, txdata=False):
         """set value associated to `key` in shared data
 
-        if `querydata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction, and won't be available through the connexion, only on the
-        repository side.
+        if `txdata` is true, the value will be added to the repository session's
+        transaction's data which are cleared on commit/rollback of the current
+        transaction.
         """
         session = self._get_session(sessionid, setpool=False)
-        session.set_shared_data(key, value, querydata)
+        session.set_shared_data(key, value, txdata)
 
     def commit(self, sessionid, txid=None):
         """commit transaction for the session with the given id"""
@@ -774,7 +784,7 @@
         """close sessions not used since an amount of time specified in the
         configuration
         """
-        mintime = time() - self.config['session-time']
+        mintime = time() - self.cleanup_session_time
         self.debug('cleaning session unused since %s',
                    strftime('%T', localtime(mintime)))
         nbclosed = 0
@@ -929,7 +939,6 @@
             self._extid_cache[cachekey] = eid
             self._type_source_cache[eid] = (etype, source.uri, extid)
             entity = source.before_entity_insertion(session, extid, etype, eid)
-            entity.edited_attributes = set(entity.cw_attr_cache)
             if source.should_call_hooks:
                 self.hm.call_hooks('before_add_entity', session, entity=entity)
             # XXX call add_info with complete=False ?
@@ -1032,15 +1041,16 @@
         self._type_source_cache[entity.eid] = (entity.__regid__, suri, extid)
         return extid
 
-    def glob_add_entity(self, session, entity):
+    def glob_add_entity(self, session, edited):
         """add an entity to the repository
 
         the entity eid should originaly be None and a unique eid is assigned to
         the entity instance
         """
-        # init edited_attributes before calling before_add_entity hooks
+        entity = edited.entity
         entity._cw_is_saved = False # entity has an eid but is not yet saved
-        entity.edited_attributes = set(entity.cw_attr_cache) # XXX cw_edited_attributes
+        # init edited_attributes before calling before_add_entity hooks
+        entity.cw_edited = edited
         eschema = entity.e_schema
         source = self.locate_etype_source(entity.__regid__)
         # allocate an eid to the entity before calling hooks
@@ -1052,33 +1062,30 @@
         relations = []
         if source.should_call_hooks:
             self.hm.call_hooks('before_add_entity', session, entity=entity)
-        # XXX use entity.keys here since edited_attributes is not updated for
-        # inline relations XXX not true, right? (see edited_attributes
-        # affectation above)
-        for attr in entity.cw_attr_cache.iterkeys():
+        for attr in edited.iterkeys():
             rschema = eschema.subjrels[attr]
             if not rschema.final: # inlined relation
-                relations.append((attr, entity[attr]))
-        entity._cw_set_defaults()
+                relations.append((attr, edited[attr]))
+        edited.set_defaults()
         if session.is_hook_category_activated('integrity'):
-            entity._cw_check(creation=True)
+            edited.check(creation=True)
         try:
             source.add_entity(session, entity)
         except UniqueTogetherError, exc:
             etype, rtypes = exc.args
             problems = {}
             for col in rtypes:
-                problems[col] = _('violates unique_together constraints (%s)') % (','.join(rtypes))
+                problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes))
             raise ValidationError(entity.eid, problems)
         self.add_info(session, entity, source, extid, complete=False)
-        entity._cw_is_saved = True # entity has an eid and is saved
+        edited.saved = True
         # prefill entity relation caches
         for rschema in eschema.subject_relations():
             rtype = str(rschema)
             if rtype in schema.VIRTUAL_RTYPES:
                 continue
             if rschema.final:
-                entity.setdefault(rtype, None)
+                entity.cw_attr_cache.setdefault(rtype, None)
             else:
                 entity.cw_set_relation_cache(rtype, 'subject',
                                              session.empty_rset())
@@ -1102,23 +1109,24 @@
                                     eidfrom=entity.eid, rtype=attr, eidto=value)
         return entity.eid
 
-    def glob_update_entity(self, session, entity, edited_attributes):
+    def glob_update_entity(self, session, edited):
         """replace an entity in the repository
         the type and the eid of an entity must not be changed
         """
+        entity = edited.entity
         if server.DEBUG & server.DBG_REPO:
             print 'UPDATE entity', entity.__regid__, entity.eid, \
-                  entity.cw_attr_cache, edited_attributes
+                  entity.cw_attr_cache, edited
         hm = self.hm
         eschema = entity.e_schema
         session.set_entity_cache(entity)
-        orig_edited_attributes = getattr(entity, 'edited_attributes', None)
-        entity.edited_attributes = edited_attributes
+        orig_edited = getattr(entity, 'cw_edited', None)
+        entity.cw_edited = edited
         try:
             only_inline_rels, need_fti_update = True, False
             relations = []
             source = self.source_from_eid(entity.eid, session)
-            for attr in list(edited_attributes):
+            for attr in list(edited):
                 if attr == 'eid':
                     continue
                 rschema = eschema.subjrels[attr]
@@ -1131,13 +1139,13 @@
                     previous_value = entity.related(attr) or None
                     if previous_value is not None:
                         previous_value = previous_value[0][0] # got a result set
-                        if previous_value == entity[attr]:
+                        if previous_value == entity.cw_attr_cache[attr]:
                             previous_value = None
                         elif source.should_call_hooks:
                             hm.call_hooks('before_delete_relation', session,
                                           eidfrom=entity.eid, rtype=attr,
                                           eidto=previous_value)
-                    relations.append((attr, entity[attr], previous_value))
+                    relations.append((attr, edited[attr], previous_value))
             if source.should_call_hooks:
                 # call hooks for inlined relations
                 for attr, value, _t in relations:
@@ -1146,16 +1154,16 @@
                 if not only_inline_rels:
                     hm.call_hooks('before_update_entity', session, entity=entity)
             if session.is_hook_category_activated('integrity'):
-                entity._cw_check()
+                edited.check()
             try:
                 source.update_entity(session, entity)
+                edited.saved = True
             except UniqueTogetherError, exc:
                 etype, rtypes = exc.args
                 problems = {}
                 for col in rtypes:
-                    problems[col] = _('violates unique_together constraints (%s)') % (','.join(rtypes))
+                    problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes))
                 raise ValidationError(entity.eid, problems)
-
             self.system_source.update_info(session, entity, need_fti_update)
             if source.should_call_hooks:
                 if not only_inline_rels:
@@ -1177,8 +1185,8 @@
                     hm.call_hooks('after_add_relation', session,
                                   eidfrom=entity.eid, rtype=attr, eidto=value)
         finally:
-            if orig_edited_attributes is not None:
-                entity.edited_attributes = orig_edited_attributes
+            if orig_edited is not None:
+                entity.cw_edited = orig_edited
 
     def glob_delete_entity(self, session, eid):
         """delete an entity and all related entities from the repository"""
--- a/server/serverconfig.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/serverconfig.py	Mon Sep 13 16:47:03 2010 +0200
@@ -120,10 +120,16 @@
 the repository rather than the user running the command',
           'group': 'main', 'level': (CubicWebConfiguration.mode == 'installed') and 0 or 1,
           }),
-        ('session-time',
+        ('cleanup-session-time',
          {'type' : 'time',
-          'default': '30min',
-          'help': 'session expiration time, default to 30 minutes',
+          'default': '24h',
+          'help': 'duration of inactivity after which a session '
+          'will be closed, to limit memory consumption (avoid sessions that '
+          'never expire and cause memory leak when http-session-time is 0, or '
+          'because of bad client that never closes their connection). '
+          'So notice that even if http-session-time is 0 and the user don\'t '
+          'close his browser, he will have to reauthenticate after this time '
+          'of inactivity. Default to 24h.',
           'group': 'main', 'level': 3,
           }),
         ('connections-pool-size',
--- a/server/session.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/session.py	Mon Sep 13 16:47:03 2010 +0200
@@ -64,6 +64,14 @@
 
     If mode is session.`HOOKS_ALLOW_ALL`, given hooks categories will
     be disabled.
+
+    .. sourcecode:: python
+
+       with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, 'integrity'):
+           # ... do stuff with all but 'integrity' hooks activated
+
+       with hooks_control(self.session, self.session.HOOKS_DENY_ALL, 'integrity'):
+           # ... do stuff with none but 'integrity' hooks activated
     """
     def __init__(self, session, mode, *categories):
         self.session = session
@@ -618,16 +626,20 @@
 
     # shared data handling ###################################################
 
-    def get_shared_data(self, key, default=None, pop=False):
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
         """return value associated to `key` in session data"""
-        if pop:
-            return self.data.pop(key, default)
+        if txdata:
+            data = self.transaction_data
         else:
-            return self.data.get(key, default)
+            data = self.data
+        if pop:
+            return data.pop(key, default)
+        else:
+            return data.get(key, default)
 
-    def set_shared_data(self, key, value, querydata=False):
+    def set_shared_data(self, key, value, txdata=False):
         """set value associated to `key` in session data"""
-        if querydata:
+        if txdata:
             self.transaction_data[key] = value
         else:
             self.data[key] = value
@@ -735,51 +747,50 @@
         try:
             # by default, operations are executed with security turned off
             with security_enabled(self, False, False):
-                for trstate in ('precommit', 'commit'):
-                    processed = []
-                    self.commit_state = trstate
-                    try:
-                        while self.pending_operations:
-                            operation = self.pending_operations.pop(0)
-                            operation.processed = trstate
-                            processed.append(operation)
-                            operation.handle_event('%s_event' % trstate)
-                        self.pending_operations[:] = processed
-                        self.debug('%s session %s done', trstate, self.id)
-                    except:
-                        # if error on [pre]commit:
-                        #
-                        # * set .failed = True on the operation causing the failure
-                        # * call revert<event>_event on processed operations
-                        # * call rollback_event on *all* operations
-                        #
-                        # that seems more natural than not calling rollback_event
-                        # for processed operations, and allow generic rollback
-                        # instead of having to implements rollback, revertprecommit
-                        # and revertcommit, that will be enough in mont case.
-                        operation.failed = True
-                        for operation in reversed(processed):
-                            try:
-                                operation.handle_event('revert%s_event' % trstate)
-                            except:
-                                self.critical('error while reverting %sing', trstate,
-                                              exc_info=True)
-                        # XXX use slice notation since self.pending_operations is a
-                        # read-only property.
-                        self.pending_operations[:] = processed + self.pending_operations
-                        self.rollback(reset_pool)
-                        raise
+                processed = []
+                self.commit_state = 'precommit'
+                try:
+                    while self.pending_operations:
+                        operation = self.pending_operations.pop(0)
+                        operation.processed = 'precommit'
+                        processed.append(operation)
+                        operation.handle_event('precommit_event')
+                    self.pending_operations[:] = processed
+                    self.debug('precommit session %s done', self.id)
+                except:
+                    # if error on [pre]commit:
+                    #
+                    # * set .failed = True on the operation causing the failure
+                    # * call revert<event>_event on processed operations
+                    # * call rollback_event on *all* operations
+                    #
+                    # that seems more natural than not calling rollback_event
+                    # for processed operations, and allow generic rollback
+                    # instead of having to implements rollback, revertprecommit
+                    # and revertcommit, that will be enough in mont case.
+                    operation.failed = True
+                    for operation in reversed(processed):
+                        try:
+                            operation.handle_event('revertprecommit_event')
+                        except:
+                            self.critical('error while reverting precommit',
+                                          exc_info=True)
+                    # XXX use slice notation since self.pending_operations is a
+                    # read-only property.
+                    self.pending_operations[:] = processed + self.pending_operations
+                    self.rollback(reset_pool)
+                    raise
                 self.pool.commit()
-                self.commit_state = trstate = 'postcommit'
+                self.commit_state = 'postcommit'
                 while self.pending_operations:
                     operation = self.pending_operations.pop(0)
-                    operation.processed = trstate
+                    operation.processed = 'postcommit'
                     try:
-                        operation.handle_event('%s_event' % trstate)
+                        operation.handle_event('postcommit_event')
                     except:
-                        self.critical('error while %sing', trstate,
+                        self.critical('error while postcommit',
                                       exc_info=sys.exc_info())
-                self.debug('%s session %s done', trstate, self.id)
+                self.debug('postcommit session %s done', self.id)
                 return self.transaction_uuid(set=False)
         finally:
             self._touch()
--- a/server/sources/__init__.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/sources/__init__.py	Mon Sep 13 16:47:03 2010 +0200
@@ -26,6 +26,7 @@
 from cubicweb import set_log_methods, server
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.server.ssplanner import EditedEntity
 
 
 def dbg_st_search(uri, union, varmap, args, cachekey=None, prefix='rql for'):
@@ -343,6 +344,7 @@
         """
         entity = self.repo.vreg['etypes'].etype_class(etype)(session)
         entity.eid = eid
+        entity.cw_edited = EditedEntity(entity)
         return entity
 
     def after_entity_insertion(self, session, lid, entity):
--- a/server/sources/ldapuser.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/sources/ldapuser.py	Mon Sep 13 16:47:03 2010 +0200
@@ -574,7 +574,7 @@
         entity = super(LDAPUserSource, self).before_entity_insertion(session, lid, etype, eid)
         res = self._search(session, lid, BASE)[0]
         for attr in entity.e_schema.indexable_attributes():
-            entity[attr] = res[self.user_rev_attrs[attr]]
+            entity.cw_edited[attr] = res[self.user_rev_attrs[attr]]
         return entity
 
     def after_entity_insertion(self, session, dn, entity):
--- a/server/sources/native.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/sources/native.py	Mon Sep 13 16:47:03 2010 +0200
@@ -55,6 +55,7 @@
 from cubicweb.server.rqlannotation import set_qdata
 from cubicweb.server.hook import CleanupDeletedEidsCacheOp
 from cubicweb.server.session import hooks_control, security_enabled
+from cubicweb.server.ssplanner import EditedEntity
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
 
@@ -547,21 +548,20 @@
         etype = entity.__regid__
         for attr, storage in self._storages.get(etype, {}).items():
             try:
-                edited = entity.edited_attributes
+                edited = entity.cw_edited
             except AttributeError:
                 assert event == 'deleted'
                 getattr(storage, 'entity_deleted')(entity, attr)
             else:
                 if attr in edited:
                     handler = getattr(storage, 'entity_%s' % event)
-                    real_value = handler(entity, attr)
-                    restore_values[attr] = real_value
+                    restore_values[attr] = handler(entity, attr)
         try:
             yield # 2/ execute the source's instructions
         finally:
             # 3/ restore original values
             for attr, value in restore_values.items():
-                entity[attr] = value
+                entity.cw_edited.edited_attribute(attr, value)
 
     def add_entity(self, session, entity):
         """add a new entity to the source"""
@@ -1119,6 +1119,7 @@
             err("can't restore entity %s of type %s, type no more supported"
                 % (eid, etype))
             return errors
+        entity.cw_edited = edited = EditedEntity(entity)
         # check for schema changes, entities linked through inlined relation
         # still exists, rewrap binary values
         eschema = entity.e_schema
@@ -1135,15 +1136,14 @@
                 assert value is None
             elif eschema.destination(rtype) in ('Bytes', 'Password'):
                 action.changes[column] = self._binary(value)
-                entity[rtype] = Binary(value)
+                edited[rtype] = Binary(value)
             elif isinstance(value, str):
-                entity[rtype] = unicode(value, session.encoding, 'replace')
+                edited[rtype] = unicode(value, session.encoding, 'replace')
             else:
-                entity[rtype] = value
+                edited[rtype] = value
         entity.eid = eid
         session.repo.init_entity_caches(session, entity, self)
-        entity.edited_attributes = set(entity)
-        entity._cw_check()
+        edited.check()
         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
         # restore the entity
         action.changes['cw_eid'] = eid
--- a/server/sources/storages.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/sources/storages.py	Mon Sep 13 16:47:03 2010 +0200
@@ -23,6 +23,8 @@
 
 from cubicweb import Binary, ValidationError
 from cubicweb.server import hook
+from cubicweb.server.ssplanner import EditedEntity
+
 
 def set_attribute_storage(repo, etype, attr, storage):
     repo.system_source.set_storage(etype, attr, storage)
@@ -30,6 +32,7 @@
 def unset_attribute_storage(repo, etype, attr):
     repo.system_source.unset_storage(etype, attr)
 
+
 class Storage(object):
     """abstract storage
 
@@ -114,12 +117,12 @@
     def entity_added(self, entity, attr):
         """an entity using this storage for attr has been added"""
         if entity._cw.transaction_data.get('fs_importing'):
-            binary = Binary(file(entity[attr].getvalue(), 'rb').read())
+            binary = Binary(file(entity.cw_edited[attr].getvalue(), 'rb').read())
         else:
-            binary = entity.pop(attr)
+            binary = entity.cw_edited.pop(attr)
             fpath = self.new_fs_path(entity, attr)
             # bytes storage used to store file's path
-            entity[attr] = Binary(fpath)
+            entity.cw_edited.edited_attribute(attr, Binary(fpath))
             file(fpath, 'wb').write(binary.getvalue())
             hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp)
         return binary
@@ -132,7 +135,7 @@
             # If we are importing from the filesystem, the file already exists.
             # We do not need to create it but we need to fetch the content of
             # the file as the actual content of the attribute
-            fpath = entity[attr].getvalue()
+            fpath = entity.cw_edited[attr].getvalue()
             binary = Binary(file(fpath, 'rb').read())
         else:
             # We must store the content of the attributes
@@ -144,7 +147,7 @@
             # went ok.
             #
             # fetch the current attribute value in memory
-            binary = entity.pop(attr)
+            binary = entity.cw_edited.pop(attr)
             # Get filename for it
             fpath = self.new_fs_path(entity, attr)
             assert not osp.exists(fpath)
@@ -155,7 +158,7 @@
             hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp)
         if oldpath != fpath:
             # register the new location for the file.
-            entity[attr] = Binary(fpath)
+            entity.cw_edited.edited_attribute(attr, Binary(fpath))
             # Mark the old file as useless so the file will be removed at
             # commit.
             hook.set_operation(entity._cw, 'bfss_deleted', oldpath,
@@ -197,7 +200,7 @@
 
     def migrate_entity(self, entity, attribute):
         """migrate an entity attribute to the storage"""
-        entity.edited_attributes = set()
+        entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
         self.entity_added(entity, attribute)
         session = entity._cw
         source = session.repo.system_source
@@ -205,6 +208,7 @@
         sql = source.sqlgen.update('cw_' + entity.__regid__, attrs,
                                    ['cw_eid'])
         source.doexec(session, sql, attrs)
+        entity.cw_edited = None
 
 
 class AddFileOp(hook.Operation):
@@ -216,7 +220,7 @@
                 self.error('cant remove %s: %s' % (filepath, ex))
 
 class DeleteFileOp(hook.Operation):
-    def commit_event(self):
+    def postcommit_event(self):
         for filepath in self.session.transaction_data.pop('bfss_deleted'):
             try:
                 unlink(filepath)
--- a/server/sqlutils.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/sqlutils.py	Mon Sep 13 16:47:03 2010 +0200
@@ -260,11 +260,10 @@
         """
         attrs = {}
         eschema = entity.e_schema
-        for attr in entity.edited_attributes:
-            value = entity[attr]
+        for attr, value in entity.cw_edited.iteritems():
             rschema = eschema.subjrels[attr]
             if rschema.final:
-                atype = str(entity.e_schema.destination(attr))
+                atype = str(eschema.destination(attr))
                 if atype == 'Boolean':
                     value = self.dbhelper.boolean_value(value)
                 elif atype == 'Password':
--- a/server/ssplanner.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/ssplanner.py	Mon Sep 13 16:47:03 2010 +0200
@@ -21,6 +21,8 @@
 
 __docformat__ = "restructuredtext en"
 
+from copy import copy
+
 from rql.stmts import Union, Select
 from rql.nodes import Constant, Relation
 
@@ -55,11 +57,11 @@
             if isinstance(rhs, Constant) and not rhs.uid:
                 # add constant values to entity def
                 value = rhs.eval(plan.args)
-                eschema = edef.e_schema
+                eschema = edef.entity.e_schema
                 attrtype = eschema.subjrels[rtype].objects(eschema)[0]
                 if attrtype == 'Password' and isinstance(value, unicode):
                     value = value.encode('UTF8')
-                edef[rtype] = value
+                edef.edited_attribute(rtype, value)
             elif to_build.has_key(str(rhs)):
                 # create a relation between two newly created variables
                 plan.add_relation_def((edef, rtype, to_build[rhs.name]))
@@ -126,6 +128,132 @@
     return select
 
 
+_MARKER = object()
+
+class dict_protocol_catcher(object):
+    def __init__(self, entity):
+        self.__entity = entity
+    def __getitem__(self, attr):
+        return self.__entity.cw_edited[attr]
+    def __setitem__(self, attr, value):
+        self.__entity.cw_edited[attr] = value
+    def __getattr__(self, attr):
+        return getattr(self.__entity, attr)
+
+
+class EditedEntity(dict):
+    """encapsulate entities attributes being written by an RQL query"""
+    def __init__(self, entity, **kwargs):
+        dict.__init__(self, **kwargs)
+        self.entity = entity
+        self.skip_security = set()
+        self.querier_pending_relations = {}
+        self.saved = False
+
+    def __hash__(self):
+        # dict|set keyable
+        return hash(id(self))
+
+    def __cmp__(self, other):
+        # we don't want comparison by value inherited from dict
+        return cmp(id(self), id(other))
+
+    def __setitem__(self, attr, value):
+        assert attr != 'eid'
+        # don't add attribute into skip_security if already in edited
+        # attributes, else we may accidentaly skip a desired security check
+        if attr not in self:
+            self.skip_security.add(attr)
+        self.edited_attribute(attr, value)
+
+    def __delitem__(self, attr):
+        assert not self.saved, 'too late to modify edited attributes'
+        super(EditedEntity, self).__delitem__(attr)
+        self.entity.cw_attr_cache.pop(attr, None)
+
+    def pop(self, attr, *args):
+        # don't update skip_security by design (think to storage api)
+        assert not self.saved, 'too late to modify edited attributes'
+        value = super(EditedEntity, self).pop(attr, *args)
+        self.entity.cw_attr_cache.pop(attr, *args)
+        return value
+
+    def setdefault(self, attr, default):
+        assert attr != 'eid'
+        # don't add attribute into skip_security if already in edited
+        # attributes, else we may accidentaly skip a desired security check
+        if attr not in self:
+            self[attr] = default
+        return self[attr]
+
+    def update(self, values, skipsec=True):
+        if skipsec:
+            setitem = self.__setitem__
+        else:
+            setitem = self.edited_attribute
+        for attr, value in values.iteritems():
+            setitem(attr, value)
+
+    def edited_attribute(self, attr, value):
+        """attribute being edited by a rql query: should'nt be added to
+        skip_security
+        """
+        assert not self.saved, 'too late to modify edited attributes'
+        super(EditedEntity, self).__setitem__(attr, value)
+        self.entity.cw_attr_cache[attr] = value
+
+    def oldnewvalue(self, attr):
+        """returns the couple (old attr value, new attr value)
+
+        NOTE: will only work in a before_update_entity hook
+        """
+        assert not self.saved, 'too late to get the old value'
+        # get new value and remove from local dict to force a db query to
+        # fetch old value
+        newvalue = self.entity.cw_attr_cache.pop(attr, _MARKER)
+        oldvalue = getattr(self.entity, attr)
+        if newvalue is not _MARKER:
+            self.entity.cw_attr_cache[attr] = newvalue
+        else:
+            newvalue = oldvalue
+        return oldvalue, newvalue
+
+    def set_defaults(self):
+        """set default values according to the schema"""
+        for attr, value in self.entity.e_schema.defaults():
+            if not attr in self:
+                self[str(attr)] = value
+
+    def check(self, creation=False):
+        """check the entity edition against its schema. Only final relation
+        are checked here, constraint on actual relations are checked in hooks
+        """
+        entity = self.entity
+        if creation:
+            # on creations, we want to check all relations, especially
+            # required attributes
+            relations = [rschema for rschema in entity.e_schema.subject_relations()
+                         if rschema.final and rschema.type != 'eid']
+        else:
+            relations = [entity._cw.vreg.schema.rschema(rtype)
+                         for rtype in self]
+        from yams import ValidationError
+        try:
+            entity.e_schema.check(dict_protocol_catcher(entity),
+                                  creation=creation, _=entity._cw._,
+                                  relations=relations)
+        except ValidationError, ex:
+            ex.entity = self.entity
+            raise
+
+    def clone(self):
+        thecopy = EditedEntity(copy(self.entity))
+        thecopy.entity.cw_attr_cache = copy(self.entity.cw_attr_cache)
+        thecopy.entity._cw_related_cache = {}
+        thecopy.update(self, skipsec=False)
+        return thecopy
+
+
 class SSPlanner(object):
     """SingleSourcePlanner: build execution plan for rql queries
 
@@ -162,7 +290,7 @@
         etype_class = session.vreg['etypes'].etype_class
         for etype, var in rqlst.main_variables:
             # need to do this since entity class is shared w. web client code !
-            to_build[var.name] = etype_class(etype)(session)
+            to_build[var.name] = EditedEntity(etype_class(etype)(session))
             plan.add_entity_def(to_build[var.name])
         # add constant values to entity def, mark variables to be selected
         to_select = _extract_const_attributes(plan, rqlst, to_build)
@@ -177,7 +305,7 @@
         for edef, rdefs in to_select.items():
             # create a select rql st to fetch needed data
             select = Select()
-            eschema = edef.e_schema
+            eschema = edef.entity.e_schema
             for i, (rtype, term, reverse) in enumerate(rdefs):
                 if getattr(term, 'variable', None) in eidconsts:
                     value = eidconsts[term.variable]
@@ -284,10 +412,8 @@
                 rhsinfo = selectedidx[rhskey][:-1] + (None,)
             rschema = getrschema(relation.r_type)
             updatedefs.append( (lhsinfo, rhsinfo, rschema) )
-            if rschema.final or rschema.inlined:
-                attributes.add(relation.r_type)
         # the update step
-        step = UpdateStep(plan, updatedefs, attributes)
+        step = UpdateStep(plan, updatedefs)
         # when necessary add substep to fetch yet unknown values
         select = _build_substep_query(select, rqlst)
         if select is not None:
@@ -476,7 +602,7 @@
             result = [[]]
         for row in result:
             # get a new entity definition for this row
-            edef = base_edef.cw_copy()
+            edef = base_edef.clone()
             # complete this entity def using row values
             index = 0
             for rtype, rorder, value in self.rdefs:
@@ -484,7 +610,7 @@
                     value = row[index]
                     index += 1
                 if rorder == InsertRelationsStep.FINAL:
-                    edef._cw_rql_set_value(rtype, value)
+                    edef.edited_attribute(rtype, value)
                 elif rorder == InsertRelationsStep.RELATION:
                     self.plan.add_relation_def( (edef, rtype, value) )
                     edef.querier_pending_relations[(rtype, 'subject')] = value
@@ -495,6 +621,7 @@
         self.plan.substitute_entity_def(base_edef, edefs)
         return result
 
+
 class InsertStep(Step):
     """step consisting in inserting new entities / relations"""
 
@@ -555,10 +682,9 @@
     definitions and from results fetched in previous step
     """
 
-    def __init__(self, plan, updatedefs, attributes):
+    def __init__(self, plan, updatedefs):
         Step.__init__(self, plan)
         self.updatedefs = updatedefs
-        self.attributes = attributes
 
     def execute(self):
         """execute this step"""
@@ -578,16 +704,17 @@
                 if rschema.final or rschema.inlined:
                     eid = typed_eid(lhsval)
                     try:
-                        edef = edefs[eid]
+                        edited = edefs[eid]
                     except KeyError:
-                        edefs[eid] = edef = session.entity_from_eid(eid)
-                    edef._cw_rql_set_value(str(rschema), rhsval)
+                        edef = session.entity_from_eid(eid)
+                        edefs[eid] = edited = EditedEntity(edef)
+                    edited.edited_attribute(str(rschema), rhsval)
                 else:
                     repo.glob_add_relation(session, lhsval, str(rschema), rhsval)
             result[i] = newrow
         # update entities
-        for eid, edef in edefs.iteritems():
-            repo.glob_update_entity(session, edef, set(self.attributes))
+        for eid, edited in edefs.iteritems():
+            repo.glob_update_entity(session, edited)
         return result
 
 def _handle_relterm(info, row, newrow):
--- a/server/test/data/schema.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/test/data/schema.py	Mon Sep 13 16:47:03 2010 +0200
@@ -17,7 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
-                            SubjectRelation, RichString, String, Int, Boolean, Datetime)
+                            SubjectRelation, RichString, String, Int, Float,
+                            Boolean, Datetime)
 from yams.constraints import SizeConstraint
 from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
                              ERQLExpression, RRQLExpression)
@@ -39,7 +40,7 @@
                        description=_('more detailed description'))
 
     duration = Int()
-    invoiced = Int()
+    invoiced = Float()
 
     depends_on = SubjectRelation('Affaire')
     require_permission = SubjectRelation('CWPermission')
--- a/server/test/unittest_msplanner.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/test/unittest_msplanner.py	Mon Sep 13 16:47:03 2010 +0200
@@ -1914,7 +1914,7 @@
                    [('FetchStep', [('Any WP WHERE 999999 multisource_rel WP, WP is Note', [{'WP': 'Note'}])],
                      [self.cards], None, {'WP': u'table0.C0'}, []),
                     ('OneFetchStep', [('Any S,SUM(DUR),SUM(I),(SUM(I) - SUM(DUR)),MIN(DI),MAX(DI) GROUPBY S ORDERBY S WHERE A duration DUR, A invoiced I, A modification_date DI, A in_state S, S name SN, (EXISTS(A concerne WP, WP is Note)) OR (EXISTS(A concerne 999999)), A is Affaire, S is State',
-                                       [{'A': 'Affaire', 'DI': 'Datetime', 'DUR': 'Int', 'I': 'Int', 'S': 'State', 'SN': 'String', 'WP': 'Note'}])],
+                                       [{'A': 'Affaire', 'DI': 'Datetime', 'DUR': 'Int', 'I': 'Float', 'S': 'State', 'SN': 'String', 'WP': 'Note'}])],
                      None, None, [self.system], {'WP': u'table0.C0'}, [])],
                    {'n': 999999})
 
--- a/server/test/unittest_querier.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/test/unittest_querier.py	Mon Sep 13 16:47:03 2010 +0200
@@ -545,12 +545,25 @@
         self.assertEquals(rset.rows[0][0], 'ADMIN')
         self.assertEquals(rset.description, [('String',)])
 
-##     def test_select_simplified(self):
-##         ueid = self.session.user.eid
-##         rset = self.execute('Any L WHERE %s login L'%ueid)
-##         self.assertEquals(rset.rows[0][0], 'admin')
-##         rset = self.execute('Any L WHERE %(x)s login L', {'x':ueid})
-##         self.assertEquals(rset.rows[0][0], 'admin')
+    def test_select_float_abs(self):
+        # test positive number
+        eid = self.execute('INSERT Affaire A: A invoiced %(i)s', {'i': 1.2})[0][0]
+        rset = self.execute('Any ABS(I) WHERE X eid %(x)s, X invoiced I', {'x': eid})
+        self.assertEquals(rset.rows[0][0], 1.2)
+        # test negative number
+        eid = self.execute('INSERT Affaire A: A invoiced %(i)s', {'i': -1.2})[0][0]
+        rset = self.execute('Any ABS(I) WHERE X eid %(x)s, X invoiced I', {'x': eid})
+        self.assertEquals(rset.rows[0][0], 1.2)
+
+    def test_select_int_abs(self):
+        # test positive number
+        eid = self.execute('INSERT Affaire A: A duration %(d)s', {'d': 12})[0][0]
+        rset = self.execute('Any ABS(D) WHERE X eid %(x)s, X duration D', {'x': eid})
+        self.assertEquals(rset.rows[0][0], 12)
+        # test negative number
+        eid = self.execute('INSERT Affaire A: A duration %(d)s', {'d': -12})[0][0]
+        rset = self.execute('Any ABS(D) WHERE X eid %(x)s, X duration D', {'x': eid})
+        self.assertEquals(rset.rows[0][0], 12)
 
     def test_select_searchable_text_1(self):
         rset = self.execute(u"INSERT Personne X: X nom 'bidüle'")
--- a/server/test/unittest_repository.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/server/test/unittest_repository.py	Mon Sep 13 16:47:03 2010 +0200
@@ -179,7 +179,7 @@
     def test_check_session(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        self.assertEquals(repo.check_session(cnxid), None)
+        self.assertIsInstance(repo.check_session(cnxid), float)
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.check_session, cnxid)
 
@@ -448,7 +448,7 @@
                               'EmailAddress', address=u'a@b.fr')
 
     def test_multiple_edit_set_attributes(self):
-        """make sure edited_attributes doesn't get cluttered
+        """make sure cw_edited doesn't get cluttered
         by previous entities on multiple set
         """
         # local hook
@@ -459,9 +459,9 @@
             events = ('before_update_entity',)
             def __call__(self):
                 # invoiced attribute shouldn't be considered "edited" before the hook
-                self._test.failIf('invoiced' in self.entity.edited_attributes,
-                                  'edited_attributes cluttered by previous update')
-                self.entity['invoiced'] = 10
+                self._test.failIf('invoiced' in self.entity.cw_edited,
+                                  'cw_edited cluttered by previous update')
+                self.entity.cw_edited['invoiced'] = 10
         with self.temporary_appobjects(DummyBeforeHook):
             req = self.request()
             req.create_entity('Affaire', ref=u'AFF01')
--- a/sobjects/supervising.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/sobjects/supervising.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,10 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""some hooks and views to handle supervising of any data changes
+"""some hooks and views to handle supervising of any data changes"""
 
-
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb import UnknownEid
@@ -185,6 +183,6 @@
         msg = format_mail(uinfo, recipients, content, view.subject(), config=config)
         self.to_send = [(msg, recipients)]
 
-    def commit_event(self):
+    def postcommit_event(self):
         self._prepare_email()
-        SendMailOp.commit_event(self)
+        SendMailOp.postcommit_event(self)
--- a/tags.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/tags.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""helper classes to generate simple (X)HTML tags
+"""helper classes to generate simple (X)HTML tags"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.uilib import simple_sgml_tag, sgml_attributes
--- a/test/unittest_entity.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/test/unittest_entity.py	Mon Sep 13 16:47:03 2010 +0200
@@ -16,9 +16,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unit tests for cubicweb.web.views.entities module
-
-"""
+"""unit tests for cubicweb.web.views.entities module"""
 
 from datetime import datetime
 
@@ -322,30 +320,30 @@
                             content_format=u'text/rest')
         self.assertEquals(e.printable_value('content'),
                           '<p>du <a class="reference" href="http://testing.fr/cubicweb/cwgroup/guests">*ReST*</a></p>\n')
-        e['content'] = 'du <em>html</em> <ref rql="CWUser X">users</ref>'
-        e['content_format'] = 'text/html'
+        e.cw_attr_cache['content'] = 'du <em>html</em> <ref rql="CWUser X">users</ref>'
+        e.cw_attr_cache['content_format'] = 'text/html'
         self.assertEquals(e.printable_value('content'),
                           'du <em>html</em> <a href="http://testing.fr/cubicweb/view?rql=CWUser%20X">users</a>')
-        e['content'] = 'du *texte*'
-        e['content_format'] = 'text/plain'
+        e.cw_attr_cache['content'] = 'du *texte*'
+        e.cw_attr_cache['content_format'] = 'text/plain'
         self.assertEquals(e.printable_value('content'),
                           '<p>\ndu *texte*<br/>\n</p>')
-        e['title'] = 'zou'
-        e['content'] = '''\
+        e.cw_attr_cache['title'] = 'zou'
+        e.cw_attr_cache['content'] = '''\
 a title
 =======
 du :eid:`1:*ReST*`'''
-        e['content_format'] = 'text/rest'
+        e.cw_attr_cache['content_format'] = 'text/rest'
         self.assertEquals(e.printable_value('content', format='text/plain'),
-                          e['content'])
+                          e.cw_attr_cache['content'])
 
-        e['content'] = u'<b>yo (zou éà ;)</b>'
-        e['content_format'] = 'text/html'
+        e.cw_attr_cache['content'] = u'<b>yo (zou éà ;)</b>'
+        e.cw_attr_cache['content_format'] = 'text/html'
         self.assertEquals(e.printable_value('content', format='text/plain').strip(),
                           u'**yo (zou éà ;)**')
         if HAS_TAL:
-            e['content'] = '<h1 tal:content="self/title">titre</h1>'
-            e['content_format'] = 'text/cubicweb-page-template'
+            e.cw_attr_cache['content'] = '<h1 tal:content="self/title">titre</h1>'
+            e.cw_attr_cache['content_format'] = 'text/cubicweb-page-template'
             self.assertEquals(e.printable_value('content'),
                               '<h1>zou</h1>')
 
@@ -387,30 +385,30 @@
         tidy = lambda x: x.replace('\n', '')
         self.assertEquals(tidy(e.printable_value('content')),
                           '<div>R&amp;D<br/></div>')
-        e['content'] = u'yo !! R&D <div> pas fermé'
+        e.cw_attr_cache['content'] = u'yo !! R&D <div> pas fermé'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'yo !! R&amp;D <div> pas fermé</div>')
-        e['content'] = u'R&D'
+        e.cw_attr_cache['content'] = u'R&D'
         self.assertEquals(tidy(e.printable_value('content')), u'R&amp;D')
-        e['content'] = u'R&D;'
+        e.cw_attr_cache['content'] = u'R&D;'
         self.assertEquals(tidy(e.printable_value('content')), u'R&amp;D;')
-        e['content'] = u'yo !! R&amp;D <div> pas fermé'
+        e.cw_attr_cache['content'] = u'yo !! R&amp;D <div> pas fermé'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'yo !! R&amp;D <div> pas fermé</div>')
-        e['content'] = u'été <div> été'
+        e.cw_attr_cache['content'] = u'été <div> été'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'été <div> été</div>')
-        e['content'] = u'C&apos;est un exemple s&eacute;rieux'
+        e.cw_attr_cache['content'] = u'C&apos;est un exemple s&eacute;rieux'
         self.assertEquals(tidy(e.printable_value('content')),
                           u"C'est un exemple sérieux")
         # make sure valid xhtml is left untouched
-        e['content'] = u'<div>R&amp;D<br/></div>'
-        self.assertEquals(e.printable_value('content'), e['content'])
-        e['content'] = u'<div>été</div>'
-        self.assertEquals(e.printable_value('content'), e['content'])
-        e['content'] = u'été'
-        self.assertEquals(e.printable_value('content'), e['content'])
-        e['content'] = u'hop\r\nhop\nhip\rmomo'
+        e.cw_attr_cache['content'] = u'<div>R&amp;D<br/></div>'
+        self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content'])
+        e.cw_attr_cache['content'] = u'<div>été</div>'
+        self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content'])
+        e.cw_attr_cache['content'] = u'été'
+        self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content'])
+        e.cw_attr_cache['content'] = u'hop\r\nhop\nhip\rmomo'
         self.assertEquals(e.printable_value('content'), u'hop\nhop\nhip\nmomo')
 
     def test_printable_value_bad_html_ms(self):
@@ -419,7 +417,7 @@
         e = req.create_entity('Card', title=u'bad html', content=u'<div>R&D<br>',
                             content_format=u'text/html')
         tidy = lambda x: x.replace('\n', '')
-        e['content'] = u'<div x:foo="bar">ms orifice produces weird html</div>'
+        e.cw_attr_cache['content'] = u'<div x:foo="bar">ms orifice produces weird html</div>'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'<div>ms orifice produces weird html</div>')
         import tidy as tidymod # apt-get install python-tidy
@@ -435,12 +433,12 @@
 
     def test_fulltextindex(self):
         e = self.vreg['etypes'].etype_class('File')(self.request())
-        e['description'] = 'du <em>html</em>'
-        e['description_format'] = 'text/html'
-        e['data'] = Binary('some <em>data</em>')
-        e['data_name'] = 'an html file'
-        e['data_format'] = 'text/html'
-        e['data_encoding'] = 'ascii'
+        e.cw_attr_cache['description'] = 'du <em>html</em>'
+        e.cw_attr_cache['description_format'] = 'text/html'
+        e.cw_attr_cache['data'] = Binary('some <em>data</em>')
+        e.cw_attr_cache['data_name'] = 'an html file'
+        e.cw_attr_cache['data_format'] = 'text/html'
+        e.cw_attr_cache['data_encoding'] = 'ascii'
         e._cw.transaction_data = {} # XXX req should be a session
         self.assertEquals(e.cw_adapt_to('IFTIndexable').get_words(),
                           {'C': [u'du', u'html', 'an', 'html', 'file', u'some', u'data']})
@@ -461,7 +459,7 @@
             'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0]
         trinfo = self.execute('Any X WHERE X eid %(x)s', {'x': eid}).get_entity(0, 0)
         trinfo.complete()
-        self.failUnless(isinstance(trinfo['creation_date'], datetime))
+        self.failUnless(isinstance(trinfo.cw_attr_cache['creation_date'], datetime))
         self.failUnless(trinfo.cw_relation_cached('from_state', 'subject'))
         self.failUnless(trinfo.cw_relation_cached('to_state', 'subject'))
         self.failUnless(trinfo.cw_relation_cached('wf_info_for', 'subject'))
--- a/test/unittest_rset.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/test/unittest_rset.py	Mon Sep 13 16:47:03 2010 +0200
@@ -157,13 +157,13 @@
         rs.req = self.request()
         rs.vreg = self.vreg
 
-        rs2 = rs.sorted_rset(lambda e:e['login'])
+        rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login'])
         self.assertEquals(len(rs2), 3)
         self.assertEquals([login for _, login in rs2], ['adim', 'nico', 'syt'])
         # make sure rs is unchanged
         self.assertEquals([login for _, login in rs], ['adim', 'syt', 'nico'])
 
-        rs2 = rs.sorted_rset(lambda e:e['login'], reverse=True)
+        rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login'], reverse=True)
         self.assertEquals(len(rs2), 3)
         self.assertEquals([login for _, login in rs2], ['syt', 'nico', 'adim'])
         # make sure rs is unchanged
@@ -187,7 +187,7 @@
         rs.req = self.request()
         rs.vreg = self.vreg
 
-        rsets = rs.split_rset(lambda e:e['login'])
+        rsets = rs.split_rset(lambda e:e.cw_attr_cache['login'])
         self.assertEquals(len(rsets), 3)
         self.assertEquals([login for _, login,_ in rsets[0]], ['adim', 'adim'])
         self.assertEquals([login for _, login,_ in rsets[1]], ['syt'])
@@ -195,7 +195,7 @@
         # make sure rs is unchanged
         self.assertEquals([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico'])
 
-        rsets = rs.split_rset(lambda e:e['login'], return_dict=True)
+        rsets = rs.split_rset(lambda e:e.cw_attr_cache['login'], return_dict=True)
         self.assertEquals(len(rsets), 3)
         self.assertEquals([login for _, login,_ in rsets['nico']], ['nico', 'nico'])
         self.assertEquals([login for _, login,_ in rsets['adim']], ['adim', 'adim'])
@@ -230,12 +230,12 @@
         self.request().create_entity('CWUser', login=u'adim', upassword='adim',
                         surname=u'di mascio', firstname=u'adrien')
         e = self.execute('Any X,T WHERE X login "adim", X surname T').get_entity(0, 0)
-        self.assertEquals(e['surname'], 'di mascio')
-        self.assertRaises(KeyError, e.__getitem__, 'firstname')
-        self.assertRaises(KeyError, e.__getitem__, 'creation_date')
+        self.assertEquals(e.cw_attr_cache['surname'], 'di mascio')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'creation_date')
         self.assertEquals(pprelcachedict(e._cw_related_cache), [])
         e.complete()
-        self.assertEquals(e['firstname'], 'adrien')
+        self.assertEquals(e.cw_attr_cache['firstname'], 'adrien')
         self.assertEquals(pprelcachedict(e._cw_related_cache), [])
 
     def test_get_entity_advanced(self):
@@ -246,20 +246,20 @@
         e = rset.get_entity(0, 0)
         self.assertEquals(e.cw_row, 0)
         self.assertEquals(e.cw_col, 0)
-        self.assertEquals(e['title'], 'zou')
-        self.assertRaises(KeyError, e.__getitem__, 'path')
+        self.assertEquals(e.cw_attr_cache['title'], 'zou')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'path')
         self.assertEquals(e.view('text'), 'zou')
         self.assertEquals(pprelcachedict(e._cw_related_cache), [])
 
         e = rset.get_entity(0, 1)
         self.assertEquals(e.cw_row, 0)
         self.assertEquals(e.cw_col, 1)
-        self.assertEquals(e['login'], 'anon')
-        self.assertRaises(KeyError, e.__getitem__, 'firstname')
+        self.assertEquals(e.cw_attr_cache['login'], 'anon')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname')
         self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [])
         e.complete()
-        self.assertEquals(e['firstname'], None)
+        self.assertEquals(e.cw_attr_cache['firstname'], None)
         self.assertEquals(e.view('text'), 'anon')
         self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [])
@@ -282,17 +282,17 @@
         rset = self.execute('Any X,U,S,XT,UL,SN WHERE X created_by U, U in_state S, '
                             'X title XT, S name SN, U login UL, X eid %s' % e.eid)
         e = rset.get_entity(0, 0)
-        self.assertEquals(e['title'], 'zou')
+        self.assertEquals(e.cw_attr_cache['title'], 'zou')
         self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [('created_by_subject', [5])])
         # first level of recursion
         u = e.created_by[0]
-        self.assertEquals(u['login'], 'admin')
-        self.assertRaises(KeyError, u.__getitem__, 'firstname')
+        self.assertEquals(u.cw_attr_cache['login'], 'admin')
+        self.assertRaises(KeyError, u.cw_attr_cache.__getitem__, 'firstname')
         # second level of recursion
         s = u.in_state[0]
-        self.assertEquals(s['name'], 'activated')
-        self.assertRaises(KeyError, s.__getitem__, 'description')
+        self.assertEquals(s.cw_attr_cache['name'], 'activated')
+        self.assertRaises(KeyError, s.cw_attr_cache.__getitem__, 'description')
 
 
     def test_get_entity_cache_with_left_outer_join(self):
@@ -322,7 +322,7 @@
             etype, n = expected[entity.cw_row]
             self.assertEquals(entity.__regid__, etype)
             attr = etype == 'Bookmark' and 'title' or 'name'
-            self.assertEquals(entity[attr], n)
+            self.assertEquals(entity.cw_attr_cache[attr], n)
 
     def test_related_entity_optional(self):
         e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
--- a/test/unittest_uilib.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/test/unittest_uilib.py	Mon Sep 13 16:47:03 2010 +0200
@@ -16,14 +16,11 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unittests for cubicweb.uilib
-
-"""
+"""unittests for cubicweb.uilib"""
 
 __docformat__ = "restructuredtext en"
 
 from logilab.common.testlib import TestCase, unittest_main
-from logilab.common.tree import Node
 
 from cubicweb import uilib
 
--- a/test/unittest_utils.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/test/unittest_utils.py	Mon Sep 13 16:47:03 2010 +0200
@@ -140,14 +140,14 @@
 
     def test_encoding_bare_entity(self):
         e = Entity(None)
-        e['pouet'] = 'hop'
+        e.cw_attr_cache['pouet'] = 'hop'
         e.eid = 2
         self.assertEquals(json.loads(self.encode(e)),
                           {'pouet': 'hop', 'eid': 2})
 
     def test_encoding_entity_in_list(self):
         e = Entity(None)
-        e['pouet'] = 'hop'
+        e.cw_attr_cache['pouet'] = 'hop'
         e.eid = 2
         self.assertEquals(json.loads(self.encode([e])),
                           [{'pouet': 'hop', 'eid': 2}])
--- a/utils.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/utils.py	Mon Sep 13 16:47:03 2010 +0200
@@ -24,6 +24,7 @@
 import decimal
 import datetime
 import random
+from inspect import getargspec
 from itertools import repeat
 from uuid import uuid4
 from warnings import warn
@@ -64,6 +65,40 @@
                                        '__doc__': cls.__doc__,
                                        '__module__': cls.__module__})
 
+def support_args(callable, *argnames):
+    """return true if the callable support given argument names"""
+    argspec = getargspec(callable)
+    if argspec[2]:
+        return True
+    for argname in argnames:
+        if argname not in argspec[0]:
+            return False
+    return True
+
+
+class wrap_on_write(object):
+    def __init__(self, w, tag, closetag=None):
+        self.written = False
+        self.tag = unicode(tag)
+        self.closetag = closetag
+        self.w = w
+
+    def __enter__(self):
+        return self
+
+    def __call__(self, data):
+        if self.written is False:
+            self.w(self.tag)
+            self.written = True
+        self.w(data)
+
+    def __exit__(self, exctype, value, traceback):
+        if self.written is True:
+            if self.closetag:
+                self.w(unicode(self.closetag))
+            else:
+                self.w(self.tag.replace('<', '</', 1))
+
 
 # use networkX instead ?
 # http://networkx.lanl.gov/reference/algorithms.traversal.html#module-networkx.algorithms.traversal.astar
--- a/view.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/view.py	Mon Sep 13 16:47:03 2010 +0200
@@ -319,21 +319,11 @@
             clabel = vtitle
         return u'%s (%s)' % (clabel, self._cw.property_value('ui.site-title'))
 
-    def output_url_builder( self, name, url, args ):
-        self.w(u'<script language="JavaScript"><!--\n' \
-               u'function %s( %s ) {\n' % (name, ','.join(args) ) )
-        url_parts = url.split("%s")
-        self.w(u' url="%s"' % url_parts[0] )
-        for arg, part in zip(args, url_parts[1:]):
-            self.w(u'+str(%s)' % arg )
-            if part:
-                self.w(u'+"%s"' % part)
-        self.w('\n document.window.href=url;\n')
-        self.w('}\n-->\n</script>\n')
-
+    @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.build_url('add/%s' % etype, **kwargs)
+        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):
@@ -514,8 +504,13 @@
 
     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 ''
+        return self.domid
 
 
 class Component(ReloadableMixIn, View):
@@ -523,14 +518,20 @@
     __registry__ = 'components'
     __select__ = yes()
 
-    # XXX huummm, much probably useless
+    # XXX huummm, much probably useless (should be...)
     htmlclass = 'mainRelated'
+    @property
+    def cssclass(self):
+        return '%s %s' % (self.htmlclass, domid(self.__regid__))
+
+    # XXX should rely on ReloadableMixIn.domid
+    @property
+    def domid(self):
+        return '%sComponent' % domid(self.__regid__)
+
+    @deprecated('[3.10] use .cssclass property')
     def div_class(self):
-        return '%s %s' % (self.htmlclass, self.__regid__)
-
-    # XXX a generic '%s%s' % (self.__regid__, self.__registry__.capitalize()) would probably be nicer
-    def div_id(self):
-        return '%sComponent' % self.__regid__
+        return self.cssclass
 
 
 class Adapter(AppObject):
--- a/vregistry.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/vregistry.py	Mon Sep 13 16:47:03 2010 +0200
@@ -173,7 +173,7 @@
         assert len(objects) == 1, objects
         return objects[0](*args, **kwargs)
 
-    def select(self, oid, *args, **kwargs):
+    def select(self, __oid, *args, **kwargs):
         """return the most specific object among those with the given oid
         according to the given context.
 
@@ -181,14 +181,14 @@
 
         raise :exc:`NoSelectableObject` if not object apply
         """
-        return self._select_best(self[oid], *args, **kwargs)
+        return self._select_best(self[__oid], *args, **kwargs)
 
-    def select_or_none(self, oid, *args, **kwargs):
+    def select_or_none(self, __oid, *args, **kwargs):
         """return the most specific object among those with the given oid
         according to the given context, or None if no object applies.
         """
         try:
-            return self.select(oid, *args, **kwargs)
+            return self.select(__oid, *args, **kwargs)
         except (NoSelectableObject, ObjectNotFound):
             return None
     select_object = deprecated('[3.6] use select_or_none instead of select_object'
@@ -467,7 +467,7 @@
         self.load_module(module)
 
     def load_module(self, module):
-        self.info('loading %s', module)
+        self.info('loading %s from %s', module.__name__, module.__file__)
         if hasattr(module, 'registration_callback'):
             module.registration_callback(self)
         else:
--- a/web/action.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/action.py	Mon Sep 13 16:47:03 2010 +0200
@@ -45,35 +45,31 @@
         for action in self.actual_actions():
             menu.append(box.box_action(action))
 
+    def html_class(self):
+        if self._cw.selected(self.url()):
+            return 'selected'
+
+    def build_action(self, title, url, **kwargs):
+        return UnregisteredAction(self._cw, title, url, **kwargs)
+
     def url(self):
         """return the url associated with this action"""
         raise NotImplementedError
 
-    def html_class(self):
-        if self._cw.selected(self.url()):
-            return 'selected'
-        if self.category:
-            return 'box' + self.category.capitalize()
-
-    def build_action(self, title, path, **kwargs):
-        return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs)
-
 
 class UnregisteredAction(Action):
-    """non registered action used to build boxes. Unless you set them
-    explicitly, .vreg and .schema attributes at least are None.
-    """
+    """non registered action, used to build boxes"""
     category = None
     id = None
 
-    def __init__(self, req, rset, title, path, **kwargs):
-        Action.__init__(self, req, rset=rset)
+    def __init__(self, req, title, url, **kwargs):
+        Action.__init__(self, req)
         self.title = req._(title)
-        self._path = path
+        self._url = url
         self.__dict__.update(kwargs)
 
     def url(self):
-        return self._path
+        return self._url
 
 
 class LinkToEntityAction(Action):
--- a/web/application.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/application.py	Mon Sep 13 16:47:03 2010 +0200
@@ -31,7 +31,7 @@
 from cubicweb import set_log_methods, cwvreg
 from cubicweb import (
     ValidationError, Unauthorized, AuthenticationError, NoSelectableObject,
-    RepositoryError, CW_EVENT_MANAGER)
+    RepositoryError, BadConnectionId, CW_EVENT_MANAGER)
 from cubicweb.dbapi import DBAPISession
 from cubicweb.web import LOGGER, component
 from cubicweb.web import (
@@ -48,48 +48,43 @@
 
     def __init__(self, vreg):
         self.session_time = vreg.config['http-session-time'] or None
-        if self.session_time is not None:
-            assert self.session_time > 0
-            self.cleanup_session_time = self.session_time
-        else:
-            self.cleanup_session_time = vreg.config['cleanup-session-time'] or 1440 * 60
-            assert self.cleanup_session_time > 0
-        self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
-        assert self.cleanup_anon_session_time > 0
         self.authmanager = vreg['components'].select('authmanager', vreg=vreg)
+        interval = (self.session_time or 0) / 2.
         if vreg.config.anonymous_user() is not None:
-            self.clean_sessions_interval = max(
-                5 * 60, min(self.cleanup_session_time / 2.,
-                            self.cleanup_anon_session_time / 2.))
-        else:
-            self.clean_sessions_interval = max(
-                5 * 60,
-                self.cleanup_session_time / 2.)
+            self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
+            assert self.cleanup_anon_session_time > 0
+            if self.session_time is not None:
+                self.cleanup_anon_session_time = min(self.session_time,
+                                                     self.cleanup_anon_session_time)
+            interval = self.cleanup_anon_session_time / 2.
+        # we don't want to check session more than once every 5 minutes
+        self.clean_sessions_interval = max(5 * 60, interval)
 
     def clean_sessions(self):
         """cleanup sessions which has not been unused since a given amount of
         time. Return the number of sessions which have been closed.
         """
         self.debug('cleaning http sessions')
+        session_time = self.session_time
         closed, total = 0, 0
         for session in self.current_sessions():
-            no_use_time = (time() - session.last_usage_time)
             total += 1
-            if session.anonymous_session:
-                if no_use_time >= self.cleanup_anon_session_time:
+            try:
+                last_usage_time = session.cnx.check()
+            except BadConnectionId:
+                self.close_session(session)
+                closed += 1
+            else:
+                no_use_time = (time() - last_usage_time)
+                if session.anonymous_session:
+                    if no_use_time >= self.cleanup_anon_session_time:
+                        self.close_session(session)
+                        closed += 1
+                elif session_time is not None and no_use_time >= session_time:
                     self.close_session(session)
                     closed += 1
-            elif no_use_time >= self.cleanup_session_time:
-                self.close_session(session)
-                closed += 1
         return closed, total - closed
 
-    def has_expired(self, session):
-        """return True if the web session associated to the session is expired
-        """
-        return not (self.session_time is None or
-                    time() < session.last_usage_time + self.session_time)
-
     def current_sessions(self):
         """return currently open sessions"""
         raise NotImplementedError()
@@ -213,8 +208,6 @@
                 except AuthenticationError:
                     req.remove_cookie(cookie, self.SESSION_VAR)
                     raise
-        # remember last usage time for web session tracking
-        session.last_usage_time = time()
 
     def get_session(self, req, sessionid):
         return self.session_manager.get_session(req, sessionid)
@@ -224,8 +217,6 @@
         cookie = req.get_cookie()
         cookie[self.SESSION_VAR] = session.sessionid
         req.set_cookie(cookie, self.SESSION_VAR, maxage=None)
-        # remember last usage time for web session tracking
-        session.last_usage_time = time()
         if not session.anonymous_session:
             self._postlogin(req)
         return session
--- a/web/box.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/box.py	Mon Sep 13 16:47:03 2010 +0200
@@ -21,27 +21,44 @@
 _ = unicode
 
 from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated, class_renamed
 
 from cubicweb import Unauthorized, role as get_role, target as get_target
 from cubicweb.schema import display_name
-from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
-                                match_context_prop, partial_relation_possible,
-                                partial_has_related_entities)
-from cubicweb.view import View, ReloadableMixIn
-from cubicweb.uilib import domid, js
+from cubicweb.selectors import no_cnx, one_line_rset
+from cubicweb.view import View
 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
                                       RawBoxItem, BoxSeparator)
 from cubicweb.web.action import UnregisteredAction
 
 
+def sort_by_category(actions, categories_in_order=None):
+    """return a list of (category, actions_sorted_by_title)"""
+    result = []
+    actions_by_cat = {}
+    for action in actions:
+        actions_by_cat.setdefault(action.category, []).append(
+            (action.title, action) )
+    for key, values in actions_by_cat.items():
+        actions_by_cat[key] = [act for title, act in sorted(values)]
+    if categories_in_order:
+        for cat in categories_in_order:
+            if cat in actions_by_cat:
+                result.append( (cat, actions_by_cat[cat]) )
+    for item in sorted(actions_by_cat.items()):
+        result.append(item)
+    return result
+
+
+# old box system, deprecated ###################################################
+
 class BoxTemplate(View):
     """base template for boxes, usually a (contextual) list of possible
-
     actions. Various classes attributes may be used to control the box
     rendering.
 
-    You may override on of the formatting callbacks is this is not necessary
+    You may override one of the formatting callbacks if this is not necessary
     for your custom box.
 
     Classes inheriting from this class usually only have to override call
@@ -49,8 +66,11 @@
 
         box.render(self.w)
     """
-    __registry__ = 'boxes'
-    __select__ = ~no_cnx() & match_context_prop()
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] *BoxTemplate classes are deprecated, use *CtxComponent instead (%(cls)s)'
+
+    __registry__ = 'ctxcomponents'
+    __select__ = ~no_cnx()
 
     categories_in_order = ()
     cw_property_defs = {
@@ -64,34 +84,21 @@
                            help=_('context where this box should be displayed')),
         }
     context = 'left'
-    htmlitemclass = 'boxItem'
 
     def sort_actions(self, actions):
         """return a list of (category, actions_sorted_by_title)"""
-        result = []
-        actions_by_cat = {}
-        for action in actions:
-            actions_by_cat.setdefault(action.category, []).append(
-                (action.title, action) )
-        for key, values in actions_by_cat.items():
-            actions_by_cat[key] = [act for title, act in sorted(values)]
-        for cat in self.categories_in_order:
-            if cat in actions_by_cat:
-                result.append( (cat, actions_by_cat[cat]) )
-        for item in sorted(actions_by_cat.items()):
-            result.append(item)
-        return result
+        return sort_by_category(actions, self.categories_in_order)
 
-    def mk_action(self, title, path, escape=True, **kwargs):
+    def mk_action(self, title, url, escape=True, **kwargs):
         """factory function to create dummy actions compatible with the
         .format_actions method
         """
         if escape:
             title = xml_escape(title)
-        return self.box_action(self._action(title, path, **kwargs))
+        return self.box_action(self._action(title, url, **kwargs))
 
-    def _action(self, title, path, **kwargs):
-        return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs)
+    def _action(self, title, url, **kwargs):
+        return UnregisteredAction(self._cw, title, url, **kwargs)
 
     # formating callbacks
 
@@ -101,18 +108,14 @@
         return u''
 
     def box_action(self, action):
-        cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass
+        klass = getattr(action, 'html_class', lambda: None)()
         return BoxLink(action.url(), self._cw._(action.title),
-                       cls, self.boxitem_link_tooltip(action))
+                       klass, self.boxitem_link_tooltip(action))
 
 
 class RQLBoxTemplate(BoxTemplate):
     """abstract box for boxes displaying the content of a rql query not
     related to the current result set.
-
-    It rely on etype, rtype (both optional, usable to control registration
-    according to application schema and display according to connected
-    user's rights) and rql attributes
     """
 
     rql  = None
@@ -148,29 +151,17 @@
 
 class EntityBoxTemplate(BoxTemplate):
     """base class for boxes related to a single entity"""
-    __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view()
+    __select__ = BoxTemplate.__select__ & one_line_rset()
     context = 'incontext'
 
     def call(self, row=0, col=0, **kwargs):
         """classes inheriting from EntityBoxTemplate should define cell_call"""
         self.cell_call(row, col, **kwargs)
 
-
-class RelatedEntityBoxTemplate(EntityBoxTemplate):
-    __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities()
-
-    def cell_call(self, row, col, **kwargs):
-        entity = self.cw_rset.get_entity(row, col)
-        limit = self._cw.property_value('navigation.related-limit') + 1
-        role = get_role(self)
-        self.w(u'<div class="sideBox">')
-        self.wview('sidebox', entity.related(self.rtype, role, limit=limit),
-                   title=display_name(self._cw, self.rtype, role,
-                                      context=entity.__regid__))
-        self.w(u'</div>')
+from cubicweb.web.component import AjaxEditRelationCtxComponent, EditRelationMixIn
 
 
-class EditRelationBoxTemplate(ReloadableMixIn, EntityBoxTemplate):
+class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate):
     """base class for boxes which let add or remove entities linked
     by a given relation
 
@@ -181,7 +172,8 @@
     def cell_call(self, row, col, view=None, **kwargs):
         self._cw.add_js('cubicweb.ajax.js')
         entity = self.cw_rset.get_entity(row, col)
-        title = display_name(self._cw, self.rtype, get_role(self), context=entity.__regid__)
+        title = display_name(self._cw, self.rtype, get_role(self),
+                             context=entity.__regid__)
         box = SideBoxWidget(title, self.__regid__)
         related = self.related_boxitems(entity)
         unrelated = self.unrelated_boxitems(entity)
@@ -191,144 +183,13 @@
         box.extend(unrelated)
         box.render(self.w)
 
-    def div_id(self):
-        return self.__regid__
-
     def box_item(self, entity, etarget, rql, label):
-        """builds HTML link to edit relation between `entity` and `etarget`
-        """
-        role, target = get_role(self), get_target(self)
-        args = {role[0] : entity.eid, target[0] : etarget.eid}
-        url = self._cw.user_rql_callback((rql, args))
-        # for each target, provide a link to edit the relation
-        label = u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
-                                               etarget.view('incontext'))
+        label = super(EditRelationBoxTemplate, self).box_item(
+            entity, etarget, rql, label)
         return RawBoxItem(label, liclass=u'invisible')
 
-    def related_boxitems(self, entity):
-        rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
-        related = []
-        for etarget in self.related_entities(entity):
-            related.append(self.box_item(entity, etarget, rql, u'-'))
-        return related
-
-    def unrelated_boxitems(self, entity):
-        rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
-        unrelated = []
-        for etarget in self.unrelated_entities(entity):
-            unrelated.append(self.box_item(entity, etarget, rql, u'+'))
-        return unrelated
-
-    def related_entities(self, entity):
-        return entity.related(self.rtype, get_role(self), entities=True)
-
-    def unrelated_entities(self, entity):
-        """returns the list of unrelated entities, using the entity's
-        appropriate vocabulary function
-        """
-        skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self),
-                                                          entities=True))
-        skip.add(None)
-        skip.add(INTERNAL_FIELD_VALUE)
-        filteretype = getattr(self, 'etype', None)
-        entities = []
-        form = self._cw.vreg['forms'].select('edition', self._cw,
-                                             rset=self.cw_rset,
-                                             row=self.cw_row or 0)
-        field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
-        for _, eid in field.vocabulary(form):
-            if eid not in skip:
-                entity = self._cw.entity_from_eid(eid)
-                if filteretype is None or entity.__regid__ == filteretype:
-                    entities.append(entity)
-        return entities
-
 
-class AjaxEditRelationBoxTemplate(EntityBoxTemplate):
-    __select__ = EntityBoxTemplate.__select__ & partial_relation_possible()
-
-    # view used to display related entties
-    item_vid = 'incontext'
-    # values separator when multiple values are allowed
-    separator = ','
-    # msgid of the message to display when some new relation has been added/removed
-    added_msg = None
-    removed_msg = None
-
-    # class attributes below *must* be set in concret classes (additionaly to
-    # rtype / role [/ target_etype]. They should correspond to js_* methods on
-    # the json controller
-
-    # function(eid)
-    # -> expected to return a list of values to display as input selector
-    #    vocabulary
-    fname_vocabulary = None
-
-    # function(eid, value)
-    # -> handle the selector's input (eg create necessary entities and/or
-    # relations). If the relation is multiple, you'll get a list of value, else
-    # a single string value.
-    fname_validate = None
-
-    # function(eid, linked entity eid)
-    # -> remove the relation
-    fname_remove = None
+AjaxEditRelationBoxTemplate = class_renamed(
+    'AjaxEditRelationBoxTemplate', AjaxEditRelationCtxComponent,
+    '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationCtxComponent')
 
-    def cell_call(self, row, col, **kwargs):
-        req = self._cw
-        entity = self.cw_rset.get_entity(row, col)
-        related = entity.related(self.rtype, self.role)
-        rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
-        if self.role == 'subject':
-            mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid)
-            maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid)
-        else:
-            mayadd = rdef.has_perm(req, 'add', toeid=entity.eid)
-            maydel = rdef.has_perm(req, 'delete', toeid=entity.eid)
-        if not (related or mayadd):
-            return
-        if mayadd or maydel:
-            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
-        _ = req._
-        w = self.w
-        divid = domid(self.__regid__) + unicode(entity.eid)
-        w(u'<div class="sideBox" id="%s%s">' % (domid(self.__regid__), entity.eid))
-        w(u'<div class="sideBoxTitle"><span>%s</span></div>' %
-               rdef.rtype.display_name(req, self.role, context=entity.__regid__))
-        w(u'<div class="sideBox"><div class="sideBoxBody">')
-        if related:
-            w(u'<table>')
-            for rentity in related.entities():
-                # for each related entity, provide a link to remove the relation
-                subview = rentity.view(self.item_vid)
-                if maydel:
-                    jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
-                        self.__regid__, entity.eid, rentity.eid,
-                        self.fname_remove,
-                        self.removed_msg and _(self.removed_msg)))
-                    w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
-                      '<td class="tagged">%s</td></tr>' % (xml_escape(jscall),
-                                                           subview))
-                else:
-                    w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
-            w(u'</table>')
-        else:
-            w(_('no related entity'))
-        if mayadd:
-            req.add_js('jquery.autocomplete.js')
-            req.add_css('jquery.autocomplete.css')
-            multiple = rdef.role_cardinality(self.role) in '*+'
-            w(u'<table><tr><td>')
-            jscall = unicode(js.ajaxBoxShowSelector(
-                self.__regid__, entity.eid, self.fname_vocabulary,
-                self.fname_validate, self.added_msg and _(self.added_msg),
-                _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
-                multiple and self.separator))
-            w('<a class="button sglink" href="javascript: %s">%s</a>' % (
-                xml_escape(jscall),
-                multiple and _('add_relation') or _('update_relation')))
-            w(u'</td><td>')
-            w(u'<div id="%sHolder"></div>' % divid)
-            w(u'</td></tr></table>')
-        w(u'</div>\n')
-        w(u'</div></div>\n')
--- a/web/component.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/component.py	Mon Sep 13 16:47:03 2010 +0200
@@ -22,57 +22,20 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from logilab.common.deprecation import class_renamed
+from logilab.common.deprecation import class_deprecated, class_renamed
 from logilab.mtconverter import xml_escape
 
-from cubicweb import role
-from cubicweb.utils import json_dumps
-from cubicweb.view import Component
-from cubicweb.selectors import (
-    paginated_rset, one_line_rset, primary_view, match_context_prop,
-    partial_has_related_entities)
+from cubicweb import Unauthorized, role, tags
+from cubicweb.uilib import js, domid
+from cubicweb.view import ReloadableMixIn, Component
+from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset,
+                                non_final_entity, partial_relation_possible,
+                                partial_has_related_entities)
+from cubicweb.appobject import AppObject
+from cubicweb.web import htmlwidgets, stdmsgs
 
 
-class EntityVComponent(Component):
-    """abstract base class for additinal components displayed in content
-    headers and footer according to:
-
-    * the displayed entity's type
-    * a context (currently 'header' or 'footer')
-
-    it should be configured using .accepts, .etype, .rtype, .target and
-    .context class attributes
-    """
-
-    __registry__ = 'contentnavigation'
-    __select__ = one_line_rset() & primary_view() & match_context_prop()
-
-    cw_property_defs = {
-        _('visible'):  dict(type='Boolean', default=True,
-                            help=_('display the component or not')),
-        _('order'):    dict(type='Int', default=99,
-                            help=_('display order of the component')),
-        _('context'):  dict(type='String', default='navtop',
-                            vocabulary=(_('navtop'), _('navbottom'),
-                                        _('navcontenttop'), _('navcontentbottom'),
-                                        _('ctxtoolbar')),
-                            help=_('context where this component should be displayed')),
-    }
-
-    context = 'navcontentbottom'
-
-    def call(self, view=None):
-        if self.cw_rset is None:
-            self.entity_call(self.cw_extra_kwargs.pop('entity'))
-        else:
-            self.cell_call(0, 0, view=view)
-
-    def cell_call(self, row, col, view=None):
-        self.entity_call(self.cw_rset.get_entity(row, col), view=view)
-
-    def entity_call(self, entity, view=None):
-        raise NotImplementedError()
-
+# abstract base class for navigation components ################################
 
 class NavigationComponent(Component):
     """abstract base class for navigation components"""
@@ -145,10 +108,9 @@
         elif path == 'json':
             rql = params.pop('rql', self.cw_rset.printable_rql())
             # latest 'true' used for 'swap' mode
-            url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
-                json_dumps(params.get('divid', 'pageContent')),
-                json_dumps(rql), json_dumps(params.pop('vid', None)),
-                json_dumps(params))
+            url = 'javascript: %s' % (js.replacePageChunk(
+                params.get('divid', 'pageContent'), rql,
+                params.pop('vid', None), params))
         else:
             url = self._cw.build_url(path, **params)
         return url
@@ -177,6 +139,405 @@
         return self.next_page_link_templ % (url, title, content)
 
 
+# new contextual components system #############################################
+
+def override_ctx(cls, **kwargs):
+    cwpdefs = cls.cw_property_defs.copy()
+    cwpdefs['context']  = cwpdefs['context'].copy()
+    cwpdefs['context'].update(kwargs)
+    return cwpdefs
+
+
+class EmptyComponent(Exception):
+    """some selectable component has actually no content and should not be
+    rendered
+    """
+
+class Layout(Component):
+    __regid__ = 'layout'
+    __abstract__ = True
+
+    def init_rendering(self):
+        """init view for rendering. Return true if we should go on, false
+        if we should stop now.
+        """
+        view = self.cw_extra_kwargs['view']
+        try:
+            view.init_rendering()
+        except Unauthorized, ex:
+            self.warning("can't render %s: %s", view, ex)
+            return False
+        except EmptyComponent:
+            return False
+        return True
+
+
+class CtxComponent(AppObject):
+    """base class for contextual compontents. The following contexts are
+    predefined:
+
+    * boxes: 'left', 'incontext', 'right'
+    * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom'
+    * other: 'ctxtoolbar'
+
+    The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar'
+    context are handled by the default primary view, others by the default main
+    template.
+
+    All subclasses may not support all those contexts (for instance if it can't
+    be displayed as box, or as a toolbar icon). You may restrict allowed context
+    as followed:
+
+    .. sourcecode:: python
+
+      class MyComponent(CtxComponent):
+          cw_property_defs = override_ctx(CtxComponent,
+                                          vocabulary=[list of contexts])
+          context = 'my default context'
+
+    You can configure default component's context by simply giving appropriate
+    value to the `context` class attribute, as seen above.
+    """
+    __registry__ = 'ctxcomponents'
+    __select__ = ~no_cnx()
+
+    categories_in_order = ()
+    cw_property_defs = {
+        _('visible'): dict(type='Boolean', default=True,
+                           help=_('display the box or not')),
+        _('order'):   dict(type='Int', default=99,
+                           help=_('display order of the box')),
+        _('context'): dict(type='String', default='left',
+                           vocabulary=(_('left'), _('incontext'), _('right'),
+                                       _('navtop'), _('navbottom'),
+                                       _('navcontenttop'), _('navcontentbottom'),
+                                       _('ctxtoolbar')),
+                           help=_('context where this component should be displayed')),
+        }
+    context = 'left'
+    contextual = False
+    title = None
+
+    # XXX support kwargs for compat with old boxes which gets the view as
+    # argument
+    def render(self, w, **kwargs):
+        getlayout = self._cw.vreg['components'].select
+        try:
+            # XXX ensure context is given when the component is reloaded through
+            # ajax
+            context = self.cw_extra_kwargs['context']
+        except KeyError:
+            context = self.cw_propval('context')
+        layout = getlayout('layout', self._cw, rset=self.cw_rset,
+                           row=self.cw_row, col=self.cw_col,
+                           view=self, context=context)
+        layout.render(w)
+
+    def init_rendering(self):
+        """init rendering callback: that's the good time to check your component
+        has some content to display. If not, you can still raise
+        :exc:`EmptyComponent` to inform it should be skipped.
+
+        Also, :exc:`Unauthorized` will be catched, logged, then the component
+        will be skipped.
+        """
+        self.items = []
+
+    @property
+    def domid(self):
+        """return the HTML DOM identifier for this component"""
+        return domid(self.__regid__)
+
+    @property
+    def cssclass(self):
+        """return the CSS class name for this component"""
+        return domid(self.__regid__)
+
+    def render_title(self, w):
+        """return the title for this component"""
+        if self.title:
+            w(self._cw._(self.title))
+
+    def render_body(self, w):
+        """return the body (content) for this component"""
+        raise NotImplementedError()
+
+    def render_items(self, w, items=None, klass=u'boxListing'):
+        if items is None:
+            items = self.items
+        assert items
+        w(u'<ul class="%s">' % klass)
+        for item in items:
+            if hasattr(item, 'render'):
+                item.render(w) # XXX display <li> by itself
+            else:
+                w(u'<li>')
+                w(item)
+                w(u'</li>')
+        w(u'</ul>')
+
+    def append(self, item):
+        self.items.append(item)
+
+    def box_action(self, action): # XXX action_link
+        return self.build_link(self._cw._(action.title), action.url())
+
+    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"""
+    __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset()
+    context = 'incontext'
+    contextual = True
+
+    def __init__(self, *args, **kwargs):
+        super(EntityCtxComponent, self).__init__(*args, **kwargs)
+        try:
+            entity = kwargs['entity']
+        except KeyError:
+            entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        self.entity = entity
+
+    @property
+    def domid(self):
+        return domid(self.__regid__) + unicode(self.entity.eid)
+
+
+# high level abstract classes ##################################################
+
+class RQLCtxComponent(CtxComponent):
+    """abstract box for boxes displaying the content of a rql query not
+    related to the current result set.
+    """
+    rql  = None
+
+    def to_display_rql(self):
+        assert self.rql is not None, self.__regid__
+        return (self.rql,)
+
+    def init_rendering(self):
+        rset = self._cw.execute(*self.to_display_rql())
+        if not rset:
+            raise EmptyComponent()
+        if len(rset[0]) == 2:
+            self.items = []
+            for i, (eid, label) in enumerate(rset):
+                entity = rset.get_entity(i, 0)
+                self.items.append(self.build_link(label, entity.absolute_url()))
+        else:
+            self.items = [self.build_link(e.dc_title(), e.absolute_url())
+                          for e in rset.entities()]
+
+    def render_body(self, w):
+        self.render_items(w)
+
+
+class EditRelationMixIn(ReloadableMixIn):
+    def box_item(self, entity, etarget, rql, label):
+        """builds HTML link to edit relation between `entity` and `etarget`"""
+        role, target = role(self), get_target(self)
+        args = {role[0] : entity.eid, target[0] : etarget.eid}
+        url = self._cw.user_rql_callback((rql, args))
+        # for each target, provide a link to edit the relation
+        return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
+                                              etarget.view('incontext'))
+
+    def related_boxitems(self, entity):
+        rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
+        return [self.box_item(entity, etarget, rql, u'-')
+                for etarget in self.related_entities(entity)]
+
+    def related_entities(self, entity):
+        return entity.related(self.rtype, role(self), entities=True)
+
+    def unrelated_boxitems(self, entity):
+        rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
+        return [self.box_item(entity, etarget, rql, u'+')
+                for etarget in self.unrelated_entities(entity)]
+
+    def unrelated_entities(self, entity):
+        """returns the list of unrelated entities, using the entity's
+        appropriate vocabulary function
+        """
+        skip = set(unicode(e.eid) for e in entity.related(self.rtype, role(self),
+                                                          entities=True))
+        skip.add(None)
+        skip.add(INTERNAL_FIELD_VALUE)
+        filteretype = getattr(self, 'etype', None)
+        entities = []
+        form = self._cw.vreg['forms'].select('edition', self._cw,
+                                             rset=self.cw_rset,
+                                             row=self.cw_row or 0)
+        field = form.field_by_name(self.rtype, role(self), entity.e_schema)
+        for _, eid in field.vocabulary(form):
+            if eid not in skip:
+                entity = self._cw.entity_from_eid(eid)
+                if filteretype is None or entity.__regid__ == filteretype:
+                    entities.append(entity)
+        return entities
+
+
+class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent):
+    """base class for boxes which let add or remove entities linked by a given
+    relation
+
+    subclasses should define at least id, rtype and target class attributes.
+    """
+    def render_title(self, w):
+        return display_name(self._cw, self.rtype, role(self),
+                            context=self.entity.__regid__)
+
+    def render_body(self, w):
+        self._cw.add_js('cubicweb.ajax.js')
+        related = self.related_boxitems(self.entity)
+        unrelated = self.unrelated_boxitems(self.entity)
+        self.items.extend(related)
+        if related and unrelated:
+            self.items.append(htmlwidgets.BoxSeparator())
+        self.items.extend(unrelated)
+        self.render_items(w)
+
+
+class AjaxEditRelationCtxComponent(EntityCtxComponent):
+    __select__ = EntityCtxComponent.__select__ & (
+        partial_relation_possible(action='add') | partial_has_related_entities())
+
+    # view used to display related entties
+    item_vid = 'incontext'
+    # values separator when multiple values are allowed
+    separator = ','
+    # msgid of the message to display when some new relation has been added/removed
+    added_msg = None
+    removed_msg = None
+
+    # class attributes below *must* be set in concret classes (additionaly to
+    # rtype / role [/ target_etype]. They should correspond to js_* methods on
+    # the json controller
+
+    # function(eid)
+    # -> expected to return a list of values to display as input selector
+    #    vocabulary
+    fname_vocabulary = None
+
+    # function(eid, value)
+    # -> handle the selector's input (eg create necessary entities and/or
+    # relations). If the relation is multiple, you'll get a list of value, else
+    # a single string value.
+    fname_validate = None
+
+    # function(eid, linked entity eid)
+    # -> remove the relation
+    fname_remove = None
+
+    def __init__(self, *args, **kwargs):
+        super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs)
+        self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
+
+    def render_title(self, w):
+        w(self.rdef.rtype.display_name(self._cw, self.role,
+                                       context=self.entity.__regid__))
+
+    def render_body(self, w):
+        req = self._cw
+        entity = self.entity
+        related = entity.related(self.rtype, self.role)
+        if self.role == 'subject':
+            mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
+            maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid)
+        else:
+            mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
+            maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid)
+        if mayadd or maydel:
+            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
+        _ = req._
+        if related:
+            w(u'<table>')
+            for rentity in related.entities():
+                # for each related entity, provide a link to remove the relation
+                subview = rentity.view(self.item_vid)
+                if maydel:
+                    jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
+                        self.__regid__, entity.eid, rentity.eid,
+                        self.fname_remove,
+                        self.removed_msg and _(self.removed_msg)))
+                    w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
+                      '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall),
+                                                            subview))
+                else:
+                    w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
+            w(u'</table>')
+        else:
+            w(_('no related entity'))
+        if mayadd:
+            req.add_js('jquery.autocomplete.js')
+            req.add_css('jquery.autocomplete.css')
+            multiple = self.rdef.role_cardinality(self.role) in '*+'
+            w(u'<table><tr><td>')
+            jscall = unicode(js.ajaxBoxShowSelector(
+                self.__regid__, entity.eid, self.fname_vocabulary,
+                self.fname_validate, self.added_msg and _(self.added_msg),
+                _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
+                multiple and self.separator))
+            w('<a class="button sglink" href="javascript: %s">%s</a>' % (
+                xml_escape(jscall),
+                multiple and _('add_relation') or _('update_relation')))
+            w(u'</td><td>')
+            w(u'<div id="%sHolder"></div>' % self.domid)
+            w(u'</td></tr></table>')
+
+
+# old contextual components, deprecated ########################################
+
+class EntityVComponent(Component):
+    """abstract base class for additinal components displayed in content
+    headers and footer according to:
+
+    * the displayed entity's type
+    * a context (currently 'header' or 'footer')
+
+    it should be configured using .accepts, .etype, .rtype, .target and
+    .context class attributes
+    """
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)'
+
+    __registry__ = 'ctxcomponents'
+    __select__ = one_line_rset()
+
+    cw_property_defs = {
+        _('visible'):  dict(type='Boolean', default=True,
+                            help=_('display the component or not')),
+        _('order'):    dict(type='Int', default=99,
+                            help=_('display order of the component')),
+        _('context'):  dict(type='String', default='navtop',
+                            vocabulary=(_('navtop'), _('navbottom'),
+                                        _('navcontenttop'), _('navcontentbottom'),
+                                        _('ctxtoolbar')),
+                            help=_('context where this component should be displayed')),
+    }
+
+    context = 'navcontentbottom'
+
+    def call(self, view=None):
+        if self.cw_rset is None:
+            self.entity_call(self.cw_extra_kwargs.pop('entity'))
+        else:
+            self.cell_call(0, 0, view=view)
+
+    def cell_call(self, row, col, view=None):
+        self.entity_call(self.cw_rset.get_entity(row, col), view=view)
+
+    def entity_call(self, entity, view=None):
+        raise NotImplementedError()
+
+
 class RelatedObjectsVComponent(EntityVComponent):
     """a section to display some related entities"""
     __select__ = EntityVComponent.__select__ & partial_has_related_entities()
@@ -197,14 +558,15 @@
             rset = self._cw.execute(self.rql(), {'x': eid})
         if not rset.rowcount:
             return
-        self.w(u'<div class="%s">' % self.div_class())
+        self.w(u'<div class="%s">' % self.cssclass)
         self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
         self.wview(self.vid, rset)
         self.w(u'</div>')
 
 
+
 VComponent = class_renamed('VComponent', Component,
-                           'VComponent is deprecated, use Component')
+                           '[3.2] VComponent is deprecated, use Component')
 SingletonVComponent = class_renamed('SingletonVComponent', Component,
-                                    'SingletonVComponent is deprecated, use '
+                                    '[3.2] SingletonVComponent is deprecated, use '
                                     'Component and explicit registration control')
Binary file web/data/actionBoxHeader.png has changed
Binary file web/data/boxHeader.png has changed
Binary file web/data/contextFreeBoxHeader.png has changed
Binary file web/data/contextualBoxHeader.png has changed
--- a/web/data/cubicweb.calendar.css	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/data/cubicweb.calendar.css	Mon Sep 13 16:47:03 2010 +0200
@@ -230,7 +230,7 @@
 .calendar th.month {
  font-weight:bold;
  padding-bottom:0.2em;
- background: %(actionBoxTitleBgColor)s;
+ background: %(incontextBoxBodyBgColor)s;
 }
 
 .calendar th.month a{
--- a/web/data/cubicweb.css	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/data/cubicweb.css	Mon Sep 13 16:47:03 2010 +0200
@@ -31,19 +31,22 @@
 /* h3 { font-size:1.30769em; } */
 
 /* scale traditional */
-h1 { font-size: %(h1FontSize)s; }
+h1,
+.vtitle { font-size: %(h1FontSize)s; }
 h2 { font-size: %(h2FontSize)s; }
 h3 { font-size: %(h3FontSize)s; }
 
 /* paddings */
-h1 {
+h1,
+.vtitle {
   border-bottom: %(h1BorderBottomStyle)s;
   padding: %(h1Padding)s;
   margin: %(h1Margin)s;
   color: %(h1Color)s;
 }
 
-div.tabbedprimary + h1, h1.plain {
+div.tabbedprimary + h1,
+h1.plain {
  border-bottom: none;
 }
 
@@ -100,7 +103,7 @@
 }
 
 ol ol,
-ul ul{
+ul ul {
   margin-left: 8px;
   margin-bottom : 0px;
 }
@@ -113,7 +116,7 @@
   margin-left: 1.5em;
 }
 
-img{
+img {
   border: none;
 }
 
@@ -139,7 +142,7 @@
   border: 1px inset %(headerBgColor)s;
 }
 
-hr{
+hr {
   border: none;
   border-bottom: 1px solid %(defaultColor)s;
   height: 1px;
@@ -234,16 +237,11 @@
 /* Popup on login box and userActionBox */
 div.popupWrapper {
   position: relative;
-  z-index: 100;
 }
 
 div.popup {
   position: absolute;
   background: #fff;
-  /* background-color: #f0eff0; */
-  /* background-image: url(popup.png); */
-  /* background-repeat: repeat-x; */
-  /* background-positon: top left; */
   border: 1px solid %(listingBorderColor)s;
   border-top: none;
   text-align: left;
@@ -261,12 +259,13 @@
   margin: %(defaultLayoutMargin)s;
 }
 
-table#mainLayout #navColumnLeft {
+table#mainLayout td#navColumnLeft {
   width: 16em;
   padding-right: %(defaultLayoutMargin)s;
+
 }
 
-table#mainLayout #navColumnRight {
+table#mainLayout td#navColumnRight {
   width: 16em;
   padding-left: %(defaultLayoutMargin)s;
 }
@@ -301,28 +300,15 @@
   color: %(defaultColor)s;
 }
 
-/* rql bar */
-
-div#rqlinput {
-  margin-bottom: %(defaultLayoutMargin)s;
-}
-
-input#rql{
-  padding: 0.25em 0.3em;
-  width: 99%;
-}
-
-/* boxes */
+/* XXX old boxes, deprecated */
 
 div.boxFrame {
   width: 100%;
 }
 
 div.boxTitle {
-  overflow: hidden;
-  font-weight: bold;
   color: #fff;
-  background: %(boxTitleBg)s;
+  background: %(contextualBoxTitleBgColor)s;
 }
 
 div.boxTitle span,
@@ -331,14 +317,7 @@
   white-space: nowrap;
 }
 
-div.searchBoxFrame div.boxTitle,
-div.greyBoxFrame div.boxTitle {
-  background: %(actionBoxTitleBg)s;
-}
-
-div.sideBoxTitle span,
-div.searchBoxFrame div.boxTitle span,
-div.greyBoxFrame div.boxTitle span {
+div.sideBoxTitle span {
   color: %(defaultColor)s;
 }
 
@@ -352,34 +331,13 @@
   border-top: none;
 }
 
-a.boxMenu {
-  display: block;
-  padding: 1px 9px 1px 3px;
-  background: transparent %(bulletDownImg)s;
-}
-
-a.boxMenu:hover {
-  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
-  cursor: pointer;
-}
-
-a.popupMenu {
-  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
-  padding-left: 2em;
-}
-
-div.searchBoxFrame div.boxContent {
-  padding: 4px 4px 3px;
-  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
-}
-
 div.shadow{
   height: 14px;
   background: url("shadow.gif") no-repeat top right;
 }
 
 div.sideBoxTitle {
-  background: %(actionBoxTitleBg)s;
+  background: %(incontextBoxBodyBg)s;
   display: block;
   font-weight: bold;
 }
@@ -400,11 +358,11 @@
 
 div.sideBoxBody {
   padding: 0.2em 5px;
-  background: %(sideBoxBodyBg)s;
+  background: %(incontextBoxBodyBg)s;
 }
 
 div.sideBoxBody a {
-  color: %(sideBoxBodyColor)s;
+  color: %(incontextBoxBodyColor)s;
 }
 
 div.sideBoxBody a:hover {
@@ -415,6 +373,174 @@
   padding-right: 1em;
 }
 
+/* boxes */
+
+div.boxTitle {
+  overflow: hidden;
+  font-weight: bold;
+}
+
+div.boxTitle span {
+  padding: 0px 0.5em;
+  white-space: nowrap;
+}
+
+div.boxBody {
+  padding: 5px;
+  border-top: none;
+  background-color: %(leftrightBoxBodyBgColor)s;
+}
+
+div.boxBody a {
+  color: %(leftrightBoxBodyColor)s;
+}
+
+div.boxBody a:hover {
+  text-decoration: none;
+  cursor: pointer;
+  background-color: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+/* boxes contextual customization */
+
+.contextFreeBox div.boxTitle {
+  background: %(contextFreeBoxTitleBg)s;
+  color: %(contextFreeBoxTitleColor)s;
+}
+
+.contextualBox div.boxTitle {
+  background: %(contextualBoxTitleBg)s;
+  color: %(contextualBoxTitleColor)s;
+}
+
+.primaryRight div.boxTitle {
+  background: %(incontextBoxTitleBg)s;
+  color: %(incontextBoxTitleColor)s;
+}
+
+.primaryRight div.boxBody {
+  padding: 0.2em 5px;
+  background: %(incontextBoxBodyBgColor)s;
+}
+
+.primaryRight div.boxBody a {
+  color: %(incontextBoxBodyColor)s;
+}
+
+.primaryRight div.boxBody a:hover {
+  background-color: %(incontextBoxBodyHoverBgColor)s;
+}
+
+.primaryRight div.boxFooter {
+  margin-bottom: 1em;
+}
+
+#navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{
+  height: 14px;
+  background: url("shadow.gif") no-repeat top right;
+}
+
+/* boxes lists and menus */
+
+ul.boxListing {
+  margin: 0;
+  padding: 0;
+}
+
+ul.boxListing ul {
+  padding: 1px 3px;
+}
+
+ul.boxListing a {
+  color: %(defaultColor)s;
+  display: block;
+  padding: 1px 9px 1px 3px;
+}
+
+ul.boxListing li {
+  margin: 0px;
+  padding: 0px;
+  background-image: none;
+}
+
+ul.boxListing ul li {
+  margin: 0px;
+  padding-left: 8px;
+}
+
+ul.boxListing ul li a {
+  padding-left: 10px;
+  background-image: url("bullet_orange.png");
+  background-repeat: no-repeat;
+  background-position: 0 6px;
+}
+
+ul.boxListing .selected {
+  color: %(aColor)s;
+  font-weight: bold;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+a.boxMenu,
+ul.boxListing a.boxMenu{
+  display: block;
+  padding: 1px 3px;
+  background: transparent %(bulletDownImg)s;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s %(bulletDownImg)s;
+}
+
+a.boxMenu:hover {
+  cursor: pointer;
+}
+
+a.popupMenu {
+  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+  padding-left: 2em;
+}
+
+/* custom boxes */
+
+.search_box div.boxBody {
+  padding: 4px 4px 3px;
+  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
+}
+
+.bookmarks_box ul.boxListing div a{
+  background: #fff;
+  display: inline;
+  padding: 0;
+}
+.bookmarks_box ul.boxListing div a:hover{
+  border-bottom: 1px solid #000;
+}
+
+.download_box div.boxTitle {
+  background : #8fbc8f !important;
+}
+
+.download_box div.boxBody {
+  background : #eefed9;
+}
+
+/* search box and rql bar */
+
+div#rqlinput {
+  margin-bottom: %(defaultLayoutMargin)s;
+}
+
+input#rql{
+  padding: 0.25em 0.3em;
+  width: 99%;
+}
+
 input.rqlsubmit{
   display: block;
   width: 20px;
@@ -424,7 +550,7 @@
 }
 
 input#norql{
-  width:13em;
+  width:155px;
   margin-right: 2px;
 }
 
@@ -435,7 +561,7 @@
 }
 
 div#userActionsBox {
-  width: 14em;
+  width: 15em;
   text-align: right;
 }
 
@@ -445,20 +571,6 @@
   padding-right: 2em;
 }
 
-/* download box XXX move to its own file? */
-div.downloadBoxTitle{
- background : #8fbc8f;
- font-weight: bold;
-}
-
-div.downloadBox{
- font-weight: bold;
-}
-
-div.downloadBox div.sideBoxBody{
- background : #eefed9;
-}
-
 /**************/
 /* navigation */
 /**************/
@@ -566,7 +678,7 @@
 
 div#appMsg {
   margin-bottom: %(defaultLayoutMargin)s;
-  border: 1px solid %(actionBoxTitleBgColor)s;
+  border: 1px solid %(incontextBoxTitleBgColor)s;
 }
 
 .message {
@@ -579,7 +691,7 @@
   padding-left: 25px;
   background: %(msgBgColor)s url("critical.png") 2px center no-repeat;
   color: %(errorMsgColor)s;
-  border: 1px solid %(actionBoxTitleBgColor)s;
+  border: 1px solid %(incontextBoxTitleBgColor)s;
 }
 
 /* search-associate message */
@@ -734,7 +846,7 @@
 input.button{
   margin: 1em 1em 0px 0px;
   border: 1px solid %(buttonBorderColor)s;
-  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s;
 }
 
 /* FileItemInnerView  jquery.treeview.css */
@@ -754,74 +866,20 @@
 
 ul.startup li,
 ul.section li {
-  margin-left:0px
-}
-
-ul.boxListing {
-  margin: 0px;
-  padding: 0px 3px;
-}
-
-ul.boxListing li,
-ul.boxListing ul li {
-  margin: 0px;
-  padding: 0px;
-  background-image: none;
-}
-
-ul.boxListing ul {
-  padding: 1px 3px;
-}
-
-ul.boxListing a {
-  color: %(defaultColor)s;
-  display:block;
-  padding: 1px 9px 1px 3px;
-}
-
-ul.boxListing .selected {
-  color: %(aColor)s;
-  font-weight: bold;
-}
-
-ul.boxListing a.boxMenu:hover {
-  border-top: medium none;
-  background: %(sideBoxBodyBgColor)s %(bulletDownImg)s;
-}
-
-ul.boxListing a.boxBookmark {
-  padding-left: 3px;
-  background-image: none;
-  background:#fff;
+  margin-left: 0px
 }
 
 ul.simple li,
-ul.boxListing ul li ,
 .popupWrapper ul li {
   background: transparent url("bullet_orange.png") no-repeat 0% 6px;
 }
 
-ul.boxListing a.boxBookmark:hover,
-ul.boxListing a:hover,
-ul.boxListing ul li a:hover {
-  text-decoration: none;
-  background: %(sideBoxBodyBg)s;
-}
-
-ul.boxListing ul li a:hover{
-  background-color: transparent;
-}
-
-ul.boxListing ul li a {
-  padding: 1px 3px 0px 10px;
-}
-
 ul.simple li {
   padding-left: 8px;
 }
 
 .popupWrapper ul {
-  padding:0.2em 0.3em;
+  padding: 0.2em 0.3em;
   margin-bottom: 0px;
 }
 
@@ -854,20 +912,12 @@
 .validateButton {
   margin: 1em 1em 0px 0px;
   border: 1px solid %(buttonBorderColor)s;
-  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s;
   background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
 }
 
 /********************************/
-/* placement of alt. view icons */
-/********************************/
-
-.otherView {
-  float: right;
-}
-
-/********************************/
-/* rest releted classes         */
+/* rest related classes         */
 /********************************/
 
 img.align-right {
--- a/web/data/cubicweb.form.css	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/data/cubicweb.form.css	Mon Sep 13 16:47:03 2010 +0200
@@ -229,6 +229,6 @@
   margin: 1em 1em 0px 0px;
   border-width: 1px;
   border-style: solid;
-  border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+  border-color: %(buttonBorderColor)s %(incontextBoxBodyBgColor)s %(incontextBoxBodyBgColor)s %(buttonBorderColor)s;
   background: %(buttonBgColor)s %(buttonBgImg)s;
 }
--- a/web/data/cubicweb.login.css	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/data/cubicweb.login.css	Mon Sep 13 16:47:03 2010 +0200
@@ -30,7 +30,7 @@
   margin-left: -14em;
   width: 28em;
   background: #fff;
-  border: 2px solid %(actionBoxTitleBgColor)s;
+  border: 2px solid %(incontextBoxBodyBgColor)s;
   padding-bottom: 0.5em;
   text-align: center;
 }
@@ -80,7 +80,7 @@
 
 .loginButton {
   border: 1px solid #edecd2;
-  border-color: #edecd2 %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s  #edecd2;
+  border-color: #edecd2 %(incontextBoxBodyBgColor)s %(incontextBoxBodyBgColor)s  #edecd2;
   margin: 2px 0px 0px;
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
 }
--- a/web/data/cubicweb.old.css	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/data/cubicweb.old.css	Mon Sep 13 16:47:03 2010 +0200
@@ -22,7 +22,8 @@
   font-family: Verdana, sans-serif;
 }
 
-h1 {
+h1,
+.vtitle {
   font-size: 188%;
   margin: 0.2em 0px 0.3em;
   border-bottom: 1px solid #000;
@@ -243,10 +244,6 @@
 }
 
 /* Popup on login box and userActionBox */
-div.popupWrapper{
- position:relative;
- z-index:100;
-}
 
 div.popup {
   position: absolute;
@@ -303,18 +300,23 @@
 div#rqlinput {
   border: 1px solid #cfceb7;
   margin-bottom: 8px;
-  padding: 3px;
+  padding: 1px;
   background: #cfceb7;
+  width: 100%;
+}
+
+input#rql {
+  width: 99%;
 }
 
-input#rql{
-  width: 95%;
+input.rqlsubmit{
+  display: block;
+  width: 20px;
+  height: 20px;
+  background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
+  vertical-align: bottom;
 }
-
-/* boxes */
-div.navboxes {
- margin-top: 8px;
-}
+/* old boxes, deprecated */
 
 div.boxFrame {
   width: 100%;
@@ -324,25 +326,17 @@
   padding-top: 0px;
   padding-bottom: 0.2em;
   font: bold 100% Georgia;
-  overflow: hidden;
   color: #fff;
   background: #ff9900 url("search.png") left bottom repeat-x;
 }
 
-div.searchBoxFrame div.boxTitle,
-div.greyBoxFrame div.boxTitle {
-  background: #cfceb7;
-}
-
 div.boxTitle span,
 div.sideBoxTitle span {
   padding: 0px 5px;
   white-space: nowrap;
 }
 
-div.sideBoxTitle span,
-div.searchBoxFrame div.boxTitle span,
-div.greyBoxFrame div.boxTitle span {
+div.sideBoxTitle span {
   color: #222211;
 }
 
@@ -356,85 +350,6 @@
   border-top: none;
 }
 
-ul.boxListing {
-  margin: 0px;
-  padding: 0px 3px;
-}
-
-ul.boxListing li,
-ul.boxListing ul li {
-  display: inline;
-  margin: 0px;
-  padding: 0px;
-  background-image: none;
-}
-
-ul.boxListing ul {
-  margin: 0px 0px 0px 7px;
-  padding: 1px 3px;
-}
-
-ul.boxListing a {
-  color: #000;
-  display: block;
-  padding: 1px 9px 1px 3px;
-}
-
-ul.boxListing .selected {
-  color: #FF4500;
-  font-weight: bold;
-}
-
-ul.boxListing a.boxBookmark:hover,
-ul.boxListing a:hover,
-ul.boxListing ul li a:hover {
-  text-decoration: none;
-  background: #eeedd9;
-  color: #111100;
-}
-
-ul.boxListing a.boxMenu:hover {
-                                background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px;
-                                cursor:pointer;
-                                border-top:medium none;
-                                }
-a.boxMenu {
-  background: transparent url("puce_down.png") 98% 6px no-repeat;
-  display: block;
-  padding: 1px 9px 1px 3px;
-}
-
-
-a.popupMenu {
-  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
-  padding-left: 2em;
-}
-
-ul.boxListing ul li a:hover {
-  background: #eeedd9  url("bullet_orange.png") 0% 6px no-repeat;
-}
-
-a.boxMenu:hover {
-  background: #eeedd9 url("puce_down.png") 98% 6px no-repeat;
-  cursor: pointer;
-}
-
-ul.boxListing a.boxBookmark {
-  padding-left: 3px;
-  background-image:none;
-  background:#fff;
-}
-
-ul.boxListing ul li a {
-  background: #fff url("bullet_orange.png") 0% 6px no-repeat;
-  padding: 1px 3px 0px 10px;
-}
-
-div.searchBoxFrame div.boxContent {
-  padding: 4px 4px 3px;
-  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
-}
-
 div.shadow{
   height: 14px;
   background: url("shadow.gif") no-repeat top right;
@@ -474,16 +389,164 @@
   padding-right: 1em;
 }
 
-input.rqlsubmit{
-  background: #fffff8 url("go.png") 50% 50% no-repeat;
-  width: 20px;
-  height: 20px;
-  margin: 0px;
+/* boxes */
+
+div.navboxes {
+  padding-top: 0.5em;
+}
+
+div.boxTitle {
+  overflow: hidden;
+  font-weight: bold;
+}
+
+div.boxTitle span {
+  padding: 0px 0.5em;
+  white-space: nowrap;
+}
+
+div.boxBody {
+  padding: 3px 3px;
+  border-top: none;
+  background-color: %(leftrightBoxBodyBgColor)s;
+}
+
+div.boxBody a {
+  color: %(leftrightBoxBodyColor)s;
+}
+
+div.boxBody a:hover {
+  text-decoration: none;
+  cursor: pointer;
+  background-color: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+/* boxes contextual customization */
+
+.contextFreeBox div.boxTitle {
+  background: %(contextFreeBoxTitleBg)s;
+  color: %(contextFreeBoxTitleColor)s;
+}
+
+.contextualBox div.boxTitle {
+  background: %(contextualBoxTitleBg)s;
+  color: %(contextualBoxTitleColor)s;
+}
+
+.primaryRight div.boxTitle {
+  background: %(incontextBoxTitleBg)s;
+  color: %(incontextBoxTitleColor)s;
+}
+
+.primaryRight div.boxBody {
+  padding: 0.2em 5px;
+  background: %(incontextBoxBodyBgColor)s;
+}
+
+.primaryRight div.boxBody a {
+  color: %(incontextBoxBodyColor)s;
+}
+
+.primaryRight div.boxBody a:hover {
+  background-color: %(incontextBoxBodyHoverBgColor)s;
+}
+
+.primaryRight div.boxFooter {
+  margin-bottom: 1em;
+}
+
+#navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{
+  height: 14px;
+  background: url("shadow.gif") no-repeat top right;
+}
+
+/* boxes lists and menus */
+
+ul.boxListing {
+  margin: 0;
+  padding: 0;
 }
 
-input#norql{
-  width:13em;
-  margin-right: 2px;
+ul.boxListing ul {
+  padding: 1px 3px;
+}
+
+ul.boxListing a {
+  color: %(defaultColor)s;
+  display: block;
+  padding: 1px 3px;
+}
+
+ul.boxListing li {
+  margin: 0px;
+  padding: 0px;
+  background-image: none;
+}
+
+ul.boxListing ul li {
+  margin: 0px;
+  padding-left: 1em;
+}
+
+ul.boxListing ul li a {
+  padding-left: 10px;
+  background-image: url("bullet_orange.png");
+  background-repeat: no-repeat;
+  background-position: 0 6px;
+}
+
+ul.boxListing .selected {
+  color: %(aColor)s;
+  font-weight: bold;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s;
+}
+
+a.boxMenu,
+ul.boxListing a.boxMenu{
+  display: block;
+  padding: 1px 3px;
+  background: transparent %(bulletDownImg)s;
+}
+
+ul.boxListing a.boxMenu:hover {
+  border-top: medium none;
+  background: %(leftrightBoxBodyHoverBgColor)s %(bulletDownImg)s;
+}
+
+a.boxMenu:hover {
+  cursor: pointer;
+}
+
+a.popupMenu {
+  background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+  padding-left: 2em;
+}
+
+
+/* custom boxes */
+
+.search_box div.boxBody {
+  padding: 4px 4px 3px;
+  background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
+}
+
+.bookmarks_box ul.boxListing div a {
+  background: #fff;
+  display: inline;
+  padding: 0;
+}
+
+.download_box div.boxTitle {
+  background : #8fbc8f !important;
+}
+
+.download_box div.boxBody {
+  background : #eefed9;
+  vertical-align: center;
 }
 
 /* user actions menu */
@@ -503,20 +566,6 @@
   padding-right: 2em;
 }
 
-/* download box XXX move to its own file? */
-div.downloadBoxTitle{
- background : #8FBC8F;
- font-weight: bold;
-}
-
-div.downloadBox{
- font-weight: bold;
-}
-
-div.downloadBox div.sideBoxBody{
- background : #EEFED9;
-}
-
 /**************/
 /* navigation */
 /**************/
@@ -852,11 +901,3 @@
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
   background: #fffff8 url("button.png") bottom left repeat-x;
 }
-
-/********************************/
-/* placement of alt. view icons */
-/********************************/
-
-.otherView {
-  float: right;
-}
--- a/web/data/cubicweb.tableview.css	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/data/cubicweb.tableview.css	Mon Sep 13 16:47:03 2010 +0200
@@ -6,7 +6,7 @@
   font-weight: bold;
   background: #ebe8d9 url("button.png") repeat-x;
   padding: 0.3em;
-  border-bottom: 1px solid %(actionBoxTitleBgColor)s;
+  border-bottom: 1px solid %(incontextBoxBodyBgColor)s;
   text-align: left;
 }
 
Binary file web/data/incontextBoxHeader.png has changed
--- a/web/data/uiprops.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/data/uiprops.py	Mon Sep 13 16:47:03 2010 +0200
@@ -103,18 +103,36 @@
 pageContentPadding = '1em'
 pageMinHeight = '800px'
 
-# boxes
-boxTitleBg = lazystr('%(headerBgColor)s url("boxHeader.png") repeat-x 50%% 50%%')
-boxBodyBgColor = '#efefde'
+# boxes ########################################################################
+
+# title of contextFree / contextual boxes
+contextFreeBoxTitleBgColor = '#CFCEB7'
+contextFreeBoxTitleBg = lazystr('%(contextFreeBoxTitleBgColor)s url("contextFreeBoxHeader.png") repeat-x 50%% 50%%')
+contextFreeBoxTitleColor = '#000'
+
+contextualBoxTitleBgColor = '#FF9900'
+contextualBoxTitleBg = lazystr('%(contextualBoxTitleBgColor)s url("contextualBoxHeader.png") repeat-x 50%% 50%%')
+contextualBoxTitleColor = '#FFF'
 
-# action, search, sideBoxes
-actionBoxTitleBgColor = '#cfceb7'
-actionBoxTitleBg = lazystr('%(actionBoxTitleBgColor)s url("actionBoxHeader.png") repeat-x 50%% 50%%')
-sideBoxBodyBgColor = '#f8f8ee'
-sideBoxBodyBg = lazystr('%(sideBoxBodyBgColor)s')
-sideBoxBodyColor = '#555544'
+# title of 'incontext' boxes (eg displayed insinde the primary view)
+incontextBoxTitleBgColor = lazystr('%(contextFreeBoxTitleBgColor)s')
+incontextBoxTitleBg = lazystr('%(incontextBoxTitleBgColor)s url("incontextBoxHeader.png") repeat-x 50%% 50%%')
+incontextBoxTitleColor = '#000'
 
-# table listing & co
+# body of boxes in the left or right column (whatever contextFree / contextual)
+leftrightBoxBodyBgColor = '#FFF'
+leftrightBoxBodyBg = lazystr('%(leftrightBoxBodyBgColor)s')
+leftrightBoxBodyColor = '#black'
+leftrightBoxBodyHoverBgColor = '#EEEDD9'
+
+# body of 'incontext' boxes (eg displayed insinde the primary view)
+incontextBoxBodyBgColor = '#f8f8ee'
+incontextBoxBodyBg = lazystr('%(incontextBoxBodyBgColor)s')
+incontextBoxBodyColor = '#555544'
+incontextBoxBodyHoverBgColor = lazystr('%(incontextBoxBodyBgColor)s')
+
+
+# table listing & co ###########################################################
 listingBorderColor = '#ccc'
 listingHeaderBgColor = '#efefef'
 listingHihligthedBgColor = '#fbfbfb'
--- a/web/formfields.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/formfields.py	Mon Sep 13 16:47:03 2010 +0200
@@ -73,6 +73,7 @@
                               FormatConstraint)
 
 from cubicweb import Binary, tags, uilib
+from cubicweb.utils import support_args
 from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \
      formwidgets as fw, uicfg
 
@@ -332,7 +333,7 @@
         if self.eidparam and self.role is not None:
             entity = form.edited_entity
             if form._cw.vreg.schema.rschema(self.name).final:
-                if entity.has_eid() or self.name in entity:
+                if entity.has_eid() or self.name in entity.cw_attr_cache:
                     value = getattr(entity, self.name)
                     if value is not None or not self.fallback_on_none_attribute:
                         return value
@@ -345,7 +346,12 @@
     def initial_typed_value(self, form, load_bytes):
         if self.value is not _MARKER:
             if callable(self.value):
-                return self.value(form)
+                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",
+                         DeprecationWarning)
+                    return self.value(form)
             return self.value
         formattr = '%s_%s_default' % (self.role, self.name)
         if hasattr(form, formattr):
@@ -427,7 +433,7 @@
         if self.eidparam and self.role == 'subject':
             entity = form.edited_entity
             if entity.e_schema.has_metadata(self.name, 'format') and (
-                entity.has_eid() or '%s_format' % self.name in entity):
+                entity.has_eid() or '%s_format' % self.name in entity.cw_attr_cache):
                 return form.edited_entity.cw_attr_metadata(self.name, 'format')
         return form._cw.property_value('ui.default-text-format')
 
--- a/web/htmlwidgets.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/htmlwidgets.py	Mon Sep 13 16:47:03 2010 +0200
@@ -19,11 +19,10 @@
 
 those are in cubicweb since we need to know available widgets at schema
 serialization time
-
 """
 
+import random
 from math import floor
-import random
 
 from logilab.mtconverter import xml_escape
 
@@ -182,7 +181,10 @@
             toggle_action(ident), self.link_class, self.label))
         self._begin_menu(ident)
         for item in self.items:
-            item.render(self.w)
+            if hasattr(item, 'render'):
+                item.render(self.w)
+            else:
+                self.w(u'<li>%s</li>' % item)
         self._end_menu()
         if self.isitem:
             self.w(u'</li>')
--- a/web/test/unittest_session.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/test/unittest_session.py	Mon Sep 13 16:47:03 2010 +0200
@@ -7,10 +7,11 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web import InvalidSession
 
 class SessionTC(CubicWebTC):
 
-    def test_auto_reconnection(self):
+    def test_session_expiration(self):
         sm = self.app.session_handler.session_manager
         # make is if the web session has been opened by the session manager
         sm._sessions[self.cnx.sessionid] = self.websession
@@ -23,11 +24,8 @@
             # fake an incoming http query with sessionid in session cookie
             # don't use self.request() which try to call req.set_session
             req = self.requestcls(self.vreg)
-            websession = sm.get_session(req, sessionid)
-            self.assertEquals(len(sm._sessions), 1)
-            self.assertIs(websession, self.websession)
-            self.assertEquals(websession.sessionid, sessionid)
-            self.assertNotEquals(websession.sessionid, websession.cnx.sessionid)
+            self.assertRaises(InvalidSession, sm.get_session, req, sessionid)
+            self.assertEquals(len(sm._sessions), 0)
         finally:
             # avoid error in tearDown by telling this connection is closed...
             self.cnx._closed = True
--- a/web/test/unittest_views_navigation.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/test/unittest_views_navigation.py	Mon Sep 13 16:47:03 2010 +0200
@@ -122,26 +122,26 @@
     #     view = mock_object(is_primary=lambda x: True)
     #     rset = self.execute('CWUser X LIMIT 1')
     #     req = self.request()
-    #     objs = self.vreg['contentnavigation'].poss_visible_objects(
+    #     objs = self.vreg['ctxcomponents'].poss_visible_objects(
     #         req, rset=rset, view=view, context='navtop')
     #     # breadcrumbs should be in headers by default
     #     clsids = set(obj.id for obj in objs)
     #     self.failUnless('breadcrumbs' in clsids)
-    #     objs = self.vreg['contentnavigation'].poss_visible_objects(
+    #     objs = self.vreg['ctxcomponents'].poss_visible_objects(
     #         req, rset=rset, view=view, context='navbottom')
     #     # breadcrumbs should _NOT_ be in footers by default
     #     clsids = set(obj.id for obj in objs)
     #     self.failIf('breadcrumbs' in clsids)
-    #     self.execute('INSERT CWProperty P: P pkey "contentnavigation.breadcrumbs.context", '
+    #     self.execute('INSERT CWProperty P: P pkey "ctxcomponents.breadcrumbs.context", '
     #                  'P value "navbottom"')
     #     # breadcrumbs should now be in footers
     #     req.cnx.commit()
-    #     objs = self.vreg['contentnavigation'].poss_visible_objects(
+    #     objs = self.vreg['ctxcomponents'].poss_visible_objects(
     #         req, rset=rset, view=view, context='navbottom')
 
     #     clsids = [obj.id for obj in objs]
     #     self.failUnless('breadcrumbs' in clsids)
-    #     objs = self.vreg['contentnavigation'].poss_visible_objects(
+    #     objs = self.vreg['ctxcomponents'].poss_visible_objects(
     #         req, rset=rset, view=view, context='navtop')
 
     #     clsids = [obj.id for obj in objs]
--- a/web/test/unittest_viewselector.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/test/unittest_viewselector.py	Mon Sep 13 16:47:03 2010 +0200
@@ -468,18 +468,18 @@
 
     def test_properties(self):
         self.assertEquals(sorted(k for k in self.vreg['propertydefs'].keys()
-                                 if k.startswith('boxes.edit_box')),
-                          ['boxes.edit_box.context',
-                           'boxes.edit_box.order',
-                           'boxes.edit_box.visible'])
+                                 if k.startswith('ctxcomponents.edit_box')),
+                          ['ctxcomponents.edit_box.context',
+                           'ctxcomponents.edit_box.order',
+                           'ctxcomponents.edit_box.visible'])
         self.assertEquals([k for k in self.vreg['propertyvalues'].keys()
                            if not k.startswith('system.version')],
                           [])
-        self.assertEquals(self.vreg.property_value('boxes.edit_box.visible'), True)
-        self.assertEquals(self.vreg.property_value('boxes.edit_box.order'), 2)
-        self.assertEquals(self.vreg.property_value('boxes.possible_views_box.visible'), False)
-        self.assertEquals(self.vreg.property_value('boxes.possible_views_box.order'), 10)
-        self.assertRaises(UnknownProperty, self.vreg.property_value, 'boxes.actions_box')
+        self.assertEquals(self.vreg.property_value('ctxcomponents.edit_box.visible'), True)
+        self.assertEquals(self.vreg.property_value('ctxcomponents.edit_box.order'), 2)
+        self.assertEquals(self.vreg.property_value('ctxcomponents.possible_views_box.visible'), False)
+        self.assertEquals(self.vreg.property_value('ctxcomponents.possible_views_box.order'), 10)
+        self.assertRaises(UnknownProperty, self.vreg.property_value, 'ctxcomponents.actions_box')
 
 
 
--- a/web/uicfg.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/uicfg.py	Mon Sep 13 16:47:03 2010 +0200
@@ -107,11 +107,8 @@
 def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role):
     if role == 'subject':
         oschema = '*'
-        label = rschema.type
     else:
         sschema = '*'
-        label = '%s_%s' % (rschema, role)
-    rtag.setdefault((sschema, rschema, oschema, role), 'label', label)
     rtag.counter += 1
     rtag.setdefault((sschema, rschema, oschema, role), 'order', rtag.counter)
 
--- a/web/views/ajaxedit.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/ajaxedit.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,21 +15,19 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Set of views allowing edition of entities/relations using ajax
+"""Set of views allowing edition of entities/relations using ajax"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb import role
+from cubicweb.view import View
 from cubicweb.selectors import match_form_params, match_kwargs
-from cubicweb.web.box import EditRelationBoxTemplate
+from cubicweb.web.component import EditRelationMixIn
 
-class AddRelationView(EditRelationBoxTemplate):
-    """base class for view which let add entities linked
-    by a given relation
+class AddRelationView(EditRelationMixIn, View):
+    """base class for view which let add entities linked by a given relation
 
-    subclasses should define at least id, rtype and target
-    class attributes.
+    subclasses should define at least id, rtype and target class attributes.
     """
     __registry__ = 'views'
     __regid__ = 'xaddrelation'
@@ -38,7 +36,7 @@
     cw_property_defs = {} # don't want to inherit this from Box
     expected_kwargs = form_params = ('rtype', 'target')
 
-    build_js = EditRelationBoxTemplate.build_reload_js_call
+    build_js = 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']
@@ -53,13 +51,13 @@
                 etypes = rschema.subjects(entity.e_schema)
             if len(etypes) == 1:
                 self.etype = etypes[0]
-        self.w(u'<div id="%s">' % self.__regid__)
+        self.w(u'<div id="%s">' % self.domid)
         self.w(u'<h1>%s</h1>' % self._cw._('relation %(relname)s of %(ent)s')
                % {'relname': rschema.display_name(self._cw, role(self)),
                   'ent': entity.view('incontext')})
         self.w(u'<ul>')
         for boxitem in self.unrelated_boxitems(entity):
-            boxitem.render(self.w)
+            self.w('<li class="invisible">%s</li>' % botitem)
         self.w(u'</ul></div>')
 
     def unrelated_entities(self, entity):
@@ -74,11 +72,4 @@
                                     ordermethod='fetch_order')
             self.pagination(self._cw, rset, w=self.w)
             return rset.entities()
-        # in other cases, use vocabulary functions
-        entities = []
-        # XXX to update for 3.2
-        for _, eid in entity.vocabulary(self.rtype, role(self)):
-            if eid is not None:
-                rset = self._cw.eid_rset(eid)
-                entities.append(rset.get_entity(0, 0))
-        return entities
+        super(AddRelationView, self).unrelated_entities(self)
--- a/web/views/authentication.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/authentication.py	Mon Sep 13 16:47:03 2010 +0200
@@ -74,7 +74,7 @@
         self.repo = vreg.config.repository(vreg)
         self.log_queries = vreg.config['query-log-file']
         self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg),
-                                    key=lambda x: x.order)
+                                         key=lambda x: x.order)
         # 2-uple login / password, login is None when no anonymous access
         # configured
         self.anoninfo = vreg.config.anonymous_user()
@@ -98,25 +98,11 @@
         if login and session.login != login:
             raise InvalidSession('login mismatch')
         try:
-            lock = session.reconnection_lock
-        except AttributeError:
-            lock = session.reconnection_lock = Lock()
-        # need to be locked two avoid duplicated reconnections on concurrent
-        # requests
-        with lock:
-            cnx = session.cnx
-            try:
-                # calling cnx.user() check connection validity, raise
-                # BadConnectionId on failure
-                user = cnx.user(req)
-            except BadConnectionId:
-                # check if a connection should be automatically restablished
-                if (login is None or login == session.login):
-                    cnx = self._authenticate(session.login, session.authinfo)
-                    user = cnx.user(req)
-                    session.cnx = cnx
-                else:
-                    raise InvalidSession('bad connection id')
+            # calling cnx.user() check connection validity, raise
+            # BadConnectionId on failure
+            user = session.cnx.user(req)
+        except BadConnectionId:
+            raise InvalidSession('bad connection id')
         return user
 
     def authenticate(self, req):
--- a/web/views/basecomponents.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/basecomponents.py	Mon Sep 13 16:47:03 2010 +0200
@@ -20,6 +20,7 @@
 * the rql input form
 * the logged user link
 """
+from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
 _ = unicode
@@ -27,9 +28,11 @@
 from logilab.mtconverter import xml_escape
 from rql import parse
 
-from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params,
+from cubicweb.selectors import (yes, multi_etypes_rset,
+                                match_form_params, match_context,
                                 anonymous_user, authenticated_user)
 from cubicweb.schema import display_name
+from cubicweb.utils import wrap_on_write
 from cubicweb.uilib import toggle_action
 from cubicweb.web import component
 from cubicweb.web.htmlwidgets import (MenuWidget, PopupBoxMenu, BoxSeparator,
@@ -148,8 +151,7 @@
         self.w(u'<div id="appMsg" onclick="%s" class="%s">\n' %
                (toggle_action('appMsg'), (msgs and ' ' or 'hidden')))
         for msg in msgs:
-            self.w(u'<div class="message" id="%s">%s</div>' % (
-                self.div_id(), msg))
+            self.w(u'<div class="message" id="%s">%s</div>' % (self.domid, msg))
         self.w(u'</div>')
 
 
@@ -167,18 +169,6 @@
                 self._cw.base_url(), xml_escape(title)))
 
 
-class SeeAlsoVComponent(component.RelatedObjectsVComponent):
-    """display any entity's see also"""
-    __regid__ = 'seealso'
-    context = 'navcontentbottom'
-    rtype = 'see_also'
-    role = 'subject'
-    order = 40
-    # register msg not generated since no entity use see_also in cubicweb itself
-    title = _('contentnavigation_seealso')
-    help = _('contentnavigation_seealso_description')
-
-
 class EtypeRestrictionComponent(component.Component):
     """displays the list of entity types contained in the resultset
     to be able to filter accordingly.
@@ -230,17 +220,46 @@
         self.w(u'&#160;|&#160;'.join(html))
         self.w(u'</div>')
 
+# contextual components ########################################################
 
-class MetaDataComponent(component.EntityVComponent):
+# class SeeAlsoVComponent(component.RelatedObjectsVComponent):
+#     """display any entity's see also"""
+#     __regid__ = 'seealso'
+#     context = 'navcontentbottom'
+#     rtype = 'see_also'
+#     role = 'subject'
+#     order = 40
+#     # register msg not generated since no entity use see_also in cubicweb itself
+#     title = _('ctxcomponents_seealso')
+#     help = _('ctxcomponents_seealso_description')
+
+
+class MetaDataComponent(component.EntityCtxComponent):
     __regid__ = 'metadata'
     context = 'navbottom'
     order = 1
 
-    def cell_call(self, row, col, view=None):
-        self.wview('metadata', self.cw_rset, row=row, col=col)
+    def render_body(self, w):
+        self.entity.view('metadata', w=w)
 
 
-def registration_callback(vreg):
-    vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
-    if 'see_also' in vreg.schema:
-        vreg.register(SeeAlsoVComponent)
+class SectionLayout(component.Layout):
+    __select__ = match_context('navtop', 'navbottom',
+                               'navcontenttop', 'navcontentbottom')
+    cssclass = 'section'
+
+    def render(self, w):
+        if self.init_rendering():
+            view = self.cw_extra_kwargs['view']
+            w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass,
+                                                view.domid))
+            with wrap_on_write(w, '<h4>') as wow:
+                view.render_title(wow)
+            view.render_body(w)
+            w(u'</div>\n')
+
+
+# def registration_callback(vreg):
+#     vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
+#     if 'see_also' in vreg.schema:
+#         vreg.register(SeeAlsoVComponent)
--- a/web/views/basecontrollers.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/basecontrollers.py	Mon Sep 13 16:47:03 2010 +0200
@@ -26,7 +26,7 @@
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, typed_eid)
-from cubicweb.utils import json, json_dumps
+from cubicweb.utils import UStringIO, json, json_dumps
 from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
 from cubicweb.mail import format_mail
 from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse
@@ -346,14 +346,19 @@
     def _call_view(self, view, paginate=False, **kwargs):
         divid = self._cw.form.get('divid', 'pageContent')
         # we need to call pagination before with the stream set
-        stream = view.set_stream()
+        try:
+            stream = view.set_stream()
+        except AttributeError:
+            stream = UStringIO()
+            kwargs['w'] = stream.write
+            assert not paginate
         if paginate:
             if divid == 'pageContent':
                 # mimick main template behaviour
                 stream.write(u'<div id="pageContent">')
                 vtitle = self._cw.form.get('vtitle')
                 if vtitle:
-                    stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+                    stream.write(u'<div class="vtitle">%s</div>\n' % vtitle)
             view.paginate()
             if divid == 'pageContent':
                 stream.write(u'<div id="contentmain">')
--- a/web/views/basetemplates.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/basetemplates.py	Mon Sep 13 16:47:03 2010 +0200
@@ -127,7 +127,7 @@
         w(u'<div id="pageContent">\n')
         vtitle = self._cw.form.get('vtitle')
         if vtitle:
-            w(u'<h1 class="vtitle">%s</h1>\n' % xml_escape(vtitle))
+            w(u'<div class="vtitle">%s</div>\n' % xml_escape(vtitle))
         # display entity type restriction component
         etypefilter = self._cw.vreg['components'].select_or_none(
             'etypenavigation', self._cw, rset=self.cw_rset)
@@ -188,9 +188,10 @@
         self.w(u'</body>')
 
     def nav_column(self, view, context):
-        boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
+        boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context=context))
         if boxes:
+            getlayout = self._cw.vreg['components'].select
             self.w(u'<td id="navColumn%s"><div class="navboxes">\n' % context.capitalize())
             for box in boxes:
                 box.render(w=self.w, view=view)
@@ -257,7 +258,7 @@
         w(u'<table width="100%" height="100%" border="0"><tr>\n')
         w(u'<td id="navColumnLeft">\n')
         self.topleft_header()
-        boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
+        boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context='left'))
         if boxes:
             w(u'<div class="navboxes">\n')
@@ -269,7 +270,7 @@
         w(u'<div id="pageContent">\n')
         vtitle = self._cw.form.get('vtitle')
         if vtitle:
-            w(u'<h1 class="vtitle">%s</h1>' % xml_escape(vtitle))
+            w(u'<div class="vtitle">%s</div>' % xml_escape(vtitle))
 
     def topleft_header(self):
         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
@@ -392,10 +393,8 @@
                                                             rset=self.cw_rset)
         footeractions = actions.get('footer', ())
         for i, action in enumerate(footeractions):
-            self.w(u'<a href="%s"' % action.url())
-            if getattr(action, 'html_class'):
-                self.w(u' class="%s"' % action.html_class())
-            self.w(u'>%s</a>' % self._cw._(action.title))
+            self.w(u'<a href="%s">%s</a>' % (action.url(),
+                                             self._cw._(action.title)))
             if i < (len(footeractions) - 1):
                 self.w(u' | ')
         self.w(u'</div>')
@@ -410,7 +409,7 @@
 
     def call(self, view, **kwargs):
         """by default, display informal messages in content header"""
-        components = self._cw.vreg['contentnavigation'].poss_visible_objects(
+        components = self._cw.vreg['ctxcomponents'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context='navtop')
         if components:
             self.w(u'<div id="contentheader">')
@@ -426,7 +425,7 @@
     __regid__ = 'contentfooter'
 
     def call(self, view, **kwargs):
-        components = self._cw.vreg['contentnavigation'].poss_visible_objects(
+        components = self._cw.vreg['ctxcomponents'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context='navbottom')
         if components:
             self.w(u'<div id="contentfooter">')
--- a/web/views/bookmark.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/bookmark.py	Mon Sep 13 16:47:03 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Primary view for bookmarks + user's bookmarks box
+"""Primary view for bookmarks + user's bookmarks box"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
@@ -25,7 +24,7 @@
 from cubicweb import Unauthorized
 from cubicweb.selectors import is_instance, one_line_rset
 from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, RawBoxItem
-from cubicweb.web import action, box, uicfg, formwidgets as fw
+from cubicweb.web import action, component, uicfg, formwidgets as fw
 from cubicweb.web.views import primary
 
 _abaa = uicfg.actionbox_appearsin_addmenu
@@ -70,58 +69,55 @@
         self.w(u'</div>')
 
 
-class BookmarksBox(box.UserRQLBoxTemplate):
+class BookmarksBox(component.CtxComponent):
     """display a box containing all user's bookmarks"""
     __regid__ = 'bookmarks_box'
+
+    title = _('bookmarks')
     order = 40
-    title = _('bookmarks')
     rql = ('Any B,T,P ORDERBY lower(T) '
            'WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, '
            'U eid %(x)s')
-    etype = 'Bookmark'
-    rtype = 'bookmarked_by'
 
+    def init_rendering(self):
+        ueid = self._cw.user.eid
+        self.bookmarks_rset = self._cw.execute(self.rql, {'x': ueid})
+        rschema = self._cw.vreg.schema.rschema('bookmarked_by')
+        eschema = self._cw.vreg.schema.eschema('Bookmark')
+        self.can_delete = rschema.has_perm(self._cw, 'delete', toeid=ueid)
+        self.can_edit = (eschema.has_perm(self._cw, 'add') and
+                         rschema.has_perm(self._cw, 'add', toeid=ueid))
+        if not self.bookmarks_rset and not self.can_edit:
+            raise component.EmptyComponent()
+        self.items = []
 
-    def call(self, **kwargs):
+    def render_body(self, w):
+        ueid = self._cw.user.eid
         req = self._cw
-        ueid = req.user.eid
-        try:
-            rset = req.execute(self.rql, {'x': ueid})
-        except Unauthorized:
-            # can't access to something in the query, forget this box
-            return
-        box = BoxWidget(req._(self.title), self.__regid__)
-        box.listing_class = 'sideBox'
-        rschema = self._cw.vreg.schema.rschema(self.rtype)
-        eschema = self._cw.vreg.schema.eschema(self.etype)
-        candelete = rschema.has_perm(req, 'delete', toeid=ueid)
-        if candelete:
+        if self.can_delete:
             req.add_js('cubicweb.ajax.js')
-        else:
-            dlink = None
-        for bookmark in rset.entities():
-            label = '<a href="%s">%s</a>' % (xml_escape(bookmark.action_url()),
-                                             xml_escape(bookmark.title))
-            if candelete:
+        for bookmark in self.bookmarks_rset.entities():
+            label = self.build_link(bookmark.title, bookmark.action_url())
+            if self.can_delete:
                 dlink = u'[<a href="javascript:removeBookmark(%s)" title="%s">-</a>]' % (
                     bookmark.eid, _('delete this bookmark'))
-                label = '%s %s' % (dlink, label)
-            box.append(RawBoxItem(label))
-        if eschema.has_perm(req, 'add') and rschema.has_perm(req, 'add', toeid=ueid):
-            boxmenu = BoxMenu(req._('manage bookmarks'))
+                label = '<div>%s %s</div>' % (dlink, label)
+            self.append(label)
+        if self.can_edit:
+            menu = BoxMenu(req._('manage bookmarks'))
             linkto = 'bookmarked_by:%s:subject' % ueid
             # use a relative path so that we can move the instance without
             # loosing bookmarks
             path = req.relative_path()
-            # XXX if vtitle specified in params, extract it and use it as default value
-            # for bookmark's title
-            url = self.create_url(self.etype, __linkto=linkto, path=path)
-            boxmenu.append(self.mk_action(req._('bookmark this page'), url,
-                                          category='manage', id='bookmark'))
-            if rset:
+            # XXX if vtitle specified in params, extract it and use it as
+            # default value for bookmark's title
+            url = req.vreg['etypes'].etype_class('Bookmark').cw_create_url(
+                req, __linkto=linkto, path=path)
+            menu.append(self.build_link(req._('bookmark this page'), url))
+            if self.bookmarks_rset:
                 if req.user.is_in_group('managers'):
                     bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, U eid %s' % ueid
-                    erset = rset
+                    erset = self.bookmarks_rset
                 else:
                     # we can't edit shared bookmarks we don't own
                     bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, B owned_by U, U eid %(x)s'
@@ -129,11 +125,10 @@
                                         build_descr=False)
                     bookmarksrql %= {'x': ueid}
                 if erset:
-                    url = self._cw.build_url(vid='muledit', rql=bookmarksrql)
-                    boxmenu.append(self.mk_action(self._cw._('edit bookmarks'), url, category='manage'))
+                    url = req.build_url(vid='muledit', rql=bookmarksrql)
+                    menu.append(self.build_link(req._('edit bookmarks'), url))
             url = req.user.absolute_url(vid='xaddrelation', rtype='bookmarked_by',
                                         target='subject')
-            boxmenu.append(self.mk_action(self._cw._('pick existing bookmarks'), url, category='manage'))
-            box.append(boxmenu)
-        if not box.is_empty():
-            box.render(self.w)
+            menu.append(self.build_link(req._('pick existing bookmarks'), url))
+            self.append(menu)
+        self.render_items(w)
--- a/web/views/boxes.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/boxes.py	Mon Sep 13 16:47:03 2010 +0200
@@ -18,12 +18,14 @@
 """Generic boxes for CubicWeb web client:
 
 * actions box
-* possible views box
+* search box
 
-additional (disabled by default) boxes
+Additional boxes (disabled by default):
 * schema box
+* possible views box
 * startup views box
 """
+from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
 _ = unicode
@@ -31,42 +33,43 @@
 from warnings import warn
 
 from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated
 
-from cubicweb.selectors import match_user_groups, non_final_entity
+from cubicweb import Unauthorized
+from cubicweb.selectors import (match_user_groups, match_kwargs,
+                                non_final_entity, nonempty_rset,
+                                match_context, contextual)
+from cubicweb.utils import wrap_on_write
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
-from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, BoxHtml, RawBoxItem
-from cubicweb.web.box import BoxTemplate
+from cubicweb.web import component, box, htmlwidgets
 
+# XXX bw compat, some cubes import this class from here
+BoxTemplate = box.BoxTemplate
+BoxHtml = htmlwidgets.BoxHtml
 
-class EditBox(BoxTemplate): # XXX rename to ActionsBox
+class EditBox(component.CtxComponent): # XXX rename to ActionsBox
     """
     box with all actions impacting the entity displayed: edit, copy, delete
     change state, add related entities
     """
     __regid__ = 'edit_box'
-    __select__ = BoxTemplate.__select__ & non_final_entity()
+    __select__ = component.CtxComponent.__select__ & non_final_entity()
 
     title = _('actions')
     order = 2
+    contextual = True
 
-    def call(self, view=None, **kwargs):
+    def init_rendering(self):
+        super(EditBox, self).init_rendering()
         _ = self._cw._
-        title = _(self.title)
-        if self.cw_rset:
-            etypes = self.cw_rset.column_types(0)
-            if len(etypes) == 1:
-                plural = self.cw_rset.rowcount > 1 and 'plural' or ''
-                etypelabel = display_name(self._cw, iter(etypes).next(), plural)
-                title = u'%s - %s' % (title, etypelabel.lower())
-        box = BoxWidget(title, self.__regid__, _class="greyBoxFrame")
         self._menus_in_order = []
         self._menus_by_id = {}
         # build list of actions
         actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
-                                                            view=view)
+                                                            **self.cw_extra_kwargs)
         other_menu = self._get_menu('moreactions', _('more actions'))
-        for category, defaultmenu in (('mainactions', box),
+        for category, defaultmenu in (('mainactions', self),
                                       ('moreactions', other_menu),
                                       ('addrelated', None)):
             for action in actions.get(category, ()):
@@ -81,16 +84,28 @@
                     menu = defaultmenu
                 action.fill_menu(self, menu)
         # if we've nothing but actions in the other_menu, add them directly into the box
-        if box.is_empty() and len(self._menus_by_id) == 1 and not other_menu.is_empty():
-            box.items = other_menu.items
-            other_menu.items = []
+        if not self.items and len(self._menus_by_id) == 1 and not other_menu.is_empty():
+            self.items = other_menu.items
         else: # ensure 'more actions' menu appears last
             self._menus_in_order.remove(other_menu)
             self._menus_in_order.append(other_menu)
-        for submenu in self._menus_in_order:
-            self.add_submenu(box, submenu)
-        if not box.is_empty():
-            box.render(self.w)
+            for submenu in self._menus_in_order:
+                self.add_submenu(self, submenu)
+        if not self.items:
+            raise component.EmptyComponent()
+
+    def render_title(self, w):
+        title = self._cw._(self.title)
+        if self.cw_rset:
+            etypes = self.cw_rset.column_types(0)
+            if len(etypes) == 1:
+                plural = self.cw_rset.rowcount > 1 and 'plural' or ''
+                etypelabel = display_name(self._cw, iter(etypes).next(), plural)
+                title = u'%s - %s' % (title, etypelabel.lower())
+        w(title)
+
+    def render_body(self, w):
+        self.render_items(w)
 
     def _get_menu(self, id, title=None, label_prefix=None):
         try:
@@ -98,7 +113,7 @@
         except KeyError:
             if title is None:
                 title = self._cw._(id)
-            self._menus_by_id[id] = menu = BoxMenu(title)
+            self._menus_by_id[id] = menu = htmlwidgets.BoxMenu(title)
             menu.label_prefix = label_prefix
             self._menus_in_order.append(menu)
             return menu
@@ -108,19 +123,22 @@
         if len(submenu.items) == 1 and not appendanyway:
             boxlink = submenu.items[0]
             if submenu.label_prefix:
-                boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
+                # XXX iirk
+                if hasattr(boxlink, 'label'):
+                    boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
+                else:
+                    submenu.items[0] = u'%s %s' % (submenu.label_prefix, boxlink)
             box.append(boxlink)
         elif submenu.items:
             box.append(submenu)
         elif appendanyway:
-            box.append(RawBoxItem(xml_escape(submenu.label)))
+            box.append(xml_escape(submenu.label))
 
 
-class SearchBox(BoxTemplate):
+class SearchBox(component.CtxComponent):
     """display a box with a simple search form"""
     __regid__ = 'search_box'
 
-    visible = True # enabled by default
     title = _('search')
     order = 0
     formdef = u"""<form action="%s">
@@ -130,74 +148,119 @@
 <input type="hidden" name="subvid" value="tsearch" />
 </td><td>
 <input tabindex="%s" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
-</td></tr></table>
-</form>"""
+ </td></tr></table>
+ </form>"""
 
-    def call(self, view=None, **kwargs):
-        req = self._cw
-        if req.form.pop('__fromsearchbox', None):
-            rql = req.form.get('rql', '')
+    def render_title(self, w):
+        w(u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>"""
+          % self._cw._(self.title))
+
+    def render_body(self, w):
+        if self._cw.form.pop('__fromsearchbox', None):
+            rql = self._cw.form.get('rql', '')
         else:
             rql = ''
-        form = self.formdef % (req.build_url('view'), req.next_tabindex(),
-                               xml_escape(rql), req.next_tabindex())
-        title = u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>""" % req._(self.title)
-        box = BoxWidget(title, self.__regid__, _class="searchBoxFrame", islist=False, escape=False)
-        box.append(BoxHtml(form))
-        box.render(self.w)
+        w(self.formdef % (self._cw.build_url('view'), self._cw.next_tabindex(),
+                          xml_escape(rql), self._cw.next_tabindex()))
 
 
 # boxes disabled by default ###################################################
 
-class PossibleViewsBox(BoxTemplate):
+class PossibleViewsBox(component.CtxComponent):
     """display a box containing links to all possible views"""
     __regid__ = 'possible_views_box'
-    __select__ = BoxTemplate.__select__ & match_user_groups('users', 'managers')
 
     visible = False
     title = _('possible views')
     order = 10
 
-    def call(self, **kwargs):
-        box = BoxWidget(self._cw._(self.title), self.__regid__)
-        views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
-                                                              rset=self.cw_rset)
-                 if v.category != 'startupview']
-        for category, views in self.sort_actions(views):
-            menu = BoxMenu(category)
+    def init_rendering(self):
+        self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
+                                                                       rset=self.cw_rset)
+                      if v.category != 'startupview']
+        if not self.views:
+            raise component.EmptyComponent()
+        self.items = []
+
+    def render_body(self, w):
+        for category, views in box.sort_by_category(self.views):
+            menu = htmlwidgets.BoxMenu(category)
             for view in views:
                 menu.append(self.box_action(view))
-            box.append(menu)
-        if not box.is_empty():
-            box.render(self.w)
+            self.append(menu)
+        self.render_items(w)
 
 
-class StartupViewsBox(BoxTemplate):
+class StartupViewsBox(PossibleViewsBox):
     """display a box containing links to all startup views"""
     __regid__ = 'startup_views_box'
+
     visible = False # disabled by default
     title = _('startup views')
     order = 70
 
-    def call(self, **kwargs):
-        box = BoxWidget(self._cw._(self.title), self.__regid__)
-        for view in self._cw.vreg['views'].possible_views(self._cw, None):
-            if view.category == 'startupview':
-                box.append(self.box_action(view))
-        if not box.is_empty():
-            box.render(self.w)
+    def init_rendering(self):
+        self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw)
+                      if v.category == 'startupview']
+        if not self.views:
+            raise component.EmptyComponent()
+        self.items = []
 
 
-# helper classes ##############################################################
+class RsetBox(component.CtxComponent):
+    """helper view class to display an rset in a sidebox"""
+    __select__ = nonempty_rset() & match_kwargs('title', 'vid')
+    __regid__ = 'rsetbox'
+    cw_property_defs = {}
+    context = 'incontext'
+
+    @property
+    def domid(self):
+        return super(RsetBox, self).domid + unicode(abs(id(self)))
+
+    def render_title(self, w):
+        w(self.cw_extra_kwargs['title'])
+
+    def render_body(self, w):
+        self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w)
+
+ # helper classes ##############################################################
 
 class SideBoxView(EntityView):
     """helper view class to display some entities in a sidebox"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] SideBoxView is deprecated, use RsetBox instead (%(cls)s)'
+
     __regid__ = 'sidebox'
 
-    def call(self, boxclass='sideBox', title=u''):
+    def call(self, **kwargs):
         """display a list of entities by calling their <item_vid> view"""
-        if title:
-            self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title)
-        self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass)
-        self.wview('autolimited', self.cw_rset, **self.cw_extra_kwargs)
-        self.w(u'</div>\n</div>\n')
+        box = self._cw.vreg['ctxcomponents'].select(
+            'rsetbox', self._cw, rset=self.cw_rset, vid='autolimited',
+            title=title, **self.cw_extra_kwargs)
+        box.render(self.w)
+
+
+class ContextualBoxLayout(component.Layout):
+    __select__ = match_context('incontext', 'left', 'right') & contextual()
+    # predefined class in cubicweb.css: contextualBox | contextFreeBox
+    # XXX: navigationBox | actionBox
+    cssclass = 'contextualBox'
+
+    def render(self, w):
+        if self.init_rendering():
+            view = self.cw_extra_kwargs['view']
+            w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass,
+                                                view.domid))
+            with wrap_on_write(w, u'<div class="boxTitle"><span>',
+                               u'</span></div>') as wow:
+                view.render_title(wow)
+            w(u'<div class="boxBody">')
+            view.render_body(w)
+            # boxFooter div is a CSS place holder (for shadow for example)
+            w(u'</div><div class="boxFooter"></div></div>\n')
+
+
+class ContextFreeBoxLayout(ContextualBoxLayout):
+    __select__ = match_context('incontext', 'left', 'right') & ~contextual()
+    cssclass = 'contextFreeBox'
--- a/web/views/cwproperties.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/cwproperties.py	Mon Sep 13 16:47:03 2010 +0200
@@ -45,7 +45,7 @@
 _('ui')
 _('boxes')
 _('components')
-_('contentnavigation')
+_('ctxcomponents')
 _('navigation.combobox-limit')
 _('navigation.page-size')
 _('navigation.related-limit')
@@ -200,8 +200,8 @@
         else:
             entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
             entity.eid = self._cw.varmaker.next()
-            entity['pkey'] = key
-            entity['value'] = self._cw.vreg.property_value(key)
+            entity.cw_attr_cache['pkey'] = key
+            entity.cw_attr_cache['value'] = self._cw.vreg.property_value(key)
         return entity
 
     def form(self, formid, keys, splitlabel=False):
@@ -329,7 +329,7 @@
 
     def form_init(self, form):
         entity = form.edited_entity
-        if not (entity.has_eid() or 'pkey' in entity):
+        if not (entity.has_eid() or 'pkey' in entity.cw_attr_cache):
             # no key set yet, just include an empty div which will be filled
             # on key selection
             return
@@ -353,7 +353,7 @@
         if vocab is not None:
             if callable(vocab):
                 # list() just in case its a generator function
-                self.choices = list(vocab(form._cw))
+                self.choices = list(vocab())
             else:
                 self.choices = vocab
             wdg = Select()
--- a/web/views/debug.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/debug.py	Mon Sep 13 16:47:03 2010 +0200
@@ -119,10 +119,15 @@
             if sessions:
                 w(u'<ul>')
                 for session in sessions:
+                    try:
+                        last_usage_time = session.cnx.check()
+                    except BadConnectionId:
+                        w(u'<li>%s (INVALID)</li>' % session.sessionid)
+                        continue
                     w(u'<li>%s (%s: %s)<br/>' % (
                         session.sessionid,
                         _('last usage'),
-                        strftime(dtformat, localtime(session.last_usage_time))))
+                        strftime(dtformat, localtime(last_usage_time))))
                     dict_to_html(w, session.data)
                     w(u'</li>')
                 w(u'</ul>')
@@ -145,6 +150,8 @@
         self.w(u'<p>%s</p>\n' % ' - '.join('<a href="%s#%s">%s</a>'
                                            % (url, key, key) for key in keys))
         for key in keys:
+            if key in ('boxes', 'contentnavigation'): # those are bw compat registries
+                continue
             self.w(u'<h2 id="%s">%s</h2>' % (key, key))
             if self._cw.vreg[key]:
                 values = sorted(self._cw.vreg[key].iteritems())
--- a/web/views/editcontroller.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/editcontroller.py	Mon Sep 13 16:47:03 2010 +0200
@@ -115,7 +115,7 @@
         form = req.form
         # so we're able to know the main entity from the repository side
         if '__maineid' in form:
-            req.set_shared_data('__maineid', form['__maineid'], querydata=True)
+            req.set_shared_data('__maineid', form['__maineid'], txdata=True)
         # no specific action, generic edition
         self._to_create = req.data['eidmap'] = {}
         self._pending_fields = req.data['pendingfields'] = set()
--- a/web/views/editforms.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/editforms.py	Mon Sep 13 16:47:03 2010 +0200
@@ -169,7 +169,9 @@
 
     def url(self):
         """return the url associated with this view"""
-        return self.create_url(self._cw.form.get('etype'))
+        req = self._cw
+        return req.vreg["etypes"].etype_class(req.form['etype']).cw_create_url(
+            req)
 
     def submited_message(self):
         """return the message that will be displayed on successful edition"""
--- a/web/views/facets.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/facets.py	Mon Sep 13 16:47:03 2010 +0200
@@ -25,7 +25,7 @@
 from cubicweb.selectors import (non_final_entity, multi_lines_rset,
                                 match_context_prop, yes, relation_possible)
 from cubicweb.utils import json_dumps
-from cubicweb.web.box import BoxTemplate
+from cubicweb.web import component
 from cubicweb.web.facet import (AbstractFacet, FacetStringWidget, RelationFacet,
                                 prepare_facets_rqlst, filter_hiddens, _cleanup_rqlst,
                                 _prepare_vocabulary_rqlst)
@@ -38,13 +38,12 @@
     return 0
 
 
-class FilterBox(BoxTemplate):
+class FilterBox(component.CtxComponent):
     """filter results of a query"""
     __regid__ = 'filter_box'
-    __select__ = (((non_final_entity() & multi_lines_rset())
-                   | contextview_selector()
-                   ) & match_context_prop())
-    context = 'left'
+    __select__ = ((non_final_entity() & multi_lines_rset())
+                  | contextview_selector())
+    context = 'left' # XXX doesn't support 'incontext', only 'left' or 'right'
     title = _('boxes_filter_box')
     visible = True # functionality provided by the search box by default
     order = 1
@@ -61,7 +60,8 @@
         """
         return {}
 
-    def _get_context(self, view):
+    def _get_context(self):
+        view = self.cw_extra_kwargs.get('view')
         context = getattr(view, 'filter_box_context_info', lambda: None)()
         if context:
             rset, vid, divid, paginate = context
@@ -71,14 +71,15 @@
             paginate = view and view.paginable
         return rset, vid, divid, paginate
 
-    def call(self, view=None):
+    def render(self, w, **kwargs):
         req = self._cw
         req.add_js( self.needs_js )
         req.add_css( self.needs_css)
         if self.roundcorners:
             req.html_headers.add_onload('jQuery(".facet").corner("tl br 10px");')
-        rset, vid, divid, paginate = self._get_context(view)
-        if rset.rowcount < 2: # XXX done by selectors, though maybe necessary when rset has been hijacked
+        rset, vid, divid, paginate = self._get_context()
+        # XXX done by selectors, though maybe necessary when rset has been hijacked
+        if rset.rowcount < 2:
             return
         rqlst = rset.syntax_tree()
         # union not yet supported
@@ -97,9 +98,8 @@
             return
         if vid is None:
             vid = req.form.get('vid')
-        if self.bk_linkbox_template:
-            self.display_bookmark_link(rset)
-        w = self.w
+        if self.bk_linkbox_template and req.vreg.schema['Bookmark'].has_perm(req, 'add'):
+            w(self.bookmark_link(rset))
         w(u'<form method="post" id="%sForm" cubicweb:facetargs="%s" action="">'  % (
             divid, xml_escape(json_dumps([divid, vid, paginate, self.facetargs()]))))
         w(u'<fieldset>')
@@ -110,25 +110,25 @@
                 hiddens[param] = req.form[param]
         filter_hiddens(w, **hiddens)
         for wdg in widgets:
-            wdg.render(w=self.w)
+            wdg.render(w=w)
         w(u'</fieldset>\n</form>\n')
 
-    def display_bookmark_link(self, rset):
-        eschema = self._cw.vreg.schema.eschema('Bookmark')
-        if eschema.has_perm(self._cw, 'add'):
-            bk_path = 'rql=%s' % self._cw.url_quote(rset.printable_rql())
-            if self._cw.form.get('vid'):
-                bk_path += '&vid=%s' % self._cw.url_quote(self._cw.form['vid'])
-            bk_path = 'view?' + bk_path
-            bk_title = self._cw._('my custom search')
-            linkto = 'bookmarked_by:%s:subject' % self._cw.user.eid
-            bk_add_url = self._cw.build_url('add/Bookmark', path=bk_path, title=bk_title, __linkto=linkto)
-            bk_base_url = self._cw.build_url('add/Bookmark', title=bk_title, __linkto=linkto)
-            bk_link = u'<a cubicweb:target="%s" id="facetBkLink" href="%s">%s</a>' % (
-                    xml_escape(bk_base_url),
-                    xml_escape(bk_add_url),
-                    self._cw._('bookmark this search'))
-            self.w(self.bk_linkbox_template % bk_link)
+    def bookmark_link(self, rset):
+        req = self._cw
+        bk_path = u'rql=%s' % req.url_quote(rset.printable_rql())
+        if req.form.get('vid'):
+            bk_path += u'&vid=%s' % req.url_quote(req.form['vid'])
+        bk_path = u'view?' + bk_path
+        bk_title = req._('my custom search')
+        linkto = u'bookmarked_by:%s:subject' % req.user.eid
+        bkcls = req.vreg['etypes'].etype_class('Bookmark')
+        bk_add_url = bkcls.cw_create_url(req, path=bk_path, title=bk_title,
+                                         __linkto=linkto)
+        bk_base_url = bkcls.cw_create_url(req, title=bk_title, __linkto=linkto)
+        bk_link = u'<a cubicweb:target="%s" id="facetBkLink" href="%s">%s</a>' % (
+                xml_escape(bk_base_url), xml_escape(bk_add_url),
+                req._('bookmark this search'))
+        return self.bk_linkbox_template % bk_link
 
     def get_facets(self, rset, rqlst, mainvar):
         return self._cw.vreg['facets'].poss_visible_objects(
--- a/web/views/formrenderers.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/formrenderers.py	Mon Sep 13 16:47:03 2010 +0200
@@ -41,7 +41,7 @@
 from cubicweb import tags
 from cubicweb.appobject import AppObject
 from cubicweb.selectors import is_instance, yes
-from cubicweb.utils import json_dumps
+from cubicweb.utils import json_dumps, support_args
 from cubicweb.web import eid_param, formwidgets as fwdgs
 
 
@@ -53,6 +53,8 @@
         name, value, checked, attrs)
 
 def field_label(form, field):
+    if callable(field.label):
+        return field.label(form, field)
     # XXX with 3.6 we can now properly rely on 'if field.role is not None' and
     # stop having a tuple for label
     if isinstance(field.label, tuple): # i.e. needs contextual translation
@@ -133,7 +135,12 @@
         help = []
         descr = field.help
         if callable(descr):
-            descr = descr(form)
+            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",
+                     DeprecationWarning)
+                descr = descr(form)
         if descr:
             help.append('<div class="helper">%s</div>' % self._cw._(descr))
         example = field.example_format(self._cw)
--- a/web/views/ibreadcrumbs.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/ibreadcrumbs.py	Mon Sep 13 16:47:03 2010 +0200
@@ -98,8 +98,8 @@
         _('visible'):  dict(type='Boolean', default=True,
                             help=_('display the component or not')),
         }
-    title = _('contentnavigation_breadcrumbs')
-    help = _('contentnavigation_breadcrumbs_description')
+    # title = _('ctxcomponents_breadcrumbs')
+    # help = _('ctxcomponents_breadcrumbs_description')
     separator = u'&#160;&gt;&#160;'
     link_template = u'<a href="%s">%s</a>'
 
--- a/web/views/idownloadable.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/idownloadable.py	Mon Sep 13 16:47:03 2010 +0200
@@ -27,7 +27,7 @@
 from cubicweb.selectors import (one_line_rset, is_instance, match_context_prop,
                                 adaptable, has_mimetype)
 from cubicweb.mttransforms import ENGINE
-from cubicweb.web import box, httpcache
+from cubicweb.web import component, httpcache
 from cubicweb.web.views import primary, baseviews
 
 
@@ -42,22 +42,25 @@
     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'],
-         _('download icon'), xml_escape(label or entity.dc_title())))
+         req._('download icon'), xml_escape(label or entity.dc_title())))
     w(u'%s</div>' % footer)
     w(u'</div></div>\n')
 
 
-class DownloadBox(box.EntityBoxTemplate):
+class DownloadBox(component.EntityCtxComponent):
     __regid__ = 'download_box'
     # no download box for images
-    # XXX primary_view selector ?
-    __select__ = (one_line_rset() & match_context_prop()
-                  & adaptable('IDownloadable') & ~has_mimetype('image/'))
+    __select__ = (component.EntityCtxComponent.__select__ &
+                  adaptable('IDownloadable') & ~has_mimetype('image/'))
+
     order = 10
+    title = _('download')
 
-    def cell_call(self, row, col, title=None, label=None, **kwargs):
-        entity = self.cw_rset.get_entity(row, col)
-        download_box(self.w, entity, title, label)
+    def render_body(self, w):
+        w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
+          % (xml_escape(self.entity.cw_adapt_to('IDownloadable').download_url()),
+             self._cw.uiprops['DOWNLOAD_ICON'],
+             self._cw._('download icon'), xml_escape(self.entity.dc_title())))
 
 
 class DownloadView(EntityView):
--- a/web/views/navigation.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/navigation.py	Mon Sep 13 16:47:03 2010 +0200
@@ -29,7 +29,7 @@
                                 adaptable, implements)
 from cubicweb.uilib import cut
 from cubicweb.view import EntityAdapter, implements_adapter_compat
-from cubicweb.web.component import EntityVComponent, NavigationComponent
+from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent
 
 
 class PageNavigation(NavigationComponent):
@@ -201,59 +201,55 @@
         raise NotImplementedError
 
 
-class NextPrevNavigationComponent(EntityVComponent):
+class NextPrevNavigationComponent(EntityCtxComponent):
     __regid__ = 'prevnext'
     # register msg not generated since no entity implements IPrevNext in cubicweb
     # itself
-    title = _('contentnavigation_prevnext')
-    help = _('contentnavigation_prevnext_description')
-    __select__ = EntityVComponent.__select__ & adaptable('IPrevNext')
+    help = _('ctxcomponents_prevnext_description')
+    __select__ = EntityCtxComponent.__select__ & adaptable('IPrevNext')
     context = 'navbottom'
     order = 10
 
-    def call(self, view=None):
-        self.cell_call(0, 0, view=view)
+    def init_rendering(self):
+        adapter = self.entity.cw_adapt_to('IPrevNext')
+        self.previous = adapter.previous_entity()
+        self.next = adapter.next_entity()
+        if not (self.previous or self.next):
+            raise EmptyComponent()
 
-    def cell_call(self, row, col, view=None):
-        entity = self.cw_rset.get_entity(row, col)
-        adapter = entity.cw_adapt_to('IPrevNext')
-        previous = adapter.previous_entity()
-        next = adapter.next_entity()
-        if previous or next:
-            textsize = self._cw.property_value('navigation.short-line-size')
-            self.w(u'<div class="prevnext">')
-            if previous:
-                self.previous_div(previous, textsize)
-            if next:
-                self.next_div(next, textsize)
-            self.w(u'</div>')
-            self.w(u'<div class="clear"></div>')
+    def render_body(self, w):
+        w(u'<div class="prevnext">')
+        self.prevnext(w)
+        w(u'</div>')
+        w(u'<div class="clear"></div>')
+
+    def prevnext(self, w):
+        if self.previous:
+            self.prevnext_entity(w, self.previous, 'prev')
+        if self.next:
+            self.prevnext_entity(w, self.next, 'next')
 
-    def previous_div(self, previous, textsize):
-        self.w(u'<div class="previousEntity left">')
-        self.w(self.previous_link(previous, textsize))
-        self.w(u'</div>')
-        self._cw.html_headers.add_raw('<link rel="prev" href="%s" />'
-                                      % xml_escape(previous.absolute_url()))
-
-    def previous_link(self, previous, textsize):
-        return u'<a href="%s" title="%s">&lt;&lt; %s</a>' % (
-            xml_escape(previous.absolute_url()),
-            self._cw._('i18nprevnext_previous'),
-            xml_escape(cut(previous.dc_title(), textsize)))
+    def prevnext_entity(self, w, entity, type):
+        textsize = self._cw.property_value('navigation.short-line-size')
+        if type == 'prev':
+            title = self._cw._('i18nprevnext_previous')
+            icon = u'&lt;&lt; '
+            cssclass = u'previousEntity left'
+        else:
+            title = self._cw._('i18nprevnext_next')
+            icon = u'&gt;&gt; '
+            cssclass = u'nextEntity right'
+        self.prevnext_div(w, type, cssclass, entity.absolute_url(),
+                          title, icon + xml_escape(cut(entity.dc_title(), textsize)))
 
-    def next_div(self, next, textsize):
-        self.w(u'<div class="nextEntity right">')
-        self.w(self.next_link(next, textsize))
-        self.w(u'</div>')
-        self._cw.html_headers.add_raw('<link rel="next" href="%s" />'
-                                      % xml_escape(next.absolute_url()))
-
-    def next_link(self, next, textsize):
-        return u'<a href="%s" title="%s">%s &gt;&gt;</a>' % (
-            xml_escape(next.absolute_url()),
-            self._cw._('i18nprevnext_next'),
-            xml_escape(cut(next.dc_title(), textsize)))
+    def prevnext_div(self, w, type, cssclass, url, title, content):
+        w(u'<div class="%s">' % cssclass)
+        w(u'<a href="%s" title="%s">%s</a>' % (xml_escape(url),
+                                               xml_escape(title),
+                                               content))
+        w(u'</div>')
+        self._cw.html_headers.add_raw('<link rel="%s" href="%s" />' % (
+              type, xml_escape(url)))
 
 
 def do_paginate(view, rset=None, w=None, show_all_option=True, page_size=None):
--- a/web/views/primary.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/primary.py	Mon Sep 13 16:47:03 2010 +0200
@@ -25,6 +25,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import Unauthorized
+from cubicweb.utils import support_args
 from cubicweb.selectors import match_kwargs
 from cubicweb.view import EntityView
 from cubicweb.schema import VIRTUAL_RTYPES, display_name
@@ -89,7 +90,7 @@
 
     def content_navigation_components(self, context):
         self.w(u'<div class="%s">' % context)
-        for comp in self._cw.vreg['contentnavigation'].poss_visible_objects(
+        for comp in self._cw.vreg['ctxcomponents'].poss_visible_objects(
             self._cw, rset=self.cw_rset, row=self.cw_row, view=self, context=context):
             try:
                 comp.render(w=self.w, row=self.cw_row, view=self)
@@ -143,10 +144,15 @@
         if display_attributes:
             self.w(u'<table>')
             for rschema, role, dispctrl, value in display_attributes:
-                try:
-                    self._render_attribute(dispctrl, rschema, value,
-                                           role=role, table=True)
-                except TypeError:
+                if support_args(self._render_attribute, 'label'):
+                    label = self._rel_label(entity, rschema, role, dispctrl)
+                    self._render_attribute(label, value, table=True)
+                elif support_args(self._render_attribute, 'dispctrl'):
+                    warn('[3.10] _render_attribute prototype has changed, please'
+                         ' update %s' % self.__class___, DeprecationWarning)
+                    self._render_attribute(dispctrl, rschema, value, role=role,
+                                           table=True)
+                else:
                     warn('[3.6] _render_attribute prototype has changed, please'
                          ' update %s' % self.__class___, DeprecationWarning)
                     self._render_attribute(rschema, value, role=role, table=True)
@@ -166,9 +172,14 @@
                 continue
             rset = self._relation_rset(entity, rschema, role, dispctrl)
             if rset:
-                try:
+                if support_args(self._render_relation, 'label'):
+                    label = self._rel_label(entity, rschema, role, dispctrl)
+                    self._render_relation(label, dispctrl, rset, 'autolimited')
+                elif not support_args(self._render_relation, 'showlabel'):
+                    warn('[3.10] _render_relation prototype has changed, '
+                         'please update %s' % self.__class__, DeprecationWarning)
                     self._render_relation(dispctrl, rset, 'autolimited')
-                except TypeError:
+                else:
                     warn('[3.6] _render_relation prototype has changed, '
                          'please update %s' % self.__class__, DeprecationWarning)
                     self._render_relation(rset, dispctrl, 'autolimited',
@@ -183,42 +194,45 @@
                 try:
                     label, rset, vid, dispctrl  = box
                 except ValueError:
-                    warn('[3.5] box views should now be defined as a 4-uple (label, rset, vid, dispctrl), '
-                         'please update %s' % self.__class__.__name__,
-                         DeprecationWarning)
                     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 NotImplementedError:
-                    # much probably a context insensitive box, which only implements
-                    # .call() and not cell_call()
+                 try:
+                     box.render(w=self.w, row=self.cw_row)
+                 except NotImplementedError:
+                    # much probably a context insensitive box, which only
+                    # implements .call() and not cell_call()
+                    # XXX shouldn't occurs with the new box system
                     box.render(w=self.w)
 
     def _prepare_side_boxes(self, entity):
         sideboxes = []
+        boxesreg = self._cw.vreg['ctxcomponents']
         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'sideboxes'):
             rset = self._relation_rset(entity, rschema, role, dispctrl)
             if not rset:
                 continue
-            label = display_name(self._cw, rschema.type, role)
-            vid = dispctrl.get('vid', 'sidebox')
-            sideboxes.append( (label, rset, vid, dispctrl) )
-        sideboxes += self._cw.vreg['boxes'].poss_visible_objects(
-            self._cw, rset=self.cw_rset, row=self.cw_row, view=self,
-            context='incontext')
+            label = self._rel_label(entity, rschema, role, dispctrl)
+            vid = dispctrl.get('vid', 'autolimited')
+            box = boxesreg.select('rsetbox', self._cw, rset=rset,
+                                  vid=vid, title=label, dispctrl=dispctrl,
+                                  context='incontext')
+            sideboxes.append(box)
+        sideboxes += boxesreg.poss_visible_objects(
+             self._cw, rset=self.cw_rset, row=self.cw_row, view=self,
+             context='incontext')
         # XXX since we've two sorted list, it may be worth using bisect
         def get_order(x):
-            if isinstance(x, tuple):
-                # x is a view box (label, rset, vid, dispctrl)
-                # default to 1000 so view boxes occurs after component boxes
-                return x[-1].get('order', 1000)
-            # x is a component box
-            return x.cw_propval('order')
+            if 'order' in x.cw_property_defs:
+                return x.cw_propval('order')
+            # default to 9999 so view boxes occurs after component boxes
+            return x.cw_extra_kwargs.get('dispctrl', {}).get('order', 9999)
         return sorted(sideboxes, key=get_order)
 
     def _section_def(self, entity, where):
@@ -251,25 +265,16 @@
             rset = dispctrl['filter'](rset)
         return rset
 
-    def _render_relation(self, dispctrl, rset, defaultvid):
+    def _render_relation(self, label, dispctrl, rset, defaultvid):
         self.w(u'<div class="section">')
-        if dispctrl.get('showlabel', self.show_rel_label):
-            self.w(u'<h4>%s</h4>' % self._cw._(dispctrl['label']))
+        if label:
+            self.w(u'<h4>%s</h4>' % label)
         self.wview(dispctrl.get('vid', defaultvid), rset,
                    initargs={'dispctrl': dispctrl})
         self.w(u'</div>')
 
-    def _render_attribute(self, dispctrl, rschema, value,
-                          role='subject', table=False):
-        if rschema.final:
-            showlabel = dispctrl.get('showlabel', self.show_attr_label)
-        else:
-            showlabel = dispctrl.get('showlabel', self.show_rel_label)
-        if dispctrl.get('label'):
-            label = self._cw._(dispctrl.get('label'))
-        else:
-            label = display_name(self._cw, rschema.type, role)
-        self.field(label, value, show_label=showlabel, tr=False, table=table)
+    def _render_attribute(self, label, value, table=False):
+        self.field(label, value, tr=False, table=table)
 
     def _rel_label(self, entity, rschema, role, dispctrl):
         if rschema.final:
@@ -339,6 +344,6 @@
 _pvs = uicfg.primaryview_section
 for rtype in ('eid', 'creation_date', 'modification_date', 'cwuri',
               'is', 'is_instance_of', 'identity', 'owned_by', 'created_by',
-              'require_permission', 'see_also'):
+              'require_permission'):
     _pvs.tag_subject_of(('*', rtype, '*'), 'hidden')
     _pvs.tag_object_of(('*', rtype, '*'), 'hidden')
--- a/web/views/sessions.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/sessions.py	Mon Sep 13 16:47:03 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """web session component: by dfault the session is actually the db connection
 object :/
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.web import InvalidSession
@@ -51,9 +51,6 @@
         if not sessionid in self._sessions:
             raise InvalidSession()
         session = self._sessions[sessionid]
-        if self.has_expired(session):
-            self.close_session(session)
-            raise InvalidSession()
         try:
             user = self.authmanager.validate_session(req, session)
         except InvalidSession:
--- a/web/views/startup.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/startup.py	Mon Sep 13 16:47:03 2010 +0200
@@ -159,15 +159,14 @@
             url = self._cw.build_url(etype)
             etypelink = u'&#160;<a href="%s">%s</a> (%d)' % (
                 xml_escape(url), label, nb)
-            yield (label, etypelink, self.add_entity_link(eschema, req))
+            if eschema.has_perm(req, 'add'):
+                yield (label, etypelink, self.add_entity_link(etype))
 
-    def add_entity_link(self, eschema, req):
-        """creates a [+] link for adding an entity if user has permission to do so"""
-        if not eschema.has_perm(req, 'add'):
-            return u''
+    def add_entity_link(self, etype):
+        """creates a [+] link for adding an entity"""
+        url = self._cw.vreg["etypes"].etype_class(etype).cw_create_url(self._cw)
         return u'[<a href="%s" title="%s">+</a>]' % (
-            xml_escape(self.create_url(eschema.type)),
-            self._cw.__('add a %s' % eschema))
+            xml_escape(url), self._cw.__('add a %s' % etype))
 
 
 class IndexView(ManageView):
--- a/web/views/treeview.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/treeview.py	Mon Sep 13 16:47:03 2010 +0200
@@ -110,7 +110,7 @@
     __regid__ = 'treeview'
     itemvid = 'treeitemview'
     subvid = 'oneline'
-    css_classes = 'treeview widget'
+    cssclass = 'treeview widget'
     title = _('tree view')
 
     def _init_params(self, subvid, treeid, initial_load, initial_thru_ajax, morekwargs):
@@ -144,7 +144,7 @@
         if toplevel:
             self._init_headers(treeid, toplevel_thru_ajax)
             ulid = ' id="tree-%s"' % treeid
-        self.w(u'<ul%s class="%s">' % (ulid, self.css_classes))
+        self.w(u'<ul%s class="%s">' % (ulid, self.cssclass))
         # XXX force sorting on x.sortvalue() (which return dc_title by default)
         # we need proper ITree & co specification to avoid this.
         # (pb when type ambiguity at the other side of the tree relation,
@@ -171,7 +171,7 @@
     """specific version of the treeview to display file trees
     """
     __regid__ = 'filetree'
-    css_classes = 'treeview widget filetree'
+    cssclass = 'treeview widget filetree'
     title = _('file tree view')
 
     def call(self, subvid=None, treeid=None, initial_load=True, **kwargs):
--- a/web/views/workflow.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/workflow.py	Mon Sep 13 16:47:03 2010 +0200
@@ -25,6 +25,7 @@
 _ = unicode
 
 import os
+from warnings import warn
 
 from logilab.mtconverter import xml_escape
 from logilab.common.graph import escape
@@ -160,15 +161,21 @@
                        displaycols=displaycols, headers=headers)
 
 
-class WFHistoryVComponent(component.EntityVComponent):
+class WFHistoryVComponent(component.CtxComponent):
     """display the workflow history for entities supporting it"""
     __regid__ = 'wfhistory'
     __select__ = WFHistoryView.__select__ & component.EntityVComponent.__select__
     context = 'navcontentbottom'
     title = _('Workflow history')
 
-    def cell_call(self, row, col, view=None):
-        self.wview('wfhistory', self.cw_rset, row=row, col=col, view=view)
+    def render_body(self, w):
+        if hasattr(self, 'cell_call'):
+            warn('[3.10] %s should now implement render_body instead of cell_call',
+                 DeprecationWarning, self.__class__)
+            self.w = w
+            self.cell_call(self.entity.cw_row, self.entity.cw_col)
+        else:
+            self.entity.view('wfhistory', w=w)
 
 
 # workflow actions #############################################################
--- a/web/views/xmlrss.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/views/xmlrss.py	Mon Sep 13 16:47:03 2010 +0200
@@ -29,7 +29,7 @@
 from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
 from cubicweb.view import implements_adapter_compat
 from cubicweb.uilib import simple_sgml_tag
-from cubicweb.web import httpcache, box
+from cubicweb.web import httpcache, component
 
 
 # base xml views ##############################################################
@@ -68,7 +68,7 @@
                 value = entity.eid
             else:
                 try:
-                    value = entity[attr]
+                    value = entity.cw_attr_cache[attr]
                 except KeyError:
                     # Bytes
                     continue
@@ -148,25 +148,25 @@
         return entity.cw_adapt_to('IFeed').rss_feed_url()
 
 
-class RSSIconBox(box.BoxTemplate):
+class RSSIconBox(component.CtxComponent):
     """just display the RSS icon on uniform result set"""
     __regid__ = 'rss'
-    __select__ = (box.BoxTemplate.__select__
+    __select__ = (component.CtxComponent.__select__
                   & appobject_selectable('components', 'rss_feed_url'))
 
     visible = False
     order = 999
 
-    def call(self, **kwargs):
+    def render(self, w, **kwargs):
         try:
             rss = self._cw.uiprops['RSS_LOGO']
         except KeyError:
             self.error('missing RSS_LOGO external resource')
             return
         urlgetter = self._cw.vreg['components'].select('rss_feed_url', self._cw,
-                                                   rset=self.cw_rset)
+                                                       rset=self.cw_rset)
         url = urlgetter.feed_url()
-        self.w(u'<a href="%s"><img src="%s" alt="rss"/></a>\n' % (xml_escape(url), rss))
+        w(u'<a href="%s"><img src="%s" alt="rss"/></a>\n' % (xml_escape(url), rss))
 
 
 class RSSView(XMLView):
--- a/web/webconfig.py	Mon Sep 13 16:46:52 2010 +0200
+++ b/web/webconfig.py	Mon Sep 13 16:47:03 2010 +0200
@@ -135,17 +135,6 @@
           "Should be 0 or greater than repository\'s session-time.",
           'group': 'web', 'level': 2,
           }),
-        ('cleanup-session-time',
-         {'type' : 'time',
-          'default': '24h',
-          'help': 'duration of inactivity after which a connection '
-          'will be closed, to limit memory consumption (avoid sessions that '
-          'never expire and cause memory leak when http-session-time is 0). '
-          'So even if http-session-time is 0 and the user don\'t close his '
-          'browser, he will have to reauthenticate after this time of '
-          'inactivity. Default to 24h.',
-          'group': 'web', 'level': 3,
-          }),
         ('cleanup-anonymous-session-time',
          {'type' : 'time',
           'default': '5min',