backport stable into default
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 16 Sep 2010 18:56:35 +0200
changeset 6279 42079f752a9c
parent 6229 c4a70a5dd144 (current diff)
parent 6278 7bbdc3b6e9ef (diff)
child 6292 054fa36060d5
backport stable into default
dbapi.py
entity.py
hooks/syncschema.py
i18n/en.po
i18n/es.po
i18n/fr.po
server/hook.py
server/repository.py
server/sources/native.py
server/test/data/schema.py
server/test/unittest_repository.py
web/application.py
web/component.py
web/data/cubicweb.css
web/data/cubicweb.old.css
web/uicfg.py
web/views/basecontrollers.py
web/views/primary.py
web/views/sessions.py
--- a/.hgtags	Tue Sep 14 08:48:44 2010 +0200
+++ b/.hgtags	Thu Sep 16 18:56:35 2010 +0200
@@ -151,3 +151,5 @@
 12038ca95f0fff2205f7ee029f5602d192118aec cubicweb-debian-version-3.9.5-1
 d37428222a6325583be958d7c7fe7c595115663d cubicweb-version-3.9.6
 7d2cab567735a17cab391c1a7f1bbe39118308a2 cubicweb-debian-version-3.9.6-1
+de588e756f4fbe9c53c72159c6b96580a36d3fa6 cubicweb-version-3.9.7
+1c01f9dffd64d507863c9f8f68e3585b7aa24374 cubicweb-debian-version-3.9.7-1
--- a/__pkginfo__.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/__pkginfo__.py	Thu Sep 16 18:56:35 2010 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 9, 6)
+numversion = (3, 9, 7)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -57,7 +57,7 @@
     }
 
 __recommends__ = {
-    'Pyro': '>= 3.9.1',
+    'Pyro': '>= 3.9.1, < 4.0.0',
     'PIL': '',                  # for captcha
     'pycrypto': '',             # for crypto extensions
     'fyzz': '>= 0.1.0',         # for sparql
--- a/dbapi.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/dbapi.py	Thu Sep 16 18:56:35 2010 +0200
@@ -457,6 +457,12 @@
                                                  time() - tstart, clock() - cstart))
         return rset
 
+def check_not_closed(func):
+    def decorator(self, *args, **kwargs):
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
+        return func(self, *args, **kwargs)
+    return decorator
 
 class Connection(object):
     """DB-API 2.0 compatible Connection object for CubicWeb
@@ -497,59 +503,15 @@
             self.rollback()
             return False #propagate the exception
 
-    def _txid(self, cursor=None): # XXX could now handle various isolation level!
-        # return a dict as bw compat trick
-        return {'txid': currentThread().getName()}
-
-    def request(self):
-        return DBAPIRequest(self.vreg, DBAPISession(self))
-
-    def check(self):
-        """raise `BadConnectionId` if the connection is no more valid, else
-        return its latest activity timestamp.
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.check_session(self.sessionid)
-
-    def set_session_props(self, **props):
-        """raise `BadConnectionId` if the connection is no more valid"""
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        self._repo.set_session_props(self.sessionid, props)
-
-    def get_shared_data(self, key, default=None, pop=False, txdata=False):
-        """return value associated to key in the session's data dictionary or
-        session's transaction's data if `txdata` is true.
-
-        If pop is True, value will be removed from the dictionnary.
+    def __del__(self):
+        """close the remote connection if necessary"""
+        if self._closed is None and self._close_on_del:
+            try:
+                self.close()
+            except:
+                pass
 
-        If key isn't defined in the dictionnary, value specified by the
-        `default` argument will be returned.
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
-
-    def set_shared_data(self, key, value, txdata=False):
-        """set value associated to `key` in shared data
-
-        if `txdata` is true, the value will be added to the repository session's
-        transaction's data which are cleared on commit/rollback of the current
-        transaction.
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.set_shared_data(self.sessionid, key, value, txdata)
-
-    def get_schema(self):
-        """Return the schema currently used by the repository.
-
-        This is NOT part of the DB-API.
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.get_schema()
+    # connection initialization methods ########################################
 
     def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
         config = self.vreg.config
@@ -605,20 +567,18 @@
         if sitetitle is not None:
             self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle}
 
+    @check_not_closed
     def source_defs(self):
         """Return the definition of sources used by the repository.
 
         This is NOT part of the DB-API.
         """
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
         return self._repo.source_defs()
 
+    @check_not_closed
     def user(self, req=None, props=None):
         """return the User object associated to this connection"""
         # cnx validity is checked by the call to .user_info
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
         eid, login, groups, properties = self._repo.user_info(self.sessionid,
                                                               props)
         if req is None:
@@ -634,19 +594,103 @@
         user.cw_attr_cache['login'] = login # cache login
         return user
 
-    def __del__(self):
-        """close the remote connection if necessary"""
-        if self._closed is None and self._close_on_del:
-            try:
-                self.close()
-            except:
-                pass
+    @check_not_closed
+    def check(self):
+        """raise `BadConnectionId` if the connection is no more valid, else
+        return its latest activity timestamp.
+        """
+        self._repo.check_session(self.sessionid)
+
+    def _txid(self, cursor=None): # XXX could now handle various isolation level!
+        # return a dict as bw compat trick
+        return {'txid': currentThread().getName()}
+
+    def request(self):
+        return DBAPIRequest(self.vreg, DBAPISession(self))
+
+    # session data methods #####################################################
+
+    @check_not_closed
+    def set_session_props(self, **props):
+        """raise `BadConnectionId` if the connection is no more valid"""
+        self._repo.set_session_props(self.sessionid, props)
+
+    @check_not_closed
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
+        """return value associated to key in the session's data dictionary or
+        session's transaction's data if `txdata` is true.
+
+        If pop is True, value will be removed from the dictionnary.
 
+        If key isn't defined in the dictionnary, value specified by the
+        `default` argument will be returned.
+        """
+        return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
+
+    @check_not_closed
+    def set_shared_data(self, key, value, txdata=False):
+        """set value associated to `key` in shared data
+
+        if `txdata` is true, the value will be added to the repository
+        session's query data which are cleared on commit/rollback of the current
+        transaction.
+        """
+        return self._repo.set_shared_data(self.sessionid, key, value, txdata)
+
+    # meta-data accessors ######################################################
+
+    @check_not_closed
+    def get_schema(self):
+        """Return the schema currently used by the repository."""
+        return self._repo.get_schema()
+
+    @check_not_closed
+    def get_option_value(self, option):
+        """return the value for `option` in the repository configuration."""
+        return self._repo.get_option_value(option)
+
+    @check_not_closed
     def describe(self, eid):
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
         return self._repo.describe(self.sessionid, eid, **self._txid())
 
+    # db-api like interface ####################################################
+
+    @check_not_closed
+    def commit(self):
+        """Commit pending transaction for this connection to the repository.
+
+        may raises `Unauthorized` or `ValidationError` if we attempted to do
+        something we're not allowed to for security or integrity reason.
+
+        If the transaction is undoable, a transaction id will be returned.
+        """
+        return self._repo.commit(self.sessionid, **self._txid())
+
+    @check_not_closed
+    def rollback(self):
+        """This method is optional since not all databases provide transaction
+        support.
+
+        In case a database does provide transactions this method causes the the
+        database to roll back to the start of any pending transaction.  Closing
+        a connection without committing the changes first will cause an implicit
+        rollback to be performed.
+        """
+        self._repo.rollback(self.sessionid, **self._txid())
+
+    @check_not_closed
+    def cursor(self, req=None):
+        """Return a new Cursor Object using the connection.
+
+        On pyro connection, you should get cursor after calling if
+        load_appobjects method if desired (which you should call if you intend
+        to use ORM abilities).
+        """
+        if req is None:
+            req = self.request()
+        return self.cursor_class(self, self._repo, req=req)
+
+    @check_not_closed
     def close(self):
         """Close the connection now (rather than whenever __del__ is called).
 
@@ -656,52 +700,13 @@
         connection.  Note that closing a connection without committing the
         changes first will cause an implicit rollback to be performed.
         """
-        if self._closed:
-            raise ProgrammingError('Connection is already closed')
         self._repo.close(self.sessionid, **self._txid())
         del self._repo # necessary for proper garbage collection
         self._closed = 1
 
-    def commit(self):
-        """Commit pending transaction for this connection to the repository.
-
-        may raises `Unauthorized` or `ValidationError` if we attempted to do
-        something we're not allowed to for security or integrity reason.
-
-        If the transaction is undoable, a transaction id will be returned.
-        """
-        if not self._closed is None:
-            raise ProgrammingError('Connection is already closed')
-        return self._repo.commit(self.sessionid, **self._txid())
-
-    def rollback(self):
-        """This method is optional since not all databases provide transaction
-        support.
-
-        In case a database does provide transactions this method causes the the
-        database to roll back to the start of any pending transaction.  Closing
-        a connection without committing the changes first will cause an implicit
-        rollback to be performed.
-        """
-        if not self._closed is None:
-            raise ProgrammingError('Connection is already closed')
-        self._repo.rollback(self.sessionid, **self._txid())
-
-    def cursor(self, req=None):
-        """Return a new Cursor Object using the connection.
-
-        On pyro connection, you should get cursor after calling if
-        load_appobjects method if desired (which you should call if you intend
-        to use ORM abilities).
-        """
-        if self._closed is not None:
-            raise ProgrammingError('Can\'t get cursor on closed connection')
-        if req is None:
-            req = self.request()
-        return self.cursor_class(self, self._repo, req=req)
-
     # undo support ############################################################
 
+    @check_not_closed
     def undoable_transactions(self, ueid=None, req=None, **actionfilters):
         """Return a list of undoable transaction objects by the connection's
         user, ordered by descendant transaction time.
@@ -734,6 +739,7 @@
             txinfo.req = req
         return txinfos
 
+    @check_not_closed
     def transaction_info(self, txuuid, req=None):
         """Return transaction object for the given uid.
 
@@ -748,6 +754,7 @@
         txinfo.req = req
         return txinfo
 
+    @check_not_closed
     def transaction_actions(self, txuuid, public=True):
         """Return an ordered list of action effectued during that transaction.
 
@@ -761,6 +768,7 @@
         return self._repo.transaction_actions(self.sessionid, txuuid, public,
                                               **self._txid())
 
