backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 13 Apr 2010 19:43:51 +0200
changeset 5238 31c12863fd9d
parent 5237 10dd0dd78778 (current diff)
parent 5231 40f53867e332 (diff)
child 5239 471554b842d2
backport stable
devtools/__init__.py
devtools/testlib.py
doc/book/en/development/devrepo/operations.rst
server/hook.py
server/session.py
server/test/unittest_repository.py
web/facet.py
web/formfields.py
--- a/devtools/__init__.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/devtools/__init__.py	Tue Apr 13 19:43:51 2010 +0200
@@ -201,7 +201,7 @@
     elif driver == 'postgres':
         init_test_database_postgres(config)
     elif driver == 'sqlserver2005':
-        init_test_database_sqlserver2005(config, source)
+        init_test_database_sqlserver2005(config)
     else:
         raise ValueError('no initialization function for driver %r' % driver)
     config._cubes = None # avoid assertion error
@@ -217,6 +217,8 @@
     driver = config.sources()['system']['db-driver']
     if driver == 'sqlite':
         reset_test_database_sqlite(config)
+    elif driver == 'sqlserver2005':
+        reset_test_database_sqlserver2005(config)
     else:
         raise ValueError('no reset function for driver %r' % driver)
 
@@ -237,6 +239,8 @@
         from cubicweb.server import init_repository
         init_repository(config, interactive=False, drop=True, vreg=vreg)
 
+def reset_test_database_sqlserver2005(config):
+    pass
 
 ### sqlite test database handling ##############################################
 
--- a/devtools/testlib.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/devtools/testlib.py	Tue Apr 13 19:43:51 2010 +0200
@@ -134,18 +134,17 @@
 
 class CubicWebTC(TestCase):
     """abstract class for test using an apptest environment
+    attributes:
 
-    attributes:
-    `vreg`, the vregistry
-    `schema`, self.vreg.schema
-    `config`, cubicweb configuration
-    `cnx`, dbapi connection to the repository using an admin user
-    `session`, server side session associated to `cnx`
-    `app`, the cubicweb publisher (for web testing)
-    `repo`, the repository object
-
-    `admlogin`, login of the admin user
-    `admpassword`, password of the admin user
+    * `vreg`, the vregistry
+    * `schema`, self.vreg.schema
+    * `config`, cubicweb configuration
+    * `cnx`, dbapi connection to the repository using an admin user
+    * `session`, server side session associated to `cnx`
+    * `app`, the cubicweb publisher (for web testing)
+    * `repo`, the repository object
+    * `admlogin`, login of the admin user
+    * `admpassword`, password of the admin user
 
     """
     appid = 'data'
--- a/doc/book/en/annexes/faq.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/annexes/faq.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -9,6 +9,23 @@
 http://groups.google.com/group/google-appengine/browse_frm/thread/f48cf6099973aef5/c28cd6934dd72457
 ]
 
+Generalities
+````````````
+
+Why do you use the LGPL license to prevent me from doing X ?
+------------------------------------------------------------
+
+LGPL means that *if* you redistribute your application, you need to
+redistribute the changes you made to CubicWeb under the LGPL licence.
+
+Publishing a web site has nothing to do with redistributing source
+code according to the terms of the LGPL. A fair amount of companies
+use modified LGPL code for internal use. And someone could publish a
+*CubicWeb* component under a BSD licence for others to plug into a
+LGPL framework without any problem. The only thing we are trying to
+prevent here is someone taking the framework and packaging it as
+closed source to his own clients.
+
 Why does not CubicWeb have a template language ?
 ------------------------------------------------
 
@@ -44,21 +61,6 @@
 learning a new dialect. By using Python, we use standard OOP techniques and
 this is a key factor in a robust application.
 
-Why do you use the LGPL license to prevent me from doing X ?
-------------------------------------------------------------
-
-LGPL means that *if* you redistribute your application, you need to
-redistribute the changes you made to CubicWeb under the LGPL licence.
-
-Publishing a web site has nothing to do with redistributing source
-code according to the terms of the LGPL. A fair amount of companies
-use modified LGPL code for internal use. And someone could publish a
-*CubicWeb* component under a BSD licence for others to plug into a
-LGPL framework without any problem. The only thing we are trying to
-prevent here is someone taking the framework and packaging it as
-closed source to his own clients.
-
-
 CubicWeb looks pretty recent. Is it stable ?
 --------------------------------------------
 
@@ -66,6 +68,10 @@
 2001 and data has been migrated from one schema to the other ever since. There
 is a well-defined way to handle data and schema migration.
 
+You can see the roadmap there:
+http://www.cubicweb.org/project/cubicweb?tab=projectroadmap_tab.
+
+
 Why is the RQL query language looking similar to X ?
 -----------------------------------------------------
 
@@ -95,58 +101,64 @@
 that. Additionally, some jQuery plugins are provided (some are
 provided in specific cubes).
 
-
-How is security implemented ?
-------------------------------
+Development
+```````````
 
-The basis for security is a mapping from operations to groups or
-arbitrary RQL expressions. These mappings are scoped to entities and
-relations.
+How to load data from a script ?
+--------------------------------
 
-This is an example for an Entity Type definition:
+The following script aims at loading data within a script assuming pyro-nsd is
+running and your instance is configured with ``pyro-server=yes``, otherwise
+you would not be able to use dbapi.
 
 .. sourcecode:: python
 
-    class Version(EntityType):
-        """a version is defining the content of a particular project's
-        release"""
-        # definition of attributes is voluntarily missing
-        __permissions__ = {'read': ('managers', 'users', 'guests',),
-                           'update': ('managers', 'logilab', 'owners'),
-                           'delete': ('managers',),
-                           'add': ('managers', 'logilab',
-                                   ERQLExpression('X version_of PROJ, U in_group G, '
-                                                  'PROJ require_permission P, '
-                                                  'P name "add_version", P require_group G'),)}
+    from cubicweb import dbapi
+
+    cnx = dbapi.connection(database='instance-id', user='admin', password='admin')
+    cur = cnx.cursor()
+    for name in ('Personal', 'Professional', 'Computers'):
+        cur.execute('INSERT Blog B: B name %s', name)
+    cnx.commit()
+
 
-The above means that permission to read a Version is granted to any
-user that is part of one of the groups 'managers', 'users', 'guests'.
-The 'add' permission is granted to users in group 'managers' or
-'logilab' or to users in group G, if G is linked by a permission
-entity named "add_version" to the version's project.
+How to format an entity date attribute ?
+----------------------------------------
 
-An example for a Relation Definition (RelationType both defines a
-relation type and implicitly one relation definition, on which the
-permissions actually apply):
+If your schema has an attribute of type Date or Datetime, you might
+want to format it. First, you should define your preferred format using
+the site configuration panel ``http://appurl/view?vid=systempropertiesform``
+and then set ``ui.date`` and/or ``ui.datetime``.
+Then in the view code, use:
 
 .. sourcecode:: python
 
-    class version_of(RelationType):
-        """link a version to its project. A version is necessarily linked
-        to one and only one project. """
-        # some lines voluntarily missing
-        __permissions__ = {'read': ('managers', 'users', 'guests',),
-                           'delete': ('managers', ),
-                           'add': ('managers', 'logilab',
-                                   RRQLExpression('O require_permission P, P name "add_version", '
-                                                  'U in_group G, P require_group G'),) }
+    self.format_date(entity.date_attribute)
+
+What is the CubicWeb datatype corresponding to GAE datastore's UserProperty ?
+-----------------------------------------------------------------------------
+
+If you take a look at your instance schema and
+click on "display detailed view of metadata" you will see that there
+is a Euser entity in there. That's the one that is modeling users. The
+thing that corresponds to a UserProperty is a relationship between
+your entity and the Euser entity. As in:
+
+.. sourcecode:: python
 
-The main difference lies in the basic available operations (there is
-no 'update' operation) and the usage of an RRQLExpression (rql
-expression for a relation) instead of an ERQLExpression (rql
-expression for an entity).
+    class TodoItem(EntityType):
+       text = String()
+       todo_by = SubjectRelation('Euser')
+
+[XXX check that cw handle users better by mapping Google Accounts to local Euser
+entities automatically]
 
-You can find additional information in the section :ref:`securitymodel`.
+
+How do I translate an msg id defined (and translated) in another cube ?
+-----------------------------------------------------------------------
+
+You should put these translations in the `i18n/static-messages.pot`
+file of your own cube.
 
 
 What is `Error while publishing rest text ...` ?
@@ -246,6 +258,8 @@
 
    where DATADIR is ``mycube/data``.
 
+Configuration
+`````````````
 
 How to configure a LDAP source ?
 --------------------------------
