merge stable
authorAlexandre Fayolle <alexandre.fayolle@logilab.fr>
Fri, 09 Apr 2010 15:01:25 +0000
branchstable
changeset 5210 1c635a91e403
parent 5209 52c9d25101a1 (current diff)
parent 5194 395f076512a1 (diff)
child 5211 8fac9cd3c510
merge
devtools/__init__.py
--- a/devtools/__init__.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/devtools/__init__.py	Fri Apr 09 15:01:25 2010 +0000
@@ -158,6 +158,9 @@
         sources = super(TestServerConfiguration, self).sources()
         if not sources:
             sources = DEFAULT_SOURCES
+        if sources['system']['db-driver'] == 'sqlite':
+            # we need an abspath in case tests are changing the cwd
+            sources['system']['db-name'] = abspath(sources['system']['db-name'])
         return sources
 
 
--- a/doc/book/en/.static/sphinx-default.css	Fri Apr 09 15:01:14 2010 +0000
+++ b/doc/book/en/.static/sphinx-default.css	Fri Apr 09 15:01:25 2010 +0000
@@ -3,7 +3,7 @@
  */
 
 html, body {
-    background: white;	
+    background: white;
 }
 
 body {
@@ -115,7 +115,7 @@
 }
 
 div.sphinxsidebar h3 {
-    font-family: 'Verdanda', sans-serif;
+    font-family: Verdana, sans-serif;
     color: black;
     font-size: 1.2em;
     font-weight: normal;
@@ -126,7 +126,7 @@
 }
 
 div.sphinxsidebar h4 {
-    font-family: 'Verdana', sans-serif;
+    font-family: Verdana, sans-serif;
     color: black;
     font-size: 1.1em;
     font-weight: normal;
--- a/doc/book/en/annexes/rql/intro.rst	Fri Apr 09 15:01:14 2010 +0000
+++ b/doc/book/en/annexes/rql/intro.rst	Fri Apr 09 15:01:25 2010 +0000
@@ -5,10 +5,10 @@
 Goals of RQL
 ~~~~~~~~~~~~
 
-The goal is to have a language emphasizing the way of browsing relations. As
-such, attributes will be regarded as cases of special relations (in terms of
-implementation, the user should see no difference between an attribute and a
-relation).
+The goal is to have a language making relations browsing easy. As
+such, attributes will be regarded as cases of special relations (in
+terms of usage, the user should see no syntactic difference between an
+attribute and a relation).
 
 Comparison with existing languages
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -32,10 +32,10 @@
 
 We should look in more detail, but here are already some ideas for the moment
 ... Versa_ is the language most similar to what we wanted to do, but the model
-underlying data being RDF, there is some number of things such as namespaces or
+underlying data being RDF, there are some things such as namespaces or
 handling of the RDF types which does not interest us. On the functionality
 level, Versa_ is very comprehensive including through many functions of
-conversion and basic types manipulation, which may need to be guided at one time
+conversion and basic types manipulation, which we may want to look at one time
 or another.  Finally, the syntax is a little esoteric.
 
 
--- a/doc/book/en/development/devcore/dbapi.rst	Fri Apr 09 15:01:14 2010 +0000
+++ b/doc/book/en/development/devcore/dbapi.rst	Fri Apr 09 15:01:25 2010 +0000
@@ -5,9 +5,10 @@
 
 The Python API developped to interface with RQL is inspired from the standard db-api,
 with a Connection object having the methods cursor, rollback and commit essentially.
-The most important method is the `execute` method of a cursor :
+The most important method is the `execute` method of a cursor.
 
-`execute(rqlstring, args=None, cachekey=None, build_descr=True)`
+.. sourcecode:: python
+  execute(rqlstring, args=None, cachekey=None, build_descr=True)
 
 :rqlstring: the RQL query to execute (unicode)
 :args: if the query contains substitutions, a dictionary containing the values to use
@@ -18,10 +19,11 @@
    through this argument
 
 
-The `Connection` object owns the methods `commit` and `rollback`. You *should
-never need to use them* during the development of the web interface based on
-the *CubicWeb* framework as it determines the end of the transaction depending
-on the query execution success.
+The `Connection` object owns the methods `commit` and `rollback`. You
+*should never need to use them* during the development of the web
+interface based on the *CubicWeb* framework as it determines the end
+of the transaction depending on the query execution success. They are
+however useful in other contexts such as tests.
 
 .. note::
   While executing update queries (SET, INSERT, DELETE), if a query generates
@@ -30,6 +32,7 @@
 
 Executing RQL queries from a view or a hook
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 When you're within code of the web interface, the db-api like connexion is
 handled by the request object. You should not have to access it directly, but
 use the `execute` method directly available on the request, eg:
@@ -50,33 +53,30 @@
 
    self._cw.execute('Any T WHERE T in_conf C, C eid %s' % entity.eid)
 
-But it can also be written in a syntax that will benefit from the use
+But it must be written in a syntax that will benefit from the use
 of a cache on the RQL server side:
 
 .. sourcecode:: python
 
-   self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid}, 'x')
+   self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid})
 
