backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 01 Feb 2011 11:52:10 +0100
changeset 6931 0af44a38fe41
parent 6884 6fa712e9dfa5 (current diff)
parent 6930 118881289a31 (diff)
child 6932 1599ad09624f
backport stable
devtools/fill.py
i18n/en.po
server/checkintegrity.py
server/repository.py
server/sources/__init__.py
server/sources/ldapuser.py
server/sources/native.py
utils.py
view.py
web/formwidgets.py
web/views/primary.py
web/views/startup.py
--- a/cwctl.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/cwctl.py	Tue Feb 01 11:52:10 2011 +0100
@@ -42,11 +42,18 @@
 from logilab.common.shellutils import ASK
 
 from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
+from cubicweb.utils import support_args
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CWDEV, CONFIGURATIONS
 from cubicweb.toolsutils import Command, rm, create_dir, underline_title
 from cubicweb.__pkginfo__ import version
 
-CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', version=version)
+if support_args(CommandLine, 'check_duplicated_command'):
+    # don't check duplicated commands, it occurs when reloading site_cubicweb
+    CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
+                        version=version, check_duplicated_command=False)
+else:
+    CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
+                        version=version)
 
 def wait_process_end(pid, maxtry=10, waittime=1):
     """wait for a process to actually die"""
--- a/cwvreg.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/cwvreg.py	Tue Feb 01 11:52:10 2011 +0100
@@ -432,7 +432,9 @@
     def poss_visible_objects(self, *args, **kwargs):
         """return an ordered list of possible components"""
         context = kwargs.pop('context')
-        if kwargs.get('rset') is None:
+        if '__cache' in kwargs:
+            cache = kwargs.pop('__cache')
+        elif kwargs.get('rset') is None:
             cache = args[0]
         else:
             cache = kwargs['rset']
@@ -441,9 +443,19 @@
         except AttributeError:
             ctxcomps = super(CtxComponentsRegistry, self).poss_visible_objects(
                 *args, **kwargs)
+            if cache is None:
+                components = []
+                for component in ctxcomps:
+                    cctx = component.cw_propval('context')
+                    if cctx == context:
+                        component.cw_extra_kwargs['context'] = cctx
+                        components.append(component)
+                return components
             cached = cache.__components_cache = {}
             for component in ctxcomps:
-                cached.setdefault(component.cw_propval('context'), []).append(component)
+                cctx = component.cw_propval('context')
+                component.cw_extra_kwargs['context'] = cctx
+                cached.setdefault(cctx, []).append(component)
         thisctxcomps = cached.get(context, ())
         # XXX set context for bw compat (should now be taken by comp.render())
         for component in thisctxcomps:
--- a/debian/control	Mon Jan 24 19:09:42 2011 +0100
+++ b/debian/control	Tue Feb 01 11:52:10 2011 +0100
@@ -7,15 +7,15 @@
            Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
            Aurélien Campéas <aurelien.campeas@logilab.fr>,
            Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 5), python-dev (>=2.5), python-central (>= 0.5)
-Standards-Version: 3.8.0
+Build-Depends: debhelper (>= 7), python (>= 2.5), python-central (>= 0.5)
+Standards-Version: 3.9.1
 Homepage: http://www.cubicweb.org
 XS-Python-Version: >= 2.5, << 2.7
 
 Package: cubicweb
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
 XB-Recommends: (postgresql, postgresql-plpython) | mysql | sqlite3
 Recommends: postgresql | mysql | sqlite3
 Description: the complete CubicWeb framework
@@ -33,8 +33,8 @@
 Conflicts: cubicweb-multisources
 Replaces: cubicweb-multisources
 Provides: cubicweb-multisources
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.3.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
-Recommends: pyro (< 4.0.0), cubicweb-documentation (= ${source:Version})
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.3.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -46,7 +46,7 @@
 Package: cubicweb-postgresql-support
 Architecture: all
 # postgresql-client packages for backup/restore of non local database
-Depends: python-psycopg2, postgresql-client
+Depends: ${misc:Depends}, python-psycopg2, postgresql-client
 Description: postgres support for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -56,7 +56,7 @@
 Package: cubicweb-mysql-support
 Architecture: all
 # mysql-client packages for backup/restore of non local database
-Depends: python-mysqldb, mysql-client
+Depends: ${misc:Depends}, python-mysqldb, mysql-client
 Description: mysql support for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -68,8 +68,8 @@
 Architecture: all
 XB-Python-Version: ${python:Versions}
 Provides: cubicweb-web-frontend
-Depends: ${python:Depends}, cubicweb-web (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-twisted-web
-Recommends: pyro (< 4.0.0), cubicweb-documentation (= ${source:Version})
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-web (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-twisted-web
+Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
 Description: twisted-based web interface for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -82,7 +82,7 @@
 Package: cubicweb-web
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3)
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3)
 Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-imaging
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -97,7 +97,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.1), python-rql (>= 0.28.0), python-lxml
+Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.1), python-rql (>= 0.28.0), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
@@ -111,7 +111,7 @@
 Package: cubicweb-ctl
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version})
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version})
 Description: tool to manage the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -123,7 +123,7 @@
 Package: cubicweb-dev
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-web (= ${source:Version}), python-pysqlite2
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-web (= ${source:Version}), python-pysqlite2
 Suggests: w3c-dtd-xhtml
 Description: tests suite and development tools for the CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -133,7 +133,6 @@
 
 
 Package: cubicweb-documentation
-Architecture: all
 Recommends: doc-base
 Description: documentation for the CubicWeb framework
  CubicWeb is a semantic web application framework.
--- a/debian/cubicweb-ctl.cubicweb.init	Mon Jan 24 19:09:42 2011 +0100
+++ b/debian/cubicweb-ctl.cubicweb.init	Tue Feb 01 11:52:10 2011 +0100
@@ -2,8 +2,8 @@
 
 ### BEGIN INIT INFO
 # Provides:          cubicweb
-# Required-Start:    $syslog $local_fs $network
-# Required-Stop:     $syslog $local_fs $network
+# Required-Start:    $remote_fs $syslog $local_fs $network
+# Required-Stop:     $remote_fs $syslog $local_fs $network
 # Should-Start:      $postgresql $pyro-nsd
 # Should-Stop:       $postgresql $pyro-nsd
 # Default-Start:     2 3 4 5
@@ -29,7 +29,7 @@
     status)
         python -W ignore /usr/bin/cubicweb-ctl status
         ;;
-    *)
+    start|stop|restart|*)
         python -W ignore /usr/bin/cubicweb-ctl $1 --force
         ;;
 esac
--- a/debian/cubicweb-ctl.dirs	Mon Jan 24 19:09:42 2011 +0100
+++ b/debian/cubicweb-ctl.dirs	Tue Feb 01 11:52:10 2011 +0100
@@ -4,7 +4,6 @@
 etc/bash_completion.d
 usr/bin
 usr/share/doc/cubicweb-ctl
-var/run/cubicweb
 var/log/cubicweb
 var/lib/cubicweb/backup
 var/lib/cubicweb/instances
--- a/debian/cubicweb-ctl.prerm	Mon Jan 24 19:09:42 2011 +0100
+++ b/debian/cubicweb-ctl.prerm	Tue Feb 01 11:52:10 2011 +0100
@@ -2,8 +2,7 @@
  
 case "$1" in
     purge)
-        rm -rf /etc/cubicweb.d/
-    	rm -rf /var/run/cubicweb/
+	rm -rf /etc/cubicweb.d/
 	rm -rf /var/log/cubicweb/
 	rm -rf /var/lib/cubicweb/
     ;;
--- a/devtools/fill.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/devtools/fill.py	Tue Feb 01 11:52:10 2011 +0100
@@ -27,7 +27,7 @@
 
 from logilab.common import attrdict
 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
-                              IntervalBoundConstraint, BoundConstraint,
+                              IntervalBoundConstraint, BoundaryConstraint,
                               Attribute, actual_value)
 from rql.utils import decompose_b26 as base_decompose_b26
 
@@ -190,10 +190,12 @@
             minvalue = maxvalue - (index * step) # i.e. randint(-index, 0)
         return choice(list(custom_range(minvalue, maxvalue, step)))
 
-    def _actual_boundary(self, entity, boundary):
+    def _actual_boundary(self, entity, attrname, boundary):
         if isinstance(boundary, Attribute):
             # ensure we've a value for this attribute
-            self.generate_attribute_value(entity, boundary.attr)
+            entity[attrname] = None # infinite loop safety belt
+            if not boundary.attr in entity:
+                self.generate_attribute_value(entity, boundary.attr)
             boundary = actual_value(boundary, entity)
         return boundary
 
@@ -201,13 +203,13 @@
         minvalue = maxvalue = None
         for cst in self.eschema.rdef(attrname).constraints:
             if isinstance(cst, IntervalBoundConstraint):
-                minvalue = self._actual_boundary(entity, cst.minvalue)
-                maxvalue = self._actual_boundary(entity, cst.maxvalue)
-            elif isinstance(cst, BoundConstraint):
+                minvalue = self._actual_boundary(entity, attrname, cst.minvalue)
+                maxvalue = self._actual_boundary(entity, attrname, cst.maxvalue)
+            elif isinstance(cst, BoundaryConstraint):
                 if cst.operator[0] == '<':
-                    maxvalue = self._actual_boundary(entity, cst.boundary)
+                    maxvalue = self._actual_boundary(entity, attrname, cst.boundary)
                 else:
-                    minvalue = self._actual_boundary(entity, cst.boundary)
+                    minvalue = self._actual_boundary(entity, attrname, cst.boundary)
         return minvalue, maxvalue
 
     def get_choice(self, entity, attrname):
--- a/doc/book/en/admin/instance-config.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/admin/instance-config.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -4,6 +4,11 @@
 Configure an instance
 =====================
 
+On a Unix system, the instances are usually stored in the directory
+:file:`/etc/cubicweb.d/`. During development, the
+:file:`~/etc/cubicweb.d/` directory is looked up, as well as the paths
+in :envvar:`CW_INSTANCES_DIR` environment variable.
+
 While creating an instance, a configuration file is generated in::
 
     $ (CW_INSTANCES_DIR) / <instance> / <configuration name>.conf
--- a/doc/book/en/devrepo/cubes/cc-newcube.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/devrepo/cubes/cc-newcube.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -1,5 +1,5 @@
-Creating a new cube from scratch using :command:`cubicweb-ctl newcube`
-----------------------------------------------------------------------
+Creating a new cube from scratch
+--------------------------------
 
 Let's start by creating the cube environment in which we will develop ::
 
@@ -14,7 +14,7 @@
   hg ci
 
 If all went well, you should see the cube you just created in the list
-returned by ``cubicweb-ctl list`` in the  *Available cubes* section. 
+returned by ``cubicweb-ctl list`` in the  *Available cubes* section.
 If not, please refer to :ref:`ConfigurationEnv`.
 
 To reuse an existing cube, add it to the list named
@@ -24,6 +24,14 @@
 database for the instance is created (import_erschema('MyCube') will
 not properly work otherwise).
 
+On a Unix system, the available cubes are usually stored in the
+directory :file:`/usr/share/cubicweb/cubes`. If you are using the
+cubicweb mercurial repository (:ref:`SourceInstallation`), the cubes
+are searched in the directory
+:file:`/path/to/cubicweb_toplevel/cubes`. In this configuration
+cubicweb itself ought to be located at
+:file:`/path/to/cubicweb_toplevel/cubicweb`.
+
 .. note::
 
     Please note that if you do not wish to use default directory for your cubes
--- a/doc/book/en/devrepo/vreg.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/devrepo/vreg.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -79,6 +79,7 @@
 .. autoclass:: cubicweb.selectors.has_add_permission
 .. autoclass:: cubicweb.selectors.has_mimetype
 .. autoclass:: cubicweb.selectors.is_in_state
+.. autoclass:: cubicweb.selectors.on_transition
 .. autoclass:: cubicweb.selectors.implements
 
 
--- a/doc/book/en/intro/concepts.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/intro/concepts.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -1,3 +1,4 @@
+
 .. -*- coding: utf-8 -*-
 
 .. _Concepts:
@@ -27,14 +28,10 @@
 The `CubicWeb.org Forge`_ offers a large number of cubes developed by the community
 and available under a free software license.
 
-The command :command:`cubicweb-ctl list` displays the list of cubes installed on
-your system.
+.. note::
 
-On a Unix system, the available cubes are usually stored in the directory
-:file:`/usr/share/cubicweb/cubes`. If you're using the cubicweb forest
-(:ref:`SourceInstallation`), the cubes are searched in the directory
-:file:`/path/to/cubicweb_forest/cubes`. The environment variable
-:envvar:`CW_CUBES_PATH` gives additionnal locations where to search for cubes.
+ The command :command:`cubicweb-ctl list` displays the list of cubes
+installed on your system.
 
 .. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
 .. _`cubicweb-blog`: http://www.cubicweb.org/project/cubicweb-blog
@@ -64,12 +61,6 @@
 The command :command:`cubicweb-ctl list` also displays the list of instances
 installed on your system.
 
-On a Unix system, the instances are usually stored in the directory
-:file:`/etc/cubicweb.d/`. During development, the :file:`~/etc/cubicweb.d/`
-directory is looked up, as well as the paths in :envvar:`CW_INSTANCES_DIR`
-environment variable.
-
-
 .. note::
 
   The term application is used to refer to "something that should do something as
@@ -83,28 +74,20 @@
 Data Repository
 ---------------
 