@@ -304,63 +318,8 @@
 
     2009-01-09 16:43:52 - (cubicweb.selectors) WARNING: selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
 
-How to format an entity date attribute ?
-----------------------------------------
-
-If your schema has an attribute of type Date or Datetime, you might
-want to format it. First, you should define your preferred format using
-the site configuration panel ``http://appurl/view?vid=systempropertiesform``
-and then set ``ui.date`` and/or ``ui.datetime``.
-Then in the view code, use:
-
-.. sourcecode:: python
-
-    self.format_date(entity.date_attribute)
-
-Can PostgreSQL and CubicWeb authentication work with kerberos ?
-----------------------------------------------------------------
-
-If you have PostgreSQL set up to accept kerberos authentication, you can set
-the db-host, db-name and db-user parameters in the `sources` configuration
-file while leaving the password blank. It should be enough for your
-instance to connect to postgresql with a kerberos ticket.
-
-
-How to load data from a script ?
---------------------------------
-
-The following script aims at loading data within a script assuming pyro-nsd is
-running and your instance is configured with ``pyro-server=yes``, otherwise
-you would not be able to use dbapi.
-
-.. sourcecode:: python
-
-    from cubicweb import dbapi
-
-    cnx = dbapi.connection(database='instance-id', user='admin', password='admin')
-    cur = cnx.cursor()
-    for name in ('Personal', 'Professional', 'Computers'):
-        cur.execute('INSERT Blog B: B name %s', name)
-    cnx.commit()
-
-What is the CubicWeb datatype corresponding to GAE datastore's UserProperty ?
------------------------------------------------------------------------------
-
-If you take a look at your instance schema and
-click on "display detailed view of metadata" you will see that there
-is a Euser entity in there. That's the one that is modeling users. The
-thing that corresponds to a UserProperty is a relationship between
-your entity and the Euser entity. As in:
-
-.. sourcecode:: python
-
-    class TodoItem(EntityType):
-       text = String()
-       todo_by = SubjectRelation('Euser')
-
-[XXX check that cw handle users better by mapping Google Accounts to local Euser
-entities automatically]
-
+Security
+````````
 
 How to reset the password for user joe ?
 ----------------------------------------
@@ -392,8 +351,64 @@
 
 This is because you have to put your user in the "users" group. The user has to be in both groups.
 
-How do I translate an msg id defined (and translated) in another cube ?
------------------------------------------------------------------------
+How is security implemented ?
+------------------------------
+
+The basis for security is a mapping from operations to groups or
+arbitrary RQL expressions. These mappings are scoped to entities and
+relations.
+
+This is an example for an Entity Type definition:
+
+.. sourcecode:: python
+
+    class Version(EntityType):
+        """a version is defining the content of a particular project's
+        release"""
+        # definition of attributes is voluntarily missing
+        __permissions__ = {'read': ('managers', 'users', 'guests',),
+                           'update': ('managers', 'logilab', 'owners'),
+                           'delete': ('managers',),
+                           'add': ('managers', 'logilab',
+                                   ERQLExpression('X version_of PROJ, U in_group G, '
+                                                  'PROJ require_permission P, '
+                                                  'P name "add_version", P require_group G'),)}
+
+The above means that permission to read a Version is granted to any
+user that is part of one of the groups 'managers', 'users', 'guests'.
+The 'add' permission is granted to users in group 'managers' or
+'logilab' or to users in group G, if G is linked by a permission
+entity named "add_version" to the version's project.
 
-You should put these translations in the `i18n/static-messages.pot`
-file of your own cube.
+An example for a Relation Definition (RelationType both defines a
+relation type and implicitly one relation definition, on which the
+permissions actually apply):
+
+.. sourcecode:: python
+
+    class version_of(RelationType):
+        """link a version to its project. A version is necessarily linked
+        to one and only one project. """
+        # some lines voluntarily missing
+        __permissions__ = {'read': ('managers', 'users', 'guests',),
+                           'delete': ('managers', ),
+                           'add': ('managers', 'logilab',
+                                   RRQLExpression('O require_permission P, P name "add_version", '
+                                                  'U in_group G, P require_group G'),) }
+
+The main difference lies in the basic available operations (there is
+no 'update' operation) and the usage of an RRQLExpression (rql
+expression for a relation) instead of an ERQLExpression (rql
+expression for an entity).
+
+You can find additional information in the section :ref:`securitymodel`.
+
+Can PostgreSQL and CubicWeb authentication work with kerberos ?
+----------------------------------------------------------------
+
+If you have PostgreSQL set up to accept kerberos authentication, you can set
+the db-host, db-name and db-user parameters in the `sources` configuration
+file while leaving the password blank. It should be enough for your
+instance to connect to postgresql with a kerberos ticket.
+
+
--- a/doc/book/en/conf.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/conf.py	Tue Apr 13 19:43:51 2010 +0200
@@ -32,7 +32,7 @@
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'logilab.common.sphinx_ext']
+extensions = ['sphinx.ext.autodoc', 'logilab.common.sphinx_ext']#, 'sphinxcontrib.aafig']
 autoclass_content = 'both'
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['.templates']
@@ -92,11 +92,13 @@
 # The style sheet to use for HTML and HTML Help pages. A file of that name
 # must exist either in Sphinx' static/ path, or in one of the custom paths
 # given in html_static_path.