+    @check_not_closed
     def undo_transaction(self, txuuid):
         """Undo the given transaction. Return potential restoration errors.
 
--- a/debian/changelog	Tue Sep 14 08:48:44 2010 +0200
+++ b/debian/changelog	Thu Sep 16 18:56:35 2010 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.9.7-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 16 Sep 2010 15:41:51 +0200
+
 cubicweb (3.9.6-1) unstable; urgency=low
 
   * new upstream release
--- a/doc/book/README	Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/README	Thu Sep 16 18:56:35 2010 +0200
@@ -46,9 +46,26 @@
 .. [foot note] the foot note content
 
 
+Cross references
+================
 
-XXX
-* lien vers cw.cwconfig.CW_CUBES_PATH par ex.
+To arbitrary section
+--------------------
+
+:ref:`identifier` ou :ref:`label <identifier>`
+
+Label required of referencing node which as no title, else the node's title will be used.
 
 
-automodule, autofunction, automethod, autofunction
+To API objects
+--------------
+See the autodoc sphinx extension documentation. Quick overview:
+
+* ref to a class: :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+
+* if you can to see only the class name in the generated documentation, add a ~:
+  :class:`~cubicweb.devtools.testlib.AutomaticWebTest`
+
+* you can also use :mod: (module), :exc: (exception), :func: (function), :meth: (method)...
+
+* syntax explained above to specify label explicitly may also be used
--- a/doc/book/en/devrepo/migration.rst	Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/en/devrepo/migration.rst	Thu Sep 16 18:56:35 2010 +0200
@@ -91,6 +91,24 @@
 * `session`, repository session object
 
 
+New cube dependencies
+---------------------
+
+If your code depends on some new cubes, you have to add them in a migration
+script by using:
+
+* `add_cube(cube, update_database=True)`, add a cube.
+* `add_cubes(cubes, update_database=True)`, add a list of cubes.
+
+The `update_database` parameter is telling if the database schema
+should be updated or if only the relevant persistent property should be
+inserted (for the case where a new cube has been extracted from an
+existing one, so the new cube schema is actually already in there).
+
+If some of the added cubes are already used by an instance, they'll simply be
+silently skipped.
+
+
 Schema migration
 ----------------
 The following functions for schema migration are available in `repository`
--- a/doc/book/en/devrepo/testing.rst	Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/en/devrepo/testing.rst	Thu Sep 16 18:56:35 2010 +0200
@@ -184,15 +184,70 @@
             mail = MAILBOX[1]
             self.assertEquals(mail.subject, '[data] yes')
 
+Visible actions tests
+`````````````````````
+
+It is easy to write unit tests to test actions which are visible to
+user or to a category of users. Let's take an example in the
+`conference cube`_.
+
+.. _`conference cube`: http://www.cubicweb.org/project/cubicweb-conference
+.. sourcecode:: python
+
+    class ConferenceActionsTC(CubicWebTC):
+
+        def setup_database(self):
+            self.conf = self.create_entity('Conference',
+                                           title=u'my conf',
+                                           url_id=u'conf',
+                                           start_on=date(2010, 1, 27),
+                                           end_on = date(2010, 1, 29),
+                                           call_open=True,
+                                           reverse_is_chair_at=chair,
+                                           reverse_is_reviewer_at=reviewer)
+
+        def test_admin(self):
+            req = self.request()
+            rset = req.execute('Any C WHERE C is Conference')
+            self.assertListEquals(self.pactions(req, rset),
+                                  [('workflow', workflow.WorkflowActions),
+                                   ('edit', confactions.ModifyAction),
+                                   ('managepermission', actions.ManagePermissionsAction),
+                                   ('addrelated', actions.AddRelatedActions),
+                                   ('delete', actions.DeleteAction),
+                                   ('generate_badge_action', badges.GenerateBadgeAction),
+                                   ('addtalkinconf', confactions.AddTalkInConferenceAction)
+                                   ])
+            self.assertListEquals(self.action_submenu(req, rset, 'addrelated'),
+                                  [(u'add Track in_conf Conference object',
+                                    u'http://testing.fr/cubicweb/add/Track'
+                                    u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
+                                    u'__redirectpath=conference%%2Fconf&'
+                                    u'__redirectvid=' % {'conf': self.conf.eid}),
+                                   ])
+
+You just have to execute a rql query corresponding to the view you want to test,
+and to compare the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions
+that must be visible in the interface. This is a list of tuples. The first
+element is the action's `__regid__`, the second the action's class.
+
+To test actions in submenu, you just have to test the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.action_submenu` method. The last
+parameter of the method is the action's category. The result is a list of
+tuples. The first element is the action's title, and the second element the
+action's url.
+
+
 .. _automatic_views_tests:
 
 Automatic views testing
 -----------------------
 
-This is done automatically with the AutomaticWebTest class. At cube
-creation time, the mycube/test/test_mycube.py file contains such a
-test. The code here has to be uncommented to be usable, without
-further modification.
+This is done automatically with the :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+class. At cube creation time, the mycube/test/test_mycube.py file
+contains such a test. The code here has to be uncommented to be
+usable, without further modification.
 
 The ``auto_populate`` method uses a smart algorithm to create
 pseudo-random data in the database, thus enabling the views to be
@@ -212,6 +267,11 @@
   auto_populate cannot guess by itself; these must yield resultsets
   against which views may be selected.
 
+.. warning::
+
+  Take care to not let the imported `AutomaticWebTest` in your test module
+  namespace, else both your subclass *and* this parent class will be run.
+
 Testing on a real-life database
 -------------------------------
 
--- a/doc/book/en/devweb/views/reledit.rst	Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/en/devweb/views/reledit.rst	Thu Sep 16 18:56:35 2010 +0200
@@ -70,12 +70,23 @@
 controlled using the reledit_ctrl rtag, defined in
 :mod:`cubicweb.web.uicfg`.
 
-This rtag provides three control variables:
+This rtag provides four control variables:
 
-* ``default_value``
-* ``reload``, to specificy if edition of the relation entails a full page
-  reload, which defaults to False
-* ``noedit``, to explicitly inhibit edition
+* ``default_value``: alternative default value
+   The default value is what is shown when there is no value.
+* ``reload``: boolean, eid (to reload to) or function taking subject
+   and returning bool/eid This is useful when editing a relation (or
+   attribute) that impacts the url or another parts of the current
+   displayed page. Defaults to false.
+* ``rvid``: alternative view id (as str) for relation or composite
+   edition Default is 'incontext' or 'csv' depending on the
+   cardinality. They can also be statically changed by subclassing
+   ClickAndEditFormView and redefining _one_rvid (resp. _many_rvid).
+* ``edit_target``: 'rtype' (to edit the relation) or 'related' (to
+   edit the related entity) This controls whether to edit the relation
+   or the target entity of the relation.  Currently only one-to-one
+   relations support target entity edition. By default, the 'related'
+   option is taken whenever the relation is composite and one-to-one.
 
 Let's see how to use these controls.
 
@@ -86,15 +97,13 @@
     reledit_ctrl.tag_attribute(('Company', 'name'),
                                {'reload': lambda x:x.eid,
                                 'default_value': xml_escape(u'<logilab tastes better>')})
-    reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'noedit': True})
+    reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'edit_target': 'related'})
 
 The `default_value` needs to be an xml escaped unicode string.
 
-The `noedit` attribute is convenient to programmatically disable some
-relation edition on views that apply it systematically (the prime
-example being the primary view). Here we use it to forbid changing the
-`boss` relation from a `Person` side (as it could have unwanted
-effects).
+The `edit_target` tag on the `boss` relation being set to `related` will
+ensure edition of the `Person` entity instead (using a standard
+automatic form) of the association of Company and Person.
 
 Finally, the `reload` key accepts either a boolean, an eid or an
 unicode string representing an url. If an eid is provided, it will be
--- a/entity.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/entity.py	Thu Sep 16 18:56:35 2010 +0200
@@ -222,9 +222,10 @@
                 destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
                                             selection, orderby, restrictions,
                                             user, ordermethod, visited=visited)
-            orderterm = getattr(cls, ordermethod)(attr, var)
-            if orderterm:
-                orderby.append(orderterm)
+            if ordermethod is not None:
+                orderterm = getattr(cls, ordermethod)(attr, var)
+                if orderterm:
+                    orderby.append(orderterm)
         return selection, orderby, restrictions
 
     @classmethod
--- a/etwist/service.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/etwist/service.py	Thu Sep 16 18:56:35 2010 +0200
@@ -72,8 +72,9 @@
             # create the site
             config = cwcfg.config_for(self.instance)
             config.init_log(force=True)
+            config.debugmode = False
             logger.info('starting cubicweb instance %s ', self.instance)
-            root_resource = CubicWebRootResource(config, False)
+            root_resource = CubicWebRootResource(config)
             website = server.Site(root_resource)
             # serve it via standard HTTP on port set in the configuration
             port = config['port'] or 8080
--- a/ext/rest.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/ext/rest.py	Thu Sep 16 18:56:35 2010 +0200
@@ -242,8 +242,14 @@
         data = data.translate(ESC_CAR_TABLE)
     settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
                 'warning_stream': StringIO(),
+                'traceback': True, # don't sys.exit
+                'stylesheet': None, # don't try to embed stylesheet (may cause
+                                    # obscure bug due to docutils computing
+                                    # relative path according to the directory
+                                    # used *at import time*
                 # dunno what's the max, severe is 4, and we never want a crash
-                # (though try/except may be a better option...)
+                # (though try/except may be a better option...). May be the
+                # above traceback option will avoid this?
                 'halt_level': 10,
                 }
     if context:
--- a/hooks/syncschema.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/hooks/syncschema.py	Thu Sep 16 18:56:35 2010 +0200
@@ -34,8 +34,8 @@
 
 from cubicweb import ValidationError
 from cubicweb.selectors import is_instance
-from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS,
-                             ETYPE_NAME_MAP, display_name)
+from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
+                             CONSTRAINTS, ETYPE_NAME_MAP, display_name)
 from cubicweb.server import hook, schemaserial as ss
 from cubicweb.server.sqlutils import SQL_PREFIX
 
@@ -52,16 +52,9 @@
     }
 
 # core entity and relation types which can't be removed
-CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
-                                  'CWConstraint', 'CWAttribute', 'CWRelation']
-CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
-               'login', 'upassword', 'name',
-               'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
-               'relation_type', 'from_entity', 'to_entity',
-               'constrainted_by',
-               'read_permission', 'add_permission',
-               'delete_permission', 'updated_permission',
-               ]
+CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
+    ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group'))
+
 
 def get_constraints(session, entity):
     constraints = []
@@ -873,7 +866,7 @@
     def __call__(self):
         # final entities can't be deleted, don't care about that
         name = self.entity.name
-        if name in CORE_ETYPES:
+        if name in CORE_TYPES:
             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
         # delete every entities of this type
         if not name in ETYPE_NAME_MAP:
@@ -939,7 +932,7 @@
 
     def __call__(self):
         name = self.entity.name
-        if name in CORE_RTYPES:
+        if name in CORE_TYPES:
             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
         # delete relation definitions using this relation type
         self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
--- a/i18n/en.po	Tue Sep 14 08:48:44 2010 +0200
+++ b/i18n/en.po	Thu Sep 16 18:56:35 2010 +0200
@@ -5,7 +5,7 @@
 msgstr ""
 "Project-Id-Version: 2.0\n"
 "POT-Creation-Date: 2006-01-12 17:35+CET\n"
-"PO-Revision-Date: 2010-05-16 18:58+0200\n"
+"PO-Revision-Date: 2010-09-15 14:55+0200\n"
 "Last-Translator: Sylvain Thenault <sylvain.thenault@logilab.fr>\n"
 "Language-Team: English <devel@logilab.fr.org>\n"
 "Language: en\n"
@@ -317,6 +317,12 @@
 msgid "CWRelation_plural"
 msgstr "Relations"
 
+msgid "CWUniqueTogetherConstraint"
+msgstr ""
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr ""
+
 msgid "CWUser"
 msgstr "User"
 
@@ -496,6 +502,9 @@
 msgid "New CWRelation"
 msgstr "New relation"
 
+msgid "New CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "New CWUser"
 msgstr "New user"
 
@@ -686,6 +695,9 @@
 msgid "This CWRelation"
 msgstr "This relation"
 
+msgid "This CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "This CWUser"
 msgstr "This user"
 
@@ -880,6 +892,9 @@
 msgid "add CWRelation relation_type CWRType object"
 msgstr "relation definition"
 
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr ""
+
 msgid "add CWUser in_group CWGroup object"
 msgstr "user"
 
@@ -1436,6 +1451,20 @@
 msgid "constraint factory"
 msgstr ""
 
+msgid "constraint_of"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr ""
+
+msgid "constraint_of_object"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr ""
+
 msgid "constraints"
 msgstr ""
 
@@ -1559,6 +1588,11 @@
 msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
 msgstr "creating relation %(linkto)s"
 
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr ""
+
 msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
 msgstr "creating a new user in group %(linkto)s"
 
@@ -1800,6 +1834,9 @@
 msgid "define how we get out from a sub-workflow"
 msgstr ""
 
+msgid "defines a sql-level multicolumn unique index"
+msgstr ""
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -3116,9 +3153,27 @@
 msgid "relation_type_object"
 msgstr "relation definitions"
 
+msgid "relations"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr ""
+
 msgid "relations deleted"
 msgstr ""
 
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr ""
+
 msgid "relative url of the bookmarked page"
 msgstr ""
 
@@ -3898,6 +3953,10 @@
 msgid "view_index"
 msgstr "index"
 
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr "violates unique_together constraints (%s)"
+
 msgid "visible"
 msgstr ""
 
--- a/i18n/es.po	Tue Sep 14 08:48:44 2010 +0200
+++ b/i18n/es.po	Thu Sep 16 18:56:35 2010 +0200
@@ -326,6 +326,12 @@
 msgid "CWRelation_plural"
 msgstr "Relaciones"
 
+msgid "CWUniqueTogetherConstraint"
+msgstr ""
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr ""
+
 msgid "CWUser"
 msgstr "Usuario"
 
@@ -517,6 +523,9 @@
 msgid "New CWRelation"
 msgstr "Nueva definición de relación final"
 
+msgid "New CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "New CWUser"
 msgstr "Agregar usuario"
 
@@ -707,6 +716,9 @@
 msgid "This CWRelation"
 msgstr "Esta definición de relación no final"
 
+msgid "This CWUniqueTogetherConstraint"
+msgstr ""
+
 msgid "This CWUser"
 msgstr "Este usuario"
 
@@ -922,6 +934,9 @@
 msgid "add CWRelation relation_type CWRType object"
 msgstr "Definición de relación"
 
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr ""
+
 msgid "add CWUser in_group CWGroup object"
 msgstr "Usuario"
 
@@ -1487,6 +1502,20 @@
 msgid "constraint factory"
 msgstr "Fábrica de restricciones"
 
+msgid "constraint_of"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr ""
+
+msgid "constraint_of_object"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr ""
+
 msgid "constraints"
 msgstr "Restricciones"
 
@@ -1619,6 +1648,11 @@
 msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
 msgstr "Creación de la relación %(linkto)s"
 
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr ""
+
 msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
 msgstr "Creación de un usuario para agregar al grupo %(linkto)s"
 
@@ -1873,6 +1907,9 @@
 msgid "define how we get out from a sub-workflow"
 msgstr "Define como salir de un sub-Workflow"
 
+msgid "defines a sql-level multicolumn unique index"
+msgstr ""
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -3231,9 +3268,27 @@
 msgid "relation_type_object"
 msgstr "Definición de Relaciones"
 
+msgid "relations"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr ""
+
 msgid "relations deleted"
 msgstr "Relaciones Eliminadas"
 
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr ""
+
 msgid "relative url of the bookmarked page"
 msgstr "Url relativa de la página"
 
@@ -4035,6 +4090,10 @@
 msgid "view_index"
 msgstr "Inicio"
 
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr ""
+
 msgid "visible"
 msgstr "Visible"
 
--- a/i18n/fr.po	Tue Sep 14 08:48:44 2010 +0200
+++ b/i18n/fr.po	Thu Sep 16 18:56:35 2010 +0200
@@ -4,7 +4,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2010-09-06 21:44+0200\n"
+"PO-Revision-Date: 2010-09-15 15:12+0200\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
 "Language: \n"
@@ -37,7 +37,7 @@
 msgstr "  de l'état %(fromstate)s vers l'état %(tostate)s\n"
 
 msgid " :"
-msgstr ""
+msgstr " :"
 
 #, python-format
 msgid "%(attr)s set to %(newvalue)s"
@@ -324,6 +324,12 @@
 msgid "CWRelation_plural"
 msgstr "Relations"
 
+msgid "CWUniqueTogetherConstraint"
+msgstr "Contrainte unique_together"
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr "Contraintes unique_together"
+
 msgid "CWUser"
 msgstr "Utilisateur"
 
@@ -515,6 +521,9 @@
 msgid "New CWRelation"
 msgstr "Nouvelle définition de relation non finale"
 
+msgid "New CWUniqueTogetherConstraint"
+msgstr "Nouvelle contrainte unique_together"
+
 msgid "New CWUser"
 msgstr "Nouvel utilisateur"
 
@@ -705,6 +714,9 @@
 msgid "This CWRelation"
 msgstr "Cette définition de relation"
 
+msgid "This CWUniqueTogetherConstraint"
+msgstr "Cette contrainte unique_together"
+
 msgid "This CWUser"
 msgstr "Cet utilisateur"
 
@@ -920,6 +932,9 @@
 msgid "add CWRelation relation_type CWRType object"
 msgstr "définition de relation"
 
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr "contrainte unique_together"
+
 msgid "add CWUser in_group CWGroup object"
 msgstr "utilisateur"
 
@@ -1487,6 +1502,20 @@
 msgid "constraint factory"
 msgstr "fabrique de contraintes"
 
+msgid "constraint_of"
+msgstr "contrainte de"
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr "contrainte de"
+
+msgid "constraint_of_object"
+msgstr "contraint par"
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr "contraint par"
+
 msgid "constraints"
 msgstr "contraintes"
 
@@ -1546,7 +1575,9 @@
 msgstr "contexte où ce composant doit être affiché"
 
 msgid "context where this facet should be displayed, leave empty for both"
-msgstr "contexte où cette facette doit être affichée. Laissez ce champ vide pour l'avoir dans les deux."
+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"
@@ -1619,6 +1650,11 @@
 msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
 msgstr "création relation %(linkto)s"
 
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr "création d'une contrainte unique_together sur %(linkto)s"
+
 msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
 msgstr "création d'un utilisateur à rajouter au groupe %(linkto)s"
 
@@ -1869,6 +1905,9 @@
 msgid "define how we get out from a sub-workflow"
 msgstr "définit comment sortir d'un sous-workflow"
 
+msgid "defines a sql-level multicolumn unique index"
+msgstr "définit un index SQL unique sur plusieurs colonnes"
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -3229,9 +3268,27 @@
 msgid "relation_type_object"
 msgstr "définition"
 
+msgid "relations"
+msgstr "relations"
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr "relations"
+
 msgid "relations deleted"
 msgstr "relations supprimées"
 
+msgid "relations_object"
+msgstr "relations de"
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr "contraint par"
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr "contraint par"
+
 msgid "relative url of the bookmarked page"
 msgstr "url relative de la page"
 
@@ -4032,6 +4089,10 @@
 msgid "view_index"
 msgstr "accueil"
 
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr "violation de contrainte unique_together (%s)"
+
 msgid "visible"
 msgstr "visible"
 
--- a/rtags.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/rtags.py	Thu Sep 16 18:56:35 2010 +0200
@@ -34,8 +34,6 @@
    * ``tag_subject_of`` tag a relation in the subject's context
    * ``tag_object_of`` tag a relation in the object's context
    * ``tag_attribute`` shortcut for tag_subject_of
-
-
 """
 __docformat__ = "restructuredtext en"
 