-The data repository [1]_ provides access to one or more data sources (including
-SQL databases, LDAP repositories, other |cubicweb| instance repositories, GAE's
+The data repository [1]_ encapsulates and groups an access to one or
+more data sources (including SQL databases, LDAP repositories, other
+|cubicweb| instance repositories, filesystems, Google AppEngine's
 DataStore, etc).
 
-All interactions with the repository are done using the Relation Query Language
+All interactions with the repository are done using the `Relation Query Language`
 (:ref:`RQL`). The repository federates the data sources and hides them from the
-querier, which does not realize when a query spans accross several data sources
+querier, which does not realize when a query spans several data sources
 and requires running sub-queries and merges to complete.
 
-It is common to run the web engine and the repository in the same process (see
-instances of type all-in-one above), but this is not a requirement. A repository
-can be set up to be accessed remotely using Pyro (`Python Remote Objects`_) and
-act as a server. However, it's important to know if code you're writing is
-executed on the repository side, on our client side (the web engine being a
-client for instance): you don't have the same abilities on both side. On the
-repository side, you can for instance by-pass security checks, which isn't
-possible from client code.
-
-Some logic can be attached to events that happen in the repository,
-like creation of entities, deletion of relations, etc. This is used
-for example to send email notifications when the state of an object
-changes. See :ref:`HookIntro` below.
+Application logic can be mapped to data events happenning within the
+repository, like creation of entities, deletion of relations,
+etc. This is used for example to send email notifications when the
+state of an object changes. See :ref:`HookIntro` below.
 
 .. [1] not to be confused with a Mercurial repository or a Debian repository.
 .. _`Python Remote Objects`: http://pyro.sourceforge.net/
@@ -114,14 +97,19 @@
 Web Engine
 ----------
 
-The web engine replies to http requests and runs the user interface
-and most of the application logic.
+The web engine replies to http requests and runs the user interface.
 
 By default the web engine provides a `CRUD`_ user interface based on
 the data model of the instance. Entities can be created, displayed,
 updated and deleted. As the default user interface is not very fancy,
 it is usually necessary to develop your own.
 
+It is common to run the web engine and the repository in the same
+process (see instances of type all-in-one above), but this is not a
+requirement. A repository can be set up to be accessed remotely using
+Pyro (`Python Remote Objects`_) and act as a standalone server, which
+can be directly accessed or also through a standalone web engine.
+
 .. _`CRUD`: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
 
 .. _SchemaIntro:
@@ -134,24 +122,24 @@
 
 .. _yams: http://www.logilab.org/project/yams/
 
-An `entity type` defines a set of attributes and is used in some relations.
-Attributes may be of the following types: `String`, `Int`, `Float`, `Boolean`,
-`Date`, `Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`.
+An `entity type` defines a sequence of attributes. Attributes may be
+of the following types: `String`, `Int`, `Float`, `Boolean`, `Date`,
+`Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`.
 
-A `relation type` is used to define an oriented binary relation between two
-entity types.  The left-hand part of a relation is named the `subject` and the
-right-hand part is named the `object`.
+A `relation type` is used to define an oriented binary relation
+between entity types.  The left-hand part of a relation is named the
+`subject` and the right-hand part is named the `object`.
 
 A `relation definition` is a triple (*subject entity type*, *relation type*, *object
 entity type*) associated with a set of properties such as cardinality,
 constraints, etc.
 
-Permissions can be set on entity types and relation definition to control who
+Permissions can be set on entity types or relation definition to control who
 will be able to create, read, update or delete entities and relations. Permissions
-are granted to groups (to which users may belong) or using rql expression (if the
+are granted to groups (to which users may belong) or using rql expressions (if the
 rql expression returns some results, the permission is granted).
 
-Some meta-data necessary to the system is added to the data model. That includes
+Some meta-data necessary to the system are added to the data model. That includes
 entities like users and groups, the entities used to store the data model
 itself and attributes like unique identifier, creation date, creator, etc.
 
@@ -169,19 +157,15 @@
 Application objects
 ~~~~~~~~~~~~~~~~~~~
 
-Beside a few core functionalities, almost every feature of the framework is
+Besides a few core functionalities, almost every feature of the framework is
 achieved by dynamic objects (`application objects` or `appobjects`) stored in a
-two-levels registry (the `vregistry`). Each object is affected to a registry with
+two-levels registry. Each object is affected to a registry with
 an identifier in this registry. You may have more than one object sharing an
-identifier in the same registry. At runtime, appobjects are selected in a
-registry according to the context. Selection is done by comparing the *score*
-returned by each appobject's *selector*.
-
-Application objects are stored in the vregistry using a two-level hierarchy :
+identifier in the same registry:
 
   object's `__registry__` : object's `__regid__` : [list of app objects]
 
-In other words, the `vregistry` contains several (sub-)registries which hold a
+In other words, the `registry` contains several (sub-)registries which hold a
 list of appobjects associated to an identifier.
 
 The base class of appobjects is :class:`cubicweb.appobject.AppObject`.
@@ -189,10 +173,14 @@
 Selectors
 ~~~~~~~~~
 
-Each appobject has a selector that is used to compute how well the object fits a
-given context. The better the object fits the context, the higher the score. Scores
-are the glue that ties appobjects to the data model. Using them appropriately is
-an essential part of the construction of well behaved cubes.
+At runtime, appobjects can be selected in a registry according to some
+contextual information. Selection is done by comparing the *score*
+returned by each appobject's *selector*.
+
+The better the object fits the context, the higher the score. Scores
+are the glue that ties appobjects to the data model. Using them
+appropriately is an essential part of the construction of well behaved
+cubes.
 
 |cubicweb| provides a set of basic selectors that may be parametrized.  Also,
 selectors can be combined with the `~` unary operator (negation) and the binary
@@ -200,35 +188,36 @@
 selectors. Of course complex selectors may be combined too. Last but not least, you
 can write your own selectors.
 
-The `vregistry`
+The `registry`
 ~~~~~~~~~~~~~~~
 
-At startup, the `vregistry` inspects a number of directories looking for
-compatible classes definition. After a recording process, the objects are
-assigned to registries so that they can be selected dynamically while the
-instance is running.
+At startup, the `registry` inspects a number of directories looking
+for compatible class definitions. After a recording process, the
+objects are assigned to registries and become available through the
+selection process.
 
 In a cube, application object classes are looked in the following modules or
 packages:
 
 - `entities`
 - `views`
+- `hooks`
 - `sobjects`
 
-
-Once initialized, there are three common ways to retrieve some application object
-from a registry:
+There are three common ways to look up some application object from a
+registry:
 
-* get the most appropriate object by specifying an identifier. In that case, the
-  object with the greatest score is selected. There should always be a single
-  appobject with a greater score than others for a particular context.
+* get the most appropriate object by specifying an identifier and
+  context objects. The object with the greatest score is
+  selected. There should always be a single appobject with a greater
+  score than others for a particular context.
 
-* get all objects applying to a context by specifying a registry. In that case, a
-  list of objects will be returned containing the object with the highest score
-  (> 0) for each identifier in that registry.
+* get all objects applying to a context by specifying a registry. A
+  list of objects will be returned containing the object with the
+  highest score (> 0) for each identifier in that registry.
 
-* get the object within a particular registry/identifier. In that case no
-  selection process is involved, the vregistry will expect to find a single
+* get the object within a particular registry/identifier. No selection
+  process is involved: the registry will expect to find a single
   object in that cell.
 
 
@@ -247,21 +236,6 @@
 emphasize browsing relations.
 
 
-DB-API
-~~~~~~
-
-The repository exposes a `db-api`_ like api but using the RQL instead of SQL.
-
-.. _`db-api`: http://www.python.org/dev/peps/pep-0249/
-
-You basically get a connection using :func:`cubicweb.dbapi.connect` , then
-get a cursor to call its `execute` method which will return result set for the
-given rql query.
-
-You can also get additional information through the connection, such as the
-repository'schema, version configuration, etc.
-
-
 Result set
 ~~~~~~~~~~
 
@@ -285,18 +259,10 @@
 something, eg producing some html, text, xml, pdf, or whatsover that can be
 displayed to a user.
 
-The two main entry points of a view are:
-
-* `call()`, used to render a view on a context with no result set, or on a whole
-  result set
-
-* `cell_call(row, col)`, used to render a view on a the cell with index `row` and
-  `col` of the context's result set (remember result set may be seen as a two
-  dimensions array).
-
-Then view may gets refined into different kind of objects such as `template`,
-`boxes`, `components`, which are more high-level abstraction useful to build
-the user interface in an object oriented way.
+Views actually are partitioned into different kind of objects such as
+`templates`, `boxes`, `components` and proper `views`, which are more
+high-level abstraction useful to build the user interface in an object
+oriented way.
 
 
 .. _HookIntro:
@@ -312,7 +278,7 @@
 
 * managing computed attributes
 
-* enforcing complicated structural invariants
+* enforcing complicated business rules
 
 * real-world side-effects linked to data events (email notification
   being a prime example)
@@ -326,10 +292,9 @@
 
 * it is well-coupled to the rest of the framework
 
-Hooks are also application objects registered on events such as after/before
-add/update/delete on entities/relations, server startup or shutdown, etc. As all
-application objects, they have a selector defining when they should be called or
-not.
+Hooks are also application objects (in the `hooks` registry) and
+selected on events such as after/before add/update/delete on
+entities/relations, server startup or shutdown, etc.
 
 `Operations` may be instantiated by hooks to do further processing at different
 steps of the transaction's commit / rollback, which usually can not be done
--- a/doc/book/en/tutorials/advanced/part01_create-cube.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/tutorials/advanced/part01_create-cube.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -34,8 +34,8 @@
   both 'album' and a way to map file system folders. Entities are
   added to a given folder using the `filed_under` relation.
 
-* `file`, containing `File` and `Image` entity types, gallery view,
-  and a file system import utility.
+* `file`, containing `File` entity type, gallery view, and a file system import
+  utility.
 
 * `zone`, containing the `Zone` entity type for hierarchical geographical
   zones. Entities (including sub-zones) are added to a given zone using the
@@ -54,8 +54,8 @@
 
   .. sourcecode:: python
 
-    __depends__ = {'cubicweb': '>= 3.8.0',
-                   'cubicweb-file': '>= 1.2.0',
+    __depends__ = {'cubicweb': '>= 3.10.0',
+                   'cubicweb-file': '>= 1.9.0',
 		   'cubicweb-folder': '>= 1.1.0',
 		   'cubicweb-person': '>= 1.2.0',
 		   'cubicweb-comment': '>= 1.2.0',
@@ -71,14 +71,17 @@
 
   .. sourcecode:: python
 
-    __depends__ = {'cubicweb': '>= 3.8.0'}
-    __depends_cubes__ = {'file': '>= 1.2.0',
+    __depends__ = {'cubicweb': '>= 3.10.0'}
+    __depends_cubes__ = {'file': '>= 1.9.0',
 		         'folder': '>= 1.1.0',
 		   	 'person': '>= 1.2.0',
 		   	 'comment': '>= 1.2.0',
 		   	 'tag': '>= 1.2.0',
 		   	 'zone': None}
 
+If your cube is packaged for debian, it's a good idea to update the
+`debian/control` file at the same time, so you won't forget it.
+
 
 Step 3: glue everything together in my cube's schema
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -89,34 +92,33 @@
 
     class comments(RelationDefinition):
 	subject = 'Comment'
-	object = ('File', 'Image')
+	object = 'File'
 	cardinality = '1*'
 	composite = 'object'
 
     class tags(RelationDefinition):
 	subject = 'Tag'
-	object = ('File', 'Image')
+	object = 'File'
 
     class filed_under(RelationDefinition):
-	subject = ('File', 'Image')
+	subject = 'File'
 	object = 'Folder'
 
     class situated_in(RelationDefinition):
-	subject = 'Image'
+	subject = 'File'
 	object = 'Zone'
 
     class displayed_on(RelationDefinition):
 	subject = 'Person'
-	object = 'Image'
+	object = 'File'
 
 
 This schema:
 
-* allows to comment and tag on `File` and `Image` entity types by adding the
-  `comments` and `tags` relations. This should be all we've to do for this
-  feature since the related cubes provide 'pluggable section' which are
-  automatically displayed on the primary view of entity types supporting the
-  relation.
+* allows to comment and tag on `File` entity type by adding the `comments` and
+  `tags` relations. This should be all we've to do for this feature since the
+  related cubes provide 'pluggable section' which are automatically displayed on
+  the primary view of entity types supporting the relation.
 
 * adds a `situated_in` relation definition so that image entities can be
   geolocalized.
--- a/doc/book/en/tutorials/advanced/part02_security.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/tutorials/advanced/part02_security.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -46,10 +46,10 @@
 security defined earlier, groups are not enough, we'll need some RQL expression. Here
 is the idea:
 
-* add a `visibility` attribute on Folder, Image and Comment, which may be one of
+* add a `visibility` attribute on Folder, File and Comment, which may be one of
   the value explained above
 
-* add a `may_be_read_by` relation from Folder, Image and Comment to users,
+* add a `may_be_read_by` relation from Folder, File and Comment to users,
   which will define who can see the entity
 
 * security propagation will be done in hook.
@@ -62,7 +62,7 @@
     from yams.constraints import StaticVocabularyConstraint
 
     class visibility(RelationDefinition):
-	subject = ('Folder', 'File', 'Image', 'Comment')
+	subject = ('Folder', 'File', 'Comment')
 	object = 'String'
 	constraints = [StaticVocabularyConstraint(('public', 'authenticated',
 						   'restricted', 'parent'))]
@@ -76,7 +76,7 @@
 	    'delete': ('managers',),
 	    }
 
-	subject = ('Folder', 'File', 'Image', 'Comment',)
+	subject = ('Folder', 'File', 'Comment',)
 	object = 'CWUser'
 
 We can note the following points:
@@ -123,7 +123,7 @@
 	    }
 
     from cubes.folder.schema import Folder
-    from cubes.file.schema import File, Image
+    from cubes.file.schema import File
     from cubes.comment.schema import Comment
     from cubes.person.schema import Person
     from cubes.zone.schema import Zone
@@ -131,7 +131,6 @@
 
     Folder.__permissions__ = VISIBILITY_PERMISSIONS
     File.__permissions__ = VISIBILITY_PERMISSIONS
-    Image.__permissions__ = VISIBILITY_PERMISSIONS
     Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
     Comment.__permissions__['add'] = ('managers', 'users',)
     Person.__permissions__ = AUTH_ONLY_PERMISSIONS
@@ -174,7 +173,7 @@
 
 The tricky part of the requirement is in *unless explicitly specified*, notably
 because when the entity is added, we don't know yet its 'parent'
-entity (e.g. Folder of an Image, Image commented by a Comment). To handle such things,
+entity (e.g. Folder of an File, File commented by a Comment). To handle such things,
 CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
 
 In our case we will:
@@ -200,7 +199,7 @@
 
     class SetVisibilityHook(hook.Hook):
 	__regid__ = 'sytweb.setvisibility'
-	__select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Image', 'Comment')
+	__select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Comment')
 	events = ('after_add_entity',)
 	def __call__(self):
 	    hook.set_operation(self._cw, 'pending_visibility', self.entity.eid,
@@ -321,7 +320,7 @@
 	    folder = req.create_entity('Folder',
 				       name=u'restricted',
 				       visibility=u'restricted')
-	    photo1 = req.create_entity('Image',
+	    photo1 = req.create_entity('File',
 				       data_name=u'photo1.jpg',
 				       data=Binary('xxx'),
 				       filed_under=folder)
@@ -330,7 +329,7 @@
 	    # visibility propagation
 	    self.assertEquals(photo1.visibility, 'restricted')
 	    # unless explicitly specified
-	    photo2 = req.create_entity('Image',
+	    photo2 = req.create_entity('File',
 				       data_name=u'photo2.jpg',
 				       data=Binary('xxx'),
 				       visibility=u'public',
@@ -340,7 +339,7 @@
 	    # test security
 	    self.login('toto')
 	    req = self.request()
-	    self.assertEquals(len(req.execute('Image X')), 1) # only the public one
+	    self.assertEquals(len(req.execute('File X')), 1) # only the public one
 	    self.assertEquals(len(req.execute('Folder X')), 0) # restricted...
 	    # may_be_read_by propagation
 	    self.restore_connection()
@@ -351,7 +350,7 @@
 	    # test security with permissions
 	    self.login('toto')
 	    req = self.request()
-	    self.assertEquals(len(req.execute('Image X')), 2) # now toto has access to photo2
+	    self.assertEquals(len(req.execute('File X')), 2) # now toto has access to photo2
 	    self.assertEquals(len(req.execute('Folder X')), 1) # and to restricted folder
 
     if __name__ == '__main__':
--- a/doc/book/en/tutorials/advanced/part03_bfss.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/tutorials/advanced/part03_bfss.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -4,11 +4,11 @@
 Step 1: configuring the BytesFileSystem storage
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-To avoid cluttering my database, and to ease file manipulation, I don't want
-them to be stored in the database. I want to be able create File/Image entities
-for some files on the server file system, where those file will be accessed to
-get entities data. To do so I've to set a custom :class:`BytesFileSystemStorage` storage
-for the File/Image 'data' attribute, which hold the actual file's content.
+To avoid cluttering my database, and to ease file manipulation, I don't want them
+to be stored in the database. I want to be able create File entities for some
+files on the server file system, where those file will be accessed to get
+entities data. To do so I've to set a custom :class:`BytesFileSystemStorage`
+storage for the File 'data' attribute, which hold the actual file's content.
 
 Since the function to register a custom storage needs to have a repository
 instance as first argument, we've to call it in a server startup hook. So I added
@@ -33,7 +33,6 @@
 		print 'created', bfssdir
 	    storage = storages.BytesFileSystemStorage(bfssdir)
 	    set_attribute_storage(self.repo, 'File', 'data', storage)
-	    set_attribute_storage(self.repo, 'Image', 'data', storage)
 
 .. Note::
 
@@ -52,7 +51,7 @@
     (or in the database before migration) will be located
 
   * be ware that by doing this, you can't anymore write queries that will try to
-    restrict on File and Image `data` attribute. Hopefuly we don't do that usually
+    restrict on File `data` attribute. Hopefuly we don't do that usually
     on file's content or more generally on attributes for the Bytes type
 
 Now, if you've already added some photos through the web ui, you'll have to
@@ -69,8 +68,6 @@
     type "exit" or Ctrl-D to quit the shell and resume operation
     >>> storage_changed('File', 'data')
     [........................]
-    >>> storage_changed('Image', 'data')
-    [........................]
 
 
 That's it. Now, file added through the web ui will have their content stored on
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst	Mon Jan 24 19:09:42 2011 +0100
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst	Tue Feb 01 11:52:10 2011 +0100
@@ -2,94 +2,6 @@
 ================================
 
 
-Step 0: updating code to CubicWeb 3.9 / cubicweb-file 1.9
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-CubicWeb 3.9 brings `several improvments`_ that we'll want to use, and the 1.9
-version of the file cube has a major change: the `Image` type has been dropped in
-favor of an `IImage` adapter that makes code globally much cleaner (though we wont
-see that much this here). So the first thing to do is to upgrade our cube to the
-3.9 API. As CubicWeb releases are mostly backward compatible, this is not
-mandatory but it's easier to follow change as they come than having a huge
-upgrade to do at some point. Also, this remove deprecation warnings which are a
-bit tedious...
-
-Also, since we've only a few lines of code yet, this is quite easy to upgrade.
-Actually the main thing we've to do is to upgrade our schema, to remove occurences
-of the `Image` type or replace them by the `File` type. Here is the (striped) diff:
-
-.. sourcecode:: diff
-
-     class comments(RelationDefinition):
-	 subject = 'Comment'
-    -    object = ('File', 'Image')
-    +    object = 'File'
-	 cardinality = '1*'
-	 composite = 'object'
-
-     class tags(RelationDefinition):
-	 subject = 'Tag'
-    -    object = ('File', 'Image')
-    +    object = 'File'
-
-     class displayed_on(RelationDefinition):
-	 subject = 'Person'
-    -    object = 'Image'
-    +    object = 'File'
-
-     class situated_in(RelationDefinition):
-    -    subject = 'Image'
-    +    subject = 'File'
-	 object = 'Zone'
-
-     class filed_under(RelationDefinition):
-    -    subject = ('File', 'Image')
-    +    subject = 'File'
-	 object = 'Folder'
-
-     class visibility(RelationDefinition):
-    -    subject = ('Folder', 'File', 'Image', 'Comment')
-    +    subject = ('Folder', 'File', 'Comment')
-	 object = 'String'
-	 constraints = [StaticVocabularyConstraint(('public', 'authenticated',
-						    'restricted', 'parent'))]
-
-     class may_be_readen_by(RelationDefinition):
-    -    subject = ('Folder', 'File', 'Image', 'Comment',)
-    +    subject = ('Folder', 'File', 'Comment',)
-	 object = 'CWUser'
-
-
-    -from cubes.file.schema import File, Image
-    +from cubes.file.schema import File
-
-     File.__permissions__ = VISIBILITY_PERMISSIONS
-    -Image.__permissions__ = VISIBILITY_PERMISSIONS
-
-Now, let's record that we depends on the versions in the __pkginfo__ file.  As
-`3.8`_ simplify this file, we can merge `__depends_cubes__` (as introduced if the
-`first blog of this series`_) with `__depends__` to get the following result:
-
-.. sourcecode:: python
-
-    __depends__ = {'cubicweb': '>= 3.9.0',
-		   'cubicweb-file': '>= 1.9.0',
-		   'cubicweb-folder': None,
-		   'cubicweb-person': None,
-		   'cubicweb-zone': None,
-		   'cubicweb-comment': None,
-		   'cubicweb-tag': None,
-		   }
-
-If your cube is packaged for debian, it's a good idea to update the
-`debian/control` file at the same time, so you won't forget it.
-
-That's it for the API update, CubicWeb, cubicweb-file will handle other stuff for
-us. Easy, no?
-
-We can now start some more funny stuff...
-
-
 Step 1: let's improve site's usability for our visitors
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
--- a/entities/schemaobjs.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/entities/schemaobjs.py	Tue Feb 01 11:52:10 2011 +0100
@@ -139,7 +139,7 @@
                 rtype = self.name
                 stype = rdef.stype
                 otype = rdef.otype
-                msg = self._cw._("can't set inlined=%(inlined)s, "
+                msg = self._cw._("can't set inlined=True, "
                                  "%(stype)s %(rtype)s %(otype)s "
                                  "has cardinality=%(card)s")
                 raise ValidationError(self.eid, {qname: msg % locals()})
--- a/entities/wfobjs.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/entities/wfobjs.py	Tue Feb 01 11:52:10 2011 +0100
@@ -489,7 +489,7 @@
         try:
             return self.current_state.name
         except AttributeError:
-            self.warning('entity %s has no state', self)
+            self.warning('entity %s has no state', self.entity)
             return None
 
     @property
--- a/hooks/integrity.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/hooks/integrity.py	Tue Feb 01 11:52:10 2011 +0100
@@ -110,15 +110,32 @@
     category = 'integrity'
 
 
-class CheckCardinalityHook(IntegrityHook):
+class CheckCardinalityHookBeforeDeleteRelation(IntegrityHook):
     """check cardinalities are satisfied"""
-    __regid__ = 'checkcard'
-    events = ('after_add_entity', 'before_delete_relation')
+    __regid__ = 'checkcard_before_delete_relation'
+    events = ('before_delete_relation',)
 
     def __call__(self):
-        getattr(self, self.event)()
+        rtype = self.rtype
+        if rtype in DONT_CHECK_RTYPES_ON_DEL:
+            return
+        session = self._cw
+        eidfrom, eidto = self.eidfrom, self.eidto
+        pendingrdefs = session.transaction_data.get('pendingrdefs', ())
+        if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
+            return
+        card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
+        if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
+            _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype))
+        if card[1] in '1+' and not session.deleted_in_transaction(eidto):
+            _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype))
 
-    def after_add_entity(self):
+class CheckCardinalityHookAfterAddEntity(IntegrityHook):
+    """check cardinalities are satisfied"""
+    __regid__ = 'checkcard_after_add_entity'
+    events = ('after_add_entity',)
+
+    def __call__(self):
         eid = self.entity.eid
         eschema = self.entity.e_schema
         for rschema, targetschemas, role in eschema.relation_definitions():
@@ -159,9 +176,9 @@
             # first check related entities have not been deleted in the same
             # transaction
             if session.deleted_in_transaction(eidfrom):
-                return
+                continue
             if session.deleted_in_transaction(eidto):
-                return
+                continue
             for constraint in constraints:
                 # XXX
                 # * lock RQLConstraint as well?
@@ -299,19 +316,33 @@
         session = self.session
         pendingeids = session.transaction_data.get('pendingeids', ())
         neweids = session.transaction_data.get('neweids', ())
+        eids_by_etype_rtype = {}
         for eid, rtype in self.get_data():
             # don't do anything if the entity is being created or deleted
             if not (eid in pendingeids or eid in neweids):
                 etype = session.describe(eid)[0]
-                session.execute(self.base_rql % (etype, rtype), {'x': eid})
+                key = (etype, rtype)
+                if key not in eids_by_etype_rtype:
+                    eids_by_etype_rtype[key] = [str(eid)]
+                else:
+                    eids_by_etype_rtype[key].append(str(eid))
+        for (etype, rtype), eids in eids_by_etype_rtype.iteritems():
+            # quite unexpectedly, not deleting too many entities at a time in
+            # this operation benefits to the exec speed (possibly on the RQL
+            # parsing side)
+            start = 0
+            incr = 500
+            while start < len(eids):
+                session.execute(self.base_rql % (etype, ','.join(eids[start:start+incr]), rtype))
+                start += incr
 
 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
     """delete orphan subject entity of a composite relation"""
-    base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
+    base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT X %s Y'
 
 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
     """check required object relation"""
-    base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
+    base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT Y %s X'
 
 
 class DeleteCompositeOrphanHook(hook.Hook):
--- a/hooks/syncschema.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/hooks/syncschema.py	Tue Feb 01 11:52:10 2011 +0100
@@ -1212,11 +1212,11 @@
                       len(rset), etype)
             still_fti = list(schema[etype].indexable_attributes())
             for entity in rset.entities():
-                source.fti_unindex_entity(session, entity.eid)
+                source.fti_unindex_entities(session, [entity])
                 for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
                     if still_fti or container is not entity:
-                        source.fti_unindex_entity(session, container.eid)
-                        source.fti_index_entity(session, container)
+                        source.fti_unindex_entities(session, [container])
+                        source.fti_index_entities(session, [container])
         if to_reindex:
             # Transaction has already been committed
             session.pool.commit()
--- a/i18n/en.po	Mon Jan 24 19:09:42 2011 +0100
+++ b/i18n/en.po	Tue Feb 01 11:52:10 2011 +0100
@@ -4156,10 +4156,10 @@
 msgstr "workflow history"
 
 msgid "wf_tab_info"
-msgstr ""
+msgstr "states and transitions"
 
 msgid "wfgraph"
-msgstr ""
+msgstr "graph"
 
 msgid ""
 "when multiple addresses are equivalent (such as python-projects@logilab.org "
--- a/misc/migration/3.10.7_Any.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/misc/migration/3.10.7_Any.py	Tue Feb 01 11:52:10 2011 +0100
@@ -1,9 +1,2 @@
-if not ('CWUniqueTogetherConstraint', 'CWRType') in schema['relations'].rdefs:
-    add_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRType')
-    rql('SET C relations RT WHERE C relations RDEF, RDEF relation_type RT')
-    commit()
-    drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWAttribute')
-    drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRelation')
-
 add_attribute('TrInfo', 'tr_count')
 sync_schema_props_perms('TrInfo')
--- a/misc/migration/bootstrapmigration_repository.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Tue Feb 01 11:52:10 2011 +0100
@@ -97,6 +97,14 @@
 if applcubicwebversion < (3, 9, 6) and cubicwebversion >= (3, 9, 6):
     add_entity_type('CWUniqueTogetherConstraint')
 
+if not ('CWUniqueTogetherConstraint', 'CWRType') in schema['relations'].rdefs:
+    add_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRType')
+    rql('SET C relations RT WHERE C relations RDEF, RDEF relation_type RT')
+    commit()
+    drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWAttribute')
+    drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRelation')
+
+
 if applcubicwebversion < (3, 4, 0) and cubicwebversion >= (3, 4, 0):
 
     with hooks_control(session, session.HOOKS_ALLOW_ALL, 'integrity'):
--- a/rset.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/rset.py	Tue Feb 01 11:52:10 2011 +0100
@@ -487,24 +487,21 @@
                 select = rqlst
             # take care, due to outer join support, we may find None
             # values for non final relation
-            for i, attr, role in attr_desc_iterator(select, col):
-                outerselidx = rqlst.subquery_selection_index(select, i)
-                if outerselidx is None:
-                    continue
+            for i, attr, role in attr_desc_iterator(select, col, entity.cw_col):
                 if role == 'subject':
                     rschema = eschema.subjrels[attr]
                     if rschema.final:
                         if attr == 'eid':
-                            entity.eid = rowvalues[outerselidx]
+                            entity.eid = rowvalues[i]
                         else:
-                            entity.cw_attr_cache[attr] = rowvalues[outerselidx]
+                            entity.cw_attr_cache[attr] = rowvalues[i]
                         continue
                 else:
                     rschema = eschema.objrels[attr]
                 rdef = eschema.rdef(attr, role)
                 # only keep value if it can't be multivalued
                 if rdef.role_cardinality(role) in '1?':
-                    if rowvalues[outerselidx] is None:
+                    if rowvalues[i] is None:
                         if role == 'subject':
                             rql = 'Any Y WHERE X %s Y, X eid %s'
                         else:
@@ -512,7 +509,7 @@
                         rrset = ResultSet([], rql % (attr, entity.eid))
                         rrset.req = req
                     else:
-                        rrset = self._build_entity(row, outerselidx).as_rset()
+                        rrset = self._build_entity(row, i).as_rset()
                     entity.cw_set_relation_cache(attr, role, rrset)
         return entity
 
@@ -650,8 +647,13 @@
                 return rhs.eval(self.args)
         return None
 
+def _get_variable(term):
+    # XXX rewritten const
+    # use iget_nodes for (hack) case where we have things like MAX(V)
+    for vref in term.iget_nodes(nodes.VariableRef):
+        return vref.variable
 
-def attr_desc_iterator(rqlst, index=0):
+def attr_desc_iterator(select, selectidx, rootidx):
     """return an iterator on a list of 2-uple (index, attr_relation)
     localizing attribute relations of the main variable in a result's row
 
@@ -662,25 +664,33 @@
       a generator on (index, relation, target) describing column being
       attribute of the main variable
     """
-    main = rqlst.selection[index]
-    for i, term in enumerate(rqlst.selection):
-        if i == index:
+    rootselect = select
+    while rootselect.parent.parent is not None:
+        rootselect = rootselect.parent.parent.parent
+    rootmain = rootselect.selection[selectidx]
+    rootmainvar = _get_variable(rootmain)
+    assert rootmainvar
+    root = rootselect.parent
+    selectmain = select.selection[selectidx]
+    for i, term in enumerate(rootselect.selection):
+        rootvar = _get_variable(term)
+        if rootvar is None:
             continue
-        # XXX rewritten const
-        # use iget_nodes for (hack) case where we have things like MAX(V)
-        for vref in term.iget_nodes(nodes.VariableRef):
-            var = vref.variable
-            break
-        else:
+        if rootvar.name == rootmainvar.name:
+            continue
+        if select is not rootselect:
+            term = select.selection[root.subquery_selection_index(select, i)]
+        var = _get_variable(term)
+        if var is None:
             continue
         for ref in var.references():
             rel = ref.relation()
             if rel is None or rel.is_types_restriction():
                 continue
             lhs, rhs = rel.get_variable_parts()
-            if main.is_equivalent(lhs):
+            if selectmain.is_equivalent(lhs):
                 if rhs.is_equivalent(term):
                     yield (i, rel.r_type, 'subject')
-            elif main.is_equivalent(rhs):
+            elif selectmain.is_equivalent(rhs):
                 if lhs.is_equivalent(term):
                     yield (i, rel.r_type, 'object')
--- a/rtags.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/rtags.py	Tue Feb 01 11:52:10 2011 +0100
@@ -105,6 +105,8 @@
 
     def apply(self, schema, func):
         for eschema in schema.entities():
+            if eschema.final:
+                continue
             for rschema, tschemas, role in eschema.relation_definitions(True):
                 for tschema in tschemas:
                     if role == 'subject':
@@ -216,6 +218,9 @@
     def name(self):
         return self.__class__.name
 
+    # tag_subject_of / tag_object_of issue warning if '*' is not given as target
+    # type, while tag_relation handle it silently since it may be used during
+    # initialization
     def tag_subject_of(self, key, tag):
         subj, rtype, obj = key
         if obj != '*':
@@ -232,5 +237,14 @@
                          self.name, rtype, obj, subj, rtype, obj)
         super(NoTargetRelationTagsDict, self).tag_object_of(('*', rtype, obj), tag)
 
-
+    def tag_relation(self, key, tag):
+        if key[-1] == 'subject' and key[-2] != '*':
+            if isinstance(key, tuple):
+                key = list(key)
+            key[-2] = '*'
+        elif key[-1] == 'object' and key[0] != '*':
+            if isinstance(key, tuple):
+                key = list(key)
+            key[0] = '*'
+        super(NoTargetRelationTagsDict, self).tag_relation(key, tag)
 set_log_methods(RelationTags, logging.getLogger('cubicweb.rtags'))
--- a/selectors.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/selectors.py	Tue Feb 01 11:52:10 2011 +0100
@@ -551,6 +551,8 @@
     """Return 1 if the result set is of size 1, or greater but a specific row in
       the result set is specified ('row' argument).
     """
+    if rset is None and 'entity' in kwargs:
+        return 1
     if rset is not None and (row is not None or rset.rowcount == 1):
         return 1
     return 0
@@ -1173,24 +1175,92 @@
 
 
 class is_in_state(score_entity):
-    """return 1 if entity is in one of the states given as argument list
+    """Return 1 if entity is in one of the states given as argument list
 
-    you should use this instead of your own :class:`score_entity` selector to
+    You should use this instead of your own :class:`score_entity` selector to
     avoid some gotchas:
 
     * possible views gives a fake entity with no state
-    * you must use the latest tr info, not entity.in_state for repository side
-      checking of the current state
+    * you must use the latest tr info thru the workflow adapter for repository
+      side checking of the current state
+
+    In debug mode, this selector can raise:
+    :raises: :exc:`ValueError` for unknown states names
+        (etype workflow only not checked in custom workflow)
+
+    :rtype: int
     """
-    def __init__(self, *states):
-        def score(entity, states=set(states)):
-            trinfo = entity.cw_adapt_to('IWorkflowable').latest_trinfo()
-            try:
-                return trinfo.new_state.name in states
-            except AttributeError:
-                return None
+    def __init__(self, *expected):
+        assert expected, self
+        self.expected = frozenset(expected)
+        def score(entity, expected=self.expected):
+            adapted = entity.cw_adapt_to('IWorkflowable')
+            # in debug mode only (time consuming)
+            if entity._cw.vreg.config.debugmode:
+                # validation can only be done for generic etype workflow because
+                # expected transition list could have been changed for a custom
+                # workflow (for the current entity)
+                if not entity.custom_workflow:
+                    self._validate(adapted)
+            return self._score(adapted)
         super(is_in_state, self).__init__(score)
 
+    def _score(self, adapted):
+        trinfo = adapted.latest_trinfo()
+        if trinfo is None: # entity is probably in it's initial state
+            statename = adapted.state
+        else:
+            statename = trinfo.new_state.name
+        return statename in self.expected
+
+    def _validate(self, adapted):
+        wf = adapted.current_workflow
+        valid = [n.name for n in wf.reverse_state_of]
+        unknown = sorted(self.expected.difference(valid))
+        if unknown:
+            raise ValueError("%s: unknown state(s): %s"
+                             % (wf.name, ",".join(unknown)))
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                           ','.join(str(s) for s in self.expected))
+
+
+class on_transition(is_in_state):
+    """Return 1 if entity is in one of the transitions given as argument list
+
+    Especially useful to match passed transition to enable notifications when
+    your workflow allows several transition to the same states.
+
+    Note that if workflow `change_state` adapter method is used, this selector
+    will not be triggered.
+
+    You should use this instead of your own :class:`score_entity` selector to
+    avoid some gotchas:
+
+    * possible views gives a fake entity with no state
+    * you must use the latest tr info thru the workflow adapter for repository
+      side checking of the current state
+
+    In debug mode, this selector can raise:
+    :raises: :exc:`ValueError` for unknown transition names
+        (etype workflow only not checked in custom workflow)
+
+    :rtype: int
+    """
+    def _score(self, adapted):
+        trinfo = adapted.latest_trinfo()
+        if trinfo and trinfo.by_transition:
+            return trinfo.by_transition[0].name in self.expected
+
+    def _validate(self, adapted):
+        wf = adapted.current_workflow
+        valid = [n.name for n in wf.reverse_transition_of]
+        unknown = sorted(self.expected.difference(valid))
+        if unknown:
+            raise ValueError("%s: unknown transition(s): %s"
+                             % (wf.name, ",".join(unknown)))
+
 
 # logged user selectors ########################################################
 
@@ -1429,7 +1499,7 @@
 
 # Other selectors ##############################################################
 
-
+# XXX deprecated ? maybe use on_transition selector instead ?
 class match_transition(ExpectedValueSelector):
     """Return 1 if `transition` argument is found in the input context which has
     a `.name` attribute matching one of the expected names given to the
--- a/server/checkintegrity.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/checkintegrity.py	Tue Feb 01 11:52:10 2011 +0100
@@ -129,8 +129,8 @@
     # attribute to their current value
     source = repo.system_source
     for eschema in etypes:
-        for entity in session.execute('Any X WHERE X is %s' % eschema).entities():
-            source.fti_index_entity(session, entity)
+        rset = session.execute('Any X WHERE X is %s' % eschema)
+        source.fti_index_entities(session, rset.entities())
         if withpb:
             pb.update()
 
--- a/server/hook.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/hook.py	Tue Feb 01 11:52:10 2011 +0100
@@ -274,6 +274,14 @@
                     'session_open', 'session_close'))
 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
 