-html_style = 'sphinx-default.css'
+#html_style = 'sphinx-default.css'
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
 html_title = '%s %s' % (project, release)
+html_theme = 'lglb_doc'
+html_theme_path = ['_theme']
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
 #html_short_title = None
@@ -186,3 +188,5 @@
 
 # If false, no module index is generated.
 #latex_use_modindex = True
+#aafig_format = dict(latex='pdf', html='svg', text=None)
+
--- a/doc/book/en/development/datamodel/definition.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/datamodel/definition.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -37,14 +37,18 @@
 
 Entity type
 ~~~~~~~~~~~
+
 An entity type is an instance of :class:`yams.schema.EntitySchema`. Each entity type has
 a set of attributes and relations, and some permissions which define who can add, read,
 update or delete entities of this type.
 
 XXX yams inheritance
 
+.. _RelationType:
+
 Relation type
 ~~~~~~~~~~~~~
+
 A relation type is an instance of :class:`yams.schema.RelationSchema`. A relation type is simply
 a semantic definition of a kind of relationship that may occur in an application.
 
@@ -66,6 +70,7 @@
 
 Relation definition
 ~~~~~~~~~~~~~~~~~~~
+
 A relation definition is an instance of :class:`yams.schema.RelationDefinition`. It is a complete triplet
 "<subject entity type> <relation type> <object entity type>".
 
@@ -147,14 +152,27 @@
 * `SizeConstraint`: allows to specify a minimum and/or maximum size on
   string (generic case of `maxsize`)
 
-* `BoundConstraint`: allows to specify a minimum and/or maximum value on
-  numeric types
+* `BoundConstraint`: allows to specify a minimum and/or maximum value
+  on numeric types and date
+
+.. sourcecode:: python
+
+   from yams.constraints import BoundConstraint, TODAY
+   BoundConstraint('<=', TODAY())
+
+* `IntervalBoundConstraint`: allows to specify an interval with
+  included values
+
+.. sourcecode:: python
+
+     class Node(EntityType):
+         latitude = Float(constraints=[IntervalBoundConstraint(-90, +90)])
 
 * `UniqueConstraint`: identical to "unique=True"
 
 * `StaticVocabularyConstraint`: identical to "vocabulary=(...)"
 
-XXX Attribute, TODAY, NOW
+XXX Attribute, NOW
 
 RQL Based Constraints
 ......................
@@ -463,7 +481,7 @@
  Although this way of defining relations uses a Python class, the
  naming convention defined earlier prevails over the PEP8 conventions
  used in the framework: relation type class names use
- ``underscore_separated_words``. 
+ ``underscore_separated_words``.
 
 :Historical note:
 
--- a/doc/book/en/development/devrepo/hooks.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/devrepo/hooks.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -5,28 +5,29 @@
 Hooks and Operations
 ====================
 