-Beside proper usage of the `args` argument, notice the latest argument: this is what's called
-the cache key. The cache key should be either a string or a tuple containing the names of keys
-in args which are referencing eids. *YOU MUST SET THIS PROPERLY* if you don't want weird result
-on queries which have ambigous solutions deambiguified by specifing an eid. So the good habit is:
-*always put in the cache key all eid keys*.
+The syntax tree is built once for the "generic" RQL and can be re-used
+with a number of different eids. There rql IN operator is an exception
+to this rule.
 
-The syntax tree is build once for the "generic" RQL and can be re-used
-with a number of different eid.
+.. sourcecode:: python
+
+   self._cw.execute('Any T WHERE T in_conf C, C name IN (%s)' % ','.join(['foo', 'bar']))
 
-Alternativelly, some of the common data related to an entity can be obtained from
-the top-level `entity.related()` method (which is used under the hood by the orm
-when you use attribute access notation on an entity to get a relation. The above
-would then be translated to:
+Alternativelly, some of the common data related to an entity can be
+obtained from the `entity.related()` method (which is used under the
+hood by the orm when you use attribute access notation on an entity to
+get a relation. The initial request would then be translated to:
 
 .. sourcecode:: python
 
    entity.related('in_conf', 'object')
 
-The `related()` method, as more generally others orm methods, makes extensive use
-of the cache mechanisms so you don't have to worry about them. Additionnaly this
-use will get you commonly used attributes that you will be able to use in your
-view generation without having to ask the data backend.
-
+Additionnaly this benefits from the fetch_attrs policy eventually
+defined on the class element, which says which attributes must be also
+loaded when the entity is loaded through the orm.
--- a/doc/book/en/development/devrepo/hooks.rst	Fri Apr 09 15:01:14 2010 +0000
+++ b/doc/book/en/development/devrepo/hooks.rst	Fri Apr 09 15:01:25 2010 +0000
@@ -2,31 +2,103 @@
 
 .. _hooks:
 
-Hooks
-=====
+Hooks and Operations
+====================
+
+Principles
+----------
 
-XXX FILLME
+Paraphrasing the `emacs`_ documentation, let us say that hooks are an
+important mechanism for customizing an application. A hook is
+basically a list of functions to be called on some well-defined
+occasion (This is called `running the hook`).
+
+.. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
+
+In CubicWeb, hooks are classes subclassing the Hook class in
+`server/hook.py`, implementing their own `call` method, and defined
+over pre-defined `events`.
+
+There are two families of events: data events and server events. In a
+typical application, most of the Hooks are defined over data
+events. There can be a lot of them.
 
-*Hooks* are executed before or after updating an entity or a relation in the
-repository.
+The purpose of data hooks is to complement the data model as defined
+in the schema.py, which is static by nature, with dynamic or value
+driven behaviours. It is functionally equivalent to a `database
+trigger`_, except that database triggers definitions languages are not
+standardized, hence not portable (for instance, PL/SQL works with
+Oracle and PostgreSQL but not SqlServer nor Sqlite).
+
+.. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
+
+Data hooks can serve the following purposes:
+
+* enforcing constraints that the static schema cannot express
+  (spanning several entities/relations, exotic cardinalities, etc.)
 
-Their prototypes are as follows:
+* implement computed attributes (an example could be the maintenance
+  of a relation representing the transitive closure of another relation)
+
+Operations are Hook-like objects that are created by Hooks and
+scheduled to happen just before (or after) the `commit` event. Hooks
+being fired immediately on data operations, it is sometime necessary
+to delay the actual work down to a time where all other Hooks have run
+and the application state converges towards consistency. Also while
+the order of execution of Hooks is data dependant (and thus hard to
+predict), it is possible to force an order on Operations.
+
+Events
+------
 
-    * after_add_entity     (session, entity)
-    * after_update_entity  (session, entity)
-    * after_delete_entity  (session, eid)
-    * before_add_entity    (session, entity)
-    * before_update_entity (session, entity)
-    * before_delete_entity (session, eid)
+Hooks are mostly defined and used to handle `dataflow`_ operations. It
+means as data gets in (mostly), specific events are issued and the
+Hooks matching these events are called.
+
+.. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow
+
+Below comes a list of the dataflow events related to entities operations:
+
+* before_add_entity
+
+* before_update_entity
+
+* before_delete_entity
+
+* after_add_entity
+
+* after_update_entity
+
+* after_delete_entity
+
+These define ENTTIES HOOKS. RELATIONS HOOKS are defined
+over the following events:
+
+* after_add_relation
 
-    * after_add_relation     (session, fromeid, rtype, toeid)
-    * after_delete_relation  (session, fromeid, rtype, toeid)
-    * before_add_relation    (session, fromeid, rtype, toeid)
-    * before_delete_relation (session, fromeid, rtype, toeid)
+* after_delete_relation
+
+* before_add_relation
+
+* before_delete_relation
+
+This is an occasion to remind us that relations support the add/delete
+operation, but no delete.
+
+Non data events also exist. These are called SYSTEM HOOKS.
+
+* server_startup
 
-    * server_startup
-    * server_shutdown
+* server_shutdown
+
+* server_maintenance
+
+* server_backup
 
-    * session_open
-    * session_close
+* server_restore
+
+* session_open
 
+* session_close
+
+
--- a/doc/book/en/development/devweb/js.rst	Fri Apr 09 15:01:14 2010 +0000
+++ b/doc/book/en/development/devweb/js.rst	Fri Apr 09 15:01:25 2010 +0000
@@ -5,7 +5,7 @@
 
 *CubicWeb* uses quite a bit of javascript in its user interface and
 ships with jquery (1.3.x) and parts of the jquery UI library, plus a
-number of homegrown files and also other thir party libraries.
+number of homegrown files and also other third party libraries.
 
 All javascript files are stored in cubicweb/web/data/. There are
 around thirty js files there. In a cube it goes to data/.
--- a/doc/book/en/development/testing.rst	Fri Apr 09 15:01:14 2010 +0000
+++ b/doc/book/en/development/testing.rst	Fri Apr 09 15:01:25 2010 +0000
@@ -24,6 +24,10 @@
 In most of the cases, you will inherit `EnvBasedTC` to write Unittest or
 functional tests for your entities, views, hooks, etc...
 
+XXX pytestconf.py & options (e.g --source to use a different db
+backend than sqlite)
+
+
 Managing connections or users
 +++++++++++++++++++++++++++++
 
@@ -33,10 +37,12 @@
 
 By default, tests run with a user with admin privileges. This
 user/connection must never be closed.
-qwq
-Before a self.login, one has to release the connection pool in use with a self.commit, self.rollback or self.close.
 
-When one is logged in as a normal user and wants to switch back to the admin user, one has to use self.restore_connection().
+Before a self.login, one has to release the connection pool in use
+with a self.commit, self.rollback or self.close.
+
+When one is logged in as a normal user and wants to switch back to the
+admin user, one has to use self.restore_connection().
 
 Usually it looks like this:
 
--- a/doc/book/en/intro/concepts.rst	Fri Apr 09 15:01:14 2010 +0000
+++ b/doc/book/en/intro/concepts.rst	Fri Apr 09 15:01:25 2010 +0000
@@ -339,5 +339,5 @@
 .. Note:
    RQL queries executed in hooks and operations are *unsafe* by default, e.g. the
    read and write security is deactivated unless explicitly asked.
-  
+
 .. |cubicweb| replace:: *CubicWeb*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.7.4_Any.py	Fri Apr 09 15:01:25 2010 +0000
@@ -0,0 +1,1 @@
+sync_schema_props_perms('TrInfo', syncprops=False)
--- a/schemas/bootstrap.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/schemas/bootstrap.py	Fri Apr 09 15:01:25 2010 +0000
@@ -311,8 +311,9 @@
 def post_build_callback(schema):
     """set attributes permissions for schema/workflow entities"""
     from cubicweb.schema import SCHEMA_TYPES, WORKFLOW_TYPES, META_RTYPES
+    wftypes = WORKFLOW_TYPES - set(('TrInfo',))
     for eschema in schema.entities():
-        if eschema in SCHEMA_TYPES or eschema in WORKFLOW_TYPES:
+        if eschema in SCHEMA_TYPES or eschema in wftypes:
             for rschema in eschema.subject_relations():
                 if rschema.final and not rschema in META_RTYPES:
                     rdef = eschema.rdef(rschema)
--- a/server/hook.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/server/hook.py	Fri Apr 09 15:01:25 2010 +0000
@@ -30,7 +30,6 @@
 
 Session hooks (eg session_open, session_close) have no special attribute.
 
-
 :organization: Logilab
 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
@@ -471,7 +470,7 @@
     try:
         session.transaction_data[datakey].add(value)
     except KeyError:
-        opcls(session, *opkwargs)
+        opcls(session, **opkwargs)
         session.transaction_data[datakey] = set((value,))
 
 
--- a/server/repository.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/server/repository.py	Fri Apr 09 15:01:25 2010 +0000
@@ -1104,7 +1104,7 @@
                                       eidfrom=entity.eid, rtype=attr, eidto=value)
                     if not only_inline_rels:
                         hm.call_hooks('before_update_entity', session, entity=entity)
-                source.update_entity(session, entity)
+            source.update_entity(session, entity)
             self.system_source.update_info(session, entity, need_fti_update)
             if source.should_call_hooks:
                 if not only_inline_rels:
--- a/server/session.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/server/session.py	Fri Apr 09 15:01:25 2010 +0000
@@ -946,6 +946,7 @@
 class InternalSession(Session):
     """special session created internaly by the repository"""
     is_internal_session = True
+    running_dbapi_query = False
 
     def __init__(self, repo, cnxprops=None):
         super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
--- a/server/sources/storages.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/server/sources/storages.py	Fri Apr 09 15:01:25 2010 +0000
@@ -85,9 +85,16 @@
 
     def entity_updated(self, entity, attr):
         """an entity using this storage for attr has been updatded"""
-        binary = entity.pop(attr)
-        fpath = self.current_fs_path(entity, attr)
-        UpdateFileOp(entity._cw, filepath=fpath, filedata=binary.getvalue())
+        if entity._cw.transaction_data.get('fs_importing'):
+            oldpath = self.current_fs_path(entity, attr)
+            fpath = entity[attr].getvalue()
+            if oldpath != fpath:
+                DeleteFileOp(entity._cw, filepath=oldpath)
+            binary = Binary(file(fpath).read())
+        else:
+            binary = entity.pop(attr)
+            fpath = self.current_fs_path(entity, attr)
+            UpdateFileOp(entity._cw, filepath=fpath, filedata=binary.getvalue())
         return binary
 
     def entity_deleted(self, entity, attr):
--- a/server/test/unittest_session.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/server/test/unittest_session.py	Fri Apr 09 15:01:25 2010 +0000
@@ -7,6 +7,7 @@
 """
 from logilab.common.testlib import TestCase, unittest_main, mock_object
 
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.session import _make_description
 
 class Variable:
@@ -32,5 +33,10 @@
         self.assertEquals(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
                           ['Int','CWUser'])
 
+class InternalSessionTC(CubicWebTC):
+    def test_dbapi_query(self):
+        session = self.repo.internal_session()
+        self.assertFalse(session.running_dbapi_query)
+
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_storage.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/server/test/unittest_storage.py	Fri Apr 09 15:01:25 2010 +0000
@@ -57,6 +57,11 @@
         return req.create_entity('File', data=Binary(content),
                                  data_format=u'text/plain', data_name=u'foo')
 
+    def fspath(self, entity):
+        fspath = self.execute('Any fspath(D) WHERE F eid %(f)s, F data D',
+                              {'f': entity.eid})[0][0]
+        return fspath.getvalue()
+
     def test_bfss_storage(self):
         f1 = self.create_file()
         expected_filepath = osp.join(self.tempdir, '%s_data' % f1.eid)
@@ -81,18 +86,14 @@
     def test_bfss_sqlite_fspath(self):
         f1 = self.create_file()
         expected_filepath = osp.join(self.tempdir, '%s_data' % f1.eid)
-        fspath = self.execute('Any fspath(D) WHERE F eid %(f)s, F data D',
-                              {'f': f1.eid})[0][0]
-        self.assertEquals(fspath.getvalue(), expected_filepath)
+        self.assertEquals(self.fspath(f1), expected_filepath)
 
     def test_bfss_fs_importing_doesnt_touch_path(self):
         self.session.transaction_data['fs_importing'] = True
         filepath = osp.abspath(__file__)
         f1 = self.session.create_entity('File', data=Binary(filepath),
                                         data_format=u'text/plain', data_name=u'foo')
-        fspath = self.execute('Any fspath(D) WHERE F eid %(f)s, F data D',
-                              {'f': f1.eid})[0][0]
-        self.assertEquals(fspath.getvalue(), filepath)
+        self.assertEquals(self.fspath(f1), filepath)
 
     def test_source_storage_transparency(self):
         with self.temporary_appobjects(DummyBeforeHook, DummyAfterHook):
@@ -180,5 +181,21 @@
         self.assertEquals(f2.data.getvalue(), 'some other data')
 
 
+    def test_bfss_update_with_fs_importing(self):
+        # use self.session to use server-side cache
+        f1 = self.session.create_entity('File', data=Binary('some data'),
+                                        data_format=u'text/plain', data_name=u'foo')
+        old_fspath = self.fspath(f1)
+        self.session.transaction_data['fs_importing'] = True
+        new_fspath = osp.join(self.tempdir, 'newfile.txt')
+        file(new_fspath, 'w').write('the new data')
+        self.execute('SET F data %(d)s WHERE F eid %(f)s',
+                     {'d': Binary(new_fspath), 'f': f1.eid})
+        self.commit()
+        self.assertEquals(f1.data.getvalue(), 'the new data')
+        self.assertEquals(self.fspath(f1), new_fspath)
+        self.failIf(osp.isfile(old_fspath))
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_schema.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/test/unittest_schema.py	Fri Apr 09 15:01:25 2010 +0000
@@ -241,6 +241,17 @@
         self.failUnless('has_text' in schema['CWUser'].subject_relations())
         self.failIf('has_text' in schema['EmailAddress'].subject_relations())
 
+    def test_permission_settings(self):
+        schema = loader.load(config)
+        aschema = schema['TrInfo'].rdef('comment')
+        self.assertEquals(aschema.get_groups('read'),
+                          set(('managers', 'users', 'guests')))
+        self.assertEquals(aschema.get_rqlexprs('read'),
+                          ())
+        self.assertEquals(aschema.get_groups('update'),
+                          set(('managers',)))
+        self.assertEquals([x.expression for x in aschema.get_rqlexprs('update')],
+                          ['U has_update_permission X'])
 
 class BadSchemaRQLExprTC(TestCase):
     def setUp(self):
@@ -257,16 +268,20 @@
         self.assertEquals(str(ex), msg)
 
     def test_rrqlexpr_on_etype(self):
-        self._test('rrqlexpr_on_eetype.py', "can't use RRQLExpression on ToTo, use an ERQLExpression")
+        self._test('rrqlexpr_on_eetype.py',
+                   "can't use RRQLExpression on ToTo, use an ERQLExpression")
 
     def test_erqlexpr_on_rtype(self):
-        self._test('erqlexpr_on_ertype.py', "can't use ERQLExpression on relation ToTo toto TuTu, use a RRQLExpression")
+        self._test('erqlexpr_on_ertype.py',
+                   "can't use ERQLExpression on relation ToTo toto TuTu, use a RRQLExpression")
 
     def test_rqlexpr_on_rtype_read(self):
-        self._test('rqlexpr_on_ertype_read.py', "can't use rql expression for read permission of relation ToTo toto TuTu")
+        self._test('rqlexpr_on_ertype_read.py',
+                   "can't use rql expression for read permission of relation ToTo toto TuTu")
 
     def test_rrqlexpr_on_attr(self):
-        self._test('rrqlexpr_on_attr.py', "can't use RRQLExpression on attribute ToTo.attr[String], use an ERQLExpression")
+        self._test('rrqlexpr_on_attr.py',
+                   "can't use RRQLExpression on attribute ToTo.attr[String], use an ERQLExpression")
 
 
 class NormalizeExpressionTC(TestCase):
@@ -277,8 +292,10 @@
 
 class RQLExpressionTC(TestCase):
     def test_comparison(self):
-        self.assertEquals(ERQLExpression('X is CWUser', 'X', 0), ERQLExpression('X is CWUser', 'X', 0))
-        self.assertNotEquals(ERQLExpression('X is CWUser', 'X', 0), ERQLExpression('X is CWGroup', 'X', 0))
+        self.assertEquals(ERQLExpression('X is CWUser', 'X', 0),
+                          ERQLExpression('X is CWUser', 'X', 0))
+        self.assertNotEquals(ERQLExpression('X is CWUser', 'X', 0),
+                             ERQLExpression('X is CWGroup', 'X', 0))
 
 class GuessRrqlExprMainVarsTC(TestCase):
     def test_exists(self):
--- a/web/_exceptions.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/web/_exceptions.py	Fri Apr 09 15:01:25 2010 +0000
@@ -53,8 +53,7 @@
     """raised when a json remote call fails
     """
     def __init__(self, reason=''):
-        #super(RequestError, self).__init__() # XXX require py >= 2.5
-        RequestError.__init__(self)
+        super(RequestError, self).__init__()
         self.reason = reason
 
     def dumps(self):
--- a/web/data/cubicweb.ajax.js	Fri Apr 09 15:01:14 2010 +0000
+++ b/web/data/cubicweb.ajax.js	Fri Apr 09 15:01:25 2010 +0000
@@ -240,6 +240,7 @@
     setProgressCursor();
     var props = {'fname' : fname, 'pageid' : pageid,
                  'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
+    // XXX we should inline the content of loadRemote here
     var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
     deferred = deferred.addErrback(remoteCallFailed);
     deferred = deferred.addErrback(resetCursor);
--- a/web/data/cubicweb.compat.js	Fri Apr 09 15:01:14 2010 +0000
+++ b/web/data/cubicweb.compat.js	Fri Apr 09 15:01:25 2010 +0000
@@ -6,22 +6,26 @@
     }
 }
 
+// XXX looks completely unused (candidate for removal)
 function getElementsByTagAndClassName(tag, klass, root) {
     root = root || document;
     // FIXME root is not used in this compat implementation
     return jQuery(tag + '.' + klass);
 }
 
+/* jQUery flattens arrays returned by the mapping function:
+   >>> y = ['a:b:c', 'd:e']
+   >>> jQuery.map(y, function(y) { return y.split(':');})
+   ["a", "b", "c", "d", "e"]
+   // where one would expect:
+   [ ["a", "b", "c"], ["d", "e"] ]
+   XXX why not the same argument order as $.map and forEach ?
+*/
 function map(func, array) {
-    // XXX jQUery tends to simplify lists with only one element :
-    // >>> y = ['a:b:c']
-    // >>> jQuery.map(y, function(y) { return y.split(':');})
-    // ["a", "b", "c"]
-    // where I would expect :
-    // [ ["a", "b", "c"] ]
-    // return jQuery.map(array, func);
     var result = [];
-    for (var i=0,length=array.length;i<length;i++) {
+    for (var i=0, length=array.length;
+         i<length;
+         i++) {
 	result.push(func(array[i]));
     }
     return result;
@@ -41,6 +45,7 @@
     jQuery(node).addClass(klass);
 }
 
+// XXX looks completely unused (candidate for removal)
 function toggleElementClass(node, klass) {
     jQuery(node).toggleClass(klass);
 }
@@ -281,26 +286,35 @@
 
 jQuery.extend(Deferred.prototype, {
     __init__: function() {
-	this.onSuccess = [];
-	this.onFailure = [];
-	this.req = null;
+	this._onSuccess = [];
+	this._onFailure = [];
+	this._req = null;
+        this._result = null;
+        this._error = null;
     },
 
     addCallback: function(callback) {
-	this.onSuccess.push([callback, sliceList(arguments, 1)]);
+        if (this._req.readyState == 4) {
+            if (this._result) { callback.apply(null, this._result, this._req); }
+        }
+        else { this._onSuccess.push([callback, sliceList(arguments, 1)]); }
 	return this;
     },
 
     addErrback: function(callback) {
-	this.onFailure.push([callback, sliceList(arguments, 1)]);
+        if (this._req.readyState == 4) {
+            if (this._error) { callback.apply(null, this._error, this._req); }
+        }
+        else { this._onFailure.push([callback, sliceList(arguments, 1)]); }
 	return this;
     },
 
     success: function(result) {
+        this._result = result;
 	try {
-	    for (var i=0; i<this.onSuccess.length; i++) {
-		var callback = this.onSuccess[i][0];
-		var args = merge([result, this.req], this.onSuccess[i][1]);
+	    for (var i=0; i<this._onSuccess.length; i++) {
+		var callback = this._onSuccess[i][0];
+		var args = merge([result, this._req], this._onSuccess[i][1]);
 		callback.apply(null, args);
 	    }
 	} catch (error) {
@@ -309,9 +323,10 @@
     },
 
     error: function(xhr, status, error) {
-	for (var i=0; i<this.onFailure.length; i++) {
-	    var callback = this.onFailure[i][0];
-	    var args = merge([error, this.req], this.onFailure[i][1]);
+        this._error = error;
+	for (var i=0; i<this._onFailure.length; i++) {
+	    var callback = this._onFailure[i][0];
+	    var args = merge([error, this._req], this._onFailure[i][1]);
 	    callback.apply(null, args);
 	}
     }
@@ -319,6 +334,46 @@
 });
 
 
+/*
+ * Asynchronously load an url and return a deferred
+ * whose callbacks args are decoded according to
+ * the Content-Type response header
+ */
+function loadRemote(url, data, reqtype) {
+    var d = new Deferred();
+    jQuery.ajax({
+	url: url,
+	type: reqtype,
+	data: data,
+
+	beforeSend: function(xhr) {
+	    d._req = xhr;
+	},
+
+	success: function(data, status) {
+            if (d._req.getResponseHeader("content-type") == 'application/json') {
+              data = evalJSON(data);
+            }
+	    d.success(data);
+	},
+
+	error: function(xhr, status, error) {
+          try {
+            if (xhr.status == 500) {
+                var reason_dict = evalJSON(xhr.responseText);
+                d.error(xhr, status, reason_dict['reason']);
+                return;
+            }
+          } catch(exc) {
+            log('error with server side error report:' + exc);
+          }
+          d.error(xhr, status, null);
+	}
+    });
+    return d;
+}
+
+
 /** @id MochiKit.DateTime.toISOTime */
 toISOTime = function (date, realISO/* = false */) {
     if (typeof(date) == "undefined" || date === null) {
@@ -366,36 +421,6 @@
 };
 
 
-/*
- * Asynchronously load an url and return a deferred
- * whose callbacks args are decoded according to
- * the Content-Type response header
- */
-function loadRemote(url, data, reqtype) {
-    var d = new Deferred();
-    jQuery.ajax({
-	url: url,
-	type: reqtype,
-	data: data,
-
-	beforeSend: function(xhr) {
-	    d.req = xhr;
-	},
-
-	success: function(data, status) {
-            if (d.req.getResponseHeader("content-type") == 'application/json') {
-              data = evalJSON(data);
-            }
-	    d.success(data);
-	},
-
-	error: function(xhr, status, error) {
-	    error = evalJSON(xhr.responseText);
-	    d.error(xhr, status, error['reason']);
-	}
-    });
-    return d;
-}
 
 /* depth-first implementation of the nodeWalk function found
  * in MochiKit.Base
--- a/web/data/cubicweb.python.js	Fri Apr 09 15:01:14 2010 +0000
+++ b/web/data/cubicweb.python.js	Fri Apr 09 15:01:25 2010 +0000
@@ -237,9 +237,9 @@
  * ['d', 'e', 'f']
  */
 function sliceList(lst, start, stop, step) {
-    var start = start || 0;
-    var stop = stop || lst.length;
-    var step = step || 1;
+    start = start || 0;
+    stop = stop || lst.length;
+    step = step || 1;
     if (stop < 0) {
 	stop = max(lst.length+stop, 0);
     }
@@ -256,6 +256,7 @@
 /* returns a partial func that calls a mehod on its argument
  * py-equiv: return lambda obj: getattr(obj, methname)(*args)
  */
+// XXX looks completely unused (candidate for removal)
 function methodcaller(methname) {
     var args = sliceList(arguments, 1);
     return function(obj) {
@@ -398,6 +399,7 @@
     jQuery(CubicWeb).trigger('server-response', [false, document]);
 });
 
+// XXX as of 2010-04-07, no known cube uses this
 jQuery(CubicWeb).bind('ajax-loaded', function() {
     log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
     jQuery(CubicWeb).trigger('server-response', [false, document]);
--- a/web/views/primary.py	Fri Apr 09 15:01:14 2010 +0000
+++ b/web/views/primary.py	Fri Apr 09 15:01:25 2010 +0000
@@ -115,11 +115,8 @@
         return u''
 
     def render_entity_attributes(self, entity, siderelations=None):
-        entity_attributes = self._section_def(entity, 'attributes')
-        if not entity_attributes:
-            return
-        self.w(u'<table>')
-        for rschema, tschemas, role, dispctrl in entity_attributes:
+        display_attributes = []
+        for rschema, _, role, dispctrl in self._section_def(entity, 'attributes'):
             vid = dispctrl.get('vid', 'reledit')
             if rschema.final or vid == 'reledit':
                 value = entity.view(vid, rtype=rschema.type, role=role)
@@ -129,16 +126,19 @@
                     value = self._cw.view(vid, rset)
                 else:
                     value = None
-            if self.skip_none and (value is None or value == ''):
-                continue
-            try:
-                self._render_attribute(dispctrl, rschema, value,
-                                       role=role, table=True)
-            except TypeError:
-                warn('[3.6] _render_attribute prototype has changed, '
-                     'please update %s' % self.__class___, DeprecationWarning)
-                self._render_attribute(rschema, value, role=role, table=True)
-        self.w(u'</table>')
+            if not self.skip_none or (value is not None and value != ''):
+                display_attributes.append( (rschema, role, dispctrl, value) )
+        if display_attributes:
+            self.w(u'<table>')
+            for rschema, role, dispctrl, value in display_attributes:
+                try:
+                    self._render_attribute(dispctrl, rschema, value,
+                                           role=role, table=True)
+                except TypeError:
+                    warn('[3.6] _render_attribute prototype has changed, please'
+                         ' update %s' % self.__class___, DeprecationWarning)
+                    self._render_attribute(rschema, value, role=role, table=True)
+            self.w(u'</table>')
 
     def render_entity_relations(self, entity, siderelations=None):
         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):