+def _iter_kwargs(entities, kwargs):
+    if not entities:
+        yield kwargs
+    else:
+        for entity in entities:
+            kwargs['entity'] = entity
+            yield kwargs
+
 
 class HooksRegistry(CWRegistry):
     def initialization_completed(self):
@@ -288,20 +296,30 @@
         super(HooksRegistry, self).register(obj, **kwargs)
 
     def call_hooks(self, event, session=None, **kwargs):
+        """call `event` hooks for an entity or a list of entities (passed
+        respectively as the `entity` or ``entities`` keyword argument).
+        """
         kwargs['event'] = event
-        if session is None:
+        if session is None: # True for events such as server_start
             for hook in sorted(self.possible_objects(session, **kwargs),
                                key=lambda x: x.order):
                 hook()
         else:
+            if 'entities' in kwargs:
+                assert 'entity' not in kwargs, \
+                       'can\'t pass "entities" and "entity" arguments simultaneously'
+                entities = kwargs.pop('entities')
+            else:
+                entities = []
             # by default, hooks are executed with security turned off
             with security_enabled(session, read=False):
-                hooks = sorted(self.possible_objects(session, **kwargs),
-                               key=lambda x: x.order)
-                with security_enabled(session, write=False):
-                    for hook in hooks:
-                        #print hook.category, hook.__regid__
-                        hook()
+                for _kwargs in _iter_kwargs(entities, kwargs):
+                    hooks = sorted(self.possible_objects(session, **_kwargs),
+                                   key=lambda x: x.order)
+                    with security_enabled(session, write=False):
+                        for hook in hooks:
+                            #print hook.category, hook.__regid__
+                            hook()
 
 class HooksManager(object):
     def __init__(self, vreg):