@@ -212,4 +210,27 @@
     _allowed_values = frozenset((True, False))
 
 
+class NoTargetRelationTagsDict(RelationTagsDict):
+
+    @property
+    def name(self):
+        return self.__class__.name
+
+    def tag_subject_of(self, key, tag):
+        subj, rtype, obj = key
+        if obj != '*':
+            self.warning('using explict target type in %s.tag_subject_of() '
+                         'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
+                         self.name, subj, rtype, subj, rtype, obj)
+        super(NoTargetRelationTagsDict, self).tag_subject_of((subj, rtype, '*'), tag)
+
+    def tag_object_of(self, key, tag):
+        subj, rtype, obj = key
+        if subj != '*':
+            self.warning('using explict subject type in %s.tag_object_of() '
+                         'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
+                         self.name, rtype, obj, subj, rtype, obj)
+        super(NoTargetRelationTagsDict, self).tag_object_of(('*', rtype, obj), tag)
+
+
 set_log_methods(RelationTags, logging.getLogger('cubicweb.rtags'))
--- a/schema.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/schema.py	Thu Sep 16 18:56:35 2010 +0200
@@ -62,6 +62,8 @@
     'relation_type', 'from_entity', 'to_entity',
     'constrained_by', 'cstrtype',
     'constraint_of', 'relations',
+    'read_permission', 'add_permission',
+    'delete_permission', 'update_permission',
     ))
 
 WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow',
@@ -613,6 +615,7 @@
 class BaseRQLConstraint(BaseConstraint):
     """base class for rql constraints
     """
+    distinct_query = None
 
     def __init__(self, restriction, mainvars=None):
         self.restriction = normalize_expression(restriction)
@@ -652,8 +655,12 @@
         pass # this is a vocabulary constraint, not enforce XXX why?
 
     def __str__(self):
-        return '%s(Any %s WHERE %s)' % (self.__class__.__name__, self.mainvars,
-                                        self.restriction)
+        if self.distinct_query:
+            selop = 'Any'
+        else:
+            selop = 'DISTINCT Any'
+        return '%s(%s %s WHERE %s)' % (self.__class__.__name__, selop,
+                                       self.mainvars, self.restriction)
 
     def __repr__(self):
         return '<%s @%#x>' % (self.__str__(), id(self))
@@ -745,13 +752,14 @@
 
 class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
     """the unique rql constraint check that the result of the query isn't
-    greater than one
-    """
-    distinct_query = True
+    greater than one.
 
-    # XXX turns mainvars into a required argument in __init__, since we've no
-    #     way to guess it correctly (eg if using S,O or U the constraint will
-    #     always be satisfied since we've to use a DISTINCT query)
+    You *must* specify mainvars when instantiating the constraint since there is
+    no way to guess it correctly (e.g. if using S,O or U the constraint will
+    always be satisfied because we've to use a DISTINCT query).
+    """
+    # XXX turns mainvars into a required argument in __init__
+    distinct_query = True
 
     def match_condition(self, session, eidfrom, eidto):
         return len(self.exec_query(session, eidfrom, eidto)) <= 1
--- a/server/hook.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/server/hook.py	Thu Sep 16 18:56:35 2010 +0200
@@ -248,7 +248,7 @@
 from itertools import chain
 
 from logilab.common.decorators import classproperty
-from logilab.common.deprecation import deprecated
+from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.logging_ext import set_log_methods
 
 from cubicweb import RegistryNotFound
@@ -480,15 +480,19 @@
 set_log_methods(Hook, getLogger('cubicweb.hook'))
 
 
-# base classes for relation propagation ########################################
+# abtract hooks for relation propagation #######################################
+# See example usage in hooks of the nosylist cube
 
-class PropagateSubjectRelationHook(Hook):
+class PropagateRelationHook(Hook):
     """propagate some `main_rtype` relation on entities linked as object of
     `subject_relations` or as subject of `object_relations` (the watched
     relations).
 
     This hook ensure that when one of the watched relation is added, the
     `main_rtype` relation is added to the target entity of the relation.
+
+    You usually want to use the :class:`match_rtype_sets` selector on concret
+    classes.
     """
     events = ('after_add_relation',)
 
@@ -514,56 +518,77 @@
             {'x': meid, 'e': seid})
 
 
-class PropagateSubjectRelationAddHook(Hook):
-    """propagate to entities at the end of watched relations when a `main_rtype`
-    relation is added
+class PropagateRelationAddHook(Hook):
+    """Propagate to entities at the end of watched relations when a `main_rtype`
+    relation is added.
+
+    `subject_relations` and `object_relations` attributes should be specified on
+    subclasses and are usually shared references with attributes of the same
+    name on :class:`PropagateRelationHook`.
+
+    Because of those shared references, you can use `skip_subject_relations` and
+    `skip_object_relations` attributes when you don't want to propagate to
+    entities linked through some particular relations.
     """
     events = ('after_add_relation',)
 
-    # to set in concrete class
+    # to set in concrete class (mandatory)
     subject_relations = None
     object_relations = None
+    # to set in concrete class (optionaly)
+    skip_subject_relations = ()
+    skip_object_relations = ()
 
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
         execute = self._cw.execute
         for rel in self.subject_relations:
-            if rel in eschema.subjrels:
+            if rel in eschema.subjrels and not rel in self.skip_subject_relations:
                 execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'X %s R, NOT R %s P' % (self.rtype, rel, self.rtype),
                         {'x': self.eidfrom, 'p': self.eidto})
         for rel in self.object_relations:
-            if rel in eschema.objrels:
+            if rel in eschema.objrels and not rel in self.skip_object_relations:
                 execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'R %s X, NOT R %s P' % (self.rtype, rel, self.rtype),
                         {'x': self.eidfrom, 'p': self.eidto})
 
 
-class PropagateSubjectRelationDelHook(Hook):
-    """propagate to entities at the end of watched relations when a `main_rtype`
-    relation is deleted
+class PropagateRelationDelHook(PropagateRelationAddHook):
+    """Propagate to entities at the end of watched relations when a `main_rtype`
+    relation is deleted.
+
+    This is the opposite of the :class:`PropagateRelationAddHook`, see its
+    documentation for how to use this class.
     """
     events = ('after_delete_relation',)
 
-    # to set in concrete class
-    subject_relations = None
-    object_relations = None
-
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
         execute = self._cw.execute
         for rel in self.subject_relations:
-            if rel in eschema.subjrels:
+            if rel in eschema.subjrels and not rel in self.skip_subject_relations:
                 execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'X %s R' % (self.rtype, rel),
                         {'x': self.eidfrom, 'p': self.eidto})
         for rel in self.object_relations:
-            if rel in eschema.objrels:
+            if rel in eschema.objrels and not rel in self.skip_object_relations:
                 execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                         'R %s X' % (self.rtype, rel),
                         {'x': self.eidfrom, 'p': self.eidto})
 
 
+PropagateSubjectRelationHook = class_renamed(
+    'PropagateSubjectRelationHook', PropagateRelationHook,
+    '[3.9] PropagateSubjectRelationHook has been renamed to PropagateRelationHook')
+PropagateSubjectRelationAddHook = class_renamed(
+    'PropagateSubjectRelationAddHook', PropagateRelationAddHook,
+    '[3.9] PropagateSubjectRelationAddHook has been renamed to PropagateRelationAddHook')
+PropagateSubjectRelationDelHook = class_renamed(
+    'PropagateSubjectRelationDelHook', PropagateRelationDelHook,
+    '[3.9] PropagateSubjectRelationDelHook has been renamed to PropagateRelationDelHook')
+
+
 # abstract classes for operation ###############################################
 
 class Operation(object):
--- a/server/repository.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/server/repository.py	Thu Sep 16 18:56:35 2010 +0200
@@ -413,6 +413,11 @@
     # public (dbapi) interface ################################################
 
     def stats(self): # XXX restrict to managers session?
+        """Return a dictionary containing some statistics about the repository
+        resources usage.
+
+        This is a public method, not requiring a session id.
+        """
         results = {}
         querier = self.querier
         source = self.system_source
@@ -435,8 +440,9 @@
         return results
 
     def get_schema(self):
-        """return the instance schema. This is a public method, not
-        requiring a session id
+        """Return the instance schema.
+
+        This is a public method, not requiring a session id.
         """
         try:
             # necessary to support pickling used by pyro
@@ -446,8 +452,9 @@
             self.schema.__hashmode__ = None
 
     def get_cubes(self):
-        """return the list of cubes used by this instance. This is a
-        public method, not requiring a session id.
+        """Return the list of cubes used by this instance.
+
+        This is a public method, not requiring a session id.
         """
         versions = self.get_versions(not (self.config.creating
                                           or self.config.repairing
@@ -457,11 +464,20 @@
         cubes.remove('cubicweb')
         return cubes
 
+    def get_option_value(self, option):
+        """Return the value for `option` in the configuration.
+
+        This is a public method, not requiring a session id.
+        """
+        # XXX we may want to check we don't give sensible information
+        return self.config[option]
+
     @cached
     def get_versions(self, checkversions=False):
-        """return the a dictionary containing cubes used by this instance
-        as key with their version as value, including cubicweb version. This is a
-        public method, not requiring a session id.
+        """Return the a dictionary containing cubes used by this instance
+        as key with their version as value, including cubicweb version.
+
+        This is a public method, not requiring a session id.
         """
         from logilab.common.changelog import Version
         vcconf = {}
@@ -491,6 +507,11 @@
 
     @cached
     def source_defs(self):
+        """Return the a dictionary containing source uris as value and a
+        dictionary describing each source as value.
+
+        This is a public method, not requiring a session id.
+        """
         sources = self.config.sources().copy()
         # remove manager information
         sources.pop('admin', None)
@@ -502,7 +523,10 @@
         return sources
 
     def properties(self):
-        """return a result set containing system wide properties"""
+        """Return a result set containing system wide properties.
+
+        This is a public method, not requiring a session id.
+        """
         session = self.internal_session()
         try:
             # don't use session.execute, we don't want rset.req set
--- a/server/sources/native.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/server/sources/native.py	Thu Sep 16 18:56:35 2010 +0200
@@ -677,6 +677,11 @@
                         etype = elements[0]
                         rtypes = elements[1:]                        
                         raise UniqueTogetherError(etype, rtypes)
+                    mo = re.search('columns (.*) are not unique', arg)
+                    if mo is not None: # sqlite in use
+                        rtypes = [c.strip().lstrip('cw_') for c in mo.group(1).split(',')]
+                        etype = '???'
+                        raise UniqueTogetherError(etype, rtypes)
             raise
         return cursor
 
--- a/server/test/data/schema.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/server/test/data/schema.py	Thu Sep 16 18:56:35 2010 +0200
@@ -20,7 +20,8 @@
                             SubjectRelation, RichString, String, Int, Float,
                             Boolean, Datetime)
 from yams.constraints import SizeConstraint
-from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
+from cubicweb.schema import (WorkflowableEntityType,
+                             RQLConstraint, RQLUniqueConstraint,
                              ERQLExpression, RRQLExpression)
 
 class Affaire(WorkflowableEntityType):
@@ -94,7 +95,10 @@
 
     migrated_from = SubjectRelation('Note')
     attachment = SubjectRelation('File')
-    inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
+    inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*',
+                              constraints=[RQLUniqueConstraint('S type T, S inline1 A1, A1 todo_by C, '
+                                                              'Y type T, Y inline1 A2, A2 todo_by C',
+                                                               'S,Y')])
     todo_by = SubjectRelation('CWUser')
 
 class Personne(EntityType):
--- a/server/test/unittest_repository.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/server/test/unittest_repository.py	Thu Sep 16 18:56:35 2010 +0200
@@ -627,6 +627,18 @@
             self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
                                        ('after_add_relation', eidn, 'ecrit_par', eidp)])
 
+    def test_unique_contraint(self):
+        req = self.request()
+        toto = req.create_entity('Personne', nom=u'toto')
+        a01 = req.create_entity('Affaire', ref=u'A01', todo_by=toto)
+        req.cnx.commit()
+        req = self.request()
+        req.create_entity('Note', type=u'todo', inline1=a01)
+        req.cnx.commit()
+        req = self.request()
+        req.create_entity('Note', type=u'todo', inline1=a01)
+        ex = self.assertRaises(ValidationError, req.cnx.commit)
+        self.assertEquals(ex.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'})
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/application.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/application.py	Thu Sep 16 18:56:35 2010 +0200
@@ -420,11 +420,12 @@
                 self.validation_error_handler(req, ex)
             except (Unauthorized, BadRQLQuery, RequestError), ex:
                 self.error_handler(req, ex, tb=False)
-            except Exception, ex:
+            except BaseException, ex:
                 self.error_handler(req, ex, tb=True)
             except:
                 self.critical('Catch all triggered!!!')
                 self.exception('this is what happened')
+                result = 'oops'
         finally:
             if req.cnx and not commited:
                 try:
--- a/web/component.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/component.py	Thu Sep 16 18:56:35 2010 +0200
@@ -27,6 +27,7 @@
 
 from cubicweb import Unauthorized, role, tags
 from cubicweb.uilib import js, domid
+from cubicweb.utils import json_dumps
 from cubicweb.view import ReloadableMixIn, Component
 from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset,
                                 non_final_entity, partial_relation_possible,
@@ -106,17 +107,23 @@
         if view is not None and hasattr(view, 'page_navigation_url'):
             url = view.page_navigation_url(self, path, params)
         elif path == 'json':
-            rql = params.pop('rql', self.cw_rset.printable_rql())
-            # latest 'true' used for 'swap' mode
-            url = 'javascript: %s' % (js.replacePageChunk(
-                params.get('divid', 'pageContent'), rql,
-                params.pop('vid', None), params))
+            url = self.ajax_page_url(**params)
         else:
             url = self._cw.build_url(path, **params)
         return url
 
+    def ajax_page_url(self, **params):
+        divid = params.setdefault('divid', 'pageContent')
+        params['rql'] = self.cw_rset.printable_rql()
+        return "javascript: $(%s).loadxhtml('json', %s, 'get', 'swap')" % (
+            json_dumps('#'+divid), js.ajaxFuncArgs('view', params))
+
     def page_link(self, path, params, start, stop, content):
         url = xml_escape(self.page_url(path, params, start, stop))
+        # XXX hack to avoid opening a new page containing the evaluation of the
+        # js expression on ajax call
+        if url.startswith('javascript:'):
+            url += '; noop();'
         if start == self.starting_from:
             return self.selected_page_link_templ % (url, content, content)
         return self.page_link_templ % (url, content, content)
--- a/web/data/cubicweb.css	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.css	Thu Sep 16 18:56:35 2010 +0200
@@ -917,6 +917,14 @@
 }
 
 /********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+  float: right;
+}
+
+/********************************/
 /* rest related classes         */
 /********************************/
 
@@ -928,6 +936,18 @@
   margin-right: 1.5em;
 }
 
+/******************************/
+/* reledit                    */
+/******************************/
+
+.releditField {
+    display: inline;
+}
+
+.releditForm {
+ display:none;
+}
+
 /********************************/
 /* overwite other css here      */
 /********************************/
@@ -955,4 +975,3 @@
   margin-left: 0.5em;
   vertical-align: bottom;
 }
-
--- a/web/data/cubicweb.edition.js	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.edition.js	Thu Sep 16 18:56:35 2010 +0200
@@ -317,7 +317,7 @@
         form.insertBefore(insertBefore).slideDown('fast');
         updateInlinedEntitiesCounters(rtype, role);
         reorderTabindex(null, $(insertBefore).closest('form')[0]);
-        jQuery(CubicWeb).trigger('inlinedform-added', form);
+        jQuery(cw).trigger('inlinedform-added', form);
         // if the inlined form contains a file input, we must force
         // the form enctype to multipart/form-data
         if (form.find('input:file').length) {
--- a/web/data/cubicweb.old.css	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.old.css	Thu Sep 16 18:56:35 2010 +0200
@@ -901,3 +901,24 @@
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
   background: #fffff8 url("button.png") bottom left repeat-x;
 }
+
+/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+  float: right;
+}
+
+
+/******************************/
+/* reledit                    */
+/******************************/
+
+.releditField {
+    display: inline;
+}
+
+.releditForm {
+ display:none;
+}
--- a/web/data/cubicweb.reledit.js	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.reledit.js	Thu Sep 16 18:56:35 2010 +0200
@@ -53,6 +53,7 @@
             }
         }
         jQuery('#'+params.divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, params, 'post');