-Principles
-----------
+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`).
+occasion (this is called `running the hook`).
 
 .. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
 
-In CubicWeb, hooks are classes subclassing the Hook class in
-`server/hook.py`, implementing their own `call` method, and defined
-over pre-defined `events`.
+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. There can be a lot of them.
+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 definitions languages are not
+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).
 
@@ -35,25 +36,35 @@
 Data hooks can serve the following purposes:
 
 * enforcing constraints that the static schema cannot express
-  (spanning several entities/relations, exotic cardinalities, etc.)
+  (spanning several entities/relations, exotic value ranges and
+  cardinalities, etc.)
 
-* implement computed attributes (an example could be the maintenance
-  of a relation representing the transitive closure of another relation)
+* implement computed attributes
 
-Operations are Hook-like objects that are created by Hooks and
+Operations are Hook-like objects that maye 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
-and the application state converges towards consistency. 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.
+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 later in this chapter).
 
 Events
 ------
 
 Hooks are mostly defined and used to handle `dataflow`_ operations. It
-means as data gets in (mostly), specific events are issued and the
-Hooks matching these events are called.
+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
 
@@ -83,7 +94,7 @@
 * before_delete_relation
 
 This is an occasion to remind us that relations support the add/delete
-operation, but no delete.
+operation, but no update.
 
 Non data events also exist. These are called SYSTEM HOOKS.
 
@@ -102,3 +113,309 @@
 * 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.
+
+.. 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.
+
+.. sourcecode:: python
+
+   from cubicweb import ValidationError
+   from cubicweb.selectors import implements
+   from cubicweb.server.hook import Hook
+
+   class PersonAgeRange(Hook):
+        __regid__ = 'person_age_range'
+        events = ('before_add_entity', 'before_update_entity')
+        __select__ = Hook.__select__ & implements('Person')
+
+        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
+`implements` 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.
+
+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
+
+   class Company(EntityType):
+        name = String(required=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+
+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')
+
+        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.
+
+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.
+
+.. sourcecode:: python
+
+   class Company(EntityType):
+        name = String(required=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+        subsidiary_of = SubjectRelation('Company', cardinality='*?')
+
+Base example
+~~~~~~~~~~~~
+
+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.
+
+.. sourcecode:: python
+
+    def check_cycle(self, session, eid, rtype, role='subject'):
+        parents = set([eid])
+        parent = session.entity_from_eid(eid)
+        while parent.related(rtype, role):
+            parent = parent.related(rtype, role)[0]
+            if parent.eid in parents:
+                msg = session._('detected %s cycle' % rtype)
+                raise ValidationError(eid, {rtype: msg})
+            parents.add(parent.eid)
+
+    class CheckSubsidiaryCycleOp(Operation):
+
+        def precommit_event(self):
+            check_cycle(self.session, self.eidto, 'subsidiary_of')
+
+
+    class CheckSubsidiaryCycleHook(Hook):
+        __regid__ = 'check_no_subsidiary_cycle'
+        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, 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.
+
+.. sourcecode:: python
+
+   from cubicweb.server.hook import set_operation
+
+   class CheckSubsidiaryCycleHook(Hook):
+       __regid__ = 'check_no_subsidiary_cycle'
+       events = ('after_add_relation',)
+       __select__ = Hook.__select__ & match_rtype('subsidiary_of')
+
+       def __call__(self):
+           set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto,
+                         CheckSubsidiaryCycleOp, rtype=self.rtype)
+
+   class CheckSubsidiaryCycleOp(Operation):
+
+       def precommit_event(self):
+           for eid in self._cw.transaction_data['subsidiary_cycle_detection']:
+               check_cycle(self.session, eid, self.rtype)
+
+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).
+
+Operation: a small API overview
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autoclass:: cubicweb.server.hook.Operation
+.. autoclass:: cubicweb.server.hook.LateOperation
+.. autofunction:: cubicweb.server.hook.set_operation
+
+Hooks writing rules
+-------------------
+
+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
+
+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.
+
+(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
+~~~~~~~~~~~~~~~~~
+
+On udpates, it is possible to ask the `entity.edited_attributes`
+variable whether one attribute has been updated.
+
+.. sourcecode:: python
+
+  if 'age' not in entity.edited_attribute:
+      return
+
+Deleted in transaction
+~~~~~~~~~~~~~~~~~~~~~~
+
+The session object has a deleted_in_transaction method, which can help
+writing deletion Hooks.
+
+.. 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.
+
+.. sourcecode:: python
+
+   with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, <category>):
+       # ... do stuff
+
+.. autoclass:: cubicweb.server.session.hooks_control
--- a/doc/book/en/development/devrepo/index.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/devrepo/index.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -8,7 +8,6 @@
    sessions
    hooks
    notifications
-   operations
    tasks
 
 
--- a/doc/book/en/development/devrepo/operations.rst	Tue Apr 13 19:43:30 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Repository operations
-======================
-
-When one needs to perform operations (real world operations like mail
-notifications, file operations, real-world side-effects) at
-transaction commit time, Operations are the way to go.
-
-Possible events are:
-
-* precommit: the pool is preparing to commit. You shouldn't do
-  anything things which has to be reverted if the commit fail 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 operation
-  during this phase but their precommit event won't be triggered
-
-* 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
-
-* 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)
-
-* 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
-
-Exceptions signaled from within a rollback are logged and swallowed.
-
-The order of operations may be important, and is controlled according
-to operation's class (see : Operation, LateOperation, SingleOperation,
-SingleLastOperation).
--- a/doc/book/en/development/devweb/internationalization.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/devweb/internationalization.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -37,7 +37,7 @@
 
  * by using the equivalent request's method ::
 
-     class NoResultView(EmptyRsetView):
+     class NoResultView(View):
          """default view when no result has been found"""
          __regid__ = 'noresult'
 
--- a/doc/book/en/development/devweb/request.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/devweb/request.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -1,5 +1,3 @@
-
-
 The `Request` class (`cubicweb.web`)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -57,3 +55,7 @@
 today). For the views or others that are executed on the server side,
 most of the interface of `Request` is defined in the session associated
 to the client.
+
+
+XXX autoclass !
+XXX create_entity
--- a/doc/book/en/development/devweb/rtags.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/devweb/rtags.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -1,5 +1,5 @@
-Configuring the generated interface
------------------------------------
+Configuring the user interface
+------------------------------
 
 
 Relation tags
--- a/doc/book/en/development/devweb/views.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/devweb/views.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -9,7 +9,7 @@
 in *CubicWeb*.
 
 We'll start with a description of the interface providing you with a basic
-understanding of the classes and methods available, then detail the view
+understanding of the available classes and methods, then detail the view
 selection principle which makes *CubicWeb* web interface very flexible.
 
 A `View` is an object applied to another object such as an entity.
@@ -26,20 +26,22 @@
 A `View` is instantiated to render a result set or part of a result set. `View`
 subclasses may be parametrized using the following class attributes:
 
-    * `templatable` indicates if the view may be embeded in a main
-      template or if it has to be rendered standalone (i.e. XML views
-      must not be embeded in the main template for HTML pages)
-    * if the view is not templatable, it should set the `content_type` class
-      attribute to the correct MIME type (text/xhtml by default)
-    * the `category` attribute may be used in the interface to regroup related
-      objects together
+* `templatable` indicates if the view may be embedded in a main
+  template or if it has to be rendered standalone (i.e. XML views must
+  not be embedded in the main template for HTML pages)
+
+* if the view is not templatable, it should set the `content_type`
+  class attribute to the correct MIME type (text/xhtml by default)
+
+* the `category` attribute may be used in the interface to regroup
+  related objects together
+
+A view writes to its output stream thanks to its attribute `w` (an
+`UStreamIO`, except for binary views).
 
 At instantiation time, the standard `_cw` and `cw_rset` attributes are
 added and the `w` attribute will be set at rendering time.
 
-A view writes to its output stream thanks to its attribute `w` (an
-`UStreamIO`, except for binary views).
-
 The basic interface for views is as follows (remember that the result set has a
 tabular structure with rows and columns, hence cells):
 
@@ -51,24 +53,21 @@
   result set)
 
 * `cell_call(row, col, **kwargs)`, call the view for a given cell of a
-  result set
+  result set (`row` and `col` being integers used to access the cell)
 
 * `url()`, returns the URL enabling us to get the view with the current
   result set
 
-* `view(__vid, rset, __fallback_vid=None, **kwargs)`, call the view of identifier
-  `__vid` on the given result set. It is possible to give a view identifier
-  of fallback that will be used if the view requested is not applicable to the
-  result set. This is actually defined on the AppObject class.
+* `wview(__vid, rset, __fallback_vid=None, **kwargs)`, call the view of
+  identifier `__vid` on the given result set. It is possible to give a
+  fallback view identifier that will be used if the requested view is
+  not applicable to the result set.
 
-* `wview(__vid, rset, __fallback_vid=None, **kwargs)`, similar to `view` except
-  the flow is automatically passed in the parameters
-
-* `html_headers()`, returns a list of HTML headers to set by the main template
+* `html_headers()`, returns a list of HTML headers to be set by the
+  main template
 
 * `page_title()`, returns the title to use in the HTML header `title`
 
-
 Other basic view classes
 ````````````````````````
 Here are some of the subclasses of `View` defined in `cubicweb.common.view`
@@ -77,8 +76,6 @@
 * `EntityView`, view applying to lines or cell containing an entity (e.g. an eid)
 * `StartupView`, start view that does not require a result set to apply to
 * `AnyRsetView`, view applicable to any result set
-* `EmptyRsetView`, view applicable to an empty result set
-
 
 Examples of views class
 -----------------------
@@ -109,6 +106,7 @@
         __select__ = one_line_rset() & match_search_state('linksearch') & implements('Any')
 
 
+
 Example of view customization and creation
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
--- a/doc/book/en/development/entityclasses/data-as-objects.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/entityclasses/data-as-objects.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -16,15 +16,15 @@
 
 :Formatting and output generation:
 
-  * `view(vid, **kwargs)`, applies the given view to the entity
+  * `view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
     (and returns an unicode string)
 
-  * `absolute_url(**kwargs)`, returns an absolute URL to access the primary view
+  * `absolute_url(*args, **kwargs)`, returns an absolute URL to access the primary view
     of an entity
 
   * `rest_path()`, returns a relative REST URL to get the entity
 