--- a/server/migractions.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/migractions.py	Tue Feb 01 11:52:10 2011 +0100
@@ -183,7 +183,7 @@
         open(backupfile,'w').close() # kinda lock
         os.chmod(backupfile, 0600)
         # backup
-        tmpdir = tempfile.mkdtemp(dir=instbkdir)
+        tmpdir = tempfile.mkdtemp()
         try:
             for source in repo.sources:
                 try:
--- a/server/repository.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/repository.py	Tue Feb 01 11:52:10 2011 +0100
@@ -1080,6 +1080,17 @@
         hook.CleanupDeletedEidsCacheOp.get_instance(session).add_data(entity.eid)
         self._delete_info(session, entity, sourceuri, extid, scleanup)
 
+    def delete_info_multi(self, session, entities, sourceuri, extids, scleanup=False):
+        """same as delete_info but accepts a list of entities and
+        extids with the same etype and belonging to the same source
+        """
+        # mark eid as being deleted in session info and setup cache update
+        # operation
+        op = hook.CleanupDeletedEidsCacheOp.get_instance(session)
+        for entity in entities:
+            op.add_data(entity.eid)
+        self._delete_info_multi(session, entities, sourceuri, extids, scleanup)
+
     def _delete_info(self, session, entity, sourceuri, extid, scleanup=False):
         """delete system information on deletion of an entity:
         * delete all remaining relations from/to this entity
@@ -1112,6 +1123,38 @@
                                    'from %s. RQL: %s', entity, sourceuri, rql)
         self.system_source.delete_info(session, entity, sourceuri, extid)
 
+    def _delete_info_multi(self, session, entities, sourceuri, extids, scleanup=False):
+        """same as _delete_info but accepts a list of entities with
+        the same etype and belinging to the same source.
+        """
+        pendingrtypes = session.transaction_data.get('pendingrtypes', ())
+        # delete remaining relations: if user can delete the entity, he can
+        # delete all its relations without security checking
+        assert entities and len(entities) == len(extids)
+        with security_enabled(session, read=False, write=False):
+            eids = [_e.eid for _e in entities]
+            in_eids = ','.join((str(eid) for eid in eids))
+            for rschema, _, role in entities[0].e_schema.relation_definitions():
+                rtype = rschema.type
+                if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
+                    continue
+                if role == 'subject':
+                    # don't skip inlined relation so they are regularly
+                    # deleted and so hooks are correctly called
+                    rql = 'DELETE X %s Y WHERE X eid IN (%s)' % (rtype, in_eids)
+                else:
+                    rql = 'DELETE Y %s X WHERE X eid IN (%s)' % (rtype, in_eids)
+                if scleanup:
+                    # source cleaning: only delete relations stored locally
+                    rql += ', NOT (Y cw_source S, S name %(source)s)'
+                try:
+                    session.execute(rql, {'source': sourceuri},
+                                    build_descr=False)
+                except:
+                    self.exception('error while cascading delete for entity %s '
+                                   'from %s. RQL: %s', entities, sourceuri, rql)
+        self.system_source.delete_info_multi(session, entities, sourceuri, extids)
+
     def locate_relation_source(self, session, subject, rtype, object):
         subjsource = self.source_from_eid(subject, session)
         objsource = self.source_from_eid(object, session)
@@ -1280,19 +1323,40 @@
             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"""
-        entity = session.entity_from_eid(eid)
-        etype, sourceuri, extid = self.type_and_source_from_eid(eid, session)
-        if server.DEBUG & server.DBG_REPO:
-            print 'DELETE entity', etype, eid
-        source = self.sources_by_uri[sourceuri]
-        if source.should_call_hooks:
-            self.hm.call_hooks('before_delete_entity', session, entity=entity)
-        self._delete_info(session, entity, sourceuri, extid)
-        source.delete_entity(session, entity)
-        if source.should_call_hooks:
-            self.hm.call_hooks('after_delete_entity', session, entity=entity)
+
+    def glob_delete_entities(self, session, eids):
+        """delete a list of  entities and all related entities from the repository"""
+        data_by_etype_source = {} # values are ([list of eids],
+                                  #             [list of extid],
+                                  #             [list of entities])
+        #
+        # WARNING: the way this dictionary is populated is heavily optimized
+        # and does not use setdefault on purpose. Unless a new release
+        # of the Python interpreter advertises large perf improvements
+        # in setdefault, this should not be changed without profiling.
+
+        for eid in eids:
+            etype, sourceuri, extid = self.type_and_source_from_eid(eid, session)
+            entity = session.entity_from_eid(eid, etype)
+            _key = (etype, sourceuri)
+            if _key not in data_by_etype_source:
+                data_by_etype_source[_key] = ([eid], [extid], [entity])
+            else:
+                _data = data_by_etype_source[_key]
+                _data[0].append(eid)
+                _data[1].append(extid)
+                _data[2].append(entity)
+        for (etype, sourceuri), (eids, extids, entities) in data_by_etype_source.iteritems():
+            if server.DEBUG & server.DBG_REPO:
+                print 'DELETE entities', etype, eids
+            #print 'DELETE entities', etype, len(eids)
+            source = self.sources_by_uri[sourceuri]
+            if source.should_call_hooks:
+                self.hm.call_hooks('before_delete_entity', session, entities=entities)
+            self._delete_info_multi(session, entities, sourceuri, extids) # xxx
+            source.delete_entities(session, entities)
+            if source.should_call_hooks:
+                self.hm.call_hooks('after_delete_entity', session, entities=entities)
         # don't clear cache here this is done in a hook on commit
 
     def glob_add_relation(self, session, subject, rtype, object):
--- a/server/sources/__init__.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/sources/__init__.py	Tue Feb 01 11:52:10 2011 +0100
@@ -22,6 +22,7 @@
 from os.path import join, splitext
 from datetime import datetime, timedelta
 from logging import getLogger
+import itertools
 
 from cubicweb import set_log_methods, server
 from cubicweb.schema import VIRTUAL_RTYPES
@@ -374,6 +375,11 @@
         """update an entity in the source"""
         raise NotImplementedError()
 
+    def delete_entities(self, session, entities):
+        """delete several entities from the source"""
+        for entity in entities:
+            self.delete_entity(session, entity)
+
     def delete_entity(self, session, entity):
         """delete an entity from the source"""
         raise NotImplementedError()
@@ -403,12 +409,19 @@
         """mark entity as being modified, fulltext reindex if needed"""
         raise NotImplementedError()
 
