backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 13 Sep 2010 15:15:21 +0200
changeset 6225 a176e68b7d0d
parent 6182 30de0be8f895 (current diff)
parent 6224 1f4beef3962d (diff)
child 6227 82d4011f54c1
backport stable
hooks/syncschema.py
i18n/en.po
i18n/es.po
i18n/fr.po
rset.py
server/repository.py
server/sources/native.py
server/test/data/schema.py
server/test/unittest_msplanner.py
server/test/unittest_querier.py
server/test/unittest_repository.py
test/unittest_rset.py
utils.py
web/data/cubicweb.css
web/formfields.py
web/views/basecontrollers.py
web/views/basetemplates.py
web/views/editforms.py
--- a/.hgtags	Tue Sep 07 17:34:42 2010 +0200
+++ b/.hgtags	Mon Sep 13 15:15:21 2010 +0200
@@ -149,3 +149,5 @@
 8d32d82134dc1d8eb0ce230191f34fd49084a168 cubicweb-debian-version-3.9.4-1
 0a1fce8ddc672ca9ee7328ed4f88c1aa6e48d286 cubicweb-version-3.9.5
 12038ca95f0fff2205f7ee029f5602d192118aec cubicweb-debian-version-3.9.5-1
+d37428222a6325583be958d7c7fe7c595115663d cubicweb-version-3.9.6
+7d2cab567735a17cab391c1a7f1bbe39118308a2 cubicweb-debian-version-3.9.6-1
--- a/__pkginfo__.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/__pkginfo__.py	Mon Sep 13 15:15:21 2010 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 9, 5)
+numversion = (3, 9, 6)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -43,7 +43,7 @@
     'logilab-common': '>= 0.51.0',
     'logilab-mtconverter': '>= 0.8.0',
     'rql': '>= 0.26.2',
-    'yams': '>= 0.29.1',
+    'yams': '>= 0.30.0',
     'docutils': '>= 0.6',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependancies
@@ -52,7 +52,7 @@
     'Twisted': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '>= 1.1.0',
+    'logilab-database': '>= 1.2.0',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
     }
 
--- a/_exceptions.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/_exceptions.py	Mon Sep 13 15:15:21 2010 +0200
@@ -80,6 +80,8 @@
 class MultiSourcesError(RepositoryError, InternalError):
     """usually due to bad multisources configuration or rql query"""
 
+class UniqueTogetherError(RepositoryError):
+    """raised when a unique_together constraint caused an IntegrityError"""
 
 # security exceptions #########################################################
 
--- a/cwconfig.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/cwconfig.py	Mon Sep 13 15:15:21 2010 +0200
@@ -51,7 +51,7 @@
         CW_INSTANCES_DATA_DIR = /var/lib/cubicweb/instances/
         CW_RUNTIME_DIR = /var/run/cubicweb/
 
- * 'user': ::
+* 'user': ::
 
         CW_INSTANCES_DIR = ~/etc/cubicweb.d/
         CW_INSTANCES_DATA_DIR = ~/etc/cubicweb.d/
--- a/debian/changelog	Tue Sep 07 17:34:42 2010 +0200
+++ b/debian/changelog	Mon Sep 13 15:15:21 2010 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.9.6-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 13 Sep 2010 10:50:15 +0200
+
 cubicweb (3.9.5-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Tue Sep 07 17:34:42 2010 +0200
+++ b/debian/control	Mon Sep 13 15:15:21 2010 +0200
@@ -33,7 +33,7 @@
 Conflicts: cubicweb-multisources
 Replaces: cubicweb-multisources
 Provides: cubicweb-multisources
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.1.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.2.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
 Recommends: pyro, cubicweb-documentation (= ${source:Version})
 Description: server part of 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.51.0), python-yams (>= 0.29.1), python-rql (>= 0.26.3), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.51.0), python-yams (>= 0.30.0), python-rql (>= 0.26.3), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/doc/book/en/admin/setup.rst	Tue Sep 07 17:34:42 2010 +0200
+++ b/doc/book/en/admin/setup.rst	Mon Sep 13 15:15:21 2010 +0200
@@ -59,6 +59,19 @@
 .. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
 
 
+.. _PipInstallation:
+
+Installation with pip
+`````````````````````
+
+|cubicweb| and its cubes have been pip_ installable since version 3.8. Search
+for them on pypi_::
+
+  pip install cubicweb
+
+.. _pip: http://pypi.python.org/pypi/pip
+.. _pypi: http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb
+
 .. _SourceInstallation:
 
 Install from source
@@ -70,12 +83,9 @@
 
 .. _`ftp site`: http://ftp.logilab.org/pub/cubicweb/
 
-Make sure you have installed the dependencies (see appendixes for the list).
+Make sure you also have all the :ref:`InstallDependencies`.
 
-|cubicweb| should soon be pip_ installable, stay tuned (expected in 3.8).
-
-.. _pip: http://pypi.python.org/pypi/pip
-
+.. _MercurialInstallation:
 
 Install from version control system
 ```````````````````````````````````
@@ -94,7 +104,7 @@
 
 Do not forget to update the forest itself (using `cd path/to/forest ; hg up`).
 
-Make sure you have installed the dependencies (see appendixes for the list).
+Make sure you also have all the :ref:`InstallDependencies`.
 
 
 .. _WindowsInstallation:
@@ -102,6 +112,10 @@
 Windows installation
 ````````````````````
 
+Your best option is probably the :ref:`PipInstallation`. If it does not work or
+if you want more control over the process, continue with the following
+instructions.
+
 Base elements
 ~~~~~~~~~~~~~
 
@@ -110,14 +124,15 @@
 done. We assume everything goes into `C:\\` in this document. Adjusting the
 installation drive should be straightforward.
 
-You should start by downloading and installing the Python(x,y) distribution. It
-contains python 2.5 plus numerous useful third-party modules and applications::
+You should start by downloading and installing Python version >= 2.5 and < 3.
 
-  http://www.pythonxy.com/download_fr.php
+An alternative option would be installing the Python(x,y)
+distribution. Python(x,y) is not a requirement, but it makes things easier for
+Windows user by wrapping in a single installer python 2.5 plus numerous useful
+third-party modules and applications (including Eclipse + pydev, which is an
+arguably good IDE for Python under Windows). Download it from this page::
 
-At the time of this writting, one gets version 2.1.15. Among the many things
-provided, one finds Eclipse + pydev (an arguably good IDE for python under
-windows).
+  http://code.google.com/p/pythonxy/wiki/Downloads
 
 Then you must grab Twisted. There is a windows installer directly available from
 this page::
@@ -166,11 +181,15 @@
 
   http://www.graphviz.org/Download_windows.php
 
-Simplejson will be provided within the forest, but a win32 compiled version will
-run much faster::
+Simplejson is needed when installing with Python 2.5, but included in the
+standard library for Python >= 2.6. It will be provided within the forest, but a
+win32 compiled version will run much faster::
 
   http://www.osuch.org/python-simplejson%3Awin32
 
+Make sure you also have all the :ref:`InstallDependencies` that are not specific
+to Windows.
+
 Tools
 ~~~~~
 
@@ -189,10 +208,14 @@
 
   http://www.vectrace.com/mercurialeclipse/
 
-Setting up the sources
-~~~~~~~~~~~~~~~~~~~~~~
+Getting the sources
+~~~~~~~~~~~~~~~~~~~
 
-You need to enable the mercurial forest extension. To do this, edit the file::
+You can either download the latest release (see :ref:`SourceInstallation`) or
+get the development version using Mercurial (see
+:ref:`MercurialInstallation` and below).
+
+To enable the Mercurial forest extension on Windows, edit the file::
 
   C:\Program Files\TortoiseHg\Mercurial.ini
 
@@ -250,14 +273,14 @@
 This currently assumes that the instances configurations is located at
 C:\\etc\\cubicweb.d.
 
-For a cube 'my_cube', you will then find
-C:\\etc\\cubicweb.d\\my_cube\\win32svc.py that has to be used thusly::
+For a cube 'my_instance', you will then find
+C:\\etc\\cubicweb.d\\my_instance\\win32svc.py that has to be used thusly::
 
   win32svc install
 
 This should just register your instance as a windows service. A simple::
 
-  net start cubicweb-my_cube
+  net start cubicweb-my_instance
 
 should start the service.
 
@@ -282,7 +305,12 @@
 
 Whatever the backend used, database connection information are stored in the
 instance's :file:`sources` file. Currently cubicweb has been tested using
-Postgresql (recommanded), MySQL, SQLServer and SQLite.
+Postgresql (recommended), MySQL, SQLServer and SQLite.
+
+Other possible sources of data include CubicWeb, Subversion, LDAP and Mercurial,
+but at least one relational database is required for CubicWeb to work. SQLite is
+not fit for production use, but it works for testing and ships with Python,
+which saves installation time when you want to get started quickly.
 
 .. _PostgresqlConfiguration:
 
@@ -394,7 +422,7 @@
     max_allowed_packet = 128M
 
 .. Note::
-    It is unclear whether mysql supports indexed string of arbitrary lenght or
+    It is unclear whether mysql supports indexed string of arbitrary length or
     not.
 
 