-  * `printable_value(attr, value=_marker, attrtype=None, format='text/html')`,
+  * `printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
     returns a string enabling the display of an attribute value in a given format
     (the value is automatically recovered if necessary)
 
--- a/doc/book/en/development/migration.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/migration.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -74,12 +74,12 @@
   if the user answers yes, false otherwise (always returns true in
   non-interactive mode)
 
-* the function `_`, it is equivalent to `unicode` allowing to flag the strings
-  to internationalize in the migration scripts.
+* `_()` is equivalent to `unicode` allowing to flag the strings to
+  internationalize in the migration scripts.
 
 In the `repository` scripts, the following identifiers are also defined:
 
-* `checkpoint`, request confirming and executing a "commit" at checking point
+* `commit(ask_confirm=True)`, request confirming and executing a "commit"
 
 * `schema`, instance schema (readen from the database)
 
--- a/doc/book/en/development/testing.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/testing.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -6,27 +6,90 @@
 .. toctree::
    :maxdepth: 1
 
-
 Unit tests
 ----------
 
-*CubicWeb* framework provides essentially two Python test classes in the
-module `cubicweb.devtools.apptest`:
+The *CubicWeb* framework provides the `CubicWebTC` test base class in
+the module `cubicweb.devtools.testlib`.
+
+Tests shall be put into the mycube/test directory. Additional test
+data shall go into mycube/test/data.
+
+It is much advised to write tests concerning entities methods, hooks
+and operations, security. The CubicWebTC base class has convenience
+methods to help test all of this.
+
+.. note::
+
+  In the realm of views, there is not much to do but check that the
+  views are valid XHTML.  See :ref:`automatic_views_tests` for
+  details. Integration of CubicWeb tests with UI testing tools such as
+  `selenium`_ are currently under invesitgation.
+
+.. _selenium: http://seleniumhq.org/projects/ide/
 
-* `EnvBasedTC`, to simulate a complete environment (web + repository)
-* `RepositoryBasedTC`, to simulate a repository environment only
+Most unit tests need a live database to work against. This is achieved
+by CubicWeb using automatically sqlite (bundled with Python, see
+http://docs.python.org/library/sqlite3.html) as a backend.
+
+The database is stored in the mycube/test/tmpdb,
+mycube/test/tmpdb-template files. If it does not (yet) exists, it will
+be built automatically when the test suit starts.
+
+.. warning::
 
-Those two classes almost have the same interface and offer numerous
-methods to write tests rapidly and efficiently.
+  Whenever the schema changes (new entities, attributes, relations)
+  one must delete these two files. Changes concerned only with entity
+  or relation type properties (constraints, cardinalities,
+  permissions) and generally dealt with using the
+  `sync_schema_props_perms()` fonction of the migration environment
+  need not a database regeneration step.
+
+Unit test by example
+````````````````````
 
-XXX FILLME describe API
+We start with an example extracted from the keyword cube (available
+from http://www.cubicweb.org/project/cubicweb-keyword).
+
+.. sourcecode:: python
+
+    from cubicweb.devtools.testlib import CubicWebTC
+    from cubicweb import ValidationError
+
+    class ClassificationHooksTC(CubicWebTC):
+
+        def setup_database(self):
+            req = self.request()
+            group_etype = req.execute('Any X WHERE X name "CWGroup"').get_entity(0,0)
+            c1 = req.create_entity('Classification', name=u'classif1',
+                                   classifies=group_etype)
+            user_etype = req.execute('Any X WHERE X name "CWUser"').get_entity(0,0)
+            c2 = req.create_entity('Classification', name=u'classif2',
+                                   classifies=user_etype)
+            self.kw1 = req.create_entity('Keyword', name=u'kwgroup', included_in=c1)
+            self.kw2 = req.create_entity('Keyword', name=u'kwuser', included_in=c2)
 
-In most of the cases, you will inherit `EnvBasedTC` to write Unittest or
-functional tests for your entities, views, hooks, etc...
+        def test_cannot_create_cycles(self):
+            # direct obvious cycle
+            self.assertRaises(ValidationError, self.kw1.set_relations,
+                              subkeyword_of=self.kw1)
+            # testing indirect cycles
+            kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
+                               'SK subkeyword_of K WHERE C name "classif1", K eid %s'
+                               % self.kw1.eid).get_entity(0,0)
+            self.kw1.set_relations(subkeyword_of=kw3)
+            self.assertRaises(ValidationError, self.commit)
 
-XXX pytestconf.py & options (e.g --source to use a different db
-backend than sqlite)
+The test class defines a `setup_database` method which populates the
+database with initial data. Each test of the class runs with this
+pre-populated database.
 
+The test case itself checks that an Operation does it job of
+preventing cycles amongst Keyword entities.
+
+
+XXX ref to advanced use case
+XXX apycot plug
 
 Managing connections or users
 +++++++++++++++++++++++++++++
@@ -62,23 +125,78 @@
     # the default admin connection and one may be tempted to close it
     self.restore_connection()
 
-Do not use the references kept to the entities created with a connection from another.
+.. warning::
 
+   Do not use the references kept to the entities created with a
+   connection from another !
+
+XXX the new context manager ?
 
 Email notifications tests
 -------------------------
-When running tests potentially generated e-mails are not really
-sent but is found in the list `MAILBOX` of module `cubicweb.devtools.apptest`.
-This list is reset at each test *setUp* (by the setUp of classes `EnvBasedTC`
-and `RepositoryBasedTC`).
 
+When running tests potentially generated e-mails are not really sent
+but is found in the list `MAILBOX` of module
+`cubicweb.devtools.testlib`.
 
 You can test your notifications by analyzing the contents of this list, which
 contains objects with two attributes:
+
 * `recipients`, the list of recipients
 * `msg`, object email.Message
 
+Let us look at simple example from the ``blog`` cube.
 
-Automatic testing
------------------
-XXXFILLME
+.. sourcecode:: python
+
+    from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
+
+    class BlogTestsCubicWebTC(CubicWebTC):
+        """test blog specific behaviours"""
+
+        def test_notifications(self):
+            req = self.request()
+            cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
+                                description=u'cubicweb is beautiful')
+            blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
+                                             content=u'cubicweb hop')
+            blog_entry_1.set_relations(entry_of=cubicweb_blog)
+            blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
+                                             content=u'cubicweb yes')
+            blog_entry_2.set_relations(entry_of=cubicweb_blog)
+            self.assertEquals(len(MAILBOX), 0)
+            self.commit()
+            self.assertEquals(len(MAILBOX), 2)
+            mail = MAILBOX[0]
+            self.assertEquals(mail.subject, '[data] hop')
+            mail = MAILBOX[1]
+            self.assertEquals(mail.subject, '[data] yes')
+
+.. _automatic_views_tests:
+
+Automatic views testing
+-----------------------
+
+This is done automatically with the AutomaticWebTest class. At cube
+creation time, the mycube/test/test_mycube.py file contains such a
+test. The code here has to be uncommented to be usable, without
+further modification.
+
+XXX more to come
+
+
+Using Pytest
+````````````
+
+.. automodule:: logilab.common.testlib
+.. autoclass:: logilab.common.testlib.TestCase
+   :members:
+
+XXX pytestconf.py & options (e.g --source to use a different db
+backend than sqlite)
+
+CubicWebTC API
+``````````````
+.. autoclass:: cubicweb.devtools.testlib.CubicWebTC
+   :members:
+
--- a/doc/book/en/development/webstdlib/baseviews.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/webstdlib/baseviews.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -12,6 +12,7 @@
 
 HTML views
 ~~~~~~~~~~
+
 Special views
 `````````````
 