-    def delete_info(self, session, entity, uri, extid, attributes, relations):
+    def delete_info(self, session, entity, uri, extid):
         """delete system information on deletion of an entity by transfering
         record from the entities table to the deleted_entities table
         """
         raise NotImplementedError()
 
+    def delete_info_multi(self, session, entities, uri, extids):
+        """ame as delete_info but accepts a list of entities with
+        the same etype and belinging to the same source.
+        """
+        for entity, extid in itertools.izip(entities, extids):
+            self.delete_info(session, entity, uri, extid)
+
     def modified_entities(self, session, etypes, mtime):
         """return a 2-uple:
         * list of (etype, eid) of entities of the given types which have been
@@ -425,14 +438,13 @@
         """
         raise NotImplementedError()
 
-    def fti_unindex_entity(self, session, eid):
-        """remove text content for entity with the given eid from the full text
-        index
+    def fti_unindex_entities(self, session, entities):
+        """remove text content for entities from the full text index
         """
         raise NotImplementedError()
 
-    def fti_index_entity(self, session, entity):
-        """add text content of a created/modified entity to the full text index
+    def fti_index_entities(self, session, entities):
+        """add text content of created/modified entities to the full text index
         """
         raise NotImplementedError()
 
--- a/server/sources/ldapuser.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/sources/ldapuser.py	Tue Feb 01 11:52:10 2011 +0100
@@ -177,7 +177,7 @@
         self.cnx_dn = source_config.get('data-cnx-dn') or ''
         self.cnx_pwd = source_config.get('data-cnx-password') or ''
         self.user_base_scope = globals()[source_config['user-scope']]
-        self.user_base_dn = source_config['user-base-dn']
+        self.user_base_dn = str(source_config['user-base-dn'])
         self.user_base_scope = globals()[source_config['user-scope']]
         self.user_classes = splitstrip(source_config['user-classes'])
         self.user_login_attr = source_config['user-login-attr']
@@ -328,7 +328,7 @@
         return None
 
     def prepare_columns(self, mainvars, rqlst):
-        """return two list describin how to build the final results
+        """return two list describing how to build the final results
         from the result of an ldap search (ie a list of dictionnary)
         """
         columns = []
@@ -532,6 +532,8 @@
                 searchstr='(objectClass=*)', attrs=()):
         """make an ldap query"""
         self.debug('ldap search %s %s %s %s %s', self.uri, base, scope, searchstr, list(attrs))
+        # XXX for now, we do not have connection pool support for LDAP, so
+        # this is always self._conn
         cnx = session.pool.connection(self.uri).cnx
         try:
             res = cnx.search_s(base, scope, searchstr, attrs)
@@ -598,12 +600,13 @@
             entity.cw_edited[attr] = res[self.user_rev_attrs[attr]]
         return entity
 
-    def after_entity_insertion(self, session, dn, entity):
+    def after_entity_insertion(self, session, lid, entity):
         """called by the repository after an entity stored here has been
         inserted in the system table.
         """
         self.debug('ldap after entity insertion')
-        super(LDAPUserSource, self).after_entity_insertion(session, dn, entity)
+        super(LDAPUserSource, self).after_entity_insertion(session, lid, entity)
+        dn = lid
         for group in self.user_default_groups:
             session.execute('SET X in_group G WHERE X eid %(x)s, G name %(group)s',
                             {'x': entity.eid, 'group': group})
--- a/server/sources/native.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/sources/native.py	Tue Feb 01 11:52:10 2011 +0100
@@ -35,6 +35,7 @@
 from contextlib import contextmanager
 from os.path import abspath
 import re
+import itertools
 
 from logilab.common.compat import any
 from logilab.common.cache import Cache
@@ -553,23 +554,29 @@
         #    on the filesystem. To make the entity.data usage absolutely
         #    transparent, we'll have to reset entity.data to its binary
         #    value once the SQL query will be executed
-        restore_values = {}
-        etype = entity.__regid__
+        restore_values = []
+        if isinstance(entity, list):
+            entities = entity
+        else:
+            entities = [entity]
+        etype = entities[0].__regid__
         for attr, storage in self._storages.get(etype, {}).items():
-            try:
-                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)
-                    restore_values[attr] = handler(entity, attr)
+            for entity in entities:
+                try:
+                    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)
+                        to_restore = handler(entity, attr)
+                        restore_values.append((entity, attr, to_restore))
         try:
             yield # 2/ execute the source's instructions
         finally:
             # 3/ restore original values
-            for attr, value in restore_values.items():
+            for entity, attr, value in restore_values:
                 entity.cw_edited.edited_attribute(attr, value)
 
     def add_entity(self, session, entity):
@@ -923,7 +930,7 @@
         * transfer it to the deleted_entities table if the entity's type is
           multi-sources
         """
-        self.fti_unindex_entity(session, entity.eid)
+        self.fti_unindex_entities(session, [entity])
         attrs = {'eid': entity.eid}
         self.doexec(session, self.sqlgen.delete('entities', attrs), attrs)
         if not entity.__regid__ in self.multisources_etypes:
@@ -935,6 +942,27 @@
                  'source': uri, 'dtime': datetime.now()}
         self.doexec(session, self.sqlgen.insert('deleted_entities', attrs), attrs)
 
+    def delete_info_multi(self, session, entities, uri, extids):
+        """delete system information on deletion of an entity:
+        * update the fti
+        * remove record from the entities table
+        * transfer it to the deleted_entities table if the entity's type is
+          multi-sources
+        """
+        self.fti_unindex_entities(session, entities)
+        attrs = {'eid': '(%s)' % ','.join([str(_e.eid) for _e in entities])}
+        self.doexec(session, self.sqlgen.delete_many('entities', attrs), attrs)
+        if entities[0].__regid__ not in self.multisources_etypes:
+            return
+        attrs = {'type': entities[0].__regid__,
+                 'source': uri, 'dtime': datetime.now()}
+        for entity, extid in itertools.izip(entities, extids):
+            if extid is not None:
+                assert isinstance(extid, str), type(extid)
+                extid = b64encode(extid)
+            attrs.update({'eid': entity.eid, 'extid': extid})
+            self.doexec(session, self.sqlgen.insert('deleted_entities', attrs), attrs)
+
     def modified_entities(self, session, etypes, mtime):
         """return a 2-uple:
         * list of (etype, eid) of entities of the given types which have been
@@ -1302,27 +1330,32 @@
         """
         FTIndexEntityOp.get_instance(session).add_data(entity.eid)
 
-    def fti_unindex_entity(self, session, eid):
-        """remove text content for entity with the given eid from the full text
-        index
+    def fti_unindex_entities(self, session, entities):
+        """remove text content for entities from the full text index
         """
+        cursor = session.pool['system']
+        cursor_unindex_object = self.dbhelper.cursor_unindex_object
         try:
-            self.dbhelper.cursor_unindex_object(eid, session.pool['system'])
+            for entity in entities:
+                cursor_unindex_object(entity.eid, cursor)
         except Exception: # let KeyboardInterrupt / SystemExit propagate
-            self.exception('error while unindexing %s', eid)
+            self.exception('error while unindexing %s', entity)
+
 
-    def fti_index_entity(self, session, entity):
-        """add text content of a created/modified entity to the full text index
+    def fti_index_entities(self, session, entities):
+        """add text content of created/modified entities to the full text index
         """
-        self.debug('reindexing %r', entity.eid)
+        cursor_index_object = self.dbhelper.cursor_index_object
+        cursor = session.pool['system']
         try:
             # use cursor_index_object, not cursor_reindex_object since
             # unindexing done in the FTIndexEntityOp
-            self.dbhelper.cursor_index_object(entity.eid,
-                                              entity.cw_adapt_to('IFTIndexable'),
-                                              session.pool['system'])
+            for entity in entities:
+                cursor_index_object(entity.eid,
+                                    entity.cw_adapt_to('IFTIndexable'),
+                                    cursor)
         except Exception: # let KeyboardInterrupt / SystemExit propagate
-            self.exception('error while reindexing %s', entity)
+            self.exception('error while indexing %s', entity)
 
 
 class FTIndexEntityOp(hook.DataOperationMixIn, hook.LateOperation):
@@ -1338,17 +1371,17 @@
         source = session.repo.system_source
         pendingeids = session.transaction_data.get('pendingeids', ())
         done = session.transaction_data.setdefault('indexedeids', set())
+        to_reindex = set()
         for eid in self.get_data():
             if eid in pendingeids or eid in done:
                 # entity added and deleted in the same transaction or already
                 # processed
-                return
+                continue
             done.add(eid)
             iftindexable = session.entity_from_eid(eid).cw_adapt_to('IFTIndexable')
-            for container in iftindexable.fti_containers():
-                source.fti_unindex_entity(session, container.eid)
-                source.fti_index_entity(session, container)
-
+            to_reindex |= set(iftindexable.fti_containers())
+        source.fti_unindex_entities(session, to_reindex)
+        source.fti_index_entities(session, to_reindex)
 
 def sql_schema(driver):
     helper = get_db_helper(driver)
--- a/server/sources/rql2sql.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/sources/rql2sql.py	Tue Feb 01 11:52:10 2011 +0100
@@ -416,7 +416,7 @@
                     p = compnode.parent
                     oor = None
                     while not isinstance(p, Select):
-                        if isinstance(p, Or):
+                        if isinstance(p, (Or, Not)):
                             oor = p
                         p = p.parent
                     if oor is not None:
@@ -434,7 +434,7 @@
             while not isinstance(p, Select):
                 if p in ors or p is None: # p is None for nodes already in fakehaving
                     break
-                if isinstance(p, Or):
+                if isinstance(p, (Or, Not)):
                     oor = p
                 p = p.parent
             else:
--- a/server/ssplanner.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/ssplanner.py	Tue Feb 01 11:52:10 2011 +0100
@@ -645,17 +645,16 @@
     def execute(self):
         """execute this step"""
         results = self.execute_child()
-        todelete = frozenset(typed_eid(eid) for eid, in results)
-        session = self.plan.session
-        delete = session.repo.glob_delete_entity
-        # mark eids as being deleted in session info and setup cache update
-        # operation (register pending eids before actual deletion to avoid
-        # multiple call to glob_delete_entity)
-        op = CleanupDeletedEidsCacheOp.get_instance(session)
-        actual = todelete - op._container
-        op._container |= actual
-        for eid in actual:
-            delete(session, eid)
+        if results:
+            todelete = frozenset(typed_eid(eid) for eid, in results)
+            session = self.plan.session
+            # mark eids as being deleted in session info and setup cache update
+            # operation (register pending eids before actual deletion to avoid
+            # multiple call to glob_delete_entities)
+            op = CleanupDeletedEidsCacheOp.get_instance(session)
+            actual = todelete - op._container
+            op._container |= actual
+            session.repo.glob_delete_entities(session, actual)
         return results
 
 class DeleteRelationsStep(Step):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data/ldap_test.ldif	Tue Feb 01 11:52:10 2011 +0100