@@ -403,9 +431,10 @@
 SQLServer configuration
 ```````````````````````
 
-As of this writing, sqlserver support is in progress. You should be able to
-connect, create a database and go quite far, but some of the generated SQL is
-still currently not accepted by the backend.
+As of this writing, support for SQLServer 2005 is functional but incomplete. You
+should be able to connect, create a database and go quite far, but some of the
+SQL generated from RQL queries is still currently not accepted by the
+backend. Porting to SQLServer 2008 is also an item on the backlog.
 
 The `source` configuration file may look like this (specific parts only are
 shown)::
@@ -440,14 +469,13 @@
 ------------------
 
 If you want to use Pyro to access your instance remotly, or to have multi-source
-or distributed configuration, it is required to have a name server Pyro running
-on your network. By by default it is detected by a broadcast request, but you can
+or distributed configuration, it is required to have a Pyro name server running
+on your network. By default it is detected by a broadcast request, but you can
 specify a location in the instance's configuration file.
 
 To do so, you need to :
 
-* launch the server manually before starting cubicweb as a server with `pyro-nsd
-  start`
+* launch the pyro name server with `pyro-nsd start` before starting cubicweb
 
 * under debian, edit the file :file:`/etc/default/pyro-nsd` so that the name
   server pyro will be launched automatically when the machine fire up
--- a/doc/book/en/annexes/depends.rst	Tue Sep 07 17:34:42 2010 +0200
+++ b/doc/book/en/annexes/depends.rst	Mon Sep 13 15:15:21 2010 +0200
@@ -1,9 +1,9 @@
 .. -*- coding: utf-8 -*-
 
-.. _dependencies:
+.. _InstallDependencies:
 
-Dependencies
-============
+Installation dependencies
+=========================
 
 When you run CubicWeb from source, either by downloading the tarball or
 cloning the mercurial forest, here is the list of tools and libraries you need
@@ -55,9 +55,7 @@
 * plpythonu extension
 * tsearch2 extension (for postgres < 8.3, in postgres-contrib)
 
-Other optional packages :
-
-:
+Other optional packages:
 
 * fyzz - http://www.logilab.org/project/fyzz -
   http://pypi.python.org/pypi/fyzz - included in the forest, *to activate Sparql querying*
--- a/doc/book/en/tutorials/tools/windmill.rst	Tue Sep 07 17:34:42 2010 +0200
+++ b/doc/book/en/tutorials/tools/windmill.rst	Mon Sep 13 15:15:21 2010 +0200
@@ -29,7 +29,7 @@
 However, the Windmill project doesn't release frequently. Our recommandation is
 to used the last snapshot of the Git repository:
 
-.. sourcecode:: shell
+.. sourcecode:: bash
 
     git clone git://github.com/windmill/windmill.git HEAD
     cd windmill
--- a/hooks/syncschema.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/hooks/syncschema.py	Mon Sep 13 15:15:21 2010 +0200
@@ -703,6 +703,45 @@
             syssource.update_rdef_unique(session, rdef)
             self.unique_changed = True
 
+class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
+    entity = None # make pylint happy
+    def precommit_event(self):
+        session = self.session
+        prefix = SQL_PREFIX
+        table = '%s%s' % (prefix, self.entity.constraint_of[0].name)
+        cols = ['%s%s' % (prefix, r.rtype.name)
+                for r in self.entity.relations]
+        dbhelper= session.pool.source('system').dbhelper
+        sql = dbhelper.sql_create_multicol_unique_index(table, cols)
+        session.system_sql(sql)
+
+    # XXX revertprecommit_event
+
+    def postcommit_event(self):
+        eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid)
+        attrs = [r.rtype.name for r in self.entity.relations]
+        eschema._unique_together.append(attrs)
+
+class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
+    entity = oldcstr = None # for pylint
+    cols = [] # for pylint
+    def precommit_event(self):
+        session = self.session
+        prefix = SQL_PREFIX
+        table = '%s%s' % (prefix, self.entity.type)
+        dbhelper= session.pool.source('system').dbhelper
+        cols = ['%s%s' % (prefix, c) for c in self.cols]
+        sql = dbhelper.sql_drop_multicol_unique_index(table, cols)
+        session.system_sql(sql)
+
+    # XXX revertprecommit_event
+
+    def postcommit_event(self):
+        eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid)
+        cols = set(self.cols)
+        unique_together = [ut for ut in eschema._unique_together
+                           if set(ut) != cols]
+        eschema._unique_together = unique_together
 
 # operations for in-memory schema synchronization  #############################
 
@@ -1049,17 +1088,19 @@
 
 
 class AfterAddConstrainedByHook(SyncSchemaHook):
-    __regid__ = 'syncdelconstrainedby'
+    __regid__ = 'syncaddconstrainedby'
     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
     events = ('after_add_relation',)
 
     def __call__(self):
         if self._cw.added_in_transaction(self.eidfrom):
+            # used by get_constraints() which is called in CWAttributeAddOp
             self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
 
 
-class BeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):
+class BeforeDeleteConstrainedByHook(SyncSchemaHook):
     __regid__ = 'syncdelconstrainedby'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
     events = ('before_delete_relation',)
 
     def __call__(self):
@@ -1075,6 +1116,32 @@
         else:
             CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr)
 
+# unique_together constraints
+# XXX: use setoperations and before_add_relation here (on constraint_of and relations)
+class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook):
+    __regid__ = 'syncadd_cwuniquetogether_constraint'
+    __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint')
+    events = ('after_add_entity', 'after_update_entity')
+
+    def __call__(self):
+        CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity)
+
+
+class BeforeDeleteConstraintOfHook(SyncSchemaHook):
+    __regid__ = 'syncdelconstraintof'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constraint_of')
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        if self._cw.deleted_in_transaction(self.eidto):
+            return
+        schema = self._cw.vreg.schema
+        cstr = self._cw.entity_from_eid(self.eidfrom)
+        entity = schema.schema_by_eid(self.eidto)
+        cols = [r.rtype.name
+                for r in cstr.relations]
+        CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols)
+
 
 # permissions synchronization hooks ############################################
 
--- a/i18n/en.po	Tue Sep 07 17:34:42 2010 +0200
+++ b/i18n/en.po	Mon Sep 13 15:15:21 2010 +0200
@@ -2016,9 +2016,6 @@
 msgid "editable-table"
 msgstr ""
 
-msgid "edition"
-msgstr ""
-
 msgid "eid"
 msgstr ""
 
@@ -2717,6 +2714,9 @@
 msgid "missing parameters for entity %s"
 msgstr ""
 
+msgid "modification"
+msgstr ""
+
 msgid "modification_date"
 msgstr "modification date"
 
@@ -2967,9 +2967,6 @@
 msgid "permissions for this entity"
 msgstr ""
 
-msgid "personnal informations"
-msgstr ""
-
 msgid "pick existing bookmarks"
 msgstr ""
 
@@ -3029,6 +3026,9 @@
 msgid "primary_email_object"
 msgstr "primary email of"
 
+msgid "profile"
+msgstr ""
+
 msgid "progress"
 msgstr ""
 
--- a/i18n/es.po	Tue Sep 07 17:34:42 2010 +0200
+++ b/i18n/es.po	Mon Sep 13 15:15:21 2010 +0200
@@ -2100,9 +2100,6 @@
 msgid "editable-table"
 msgstr "Tabla modificable"
 
-msgid "edition"
-msgstr "Edición"
-
 msgid "eid"
 msgstr "eid"
 
@@ -2831,6 +2828,9 @@
 msgid "missing parameters for entity %s"
 msgstr "Parámetros faltantes a la entidad %s"
 
+msgid "modification"
+msgstr ""
+
 msgid "modification_date"
 msgstr "Fecha de modificación"
 
@@ -3082,9 +3082,6 @@
 msgid "permissions for this entity"
 msgstr "Permisos para esta entidad"
 
-msgid "personnal informations"
-msgstr "Información personal"
-
 msgid "pick existing bookmarks"
 msgstr "Seleccionar favoritos existentes"
 
@@ -3144,6 +3141,9 @@
 msgid "primary_email_object"
 msgstr "Dirección principal de correo electrónico de"
 
+msgid "profile"
+msgstr ""
+
 msgid "progress"
 msgstr "Progreso"
 
@@ -4136,5 +4136,11 @@
 msgid "you should probably delete that property"
 msgstr "Debería probablamente suprimir esta propriedad"
 
+#~ msgid "edition"
+#~ msgstr "Edición"
+
 #~ msgid "graphical workflow for %s"
 #~ msgstr "Gráfica del workflow por %s"
+
+#~ msgid "personnal informations"
+#~ msgstr "Información personal"
--- a/i18n/fr.po	Tue Sep 07 17:34:42 2010 +0200
+++ b/i18n/fr.po	Mon Sep 13 15:15:21 2010 +0200
@@ -4,7 +4,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2010-05-16 18:59+0200\n"
+"PO-Revision-Date: 2010-09-06 21:44+0200\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
 "Language: \n"
@@ -380,7 +380,7 @@
 
 #, python-format
 msgid "Data connection graph for %s"
-msgstr ""
+msgstr "Graphique de connection des données pour %s"
 
 msgid "Date"
 msgstr "Date"
@@ -447,7 +447,7 @@
 msgstr "Information sur le ramasse-miette"
 
 msgid "Got rhythm?"
-msgstr ""
+msgstr "T'as le rythme ?"
 
 msgid "Help"
 msgstr "Aide"
@@ -1546,7 +1546,7 @@
 msgstr "contexte où ce composant doit être affiché"
 
 msgid "context where this facet should be displayed, leave empty for both"
-msgstr ""
+msgstr "contexte où cette facette doit être affichée. Laissez ce champ vide pour l'avoir dans les deux."
 
 msgid "control subject entity's relations order"
 msgstr "contrôle l'ordre des relations de l'entité sujet"
@@ -2096,9 +2096,6 @@
 msgid "editable-table"
 msgstr "table éditable"
 
-msgid "edition"
-msgstr "édition"
-
 msgid "eid"
 msgstr "eid"
 
@@ -2827,6 +2824,9 @@
 msgid "missing parameters for entity %s"
 msgstr "paramètres manquants pour l'entité %s"
 
+msgid "modification"
+msgstr "modification"
+
 msgid "modification_date"
 msgstr "date de modification"
 
@@ -3080,9 +3080,6 @@
 msgid "permissions for this entity"
 msgstr "permissions pour cette entité"
 
-msgid "personnal informations"
-msgstr "informations personnelles"
-
 msgid "pick existing bookmarks"
 msgstr "récupérer des signets existants"
 
@@ -3142,6 +3139,9 @@
 msgid "primary_email_object"
 msgstr "adresse principale de"
 
+msgid "profile"
+msgstr "profil"
+
 msgid "progress"
 msgstr "avancement"
 
@@ -4134,5 +4134,14 @@
 msgid "you should probably delete that property"
 msgstr "vous devriez probablement supprimer cette propriété"
 
+#~ msgid "edition"
+#~ msgstr "édition"
+
 #~ msgid "graphical workflow for %s"
 #~ msgstr "graphique du workflow pour %s"
+
+#~ msgid "personnal informations"
+#~ msgstr "informations personnelles"
+
+#~ msgid "yams type, rdf type or mime type of the object"
+#~ msgstr "type yams, vocabulaire rdf ou type mime de l'objet"
--- a/migration.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/migration.py	Mon Sep 13 15:15:21 2010 +0200
@@ -122,6 +122,7 @@
                           'config': self.config,
                           'interactive_mode': interactive,
                           }
+        self._context_stack = []
 
     def __getattribute__(self, name):
         try:
@@ -199,7 +200,8 @@
         if not ask_confirm or self.confirm(msg):
             return meth(*args, **kwargs)
 
-    def confirm(self, question, shell=True, abort=True, retry=False, default='y'):
+    def confirm(self, question, shell=True, abort=True, retry=False, pdb=False,
+                default='y'):
         """ask for confirmation and return true on positive answer
 
         if `retry` is true the r[etry] answer may return 2
@@ -207,6 +209,8 @@
         possibleanswers = ['y', 'n']
         if abort:
             possibleanswers.append('abort')
+        if pdb:
+            possibleanswers.append('pdb')
         if shell:
             possibleanswers.append('shell')
         if retry:
@@ -221,9 +225,13 @@
             return 2
         if answer == 'abort':
             raise SystemExit(1)
-        if shell and answer == 'shell':
+        if answer == 'shell':
             self.interactive_shell()
-            return self.confirm(question)
+            return self.confirm(question, shell, abort, retry, pdb, default)
+        if answer == 'pdb':
+            import pdb
+            pdb.set_trace()
+            return self.confirm(question, shell, abort, retry, pdb, default)
         return True
 
     def interactive_shell(self):
@@ -277,6 +285,11 @@
                     context[attr[4:]] = getattr(self, attr)
         return context
 
+    def update_context(self, key, value):
+        for context in self._context_stack:
+            context[key] = value
+        self.__context[key] = value
+
     def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
         """execute a migration script in interactive mode
 
@@ -320,6 +333,7 @@
         if not self.execscript_confirm(migrscript):
             return
         scriptlocals = self._create_context().copy()
+        self._context_stack.append(scriptlocals)
         if script_mode == 'python':
             if funcname is None:
                 pyname = '__main__'
@@ -345,6 +359,7 @@
             import doctest
             doctest.testfile(migrscript, module_relative=False,
                              optionflags=doctest.ELLIPSIS, globs=scriptlocals)
+        del self._context_stack[-1]
 
     def cmd_option_renamed(self, oldname, newname):
         """a configuration option has been renamed"""
--- a/misc/migration/bootstrapmigration_repository.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/misc/migration/bootstrapmigration_repository.py	Mon Sep 13 15:15:21 2010 +0200
@@ -35,6 +35,9 @@
     ss.execschemarql(rql, rdef, ss.rdef2rql(rdef, CSTRMAP, groupmap=None))
     commit(ask_confirm=False)
 
+if applcubicwebversion < (3, 9, 6) and cubicwebversion >= (3, 9, 6):
+    add_entity_type('CWUniqueTogetherConstraint')
+
 if applcubicwebversion == (3, 6, 0) and cubicwebversion >= (3, 6, 0):
     CSTRMAP = dict(rql('Any T, X WHERE X is CWConstraintType, X name T',
                        ask_confirm=False))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/detect_cycle.py	Mon Sep 13 15:15:21 2010 +0200
@@ -0,0 +1,15 @@
+
+try:
+    rtype, = __args__
+except ValueError:
+    print 'USAGE: cubicweb-ctl shell <instance> detect_cycle.py -- <relation type>'
+    print
+
+graph = {}
+for fromeid, toeid in rql('Any X,Y WHERE X %s Y' % rtype):
+    graph.setdefault(fromeid, []).append(toeid)
+
+from logilab.common.graph import get_cycles
+
+for cycle in get_cycles(graph):
+    print 'cycle', '->'.join(str(n) for n in cycle)
--- a/rset.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/rset.py	Mon Sep 13 15:15:21 2010 +0200
@@ -569,7 +569,8 @@
                     if i == col:
                         continue
                     coletype = self.description[row][i]
-                    # None description possible on column resulting from an outer join
+                    # None description possible on column resulting from an
+                    # outer join
                     if coletype is None or eschema(coletype).final:
                         continue
                     try:
@@ -588,11 +589,20 @@
 
     @cached
     def related_entity(self, row, col):
-        """try to get the related entity to extract format information if any"""
+        """given an cell of the result set, try to return a (entity, relation
+        name) tuple to which this cell is linked.
+
+        This is especially useful when the cell is an attribute of an entity,
+        to get the entity to which this attribute belongs to.
+        """
         rqlst = self.syntax_tree()
+        # UNION query, we've first to find a 'pivot' column to use to get the
+        # actual query from which the row is coming
         etype, locate_query_col = self._locate_query_params(rqlst, row, col)
-        # UNION query, find the subquery from which this entity has been found
+        # now find the query from which this entity has been found. Returned
+        # select node may be a subquery with different column indexes.
         select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
+        # then get the index of root query's col in the subquery
         col = rqlst.subquery_selection_index(select, col)
         if col is None:
             # XXX unexpected, should fix subquery_selection_index ?
--- a/schema.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/schema.py	Mon Sep 13 15:15:21 2010 +0200
@@ -571,13 +571,7 @@
         rdef.name = rdef.name.lower()
         rdef.subject = bw_normalize_etype(rdef.subject)
         rdef.object = bw_normalize_etype(rdef.object)
-        try:
-            rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
-        except BadSchemaDefinition:
-            reversed_etype_map = dict( (v, k) for k, v in ETYPE_NAME_MAP.iteritems() )
-            if rdef.subject in reversed_etype_map or rdef.object in reversed_etype_map:
-                return
-            raise
+        rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
         if rdefs:
             try:
                 self._eid_index[rdef.eid] = rdefs
--- a/schemas/bootstrap.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/schemas/bootstrap.py	Mon Sep 13 15:15:21 2010 +0200
@@ -154,6 +154,17 @@
     value = String(description=_('depends on the constraint type'))
 
 
+class CWUniqueTogetherConstraint(EntityType):
+    """defines a sql-level multicolumn unique index"""
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+    constraint_of = SubjectRelation('CWEType', cardinality='1*', composite='object',
+                                    inlined=True)
+    relations = SubjectRelation(('CWAttribute', 'CWRelation'), cardinality='+*',
+                                 constraints=[RQLConstraint(
+           'O from_entity X, S constraint_of X, O relation_type T, '
+           'T final TRUE OR (T final FALSE AND T inlined TRUE)')])
+
+
 class CWConstraintType(EntityType):
     """define a schema constraint type"""
     __permissions__ = PUB_SYSTEM_ENTITY_PERMS
--- a/server/checkintegrity.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/checkintegrity.py	Mon Sep 13 15:15:21 2010 +0200
@@ -240,7 +240,12 @@
                                 table, column, column, eid)
                             session.system_sql(sql)
             continue
-        cursor = session.system_sql('SELECT eid_from FROM %s_relation;' % rschema)
+        try:
+            cursor = session.system_sql('SELECT eid_from FROM %s_relation;' % rschema)
+        except Exception, ex:
+            # usually because table doesn't exist
+            print 'ERROR', ex
+            continue
         for row in cursor.fetchall():
             eid = row[0]
             if not has_eid(session, cursor, eid, eids):
--- a/server/migractions.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/migractions.py	Mon Sep 13 15:15:21 2010 +0200
@@ -50,12 +50,15 @@
 from yams.schema2sql import eschema2sql, rschema2sql
 
 from cubicweb import AuthenticationError, ExecutionError
+from cubicweb.selectors import is_instance
 from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES,
                              PURE_VIRTUAL_RTYPES,
                              CubicWebRelationSchema, order_eschemas)
+from cubicweb.cwvreg import CW_EVENT_MANAGER
 from cubicweb.dbapi import get_repository, repo_connect
 from cubicweb.migration import MigrationHelper, yes
 from cubicweb.server.session import hooks_control
+from cubicweb.server import hook
 try:
     from cubicweb.server import SOURCE_TYPES, schemaserial as ss
     from cubicweb.server.utils import manager_userpasswd, ask_source_config
@@ -63,6 +66,20 @@
 except ImportError: # LAX
     pass
 
+class ClearGroupMap(hook.Hook):
+    __regid__ = 'cw.migration.clear_group_mapping'
+    __select__ = hook.Hook.__select__ & is_instance('CWGroup')
+    events = ('after_add_entity', 'after_update_entity',)
+    def __call__(self):
+        clear_cache(self.mih, 'group_mapping')
+        self.mih._synchronized.clear()
+
+    @classmethod
+    def mih_register(cls, repo):
+        # may be already registered in tests (e.g. unittest_migractions at
+        # least)
+        if not cls.__regid__ in repo.vreg['after_add_entity_hooks']:
+            repo.vreg.register(ClearGroupMap)
 
 class ServerMigrationHelper(MigrationHelper):
     """specific migration helper for server side  migration scripts,
@@ -83,8 +100,17 @@
             self.repo_connect()
         # no config on shell to a remote instance
         if config is not None and (cnx or connect):
+            repo = self.repo
             self.session.data['rebuild-infered'] = False
-            self.repo.hm.call_hooks('server_maintenance', repo=self.repo)
+            # register a hook to clear our group_mapping cache and the
+            # self._synchronized set when some group is added or updated
+            ClearGroupMap.mih = self
+            ClearGroupMap.mih_register(repo)
+            CW_EVENT_MANAGER.bind('after-registry-reload',
+                                  ClearGroupMap.mih_register, repo)
+            # notify we're starting maintenance (called instead of server_start
+            # which is called on regular start
+            repo.hm.call_hooks('server_maintenance', repo=repo)
         if not schema and not getattr(config, 'quick_start', False):
             schema = config.load_schema(expand_cubes=True)
         self.fs_schema = schema
@@ -272,7 +298,7 @@
         if self.session:
             self.session.set_pool()
 
-    def rqlexecall(self, rqliter, ask_confirm=True):
+    def rqlexecall(self, rqliter, ask_confirm=False):
         for rql, kwargs in rqliter:
             self.rqlexec(rql, kwargs, ask_confirm=ask_confirm)
 
@@ -451,6 +477,7 @@
         * description
         * internationalizable, fulltextindexed, indexed, meta
         * relations from/to this entity
+        * __unique_together__
         * permissions if `syncperms`
         """
         etype = str(etype)
@@ -498,6 +525,44 @@
                             continue
                         self._synchronize_rdef_schema(subj, rschema, obj,
                                                       syncprops=syncprops, syncperms=syncperms)
+        if syncprops: # need to process __unique_together__ after rdefs were processed
+            repo_unique_together = set([frozenset(ut)
+                                        for ut in repoeschema._unique_together])
+            unique_together = set([frozenset(ut)
+                                   for ut in eschema._unique_together])
+            for ut in repo_unique_together - unique_together:
+                restrictions  = ', '.join(['C relations R%(i)d, '
+                                           'R%(i)d relation_type T%(i)d, '
+                                           'R%(i)d from_entity X, '
+                                           'T%(i)d name %%(T%(i)d)s' % {'i': i,
+                                                                        'col':col}
+                                           for (i, col) in enumerate(ut)])
+                substs = {'etype': etype}
+                for i, col in enumerate(ut):
+                    substs['T%d'%i] = col
+                self.rqlexec('DELETE CWUniqueTogetherConstraint C '
+                             'WHERE C constraint_of E, '
+                             '      E name %%(etype)s,'
+                             '      %s' % restrictions,
+                             substs)
+            for ut in unique_together - repo_unique_together:
+                relations = ', '.join(['C relations R%d' % i
+                                       for (i, col) in enumerate(ut)])
+                restrictions  = ', '.join(['R%(i)d relation_type T%(i)d, '
+                                           'R%(i)d from_entity E, '
+                                           'T%(i)d name %%(T%(i)d)s' % {'i': i,
+                                                                        'col':col}
+                                           for (i, col) in enumerate(ut)])
+                substs = {'etype': etype}
+                for i, col in enumerate(ut):
+                    substs['T%d'%i] = col
+                self.rqlexec('INSERT CWUniqueTogetherConstraint C:'
+                             '       C constraint_of E, '
+                             '       %s '
+                             'WHERE '
+                             '      E name %%(etype)s,'
+                             '      %s' % (relations, restrictions),
+                             substs)
 
     def _synchronize_rdef_schema(self, subjtype, rtype, objtype,
                                  syncperms=True, syncprops=True):
@@ -592,7 +657,8 @@
         newcubes_schema = self.config.load_schema(construction_mode='non-strict')
         # XXX we have to replace fs_schema, used in cmd_add_relation_type
         # etc. and fsschema of migration script contexts
-        self.fs_schema = self._create_context()['fsschema'] = newcubes_schema
+        self.fs_schema = newcubes_schema
+        self.update_context('fsschema', self.fs_schema)
         new = set()
         # execute pre-create files
         driver = self.repo.system_source.dbdriver
@@ -710,13 +776,8 @@
         targeted type is known
         """
         instschema = self.repo.schema
-        assert not etype in instschema
-        #     # XXX (syt) plz explain: if we're adding an entity type, it should
-        #     # not be there...
-        #     eschema = instschema[etype]
-        #     if eschema.final:
-        #         instschema.del_entity_type(etype)
-        # else:
+        assert not etype in instschema, \
+               '%s already defined in the instance schema' % etype
         eschema = self.fs_schema.eschema(etype)
         confirm = self.verbosity >= 2
         groupmap = self.group_mapping()
@@ -885,21 +946,49 @@
                 self.sqlexec('UPDATE %s_relation SET eid_to=%s WHERE eid_to=%s'
                              % (rtype, new.eid, oldeid), ask_confirm=False)
             # delete relations using SQL to avoid relations content removal
-            # triggered by schema synchronization hooks. Should add deleted eids
-            # into pending eids else we may get some validation error on commit
-            # since integrity hooks may think some required relation is
-            # missing...
-            pending = self.session.transaction_data.setdefault('pendingeids', set())
+            # triggered by schema synchronization hooks.
+            session = self.session
             for rdeftype in ('CWRelation', 'CWAttribute'):
+                thispending = set()
                 for eid, in self.sqlexec('SELECT cw_eid FROM cw_%s '
                                          'WHERE cw_from_entity=%%(eid)s OR '
                                          ' cw_to_entity=%%(eid)s' % rdeftype,
                                          {'eid': oldeid}, ask_confirm=False):
-                    pending.add(eid)
+                    # we should add deleted eids into pending eids else we may
+                    # get some validation error on commit since integrity hooks
+                    # may think some required relation is missing... This also ensure
+                    # repository caches are properly cleanup
+                    hook.set_operation(session, 'pendingeids', eid,
+                                       hook.CleanupDeletedEidsCacheOp)
+                    # and don't forget to remove record from system tables
+                    self.repo.system_source.delete_info(
+                        session, session.entity_from_eid(eid, rdeftype),
+                        'system', None)
+                    thispending.add(eid)
                 self.sqlexec('DELETE FROM cw_%s '
                              'WHERE cw_from_entity=%%(eid)s OR '
                              'cw_to_entity=%%(eid)s' % rdeftype,
                              {'eid': oldeid}, ask_confirm=False)
+                # now we have to manually cleanup relations pointing to deleted
+                # entities
+                thiseids = ','.join(str(eid) for eid in thispending)
+                for rschema, ttypes, role in schema[rdeftype].relation_definitions():
+                    if rschema.type in VIRTUAL_RTYPES:
+                        continue
+                    sqls = []
+                    if role == 'object':
+                        if rschema.inlined:
+                            for eschema in ttypes:
+                                sqls.append('DELETE FROM cw_%s WHERE cw_%s IN(%%s)'
+                                            % (eschema, rschema))
+                        else:
+                            sqls.append('DELETE FROM %s_relation WHERE eid_to IN(%%s)'
+                                        % rschema)
+                    elif not rschema.inlined:
+                        sqls.append('DELETE FROM %s_relation WHERE eid_from IN(%%s)'
+                                    % rschema)
+                    for sql in sqls:
+                        self.sqlexec(sql % thiseids, ask_confirm=False)
             # remove the old type: use rql to propagate deletion
             self.rqlexec('DELETE CWEType ET WHERE ET name %(on)s', {'on': oldname},
                          ask_confirm=False)
@@ -1299,7 +1388,7 @@
                 cu = self.session.system_sql(sql, args)
             except:
                 ex = sys.exc_info()[1]
-                if self.confirm('Error: %s\nabort?' % ex):
+                if self.confirm('Error: %s\nabort?' % ex, pdb=True):
                     raise
                 return
             try:
@@ -1309,7 +1398,7 @@
                 return
 
     def rqlexec(self, rql, kwargs=None, cachekey=None, build_descr=True,
-                ask_confirm=True):
+                ask_confirm=False):
         """rql action"""
         if cachekey is not None:
             warn('[3.8] cachekey is deprecated, you can safely remove this argument',
@@ -1327,7 +1416,7 @@
                 try:
                     res = execute(rql, kwargs, build_descr=build_descr)
                 except Exception, ex:
-                    if self.confirm('Error: %s\nabort?' % ex):
+                    if self.confirm('Error: %s\nabort?' % ex, pdb=True):
                         raise
         return res
 
--- a/server/repository.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/repository.py	Mon Sep 13 15:15:21 2010 +0200
@@ -50,14 +50,13 @@
                       UnknownEid, AuthenticationError, ExecutionError,
                       ETypeNotSupportedBySources, MultiSourcesError,
                       BadConnectionId, Unauthorized, ValidationError,
-                      RepositoryError, typed_eid, onevent)
+                      RepositoryError, UniqueTogetherError, typed_eid, onevent)
 from cubicweb import cwvreg, schema, server
 from cubicweb.server import utils, hook, pool, querier, sources
 from cubicweb.server.session import Session, InternalSession, InternalManager, \
      security_enabled
 from cubicweb.server.ssplanner import EditedEntity
 
-
 def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
     """delete existing relation when adding a new one if card is 1 or ?
 
@@ -82,14 +81,14 @@
     # not expected for this).  So: don't do it, we pretend to ensure repository
     # consistency.
     #
-    # XXX we don't want read permissions to be applied but we want delete
-    # permission to be checked
-    rschema = session.repo.schema.rschema(rtype)
-    if card[0] in '1?':
-        if not rschema.inlined: # inlined relations will be implicitly deleted
-            with security_enabled(session, read=False):
-                session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
-                                'NOT Y eid %%(y)s' % rtype,
+    # notes:
+    # * inlined relations will be implicitly deleted for the subject entity
+    # * we don't want read permissions to be applied but we want delete
+    #   permission to be checked
+    if card[0] in '1?' and not session.repo.schema.rschema(rtype).inlined:
+        with security_enabled(session, read=False):
+            session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
+                            'NOT Y eid %%(y)s' % rtype,
                                 {'x': eidfrom, 'y': eidto})
     if card[1] in '1?':
         with security_enabled(session, read=False):
@@ -1070,10 +1069,16 @@
         edited.set_defaults()
         if session.is_hook_category_activated('integrity'):
             edited.check(creation=True)
-        source.add_entity(session, entity)
+        try:
+            source.add_entity(session, entity)
+        except UniqueTogetherError, exc:
+            etype, rtypes = exc.args
+            problems = {}
+            for col in rtypes:
+                problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes))
+            raise ValidationError(entity.eid, problems)
+        self.add_info(session, entity, source, extid, complete=False)
         edited.saved = True
-        self.add_info(session, entity, source, extid, complete=False)
-        entity._cw_is_saved = True # entity has an eid and is saved
         # prefill entity relation caches
         for rschema in eschema.subject_relations():
             rtype = str(rschema)
@@ -1089,9 +1094,10 @@
             if rtype in schema.VIRTUAL_RTYPES:
                 continue
             entity.cw_set_relation_cache(rtype, 'object', session.empty_rset())
-        # set inline relation cache before call to after_add_entity
+        # set inlined relation cache before call to after_add_entity
         for attr, value in relations:
             session.update_rel_cache_add(entity.eid, attr, value)
+            del_existing_rel_if_needed(session, entity.eid, attr, value)
         # trigger after_add_entity after after_add_relation
         if source.should_call_hooks:
             self.hm.call_hooks('after_add_entity', session, entity=entity)
@@ -1142,15 +1148,22 @@
                     relations.append((attr, edited[attr], previous_value))
             if source.should_call_hooks:
                 # call hooks for inlined relations
-                for attr, value, _ in relations:
+                for attr, value, _t in relations:
                     hm.call_hooks('before_add_relation', session,
                                   eidfrom=entity.eid, rtype=attr, eidto=value)
                 if not only_inline_rels:
                     hm.call_hooks('before_update_entity', session, entity=entity)
             if session.is_hook_category_activated('integrity'):
                 edited.check()
-            source.update_entity(session, entity)
-            edited.saved = True
+            try:
+                source.update_entity(session, entity)
+                edited.saved = True
+            except UniqueTogetherError, exc:
+                etype, rtypes = exc.args
+                problems = {}
+                for col in rtypes:
+                    problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes))
+                raise ValidationError(entity.eid, problems)
             self.system_source.update_info(session, entity, need_fti_update)
             if source.should_call_hooks:
                 if not only_inline_rels:
--- a/server/schemaserial.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/schemaserial.py	Mon Sep 13 15:15:21 2010 +0200
@@ -24,7 +24,7 @@
 
 from logilab.common.shellutils import ProgressBar
 
-from yams import schema as schemamod, buildobjs as ybo
+from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo
 
 from cubicweb import CW_SOFTWARE_ROOT, typed_eid
 from cubicweb.schema import (CONSTRAINTS, ETYPE_NAME_MAP,
@@ -87,7 +87,7 @@
     """
     repo = session.repo
     dbhelper = repo.system_source.dbhelper
-    # 3.6 migration
+    # XXX bw compat (3.6 migration)
     sqlcu = session.pool['system']
     sqlcu.execute("SELECT * FROM cw_CWRType WHERE cw_name='symetric'")
     if sqlcu.fetchall():
@@ -95,8 +95,10 @@
                                       dbhelper.TYPE_MAPPING['Boolean'], True)
         sqlcu.execute(sql)
         sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'")
-    sidx = {}
-    permsdict = deserialize_ertype_permissions(session)
+        session.commit(False)
+    ertidx = {}
+    copiedeids = set()
+    permsidx = deserialize_ertype_permissions(session)
     schema.reading_from_database = True
     for eid, etype, desc in session.execute(
         'Any X, N, D WHERE X is CWEType, X name N, X description D',
@@ -106,7 +108,7 @@
             # just set the eid
             eschema = schema.eschema(etype)
             eschema.eid = eid
-            sidx[eid] = eschema
+            ertidx[eid] = etype
             continue
         if etype in ETYPE_NAME_MAP:
             needcopy = False
@@ -115,7 +117,8 @@
             sqlexec = session.system_sql
             if sqlexec('SELECT 1 FROM %(p)sCWEType WHERE %(p)sname=%%(n)s'
                        % {'p': sqlutils.SQL_PREFIX}, {'n': netype}).fetchone():
-                # the new type already exists, we should merge
+                # the new type already exists, we should copy (eg make existing
+                # instances of the old type instances of the new type)
                 assert etype.lower() != netype.lower()
                 needcopy = True
             else:
@@ -139,16 +142,16 @@
             repo.clear_caches(tocleanup)
             session.commit(False)
             if needcopy:
-                from logilab.common.testlib import mock_object
-                sidx[eid] = mock_object(type=netype)
+                ertidx[eid] = netype
+                copiedeids.add(eid)
                 # copy / CWEType entity removal expected to be done through
                 # rename_entity_type in a migration script
                 continue
             etype = netype
-        etype = ybo.EntityType(name=etype, description=desc, eid=eid)
-        eschema = schema.add_entity_type(etype)
-        sidx[eid] = eschema
-        set_perms(eschema, permsdict)
+        ertidx[eid] = etype
+        eschema = schema.add_entity_type(
+            ybo.EntityType(name=etype, description=desc, eid=eid))
+        set_perms(eschema, permsidx)
     for etype, stype in session.execute(
         'Any XN, ETN WHERE X is CWEType, X name XN, X specializes ET, ET name ETN',
         build_descr=False):
@@ -159,43 +162,81 @@
     for eid, rtype, desc, sym, il, ftc in session.execute(
         'Any X,N,D,S,I,FTC WHERE X is CWRType, X name N, X description D, '
         'X symmetric S, X inlined I, X fulltext_container FTC', build_descr=False):
-        rtype = ybo.RelationType(name=rtype, description=desc,
-                                 symmetric=bool(sym), inlined=bool(il),
-                                 fulltext_container=ftc, eid=eid)
-        rschema = schema.add_relation_type(rtype)
-        sidx[eid] = rschema
-    cstrsdict = deserialize_rdef_constraints(session)
+        ertidx[eid] = rtype
+        rschema = schema.add_relation_type(
+            ybo.RelationType(name=rtype, description=desc,
+                             symmetric=bool(sym), inlined=bool(il),
+                             fulltext_container=ftc, eid=eid))
+    cstrsidx = deserialize_rdef_constraints(session)
+    pendingrdefs = []
+    # closure to factorize common code of attribute/relation rdef addition
+    def _add_rdef(rdefeid, seid, reid, oeid, **kwargs):
+        rdef = ybo.RelationDefinition(ertidx[seid], ertidx[reid], ertidx[oeid],
+                                      constraints=cstrsidx.get(rdefeid, ()),
+                                      eid=rdefeid, **kwargs)
+        if seid in copiedeids or oeid in copiedeids:
+            # delay addition of this rdef. We'll insert them later if needed. We
+            # have to do this because:
+            #
+            # * on etype renaming, we want relation of the old entity type being
+            #   redirected to the new type during migration
+            #
+            # * in the case of a copy, we've to take care that rdef already
+            #   existing in the schema are not overwritten by a redirected one,
+            #   since we want correct eid on them (redirected rdef will be
+            #   removed in rename_entity_type)
+            pendingrdefs.append(rdef)
+        else:
+            # add_relation_def return a RelationDefinitionSchema if it has been
+            # actually added (can be None on duplicated relation definitions,
+            # e.g. if the relation type is marked as beeing symmetric)
+            rdefs = schema.add_relation_def(rdef)
+            if rdefs is not None:
+                ertidx[rdefeid] = rdefs
+                set_perms(rdefs, permsidx)
+
     for values in session.execute(
         'Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT WHERE X is CWAttribute,'
         'X relation_type RT, X cardinality CARD, X ordernum ORD, X indexed IDX,'
         'X description DESC, X internationalizable I18N, X defaultval DFLT,'
         'X fulltextindexed FTIDX, X from_entity SE, X to_entity OE',
         build_descr=False):
-        rdefeid, seid, reid, teid, card, ord, desc, idx, ftidx, i18n, default = values
-        rdef = ybo.RelationDefinition(sidx[seid].type, sidx[reid].type, sidx[teid].type,
-                                      cardinality=card,
-                                      constraints=cstrsdict.get(rdefeid, ()),
-                                      order=ord, description=desc,
-                                      indexed=idx, fulltextindexed=ftidx,
-                                      internationalizable=i18n,
-                                      default=default, eid=rdefeid)
-        rdefs = schema.add_relation_def(rdef)
-        # rdefs can be None on duplicated relation definitions (e.g. symmetrics)
-        if rdefs is not None:
-            set_perms(rdefs, permsdict)
+        rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values
+        _add_rdef(rdefeid, seid, reid, oeid,
+                  cardinality=card, description=desc, order=ord,
+                  indexed=idx, fulltextindexed=ftidx, internationalizable=i18n,
+                  default=default)
     for values in session.execute(
         'Any X,SE,RT,OE,CARD,ORD,DESC,C WHERE X is CWRelation, X relation_type RT,'
         'X cardinality CARD, X ordernum ORD, X description DESC, '
         'X from_entity SE, X to_entity OE, X composite C', build_descr=False):
-        rdefeid, seid, reid, teid, card, ord, desc, c = values
-        rdef = ybo.RelationDefinition(sidx[seid].type, sidx[reid].type, sidx[teid].type,
-                                      constraints=cstrsdict.get(rdefeid, ()),
-                                      cardinality=card, order=ord, description=desc,
-                                      composite=c,  eid=rdefeid)
-        rdefs = schema.add_relation_def(rdef)
-        # rdefs can be None on duplicated relation definitions (e.g. symmetrics)
+        rdefeid, seid, reid, oeid, card, ord, desc, comp = values
+        _add_rdef(rdefeid, seid, reid, oeid,
+                  cardinality=card, description=desc, order=ord,
+                  composite=comp)
+    for rdef in pendingrdefs:
+        try:
+            rdefs = schema.add_relation_def(rdef)
+        except BadSchemaDefinition:
+            continue
         if rdefs is not None:
-            set_perms(rdefs, permsdict)
+            set_perms(rdefs, permsidx)
+    unique_togethers = {}
+    try:
+        rset = session.execute(
+        'Any X,E,R WHERE '
+        'X is CWUniqueTogetherConstraint, '
+        'X constraint_of E, X relations R', build_descr=False)
+    except Exception:
+        session.rollback() # first migration introducing CWUniqueTogetherConstraint cw 3.9.6
+    else:
+        for values in rset:
+            uniquecstreid, eeid, releid = values
+            eschema = schema.schema_by_eid(eeid)
+            relations = unique_togethers.setdefault(uniquecstreid, (eschema, []))
+            relations[1].append(ertidx[releid].rtype.type)
+        for eschema, unique_together in unique_togethers.itervalues():
+            eschema._unique_together.append(tuple(sorted(unique_together)))
     schema.infer_specialization_rules()
     session.commit()
     schema.reading_from_database = False
@@ -232,7 +273,7 @@
         res.setdefault(rdefeid, []).append(cstr)
     return res
 
-def set_perms(erschema, permsdict):
+def set_perms(erschema, permsidx):
     """set permissions on the given erschema according to the permission
     definition dictionary as built by deserialize_ertype_permissions for a
     given erschema's eid
@@ -240,7 +281,7 @@
     # reset erschema permissions here to avoid getting yams default anyway
     erschema.permissions = dict((action, ()) for action in erschema.ACTIONS)
     try:
-        thispermsdict = permsdict[erschema.eid]
+        thispermsdict = permsidx[erschema.eid]
     except KeyError:
         return
     for action, somethings in thispermsdict.iteritems():
@@ -309,6 +350,10 @@
                           rdef2rql(rdef, cstrtypemap, groupmap))
         if pb is not None:
             pb.update()
+    # serialize unique_together constraints
+    for eschema in eschemas:
+        for unique_together in eschema._unique_together:
+            execschemarql(execute, eschema, [uniquetogether2rql(eschema, unique_together)])
     for rql, kwargs in specialize2rql(schema):
         execute(rql, kwargs, build_descr=False)
         if pb is not None:
@@ -366,6 +411,31 @@
         values = {'x': eschema.eid, 'et': specialized_type.eid}
         yield 'SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', values
 
+def uniquetogether2rql(eschema, unique_together):
+    relations = []
+    restrictions = []
+    substs = {}
+    for i, name in enumerate(unique_together):
+        rschema = eschema.rdef(name)
+        var = 'R%d' % i
+        rtype = 'T%d' % i
+        substs[rtype] = rschema.rtype.type
+        relations.append('C relations %s' % var)
+        restrictions.append('%(var)s from_entity X, '
+                            '%(var)s relation_type %(rtype)s, '
+                            '%(rtype)s name %%(%(rtype)s)s' \
+                            % {'var': var,
+                               'rtype':rtype})
+    relations = ', '.join(relations)
+    restrictions = ', '.join(restrictions)
+    rql = ('INSERT CWUniqueTogetherConstraint C: '
+           '    C constraint_of X, %s  '
+           'WHERE '
+           '    X eid %%(x)s, %s' )
+
+    return rql % (relations, restrictions), substs
+
+
 def _ervalues(erschema):
     try:
         type_ = unicode(erschema.type)
--- a/server/serverctl.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/serverctl.py	Mon Sep 13 15:15:21 2010 +0200
@@ -814,6 +814,7 @@
         )
 
     def run(self, args):
+        from cubicweb.server.checkintegrity import check
         appid = args[0]
         config = ServerConfiguration.config_for(appid)
         config.repairing = self.config.force
--- a/server/sources/native.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/sources/native.py	Mon Sep 13 15:15:21 2010 +0200
@@ -34,6 +34,7 @@
 from base64 import b64decode, b64encode
 from contextlib import contextmanager
 from os.path import abspath
+import re
 
 from logilab.common.compat import any
 from logilab.common.cache import Cache
@@ -44,7 +45,7 @@
 
 from yams import schema2sql as y2sql
 
-from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary
+from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary, UniqueTogetherError
 from cubicweb import transaction as tx, server, neg_role
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
@@ -211,7 +212,7 @@
           'default': 'postgres',
           # XXX use choice type
           'help': 'database driver (postgres, mysql, sqlite, sqlserver2005)',
-          'group': 'native-source', 'level': 1,
+          'group': 'native-source', 'level': 0,
           }),
         ('db-host',
          {'type' : 'string',
@@ -666,6 +667,16 @@
                         self.critical('transaction has been rollbacked')
                 except:
                     pass
+            if ex.__class__.__name__ == 'IntegrityError':
+                # need string comparison because of various backends
+                for arg in ex.args:
+                    mo = re.search('unique_cw_[^ ]+_idx', arg)
+                    if mo is not None:
+                        index_name = mo.group(0)
+                        elements = index_name.rstrip('_idx').split('_cw_')[1:]
+                        etype = elements[0]
+                        rtypes = elements[1:]                        
+                        raise UniqueTogetherError(etype, rtypes)
             raise
         return cursor
 
--- a/server/test/data/migratedapp/schema.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/data/migratedapp/schema.py	Mon Sep 13 15:15:21 2010 +0200
@@ -101,6 +101,7 @@
 
 
 class Personne(EntityType):
+    __unique_together__ = [('nom', 'prenom', 'datenaiss')]
     nom    = String(fulltextindexed=True, required=True, maxsize=64)
     prenom = String(fulltextindexed=True, maxsize=64)
     civility   = String(maxsize=1, default='M', fulltextindexed=True)
@@ -126,7 +127,6 @@
         'delete': ('managers', 'owners'),
         'add': ('managers', 'users',)
         }
-
     nom  = String(maxsize=64, fulltextindexed=True)
     web  = String(maxsize=128)
     tel  = Int()
--- a/server/test/data/schema.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/data/schema.py	Mon Sep 13 15:15:21 2010 +0200
@@ -98,6 +98,7 @@
     todo_by = SubjectRelation('CWUser')
 
 class Personne(EntityType):
+    __unique_together__ = [('nom', 'prenom', 'inline2')]
     nom    = String(fulltextindexed=True, required=True, maxsize=64)
     prenom = String(fulltextindexed=True, maxsize=64)
     sexe   = String(maxsize=1, default='M', fulltextindexed=True)
--- a/server/test/unittest_fti.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/unittest_fti.py	Mon Sep 13 15:15:21 2010 +0200
@@ -1,5 +1,7 @@
 from __future__ import with_statement
 
+import socket
+
 from cubicweb.devtools import ApptestConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.selectors import is_instance
@@ -8,6 +10,11 @@
 class PostgresFTITC(CubicWebTC):
     config = ApptestConfiguration('data', sourcefile='sources_fti')
 
+    def setUp(self):
+        if not socket.gethostname().endswith('.logilab.fr'):
+            self.skip('XXX require logilab configuration')
+        super(PostgresFTITC, self).setUp()
+
     def test_occurence_count(self):
         req = self.request()
         c1 = req.create_entity('Card', title=u'c1',
--- a/server/test/unittest_migractions.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/unittest_migractions.py	Mon Sep 13 15:15:21 2010 +0200
@@ -309,6 +309,7 @@
         migrschema['titre'].rdefs[('Personne', 'String')].description = 'title for this person'
         delete_concerne_rqlexpr = self._rrqlexpr_rset('delete', 'concerne')
         add_concerne_rqlexpr = self._rrqlexpr_rset('add', 'concerne')
+        
         self.mh.cmd_sync_schema_props_perms(commit=False)
 
         self.assertEquals(cursor.execute('Any D WHERE X name "Personne", X description D')[0][0],
@@ -380,8 +381,15 @@
         # finally
         self.assertEquals(cursor.execute('Any COUNT(X) WHERE X is RQLExpression')[0][0],
                           nbrqlexpr_start + 1 + 2 + 2)
-
-        self.mh.rollback()
+        self.mh.commit()
+        # unique_together test
+        self.assertEqual(len(self.schema.eschema('Personne')._unique_together), 1)
+        self.assertUnorderedIterableEquals(self.schema.eschema('Personne')._unique_together[0],
+                                           ('nom', 'prenom', 'datenaiss'))
+        rset = cursor.execute('Any C WHERE C is CWUniqueTogetherConstraint')
+        self.assertEquals(len(rset), 1)
+        relations = [r.rtype.name for r in rset.get_entity(0,0).relations]
+        self.assertUnorderedIterableEquals(relations, ('nom', 'prenom', 'datenaiss'))
 
     def _erqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action
--- a/server/test/unittest_msplanner.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/unittest_msplanner.py	Mon Sep 13 15:15:21 2010 +0200
@@ -60,6 +60,7 @@
                      {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, {'X': 'CWEType'},
                      {'X': 'CWGroup'}, {'X': 'CWPermission'}, {'X': 'CWProperty'},
                      {'X': 'CWRType'}, {'X': 'CWRelation'}, {'X': 'CWUser'},
+                     {'X': 'CWUniqueTogetherConstraint'},
                      {'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'},
                      {'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'},
                      {'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'},
@@ -893,13 +894,14 @@
                                           [{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])],
                            [self.cards, self.system], {}, {'X': 'table0.C0'}, []),
                           ('FetchStep',
-                           [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+                           [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
                              [{'X': 'BaseTransition'}, {'X': 'Bookmark'},
                               {'X': 'CWAttribute'}, {'X': 'CWCache'},
                               {'X': 'CWConstraint'}, {'X': 'CWConstraintType'},
                               {'X': 'CWEType'}, {'X': 'CWGroup'},
                               {'X': 'CWPermission'}, {'X': 'CWProperty'},
                               {'X': 'CWRType'}, {'X': 'CWRelation'},
+                              {'X': 'CWUniqueTogetherConstraint'},
                               {'X': 'Comment'}, {'X': 'Division'},
                               {'X': 'Email'}, {'X': 'EmailAddress'},
                               {'X': 'EmailPart'}, {'X': 'EmailThread'},
@@ -954,14 +956,16 @@
                        [self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []),
                       # extra UnionFetchStep could be avoided but has no cost, so don't care
                       ('UnionFetchStep',
-                       [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+                       [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
                                         [{'X': 'BaseTransition', 'ET': 'CWEType'},
                                          {'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'},
                                          {'X': 'CWCache', 'ET': 'CWEType'}, {'X': 'CWConstraint', 'ET': 'CWEType'},
                                          {'X': 'CWConstraintType', 'ET': 'CWEType'}, {'X': 'CWEType', 'ET': 'CWEType'},
                                          {'X': 'CWGroup', 'ET': 'CWEType'}, {'X': 'CWPermission', 'ET': 'CWEType'},
                                          {'X': 'CWProperty', 'ET': 'CWEType'}, {'X': 'CWRType', 'ET': 'CWEType'},
-                                         {'X': 'CWRelation', 'ET': 'CWEType'}, {'X': 'Comment', 'ET': 'CWEType'},
+                                         {'X': 'CWRelation', 'ET': 'CWEType'},
+                                         {'X': 'CWUniqueTogetherConstraint', 'ET': 'CWEType'},
+                                         {'X': 'Comment', 'ET': 'CWEType'},
                                          {'X': 'Division', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'},
                                          {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'},
                                          {'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'},
@@ -2253,7 +2257,7 @@
                      None, {'X': 'table0.C0'}, []),
                     ('UnionStep', None, None,
                      [('OneFetchStep',
-                       [(u'Any X WHERE X owned_by U, U login "anon", U is CWUser, X is IN(Affaire, BaseTransition, Basket, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+                       [(u'Any X WHERE X owned_by U, U login "anon", U is CWUser, X is IN(Affaire, BaseTransition, Basket, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, CWUser, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
                          [{'U': 'CWUser', 'X': 'Affaire'},
                           {'U': 'CWUser', 'X': 'BaseTransition'},
                           {'U': 'CWUser', 'X': 'Basket'},
@@ -2268,6 +2272,7 @@
                           {'U': 'CWUser', 'X': 'CWProperty'},
                           {'U': 'CWUser', 'X': 'CWRType'},
                           {'U': 'CWUser', 'X': 'CWRelation'},
+                          {'U': 'CWUser', 'X': 'CWUniqueTogetherConstraint'},
                           {'U': 'CWUser', 'X': 'CWUser'},
                           {'U': 'CWUser', 'X': 'Division'},
                           {'U': 'CWUser', 'X': 'Email'},
--- a/server/test/unittest_multisources.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/unittest_multisources.py	Mon Sep 13 15:15:21 2010 +0200
@@ -151,7 +151,7 @@
         self.assertEquals(len(rset), 5, zip(rset.rows, rset.description))
         rset = cu.execute('Any X ORDERBY FTIRANK(X) WHERE X has_text "card"')
         self.assertEquals(len(rset), 5, zip(rset.rows, rset.description))
-        Connection_close(cnx)
+        Connection_close(cnx.cnx) # cnx is a TestCaseConnectionProxy
 
     def test_synchronization(self):
         cu = cnx2.cursor()
--- a/server/test/unittest_querier.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/unittest_querier.py	Mon Sep 13 15:15:21 2010 +0200
@@ -16,7 +16,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unit tests for modules cubicweb.server.querier and cubicweb.server.querier_steps
+"""unit tests for modules cubicweb.server.querier and cubicweb.server.ssplanner
 """
 from datetime import date, datetime
 
@@ -130,7 +130,7 @@
                                        'X': 'Affaire',
                                        'ET': 'CWEType', 'ETN': 'String'}])
         rql, solutions = partrqls[1]
-        self.assertEquals(rql,  'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
+        self.assertEquals(rql,  'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
         self.assertListEquals(sorted(solutions),
                               sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'},
@@ -143,15 +143,16 @@
                                       {'X': 'CWEType', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'CWAttribute', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'CWGroup', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWRelation', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWPermission', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWProperty', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWRType', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWUniqueTogetherConstraint', 'ETN': 'String', 'ET': 'CWEType'},
+                                      {'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Email', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailAddress', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailPart', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'EmailThread', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWRelation', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWPermission', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWProperty', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWRType', 'ETN': 'String', 'ET': 'CWEType'},
-                                      {'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'ExternalUri', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'File', 'ETN': 'String', 'ET': 'CWEType'},
                                       {'X': 'Folder', 'ETN': 'String', 'ET': 'CWEType'},
@@ -493,14 +494,14 @@
                               [[u'description_format', 12],
                                [u'description', 13],
                                [u'name', 14],
-                               [u'created_by', 37],
-                               [u'creation_date', 37],
-                               [u'cwuri', 37],
-                               [u'in_basket', 37],
-                               [u'is', 37],
-                               [u'is_instance_of', 37],
-                               [u'modification_date', 37],
-                               [u'owned_by', 37]])
+                               [u'created_by', 38],
+                               [u'creation_date', 38],
+                               [u'cwuri', 38],
+                               [u'in_basket', 38],
+                               [u'is', 38],
+                               [u'is_instance_of', 38],
+                               [u'modification_date', 38],
+                               [u'owned_by', 38]])
 
     def test_select_aggregat_having_dumb(self):
         # dumb but should not raise an error
@@ -691,19 +692,15 @@
         self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_select_ordered_distinct_1(self):
-        self.execute("INSERT Affaire X: X sujet 'cool', X ref '1'")
-        self.execute("INSERT Affaire X: X sujet 'cool', X ref '2'")
-        rset = self.execute('DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R')
-        self.assertEqual(rset.rows, [['cool']])
+        self.assertRaises(BadRQLQuery,
+                          self.execute, 'DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R')
 
     def test_select_ordered_distinct_2(self):
         self.execute("INSERT Affaire X: X sujet 'minor'")
-        self.execute("INSERT Affaire X: X sujet 'important'")
-        self.execute("INSERT Affaire X: X sujet 'normal'")
         self.execute("INSERT Affaire X: X sujet 'zou'")
         self.execute("INSERT Affaire X: X sujet 'abcd'")
         rset = self.execute('DISTINCT Any S ORDERBY S WHERE A is Affaire, A sujet S')
-        self.assertEqual(rset.rows, [['abcd'], ['important'], ['minor'], ['normal'], ['zou']])
+        self.assertEqual(rset.rows, [['abcd'], ['minor'], ['zou']])
 
     def test_select_ordered_distinct_3(self):
         rset = self.execute('DISTINCT Any N ORDERBY GROUP_SORT_VALUE(N) WHERE X is CWGroup, X name N')
@@ -1085,14 +1082,14 @@
         self.commit()
         # fill the cache
         self.execute("Any X WHERE X eid %(x)s", {'x': eid})
-        self.execute("Any X WHERE X eid %s" %eid)
+        self.execute("Any X WHERE X eid %s" % eid)
         self.execute("Folder X WHERE X eid %(x)s", {'x': eid})
-        self.execute("Folder X WHERE X eid %s" %eid)
-        self.execute("DELETE Folder T WHERE T eid %s"%eid)
+        self.execute("Folder X WHERE X eid %s" % eid)
+        self.execute("DELETE Folder T WHERE T eid %s" % eid)
         self.commit()
         rset = self.execute("Any X WHERE X eid %(x)s", {'x': eid})
         self.assertEquals(rset.rows, [])
-        rset = self.execute("Any X WHERE X eid %s" %eid)
+        rset = self.execute("Any X WHERE X eid %s" % eid)
         self.assertEquals(rset.rows, [])
         rset = self.execute("Folder X WHERE X eid %(x)s", {'x': eid})
         self.assertEquals(rset.rows, [])
--- a/server/test/unittest_repository.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/unittest_repository.py	Mon Sep 13 15:15:21 2010 +0200
@@ -74,9 +74,27 @@
                                               (u'Int',),
                                               (u'Interval',), (u'Password',),
                                               (u'String',), (u'Time',)])
+            sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to "
+                   "FROM cw_CWUniqueTogetherConstraint as cstr, "
+                   "     relations_relation as rel, "
+                   "     cw_CWEType as etype "
+                   "WHERE cstr.cw_eid = rel.eid_from "
+                   "  AND cstr.cw_constraint_of = etype.cw_eid "
+                   "  AND etype.cw_name = 'Personne' "
+                   ";")
+            cu = self.session.system_sql(sql)
+            rows = cu.fetchall()
+            self.assertEquals(len(rows), 3)
+            self.test_unique_together()
         finally:
             self.repo.set_schema(origshema)
 
+    def test_unique_together(self):
+        person = self.repo.schema.eschema('Personne')
+        self.assertEquals(len(person._unique_together), 1)
+        self.assertUnorderedIterableEquals(person._unique_together[0],
+                                           ('nom', 'prenom', 'inline2'))
+
     def test_schema_has_owner(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
@@ -385,6 +403,13 @@
         self.assertEquals(len(rset), 1)
         self.assertEquals(rset.rows[0][0], p2.eid)
 
+    def test_delete_if_object_inlined_singlecard(self):
+        req = self.request()
+        c = req.create_entity('Card', title=u'Carte')
+        req.create_entity('Personne', nom=u'Vincent', fiche=c)
+        req.create_entity('Personne', nom=u'Florent', fiche=c)
+        self.commit()
+        self.assertEquals(len(c.reverse_fiche), 1)
 
     def test_set_attributes_in_before_update(self):
         # local hook
--- a/server/test/unittest_rql2sql.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/server/test/unittest_rql2sql.py	Mon Sep 13 15:15:21 2010 +0200
@@ -429,11 +429,6 @@
 GROUP BY _X.cw_data_name,_X.cw_data_format
 ORDER BY 1,2,_X.cw_data_format'''),
 
-    ('DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R',
-     '''SELECT T1.C0 FROM (SELECT DISTINCT _A.cw_sujet AS C0, _A.cw_ref AS C1
-FROM cw_Affaire AS _A
-ORDER BY 2) AS T1'''),
-
     ('DISTINCT Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 2, DF WHERE X data_name N, X data D, X data_format DF;',
      '''SELECT T1.C0,T1.C1 FROM (SELECT DISTINCT (MAX(_X.cw_eid) + MIN(LENGTH(_X.cw_data))) AS C0, _X.cw_data_name AS C1, _X.cw_data_format AS C2
 FROM cw_File AS _X
--- a/test/unittest_cwctl.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/test/unittest_cwctl.py	Mon Sep 13 15:15:21 2010 +0200
@@ -39,7 +39,7 @@
 
     def test_list(self):
         from cubicweb.cwctl import ListCommand
-        ListCommand().run([])
+        ListCommand(None).run([])
 
 
 class CubicWebShellTC(CubicWebTC):
--- a/test/unittest_rset.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/test/unittest_rset.py	Mon Sep 13 15:15:21 2010 +0200
@@ -368,6 +368,14 @@
                             'WITH B,T BEING (Any B,T WHERE B is Bookmark, B title T)')
         rset.related_entity(0, 2)
 
+    def test_related_entity_subquery_outerjoin(self):
+        rset = self.execute('Any X,S,L WHERE X in_state S '
+                            'WITH X, L BEING (Any X,MAX(L) GROUPBY X '
+                            'WHERE X is CWUser, T? wf_info_for X, T creation_date L)')
+        self.assertEquals(len(rset), 2)
+        rset.related_entity(0, 1)
+        rset.related_entity(0, 2)
+
     def test_entities(self):
         rset = self.execute('Any U,G WHERE U in_group G')
         # make sure we have at least one element
--- a/utils.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/utils.py	Mon Sep 13 15:15:21 2010 +0200
@@ -370,6 +370,7 @@
 
 else:
     from logilab.common.date import ustrftime
+
     class CubicWebJsonEncoder(json.JSONEncoder):
         """define a json encoder to be able to encode yams std types"""
 
@@ -410,7 +411,7 @@
 _THIS_MOD_NS = globals()
 for funcname in ('date_range', 'todate', 'todatetime', 'datetime2ticks',
                  'days_in_month', 'days_in_year', 'previous_month',
-                 'next_month', 'first_day', 'last_day', 'ustrftime',
+                 'next_month', 'first_day', 'last_day',
                  'strptime'):
     msg = '[3.6] %s has been moved to logilab.common.date' % funcname
     _THIS_MOD_NS[funcname] = deprecated(msg)(getattr(date, funcname))
--- a/web/data/cubicweb.ajax.box.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.ajax.box.js	Mon Sep 13 15:15:21 2010 +0200
@@ -11,71 +11,71 @@
     var holderid = cw.utils.domid(boxid) + eid + 'Holder';
     var value = $('#' + holderid + 'Input').val();
     if (separator) {
-	value = $.map(value.split(separator), jQuery.trim);
+        value = $.map(value.split(separator), jQuery.trim);
     }
     var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value));
     d.addCallback(function() {
-	    $('#' + holderid).empty();
-	    var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
-	    $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
-	    if (msg) {
-		document.location.hash = '#header';
-		updateMessage(msg);
-	    }
-	});
+            $('#' + holderid).empty();
+            var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            if (msg) {
+                document.location.hash = '#header';
+                updateMessage(msg);
+            }
+        });
 }
 
 function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) {
     var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid));
     d.addCallback(function() {
-	    var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
-	    $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
-	    if (msg) {
-		document.location.hash = '#header';
-		updateMessage(msg);
-	    }
+            var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            if (msg) {
+                document.location.hash = '#header';
+                updateMessage(msg);
+            }
     });
 }
 
 function ajaxBoxShowSelector(boxid, eid,
-			     unrelfname,
-			     addfname, msg,
-			     oklabel, cancellabel,
-			     separator) {
+                             unrelfname,
+                             addfname, msg,
+                             oklabel, cancellabel,
+                             separator) {
     var holderid = cw.utils.domid(boxid) + eid + 'Holder';
     var holder = $('#' + holderid);
     if (holder.children().length) {
-	holder.empty();
+        holder.empty();
     }
     else {
-	var inputid = holderid + 'Input';
-	var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
-	deferred.addCallback(function (unrelated) {
-	    var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
-	    holder.append(input).show();
-	    $input = $(input);
-	    $input.keypress(function (event) {
-		if (event.keyCode == KEYS.KEY_ENTER) {
-		    // XXX not very user friendly: we should test that the suggestions
-		    //     aren't visible anymore
-		    ajaxBoxValidateSelectorInput(boxid, eid, separator, addfname, msg);
-		}
-	    });
-	    var buttons = DIV({'class' : "sgformbuttons"},
-			      A({'href' : "javascript: noop();",
-				 'onclick' : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput',
-								  boxid, eid, separator, addfname, msg)},
-				  oklabel),
-			      ' / ',
-			      A({'href' : "javascript: noop();",
-				 'onclick' : '$("#' + holderid + '").empty()'},
-				  cancellabel));
-	    holder.append(buttons);
-	    $input.autocomplete(unrelated, {
-		multiple: separator,
-		max: 15
-	    });
-	    $input.focus();
-	});
+        var inputid = holderid + 'Input';
+        var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
+        deferred.addCallback(function (unrelated) {
+            var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
+            holder.append(input).show();
+            $input = $(input);
+            $input.keypress(function (event) {
+                if (event.keyCode == KEYS.KEY_ENTER) {
+                    // XXX not very user friendly: we should test that the suggestions
+                    //     aren't visible anymore
+                    ajaxBoxValidateSelectorInput(boxid, eid, separator, addfname, msg);
+                }
+            });
+            var buttons = DIV({'class' : "sgformbuttons"},
+                              A({'href' : "javascript: noop();",
+                                 'onclick' : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput',
+                                                                  boxid, eid, separator, addfname, msg)},
+                                  oklabel),
+                              ' / ',
+                              A({'href' : "javascript: noop();",
+                                 'onclick' : '$("#' + holderid + '").empty()'},
+                                  cancellabel));
+            holder.append(buttons);
+            $input.autocomplete(unrelated, {
+                multiple: separator,
+                max: 15
+            });
+            $input.focus();
+        });
     }
 }
--- a/web/data/cubicweb.ajax.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.ajax.js	Mon Sep 13 15:15:21 2010 +0200
@@ -94,28 +94,28 @@
 function _loadAjaxHtmlHead($node, $head, tag, srcattr) {
     var jqtagfilter = tag + '[' + srcattr + ']';
     if (cw['loaded_'+srcattr] === undefined) {
-	cw['loaded_'+srcattr] = [];
-	var loaded = cw['loaded_'+srcattr];
-	jQuery('head ' + jqtagfilter).each(function(i) {
-		loaded.push(this.getAttribute(srcattr));
-	    });
+        cw['loaded_'+srcattr] = [];
+        var loaded = cw['loaded_'+srcattr];
+        jQuery('head ' + jqtagfilter).each(function(i) {
+                   loaded.push(this.getAttribute(srcattr));
+               });
     } else {
-	var loaded = cw['loaded_'+srcattr];
+        var loaded = cw['loaded_'+srcattr];
     }
     $node.find(tag).each(function(i) {
-	 var url = this.getAttribute(srcattr);
+        var url = this.getAttribute(srcattr);
         if (url) {
             if (jQuery.inArray(url, loaded) == -1) {
-		// take care to <script> tags: jQuery append method script nodes
-		// don't appears in the DOM (See comments on
-		// http://api.jquery.com/append/), which cause undesired
-		// duplicated load in our case. After trying to use bare DOM api
-		// to avoid this, we switched to handle a list of already loaded
-		// stuff ourselves, since bare DOM api gives bug with the
-		// server-response event, since we loose control on when the
-		// script is loaded (jQuery load it immediatly).
-		loaded.push(url);
-		jQuery(this).appendTo($head);
+                // take care to <script> tags: jQuery append method script nodes
+                // don't appears in the DOM (See comments on
+                // http://api.jquery.com/append/), which cause undesired
+                // duplicated load in our case. After trying to use bare DOM api
+                // to avoid this, we switched to handle a list of already loaded
+                // stuff ourselves, since bare DOM api gives bug with the
+                // server-response event, since we loose control on when the
+                // script is loaded (jQuery load it immediatly).
+                loaded.push(url);
+                jQuery(this).appendTo($head);
             }
         } else {
             jQuery(this).appendTo($head);
@@ -441,7 +441,7 @@
     var d = userCallback(cbname);
     d.addCallback(function() {
         $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {'rql': rql},
-						       registry, compid));
+                                                       registry, compid));
         if (msg) {
             updateMessage(msg);
         }
@@ -560,9 +560,9 @@
     }
     // several children => wrap them in a single node and return the wrap
     return DIV({'cubicweb:type': "cwResponseWrapper"},
-	       $.map(children, function(node) {
-		       return jQuery(node).clone().context;})
-	       );
+               $.map(children, function(node) {
+                       return jQuery(node).clone().context;})
+               );
 }
 
 /* DEPRECATED *****************************************************************/
--- a/web/data/cubicweb.css	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.css	Mon Sep 13 15:15:21 2010 +0200
@@ -45,8 +45,8 @@
   color: %(h1Color)s;
 }
 
-h1.plain,
-.vtitle {
+div.tabbedprimary + h1,
+h1.plain {
  border-bottom: none;
 }
 
--- a/web/data/cubicweb.edition.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.edition.js	Mon Sep 13 15:15:21 2010 +0200
@@ -274,7 +274,7 @@
 
 function selectForAssociation(tripletIdsString, originalEid) {
     var tripletlist = $.map(tripletIdsString.split('-'),
-			    function(x) { return [x.split(':')] ;});
+                            function(x) { return [x.split(':')] ;});
     var d = loadRemote('json', ajaxFuncArgs('add_pending_inserts', null, tripletlist));
     d.addCallback(function() {
         var args = {
--- a/web/data/cubicweb.fckcwconfig-full.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.fckcwconfig-full.js	Mon Sep 13 15:15:21 2010 +0200
@@ -1,28 +1,28 @@
 // cf /usr/share/fckeditor/fckconfig.js
 
-FCKConfig.AutoDetectLanguage	= false ;
+FCKConfig.AutoDetectLanguage = false ;
 
 FCKConfig.ToolbarSets["Default"] = [
     // removed : 'Save','NewPage','DocProps','-','Templates','-','Preview'
-	['Source'],
+        ['Source'],
     // removed: 'Print','-','SpellCheck'
-	['Cut','Copy','Paste','PasteText','PasteWord'],
-	['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
+        ['Cut','Copy','Paste','PasteText','PasteWord'],
+        ['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
     //['Form','Checkbox','Radio','TextField','Textarea','Select','Button','ImageButton','HiddenField'],
-	'/',
+        '/',
     // ,'StrikeThrough','-','Subscript','Superscript'
-	['Bold','Italic','Underline'],
+        ['Bold','Italic','Underline'],
     // ,'-','Outdent','Indent','Blockquote'
-	['OrderedList','UnorderedList'],
+        ['OrderedList','UnorderedList'],
     // ['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],
-	['Link','Unlink','Anchor'],
+        ['Link','Unlink','Anchor'],
     // removed : 'Image','Flash','Smiley','PageBreak'
-	['Table','Rule','SpecialChar']
+        ['Table','Rule','SpecialChar']
     // , '/',
     // ['Style','FontFormat','FontName','FontSize'],
     // ['TextColor','BGColor'],
     //,'ShowBlocks'
-    // ['FitWindow','-','About']		// No comma for the last row.
+    // ['FitWindow','-','About']                // No comma for the last row.
 ] ;
 
 // 'Flash','Select','Textarea','Checkbox','Radio','TextField','HiddenField','ImageButton','Button','Form',
--- a/web/data/cubicweb.goa.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.goa.js	Mon Sep 13 15:15:21 2010 +0200
@@ -12,6 +12,5 @@
  * overrides rql_for_eid function from htmlhelpers.hs
  */
 function rql_for_eid(eid) {
-	return 'Any X WHERE X eid "' + eid + '"';
+        return 'Any X WHERE X eid "' + eid + '"';
 }
-
--- a/web/data/cubicweb.image.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.image.js	Mon Sep 13 15:15:21 2010 +0200
@@ -1,4 +1,3 @@
-
 jQuery.fn.autoResize = function() {
     // remove enforced with / height (by CSS and/or HTML attributes)
     this.css("width", "auto").css("height", "auto");
@@ -10,23 +9,23 @@
     // we don't mind if content in [content]footer moved out of the screen
     var maxVSize = $(window).height() - ($(document).height() - imgVSize) + $('#footer').height() + $('#contentfooter').height();
     if (maxHSize > 0 && maxVSize > 0) {
-	// if image don't fit screen, set width or height so that
-	// browser keep img ratio, ensuring the other dimension will
-	// also fit the screen
-	if (imgHSize > maxHSize && ((imgVSize / imgHSize) * maxHSize) <= maxVSize) {
-	    this.css("width", maxHSize);
-	} else if (imgVSize > maxVSize && ((imgHSize / imgVSize) * maxVSize) <= maxHSize) {
-	    this.css("height", maxVSize);
-	}
-	else {
-	    // image already fit in screen, don't scale it up
-	}
+        // if image don't fit screen, set width or height so that
+        // browser keep img ratio, ensuring the other dimension will
+        // also fit the screen
+        if (imgHSize > maxHSize && ((imgVSize / imgHSize) * maxHSize) <= maxVSize) {
+            this.css("width", maxHSize);
+        } else if (imgVSize > maxVSize && ((imgHSize / imgVSize) * maxVSize) <= maxHSize) {
+            this.css("height", maxVSize);
+        }
+        else {
+            // image already fit in screen, don't scale it up
+        }
     } else {
-	// can't fit image in, don't do anything
+        // can't fit image in, don't do anything
     }
 };
 
 
 $(document).ready(function() {
-	$("img.contentimage").load(function() {$(this).autoResize()});
-    });
+        $("img.contentimage").load(function() {$(this).autoResize()});
+});
--- a/web/data/cubicweb.reledit.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/data/cubicweb.reledit.js	Mon Sep 13 15:15:21 2010 +0200
@@ -65,9 +65,9 @@
     loadInlineEditionForm: function(formid, eid, rtype, role, divid, reload, vid, default_value) {
         var args = {fname: 'reledit_form', rtype: rtype, role: role,
                     pageid: pageid,
-    	            eid: eid, divid: divid, formid: formid,
-    		    reload: reload, vid: vid, default_value: default_value,
-    		    callback: function () {cw.reledit.showInlineEditionForm(divid);}};
+                    eid: eid, divid: divid, formid: formid,
+                    reload: reload, vid: vid, default_value: default_value,
+                    callback: function () {cw.reledit.showInlineEditionForm(divid);}};
        jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
     }
 });
--- a/web/formfields.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/formfields.py	Mon Sep 13 15:15:21 2010 +0200
@@ -416,11 +416,16 @@
         if self.sort:
             vocab = vocab_sort(vocab)
         # XXX pre 3.9 bw compat
-        for i, (label, value) in enumerate(vocab):
+        for i, option in enumerate(vocab):
+            # option may be a 2 or 3-uple (see Select widget _render method for
+            # explanation)
+            value = option[1]
             if value is not None and not isinstance(value, basestring):
                 warn('[3.9] %s: vocabulary value should be an unicode string'
                      % self, DeprecationWarning)
-                vocab[i] = (label, unicode(value))
+                option = list(option)
+                option[1] = unicode(value)
+                vocab[i] = option
         return vocab
 
     def format(self, form):
--- a/web/test/jstests/test_ajax.js	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/test/jstests/test_ajax.js	Mon Sep 13 15:15:21 2010 +0200
@@ -3,7 +3,12 @@
     module("ajax", {
         setup: function() {
           this.scriptsLength = $('head script[src]').length-1;
-	  this.cssLength = $('head link[rel=stylesheet]').length-1;
+          this.cssLength = $('head link[rel=stylesheet]').length-1;
+          // re-initialize cw loaded cache so that each tests run in a
+          // clean environment, have a lookt at _loadAjaxHtmlHead implementation
+          // in cubicweb.ajax.js for more information.
+          cw.loaded_src = [];
+          cw.loaded_href = [];
         },
         teardown: function() {
           $('head script[src]:gt(' + this.scriptsLength + ')').remove();
@@ -118,7 +123,7 @@
         'Hello', 'world');
     });
 
-    test('test addErrback', function() {
+  test('test addErrback', function() {
         expect(1);
         stop();
         var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
@@ -160,10 +165,11 @@
         });
     });
 
-    test('test already included resources are ignored (ajax_url2.html)', function() {
+    test('test already included resources are ignored (ajax_url1.html)', function() {
         expect(10);
         var scriptsIncluded = jsSources();
-        equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        // NOTE:
+        equals(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
         equals(jQuery('head link').length, 1);
         /* use endswith because in pytest context we have an absolute path */
         ok(jQuery('head link').attr('href').endswith('/qunit.css'));
@@ -172,7 +178,7 @@
             callback: function() {
                 var origLength = scriptsIncluded.length;
                 scriptsIncluded = jsSources();
-		try {
+                try {
                     // check that foo.js has been inserted in <head>
                     equals(scriptsIncluded.length, origLength + 1);
                     equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
@@ -186,7 +192,7 @@
                     ok(jQuery('head link').attr('href').endswith('/qunit.css'));
                 } finally {
                     start();
-		}
+                }
             }
         });
     });
--- a/web/test/unittest_web.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/test/unittest_web.py	Mon Sep 13 15:15:21 2010 +0200
@@ -26,9 +26,9 @@
         arurl = req.ajax_replace_url
         # NOTE: for the simplest use cases, we could use doctest
         self.assertEquals(arurl('foo', rql='Person P', vid='list'),
-                          """javascript: $('#foo').loadxhtml("http://testing.fr/cubicweb/json?rql=Person%20P&fname=view&vid=list", null, 'get', 'replace'); noop()""")
+                          """javascript: $('#foo').loadxhtml("http://testing.fr/cubicweb/json?rql=Person%20P&fname=view&vid=list",null,"get","replace"); noop()""")
         self.assertEquals(arurl('foo', rql='Person P', vid='oneline', name='bar', age=12),
-                          """javascript: $('#foo').loadxhtml("http://testing.fr/cubicweb/json?name=bar&age=12&rql=Person%20P&fname=view&vid=oneline", null, 'get', 'replace'); noop()""")
+                          """javascript: $('#foo').loadxhtml("http://testing.fr/cubicweb/json?name=bar&age=12&rql=Person%20P&fname=view&vid=oneline",null,"get","replace"); noop()""")
 
 
 if __name__ == '__main__':
--- a/web/views/actions.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/views/actions.py	Mon Sep 13 15:15:21 2010 +0200
@@ -356,7 +356,7 @@
     __regid__ = 'myinfos'
     __select__ = authenticated_user()
 
-    title = _('personnal informations')
+    title = _('profile')
     category = 'useractions'
     order = 20
 
--- a/web/views/basecontrollers.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/views/basecontrollers.py	Mon Sep 13 15:15:21 2010 +0200
@@ -291,7 +291,8 @@
         try:
             args = [json.loads(arg) for arg in args]
         except ValueError, exc:
-            self.exception('error while decoding json arguments for js_%s: %s', fname, args, exc)
+            self.exception('error while decoding json arguments for js_%s: %s',
+                           fname, args, exc)
             raise RemoteCallFailed(repr(exc))
         try:
             result = func(*args)
@@ -433,7 +434,7 @@
     def js_render(self, registry, oid, eid=None, selectargs=None, renderargs=None):
         if eid is not None:
             rset = self._cw.eid_rset(eid)
-        elif 'rql' in self._cw.form:
+        elif self._cw.form.get('rql'):
             rset = self._cw.execute(self._cw.form['rql'])
         else:
             rset = None
--- a/web/views/basetemplates.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/views/basetemplates.py	Mon Sep 13 15:15:21 2010 +0200
@@ -136,11 +136,11 @@
         nav_html = UStringIO()
         if view:
             view.paginate(w=nav_html.write)
-        w(_(nav_html.getvalue()))
+        w(nav_html.getvalue())
         w(u'<div id="contentmain">\n')
         view.render(w=w)
         w(u'</div>\n') # close id=contentmain
-        w(_(nav_html.getvalue()))
+        w(nav_html.getvalue())
         w(u'</div>\n') # closes id=pageContent
         self.template_footer(view)
 
--- a/web/views/editforms.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/views/editforms.py	Mon Sep 13 15:15:21 2010 +0200
@@ -100,7 +100,7 @@
     # though not baseforms based customized view
     __select__ = one_line_rset() & non_final_entity() & yes()
 
-    title = _('edition')
+    title = _('modification')
 
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.complete_entity(row, col)
--- a/web/views/tabs.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/views/tabs.py	Mon Sep 13 15:15:21 2010 +0200
@@ -209,6 +209,7 @@
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
         self.render_entity_toolbox(entity)
+        self.w(u'<div class="tabbedprimary"></div>')
         self.render_entity_title(entity)
         self.render_tabs(self.tabs, self.default_tab, entity)
 
--- a/web/views/urlpublishing.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/views/urlpublishing.py	Mon Sep 13 15:15:21 2010 +0200
@@ -120,6 +120,7 @@
         self.urlpublisher = urlpublisher
         self.vreg = urlpublisher.vreg
 
+
 class RawPathEvaluator(URLPathEvaluator):
     """handle path of the form::
 
--- a/web/views/urlrewrite.py	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/views/urlrewrite.py	Mon Sep 13 15:15:21 2010 +0200
@@ -24,7 +24,7 @@
 
 
 def rgx(pattern, flags=0):
-    """this is just a convenient shortcout to add the $ sign"""
+    """this is just a convenient shortcut to add the $ sign"""
     return re.compile(pattern+'$', flags)
 
 class metarewriter(type):
--- a/web/wdoc/userprefs_en.rst	Tue Sep 07 17:34:42 2010 +0200
+++ b/web/wdoc/userprefs_en.rst	Mon Sep 13 15:15:21 2010 +0200
@@ -1,7 +1,7 @@
-User's personnal information are modifiable using user's edit form. You can
-access it through the dropdown-menu under the link on the top-right of the
-window, labeled by your login. In this menu, click the "personal information"
-link to go to this form.
+The personal information describing a User can be modified using the edit form
+of the user. You can access it through the dropdown-menu under the link on the
+top-right of the window, labeled by your login. In this menu, click the
+"profile" link to go to this form.
 
 Each user can as well customize the site appearance using the "user's
 preferences" link in this menu. This will show you a form to configure which