+        jQuery(cw).trigger('reledit-reloaded', params);
     },
 
     /* called by reledit forms to submit changes
--- a/web/facet.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/facet.py	Thu Sep 16 18:56:35 2010 +0200
@@ -515,6 +515,7 @@
     def title(self):
         return display_name(self._cw, self.rtype, form=self.role)
 
+    @property
     def rql_sort(self):
         """return true if we can handle sorting in the rql query. E.g.  if
         sortfunc is set or if we have not to transform the returned value (eg no
--- a/web/formwidgets.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/formwidgets.py	Thu Sep 16 18:56:35 2010 +0200
@@ -653,10 +653,16 @@
         timestr = req.form.get(field.input_name(form, 'time')).strip() or None
         if datestr is None:
             return None
-        date = todatetime(req.parse_datetime(datestr, 'Date'))
+        try:
+            date = todatetime(req.parse_datetime(datestr, 'Date'))
+        except ValueError, exc:
+            raise ProcessFormError(unicode(exc))
         if timestr is None:
             return date
-        time = req.parse_datetime(timestr, 'Time')
+        try:
+            time = req.parse_datetime(timestr, 'Time')
+        except ValueError, exc:
+            raise ProcessFormError(unicode(exc))
         return date.replace(hour=time.hour, minute=time.minute, second=time.second)
 
 
--- a/web/request.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/request.py	Thu Sep 16 18:56:35 2010 +0200
@@ -99,9 +99,6 @@
         # search state: 'normal' or 'linksearch' (eg searching for an object
         # to create a relation with another)
         self.search_state = ('normal',)
-        # tabindex generator
-        self.tabindexgen = count(1)
-        self.next_tabindex = self.tabindexgen.next
         # page id, set by htmlheader template
         self.pageid = None
         self._set_pageid()
@@ -131,6 +128,13 @@
         """
         return self.set_varmaker()
 
+    def _get_tabindex_func(self):
+        nextfunc = self.get_page_data('nexttabfunc')
+        if nextfunc is None:
+            nextfunc = count(1).next
+            self.set_page_data('nexttabfunc', nextfunc)
+        return nextfunc
+
     def set_varmaker(self):
         varmaker = self.get_page_data('rql_varmaker')
         if varmaker is None:
@@ -143,6 +147,8 @@
         or an anonymous connection is open
         """
         super(CubicWebRequestBase, self).set_session(session, user)
+        # tabindex generator
+        self.next_tabindex = self._get_tabindex_func()
         # set request language
         vreg = self.vreg
         if self.user:
--- a/web/test/data/schema.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/test/data/schema.py	Thu Sep 16 18:56:35 2010 +0200
@@ -75,3 +75,19 @@
     cp   = String(maxsize=12)
     ville= String(maxsize=32)
 
+# enough relations to cover most reledit use cases
+class Project(EntityType):
+    title = String(maxsize=32, required=True, fulltextindexed=True)
+    long_desc = SubjectRelation('Blog', composite='subject', cardinality='?*')
+    manager = SubjectRelation('Personne', cardinality='?*')
+
+class composite_card11_2ttypes(RelationDefinition):
+    subject = 'Project'
+    object = ('File', 'Blog')
+    composite = 'subject'
+    cardinality = '?*'
+
+class Ticket(EntityType):
+    title = String(maxsize=32, required=True, fulltextindexed=True)
+    concerns = SubjectRelation('Project', composite='object')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_reledit.py	Thu Sep 16 18:56:35 2010 +0200
@@ -0,0 +1,223 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""
+mainly regression-preventing tests for reledit/doreledit views
+"""
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web.uicfg import reledit_ctrl
+
+class ReleditMixinTC(object):
+
+    def setup_database(self):
+        self.req = self.request()
+        self.proj = self.req.create_entity('Project', title=u'cubicweb-world-domination')
+        self.tick = self.req.create_entity('Ticket', title=u'write the code')
+        self.toto = self.req.create_entity('Personne', nom=u'Toto')
+
+class ClickAndEditFormTC(ReleditMixinTC, CubicWebTC):
+
+    def test_default_config(self):
+        reledit = {'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, 917, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-917&#39;, false, &#39;&#39;, &#39;&amp;lt;title not specified&amp;gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+                   'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue">&lt;long_desc not specified&gt;</div><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, 917, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-917&#39;, false, &#39;incontext&#39;, &#39;&amp;lt;long_desc not specified&amp;gt;&#39;);" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
+                   'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue">&lt;manager not specified&gt;</div><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, 917, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-917&#39;, false, &#39;incontext&#39;, &#39;&amp;lt;manager not specified&amp;gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+                   'composite_card11_2ttypes': """&lt;composite_card11_2ttypes not specified&gt;""",
+                   'concerns': """&lt;concerns_object not specified&gt;"""}
+
+        for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+            if rschema not in reledit:
+                continue
+            rtype = rschema.type
+            self.assertTextEquals(reledit[rtype], self.proj.view('reledit', rtype=rtype, role=role), rtype)
+
+    def test_default_forms(self):
+        doreledit = {'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="title-subject-917-form" onsubmit="return freezeFormButtons(&#39;title-subject-917-form&#39;);" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="base" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#title-subject-917-form" />
+<input name="__domid" type="hidden" value="title-subject-917-form" />
+<input name="__type:917" type="hidden" value="Project" />
+<input name="eid" type="hidden" value="917" />
+<input name="__maineid" type="hidden" value="917" />
+<input name="__reledit|default_value" type="hidden" value="&amp;lt;title not specified&amp;gt;" />
+<input name="__reledit|vid" type="hidden" value="" />
+<input name="__reledit|rtype" type="hidden" value="title" />
+<input name="__reledit|divid" type="hidden" value="title-subject-917" />
+<input name="__reledit|formid" type="hidden" value="base" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="917" />
+<input name="_cw_edited_fields:917" type="hidden" value="title-subject,__type" />
+<fieldset class="default">
+<table class="">
+<tr class="title_subject_row">
+<td
+>
+<input id="title-subject:917" maxlength="32" name="title-subject:917" size="32" tabindex="1" type="text" value="cubicweb-world-domination" />
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="2" type="submit" value="button_ok"><img alt="OK_ICON" src="http://crater:8080/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;title-subject-917&#39;)" tabindex="3" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://crater:8080/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, 917, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-917&#39;, false, &#39;&#39;, &#39;&amp;lt;title not specified&amp;gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+
+                     'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue">&lt;long_desc not specified&gt;</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="long_desc-subject-917-form" onsubmit="return freezeFormButtons(&#39;long_desc-subject-917-form&#39;);" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="edition" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#long_desc-subject-917-form" />
+<input name="__domid" type="hidden" value="long_desc-subject-917-form" />
+<input name="__type:A" type="hidden" value="Blog" />
+<input name="eid" type="hidden" value="A" />
+<input name="__maineid" type="hidden" value="A" />
+<input name="__linkto" type="hidden" value="long_desc:917:object" />
+<input name="__message" type="hidden" value="entity linked" />
+<input name="__reledit|default_value" type="hidden" value="&amp;lt;long_desc not specified&amp;gt;" />
+<input name="__reledit|vid" type="hidden" value="incontext" />
+<input name="__reledit|rtype" type="hidden" value="long_desc" />
+<input name="__reledit|divid" type="hidden" value="long_desc-subject-917" />
+<input name="__reledit|formid" type="hidden" value="edition" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="917" />
+<input name="_cw_edited_fields:A" type="hidden" value="title-subject,rss_url-subject,__type,description-subject" />
+<fieldset class="default">
+<table class="attributeForm">
+<tr class="title_subject_row">
+<th class="labelCol"><label class="required" for="title-subject:A">title</label></th>
+<td
+>
+<input id="title-subject:A" maxlength="50" name="title-subject:A" size="45" tabindex="4" type="text" value="" />
+</td></tr>
+<tr class="description_subject_row">
+<th class="labelCol"><label for="description-subject:A">description</label></th>
+<td
+>
+<input name="description_format-subject:A" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:A" name="description-subject:A" onkeyup="autogrow(this)" rows="2" tabindex="5"></textarea>
+</td></tr>
+<tr class="rss_url_subject_row">
+<th class="labelCol"><label for="rss_url-subject:A">rss_url</label></th>
+<td
+>
+<input id="rss_url-subject:A" maxlength="128" name="rss_url-subject:A" size="45" tabindex="6" type="text" value="" />
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="7" type="submit" value="button_ok"><img alt="OK_ICON" src="http://crater:8080/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;long_desc-subject-917&#39;)" tabindex="8" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://crater:8080/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, 917, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-917&#39;, false, &#39;incontext&#39;, &#39;&amp;lt;long_desc not specified&amp;gt;&#39;);" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
+
+                     'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue">&lt;manager not specified&gt;</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="manager-subject-917-form" onsubmit="return freezeFormButtons(&#39;manager-subject-917-form&#39;);" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="base" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#manager-subject-917-form" />
+<input name="__domid" type="hidden" value="manager-subject-917-form" />
+<input name="__type:917" type="hidden" value="Project" />
+<input name="eid" type="hidden" value="917" />
+<input name="__maineid" type="hidden" value="917" />
+<input name="__linkto" type="hidden" value="long_desc:917:object" />
+<input name="__message" type="hidden" value="entity linked" />
+<input name="__reledit|default_value" type="hidden" value="&amp;lt;manager not specified&amp;gt;" />
+<input name="__reledit|vid" type="hidden" value="incontext" />
+<input name="__reledit|rtype" type="hidden" value="manager" />
+<input name="__reledit|divid" type="hidden" value="manager-subject-917" />
+<input name="__reledit|formid" type="hidden" value="base" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="917" />
+<input name="_cw_edited_fields:917" type="hidden" value="manager-subject,__type" />
+<fieldset class="default">
+<table class="">
+<tr class="manager_subject_row">
+<td
+>
+<select id="manager-subject:917" name="manager-subject:917" size="1" tabindex="9">
+<option value="__cubicweb_internal_field__"></option>
+<option value="919">Toto</option>
+</select>
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="10" type="submit" value="button_ok"><img alt="OK_ICON" src="http://crater:8080/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;manager-subject-917&#39;)" tabindex="11" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://crater:8080/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, 917, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-917&#39;, false, &#39;incontext&#39;, &#39;&amp;lt;manager not specified&amp;gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+                     'composite_card11_2ttypes': """&lt;composite_card11_2ttypes not specified&gt;""",
+                     'concerns': """&lt;concerns_object not specified&gt;"""
+            }
+        for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+            if rschema not in doreledit:
+                continue
+            rtype = rschema.type
+            self.assertTextEquals(doreledit[rtype],
+                                  self.proj.view('doreledit', rtype=rtype, role=role,
+                                                 formid='edition' if rtype == 'long_desc' else 'base'),
+                                  rtype)
+
+class ClickAndEditFormUICFGTC(ReleditMixinTC, CubicWebTC):
+
+    def setup_database(self):
+        super(ClickAndEditFormUICFGTC, self).setup_database()
+        self.tick.set_relations(concerns=self.proj)
+        self.proj.set_relations(manager=self.toto)
+
+    def test_with_uicfg(self):
+        old_rctl = reledit_ctrl._tagdefs.copy()
+        reledit_ctrl.tag_attribute(('Project', 'title'),
+                                   {'default_value': '<title is required>', 'reload': True})
+        reledit_ctrl.tag_subject_of(('Project', 'long_desc', '*'),
+                                    {'reload': True, 'edit_target': 'rtype',
+                                     'default_value': u'<long_desc is required>'})
+        reledit_ctrl.tag_subject_of(('Project', 'manager', '*'),
+                                   {'edit_target': 'related'})
+        reledit_ctrl.tag_subject_of(('Project', 'composite_card11_2ttypes', '*'),
+                                   {'edit_target': 'related'})
+        reledit_ctrl.tag_object_of(('Ticket', 'concerns', 'Project'),
+                                   {'edit_target': 'rtype'})
+        reledit = {
+            'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, 917, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-917&#39;, true, &#39;&#39;, &#39;&lt;title is required&gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+            'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue"><long_desc is required></div><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, 917, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-917&#39;, true, &#39;incontext&#39;, &#39;&lt;long_desc is required&gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+            'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/personne/919" title="">Toto</a></div><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, 917, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-917&#39;, false, &#39;incontext&#39;, &#39;&amp;lt;manager not specified&amp;gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div><div id="manager-subject-917-delete" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;deleteconf&#39;, 917, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-917&#39;, false, &#39;incontext&#39;, &#39;&amp;lt;manager not specified&amp;gt;&#39;);" title="click to delete this value"><img title="click to delete this value" src="data/cancel.png" alt="click to delete this value"/></div></div></div>""",
+            'composite_card11_2ttypes': """&lt;composite_card11_2ttypes not specified&gt;""",
+            'concerns': """<div id="concerns-object-917-reledit" onmouseout="jQuery('#concerns-object-917').addClass('hidden')" onmouseover="jQuery('#concerns-object-917').removeClass('hidden')" class="releditField"><div id="concerns-object-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/ticket/918" title="">write the code</a></div><div id="concerns-object-917" class="editableField hidden"><div id="concerns-object-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, 917, &#39;concerns&#39;, &#39;object&#39;, &#39;concerns-object-917&#39;, false, &#39;csv&#39;, &#39;&amp;lt;concerns_object not specified&amp;gt;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>"""
+            }
+        for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+            if rschema not in reledit:
+                continue
+            rtype = rschema.type
+            self.assertTextEquals(reledit[rtype],
+                                  self.proj.view('reledit', rtype=rtype, role=role),
+                                  rtype)
+        reledit_ctrl.clear()
+        reledit_ctrl._tagdefs.update(old_rctl)
--- a/web/uicfg.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/uicfg.py	Thu Sep 16 18:56:35 2010 +0200
@@ -53,7 +53,8 @@
 
 from cubicweb import neg_role
 from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
-                            RelationTagsDict, register_rtag, _ensure_str_key)
+                            RelationTagsDict, NoTargetRelationTagsDict,
+                            register_rtag, _ensure_str_key)
 from cubicweb.schema import META_RTYPES
 
 
@@ -83,27 +84,11 @@
                                               'sideboxes', 'hidden')))
 
 
-class DisplayCtrlRelationTags(RelationTagsDict):
+class DisplayCtrlRelationTags(NoTargetRelationTagsDict):
     def __init__(self, *args, **kwargs):
         super(DisplayCtrlRelationTags, self).__init__(*args, **kwargs)
         self.counter = 0
 
-    def tag_subject_of(self, key, tag):
-        subj, rtype, obj = key
-        if obj != '*':
-            self.warning('using explict target type in display_ctrl.tag_subject_of() '
-                         'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
-                         subj, rtype, subj, rtype, obj)
-        super(DisplayCtrlRelationTags, self).tag_subject_of((subj, rtype, '*'), tag)
-
-    def tag_object_of(self, key, tag):
-        subj, rtype, obj = key
-        if subj != '*':
-            self.warning('using explict subject type in display_ctrl.tag_object_of() '
-                         'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
-                         rtype, obj, subj, rtype, obj)
-        super(DisplayCtrlRelationTags, self).tag_object_of(('*', rtype, obj), tag)
-
 def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role):
     if role == 'subject':
         oschema = '*'
@@ -378,7 +363,7 @@
 autoform_field = RelationTags('autoform_field')
 
 # relations'field explicit kwargs (given to field's __init__)
-autoform_field_kwargs = RelationTagsDict()
+autoform_field_kwargs = RelationTagsDict('autoform_field_kwargs')
 
 
 # set of tags of the form <action>_on_new on relations. <action> is a
@@ -386,31 +371,49 @@
 # permissions checking is by-passed and supposed to be ok
 autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides')
 
-class _ReleditTags(RelationTagsDict):
-    _keys = frozenset('reload default_value noedit'.split())
-
-    def tag_subject_of(self, key, *args, **kwargs):
-        subj, rtype, obj = key
-        if obj != '*':
-            self.warning('using explict target type in display_ctrl.tag_subject_of() '
-                         'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
-                         subj, rtype, subj, rtype, obj)
-        super(_ReleditTags, self).tag_subject_of(key, *args, **kwargs)
-
-    def tag_object_of(self, key, *args, **kwargs):
-        subj, rtype, obj = key
-        if subj != '*':
-            self.warning('using explict subject type in display_ctrl.tag_object_of() '
-                         'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
-                         rtype, obj, subj, rtype, obj)
-        super(_ReleditTags, self).tag_object_of(key, *args, **kwargs)
+class ReleditTags(NoTargetRelationTagsDict):
+    """
+    default_value: alternative default value
+      The default value is what is shown when there is no value.
+    reload: boolean, eid (to reload to) or function taking subject and returning bool/eid
+      This is useful when editing a relation (or attribute) that impacts the url
+      or another parts of the current displayed page. Defaults to False.
+    rvid: alternative view id (as str) for relation or composite edition
+      Default is 'incontext' or 'csv' depending on the cardinality. They can also be
+      statically changed by subclassing ClickAndEditFormView and redefining _one_rvid
+      (resp. _many_rvid).
+    edit_target: 'rtype' (to edit the relation) or 'related' (to edit the related entity)
+      This controls whether to edit the relation or the target entity of the relation.
+      Currently only one-to-one relations support target entity edition. By default,
+      the 'related' option is taken whenever the relation is composite and one-to-one.
+    """
+    _keys = frozenset('default_value reload rvid edit_target'.split())
 
     def tag_relation(self, key, tag):
         for tagkey in tag.iterkeys():
             assert tagkey in self._keys, 'tag %r not in accepted tags: %r' % (tag, self._keys)
-        return super(_ReleditTags, self).tag_relation(key, tag)
+        return super(ReleditTags, self).tag_relation(key, tag)
 
-reledit_ctrl = _ReleditTags('reledit')
+def init_reledit_ctrl(rtag, sschema, rschema, oschema, role):
+    if rschema.final:
+        return
+    composite = rschema.rdef(sschema, oschema).composite == role
+    if role == 'subject':
+        oschema = '*'
+    else:
+        sschema = '*'
+    values = rtag.get(sschema, rschema, oschema, role)
+    edittarget = values.get('edit_target')
+    if edittarget not in (None, 'rtype', 'related'):
+        rtag.warning('reledit: wrong value for edit_target on relation %s: %s',
+                     rschema, edittarget)
+        edittarget = None
+    if not edittarget:
+        edittarget = 'related' if composite else 'rtype'
+        rtag.tag_relation((sschema, rschema, oschema, role),
+                          {'edit_target': edittarget})
+
+reledit_ctrl = ReleditTags('reledit', init_reledit_ctrl)
 
 # boxes.EditBox configuration #################################################
 
--- a/web/views/__init__.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/__init__.py	Thu Sep 16 18:56:35 2010 +0200
@@ -84,7 +84,7 @@
             return VID_BY_MIMETYPE[mimetype]
     nb_rows = len(rset)
     # empty resultset
-    if nb_rows == 0 :
+    if nb_rows == 0:
         return 'noresult'
     # entity result set
     if not schema.eschema(rset.description[0][0]).final:
--- a/web/views/basecontrollers.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/basecontrollers.py	Thu Sep 16 18:56:35 2010 +0200
@@ -260,9 +260,8 @@
 def optional_kwargs(extraargs):
     if extraargs is None:
         return {}
-    else: # we receive unicode keys which is not supported by the **syntax
-        return dict((str(key), value)
-                    for key, value in extraargs.items())
+    # we receive unicode keys which is not supported by the **syntax
+    return dict((str(key), value) for key, value in extraargs.iteritems())
 
 class JSonController(Controller):
     __regid__ = 'json'
@@ -334,6 +333,9 @@
 
     def _exec(self, rql, args=None, rocheck=True):
         """json mode: execute RQL and return resultset as json"""
+        rql = rql.strip()
+        if rql.startswith('rql:'):
+            rql = rql[4:]
         if rocheck:
             self._cw.ensure_ro_rql(rql)
         try:
@@ -344,7 +346,7 @@
         return None
 
     def _call_view(self, view, paginate=False, **kwargs):
-        divid = self._cw.form.get('divid', 'pageContent')
+        divid = self._cw.form.get('divid')
         # we need to call pagination before with the stream set
         try:
             stream = view.set_stream()
@@ -352,23 +354,26 @@
             stream = UStringIO()
             kwargs['w'] = stream.write
             assert not paginate
+        if divid == 'pageContent':
+            # ensure divid isn't reused by the view (e.g. table view)
+            del self._cw.form['divid']
+            # mimick main template behaviour
+            stream.write(u'<div id="pageContent">')
+            vtitle = self._cw.form.get('vtitle')
+            if vtitle:
+                stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+            paginate = True
         if paginate:
-            if divid == 'pageContent':
-                # mimick main template behaviour
-                stream.write(u'<div id="pageContent">')
-                vtitle = self._cw.form.get('vtitle')
-                if vtitle:
-                    stream.write(u'<div class="vtitle">%s</div>\n' % vtitle)
             view.paginate()
-            if divid == 'pageContent':
-                stream.write(u'<div id="contentmain">')
+        if divid == 'pageContent':
+            stream.write(u'<div id="contentmain">')
         view.render(**kwargs)
         extresources = self._cw.html_headers.getvalue(skiphead=True)
         if extresources:
             stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
             stream.write(extresources)
             stream.write(u'</div>\n')
-        if paginate and divid == 'pageContent':
+        if divid == 'pageContent':
             stream.write(u'</div></div>')
         return stream.getvalue()
 
@@ -390,7 +395,7 @@
             vid = req.form.get('fallbackvid', 'noresult')
             view = self._cw.vreg['views'].select(vid, req, rset=rset)
         self.validate_cache(view)
-        return self._call_view(view, paginate=req.form.get('paginate'))
+        return self._call_view(view, paginate=req.form.pop('paginate', False))
 
     @xhtmlize
     def js_prop_widget(self, propkey, varname, tabindex=None):
@@ -411,11 +416,6 @@
             rset = self._exec(rql)
         else:
             rset = None
-        if extraargs is None:
-            extraargs = {}
-        else: # we receive unicode keys which is not supported by the **syntax
-            extraargs = dict((str(key), value)
-                             for key, value in extraargs.items())
         # XXX while it sounds good, addition of the try/except below cause pb:
         # when filtering using facets return an empty rset, the edition box
         # isn't anymore selectable, as expected. The pb is that with the
@@ -425,21 +425,22 @@
         # error is expected and should'nt be reported.
         #try:
         comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
-                                              **extraargs)
+                                              **optional_kwargs(extraargs))
         #except NoSelectableObject:
         #    raise RemoteCallFailed('unselectable')
         return self._call_view(comp, **extraargs)
 
     @xhtmlize
-    def js_render(self, registry, oid, eid=None, selectargs=None, renderargs=None):
+    def js_render(self, registry, oid, eid=None,
+                  selectargs=None, renderargs=None):
         if eid is not None:
             rset = self._cw.eid_rset(eid)
         elif self._cw.form.get('rql'):
             rset = self._cw.execute(self._cw.form['rql'])
         else:
             rset = None
-        selectargs = optional_kwargs(selectargs)
-        view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, **selectargs)
+        view = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
+                                              **optional_kwargs(selectargs))
         return self._call_view(view, **optional_kwargs(renderargs))
 
     @check_pageid
--- a/web/views/forms.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/forms.py	Thu Sep 16 18:56:35 2010 +0200
@@ -188,7 +188,7 @@
         if self.formvalues is not None:
             return # already built
         self.formvalues = formvalues or {}
-        # use a copy in case fields are modified while context is build (eg
+        # use a copy in case fields are modified while context is built (eg
         # __linkto handling for instance)
         for field in self.fields[:]:
             for field in field.actual_fields(self):
--- a/web/views/primary.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/primary.py	Thu Sep 16 18:56:35 2010 +0200
@@ -24,7 +24,7 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb import Unauthorized
+from cubicweb import Unauthorized, NoSelectableObject
 from cubicweb.utils import support_args
 from cubicweb.selectors import match_kwargs
 from cubicweb.view import EntityView
@@ -161,13 +161,19 @@
     def render_entity_relations(self, entity):
         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):
             if rschema.final or dispctrl.get('rtypevid'):
+                vid = dispctrl.get('vid', 'reledit')
+                try:
+                    rview = self._cw.vreg['views'].select(
+                        vid, self._cw, rset=entity.cw_rset, row=entity.cw_row,
+                        col=entity.cw_col, dispctrl=dispctrl, rtype=rschema, role=role)
+                except NoSelectableObject:
+                    continue
                 self.w(u'<div class="section">')
                 label = self._rel_label(entity, rschema, role, dispctrl)
                 if label:
                     self.w(u'<h4>%s</h4>' % label)
-                vid = dispctrl.get('vid', 'reledit')
-                entity.view(vid, rtype=rschema.type, role=role, w=self.w,
-                            initargs={'dispctrl': dispctrl})
+                rview.render(row=entity.cw_row, col=entity.cw_col, w=self.w,
+                             rtype=rschema.type, role=role)
                 self.w(u'</div>')
                 continue
             rset = self._relation_rset(entity, rschema, role, dispctrl)
@@ -248,10 +254,7 @@
                 if section == where:
                     matchtschemas.append(tschema)
             if matchtschemas:
-                # XXX pick the latest dispctrl
-                dispctrl = self.display_ctrl.etype_get(eschema, rschema, role,
-                                                       matchtschemas[-1])
-
+                dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, '*')
                 rdefs.append( (rschema, matchtschemas, role, dispctrl) )
         return sorted(rdefs, key=lambda x: x[-1]['order'])
 
@@ -338,6 +341,28 @@
         if url:
             self.w(u'<a href="%s">%s</a>' % (url, url))
 
+class AttributeView(EntityView):
+    """use this view on an entity as an alternative to more sophisticated
+    views such as reledit.
+
+    Ex. usage:
+
+    uicfg.primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'})
+    """
+    __regid__ = 'attribute'
+    __select__ = EntityView.__select__ & match_kwargs('rtype')
+
+    def cell_call(self, row, col, rtype, **kwargs):
+        entity = self.cw_rset.get_entity(row, col)
+        if self._cw.vreg.schema.rschema(rtype).final:
+            self.w(entity.printable_value(rtype))
+        else:
+            dispctrl = uicfg.primaryview_display_ctrl.etype_get(
+                entity.e_schema, rtype, kwargs['role'], '*')
+            rset = entity.related(rtype, role)
+            if rset:
+                self.wview('autolimited', rset, initargs={'dispctrl': dispctrl})
+
 
 ## default primary ui configuration ###########################################
 
--- a/web/views/reledit.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/reledit.py	Thu Sep 16 18:56:35 2010 +0200
@@ -21,6 +21,7 @@
 import copy
 
 from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import deprecated
 
 from cubicweb import neg_role
 from cubicweb.schema import display_name
@@ -39,8 +40,8 @@
         return u''
     def append_field(self, *args):
         pass
-    def field_by_name(self, rtype, role, eschema=None):
-        return None
+
+rctrl = uicfg.reledit_ctrl
 
 class ClickAndEditFormView(EntityView):
     __regid__ = 'doreledit'
@@ -60,8 +61,11 @@
     _editzonemsg = _('click to edit this field')
 
     # default relation vids according to cardinality
+    # can be changed per rtype using reledit_ctrl rtag
     _one_rvid = 'incontext'
     _many_rvid = 'csv'
+    # renderer
+    _form_renderer_id = 'base'
 
     def cell_call(self, row, col, rtype=None, role='subject',
                   reload=False, # controls reloading the whole page after change
@@ -69,89 +73,98 @@
                                 # function taking the subject entity & returning a boolean or an eid
                   rvid=None,    # vid to be applied to other side of rtype (non final relations only)
                   default_value=None,
-                  formid=None
+                  formid='base'
                   ):
         """display field to edit entity's `rtype` relation on click"""
         assert rtype
         assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
-        if self.__regid__ == 'doreledit':
-            assert formid
-        self._cw.add_js('cubicweb.reledit.js')
-        if formid:
-            self._cw.add_js('cubicweb.edition.js')
         self._cw.add_css('cubicweb.form.css')
+        self._cw.add_js('cubicweb.reledit.js', 'cubicweb.edition.js')
         entity = self.cw_rset.get_entity(row, col)
         rschema = self._cw.vreg.schema[rtype]
+        self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
         reload = self._compute_reload(entity, rschema, role, reload)
         default_value = self._compute_default_value(entity, rschema, role, default_value)
         divid = self._build_divid(rtype, role, entity.eid)
         if rschema.final:
-            self._handle_attributes(entity, rschema, role, divid, reload, default_value)
+            self._handle_attribute(entity, rschema, role, divid, reload, default_value)
         else:
-            self._handle_relations(entity, rschema, role, divid, reload, default_value, formid)
+            if self._is_composite():
+                self._handle_composite(entity, rschema, role, divid, reload, default_value, formid)
+            else:
+                self._handle_relation(entity, rschema, role, divid, reload, default_value, formid)
 
-    def _handle_attributes(self, entity, rschema, role, divid, reload, default_value):
+    def _handle_attribute(self, entity, rschema, role, divid, reload, default_value):
         rtype = rschema.type
         value = entity.printable_value(rtype)
-        form, renderer = self._build_form(entity, rtype, role, divid, 'base',
-                                          default_value, reload)
-        if not self._should_edit_attribute(entity, rschema, form):
+        if not self._should_edit_attribute(entity, rschema):
             self.w(value)
             return
+
+        display_label, related_entity = self._prepare_form(entity, rtype, role)
+        form, renderer = self._build_form(entity, rtype, role, divid, 'base', default_value,
+                                          reload, display_label, related_entity)
         value = value or default_value
-        field = form.field_by_name(rtype, role, entity.e_schema)
-        form.append_field(field)
         self.view_form(divid, value, form, renderer)
 
-    def _handle_relations(self, entity, rschema, role, divid, reload, default_value, formid):
-        rtype = rschema.type
-        rvid = self._compute_best_vid(entity.e_schema, rschema, role)
-        related_rset = entity.related(rtype, role)
+    def _compute_formid_value(self, entity, rschema, role, default_value, rvid, formid):
+        related_rset = entity.related(rschema.type, role)
         if related_rset:
             value = self._cw.view(rvid, related_rset)
         else:
             value = default_value
-        ttypes = self._compute_ttypes(rschema, role)
+        if not self._should_edit_relation(entity, rschema, role):
+            return None, value
+        return formid, value
+
+    def _handle_relation(self, entity, rschema, role, divid, reload, default_value, formid):
+        rvid = self._compute_best_vid(entity.e_schema, rschema, role)
+        formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid)
+        if formid is None:
+            return self.w(value)
 
-        if not self._should_edit_relation(entity, rschema, role):
-            self.w(value)
-            return
-        # this is for attribute-like composites (1 target type, 1 related entity at most)
+        rtype = rschema.type
+        display_label, related_entity = self._prepare_form(entity, rtype, role)
+        form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload,
+                                          display_label, related_entity, dict(vid=rvid))
+        self.view_form(divid, value, form, renderer)
+
+    def _handle_composite(self, entity, rschema, role, divid, reload, default_value, formid):
+        # this is for attribute-like composites (1 target type, 1 related entity at most, for now)
+        ttypes = self._compute_ttypes(rschema, role)
+        related_rset = entity.related(rschema.type, role)
         add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes)
         edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes)
         delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role)
-        # compute formid
-        if len(ttypes) > 1: # redundant safety belt
-            formid = 'base'
-        else:
-            afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0])
-            # is there an afs spec that says we should edit
-            # the rschema as an attribute ?
-            if afs and 'main_attributes' in afs:
-                formid = 'base'
+
+        rvid = self._compute_best_vid(entity.e_schema, rschema, role)
+        formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid)
+        if formid is None or not (edit_related or add_related):
+            # till we learn to handle cases where not (edit_related or add_related)
+            self.w(value)
+            return
 
-        form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value,
-                                          reload, dict(vid=rvid),
-                                          edit_related, add_related and ttypes[0])
-        if formid == 'base':
-            field = form.field_by_name(rtype, role, entity.e_schema)
-            form.append_field(field)
-        self.view_form(divid, value, form, renderer, edit_related,
-                       delete_related, add_related)
-
+        rtype = rschema.type
+        ttype = ttypes[0]
+        _fdata = self._prepare_composite_form(entity, rtype, role, edit_related, add_related and ttype)
+        display_label, related_entity = _fdata
+        form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload,
+                                          display_label, related_entity, dict(vid=rvid))
+        self.view_form(divid, value, form, renderer,
+                       edit_related, add_related, delete_related)
 
     def _compute_best_vid(self, eschema, rschema, role):
+        rvid = self._one_rvid
         if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
-            return self._many_rvid
-        return self._one_rvid
+            rvid = self._many_rvid
+        return self._rules.get('rvid', rvid)
 
     def _compute_ttypes(self, rschema, role):
         dual_role = neg_role(role)
         return getattr(rschema, '%ss' % dual_role)()
 
     def _compute_reload(self, entity, rschema, role, reload):
-        rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
-        ctrl_reload = rule.get('reload', reload)
+        ctrl_reload = self._rules.get('reload', reload)
         if callable(ctrl_reload):
             ctrl_reload = ctrl_reload(entity)
         if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False
@@ -160,8 +173,7 @@
 
     def _compute_default_value(self, entity, rschema, role, default_value):
         etype = entity.e_schema.type
-        rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*')
-        ctrl_default = rule.get('default_value', default_value)
+        ctrl_default = self._rules.get('default_value', default_value)
         if ctrl_default:
             return ctrl_default
         if default_value is None:
@@ -169,47 +181,42 @@
                               display_name(self._cw, rschema.type, role))
         return default_value
 
-    def _is_composite(self, eschema, rschema, role):
-        return eschema.rdef(rschema, role).composite == role
+    def _is_composite(self):
+        return self._rules.get('edit_target') == 'related'
 
     def _may_add_related(self, related_rset, entity, rschema, role, ttypes):
         """ ok for attribute-like composite entities """
-        if self._is_composite(entity.e_schema, rschema, role):
-            if len(ttypes) > 1: # wrong cardinality: do not handle
-                return False
-            rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role)
-            card = rdef.role_cardinality(role)
-            if related_rset and card in '?1':
-                return False
-            if role == 'subject':
-                kwargs = {'fromeid': entity.eid}
-            else:
-                kwargs = {'toeid': entity.eid}
-            if rdef.has_perm(self._cw, 'add', **kwargs):
-                return True
-        return False
+        if len(ttypes) > 1: # many etypes: learn how to do it
+            return False
+        rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role)
+        card = rdef.role_cardinality(role)
+        if related_rset or card not in '?1':
+            return False
+        if role == 'subject':
+            kwargs = {'fromeid': entity.eid}
+        else:
+            kwargs = {'toeid': entity.eid}
+        return rdef.has_perm(self._cw, 'add', **kwargs)
 
     def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes):
         """ controls the edition of the related entity """
-        if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1':
-            return False
-        if len(related_rset.rows) != 1:
+        if len(ttypes) > 1 or len(related_rset.rows) != 1:
             return False
-        if len(ttypes) > 1:
-            return False
-        if not self._is_composite(entity.e_schema, rschema, role):
+        if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1':
             return False
         return related_rset.get_entity(0, 0).cw_has_perm('update')
 
     def _may_delete_related(self, related_rset, entity, rschema, role):
-        # we assume may_edit_related
-        kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
-        if not rschema.has_perm(self._cw, 'delete', **kwargs):
+        # we assume may_edit_related, only 1 related entity
+        if not related_rset:
             return False
-        for related_entity in related_rset.entities():
-            if not related_entity.cw_has_perm('delete'):
-                return False
-        return True
+        rentity = related_rset.get_entity(0, 0)
+        if role == 'subject':
+            kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid}
+        else:
+            kwargs = {'fromeid': rentity.eid, 'toeid': entity.eid}
+        # NOTE: should be sufficient given a well built schema/security
+        return rschema.has_perm(self._cw, 'delete', **kwargs)
 
     def _build_edit_zone(self):
         return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))}
@@ -234,32 +241,43 @@
             event_args.update(extradata)
         return event_args
 
-    def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
-                    extradata=None, edit_related=False, add_related=False, **formargs):
-        event_args = self._build_args(entity, rtype, role, formid, default_value,
-                                      reload, extradata)
-        cancelclick = self._cancelclick % divid
+    def _prepare_form(self, entity, _rtype, role):
+        display_label = False
+        related_entity = entity
+        return display_label, related_entity
+
+    def _prepare_composite_form(self, entity, rtype, role, edit_related, add_related):
         if edit_related and not add_related:
-            display_fields = None
             display_label = True
             related_entity = entity.related(rtype, role).get_entity(0, 0)
-            self._cw.form['eid'] = related_entity.eid
         elif add_related:
-            display_fields = None
             display_label = True
             _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw)
             _new_entity.eid = self._cw.varmaker.next()
             related_entity = _new_entity
+            # XXX see forms.py ~ 276 and entities.linked_to method
+            #     is there another way ?
             self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role))
-        else: # base case: edition/attribute relation
-            display_fields = [(rtype, role)]
-            display_label = False
-            related_entity = entity
+        return display_label, related_entity
+
+    def _build_renderer(self, related_entity, display_label):
+        return self._cw.vreg['formrenderers'].select(
+            self._form_renderer_id, self._cw, entity=related_entity,
+            display_label=display_label,
+            table_class='attributeForm' if display_label else '',
+            display_help=False, button_bar_class='buttonbar',
+            display_progress_div=False)
+
+    def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
+                    display_label, related_entity, extradata=None, **formargs):
+        event_args = self._build_args(entity, rtype, role, formid, default_value,
+                                      reload, extradata)
+        cancelclick = self._cancelclick % divid
         form = self._cw.vreg['forms'].select(
-            formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid,
-            display_fields=display_fields, formtype='inlined',
-            action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'),
-            cwtarget='eformframe', cssstyle='display: none',
+            formid, self._cw, rset=related_entity.as_rset(), entity=related_entity,
+            domid='%s-form' % divid, formtype='inlined',
+            action=self._cw.build_url('validateform', __onsuccess='window.parent.cw.reledit.onSuccess'),
+            cwtarget='eformframe', cssclass='releditForm',
             **formargs)
         # pass reledit arguments
         for pname, pvalue in event_args.iteritems():
@@ -279,53 +297,37 @@
             form.form_buttons = [SubmitButton(),
                                  Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)]
         form.event_args = event_args
-        renderer = self._cw.vreg['formrenderers'].select(
-            'base', self._cw, entity=related_entity, display_label=display_label,
-            display_help=False, table_class='',
-            button_bar_class='buttonbar', display_progress_div=False)
-        return form, renderer
+        if formid == 'base':
+            field = form.field_by_name(rtype, role, entity.e_schema)
+            form.append_field(field)
+        return form, self._build_renderer(related_entity, display_label)
 
-    def _should_edit_attribute(self, entity, rschema, form):
-        # examine rtags
-        noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False)
-        if noedit:
-            return False
+    def _should_edit_attribute(self, entity, rschema):
         rdef = entity.e_schema.rdef(rschema)
-        afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object)
-        if 'main_hidden' in  afs:
-            return False
         # check permissions
         if not entity.cw_has_perm('update'):
             return False
         rdef = entity.e_schema.rdef(rschema)
-        if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
-            return False
-        # XXX ?
-        try:
-            form.field_by_name(str(rschema), 'subject', entity.e_schema)
-        except FieldNotFound:
-            return False
-        return True
+        return rdef.has_perm(self._cw, 'update', eid=entity.eid)
+
+    should_edit_attributes = deprecated('[3.9] should_edit_attributes is deprecated,'
+                                        ' use _should_edit_attribute instead',
+                                        _should_edit_attribute)
 
     def _should_edit_relation(self, entity, rschema, role):
-        # examine rtags
-        rtype = rschema.type
-        noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False)
-        if noedit:
-            return False
-        rdef = entity.e_schema.rdef(rschema, role)
-        afs = uicfg.autoform_section.etype_get(
-            entity.__regid__, rschema, role, rdef.object)
-        if 'main_hidden' in afs:
-            return False
-        perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
+        eeid = entity.eid
+        perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid}
         return rschema.has_perm(self._cw, 'add', **perm_args)
 
-    def view_form(self, divid, value, form=None, renderer=None,
-                  edit_related=False, delete_related=False, add_related=False):
+    should_edit_relations = deprecated('[3.9] should_edit_relations is deprecated,'
+                                       ' use _should_edit_relation instead',
+                                       _should_edit_relation)
+
+    def _open_form_wrapper(self, divid, value, form, renderer,
+                           _edit_related, _add_related, _delete_related):
         w = self.w
-        w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' %
-          {'id': divid,
+        w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s" class="%(css)s">' %
+          {'id': divid, 'css': 'releditField',
            'out': "jQuery('#%s').addClass('hidden')" % divid,
            'over': "jQuery('#%s').removeClass('hidden')" % divid})
         w(u'<div id="%s-value" class="editableFieldValue">' % divid)
@@ -333,33 +335,54 @@
         w(u'</div>')
         w(form.render(renderer=renderer))
         w(u'<div id="%s" class="editableField hidden">' % divid)
-        args = form.event_args.copy()
-        if not add_related: # excludes edition
-            args['formid'] = 'edition'
+
+    def _edit_action(self, divid, args, edit_related, add_related, _delete_related):
+        if not add_related: # currently, excludes edition
+            w = self.w
+            args['formid'] = 'edition' if edit_related else 'base'
             w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' %
               (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg)))
             w(self._build_edit_zone())
             w(u'</div>')
-        else:
-            args['formid'] = 'edition'
+
+    def _add_action(self, divid, args, _edit_related, add_related, _delete_related):
+        if add_related:
+            w = self.w
+            args['formid'] = 'edition' if add_related else 'base'
             w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
               (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg)))
             w(self._build_add_zone())
             w(u'</div>')
+
+    def _del_action(self, divid, args, _edit_related, _add_related, delete_related):
         if delete_related:
+            w = self.w
             args['formid'] = 'deleteconf'
             w(u'<div id="%s-delete" class="editableField" onclick="%s" title="%s">' %
               (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg)))
             w(self._build_delete_zone())
             w(u'</div>')
-        w(u'</div>')
-        w(u'</div>')
+
+    def _close_form_wrapper(self):
+        self.w(u'</div>')
+        self.w(u'</div>')
+
+    def view_form(self, divid, value, form=None, renderer=None,
+                  edit_related=False, add_related=False, delete_related=False):
+        self._open_form_wrapper(divid, value, form, renderer,
+                                edit_related, add_related, delete_related)
+        args = form.event_args.copy()
+        self._edit_action(divid, args, edit_related, add_related, delete_related)
+        self._add_action(divid, args, edit_related, add_related, delete_related)
+        self._del_action(divid, args, edit_related, add_related, delete_related)
+        self._close_form_wrapper()
+
 
 class AutoClickAndEditFormView(ClickAndEditFormView):
     __regid__ = 'reledit'
 
     def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
-                  extradata=None, edit_related=False, add_related=False, **formargs):
+                    display_label, related_entity, extradata=None, **formargs):
         event_args = self._build_args(entity, rtype, role, 'base', default_value,
                                       reload, extradata)
         form = _DummyForm()
--- a/web/views/tableview.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/tableview.py	Thu Sep 16 18:56:35 2010 +0200
@@ -140,11 +140,11 @@
         if mainindex is None:
             displayfilter, displayactions = False, False
         else:
-            if displayfilter is None and 'displayfilter' in req.form:
+            if displayfilter is None and req.form.get('displayfilter'):
                 displayfilter = True
                 if req.form['displayfilter'] == 'shown':
                     hidden = False
-            if displayactions is None and 'displayactions' in req.form:
+            if displayactions is None and req.form.get('displayactions'):
                 displayactions = True
         displaycols = self.displaycols(displaycols, headers)
         fromformfilter = 'fromformfilter' in req.form
@@ -187,14 +187,9 @@
 
     def page_navigation_url(self, navcomp, path, params):
         if hasattr(self, 'divid'):
-            divid = self.divid
-        else:
-            divid = params.get('divid', 'pageContent'),
-        rql = params.pop('rql', self.cw_rset.printable_rql())
-        # latest 'true' used for 'swap' mode
-        return 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
-            json_dumps(divid), json_dumps(rql), json_dumps(self.__regid__),
-            json_dumps(params))
+            params['divid'] = self.divid
+        params['vid'] = self.__regid__
+        return navcomp.ajax_page_url(**params)
 
     def show_hide_actions(self, divid, currentlydisplayed=False):
         showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
@@ -324,7 +319,8 @@
     title = None
 
     def call(self, title=None, subvid=None, headers=None, divid=None,
-             paginate=False, displaycols=None, displayactions=None, mainindex=None):
+             paginate=False, displaycols=None, displayactions=None,
+             mainindex=None):
         """Dumps a table displaying a composite query"""
         try:
             actrql = self._cw.form['actualrql']
@@ -347,7 +343,8 @@
             mainindex = self.main_var_index()
         if mainindex is not None:
             actions = self.form_filter(divid, displaycols, displayactions,
-                                       displayfilter=True, paginate=paginate, hidden=True)
+                                       displayfilter=True, paginate=paginate,
+                                       hidden=True)
         else:
             actions = ()
         if not subvid and 'subvid' in self._cw.form:
--- a/web/views/urlpublishing.py	Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/urlpublishing.py	Thu Sep 16 18:56:35 2010 +0200
@@ -176,18 +176,27 @@
             else:
                 attrname = cls._rest_attr_info()[0]
             value = req.url_unquote(parts.pop(0))
-            rset = self.attr_rset(req, etype, attrname, value)
-        else:
-            rset = self.cls_rset(req, cls)
+            return self.handle_etype_attr(req, cls, attrname, value)
+        return self.handle_etype(req, cls)
+
+    def set_vid_for_rset(self, req, cls, rset):# cls is there to ease overriding
         if rset.rowcount == 0:
             raise NotFound()
+        # we've to set a default vid here, since vid_from_rset may try to use a
+        # table view if fetch_rql include some non final relation
+        if rset.rowcount == 1:
+            req.form.setdefault('vid', 'primary')
+        else: # rset.rowcount >= 1
+            req.form.setdefault('vid', 'sameetypelist')
+
+    def handle_etype(self, req, cls):
+        rset = req.execute(cls.fetch_rql(req.user))
+        self.set_vid_for_rset(req, cls, rset)
         return None, rset
 
-    def cls_rset(self, req, cls):
-        return req.execute(cls.fetch_rql(req.user))
-
-    def attr_rset(self, req, etype, attrname, value):
-        rql = u'Any X WHERE X is %s, X %s %%(x)s' % (etype, attrname)
+    def handle_etype_attr(self, req, cls, attrname, value):
+        rql = cls.fetch_rql(req.user, ['X %s %%(x)s' % (attrname)],
+                            mainvar='X', ordermethod=None)
         if attrname == 'eid':
             try:
                 rset = req.execute(rql, {'x': typed_eid(value)})
@@ -196,7 +205,8 @@
                 raise PathDontMatch()
         else:
             rset = req.execute(rql, {'x': value})
-        return rset
+        self.set_vid_for_rset(req, cls, rset)
+        return None, rset
 
 
 class URLRewriteEvaluator(URLPathEvaluator):