@@ -0,0 +1,55 @@
+dn: dc=cubicweb,dc=test
+structuralObjectClass: organization
+objectClass: dcObject
+objectClass: organization
+o: cubicweb
+dc: cubicweb
+
+dn: ou=People,dc=cubicweb,dc=test
+objectClass: organizationalUnit
+ou: People
+structuralObjectClass: organizationalUnit
+
+dn: uid=syt,ou=People,dc=cubicweb,dc=test
+loginShell: /bin/bash
+objectClass: inetOrgPerson
+objectClass: posixAccount
+objectClass: top
+objectClass: shadowAccount
+structuralObjectClass: inetOrgPerson
+cn: Sylvain Thenault
+sn: Thenault
+shadowMax: 99999
+gidNumber: 1004
+uid: syt
+homeDirectory: /home/syt
+shadowFlag: 134538764
+uidNumber: 1004
+givenName: Sylvain
+telephoneNumber: 106
+displayName: sthenault
+gecos: Sylvain Thenault
+mail: sylvain.thenault@logilab.fr
+mail: syt@logilab.fr
+
+dn: uid=adim,ou=People,dc=cubicweb,dc=test
+loginShell: /bin/bash
+objectClass: inetOrgPerson
+objectClass: posixAccount
+objectClass: top
+objectClass: shadowAccount
+cn: Adrien Di Mascio
+sn: Di Mascio
+shadowMax: 99999
+gidNumber: 1006
+uid: adim
+homeDirectory: /home/adim
+uidNumber: 1006
+structuralObjectClass: inetOrgPerson
+givenName: Adrien
+telephoneNumber: 109
+displayName: adimascio
+gecos: Adrien Di Mascio
+mail: adim@logilab.fr
+mail: adrien.dimascio@logilab.fr
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data/slapd.conf	Tue Feb 01 11:52:10 2011 +0100
@@ -0,0 +1,53 @@
+# This is the main slapd configuration file. See slapd.conf(5) for more
+# info on the configuration options.
+
+#######################################################################
+# Global Directives:
+
+# Features to permit
+#allow bind_v2
+
+# Schema and objectClass definitions
+include         /etc/ldap/schema/core.schema
+include         /etc/ldap/schema/cosine.schema
+include         /etc/ldap/schema/nis.schema
+include         /etc/ldap/schema/inetorgperson.schema
+include         /etc/ldap/schema/openldap.schema
+include         /etc/ldap/schema/misc.schema
+
+# Where the pid file is put. The init.d script
+# will not stop the server if you change this.
+pidfile         ./data/test-slapd.pid
+
+# List of arguments that were passed to the server
+argsfile        ./data/slapd.args
+
+# Read slapd.conf(5) for possible values
+loglevel        sync
+# none
+
+# Where the dynamically loaded modules are stored
+modulepath	/usr/lib/ldap
+moduleload	back_hdb
+moduleload	back_bdb
+moduleload      back_monitor
+
+# The maximum number of entries that is returned for a search operation
+sizelimit 500
+
+# The tool-threads parameter sets the actual amount of cpu's that is used
+# for indexing.
+tool-threads 1
+
+database        bdb
+
+# The base of your directory in database #1
+suffix          "dc=cubicweb,dc=test"
+
+# rootdn directive for specifying a superuser on the database. This is needed
+# for syncrepl.
+#rootdn          "cn=admin,dc=cubicweb,dc=test"
+#rootpw          "cubicwebrocks"
+# Where the database file are physically stored for database #1
+directory       "./data/ldapdb"
+
--- a/server/test/unittest_ldapuser.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/test/unittest_ldapuser.py	Tue Feb 01 11:52:10 2011 +0100
@@ -17,7 +17,12 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb.server.sources.ldapusers unit and functional tests"""
 
-import socket
+import os
+import shutil
+import time
+from os.path import abspath, join, exists
+import subprocess
+from socket import socket, error as socketerror
 
 from logilab.common.testlib import TestCase, unittest_main, mock_object
 from cubicweb.devtools.testlib import CubicWebTC
@@ -25,30 +30,17 @@
 
 from cubicweb.server.sources.ldapuser import *
 
-if '17.1' in socket.gethostbyname('ldap1'):
-    SYT = 'syt'
-    SYT_EMAIL = 'Sylvain Thenault'
-    ADIM = 'adim'
-    CONFIG = u'''host=ldap1
-user-base-dn=ou=People,dc=logilab,dc=fr
+SYT = 'syt'
+SYT_EMAIL = 'Sylvain Thenault'
+ADIM = 'adim'
+CONFIG = u'''host=%s
+user-base-dn=ou=People,dc=cubicweb,dc=test
 user-scope=ONELEVEL
 user-classes=top,posixAccount
 user-login-attr=uid
 user-default-group=users
 user-attrs-map=gecos:email,uid:login
 '''
-else:
-    SYT = 'sthenault'
-    SYT_EMAIL = 'sylvain.thenault@logilab.fr'
-    ADIM = 'adimascio'
-    CONFIG = u'''host=ldap1
-user-base-dn=ou=People,dc=logilab,dc=net
-user-scope=ONELEVEL
-user-classes=top,OpenLDAPperson
-user-login-attr=uid
-user-default-group=users
-user-attrs-map=mail:email,uid:login
-'''
 
 
 def nopwd_authenticate(self, session, login, password):
@@ -71,28 +63,76 @@
     return self.extid2eid(user['dn'], 'CWUser', session)
 
 def setUpModule(*args):
+    create_slapd_configuration(LDAPUserSourceTC.config)
     global repo
-    LDAPUserSourceTC._init_repo()
-    repo = LDAPUserSourceTC.repo
-    add_ldap_source(LDAPUserSourceTC.cnx)
+    try:
+        LDAPUserSourceTC._init_repo()
+        repo = LDAPUserSourceTC.repo
+        add_ldap_source(LDAPUserSourceTC.cnx)
+    except:
+        terminate_slapd()
+        raise
 
 def tearDownModule(*args):
     global repo
     repo.shutdown()
     del repo
+    terminate_slapd()
 
 def add_ldap_source(cnx):
     cnx.request().create_entity('CWSource', name=u'ldapuser', type=u'ldapuser',
                                 config=CONFIG)
     cnx.commit()
-    # XXX: need this first query else we get 'database is locked' from
-    # sqlite since it doesn't support multiple connections on the same
-    # database
-    # so doing, ldap inserted users don't get removed between each test
-    rset = cnx.cursor().execute('CWUser X')
-    # check we get some users from ldap
-    assert len(rset) > 1
+
+def create_slapd_configuration(config):
+    global slapd_process, CONFIG
+    basedir = join(config.apphome, "ldapdb")
+    slapdconf = join(config.apphome, "slapd.conf")
+    if not exists(basedir):
+        os.makedirs(basedir)
+        # fill ldap server with some data
+        ldiffile = join(config.apphome, "ldap_test.ldif")
+        print "Initing ldap database"
+        cmdline = "/usr/sbin/slapadd -f %s -l %s -c" % (slapdconf, ldiffile)
+        subprocess.call(cmdline, shell=True)
+
 
+    #ldapuri = 'ldapi://' + join(basedir, "ldapi").replace('/', '%2f')
+    for port in range(9000, 9100):
+        try:
+            socket().bind(('localhost', port))
+        except socketerror, e:
+            if e.errno == 98: # Address already in use
+                pass
+            else:
+                raise
+        else:
+            break
+    else:
+        raise Exception("Can't find a free TCP port on localhost")
+
+    host = 'localhost:%s' % port
+    ldapuri = 'ldap://%s' % host
+    cmdline = ["/usr/sbin/slapd", "-f",  slapdconf,  "-h",  ldapuri, "-d", "0"]
+    print "Starting slapd on", ldapuri
+    slapd_process = subprocess.Popen(cmdline)
+    time.sleep(0.2)
+    if slapd_process.poll() is None:
+        print "slapd started with pid %s" % slapd_process.pid
+    else:
+        raise EnvironmentError('Cannot start slapd with cmdline="%s" (from directory "%s")' %
+                               (" ".join(cmdline), os.getcwd()))
+    CONFIG = CONFIG % host
+
+def terminate_slapd():
+    global slapd_process
+    if slapd_process.returncode is None:
+        print "terminating slapd"
+        slapd_process.terminate()
+        slapd_process.wait()
+        print "DONE"
+
+    del slapd_process
 
 class LDAPUserSourceTC(CubicWebTC):
 
@@ -117,7 +157,8 @@
 
     def test_base(self):
         # check a known one
-        e = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT}).get_entity(0, 0)
+        rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})
+        e = rset.get_entity(0, 0)
         self.assertEqual(e.login, SYT)
         e.complete()
         self.assertEqual(e.creation_date, None)
--- a/server/test/unittest_rql2sql.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/server/test/unittest_rql2sql.py	Tue Feb 01 11:52:10 2011 +0100
@@ -183,7 +183,7 @@
 ]
 
 
-ADVANCED= [
+ADVANCED = [
     ("Societe S WHERE S nom 'Logilab' OR S nom 'Caesium'",
      '''SELECT _S.cw_eid
 FROM cw_Societe AS _S
@@ -572,6 +572,15 @@
     ('Any 1 WHERE X in_group G, X is CWUser',
      '''SELECT 1
 FROM in_group_relation AS rel_in_group0'''),
+
+    ('CWEType X WHERE X name CV, X description V HAVING NOT V=CV AND NOT V = "parent"',
+     '''SELECT _X.cw_eid
+FROM cw_CWEType AS _X
+WHERE NOT (EXISTS(SELECT 1 WHERE _X.cw_description=parent)) AND NOT (EXISTS(SELECT 1 WHERE _X.cw_description=_X.cw_name))'''),
+    ('CWEType X WHERE X name CV, X description V HAVING V!=CV AND V != "parent"',
+     '''SELECT _X.cw_eid
+FROM cw_CWEType AS _X
+WHERE _X.cw_description!=parent AND _X.cw_description!=_X.cw_name'''),
     ]
 
 
--- a/skeleton/test/test_CUBENAME.py.tmpl	Mon Jan 24 19:09:42 2011 +0100
+++ b/skeleton/test/test_CUBENAME.py.tmpl	Tue Feb 01 11:52:10 2011 +0100
@@ -29,7 +29,7 @@
 
 class DefaultTC(testlib.CubicWebTC):
     def test_something(self):
-        self.skip('this cube has no test')
+        self.skipTest('this cube has no test')
 
 
 if __name__ == '__main__':
--- a/sobjects/textparsers.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/sobjects/textparsers.py	Tue Feb 01 11:52:10 2011 +0100
@@ -15,13 +15,13 @@
 #
 # 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 triggered on email entities creation:
+"""Some parsers to detect action to do from text
 
-* look for state change instruction (XXX security)
-* set email content as a comment on an entity when comments are supported and
-  linking information are found
+Currently only a parser to look for state change instruction is provided.
+Take care to security when you're using it, think about the user that
+will provide the text to analyze...
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import re
@@ -29,7 +29,6 @@
 from cubicweb import UnknownEid, typed_eid
 from cubicweb.view import Component
 
-        # XXX use user session if gpg signature validated
 
 class TextAnalyzer(Component):
     """analyze and extract information from plain text by calling registered
--- a/test/data/schema.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/test/data/schema.py	Tue Feb 01 11:52:10 2011 +0100
@@ -19,7 +19,9 @@
 
 """
 
-from yams.buildobjs import EntityType, String, SubjectRelation, RelationDefinition
+from yams.buildobjs import (EntityType, String, SubjectRelation,
+                            RelationDefinition)
+from cubicweb.schema import  WorkflowableEntityType
 
 class Personne(EntityType):
     nom = String(required=True)
@@ -48,3 +50,9 @@
 class evaluee(RelationDefinition):
     subject = 'CWUser'
     object = 'Note'
+
+
+class StateFull(WorkflowableEntityType):
+    name = String()
+
+
--- a/test/unittest_rset.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/test/unittest_rset.py	Tue Feb 01 11:52:10 2011 +0100
@@ -49,7 +49,7 @@
             'Any C where C is Company, C employs P' : [],
             }
         for rql, relations in queries.items():
-            result = list(attr_desc_iterator(parse(rql).children[0]))
+            result = list(attr_desc_iterator(parse(rql).children[0], 0, 0))
             self.assertEqual((rql, result), (rql, relations))
 
     def test_relations_description_indexed(self):
@@ -59,8 +59,8 @@
             {0: [(2,'employs', 'subject')], 1: [(3,'login', 'subject'), (4,'mail', 'subject')]},
             }
         for rql, results in queries.items():
-            for var_index, relations in results.items():
-                result = list(attr_desc_iterator(parse(rql).children[0], var_index))
+            for idx, relations in results.items():
+                result = list(attr_desc_iterator(parse(rql).children[0], idx, idx))
                 self.assertEqual(result, relations)
 
 
@@ -328,7 +328,7 @@
         self.assertEqual(entity, None)
         self.assertEqual(rtype, None)
 
-    def test_related_entity_union_subquery(self):
+    def test_related_entity_union_subquery_1(self):
         e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
         rset = self.execute('Any X,N ORDERBY N WITH X,N BEING '
                             '((Any X,N WHERE X is CWGroup, X name N)'
@@ -337,10 +337,14 @@
         entity, rtype = rset.related_entity(0, 1)
         self.assertEqual(entity.eid, e.eid)
         self.assertEqual(rtype, 'title')
+        self.assertEqual(entity.title, 'aaaa')
         entity, rtype = rset.related_entity(1, 1)
         self.assertEqual(entity.__regid__, 'CWGroup')
         self.assertEqual(rtype, 'name')
-        #
+        self.assertEqual(entity.name, 'guests')
+
+    def test_related_entity_union_subquery_2(self):
+        e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
         rset = self.execute('Any X,N ORDERBY N WHERE X is Bookmark WITH X,N BEING '
                             '((Any X,N WHERE X is CWGroup, X name N)'
                             ' UNION '
@@ -348,7 +352,10 @@
         entity, rtype = rset.related_entity(0, 1)
         self.assertEqual(entity.eid, e.eid)
         self.assertEqual(rtype, 'title')
-        #
+        self.assertEqual(entity.title, 'aaaa')
+
+    def test_related_entity_union_subquery_3(self):
+        e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
         rset = self.execute('Any X,N ORDERBY N WITH N,X BEING '
                             '((Any N,X WHERE X is CWGroup, X name N)'
                             ' UNION '
@@ -356,6 +363,18 @@
         entity, rtype = rset.related_entity(0, 1)
         self.assertEqual(entity.eid, e.eid)
         self.assertEqual(rtype, 'title')
+        self.assertEqual(entity.title, 'aaaa')
+
+    def test_related_entity_union_subquery_4(self):
+        e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
+        rset = self.execute('Any X,X, N ORDERBY N WITH X,N BEING '
+                            '((Any X,N WHERE X is CWGroup, X name N)'
+                            ' UNION '
+                            ' (Any X,N WHERE X is Bookmark, X title N))')
+        entity, rtype = rset.related_entity(0, 2)
+        self.assertEqual(entity.eid, e.eid)
+        self.assertEqual(rtype, 'title')
+        self.assertEqual(entity.title, 'aaaa')
 
     def test_related_entity_trap_subquery(self):
         req = self.request()
--- a/test/unittest_schema.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/test/unittest_schema.py	Tue Feb 01 11:52:10 2011 +0100
@@ -168,7 +168,7 @@
                              'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
                              'Password', 'Personne',
                              'RQLExpression',
-                             'Societe', 'State', 'String', 'SubNote', 'SubWorkflowExitPoint',
+                             'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint',
                              'Tag', 'Time', 'Transition', 'TrInfo',
                              'Workflow', 'WorkflowTransition']
         self.assertListEqual(entities, sorted(expected_entities))
--- a/test/unittest_selectors.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/test/unittest_selectors.py	Tue Feb 01 11:52:10 2011 +0100
@@ -24,10 +24,11 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.appobject import Selector, AndSelector, OrSelector
 from cubicweb.selectors import (is_instance, adaptable, match_user_groups,
-                                multi_lines_rset, score_entity)
-from cubicweb.interfaces import IDownloadable
+                                multi_lines_rset, score_entity, is_in_state,
+                                on_transition)
 from cubicweb.web import action
 
+
 class _1_(Selector):
     def __call__(self, *args, **kwargs):
         return 1
@@ -138,6 +139,35 @@
         self.assertEqual(selector(None), 0)
 
 
+class IsInStateSelectorTC(CubicWebTC):
+    def setup_database(self):
+        wf = self.shell().add_workflow("testwf", 'StateFull', default=True)
+        initial = wf.add_state(u'initial', initial=True)
+        final = wf.add_state(u'final')
+        wf.add_transition(u'forward', (initial,), final)
+
+    def test_initial_state(self):
+        req = self.request()
+        entity = req.create_entity('StateFull')
+        selector = is_in_state(u'initial')
+        self.commit()
+        score = selector(entity.__class__, None, entity=entity)
+        self.assertEqual(score, 1)
+
+    def test_final_state(self):
+        req = self.request()
+        entity = req.create_entity('StateFull')
+        selector = is_in_state(u'initial')
+        self.commit()
+        entity.cw_adapt_to('IWorkflowable').fire_transition(u'forward')
+        self.commit()
+        score = selector(entity.__class__, None, entity=entity)
+        self.assertEqual(score, 0)
+        selector = is_in_state(u'final')
+        score = selector(entity.__class__, None, entity=entity)
+        self.assertEqual(score, 1)
+
+
 class ImplementsSelectorTC(CubicWebTC):
     def test_etype_priority(self):
         req = self.request()
@@ -159,6 +189,131 @@
                           3)
 
 