@@ -43,6 +44,7 @@
 
 List
 `````
+
 *list*
     This view displays a list of entities by creating a HTML list (`<ul>`)
     and call the view `listitem` for each entity of the result set.
@@ -50,12 +52,12 @@
 *listitem*
     This view redirects by default to the `outofcontext` view.
 
-*adaptedlist*
+*sameetypelist*
     This view displays a list of entities of the same type, in HTML section (`<div>`)
-    and call the view `adaptedlistitem` for each entity of the result set.
+    and call the view `sameetypelistitem` for each entity of the result set.
 
-*adaptedlistitem*
-    This view redirects by default to the `outofcontext` view.
+*sameetypelistitem*
+    This view redirects by default to the `listitem` view.
 
 *csv*
     This view applies to entity groups, which are individually
--- a/doc/book/en/development/webstdlib/primary.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/development/webstdlib/primary.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -1,90 +1,162 @@
 .. _primary:
 
-The default 'primary' view (:mod:`cubicweb.web.views.primary`)
----------------------------------------------------------------
+The Primary View
+-----------------
+
+(:mod:`cubicweb.web.views.primary`)
+
+By default, *CubicWeb* provides a view that fits every available
+entity type. This is the first view you might be interested in
+modifying. It is also one of the richest and most complex.
+
+It is automatically selected on a one line result set containing an
+entity.
 
-The primary view of an entity is the view called by default when a single
-entity is in the result set and needs to be displayed.
+This view is supposed to render a maximum of informations about the
+entity.
+
+.. _primary_view_layout:
+
+Layout
+``````
 
-This view is supposed to render a maximum of informations about the entity.
+The primary view has the following layout.
+
+.. image:: ../../images/primaryview_template.png
+
+.. _primary_view_configuration:
+
+Primary view configuration
+``````````````````````````
 
-Beware when overriding this top level `cell_call` in a primary because
-you will loose a bunch of functionnality that automatically comes with
-it : `in-context` boxes, related boxes, some navigation, some
-displaying of the metadata, etc. It might be interresting to
-understand the implementation fo the `cell_call` to override specifics
-bits of it.
+If you want to customize the primary view of an entity, overriding the primary
+view class may not be necessary. For simple adjustments (attributes or relations
+display locations and styles), a much simpler way is to use uicfg.
+
+Attributes/relations display location
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In the primary view, there are 3 sections where attributes and
+relations can be displayed (represented in pink in the image above):
+
+* attributes
+* relations
+* sideboxes
+
+**Attributes** can only be displayed in the attributes section (default
+  behavior). They can also be hidden.
 
-Rendering methods and attributes for ``PrimaryView``
-----------------------------------------------------
+For instance, to hide the ``title`` attribute of the ``Blog`` entity:
+
+.. sourcecode:: python
+
+   from cubicweb.web import uicfg
+   uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
+
+**Relations** can be either displayed in one of the three sections or hidden.
 
-By default, *CubicWeb* provides a primary view for every available
-entity type. This is the first view you might be interested in
-modifying.
+For relations, there are two methods:
+
+* ``tag_object_of`` for modifying the primary view of the object
+* ``tag_subject_of`` for modifying the primary view of the subject
 
-Let's have a quick look at the EntityView ``PrimaryView`` as well as
-its rendering method
+These two methods take two arguments:
+
+* a triplet ``(subject, relation_name, object)``, where subject or object can be replaced with ``'*'``
+* the section name or ``hidden``
 
 .. sourcecode:: python
 
-    class PrimaryView(EntityView):
-        """the full view of an non final entity"""
-        __regid__ = 'primary'
-        title = _('primary')
-        show_attr_label = True
-        show_rel_label = True
-        skip_none = True
-        rsection = uicfg.primaryview_section
-        display_ctrl = uicfg.primaryview_display_ctrl
-        main_related_section = True
+   pv_section = uicfg.primaryview_section
+   # hide every relation `entry_of` in the `Blog` primary view
+   pv_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
+
+   # display `entry_of` relations in the `relations`
+   # section in the `BlogEntry` primary view
+   pv_section.tag_subject_of(('BlogEntry', 'entry_of', '*'), 'relations')
+
+
+Display content
+^^^^^^^^^^^^^^^
+
+You can use ``primaryview_display_ctrl`` to customize the display of attributes
+or relations. Values of ``primaryview_display_ctrl`` are dictionaries.
+
+
+Common keys for attributes and relations are:
 
-        ...
+* ``vid``: specifies the regid of the view for displaying the attribute or the relation.
+
+  If ``vid`` is not specified, the default value depends on the section:
+    * ``attributes`` section: 'reledit' view
+    * ``relations`` section: 'autolimited' view
+    * ``sideboxes`` section: 'sidebox' view
 
-    def cell_call(self, row, col):
-        self.row = row
-        self.maxrelated = self._cw.property_value('navigation.related-limit')
-        entity = self.complete_entity(row, col)
-        self.render_entity(entity)
+* ``order``: int used to control order within a section. When not specified,
+  automatically set according to order in which tags are added.
+
+.. sourcecode:: python
+
+   # let us remind the schema of a blog entry
+   class BlogEntry(EntityType):
+       title = String(required=True, fulltextindexed=True, maxsize=256)
+       publish_date = Date(default='TODAY')
+       content = String(required=True, fulltextindexed=True)
+       entry_of = SubjectRelation('Blog', cardinality='?*')
 
-    def render_entity(self, entity):
-        self.render_entity_title(entity)
-        self.render_entity_metadata(entity)
-        # entity's attributes and relations, excluding meta data
-        # if the entity isn't meta itself
-        boxes = self._prepare_side_boxes(entity)
-        if boxes or hasattr(self, 'render_side_related'):
-            self.w(u'<table width="100%"><tr><td style="width: 75%">')
-        self.render_entity_summary(entity)
-        self.w(u'<div class="mainInfo">')
-        self.content_navigation_components('navcontenttop')
-        self.render_entity_attributes(entity)
-        if self.main_related_section:
-            self.render_entity_relations(entity)
-        self.w(u'</div>')
-        # side boxes
-        if boxes or hasattr(self, 'render_side_related'):
-            self.w(u'</td><td>')
-            self.w(u'<div class="primaryRight">')
-            if hasattr(self, 'render_side_related'):
-                warn('render_side_related is deprecated')
-                self.render_side_related(entity, [])
-            self.render_side_boxes(boxes)
-            self.w(u'</div>')
-            self.w(u'</td></tr></table>')
-        self.content_navigation_components('navcontentbottom')
+   # now, we want to show attributes
+   # with an order different from that in the schema definition
+   view_ctrl = uicfg.primaryview_display_ctrl
+   for index, attr in enumerate('title', 'content', 'publish_date'):
+       view_ctrl.tag_attribute(('BlogEntry', attr), {'order': index})
+
+Keys for relations only:
+
+* ``label``: label for the relations section or side box
+
+* ``showlabel``: boolean telling whether the label is displayed
+
+* ``limit``: boolean telling if the results should be limited. If so, a link to all results is displayed
+
+* ``filter``: callback taking the related result set as argument and returning it filtered
+
+.. sourcecode:: python
+
+   pv_section = uicfg.primaryview_section
+   # in `CWUser` primary view, display `created_by`
+   # relations in relations section
+   pv_section.tag_object_of(('*', 'created_by', 'CWUser'), 'relations')
 
-    ...
+   # display this relation as a list, sets the label,
+   # limit the number of results and filters on comments
+   def filter_comment(rset):
+       return rset.filtered_rset(lambda x: x.e_schema == 'Comment')
+   pv_ctrl = uicfg.primaryview_display_ctrl
+   pv_ctrl.tag_object_of(('*', 'created_by', 'CWUser'),
+                         {'vid': 'list', 'label': _('latest comment(s):'),
+                          'limit': True,
+                          'filter': filter_comment})
 
-``cell_call`` is executed for each entity of a result set.
+.. warning:: with the ``primaryview_display_ctrl`` rtag, the subject or the
+   object of the relation is ignored for respectively ``tag_object_of`` or
+   ``tag_subject_of``. To avoid warnings during execution, they should be set to
+   ``'*'``.
 
-The methods you want to modify while customizing a ``PrimaryView`` are:
+Rendering methods and attributes
+````````````````````````````````
+
+The basic layout of a primary view is as in the
+:ref:`primary_view_layout` section. This layout is actually drawn by
+the `render_entity` method.
+
+The methods you may want to modify while customizing a ``PrimaryView``
+are:
 
 *render_entity_title(self, entity)*
-    Renders the entity title based on the assumption that the method
-    ``def dc_title(self)`` is implemented for the given entity type.
+    Renders the entity title using the ``def dc_title(self)`` method.
 
 *render_entity_metadata(self, entity)*
-    Renders the entity metadata by calling the 'metadata' view on the
+    Renders the entity metadata by calling the ``metadata`` view on the
     entity. This generic view is in cubicweb.views.baseviews.
 
 *render_entity_attributes(self, entity)*
@@ -92,23 +164,24 @@
     attribute of type `Password` and `Bytes`. The skip_none class
     attribute controls the display of None valued attributes.
 
-*content_navigation_components(self, context)*
-    This method is applicable only for entity type implementing the interface
-    `IPrevNext`. This interface is for entities which can be linked to a previous
-    and/or next entity. This methods will render the navigation links between
-    entities of this type, either at the top or at the bottom of the page
-    given the context (navcontent{top|bottom}).
-
 *render_entity_relations(self, entity)*
     Renders all the relations of the entity in the main section of the page.
 
 *render_side_boxes(self, entity, boxes)*
-    Renders all the relations of the entity in a side box. This is equivalent
-    to *render_entity_relations* in addition to render the relations
-    in a box.
+    Renders relations of the entity in a side box.
+
+The placement of relations in the relations section or in side boxes
+can be controlled through the :ref:`primary_view_configuration` mechanism.
 
-Also, please note that by setting the following attributes in your class,
-you can already customize some of the rendering:
+*content_navigation_components(self, context)*
+    This method is applicable only for entity type implementing the interface
+    `IPrevNext`. This interface is for entities which can be linked to a previous
+    and/or next entity. This method will render the navigation links between
+    entities of this type, either at the top or at the bottom of the page
+    given the context (navcontent{top|bottom}).
+
+Also, please note that by setting the following attributes in your
+subclass, you can already customize some of the rendering:
 
 *show_attr_label*
     Renders the attribute label next to the attribute value if set to True.
@@ -126,8 +199,5 @@
 
 A good practice is for you to identify the content of your entity type for which
 the default rendering does not answer your need so that you can focus on the specific
-method (from the list above) that needs to be modified. We do not recommand you to
-overwrite ``render_entity`` as you might potentially loose the benefits of the side
-boxes handling.
-
-.. XXX talk about uicfg.rdisplay
+method (from the list above) that needs to be modified. We do not advise you to
+overwrite ``render_entity`` unless you want a completely different layout.
--- a/doc/book/en/index.rst	Tue Apr 13 19:43:30 2010 +0200
+++ b/doc/book/en/index.rst	Tue Apr 13 19:43:51 2010 +0200
@@ -44,7 +44,7 @@
 =================
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 3
 
    intro/index
    development/index
@@ -58,4 +58,4 @@
 * the :ref:`modindex`,
 * and the :ref:`search`.
 
-.. |cubicweb| replace:: *CubicWeb*
\ No newline at end of file
+.. |cubicweb| replace:: *CubicWeb*
--- a/server/hook.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/server/hook.py	Tue Apr 13 19:43:51 2010 +0200
@@ -369,14 +369,14 @@
       revert things (including the operation which made fail the commit)
 
     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
+      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
 
-    order of operations may be important, and is controlled according to:
-    * operation's class
+    order of operations may be important, and is controlled according to
+    the insert_index's method output
     """
 
     def __init__(self, session, **kwargs):
@@ -475,7 +475,7 @@
     try:
         session.transaction_data[datakey].add(value)
     except KeyError:
-        opcls(session, *opkwargs)
+        opcls(session, **opkwargs)
         session.transaction_data[datakey] = set((value,))
 
 
--- a/server/session.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/server/session.py	Tue Apr 13 19:43:51 2010 +0200
@@ -277,7 +277,7 @@
 
     def system_sql(self, sql, args=None, rollback_on_failure=True):
         """return a sql cursor on the system database"""
-        if not sql.split(None, 1)[0].upper() == 'SELECT':
+        if sql.split(None, 1)[0].upper() != 'SELECT':
             self.mode = 'write'
         source = self.pool.source('system')
         try:
--- a/server/sources/native.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/server/sources/native.py	Tue Apr 13 19:43:51 2010 +0200
@@ -475,13 +475,15 @@
         etype = entity.__regid__
         for attr, storage in self._storages.get(etype, {}).items():
             try:
-                if attr in entity.edited_attributes:
+                edited = entity.edited_attributes
+            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
-            except AttributeError:
-                assert event == 'deleted'
-                getattr(storage, 'entity_deleted')(entity, attr)
         try:
             yield # 2/ execute the source's instructions
         finally:
@@ -1248,9 +1250,9 @@
 %s
 DROP TABLE entities;
 DROP TABLE deleted_entities;
-DROP TABLE transactions;
 DROP TABLE tx_entity_actions;
 DROP TABLE tx_relation_actions;
+DROP TABLE transactions;
 """ % helper.sql_drop_sequence('entities_id_seq')
 
 
--- a/server/sources/storages.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/server/sources/storages.py	Tue Apr 13 19:43:51 2010 +0200
@@ -1,6 +1,8 @@
 """custom storages for the system source"""
 from os import unlink, path as osp
 
+from yams.schema import role_name
+
 from cubicweb import Binary
 from cubicweb.server.hook import Operation
 
@@ -54,10 +56,27 @@
 # * better file path attribution
 # * handle backup/restore
 
+def uniquify_path(dirpath, basename):
+    """return a unique file name for `basename` in `dirpath`, or None
+    if all attemps failed.
+
+    XXX subject to race condition.
+    """
+    path = osp.join(dirpath, basename)
+    if not osp.isfile(path):
+        return path
+    base, ext = osp.splitext(path)
+    for i in xrange(1, 256):
+        path = '%s%s%s' % (base, i, ext)
+        if not osp.isfile(path):
+            return path
+    return None
+
 class BytesFileSystemStorage(Storage):
     """store Bytes attribute value on the file system"""
-    def __init__(self, defaultdir):
+    def __init__(self, defaultdir, fsencoding='utf-8'):
         self.default_directory = defaultdir
+        self.fsencoding = fsencoding
 
     def callback(self, source, value):
         """sql generator callback when some attribute with a custom storage is
@@ -102,16 +121,27 @@
         DeleteFileOp(entity._cw, filepath=self.current_fs_path(entity, attr))
 
     def new_fs_path(self, entity, attr):
-        fspath = osp.join(self.default_directory, '%s_%s' % (entity.eid, attr))
-        while osp.exists(fspath):
-            fspath = '_' + fspath
+        # We try to get some hint about how to name the file using attribute's
+        # name metadata, so we use the real file name and extension when
+        # available. Keeping the extension is useful for example in the case of
+        # PIL processing that use filename extension to detect content-type, as
+        # well as providing more understandable file names on the fs.
+        basename = [str(entity.eid), attr]
+        name = entity.attr_metadata(attr, 'name')
+        if name is not None:
+            basename.append(name.encode(self.fsencoding))
+        fspath = uniquify_path(self.default_directory, '_'.join(basename))
+        if fspath is None:
+            msg = entity._cw._('failed to uniquify path (%s, %s)') % (
+                dirpath, '_'.join(basename))
+            raise ValidationError(entity.eid, {role_name(attr, 'subject'): msg})
         return fspath
 
     def current_fs_path(self, entity, attr):
         sysource = entity._cw.pool.source('system')
         cu = sysource.doexec(entity._cw,
                              'SELECT cw_%s FROM cw_%s WHERE cw_eid=%s' % (
-                                 attr, entity.__regid__, entity.eid))
+                             attr, entity.__regid__, entity.eid))
         rawvalue = cu.fetchone()[0]
         if rawvalue is None: # no previous value
             return self.new_fs_path(entity, attr)
--- a/server/sqlutils.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/server/sqlutils.py	Tue Apr 13 19:43:51 2010 +0200
@@ -113,8 +113,6 @@
     from cubicweb.server.sources import native
     output = []
     w = output.append
-    w(native.sql_drop_schema(driver))
-    w('')
     if text_index:
         dbhelper = db.get_db_helper(driver)
         w(dbhelper.sql_drop_fti())
@@ -122,6 +120,8 @@
     w(dropschema2sql(schema, prefix=SQL_PREFIX,
                      skip_entities=skip_entities,
                      skip_relations=skip_relations))
+    w('')
+    w(native.sql_drop_schema(driver))
     return '\n'.join(output)
 
 
--- a/server/ssplanner.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/server/ssplanner.py	Tue Apr 13 19:43:51 2010 +0200
@@ -577,7 +577,7 @@
             result[i] = newrow
         # update entities
         for eid, edef in edefs.iteritems():
-            repo.glob_update_entity(session, edef, self.attributes)
+            repo.glob_update_entity(session, edef, set(self.attributes))
         return result
 
 def _handle_relterm(info, row, newrow):
--- a/server/test/unittest_repository.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/server/test/unittest_repository.py	Tue Apr 13 19:43:51 2010 +0200
@@ -400,6 +400,27 @@
             self.assertRaises(AssertionError, req.create_entity,
                               'EmailAddress', address=u'a@b.fr')
 
+    def test_multiple_edit_set_attributes(self):
+        """make sure edited_attributes doesn't get cluttered
+        by previous entities on multiple set
+        """
+        # local hook
+        class DummyBeforeHook(Hook):
+            _test = self # keep reference to test instance
+            __regid__ = 'dummy-before-hook'
+            __select__ = Hook.__select__ & implements('Affaire')
+            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
+        with self.temporary_appobjects(DummyBeforeHook):
+            req = self.request()
+            req.create_entity('Affaire', ref=u'AFF01')
+            req.create_entity('Affaire', ref=u'AFF02')
+            req.execute('SET A duration 10 WHERE A is Affaire')
+
 
 class DataHelpersTC(CubicWebTC):
 
--- a/web/facet.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/web/facet.py	Tue Apr 13 19:43:51 2010 +0200
@@ -382,7 +382,9 @@
                 return ()
         finally:
             rqlst.recover()
-        return self.rset_vocabulary(rset)
+        # don't call rset_vocabulary on empty result set, it may be an empty
+        # *list* (see rqlexec implementation)
+        return rset and self.rset_vocabulary(rset)
 
     def possible_values(self):
         """return a list of possible values (as string since it's used to
@@ -471,7 +473,9 @@
                 return ()
         finally:
             rqlst.recover()
-        return self.rset_vocabulary(rset)
+        # don't call rset_vocabulary on empty result set, it may be an empty
+        # *list* (see rqlexec implementation)
+        return rset and self.rset_vocabulary(rset)
 
     def rset_vocabulary(self, rset):
         _ = self._cw._
--- a/web/formfields.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/web/formfields.py	Tue Apr 13 19:43:51 2010 +0200
@@ -70,7 +70,7 @@
     :required:
        bool flag telling if the field is required or not.
     :value:
-       field's value, used when no value specified by other means. XXX explain
+       field value (may be an actual value, a default value or nothing)
     :choices:
        static vocabulary for this field. May be a list of values or a list of
        (label, value) tuples if specified.
--- a/web/uicfg.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/web/uicfg.py	Tue Apr 13 19:43:51 2010 +0200
@@ -11,6 +11,8 @@
 Primary view configuration
 ``````````````````````````
 
+XXX section moved to the doc, is maintained there
+
 If you want to customize the primary view of an entity, overriding the primary
 view class may not be necessary. For simple adjustments (attributes or relations
 display locations and styles), a much simpler way is to use uicfg.
--- a/web/views/baseviews.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/web/views/baseviews.py	Tue Apr 13 19:43:51 2010 +0200
@@ -297,7 +297,7 @@
 
 
 class SameETypeListView(EntityView):
-    """list of entities of the same type, when asked explicitly for adapted list
+    """list of entities of the same type, when asked explicitly for same etype list
     view (for instance, display gallery if only images)
     """
     __regid__ = 'sameetypelist'
--- a/web/views/primary.py	Tue Apr 13 19:43:30 2010 +0200
+++ b/web/views/primary.py	Tue Apr 13 19:43:51 2010 +0200
@@ -102,7 +102,6 @@
         self.content_navigation_components('ctxtoolbar')
 
     def render_entity_metadata(self, entity):
-        # XXX deprecated
         entity.view('metadata', w=self.w)
 
     def render_entity_summary(self, entity):