+class WorkflowSelectorTC(CubicWebTC):
+    def _commit(self):
+        self.commit()
+        self.wf_entity.clear_all_caches()
+
+    def setup_database(self):
+        wf = self.shell().add_workflow("wf_test", 'StateFull', default=True)
+        created   = wf.add_state('created', initial=True)
+        validated = wf.add_state('validated')
+        abandoned = wf.add_state('abandoned')
+        wf.add_transition('validate', created, validated, ('managers',))
+        wf.add_transition('forsake', (created, validated,), abandoned, ('managers',))
+
+    def setUp(self):
+        super(WorkflowSelectorTC, self).setUp()
+        self.req = self.request()
+        self.wf_entity = self.req.create_entity('StateFull', name=u'')
+        self.rset = self.wf_entity.as_rset()
+        self.adapter = self.wf_entity.cw_adapt_to('IWorkflowable')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'created')
+        # enable debug mode to state/transition validation on the fly
+        self.vreg.config.debugmode = True
+
+    def tearDown(self):
+        self.vreg.config.debugmode = False
+        super(WorkflowSelectorTC, self).tearDown()
+
+    def test_is_in_state(self):
+        for state in ('created', 'validated', 'abandoned'):
+            selector = is_in_state(state)
+            self.assertEqual(selector(None, self.req, self.rset),
+                             state=="created")
+
+        self.adapter.fire_transition('validate')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'validated')
+
+        selector = is_in_state('created')
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        selector = is_in_state('validated')
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+        selector = is_in_state('validated', 'abandoned')
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+        selector = is_in_state('abandoned')
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+
+        self.adapter.fire_transition('forsake')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'abandoned')
+
+        selector = is_in_state('created')
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        selector = is_in_state('validated')
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        selector = is_in_state('validated', 'abandoned')
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+        self.assertEqual(self.adapter.state, 'abandoned')
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+
+    def test_is_in_state_unvalid_names(self):
+        selector = is_in_state("unknown")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown state(s): unknown")
+        selector = is_in_state("weird", "unknown", "created", "weird")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown state(s): unknown,weird")
+
+    def test_on_transition(self):
+        for transition in ('validate', 'forsake'):
+            selector = on_transition(transition)
+            self.assertEqual(selector(None, self.req, self.rset), 0)
+
+        self.adapter.fire_transition('validate')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'validated')
+
+        selector = on_transition("validate")
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+        selector = on_transition("validate", "forsake")
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+        selector = on_transition("forsake")
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+
+        self.adapter.fire_transition('forsake')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'abandoned')
+
+        selector = on_transition("validate")
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        selector = on_transition("validate", "forsake")
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+        selector = on_transition("forsake")
+        self.assertEqual(selector(None, self.req, self.rset), 1)
+
+    def test_on_transition_unvalid_names(self):
+        selector = on_transition("unknown")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown transition(s): unknown")
+        selector = on_transition("weird", "unknown", "validate", "weird")
+        with self.assertRaises(ValueError) as cm:
+            selector(None, self.req, self.rset)
+        self.assertEqual(str(cm.exception),
+                         "wf_test: unknown transition(s): unknown,weird")
+
+    def test_on_transition_with_no_effect(self):
+        """selector will not be triggered with `change_state()`"""
+        self.adapter.change_state('validated')
+        self._commit()
+        self.assertEqual(self.adapter.state, 'validated')
+
+        selector = on_transition("validate")
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        selector = on_transition("validate", "forsake")
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+        selector = on_transition("forsake")
+        self.assertEqual(selector(None, self.req, self.rset), 0)
+
+
 class MatchUserGroupsTC(CubicWebTC):
     def test_owners_group(self):
         """tests usage of 'owners' group with match_user_group"""
@@ -249,8 +404,8 @@
 
     def test_intscore_entity_selector(self):
         req = self.request()
+        rset = req.execute('Any E WHERE E eid 1')
         selector = score_entity(lambda x: None)
-        rset = req.execute('Any E WHERE E eid 0')
         self.assertEqual(selector(None, req, rset), 0)
         selector = score_entity(lambda x: "something")
         self.assertEqual(selector(None, req, rset), 1)
--- a/utils.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/utils.py	Tue Feb 01 11:52:10 2011 +0100
@@ -67,6 +67,8 @@
 
 def support_args(callable, *argnames):
     """return true if the callable support given argument names"""
+    if isinstance(callable, type):
+        callable = callable.__init__
     argspec = getargspec(callable)
     if argspec[2]:
         return True
--- a/view.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/view.py	Tue Feb 01 11:52:10 2011 +0100
@@ -164,6 +164,7 @@
           the whole result set (which may be None in this case), `call` is
           called
         """
+        # XXX use .cw_row/.cw_col
         row = context.get('row')
         if row is not None:
             context.setdefault('col', 0)
@@ -210,12 +211,21 @@
         if rset is None:
             raise NotImplementedError, (self, "an rset is required")
         wrap = self.templatable and len(rset) > 1 and self.add_div_section
-        # XXX propagate self.extra_kwargs?
-        for i in xrange(len(rset)):
+        # avoid re-selection if rset of size 1, we already have the most
+        # specific view
+        if rset.rowcount != 1:
+            kwargs.setdefault('initargs', self.cw_extra_kwargs)
+            for i in xrange(len(rset)):
+                if wrap:
+                    self.w(u'<div class="section">')
+                self.wview(self.__regid__, rset, row=i, **kwargs)
+                if wrap:
+                    self.w(u"</div>")
+        else:
             if wrap:
                 self.w(u'<div class="section">')
-            self.cw_row = i
-            self.wview(self.__regid__, rset, row=i, **kwargs)
+            kwargs.setdefault('col', 0)
+            self.cell_call(row=0, **kwargs)
             if wrap:
                 self.w(u"</div>")
 
@@ -345,7 +355,7 @@
             if table:
                 w(u'<th>%s</th>' % label)
             else:
-                w(u'<span>%s</span> ' % label)
+                w(u'<span class="label">%s</span> ' % label)
         if table:
             if not (show_label and label):
                 w(u'<td colspan="2">%s</td></tr>' % value)
@@ -375,6 +385,7 @@
     def entity_call(self, entity, **kwargs):
         raise NotImplementedError()
 
+
 class StartupView(View):
     """base class for views which doesn't need a particular result set to be
     displayed (so they can always be displayed !)
--- a/web/component.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/component.py	Tue Feb 01 11:52:10 2011 +0100
@@ -294,16 +294,18 @@
             self.call(**kwargs)
             return
         getlayout = self._cw.vreg['components'].select
+        layout = getlayout('layout', self._cw, **self.layout_select_args())
+        layout.render(w)
+
+    def layout_select_args(self):
         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)
+        return dict(rset=self.cw_rset, row=self.cw_row, col=self.cw_col,
+                    view=self, context=context)
 
     def init_rendering(self):
         """init rendering callback: that's the good time to check your component
@@ -388,10 +390,37 @@
             entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
         self.entity = entity
 
+    def layout_select_args(self):
+        args = super(EntityCtxComponent, self).layout_select_args()
+        args['entity'] = self.entity
+        return args
+
     @property
     def domid(self):
         return domid(self.__regid__) + unicode(self.entity.eid)
 
+    def lazy_view_holder(self, w, entity, oid, registry='views'):
+        """add a holder and return an url that may be used to replace this
+        holder by the html generate by the view specified by registry and
+        identifier. Registry defaults to 'views'.
+        """
+        holderid = '%sHolder' % self.domid
+        w(u'<div id="%s"></div>' % holderid)
+        params = self.cw_extra_kwargs.copy()
+        params.pop('view', None)
+        params.pop('entity', None)
+        form = params.pop('formparams', {})
+        form['pageid'] = self._cw.pageid
+        if entity.has_eid():
+            eid = entity.eid
+        else:
+            eid = None
+            form['etype'] = entity.__regid__
+            form['tempEid'] = entity.eid
+        args = [json_dumps(x) for x in (registry, oid, eid, params)]
+        return self._cw.ajax_replace_url(
+            holderid, fname='render', arg=args, **form)
+
 
 # high level abstract classes ##################################################
 
--- a/web/data/cubicweb.ajax.js	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/data/cubicweb.ajax.js	Tue Feb 01 11:52:10 2011 +0100
@@ -563,6 +563,51 @@
                );
 }
 
+/* High-level functions *******************************************************/
+
+/**
+ * .. function:: reloadCtxComponentsSection(context, actualEid, creationEid=None)
+ *
+ * reload all components in the section for a given `context`.
+ *
+ * This is necessary for cases where the parent entity (on which the section
+ * apply) has been created during post, hence the section has to be reloaded to
+ * consider its new eid, hence the two additional arguments `actualEid` and
+ * `creationEid`: `actualEid` is the eid of newly created top level entity and
+ * `creationEid` the fake eid that was given as form creation marker (e.g. A).
+ *
+ * You can still call this function with only the actual eid if you're not in
+ * such creation case.
+ */
+function reloadCtxComponentsSection(context, actualEid, creationEid) {
+    // in this case, actualEid is the eid of newly created top level entity and
+    // creationEid the fake eid given as form creation marker (e.g. A)
+    if (!creationEid) { creationEid = actualEid ; }
+    var $compsholder = $('#' + context + creationEid);
+    // reload the whole components section
+    $compsholder.children().each(function (index) {
+	// XXX this.id[:-len(eid)]
+	var compid = this.id.replace("_", ".").rstrip(creationEid);
+	var params = ajaxFuncArgs('render', null, 'ctxcomponents',
+				  compid, actualEid);
+	$(this).loadxhtml('json', params, null, 'swap', true);
+    });
+    $compsholder.attr('id', context + actualEid);
+}
+
+
+/**
+ * .. function:: reload(domid, registry, formparams, *render_args)
+ *
+ * `js_render` based reloading of views and components.
+ */
+function reload(domid, compid, registry, formparams  /* ... */) {
+    var ajaxArgs = ['render', formparams, registry, compid];
+    ajaxArgs = ajaxArgs.concat(cw.utils.sliceList(arguments, 4));
+    var params = ajaxFuncArgs.apply(null, ajaxArgs);
+    $('#'+domid).loadxhtml('json', params, null, 'swap');
+}
+
 /* DEPRECATED *****************************************************************/
 
 preprocessAjaxLoad = cw.utils.deprecatedFunction(
--- a/web/data/cubicweb.css	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/data/cubicweb.css	Tue Feb 01 11:52:10 2011 +0100
@@ -221,7 +221,6 @@
 
 table#header {
   background: %(headerBgColor)s url("banner.png") repeat-x top left;
-  text-align: left;
   width: 100%;
 }
 
@@ -233,10 +232,6 @@
   color: %(defaultColor)s;
 }
 
-table#header td#headtext {
-  float: left;
-}
-
 table#header td#header-right {
   padding-top: 1em;
   float: right;
@@ -253,10 +248,6 @@
 }
 
 /* Popup on login box and userActionBox */
-div.popupWrapper {
-  position: relative;
-}
-
 div.popup {
   position: absolute;
   background: #fff;
--- a/web/data/cubicweb.edition.js	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/data/cubicweb.edition.js	Tue Feb 01 11:52:10 2011 +0100
@@ -588,7 +588,7 @@
         var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
         var d = loadRemote('json', args, 'POST');
     } catch(ex) {
-        log('got exception', ex);
+        cw.log('got exception', ex);
         return false;
     }
     d.addCallback(function(result, req) {
--- a/web/data/cubicweb.ie.css	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/data/cubicweb.ie.css	Tue Feb 01 11:52:10 2011 +0100
@@ -4,10 +4,20 @@
   margin-top: 0px;
 }
 
+table#header td#header-right  div.popupWrapper {
+  position: relative;
+  z-index: 400;
+}
+
+table#header td#header-right{
+  text-align:right;
+}
+
 /* quick and dirty solution for pop to be
    correctly displayed on right edge of window */
 div.popupWrapper{
   direction:rtl;
+  text-align:right;
 }
 
 div#rqlinput input.rqlsubmit{
--- a/web/data/cubicweb.old.css	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/data/cubicweb.old.css	Tue Feb 01 11:52:10 2011 +0100
@@ -228,7 +228,6 @@
 
 table#header {
   background: #ff7700 url("banner.png") left top repeat-x;
-  text-align: left;
   width: 100%;
 }
 
@@ -240,10 +239,6 @@
   color: #000;
 }
 
-table#header td#headtext {
-  float: left;
-}
-
 table#header td#header-right {
   padding-top: 1em;
   float: right;
@@ -262,12 +257,16 @@
 
 /* Popup on login box and userActionBox */
 
+.popupWrapper{
+  position:relative;
+}
+
 div.popup {
   position: absolute;
   background: #fff;
   border: 1px solid black;
   text-align: left;
-  z-index:400;
+  z-index: 400;
 }
 
 div.popup ul li a {
--- a/web/data/cubicweb.python.js	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/data/cubicweb.python.js	Tue Feb 01 11:52:10 2011 +0100
@@ -188,6 +188,16 @@
     return this.replace(/^\s*(.*?)\s*$/, "$1");
 };
 
+/**
+ * .. function:: String.prototype.rstrip()
+ *
+ * python-like rstrip method for js strings
+ */
+String.prototype.rstrip = function(str) {
+    if (!str) { str = '\s' ; }
+    return this.replace(new RegExp('^(.*?)' + str + '*$'), "$1");
+};
+
 // ========= class factories ========= //
 
 /**
--- a/web/data/cubicweb.widgets.js	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/data/cubicweb.widgets.js	Tue Feb 01 11:52:10 2011 +0100
@@ -542,3 +542,111 @@
         }
     }
 };
+
+
+// InOutWidget  This contains specific InOutnWidget javascript
+// IE things can not handle hide/show options on select, this cloned list solition (should propably have 2 widgets)
+
+(function ($) {
+    var defaultSettings = {
+        bindDblClick: true
+    };
+    var methods = {
+        __init__: function(fromSelect, toSelect, options) {
+            var settings = $.extend({}, defaultSettings, options);
+            var bindDblClick = settings['bindDblClick'];
+            var $fromNode = $(cw.jqNode(fromSelect));
+            var clonedSelect = $fromNode.clone();
+            var $toNode = $(cw.jqNode(toSelect));
+            var $addButton = $(this.find('.cwinoutadd')[0]);
+            var $removeButton = $(this.find('.cwinoutremove')[0]);
+            // bind buttons
+            var name = this.attr('id');
+            var instanceData = {'fromNode':fromSelect,
+                                'toNode':toSelect,
+                                'cloned':clonedSelect,
+                                'bindDblClick':bindDblClick,
+                                'name': name};
+            $addButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetAddValues);
+            $removeButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetRemoveValues);
+            if(bindDblClick){
+                $toNode.bind('dblclick', {'instanceData': instanceData}, methods.inOutWidgetRemoveValues);
+            }
+            methods.inOutWidgetRemplaceSelect($fromNode, $toNode, clonedSelect, bindDblClick, name);
+        },
+
+        inOutWidgetRemplaceSelect: function($fromNode, $toNode, clonedSelect, bindDblClick, name){
+             var $newSelect = clonedSelect.clone();
+             $toNode.find('option').each(function() {
+                 $newSelect.find('$(this)[value='+$(this).val()+']').remove();
+              });
+             var fromparent = $fromNode.parent();
+             if (bindDblClick) {
+                 //XXX jQuery live binding does not seem to work here
+                 $newSelect.bind('dblclick', {'instanceData': {'fromNode':$fromNode.attr('id'),
+                                     'toNode': $toNode.attr('id'),
+                                     'cloned':clonedSelect,
+                                     'bindDblClick':bindDblClick,
+                                     'name': name}},
+                                 methods.inOutWidgetAddValues);
+             }
+             $fromNode.remove();
+             fromparent.append($newSelect);
+        },
+
+        inOutWidgetAddValues: function(event){
+            var $fromNode = $(cw.jqNode(event.data.instanceData.fromNode));
+            var $toNode = $(cw.jqNode(event.data.instanceData.toNode));
+            $fromNode.find('option:selected').each(function() {
+                var option = $(this);
+                var newoption = OPTION({'value':option.val()},
+	 			 value=option.text());
+                $toNode.append(newoption);
+                var hiddenInput = INPUT({
+                    type: "hidden", name: event.data.instanceData.name,
+                    value:option.val()
+                });
+                $toNode.parent().append(hiddenInput);
+            });
+            methods.inOutWidgetRemplaceSelect($fromNode, $toNode, event.data.instanceData.cloned,
+                                              event.data.instanceData.bindDblClick,
+                                              event.data.instanceData.name);
+            // for ie 7 : ie does not resize correctly the select
+            if($.browser.msie && $.browser.version.substr(0,1) < 8){
+                var p = $toNode.parent();
+                var newtoNode = $toNode.clone();
+                if (event.data.instanceData.bindDblClick) {
+                    newtoNode.bind('dblclick', {'fromNode': $fromNode.attr('id'),
+                                                'toNode': $toNode.attr('id'),
+                                                'cloned': event.data.instanceData.cloned,
+                                                'bindDblClick': true,
+                                                'name': event.data.instanceData.name},
+                                   methods.inOutWidgetRemoveValues);
+                }
+                $toNode.remove();
+                p.append(newtoNode);
+            }
+        },
+
+        inOutWidgetRemoveValues: function(event){
+            var $fromNode = $(cw.jqNode(event.data.instanceData.toNode));
+            var $toNode = $(cw.jqNode(event.data.instanceData.fromNode));
+            var name = event.data.instanceData.name.replace(':', '\\:');
+            $fromNode.find('option:selected').each(function(){
+                var option = $(this);
+                var newoption = OPTION({'value':option.val()},
+	 			 value=option.text());
+                option.remove();
+                $fromNode.parent().find('input[name]='+ name).each(function() {
+                    $(this).val()==option.val()?$(this).remove():null;
+               });
+            });
+            methods.inOutWidgetRemplaceSelect($toNode, $fromNode,  event.data.instanceData.cloned,
+                                              event.data.instanceData.bindDblClick,
+                                              event.data.instanceData.name);
+        }
+    };
+    $.fn.cwinoutwidget = function(fromSelect, toSelect, options){
+        return methods.__init__.apply(this, [fromSelect, toSelect, options]);
+    };
+})(jQuery);
\ No newline at end of file
--- a/web/formwidgets.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/formwidgets.py	Tue Feb 01 11:52:10 2011 +0100
@@ -63,6 +63,7 @@
 .. autoclass:: cubicweb.web.formwidgets.FCKEditor
 .. autoclass:: cubicweb.web.formwidgets.AjaxWidget
 .. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget
+.. autoclass:: cubicweb.web.formwidgets.InOutWidget
 
 .. kill or document StaticFileAutoCompletionWidget
 .. kill or document LazyRestrictedAutoCompletionWidget
@@ -1001,3 +1002,55 @@
             'label': label, 'imgsrc': imgsrc,
             'domid': self.domid, 'href': self.href}
 
+class InOutWidget(Select):
+    needs_js = ('cubicweb.widgets.js', )
+    template = """
+<table id="%(widgetid)s">
+<tr><td>%(inoutinput)s</td>
+    <td><div style="margin-bottom:3px">%(addinput)s</div> <div>%(removeinput)s</div></td>
+    <td>%(resinput)s</td></tr>
+</table>
+"""
+    add_button = """<input type="button" id="cwinoutadd"  class="wdgButton cwinoutadd" value="&gt;&gt;" size="10" />"""
+    remove_button ="""<input type="button" class="wdgButton cwinoutremove" value="&lt;&lt;" size="10" />"""
+
+    def __init__(self, attrs=None):
+        super(InOutWidget, self).__init__(attrs, multiple=True)
+
+    def render_select(self, form, field, name, selected=False):
+        values, attrs = self.values_and_attributes(form, field)
+        options = []
+        inputs = []
+        for _option in field.vocabulary(form):
+            try:
+                label, value, oattrs = _option
+            except ValueError:
+                label, value = _option
+            if selected:
+                # add values
+                if value in values:
+                    options.append(tags.option(label, value=value))
+                    # add hidden inputs
+                    inputs.append(tags.input(value=value, name=field.dom_id(form), type="hidden"))
+            else:
+                options.append(tags.option(label, value=value))
+        if 'size' not in attrs:
+            attrs['size'] = 5
+        if 'id' in attrs :
+            attrs.pop('id')
+        return tags.select(name=name, multiple=self._multiple, id=name,
+                           options=options, **attrs) + '\n'.join(inputs)
+
+
+    def _render(self, form, field, renderer):
+        domid = field.dom_id(form)
+        jsnodes = {'widgetid': domid, 'from': 'from_' + domid, 'to': 'to_' + domid}
+        form._cw.add_onload(u'$(cw.jqNode("%s")).cwinoutwidget("%s", "%s");'
+                            % (jsnodes['widgetid'], jsnodes['from'], jsnodes['to']))
+        field.required=True
+        return self.template % {'widgetid': jsnodes['widgetid'],
+                                'inoutinput' : self.render_select(form, field, jsnodes['from']), # helpinfo select tag
+                                'resinput' : self.render_select(form, field, jsnodes['to'], selected=True), # select tag with resultats
+                                'addinput' : self.add_button % jsnodes,
+                                'removeinput': self.remove_button % jsnodes
+                                }
--- a/web/uicfg.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/uicfg.py	Tue Feb 01 11:52:10 2011 +0100
@@ -53,7 +53,7 @@
 from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
                             RelationTagsDict, NoTargetRelationTagsDict,
                             register_rtag, _ensure_str_key)
-from cubicweb.schema import META_RTYPES
+from cubicweb.schema import META_RTYPES, INTERNAL_TYPES, WORKFLOW_TYPES
 
 
 # primary view configuration ##################################################
@@ -120,6 +120,8 @@
                 continue
             if eschema.schema_entity():
                 self.setdefault(eschema, 'schema')
+            elif eschema in INTERNAL_TYPES or eschema in WORKFLOW_TYPES:
+                self.setdefault(eschema, 'system')
             elif eschema.is_subobject(strict=True):
                 self.setdefault(eschema, 'subobject')
             else:
@@ -127,14 +129,9 @@
 
 indexview_etype_section = InitializableDict(
     EmailAddress='subobject',
+    Bookmark='system',
     # entity types in the 'system' table by default (managers only)
-    CWSource='system',
     CWUser='system', CWGroup='system',
-    CWPermission='system',
-    CWCache='system',
-    Workflow='system',
-    ExternalUri='system',
-    Bookmark='system',
     )
 
 # autoform.AutomaticEntityForm configuration ##################################
@@ -405,23 +402,22 @@
         return super(ReleditTags, self).tag_relation(key, tag)
 
 def init_reledit_ctrl(rtag, sschema, rschema, oschema, role):
-    if rschema.final:
-        return
-    composite = rschema.rdef(sschema, oschema).composite == role
-    if role == 'subject':
-        oschema = '*'
-    else:
-        sschema = '*'
     values = rtag.get(sschema, rschema, oschema, role)
-    edittarget = values.get('edit_target')
-    if edittarget not in (None, 'rtype', 'related'):
-        rtag.warning('reledit: wrong value for edit_target on relation %s: %s',
-                     rschema, edittarget)
-        edittarget = None
-    if not edittarget:
-        edittarget = 'related' if composite else 'rtype'
-        rtag.tag_relation((sschema, rschema, oschema, role),
-                          {'edit_target': edittarget})
+    if not rschema.final:
+        composite = rschema.rdef(sschema, oschema).composite == role
+        if role == 'subject':
+            oschema = '*'
+        else:
+            sschema = '*'
+        edittarget = values.get('edit_target')
+        if edittarget not in (None, 'rtype', 'related'):
+            rtag.warning('reledit: wrong value for edit_target on relation %s: %s',
+                         rschema, edittarget)
+            edittarget = None
+        if not edittarget:
+            edittarget = 'related' if composite else 'rtype'
+            rtag.tag_relation((sschema, rschema, oschema, role),
+                              {'edit_target': edittarget})
     if not 'novalue_include_rtype' in values:
         showlabel = primaryview_display_ctrl.get(
             sschema, rschema, oschema, role).get('showlabel', True)
--- a/web/views/ajaxedit.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/ajaxedit.py	Tue Feb 01 11:52:10 2011 +0100
@@ -22,9 +22,9 @@
 from cubicweb import role
 from cubicweb.view import View
 from cubicweb.selectors import match_form_params, match_kwargs
-from cubicweb.web.component import EditRelationMixIn
+from cubicweb.web import component, stdmsgs, formwidgets as fw
 
-class AddRelationView(EditRelationMixIn, View):
+class AddRelationView(component.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.
@@ -36,7 +36,7 @@
     cw_property_defs = {} # don't want to inherit this from Box
     expected_kwargs = form_params = ('rtype', 'target')
 
-    build_js = EditRelationMixIn.build_reload_js_call
+    build_js = component.EditRelationMixIn.build_reload_js_call
 
     def cell_call(self, row, col, rtype=None, target=None, etype=None):
         self.rtype = rtype or self._cw.form['rtype']
@@ -73,3 +73,41 @@
             self.pagination(self._cw, rset, w=self.w)
             return rset.entities()
         super(AddRelationView, self).unrelated_entities(self)
+
+
+def ajax_composite_form(container, entity, rtype, okjs, canceljs,
+                        entityfkwargs=None):
+    """
+    * if entity is None, edit container (assert container.has_eid())
+    * if entity has not eid, will be created
+    * if container has not eid, will be created (see vcreview InsertionPoint)
+    """
+    req = container._cw
+    parentexists = entity is None or container.has_eid()
+    buttons = [fw.Button(onclick=okjs),
+               fw.Button(stdmsgs.BUTTON_CANCEL, onclick=canceljs)]
+    freg = req.vreg['forms']
+    # main form kwargs
+    mkwargs = dict(action='#', domid='%sForm%s' % (rtype, container.eid),
+                   form_buttons=buttons,
+                   onsubmit='javascript: %s; return false' % okjs)
+    # entity form kwargs
+    # use formtype=inlined to skip the generic relations edition section
+    fkwargs = dict(entity=entity or container, formtype='inlined')
+    if entityfkwargs is not None:
+        fkwargs.update(entityfkwargs)
+    # form values
+    formvalues = {}
+    if entity is not None: # creation
+        formvalues[rtype] = container.eid
+    if parentexists: # creation / edition
+        mkwargs.update(fkwargs)
+        # use formtype=inlined to avoid viewing the relation edition section
+        form = freg.select('edition', req, **mkwargs)
+    else: # creation of both container and comment entities
+        form = freg.select('composite', req, form_renderer_id='default',
+                            **mkwargs)
+        form.add_subform(freg.select('edition', req, entity=container,
+                                      mainform=False, mainentity=True))
+        form.add_subform(freg.select('edition', req, mainform=False, **fkwargs))
+    return form, formvalues
--- a/web/views/basecontrollers.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/basecontrollers.py	Tue Feb 01 11:52:10 2011 +0100
@@ -426,6 +426,7 @@
                   selectargs=None, renderargs=None):
         if eid is not None:
             rset = self._cw.eid_rset(eid)
+            # XXX set row=0
         elif self._cw.form.get('rql'):
             rset = self._cw.execute(self._cw.form['rql'])
         else:
--- a/web/views/cwuser.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/cwuser.py	Tue Feb 01 11:52:10 2011 +0100
@@ -26,7 +26,7 @@
 
 from cubicweb.selectors import one_line_rset, is_instance, match_user_groups
 from cubicweb.view import EntityView
-from cubicweb.web import action, uicfg
+from cubicweb.web import action, uicfg, formwidgets
 from cubicweb.web.views import tabs
 
 _pvs = uicfg.primaryview_section
@@ -39,6 +39,11 @@
 _pvs.tag_object_of(('*', 'in_group', 'CWGroup'), 'relations')
 _pvs.tag_object_of(('*', 'require_group', 'CWGroup'), 'relations')
 
+_affk = uicfg.autoform_field_kwargs
+
+_affk.tag_subject_of(('CWUser', 'in_group', 'CWGroup'),
+                    {'widget': formwidgets.InOutWidget})
+
 class UserPreferencesEntityAction(action.Action):
     __regid__ = 'prefs'
     __select__ = (one_line_rset() & is_instance('CWUser') &
--- a/web/views/forms.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/forms.py	Tue Feb 01 11:52:10 2011 +0100
@@ -286,7 +286,8 @@
         super(EntityFieldsForm, self).__init__(_cw, rset, row, col, **kwargs)
         self.add_hidden('__type', self.edited_entity.__regid__, eidparam=True)
         self.add_hidden('eid', self.edited_entity.eid)
-        if kwargs.get('mainform', True): # mainform default to true in parent
+        # mainform default to true in parent, hence default to True
+        if kwargs.get('mainform', True) or kwargs.get('mainentity', False):
             self.add_hidden(u'__maineid', self.edited_entity.eid)
             # If we need to directly attach the new object to another one
             if self._cw.list_form_param('__linkto'):
--- a/web/views/ibreadcrumbs.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/ibreadcrumbs.py	Tue Feb 01 11:52:10 2011 +0100
@@ -52,7 +52,7 @@
     __select__ = is_instance('Any', accept_none=False)
 
     def parent_entity(self):
-        if hasattr(self.entity, 'parent'):
+        if hasattr(self.entity, 'parent') and callable(self.entity.parent):
             warn('[3.9] parent() method is deprecated, define a '
                  'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead'
                  % self.entity.__class__, DeprecationWarning)
--- a/web/views/idownloadable.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/idownloadable.py	Tue Feb 01 11:52:10 2011 +0100
@@ -50,8 +50,7 @@
 
 
 class DownloadBox(component.EntityCtxComponent):
-    __regid__ = 'download_box'
-    # no download box for images
+    __regid__ = 'download_box'    # no download box for images
     __select__ = (component.EntityCtxComponent.__select__ &
                   adaptable('IDownloadable') & ~has_mimetype('image/'))
 
--- a/web/views/primary.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/primary.py	Tue Feb 01 11:52:10 2011 +0100
@@ -98,6 +98,7 @@
         self.w(u'<div class="%s">' % context)
         for comp in self._cw.vreg['ctxcomponents'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=self, context=context):
+            # XXX bw compat code
             try:
                 comp.render(w=self.w, row=self.cw_row, view=self)
             except TypeError:
--- a/web/views/reledit.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/reledit.py	Tue Feb 01 11:52:10 2011 +0100
@@ -81,7 +81,7 @@
         self._cw.add_js(('cubicweb.reledit.js', 'cubicweb.edition.js', 'cubicweb.ajax.js'))
         entity = self.cw_rset.get_entity(row, col)
         rschema = self._cw.vreg.schema[rtype]
-        self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
+        self._rules = rctrl.etype_get(entity.e_schema, rschema, role, '*')
         if rvid is not None or default_value is not None:
             warn('[3.9] specifying rvid/default_value on select is deprecated, '
                  'reledit_ctrl rtag to control this' % self, DeprecationWarning)
--- a/web/views/startup.py	Mon Jan 24 19:09:42 2011 +0100
+++ b/web/views/startup.py	Tue Feb 01 11:52:10 2011 +0100
@@ -84,7 +84,7 @@
     def create_links(self):
         self.w(u'<ul class="createLink">')
         for etype in self.add_etype_links:
-            eschema = self.schema.eschema(etype)
+            eschema = self._cw.vreg.schema.eschema(etype)
             if eschema.has_perm(self._cw, 'add'):
                 self.w(u'<li><a href="%s">%s</a></li>' % (
                         self._cw.build_url('add/%s' % eschema),