[ADD] port webclient RPC doc from web/doc
authorXavier Morel <xmo@openerp.com>
Fri, 12 Sep 2014 13:40:50 +0000 (15:40 +0200)
committerXavier Morel <xmo@openerp.com>
Tue, 7 Oct 2014 08:22:53 +0000 (10:22 +0200)
16 files changed:
addons/web/doc/async.rst [deleted file]
addons/web/doc/index.rst
addons/web/doc/rpc.rst [deleted file]
addons/web/doc/test-report.txt [deleted file]
addons/web/doc/testing.rst [deleted file]
addons/web/doc/web_controllers.rst [deleted file]
doc/conf.py
doc/reference/async.rst [new file with mode: 0644]
doc/reference/http.rst
doc/reference/images/runner.png [new file with mode: 0644]
doc/reference/images/runner2.png [new file with mode: 0644]
doc/reference/images/tests.png [new file with mode: 0644]
doc/reference/images/tests2.png [new file with mode: 0644]
doc/reference/images/tests3.png [new file with mode: 0644]
doc/reference/javascript.rst
doc/reference/test-report.txt [new file with mode: 0644]

diff --git a/addons/web/doc/async.rst b/addons/web/doc/async.rst
deleted file mode 100644 (file)
index 6782fda..0000000
+++ /dev/null
@@ -1,344 +0,0 @@
-Asynchronous Operations
-=======================
-
-As a language (and runtime), javascript is fundamentally
-single-threaded. This means any blocking request or computation will
-blocks the whole page (and, in older browsers, the software itself
-even preventing users from switching to an other tab): a javascript
-environment can be seen as an event-based runloop where application
-developers have no control over the runloop itself.
-
-As a result, performing long-running synchronous network requests or
-other types of complex and expensive accesses is frowned upon and
-asynchronous APIs are used instead.
-
-Asynchronous code rarely comes naturally, especially for developers
-used to synchronous server-side code (in Python, Java or C#) where the
-code will just block until the deed is gone. This is increased further
-when asynchronous programming is not a first-class concept and is
-instead implemented on top of callbacks-based programming, which is
-the case in javascript.
-
-The goal of this guide is to provide some tools to deal with
-asynchronous systems, and warn against systematic issues or dangers.
-
-Deferreds
----------
-
-Deferreds are a form of `promises`_. OpenERP Web currently uses
-`jQuery's deferred`_.
-
-The core idea of deferreds is that potentially asynchronous methods
-will return a :js:class:`Deferred` object instead of an arbitrary
-value or (most commonly) nothing.
-
-This object can then be used to track the end of the asynchronous
-operation by adding callbacks onto it, either success callbacks or
-error callbacks.
-
-A great advantage of deferreds over simply passing callback functions
-directly to asynchronous methods is the ability to :ref:`compose them
-<deferred-composition>`.
-
-Using deferreds
-~~~~~~~~~~~~~~~
-
-Deferreds's most important method is :js:func:`Deferred.then`. It is
-used to attach new callbacks to the deferred object.
-
-* the first parameter attaches a success callback, called when the
-  deferred object is successfully resolved and provided with the
-  resolved value(s) for the asynchronous operation.
-
-* the second parameter attaches a failure callback, called when the
-  deferred object is rejected and provided with rejection values
-  (often some sort of error message).
-
-Callbacks attached to deferreds are never "lost": if a callback is
-attached to an already resolved or rejected deferred, the callback
-will be called (or ignored) immediately. A deferred is also only ever
-resolved or rejected once, and is either resolved or rejected: a given
-deferred can not call a single success callback twice, or call both a
-success and a failure callbacks.
-
-:js:func:`~Deferred.then` should be the method you'll use most often
-when interacting with deferred objects (and thus asynchronous APIs).
-
-Building deferreds
-~~~~~~~~~~~~~~~~~~
-
-After using asynchronous APIs may come the time to build them: for
-`mocks`_, to compose deferreds from multiple source in a complex
-manner, in order to let the current operations repaint the screen or
-give other events the time to unfold, ...
-
-This is easy using jQuery's deferred objects.
-
-.. note:: this section is an implementation detail of jQuery Deferred
-          objects, the creation of promises is not part of any
-          standard (even tentative) that I know of. If you are using
-          deferred objects which are not jQuery's, their API may (and
-          often will) be completely different.
-
-Deferreds are created by invoking their constructor [#]_ without any
-argument. This creates a :js:class:`Deferred` instance object with the
-following methods:
-
-:js:func:`Deferred.resolve`
-
-    As its name indicates, this method moves the deferred to the
-    "Resolved" state. It can be provided as many arguments as
-    necessary, these arguments will be provided to any pending success
-    callback.
-
-:js:func:`Deferred.reject`
-
-    Similar to :js:func:`~Deferred.resolve`, but moves the deferred to
-    the "Rejected" state and calls pending failure handlers.
-
-:js:func:`Deferred.promise`
-
-    Creates a readonly view of the deferred object. It is generally a
-    good idea to return a promise view of the deferred to prevent
-    callers from resolving or rejecting the deferred in your stead.
-
-:js:func:`~Deferred.reject` and :js:func:`~Deferred.resolve` are used
-to inform callers that the asynchronous operation has failed (or
-succeeded). These methods should simply be called when the
-asynchronous operation has ended, to notify anybody interested in its
-result(s).
-
-.. _deferred-composition:
-
-Composing deferreds
-~~~~~~~~~~~~~~~~~~~
-
-What we've seen so far is pretty nice, but mostly doable by passing
-functions to other functions (well adding functions post-facto would
-probably be a chore... still, doable).
-
-Deferreds truly shine when code needs to compose asynchronous
-operations in some way or other, as they can be used as a basis for
-such composition.
-
-There are two main forms of compositions over deferred: multiplexing
-and piping/cascading.
-
-Deferred multiplexing
-`````````````````````
-
-The most common reason for multiplexing deferred is simply performing
-2+ asynchronous operations and wanting to wait until all of them are
-done before moving on (and executing more stuff).
-
-The jQuery multiplexing function for promises is :js:func:`when`.
-
-.. note:: the multiplexing behavior of jQuery's :js:func:`when` is an
-          (incompatible, mostly) extension of the behavior defined in
-          `CommonJS Promises/B`_.
-
-This function can take any number of promises [#]_ and will return a
-promise.
-
-This returned promise will be resolved when *all* multiplexed promises
-are resolved, and will be rejected as soon as one of the multiplexed
-promises is rejected (it behaves like Python's ``all()``, but with
-promise objects instead of boolean-ish).
-
-The resolved values of the various promises multiplexed via
-:js:func:`when` are mapped to the arguments of :js:func:`when`'s
-success callback, if they are needed. The resolved values of a promise
-are at the same index in the callback's arguments as the promise in
-the :js:func:`when` call so you will have:
-
-.. code-block:: javascript
-
-    $.when(p0, p1, p2, p3).then(
-            function (results0, results1, results2, results3) {
-        // code
-    });
-
-.. warning::
-
-    in a normal mapping, each parameter to the callback would be an
-    array: each promise is conceptually resolved with an array of 0..n
-    values and these values are passed to :js:func:`when`'s
-    callback. But jQuery treats deferreds resolving a single value
-    specially, and "unwraps" that value.
-
-    For instance, in the code block above if the index of each promise
-    is the number of values it resolves (0 to 3), ``results0`` is an
-    empty array, ``results2`` is an array of 2 elements (a pair) but
-    ``results1`` is the actual value resolved by ``p1``, not an array.
-
-Deferred chaining
-`````````````````
-
-A second useful composition is starting an asynchronous operation as
-the result of an other asynchronous operation, and wanting the result
-of both: with the tools described so far, handling e.g. OpenERP's
-search/read sequence with this would require something along the lines
-of:
-
-.. code-block:: javascript
-
-    var result = $.Deferred();
-    Model.search(condition).then(function (ids) {
-        Model.read(ids, fields).then(function (records) {
-            result.resolve(records);
-        });
-    });
-    return result.promise();
-
-While it doesn't look too bad for trivial code, this quickly gets
-unwieldy.
-
-But :js:func:`~Deferred.then` also allows handling this kind of
-chains: it returns a new promise object, not the one it was called
-with, and the return values of the callbacks is actually important to
-it: whichever callback is called,
-
-* If the callback is not set (not provided or left to null), the
-  resolution or rejection value(s) is simply forwarded to
-  :js:func:`~Deferred.then`'s promise (it's essentially a noop)
-
-* If the callback is set and does not return an observable object (a
-  deferred or a promise), the value it returns (``undefined`` if it
-  does not return anything) will replace the value it was given, e.g.
-
-  .. code-block:: javascript
-
-      promise.then(function () {
-          console.log('called');
-      });
-
-  will resolve with the sole value ``undefined``.
-
-* If the callback is set and returns an observable object, that object
-  will be the actual resolution (and result) of the pipe. This means a
-  resolved promise from the failure callback will resolve the pipe,
-  and a failure promise from the success callback will reject the
-  pipe.
-
-  This provides an easy way to chain operation successes, and the
-  previous piece of code can now be rewritten:
-
-  .. code-block:: javascript
-
-      return Model.search(condition).then(function (ids) {
-          return Model.read(ids, fields);
-      });
-
-  the result of the whole expression will encode failure if either
-  ``search`` or ``read`` fails (with the right rejection values), and
-  will be resolved with ``read``'s resolution values if the chain
-  executes correctly.
-
-:js:func:`~Deferred.then` is also useful to adapt third-party
-promise-based APIs, in order to filter their resolution value counts
-for instance (to take advantage of :js:func:`when` 's special
-treatment of single-value promises).
-
-
-jQuery.Deferred API
-~~~~~~~~~~~~~~~~~~~
-
-.. js:function:: when(deferreds…)
-
-    :param deferreds: deferred objects to multiplex
-    :returns: a multiplexed deferred
-    :rtype: :js:class:`Deferred`
-
-.. js:class:: Deferred
-
-    .. js:function:: Deferred.then(doneCallback[, failCallback])
-
-        Attaches new callbacks to the resolution or rejection of the
-        deferred object. Callbacks are executed in the order they are
-        attached to the deferred.
-
-        To provide only a failure callback, pass ``null`` as the
-        ``doneCallback``, to provide only a success callback the
-        second argument can just be ignored (and not passed at all).
-
-        Returns a new deferred which resolves to the result of the
-        corresponding callback, if a callback returns a deferred
-        itself that new deferred will be used as the resolution of the
-        chain.
-
-        :param doneCallback: function called when the deferred is resolved
-        :type doneCallback: Function
-        :param failCallback: function called when the deferred is rejected
-        :type failCallback: Function
-        :returns: the deferred object on which it was called
-        :rtype: :js:class:`Deferred`
-
-    .. js:function:: Deferred.done(doneCallback)
-
-        Attaches a new success callback to the deferred, shortcut for
-        ``deferred.then(doneCallback)``.
-
-        This is a jQuery extension to `CommonJS Promises/A`_ providing
-        little value over calling :js:func:`~Deferred.then` directly,
-        it should be avoided.
-
-        :param doneCallback: function called when the deferred is resolved
-        :type doneCallback: Function
-        :returns: the deferred object on which it was called
-        :rtype: :js:class:`Deferred`
-
-    .. js:function:: Deferred.fail(failCallback)
-
-        Attaches a new failure callback to the deferred, shortcut for
-        ``deferred.then(null, failCallback)``.
-
-        A second jQuery extension to `Promises/A <CommonJS
-        Promises/A>`_. Although it provides more value than
-        :js:func:`~Deferred.done`, it still is not much and should be
-        avoided as well.
-
-        :param failCallback: function called when the deferred is rejected
-        :type failCallback: Function
-        :returns: the deferred object on which it was called
-        :rtype: :js:class:`Deferred`
-
-    .. js:function:: Deferred.promise()
-
-        Returns a read-only view of the deferred object, with all
-        mutators (resolve and reject) methods removed.
-
-    .. js:function:: Deferred.resolve(value…)
-
-        Called to resolve a deferred, any value provided will be
-        passed onto the success handlers of the deferred object.
-
-        Resolving a deferred which has already been resolved or
-        rejected has no effect.
-
-    .. js:function:: Deferred.reject(value…)
-
-        Called to reject (fail) a deferred, any value provided will be
-        passed onto the failure handler of the deferred object.
-
-        Rejecting a deferred which has already been resolved or
-        rejected has no effect.
-
-.. [#] or simply calling :js:class:`Deferred` as a function, the
-       result is the same
-
-.. [#] or not-promises, the `CommonJS Promises/B`_ role of
-       :js:func:`when` is to be able to treat values and promises
-       uniformly: :js:func:`when` will pass promises through directly,
-       but non-promise values and objects will be transformed into a
-       resolved promise (resolving themselves with the value itself).
-
-       jQuery's :js:func:`when` keeps this behavior making deferreds
-       easy to build from "static" values, or allowing defensive code
-       where expected promises are wrapped in :js:func:`when` just in
-       case.
-
-.. _promises: http://en.wikipedia.org/wiki/Promise_(programming)
-.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/
-.. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A
-.. _CommonJS Promises/B: http://wiki.commonjs.org/wiki/Promises/B
-.. _mocks: http://en.wikipedia.org/wiki/Mock_object
index bad653e..5a862d2 100644 (file)
@@ -34,8 +34,6 @@ Javascript
     :maxdepth: 1
 
     guidelines
-    rpc
-    async
     client_action
     testing
 
diff --git a/addons/web/doc/rpc.rst b/addons/web/doc/rpc.rst
deleted file mode 100644 (file)
index d5fc414..0000000
+++ /dev/null
@@ -1,279 +0,0 @@
-RPC Calls
-=========
-
-Building static displays is all nice and good and allows for neat
-effects (and sometimes you're given data to display from third parties
-so you don't have to make any effort), but a point generally comes
-where you'll want to talk to the world and make some network requests.
-
-OpenERP Web provides two primary APIs to handle this, a low-level
-JSON-RPC based API communicating with the Python section of OpenERP
-Web (and of your addon, if you have a Python part) and a high-level
-API above that allowing your code to talk directly to the OpenERP
-server, using familiar-looking calls.
-
-All networking APIs are :doc:`asynchronous </async>`. As a result, all
-of them will return :js:class:`Deferred` objects (whether they resolve
-those with values or not). Understanding how those work before before
-moving on is probably necessary.
-
-High-level API: calling into OpenERP models
--------------------------------------------
-
-Access to OpenERP object methods (made available through XML-RPC from
-the server) is done via the :js:class:`openerp.web.Model` class. This
-class maps onto the OpenERP server objects via two primary methods,
-:js:func:`~openerp.web.Model.call` and
-:js:func:`~openerp.web.Model.query`.
-
-:js:func:`~openerp.web.Model.call` is a direct mapping to the
-corresponding method of the OpenERP server object. Its usage is
-similar to that of the OpenERP Model API, with three differences:
-
-* The interface is :doc:`asynchronous </async>`, so instead of
-  returning results directly RPC method calls will return
-  :js:class:`Deferred` instances, which will themselves resolve to the
-  result of the matching RPC call.
-
-* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to
-  ``__getattr__`` or ``method_missing``, there needs to be an explicit
-  method to dispatch RPC methods.
-
-* No notion of pooler, the model proxy is instantiated where needed,
-  not fetched from an other (somewhat global) object
-
-.. code-block:: javascript
-
-    var Users = new Model('res.users');
-
-    Users.call('change_password', ['oldpassword', 'newpassword'],
-                      {context: some_context}).then(function (result) {
-        // do something with change_password result
-    });
-
-:js:func:`~openerp.web.Model.query` is a shortcut for a builder-style
-interface to searches (``search`` + ``read`` in OpenERP RPC terms). It
-returns a :js:class:`~openerp.web.Query` object which is immutable but
-allows building new :js:class:`~openerp.web.Query` instances from the
-first one, adding new properties or modifiying the parent object's:
-
-.. code-block:: javascript
-
-    Users.query(['name', 'login', 'user_email', 'signature'])
-         .filter([['active', '=', true], ['company_id', '=', main_company]])
-         .limit(15)
-         .all().then(function (users) {
-        // do work with users records
-    });
-
-The query is only actually performed when calling one of the query
-serialization methods, :js:func:`~openerp.web.Query.all` and
-:js:func:`~openerp.web.Query.first`. These methods will perform a new
-RPC call every time they are called.
-
-For that reason, it's actually possible to keep "intermediate" queries
-around and use them differently/add new specifications on them.
-
-.. js:class:: openerp.web.Model(name)
-
-    .. js:attribute:: openerp.web.Model.name
-
-        name of the OpenERP model this object is bound to
-
-    .. js:function:: openerp.web.Model.call(method[, args][, kwargs])
-
-         Calls the ``method`` method of the current model, with the
-         provided positional and keyword arguments.
-
-         :param String method: method to call over rpc on the
-                               :js:attr:`~openerp.web.Model.name`
-         :param Array<> args: positional arguments to pass to the
-                              method, optional
-         :param Object<> kwargs: keyword arguments to pass to the
-                                 method, optional
-         :rtype: Deferred<>         
-
-    .. js:function:: openerp.web.Model.query(fields)
-
-         :param Array<String> fields: list of fields to fetch during
-                                      the search
-         :returns: a :js:class:`~openerp.web.Query` object
-                   representing the search to perform
-
-.. js:class:: openerp.web.Query(fields)
-
-    The first set of methods is the "fetching" methods. They perform
-    RPC queries using the internal data of the object they're called
-    on.
-
-    .. js:function:: openerp.web.Query.all()
-
-        Fetches the result of the current
-        :js:class:`~openerp.web.Query` object's search.
-
-        :rtype: Deferred<Array<>>
-
-    .. js:function:: openerp.web.Query.first()
-
-       Fetches the **first** result of the current
-       :js:class:`~openerp.web.Query`, or ``null`` if the current
-       :js:class:`~openerp.web.Query` does have any result.
-
-       :rtype: Deferred<Object | null>
-
-    .. js:function:: openerp.web.Query.count()
-
-       Fetches the number of records the current
-       :js:class:`~openerp.web.Query` would retrieve.
-
-       :rtype: Deferred<Number>
-
-    .. js:function:: openerp.web.Query.group_by(grouping...)
-
-       Fetches the groups for the query, using the first specified
-       grouping parameter
-
-       :param Array<String> grouping: Lists the levels of grouping
-                                      asked of the server. Grouping
-                                      can actually be an array or
-                                      varargs.
-       :rtype: Deferred<Array<openerp.web.QueryGroup>> | null
-
-    The second set of methods is the "mutator" methods, they create a
-    **new** :js:class:`~openerp.web.Query` object with the relevant
-    (internal) attribute either augmented or replaced.
-
-    .. js:function:: openerp.web.Query.context(ctx)
-
-       Adds the provided ``ctx`` to the query, on top of any existing
-       context
-
-    .. js:function:: openerp.web.Query.filter(domain)
-
-       Adds the provided domain to the query, this domain is
-       ``AND``-ed to the existing query domain.
-
-    .. js:function:: opeenrp.web.Query.offset(offset)
-
-       Sets the provided offset on the query. The new offset
-       *replaces* the old one.
-
-    .. js:function:: openerp.web.Query.limit(limit)
-
-       Sets the provided limit on the query. The new limit *replaces*
-       the old one.
-
-    .. js:function:: openerp.web.Query.order_by(fields…)
-
-       Overrides the model's natural order with the provided field
-       specifications. Behaves much like Django's `QuerySet.order_by
-       <https://docs.djangoproject.com/en/dev/ref/models/querysets/#order-by>`_:
-
-       * Takes 1..n field names, in order of most to least importance
-         (the first field is the first sorting key). Fields are
-         provided as strings.
-
-       * A field specifies an ascending order, unless it is prefixed
-         with the minus sign "``-``" in which case the field is used
-         in the descending order
-
-       Divergences from Django's sorting include a lack of random sort
-       (``?`` field) and the inability to "drill down" into relations
-       for sorting.
-
-Aggregation (grouping)
-~~~~~~~~~~~~~~~~~~~~~~
-
-OpenERP has powerful grouping capacities, but they are kind-of strange
-in that they're recursive, and level n+1 relies on data provided
-directly by the grouping at level n. As a result, while ``read_group``
-works it's not a very intuitive API.
-
-OpenERP Web 7.0 eschews direct calls to ``read_group`` in favor of
-calling a method of :js:class:`~openerp.web.Query`, `much in the way
-it is one in SQLAlchemy
-<http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.group_by>`_ [#]_:
-
-.. code-block:: javascript
-
-    some_query.group_by(['field1', 'field2']).then(function (groups) {
-        // do things with the fetched groups
-    });
-
-This method is asynchronous when provided with 1..n fields (to group
-on) as argument, but it can also be called without any field (empty
-fields collection or nothing at all). In this case, instead of
-returning a Deferred object it will return ``null``.
-
-When grouping criterion come from a third-party and may or may not
-list fields (e.g. could be an empty list), this provides two ways to
-test the presence of actual subgroups (versus the need to perform a
-regular query for records):
-
-* A check on ``group_by``'s result and two completely separate code
-  paths
-
-  .. code-block:: javascript
-
-      var groups;
-      if (groups = some_query.group_by(gby)) {
-          groups.then(function (gs) {
-              // groups
-          });
-      }
-      // no groups
-
-* Or a more coherent code path using :js:func:`when`'s ability to
-  coerce values into deferreds:
-
-  .. code-block:: javascript
-
-      $.when(some_query.group_by(gby)).then(function (groups) {
-          if (!groups) {
-              // No grouping
-          } else {
-              // grouping, even if there are no groups (groups
-              // itself could be an empty array)
-          }
-      });
-
-The result of a (successful) :js:func:`~openerp.web.Query.group_by` is
-an array of :js:class:`~openerp.web.QueryGroup`.
-
-.. _rpc_rpc:
-
-Low-level API: RPC calls to Python side
----------------------------------------
-
-While the previous section is great for calling core OpenERP code
-(models code), it does not work if you want to call the Python side of
-OpenERP Web.
-
-For this, a lower-level API exists on on
-:js:class:`~openerp.web.Connection` objects (usually available through
-``openerp.connection``): the ``rpc`` method.
-
-This method simply takes an absolute path (which is the combination of
-the Python controller's ``_cp_path`` attribute and the name of the
-method you want to call) and a mapping of attributes to values (applied
-as keyword arguments on the Python method [#]_). This function fetches
-the return value of the Python methods, converted to JSON.
-
-For instance, to call the ``resequence`` of the
-:class:`~web.controllers.main.DataSet` controller:
-
-.. code-block:: javascript
-
-    openerp.connection.rpc('/web/dataset/resequence', {
-        model: some_model,
-        ids: array_of_ids,
-        offset: 42
-    }).then(function (result) {
-        // resequenced on server
-    });
-
-.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
-       is not terminal, it returns a query which can still be altered.
-
-.. [#] except for ``context``, which is extracted and stored in the
-       request object itself.
diff --git a/addons/web/doc/test-report.txt b/addons/web/doc/test-report.txt
deleted file mode 100644 (file)
index ce52618..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-test_empty_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
-test_ids_shortcut (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
-test_regular_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
-web.testing.stack: direct, value, success ... ok
-web.testing.stack: direct, deferred, success ... ok
-web.testing.stack: direct, value, error ... ok
-web.testing.stack: direct, deferred, failure ... ok
-web.testing.stack: successful setup ... ok
-web.testing.stack: successful teardown ... ok
-web.testing.stack: successful setup and teardown ... ok
-
-[snip ~150 lines]
-
-test_convert_complex_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_convert_complex_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_convert_literal_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_convert_literal_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_retrieve_nonliteral_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-test_retrieve_nonliteral_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
-
-----------------------------------------------------------------------
-Ran 181 tests in 15.706s
-
-OK
-
diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst
deleted file mode 100644 (file)
index a8f78bd..0000000
+++ /dev/null
@@ -1,697 +0,0 @@
-.. highlight:: javascript
-
-.. _testing:
-
-Testing in OpenERP Web
-======================
-
-Javascript Unit Testing
------------------------
-
-OpenERP Web 7.0 includes means to unit-test both the core code of
-OpenERP Web and your own javascript modules. On the javascript side,
-unit-testing is based on QUnit_ with a number of helpers and
-extensions for better integration with OpenERP.
-
-To see what the runner looks like, find (or start) an OpenERP server
-with the web client enabled, and navigate to ``/web/tests`` e.g. `on
-OpenERP's CI <http://trunk.runbot.openerp.com/web/tests>`_. This will
-show the runner selector, which lists all modules with javascript unit
-tests, and allows starting any of them (or all javascript tests in all
-modules at once).
-
-.. image:: ./images/runner.png
-    :align: center
-
-Clicking any runner button will launch the corresponding tests in the
-bundled QUnit_ runner:
-
-.. image:: ./images/tests.png
-    :align: center
-
-Writing a test case
--------------------
-
-The first step is to list the test file(s). This is done through the
-``test`` key of the openerp manifest, by adding javascript files to it
-(next to the usual YAML files, if any):
-
-.. code-block:: python
-
-    {
-        'name': "Demonstration of web/javascript tests",
-        'category': 'Hidden',
-        'depends': ['web'],
-        'test': ['static/test/demo.js'],
-    }
-
-and to create the corresponding test file(s)
-
-.. note::
-
-    Test files which do not exist will be ignored, if all test files
-    of a module are ignored (can not be found), the test runner will
-    consider that the module has no javascript tests.
-
-After that, refreshing the runner selector will display the new module
-and allow running all of its (0 so far) tests:
-
-.. image:: ./images/runner2.png
-    :align: center
-
-The next step is to create a test case::
-
-    openerp.testing.section('basic section', function (test) {
-        test('my first test', function () {
-            ok(false, "this test has run");
-        });
-    });
-
-All testing helpers and structures live in the ``openerp.testing``
-module. OpenERP tests live in a :js:func:`~openerp.testing.section`,
-which is itself part of a module. The first argument to a section is
-the name of the section, the second one is the section body.
-
-:js:func:`test <openerp.testing.case>`, provided by the
-:js:func:`~openerp.testing.section` to the callback, is used to
-register a given test case which will be run whenever the test runner
-actually does its job. OpenERP Web test case use standard `QUnit
-assertions`_ within them.
-
-Launching the test runner at this point will run the test and display
-the corresponding assertion message, with red colors indicating the
-test failed:
-
-.. image:: ./images/tests2.png
-    :align: center
-
-Fixing the test (by replacing ``false`` to ``true`` in the assertion)
-will make it pass:
-
-.. image:: ./images/tests3.png
-    :align: center
-
-Assertions
-----------
-
-As noted above, OpenERP Web's tests use `qunit assertions`_. They are
-available globally (so they can just be called without references to
-anything). The following list is available:
-
-.. js:function:: ok(state[, message])
-
-    checks that ``state`` is truthy (in the javascript sense)
-
-.. js:function:: strictEqual(actual, expected[, message])
-
-    checks that the actual (produced by a method being tested) and
-    expected values are identical (roughly equivalent to ``ok(actual
-    === expected, message)``)
-
-.. js:function:: notStrictEqual(actual, expected[, message])
-
-    checks that the actual and expected values are *not* identical
-    (roughly equivalent to ``ok(actual !== expected, message)``)
-
-.. js:function:: deepEqual(actual, expected[, message])
-
-    deep comparison between actual and expected: recurse into
-    containers (objects and arrays) to ensure that they have the same
-    keys/number of elements, and the values match.
-
-.. js:function:: notDeepEqual(actual, expected[, message])
-
-    inverse operation to :js:func:`deepEqual`
-
-.. js:function:: throws(block[, expected][, message])
-
-    checks that, when called, the ``block`` throws an
-    error. Optionally validates that error against ``expected``.
-
-    :param Function block:
-    :param expected: if a regexp, checks that the thrown error's
-                     message matches the regular expression. If an
-                     error type, checks that the thrown error is of
-                     that type.
-    :type expected: Error | RegExp
-
-.. js:function:: equal(actual, expected[, message])
-
-    checks that ``actual`` and ``expected`` are loosely equal, using
-    the ``==`` operator and its coercion rules.
-
-.. js:function:: notEqual(actual, expected[, message])
-
-    inverse operation to :js:func:`equal`
-
-Getting an OpenERP instance
----------------------------
-
-The OpenERP instance is the base through which most OpenERP Web
-modules behaviors (functions, objects, …) are accessed. As a result,
-the test framework automatically builds one, and loads the module
-being tested and all of its dependencies inside it. This new instance
-is provided as the first positional parameter to your test
-cases. Let's observe by adding javascript code (not test code) to the
-test module:
-
-.. code-block:: python
-
-    {
-        'name': "Demonstration of web/javascript tests",
-        'category': 'Hidden',
-        'depends': ['web'],
-        'js': ['static/src/js/demo.js'],
-        'test': ['static/test/demo.js'],
-    }
-
-::
-
-    // src/js/demo.js
-    openerp.web_tests_demo = function (instance) {
-        instance.web_tests_demo = {
-            value_true: true,
-            SomeType: instance.web.Class.extend({
-                init: function (value) {
-                    this.value = value;
-                }
-            })
-        };
-    };
-
-and then adding a new test case, which simply checks that the
-``instance`` contains all the expected stuff we created in the
-module::
-
-    // test/demo.js
-    test('module content', function (instance) {
-        ok(instance.web_tests_demo.value_true, "should have a true value");
-        var type_instance = new instance.web_tests_demo.SomeType(42);
-        strictEqual(type_instance.value, 42, "should have provided value");
-    });
-
-DOM Scratchpad
---------------
-
-As in the wider client, arbitrarily accessing document content is
-strongly discouraged during tests. But DOM access is still needed to
-e.g. fully initialize :js:class:`widgets <~openerp.web.Widget>` before
-testing them.
-
-Thus, a test case gets a DOM scratchpad as its second positional
-parameter, in a jQuery instance. That scratchpad is fully cleaned up
-before each test, and as long as it doesn't do anything outside the
-scratchpad your code can do whatever it wants::
-
-    // test/demo.js
-    test('DOM content', function (instance, $scratchpad) {
-        $scratchpad.html('<div><span class="foo bar">ok</span></div>');
-        ok($scratchpad.find('span').hasClass('foo'),
-           "should have provided class");
-    });
-    test('clean scratchpad', function (instance, $scratchpad) {
-        ok(!$scratchpad.children().length, "should have no content");
-        ok(!$scratchpad.text(), "should have no text");
-    });
-
-.. note::
-
-    The top-level element of the scratchpad is not cleaned up, test
-    cases can add text or DOM children but shoud not alter
-    ``$scratchpad`` itself.
-
-Loading templates
------------------
-
-To avoid the corresponding processing costs, by default templates are
-not loaded into QWeb. If you need to render e.g. widgets making use of
-QWeb templates, you can request their loading through the
-:js:attr:`~TestOptions.templates` option to the :js:func:`test case
-function <openerp.testing.case>`.
-
-This will automatically load all relevant templates in the instance's
-qweb before running the test case:
-
-.. code-block:: python
-
-    {
-        'name': "Demonstration of web/javascript tests",
-        'category': 'Hidden',
-        'depends': ['web'],
-        'js': ['static/src/js/demo.js'],
-        'test': ['static/test/demo.js'],
-        'qweb': ['static/src/xml/demo.xml'],
-    }
-
-.. code-block:: xml
-
-    <!-- src/xml/demo.xml -->
-    <templates id="template" xml:space="preserve">
-        <t t-name="DemoTemplate">
-            <t t-foreach="5" t-as="value">
-                <p><t t-esc="value"/></p>
-            </t>
-        </t>
-    </templates>
-
-::
-
-    // test/demo.js
-    test('templates', {templates: true}, function (instance) {
-        var s = instance.web.qweb.render('DemoTemplate');
-        var texts = $(s).find('p').map(function () {
-            return $(this).text();
-        }).get();
-
-        deepEqual(texts, ['0', '1', '2', '3', '4']);
-    });
-
-Asynchronous cases
-------------------
-
-The test case examples so far are all synchronous, they execute from
-the first to the last line and once the last line has executed the
-test is done. But the web client is full of :doc:`asynchronous code
-</async>`, and thus test cases need to be async-aware.
-
-This is done by returning a :js:class:`deferred <Deferred>` from the
-case callback::
-
-    // test/demo.js
-    test('asynchronous', {
-        asserts: 1
-    }, function () {
-        var d = $.Deferred();
-        setTimeout(function () {
-            ok(true);
-            d.resolve();
-        }, 100);
-        return d;
-    });
-
-This example also uses the :js:class:`options parameter <TestOptions>`
-to specify the number of assertions the case should expect, if less or
-more assertions are specified the case will count as failed.
-
-Asynchronous test cases *must* specify the number of assertions they
-will run. This allows more easily catching situations where e.g. the
-test architecture was not warned about asynchronous operations.
-
-.. note::
-
-    Asynchronous test cases also have a 2 seconds timeout: if the test
-    does not finish within 2 seconds, it will be considered
-    failed. This pretty much always means the test will not
-    resolve. This timeout *only* applies to the test itself, not to
-    the setup and teardown processes.
-
-.. note::
-
-    If the returned deferred is rejected, the test will be failed
-    unless :js:attr:`~TestOptions.fail_on_rejection` is set to
-    ``false``.
-
-RPC
----
-
-An important subset of asynchronous test cases is test cases which
-need to perform (and chain, to an extent) RPC calls.
-
-.. note::
-
-    Because they are a subset of asynchronous cases, RPC cases must
-    also provide a valid :js:attr:`assertions count
-    <TestOptions.asserts>`.
-
-By default, test cases will fail when trying to perform an RPC
-call. The ability to perform RPC calls must be explicitly requested by
-a test case (or its containing test suite) through
-:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or
-``rpc``.
-
-.. _testing-rpc-mock:
-
-Mock RPC
-++++++++
-
-The preferred (and fastest from a setup and execution time point of
-view) way to do RPC during tests is to mock the RPC calls: while
-setting up the test case, provide what the RPC responses "should" be,
-and only test the code between the "user" (the test itself) and the
-RPC call, before the call is effectively done.
-
-To do this, set the :js:attr:`rpc option <TestOptions.rpc>` to
-``mock``. This will add a third parameter to the test case callback:
-
-.. js:function:: mock(rpc_spec, handler)
-
-    Can be used in two different ways depending on the shape of the
-    first parameter:
-
-    * If it matches the pattern ``model:method`` (if it contains a
-      colon, essentially) the call will set up the mocking of an RPC
-      call straight to the OpenERP server (through XMLRPC) as
-      performed via e.g. :js:func:`openerp.web.Model.call`.
-
-      In that case, ``handler`` should be a function taking two
-      arguments ``args`` and ``kwargs``, matching the corresponding
-      arguments on the server side and should simply return the value
-      as if it were returned by the Python XMLRPC handler::
-
-          test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) {
-              // set up mocking
-              mock('people.famous:name_search', function (args, kwargs) {
-                  strictEqual(kwargs.name, 'bob');
-                  return [
-                      [1, "Microsoft Bob"],
-                      [2, "Bob the Builder"],
-                      [3, "Silent Bob"]
-                  ];
-              });
-
-              // actual test code
-              return new instance.web.Model('people.famous')
-                  .call('name_search', {name: 'bob'}).then(function (result) {
-                      strictEqual(result.length, 3, "shoud return 3 people");
-                      strictEqual(result[0][1], "Microsoft Bob",
-                          "the most famous bob should be Microsoft Bob");
-                  });
-          });
-
-    * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it
-      will mock a JSON-RPC call to a web client controller, such as
-      ``/web/webclient/translations``. In that case, the handler takes
-      a single ``params`` argument holding all of the parameters
-      provided over JSON-RPC.
-
-      As previously, the handler should simply return the result value
-      as if returned by the original JSON-RPC handler::
-
-          test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) {
-              var fetched_dbs = false, fetched_langs = false;
-              mock('/web/database/get_list', function () {
-                  fetched_dbs = true;
-                  return ['foo', 'bar', 'baz'];
-              });
-              mock('/web/session/get_lang_list', function () {
-                  fetched_langs = true;
-                  return [['vo_IS', 'Hopelandic / Vonlenska']];
-              });
-
-              // widget needs that or it blows up
-              instance.webclient = {toggle_bars: openerp.testing.noop};
-              var dbm = new instance.web.DatabaseManager({});
-              return dbm.appendTo($s).then(function () {
-                  ok(fetched_dbs, "should have fetched databases");
-                  ok(fetched_langs, "should have fetched languages");
-                  deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
-              });
-          });
-
-.. note::
-
-    Mock handlers can contain assertions, these assertions should be
-    part of the assertions count (and if multiple calls are made to a
-    handler containing assertions, it multiplies the effective number
-    of assertions).
-
-.. _testing-rpc-rpc:
-
-Actual RPC
-++++++++++
-
-A more realistic (but significantly slower and more expensive) way to
-perform RPC calls is to perform actual calls to an actually running
-OpenERP server. To do this, set the :js:attr:`rpc option
-<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter
-but will enable actual RPC, and the automatic creation and destruction
-of databases (from a specified source) around tests.
-
-First, create a basic model we can test stuff with:
-
-.. code-block:: javascript
-
-    from openerp.osv import orm, fields
-
-    class TestObject(orm.Model):
-        _name = 'web_tests_demo.model'
-
-        _columns = {
-            'name': fields.char("Name", required=True),
-            'thing': fields.char("Thing"),
-            'other': fields.char("Other", required=True)
-        }
-        _defaults = {
-            'other': "bob"
-        }
-
-then the actual test::
-
-    test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) {
-        var Model = new instance.web.Model('web_tests_demo.model');
-        return Model.call('create', [{name: "Bob"}])
-            .then(function (id) {
-                return Model.call('read', [[id]]);
-            }).then(function (records) {
-                strictEqual(records.length, 1);
-                var record = records[0];
-                strictEqual(record.name, "Bob");
-                strictEqual(record.thing, false);
-                // default value
-                strictEqual(record.other, 'bob');
-            });
-    });
-
-This test looks like a "mock" RPC test but for the lack of mock
-response (and the different ``rpc`` type), however it has further
-ranging consequences in that it will copy an existing database to a
-new one, run the test in full on that temporary database and destroy
-the database, to simulate an isolated and transactional context and
-avoid affecting other tests. One of the consequences is that it takes
-a *long* time to run (5~10s, most of that time being spent waiting for
-a database duplication).
-
-Furthermore, as the test needs to clone a database, it also has to ask
-which database to clone, the database/super-admin password and the
-password of the ``admin`` user (in order to authenticate as said
-user). As a result, the first time the test runner encounters an
-``rpc: "rpc"`` test configuration it will produce the following
-prompt:
-
-.. image:: ./images/db-query.png
-    :align: center
-
-and stop the testing process until the necessary information has been
-provided.
-
-The prompt will only appear once per test run, all tests will use the
-same "source" database.
-
-.. note::
-
-    The handling of that information is currently rather brittle and
-    unchecked, incorrect values will likely crash the runner.
-
-.. note::
-
-    The runner does not currently store this information (for any
-    longer than a test run that is), the prompt will have to be filled
-    every time.
-
-Testing API
------------
-
-.. js:function:: openerp.testing.section(name[, options], body)
-
-    A test section, serves as shared namespace for related tests (for
-    constants or values to only set up once). The ``body`` function
-    should contain the tests themselves.
-
-    Note that the order in which tests are run is essentially
-    undefined, do *not* rely on it.
-
-    :param String name:
-    :param TestOptions options:
-    :param body:
-    :type body: Function<:js:func:`~openerp.testing.case`, void>
-
-.. js:function:: openerp.testing.case(name[, options], callback)
-
-    Registers a test case callback in the test runner, the callback
-    will only be run once the runner is started (or maybe not at all,
-    if the test is filtered out).
-
-    :param String name:
-    :param TestOptions options:
-    :param callback:
-    :type callback: Function<instance, $, Function<String, Function, void>>
-
-.. js:class:: TestOptions
-
-    the various options which can be passed to
-    :js:func:`~openerp.testing.section` or
-    :js:func:`~openerp.testing.case`. Except for
-    :js:attr:`~TestOptions.setup` and
-    :js:attr:`~TestOptions.teardown`, an option on
-    :js:func:`~openerp.testing.case` will overwrite the corresponding
-    option on :js:func:`~openerp.testing.section` so
-    e.g. :js:attr:`~TestOptions.rpc` can be set for a
-    :js:func:`~openerp.testing.section` and then differently set for
-    some :js:func:`~openerp.testing.case` of that
-    :js:func:`~openerp.testing.section`
-
-    .. js:attribute:: TestOptions.asserts
-
-        An integer, the number of assertions which should run during a
-        normal execution of the test. Mandatory for asynchronous tests.
-
-    .. js:attribute:: TestOptions.setup
-
-        Test case setup, run right before each test case. A section's
-        :js:func:`~TestOptions.setup` is run before the case's own, if
-        both are specified.
-
-    .. js:attribute:: TestOptions.teardown
-
-        Test case teardown, a case's :js:func:`~TestOptions.teardown`
-        is run before the corresponding section if both are present.
-
-    .. js:attribute:: TestOptions.fail_on_rejection
-
-        If the test is asynchronous and its resulting promise is
-        rejected, fail the test. Defaults to ``true``, set to
-        ``false`` to not fail the test in case of rejection::
-
-            // test/demo.js
-            test('unfail rejection', {
-                asserts: 1,
-                fail_on_rejection: false
-            }, function () {
-                var d = $.Deferred();
-                setTimeout(function () {
-                    ok(true);
-                    d.reject();
-                }, 100);
-                return d;
-            });
-
-    .. js:attribute:: TestOptions.rpc
-
-        RPC method to use during tests, one of ``"mock"`` or
-        ``"rpc"``. Any other value will disable RPC for the test (if
-        they were enabled by the suite for instance).
-
-    .. js:attribute:: TestOptions.templates
-
-        Whether the current module (and its dependencies)'s templates
-        should be loaded into QWeb before starting the test. A
-        boolean, ``false`` by default.
-
-The test runner can also use two global configuration values set
-directly on the ``window`` object:
-
-* ``oe_all_dependencies`` is an ``Array`` of all modules with a web
-  component, ordered by dependency (for a module ``A`` with
-  dependencies ``A'``, any module of ``A'`` must come before ``A`` in
-  the array)
-
-* ``oe_db_info`` is an object with 3 keys ``source``, ``supadmin`` and
-  ``password``. It is used to pre-configure :ref:`actual RPC
-  <testing-rpc-rpc>` tests, to avoid a prompt being displayed
-  (especially for headless situations).
-
-Running through Python
-----------------------
-
-The web client includes the means to run these tests on the
-command-line (or in a CI system), but while actually running it is
-pretty simple the setup of the pre-requisite parts has some
-complexities.
-
-1. Install unittest2_ and QUnitSuite_ in your Python environment. Both
-   can trivially be installed via `pip <http://pip-installer.org>`_ or
-   `easy_install
-   <http://packages.python.org/distribute/easy_install.html>`_.
-
-   The former is the unit-testing framework used by OpenERP, the
-   latter is an adapter module to run qunit_ test suites and convert
-   their result into something unittest2_ can understand and report.
-
-2. Install PhantomJS_. It is a headless
-   browser which allows automating running and testing web
-   pages. QUnitSuite_ uses it to actually run the qunit_ test suite.
-
-   The PhantomJS_ website provides pre-built binaries for some
-   platforms, and your OS's package management probably provides it as
-   well.
-
-   If you're building PhantomJS_ from source, I recommend preparing
-   for some knitting time as it's not exactly fast (it needs to
-   compile both `Qt <http://qt-project.org/>`_ and `Webkit
-   <http://www.webkit.org/>`_, both being pretty big projects).
-
-   .. note::
-
-       Because PhantomJS_ is webkit-based, it will not be able to test
-       if Firefox, Opera or Internet Explorer can correctly run the
-       test suite (and it is only an approximation for Safari and
-       Chrome). It is therefore recommended to *also* run the test
-       suites in actual browsers once in a while.
-
-   .. note::
-
-       The version of PhantomJS_ this was build through is 1.7,
-       previous versions *should* work but are not actually supported
-       (and tend to just segfault when something goes wrong in
-       PhantomJS_ itself so they're a pain to debug).
-
-3. Set up :ref:`OpenERP Command <openerpcommand:openerp-command>`,
-   which will be used to actually run the tests: running the qunit_
-   test suite requires a running server, so at this point OpenERP
-   Server isn't able to do it on its own during the building/testing
-   process.
-
-4. Install a new database with all relevant modules (all modules with
-   a web component at least), then restart the server
-
-   .. note::
-
-       For some tests, a source database needs to be duplicated. This
-       operation requires that there be no connection to the database
-       being duplicated, but OpenERP doesn't currently break
-       existing/outstanding connections, so restarting the server is
-       the simplest way to ensure everything is in the right state.
-
-5. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct
-   addons-path specified (and replacing ``$DATABASE`` by the source
-   database you created above)
-
-   .. note::
-
-       If you leave out ``-mweb``, the runner will attempt to run all
-       the tests in all the modules, which may or may not work.
-
-If everything went correctly, you should now see a list of tests with
-(hopefully) ``ok`` next to their names, closing with a report of the
-number of tests run and the time it took:
-
-.. literalinclude:: test-report.txt
-    :language: text
-
-Congratulation, you have just performed a successful "offline" run of
-the OpenERP Web test suite.
-
-.. note::
-
-    Note that this runs all the Python tests for the ``web`` module,
-    but all the web tests for all of OpenERP. This can be surprising.
-
-.. _qunit: http://qunitjs.com/
-
-.. _qunit assertions: http://api.qunitjs.com/category/assert/
-
-.. _unittest2: http://pypi.python.org/pypi/unittest2
-
-.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/
-
-.. _PhantomJS: http://phantomjs.org/
diff --git a/addons/web/doc/web_controllers.rst b/addons/web/doc/web_controllers.rst
deleted file mode 100644 (file)
index 442d2b0..0000000
+++ /dev/null
@@ -1,173 +0,0 @@
-
-Web Controllers
-===============
-
-Web controllers are classes in OpenERP able to catch the http requests sent by any browser. They allow to generate
-html pages to be served like any web server, implement new methods to be used by the Javascript client, etc...
-
-Controllers File
-----------------
-
-By convention the controllers should be placed in the controllers directory of the module. Example:
-
-.. code-block:: text
-
-    web_example
-    ├── controllers
-    │   ├── __init__.py
-    │   └── my_controllers.py
-    ├── __init__.py
-    └── __openerp__.py
-
-In ``__init__.py`` you must add:
-
-::
-
-    import controllers
-
-And here is the content of ``controllers/__init__.py``:
-
-::
-    
-    import my_controllers
-
-Now you can put the following content in ``controllers/my_controllers.py``:
-
-::
-
-    import openerp.http as http
-    from openerp.http import request
-
-
-Controller Declaration
-----------------------
-
-In your controllers file, you can now declare a controller this way:
-
-::
-
-    class MyController(http.Controller):
-
-        @http.route('/my_url/some_html', type="http")
-        def some_html(self):
-            return "<h1>This is a test</h1>"
-
-        @http.route('/my_url/some_json', type="json")
-        def some_json(self):
-            return {"sample_dictionary": "This is a sample JSON dictionary"}
-
-A controller must inherit from ``http.Controller``. Each time you define a method with ``@http.route()`` it defines a
-url to match. As example, the ``some_html()`` method will be called a client query the ``/my_url/some_html`` url.
-
-Pure HTTP Requests
-------------------
-
-You can define methods to get any normal http requests by passing ``'http'`` to the ``type`` argument of
-``http.route()``. When doing so, you get the HTTP parameters as named parameters of the method:
-
-::
-
-    @http.route('/say_hello', type="http")
-    def say_hello(self, name):
-        return "<h1>Hello %s</h1>" % name
-
-This url could be contacted by typing this url in a browser: ``http://localhost:8069/say_hello?name=Nicolas``.
-
-JSON Requests
--------------
-
-Methods that received JSON can be defined by passing ``'json'`` to the ``type`` argument of ``http.route()``. The
-OpenERP Javascript client can contact these methods using the JSON-RPC protocol. JSON methods must return JSON. Like the
-HTTP methods they receive arguments as named parameters (except these arguments are JSON-RPC parameters).
-
-::
-
-    @http.route('/division', type="json")
-    def division(self, i, j):
-        return i / j # returns a number
-
-URL Patterns
-------------
-
-Any URL passed to ``http.route()`` can contain patterns. Example:
-
-::
-
-    @http.route('/files/<path:file_path>', type="http")
-    def files(self, file_path):
-        ... # return a file identified by the path store in the 'my_path' variable
-
-When such patterns are used, the method will received additional parameters that correspond to the parameters defined in
-the url. For exact documentation about url patterns, see Werkzeug's documentation:
-http://werkzeug.pocoo.org/docs/routing/ .
-
-Also note you can pass multiple urls to ``http.route()``:
-
-
-::
-
-    @http.route(['/files/<path:file_path>', '/other_url/<path:file_path>'], type="http")
-    def files(self, file_path):
-        ...
-
-Contacting Models
------------------
-
-To use the database you must access the OpenERP models. The global ``request`` object provides the necessary objects:
-
-::
-
-    @http.route('/my_name', type="http")
-    def my_name(self):
-        my_user_record = request.registry.get("res.users").browse(request.cr, request.uid, request.uid)
-        return "<h1>Your name is %s</h1>" % my_user_record.name
-
-``request.registry`` is the registry that gives you access to the models. It is the equivalent of ``self.pool`` when
-working inside OpenERP models.
-
-``request.cr`` is the cursor object. This is the ``cr`` parameter you have to pass as first argument of every model
-method in OpenERP.
-
-``request.uid`` is the id of the current logged in user. This is the ``uid`` parameter you have to pass as second
-argument of every model method in OpenERP.
-
-Authorization Levels
---------------------
-
-By default, all access to the models will use the rights of the currently logged in user (OpenERP uses cookies to track
-logged users). It is also impossible to reach an URL without being logged (the user's browser will receive an HTTP
-error).
-
-There are some cases when the current user is not relevant, and we just want to give access to anyone to an URL. A
-typical example is be the generation of a home page for a website. The home page should be visible by anyone, whether
-they have an account or not. To do so, add the ``'admin'`` value to the ``auth`` parameter of ``http.route()``:
-
-::
-
-    @http.route('/hello', type="http", auth="admin")
-    def hello(self):
-        return "<div>Hello unknown user!</div>"
-
-When using the ``admin`` authentication the access to the OpenERP models will be performed with the ``Administrator``
-user and ``request.uid`` will be equal to ``openerp.SUPERUSER_ID`` (the id of the administrator).
-
-It is important to note that when using the ``Administrator`` user all security is bypassed. So the programmers
-implementing such methods should take great care of not creating security issues in the application.
-
-Overriding Controllers
-----------------------
-
-Existing routes can be overridden. To do so, create a controller that inherit the controller containing the route you
-want to override. Example that redefine the home page of your OpenERP application.
-
-::
-
-    import openerp.addons.web.controllers.main as main
-
-    class Home2(main.Home):
-        @http.route('/', type="http", auth="db")
-        def index(self):
-            return "<div>This is my new home page.</div>"
-
-By re-defining the ``index()`` method, you change the behavior of the original ``Home`` class. Now the ``'/'`` route
-will match the new ``index()`` method in ``Home2``.
index f563a28..673b5f7 100644 (file)
@@ -167,7 +167,9 @@ html_sidebars = {
 
 intersphinx_mapping = {
     'python': ('https://docs.python.org/2/', None),
-    'werkzeug': ('http://werkzeug.pocoo.org/docs/0.9/', None),
+    'werkzeug': ('http://werkzeug.pocoo.org/docs/', None),
+    'sqlalchemy': ('http://docs.sqlalchemy.org/en/rel_0_9/', None),
+    'django': ('https://django.readthedocs.org/en/latest/', None),
 }
 
 github_user = 'odoo'
diff --git a/doc/reference/async.rst b/doc/reference/async.rst
new file mode 100644 (file)
index 0000000..c2aded8
--- /dev/null
@@ -0,0 +1,341 @@
+:orphan:
+
+.. _reference/async:
+
+Asynchronous Operations
+=======================
+
+As a language (and runtime), javascript is fundamentally
+single-threaded. This means any blocking request or computation will
+blocks the whole page (and, in older browsers, the software itself
+even preventing users from switching to an other tab): a javascript
+environment can be seen as an event-based runloop where application
+developers have no control over the runloop itself.
+
+As a result, performing long-running synchronous network requests or
+other types of complex and expensive accesses is frowned upon and
+asynchronous APIs are used instead.
+
+The goal of this guide is to provide some tools to deal with
+asynchronous systems, and warn against systemic issues or dangers.
+
+Deferreds
+---------
+
+Deferreds are a form of `promises`_. OpenERP Web currently uses
+`jQuery's deferred`_.
+
+The core idea of deferreds is that potentially asynchronous methods
+will return a :js:class:`Deferred` object instead of an arbitrary
+value or (most commonly) nothing.
+
+This object can then be used to track the end of the asynchronous
+operation by adding callbacks onto it, either success callbacks or
+error callbacks.
+
+A great advantage of deferreds over simply passing callback functions
+directly to asynchronous methods is the ability to :ref:`compose them
+<reference/async/composition>`.
+
+Using deferreds
+~~~~~~~~~~~~~~~
+
+Deferreds's most important method is :js:func:`Deferred.then`. It is
+used to attach new callbacks to the deferred object.
+
+* the first parameter attaches a success callback, called when the
+  deferred object is successfully resolved and provided with the
+  resolved value(s) for the asynchronous operation.
+
+* the second parameter attaches a failure callback, called when the
+  deferred object is rejected and provided with rejection values
+  (often some sort of error message).
+
+Callbacks attached to deferreds are never "lost": if a callback is
+attached to an already resolved or rejected deferred, the callback
+will be called (or ignored) immediately. A deferred is also only ever
+resolved or rejected once, and is either resolved or rejected: a given
+deferred can not call a single success callback twice, or call both a
+success and a failure callbacks.
+
+:js:func:`~Deferred.then` should be the method you'll use most often
+when interacting with deferred objects (and thus asynchronous APIs).
+
+Building deferreds
+~~~~~~~~~~~~~~~~~~
+
+After using asynchronous APIs may come the time to build them: for
+mocks_, to compose deferreds from multiple source in a complex
+manner, in order to let the current operations repaint the screen or
+give other events the time to unfold, ...
+
+This is easy using jQuery's deferred objects.
+
+.. note:: this section is an implementation detail of jQuery Deferred
+          objects, the creation of promises is not part of any
+          standard (even tentative) that I know of. If you are using
+          deferred objects which are not jQuery's, their API may (and
+          often will) be completely different.
+
+Deferreds are created by invoking their constructor [#]_ without any
+argument. This creates a :js:class:`Deferred` instance object with the
+following methods:
+
+:js:func:`Deferred.resolve`
+
+    As its name indicates, this method moves the deferred to the
+    "Resolved" state. It can be provided as many arguments as
+    necessary, these arguments will be provided to any pending success
+    callback.
+
+:js:func:`Deferred.reject`
+
+    Similar to :js:func:`~Deferred.resolve`, but moves the deferred to
+    the "Rejected" state and calls pending failure handlers.
+
+:js:func:`Deferred.promise`
+
+    Creates a readonly view of the deferred object. It is generally a
+    good idea to return a promise view of the deferred to prevent
+    callers from resolving or rejecting the deferred in your stead.
+
+:js:func:`~Deferred.reject` and :js:func:`~Deferred.resolve` are used
+to inform callers that the asynchronous operation has failed (or
+succeeded). These methods should simply be called when the
+asynchronous operation has ended, to notify anybody interested in its
+result(s).
+
+.. _reference/async/composition:
+
+Composing deferreds
+~~~~~~~~~~~~~~~~~~~
+
+What we've seen so far is pretty nice, but mostly doable by passing
+functions to other functions (well adding functions post-facto would
+probably be a chore... still, doable).
+
+Deferreds truly shine when code needs to compose asynchronous
+operations in some way or other, as they can be used as a basis for
+such composition.
+
+There are two main forms of compositions over deferred: multiplexing
+and piping/cascading.
+
+Deferred multiplexing
+`````````````````````
+
+The most common reason for multiplexing deferred is simply performing
+multiple asynchronous operations and wanting to wait until all of them are
+done before moving on (and executing more stuff).
+
+The jQuery multiplexing function for promises is :js:func:`when`.
+
+.. note:: the multiplexing behavior of jQuery's :js:func:`when` is an
+          (incompatible, mostly) extension of the behavior defined in
+          `CommonJS Promises/B`_.
+
+This function can take any number of promises [#]_ and will return a
+promise.
+
+The returned promise will be resolved when *all* multiplexed promises
+are resolved, and will be rejected as soon as one of the multiplexed
+promises is rejected (it behaves like Python's ``all()``, but with
+promise objects instead of boolean-ish).
+
+The resolved values of the various promises multiplexed via
+:js:func:`when` are mapped to the arguments of :js:func:`when`'s
+success callback, if they are needed. The resolved values of a promise
+are at the same index in the callback's arguments as the promise in
+the :js:func:`when` call so you will have:
+
+.. code-block:: javascript
+
+    $.when(p0, p1, p2, p3).then(
+            function (results0, results1, results2, results3) {
+        // code
+    });
+
+.. warning::
+
+    in a normal mapping, each parameter to the callback would be an
+    array: each promise is conceptually resolved with an array of 0..n
+    values and these values are passed to :js:func:`when`'s
+    callback. But jQuery treats deferreds resolving a single value
+    specially, and "unwraps" that value.
+
+    For instance, in the code block above if the index of each promise
+    is the number of values it resolves (0 to 3), ``results0`` is an
+    empty array, ``results2`` is an array of 2 elements (a pair) but
+    ``results1`` is the actual value resolved by ``p1``, not an array.
+
+Deferred chaining
+`````````````````
+
+A second useful composition is starting an asynchronous operation as
+the result of an other asynchronous operation, and wanting the result
+of both: with the tools described so far, handling e.g. OpenERP's
+search/read sequence with this would require something along the lines
+of:
+
+.. code-block:: javascript
+
+    var result = $.Deferred();
+    Model.search(condition).then(function (ids) {
+        Model.read(ids, fields).then(function (records) {
+            result.resolve(records);
+        });
+    });
+    return result.promise();
+
+While it doesn't look too bad for trivial code, this quickly gets
+unwieldy.
+
+But :js:func:`~Deferred.then` also allows handling this kind of
+chains: it returns a new promise object, not the one it was called
+with, and the return values of the callbacks is important to this behavior:
+whichever callback is called,
+
+* If the callback is not set (not provided or left to null), the
+  resolution or rejection value(s) is simply forwarded to
+  :js:func:`~Deferred.then`'s promise (it's essentially a noop)
+
+* If the callback is set and does not return an observable object (a
+  deferred or a promise), the value it returns (``undefined`` if it
+  does not return anything) will replace the value it was given, e.g.
+
+  .. code-block:: javascript
+
+      promise.then(function () {
+          console.log('called');
+      });
+
+  will resolve with the sole value ``undefined``.
+
+* If the callback is set and returns an observable object, that object
+  will be the actual resolution (and result) of the pipe. This means a
+  resolved promise from the failure callback will resolve the pipe,
+  and a failure promise from the success callback will reject the
+  pipe.
+
+  This provides an easy way to chain operation successes, and the
+  previous piece of code can now be rewritten:
+
+  .. code-block:: javascript
+
+      return Model.search(condition).then(function (ids) {
+          return Model.read(ids, fields);
+      });
+
+  the result of the whole expression will encode failure if either
+  ``search`` or ``read`` fails (with the right rejection values), and
+  will be resolved with ``read``'s resolution values if the chain
+  executes correctly.
+
+:js:func:`~Deferred.then` is also useful to adapt third-party
+promise-based APIs, in order to filter their resolution value counts
+for instance (to take advantage of :js:func:`when` 's special
+treatment of single-value promises).
+
+jQuery.Deferred API
+~~~~~~~~~~~~~~~~~~~
+
+.. js:function:: when(deferreds…)
+
+    :param deferreds: deferred objects to multiplex
+    :returns: a multiplexed deferred
+    :rtype: :js:class:`Deferred`
+
+.. js:class:: Deferred
+
+    .. js:function:: Deferred.then(doneCallback[, failCallback])
+
+        Attaches new callbacks to the resolution or rejection of the
+        deferred object. Callbacks are executed in the order they are
+        attached to the deferred.
+
+        To provide only a failure callback, pass ``null`` as the
+        ``doneCallback``, to provide only a success callback the
+        second argument can just be ignored (and not passed at all).
+
+        Returns a new deferred which resolves to the result of the
+        corresponding callback, if a callback returns a deferred
+        itself that new deferred will be used as the resolution of the
+        chain.
+
+        :param doneCallback: function called when the deferred is resolved
+        :param failCallback: function called when the deferred is rejected
+        :returns: the deferred object on which it was called
+        :rtype: :js:class:`Deferred`
+
+    .. js:function:: Deferred.done(doneCallback)
+
+        Attaches a new success callback to the deferred, shortcut for
+        ``deferred.then(doneCallback)``.
+
+        .. note:: a difference is the result of :js:func:`Deferred.done`'s
+                  is ignored rather than forwarded through the chain
+
+        This is a jQuery extension to `CommonJS Promises/A`_ providing
+        little value over calling :js:func:`~Deferred.then` directly,
+        it should be avoided.
+
+        :param doneCallback: function called when the deferred is resolved
+        :type doneCallback: Function
+        :returns: the deferred object on which it was called
+        :rtype: :js:class:`Deferred`
+
+    .. js:function:: Deferred.fail(failCallback)
+
+        Attaches a new failure callback to the deferred, shortcut for
+        ``deferred.then(null, failCallback)``.
+
+        A second jQuery extension to `Promises/A <CommonJS
+        Promises/A>`_. Although it provides more value than
+        :js:func:`~Deferred.done`, it still is not much and should be
+        avoided as well.
+
+        :param failCallback: function called when the deferred is rejected
+        :type failCallback: Function
+        :returns: the deferred object on which it was called
+        :rtype: :js:class:`Deferred`
+
+    .. js:function:: Deferred.promise()
+
+        Returns a read-only view of the deferred object, with all
+        mutators (resolve and reject) methods removed.
+
+    .. js:function:: Deferred.resolve(value…)
+
+        Called to resolve a deferred, any value provided will be
+        passed onto the success handlers of the deferred object.
+
+        Resolving a deferred which has already been resolved or
+        rejected has no effect.
+
+    .. js:function:: Deferred.reject(value…)
+
+        Called to reject (fail) a deferred, any value provided will be
+        passed onto the failure handler of the deferred object.
+
+        Rejecting a deferred which has already been resolved or
+        rejected has no effect.
+
+.. [#] or simply calling :js:class:`Deferred` as a function, the
+       result is the same
+
+.. [#] or not-promises, the `CommonJS Promises/B`_ role of
+       :js:func:`when` is to be able to treat values and promises
+       uniformly: :js:func:`when` will pass promises through directly,
+       but non-promise values and objects will be transformed into a
+       resolved promise (resolving themselves with the value itself).
+
+       jQuery's :js:func:`when` keeps this behavior making deferreds
+       easy to build from "static" values, or allowing defensive code
+       where expected promises are wrapped in :js:func:`when` just in
+       case.
+
+.. _promises: http://en.wikipedia.org/wiki/Promise_(programming)
+.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/
+.. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A
+.. _CommonJS Promises/B: http://wiki.commonjs.org/wiki/Promises/B
+.. _mocks: http://en.wikipedia.org/wiki/Mock_object
index e36ddf9..9f83a30 100644 (file)
@@ -2,6 +2,8 @@
 Web Controllers
 ===============
 
+.. _reference/http/routing:
+
 Routing
 =======
 
diff --git a/doc/reference/images/runner.png b/doc/reference/images/runner.png
new file mode 100644 (file)
index 0000000..bd48e9d
Binary files /dev/null and b/doc/reference/images/runner.png differ
diff --git a/doc/reference/images/runner2.png b/doc/reference/images/runner2.png
new file mode 100644 (file)
index 0000000..38ea294
Binary files /dev/null and b/doc/reference/images/runner2.png differ
diff --git a/doc/reference/images/tests.png b/doc/reference/images/tests.png
new file mode 100644 (file)
index 0000000..84083d9
Binary files /dev/null and b/doc/reference/images/tests.png differ
diff --git a/doc/reference/images/tests2.png b/doc/reference/images/tests2.png
new file mode 100644 (file)
index 0000000..c8a6f8a
Binary files /dev/null and b/doc/reference/images/tests2.png differ
diff --git a/doc/reference/images/tests3.png b/doc/reference/images/tests3.png
new file mode 100644 (file)
index 0000000..247f707
Binary files /dev/null and b/doc/reference/images/tests3.png differ
index 18491ea..54bed0f 100644 (file)
@@ -11,45 +11,22 @@ Widgets
 
 .. class:: openerp.Widget
 
-This is the base class for all visual components. It corresponds to an MVC
-view. It provides a number of services to handle a section of a page:
+    The base class for all visual components. It corresponds to an MVC
+    view, and provides a number of service to simplify handling of a section
+    of a page:
 
-* Rendering with QWeb
-
-* Parenting-child relations
-
-* Life-cycle management (including facilitating children destruction when a
-  parent object is removed)
-
-* DOM insertion, via jQuery-powered insertion methods. Insertion targets can
-  be anything the corresponding jQuery method accepts (generally selectors,
-  DOM nodes and jQuery objects):
-
-  :func:`~openerp.Widget.appendTo`
-    Renders the widget and inserts it as the last child of the target, uses
-    `.appendTo()`_
-
-  :func:`~openerp.Widget.prependTo`
-    Renders the widget and inserts it as the first child of the target, uses
-    `.prependTo()`_
-
-  :func:`~openerp.Widget.insertAfter`
-    Renders the widget and inserts it as the preceding sibling of the target,
-    uses `.insertAfter()`_
-
-  :func:`~openerp.Widget.insertBefore`
-    Renders the widget and inserts it as the following sibling of the target,
-    uses `.insertBefore()`_
-
-* Backbone-compatible shortcuts
-
-.. _widget-dom_root:
+    * Handles parent/child relationships between widgets
+    * Provides extensive lifecycle management with safety features (e.g.
+      automatically destroying children widgets during the destruction of a
+      parent)
+    * Automatic rendering with :ref:`qweb <reference/qweb>`
+    * Backbone-compatible shortcuts
 
 DOM Root
 --------
 
-A :class:`~openerp.Widget` is responsible for a section of the
-page materialized by the DOM root of the widget.
+A :class:`~openerp.Widget` is responsible for a section of the page
+materialized by the DOM root of the widget.
 
 A widget's DOM root is available via two attributes:
 
@@ -129,8 +106,8 @@ A widget's lifecycle has 3 main phases:
        initialization method of widgets, synchronous, can be overridden to
        take more parameters from the widget's creator/parent
 
-       :param parent: the current widget's parent, used to handle automatic
-                      destruction and even propagation. Can be ``null`` for
+       :param parent: the new widget's parent, used to handle automatic
+                      destruction and event propagation. Can be ``null`` for
                       the widget to have no parent.
        :type parent: :class:`~openerp.Widget`
 
@@ -157,14 +134,14 @@ A widget's lifecycle has 3 main phases:
     uses `.insertBefore()`_
 
   All of these methods accept whatever the corresponding jQuery method accepts
-  (CSS selectors, DOM nodes or jQuery objects). They all return a promise and
-  are charged with three tasks:
+  (CSS selectors, DOM nodes or jQuery objects). They all return a deferred_
+  and are charged with three tasks:
 
-  * render the widget's root element via
+  * rendering the widget's root element via
     :func:`~openerp.Widget.renderElement`
-  * insert the widget's root element in the DOM using whichever jQuery method
-    they match
-  * start the widget, and return the result of starting it
+  * inserting the widget's root element in the DOM using whichever jQuery
+    method they match
+  * starting the widget, and returning the result of starting it
 
     .. function:: openerp.Widget.start()
 
@@ -192,8 +169,7 @@ A widget's lifecycle has 3 main phases:
 
     A widget being destroyed is automatically unlinked from its parent.
 
-Because a widget can be destroyed at any time, widgets also have utility
-methods to handle this case:
+Related to widget destruction is an important utility method:
 
 .. function:: openerp.Widget.alive(deferred[, reject=false])
 
@@ -231,16 +207,13 @@ methods to handle this case:
 Accessing DOM content
 '''''''''''''''''''''
 
-Because a widget is only responsible for the content below its DOM
-root, there is a shortcut for selecting sub-sections of a widget's
-DOM:
+Because a widget is only responsible for the content below its DOM root, there
+ is a shortcut for selecting sub-sections of a widget's DOM:
 
 .. function:: openerp.Widget.$(selector)
 
     Applies the CSS selector specified as parameter to the widget's
-    DOM root.
-
-    ::
+    DOM root::
 
         this.$(selector);
 
@@ -251,8 +224,7 @@ DOM:
     :param String selector: CSS selector
     :returns: jQuery object
 
-    .. note:: this helper method is compatible with
-              ``Backbone.View.$``
+    .. note:: this helper method is similar to ``Backbone.View.$``
 
 Resetting the DOM root
 ''''''''''''''''''''''
@@ -275,11 +247,11 @@ DOM events handling
 A widget will generally need to respond to user action within its
 section of the page. This entails binding events to DOM elements.
 
-To this end, :class:`~openerp.Widget` provides an shortcut:
+To this end, :class:`~openerp.Widget` provides a shortcut:
 
 .. attribute:: openerp.Widget.events
 
-    Events are a mapping of ``event selector`` (an event name and a
+    Events are a mapping of an event selector (an event name and an optional
     CSS selector separated by a space) to a callback. The callback can
     be the name of a widget's method or a function object. In either case, the
     ``this`` will be set to the widget::
@@ -299,21 +271,18 @@ To this end, :class:`~openerp.Widget` provides an shortcut:
 
 .. function:: openerp.Widget.delegateEvents
 
-    This method is in charge of binding
-    :attr:`~openerp.Widget.events` to the DOM. It is
-    automatically called after setting the widget's DOM root.
+    This method is in charge of binding :attr:`~openerp.Widget.events` to the
+    DOM. It is automatically called after setting the widget's DOM root.
 
     It can be overridden to set up more complex events than the
-    :attr:`~openerp.Widget.events` map allows, but the parent
-    should always be called (or :attr:`~openerp.Widget.events`
-    won't be handled correctly).
+    :attr:`~openerp.Widget.events` map allows, but the parent should always be
+    called (or :attr:`~openerp.Widget.events` won't be handled correctly).
 
 .. function:: openerp.Widget.undelegateEvents
 
-    This method is in charge of unbinding
-    :attr:`~openerp.Widget.events` from the DOM root when the
-    widget is destroyed or the DOM root is reset, in order to avoid
-    leaving "phantom" events.
+    This method is in charge of unbinding :attr:`~openerp.Widget.events` from
+    the DOM root when the widget is destroyed or the DOM root is reset, in
+    order to avoid leaving "phantom" events.
 
     It should be overridden to un-set any event set in an override of
     :func:`~openerp.Widget.delegateEvents`.
@@ -407,10 +376,873 @@ destroy all widget data.
 RPC
 ===
 
+To display and interact with data, calls to the Odoo server are necessary.
+This is performed using :abbr:`RPC <Remote Procedure Call>`.
+
+Odoo Web provides two primary APIs to handle this: a low-level
+JSON-RPC based API communicating with the Python section of Odoo
+Web (and of your module, if you have a Python part) and a high-level
+API above that allowing your code to talk directly to high-level Odoo models.
+
+All networking APIs are :ref:`asynchronous <reference/async>`. As a result,
+all of them will return Deferred_ objects (whether they resolve those with
+values or not). Understanding how those work before before moving on is
+probably necessary.
+
+High-level API: calling into Odoo models
+-------------------------------------------
+
+Access to Odoo object methods (made available through XML-RPC from the server)
+is done via :class:`openerp.Model`. It maps onto the Odoo server objects via
+two primary methods, :func:`~openerp.Model.call` and
+:func:`~openerp.Model.query`.
+
+:func:`~openerp.Model.call` is a direct mapping to the corresponding method of
+the Odoo server object. Its usage is similar to that of the Odoo Model API,
+with three differences:
+
+* The interface is :ref:`asynchronous <reference/async>`, so instead of
+  returning results directly RPC method calls will return
+  Deferred_ instances, which will themselves resolve to the
+  result of the matching RPC call.
+
+* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to
+  ``__getattr__`` or ``method_missing``, there needs to be an explicit
+  method to dispatch RPC methods.
+
+* No notion of pooler, the model proxy is instantiated where needed,
+  not fetched from an other (somewhat global) object::
+
+    var Users = new openerp.Model('res.users');
+
+    Users.call('change_password', ['oldpassword', 'newpassword'],
+                      {context: some_context}).then(function (result) {
+        // do something with change_password result
+    });
+
+:func:`~openerp.Model.query` is a shortcut for a builder-style
+interface to searches (``search`` + ``read`` in Odoo RPC terms). It
+returns a :class:`~openerp.web.Query` object which is immutable but
+allows building new :class:`~openerp.web.Query` instances from the
+first one, adding new properties or modifiying the parent object's::
+
+    Users.query(['name', 'login', 'user_email', 'signature'])
+         .filter([['active', '=', true], ['company_id', '=', main_company]])
+         .limit(15)
+         .all().then(function (users) {
+        // do work with users records
+    });
+
+The query is only actually performed when calling one of the query
+serialization methods, :func:`~openerp.web.Query.all` and
+:func:`~openerp.web.Query.first`. These methods will perform a new
+RPC call every time they are called.
+
+For that reason, it's actually possible to keep "intermediate" queries
+around and use them differently/add new specifications on them.
+
+.. class:: openerp.Model(name)
+
+    .. attribute:: openerp.Model.name
+
+        name of the OpenERP model this object is bound to
+
+    .. function:: openerp.Model.call(method[, args][, kwargs])
+
+         Calls the ``method`` method of the current model, with the
+         provided positional and keyword arguments.
+
+         :param String method: method to call over rpc on the
+                               :attr:`~openerp.Model.name`
+         :param Array<> args: positional arguments to pass to the
+                              method, optional
+         :param Object<> kwargs: keyword arguments to pass to the
+                                 method, optional
+         :rtype: Deferred<>
+
+    .. function:: openerp.Model.query(fields)
+
+         :param Array<String> fields: list of fields to fetch during
+                                      the search
+         :returns: a :class:`~openerp.web.Query` object
+                   representing the search to perform
+
+.. class:: openerp.web.Query(fields)
+
+    The first set of methods is the "fetching" methods. They perform
+    RPC queries using the internal data of the object they're called
+    on.
+
+    .. function:: openerp.web.Query.all()
+
+        Fetches the result of the current :class:`~openerp.web.Query` object's
+        search.
+
+        :rtype: Deferred<Array<>>
+
+    .. function:: openerp.web.Query.first()
+
+       Fetches the **first** result of the current
+       :class:`~openerp.web.Query`, or ``null`` if the current
+       :class:`~openerp.web.Query` does have any result.
+
+       :rtype: Deferred<Object | null>
+
+    .. function:: openerp.web.Query.count()
+
+       Fetches the number of records the current
+       :class:`~openerp.web.Query` would retrieve.
+
+       :rtype: Deferred<Number>
+
+    .. function:: openerp.web.Query.group_by(grouping...)
+
+       Fetches the groups for the query, using the first specified
+       grouping parameter
+
+       :param Array<String> grouping: Lists the levels of grouping
+                                      asked of the server. Grouping
+                                      can actually be an array or
+                                      varargs.
+       :rtype: Deferred<Array<openerp.web.QueryGroup>> | null
+
+    The second set of methods is the "mutator" methods, they create a
+    **new** :class:`~openerp.web.Query` object with the relevant
+    (internal) attribute either augmented or replaced.
+
+    .. function:: openerp.web.Query.context(ctx)
+
+       Adds the provided ``ctx`` to the query, on top of any existing
+       context
+
+    .. function:: openerp.web.Query.filter(domain)
+
+       Adds the provided domain to the query, this domain is
+       ``AND``-ed to the existing query domain.
+
+    .. function:: opeenrp.web.Query.offset(offset)
+
+       Sets the provided offset on the query. The new offset
+       *replaces* the old one.
+
+    .. function:: openerp.web.Query.limit(limit)
+
+       Sets the provided limit on the query. The new limit *replaces*
+       the old one.
+
+    .. function:: openerp.web.Query.order_by(fields…)
+
+       Overrides the model's natural order with the provided field
+       specifications. Behaves much like Django's :py:meth:`QuerySet.order_by
+       <django.db.models.query.QuerySet.order_by>`:
+
+       * Takes 1..n field names, in order of most to least importance
+         (the first field is the first sorting key). Fields are
+         provided as strings.
+
+       * A field specifies an ascending order, unless it is prefixed
+         with the minus sign "``-``" in which case the field is used
+         in the descending order
+
+       Divergences from Django's sorting include a lack of random sort
+       (``?`` field) and the inability to "drill down" into relations
+       for sorting.
+
+Aggregation (grouping)
+''''''''''''''''''''''
+
+Odoo has powerful grouping capacities, but they are kind-of strange
+in that they're recursive, and level n+1 relies on data provided
+directly by the grouping at level n. As a result, while
+:py:meth:`openerp.models.Model.read_group` works it's not a very intuitive
+API.
+
+Odoo Web eschews direct calls to :py:meth:`~openerp.models.Model.read_group`
+in favor of calling a method of :class:`~openerp.web.Query`, :py:meth:`much
+in the way it is one in SQLAlchemy <sqlalchemy.orm.query.Query.group_by>`
+[#terminal]_::
+
+    some_query.group_by(['field1', 'field2']).then(function (groups) {
+        // do things with the fetched groups
+    });
+
+This method is asynchronous when provided with 1..n fields (to group
+on) as argument, but it can also be called without any field (empty
+fields collection or nothing at all). In this case, instead of
+returning a Deferred object it will return ``null``.
+
+When grouping criterion come from a third-party and may or may not
+list fields (e.g. could be an empty list), this provides two ways to
+test the presence of actual subgroups (versus the need to perform a
+regular query for records):
+
+* A check on ``group_by``'s result and two completely separate code
+  paths::
+
+      var groups;
+      if (groups = some_query.group_by(gby)) {
+          groups.then(function (gs) {
+              // groups
+          });
+      }
+      // no groups
+
+* Or a more coherent code path using :func:`when`'s ability to
+  coerce values into deferreds::
+
+      $.when(some_query.group_by(gby)).then(function (groups) {
+          if (!groups) {
+              // No grouping
+          } else {
+              // grouping, even if there are no groups (groups
+              // itself could be an empty array)
+          }
+      });
+
+The result of a (successful) :func:`~openerp.web.Query.group_by` is
+an array of :class:`~openerp.web.QueryGroup`:
+
+.. class:: openerp.web.QueryGroup
+
+    .. function:: openerp.web.QueryGroup.get(key)
+
+        returns the group's attribute ``key``. Known attributes are:
+
+        ``grouped_on``
+            which grouping field resulted from this group
+        ``value``
+            ``grouped_on``'s value for this group
+        ``length``
+            the number of records in the group
+        ``aggregates``
+            a {field: value} mapping of aggregations for the group
+
+    .. function:: openerp.web.QueryGroup.query([fields...])
+
+        equivalent to :func:`openerp.web.Model.query` but pre-filtered to
+        only include the records within this group. Returns a
+        :class:`~openerp.web.Query` which can be further manipulated as
+        usual.
+
+    .. function:: openerp.web.QueryGroup.subgroups()
+
+        returns a deferred to an array of :class:`~openerp.web.QueryGroup`
+        below this one
+
+Low-level API: RPC calls to Python side
+---------------------------------------
+
+While the previous section is great for calling core OpenERP code
+(models code), it does not work if you want to call the Python side of
+Odoo Web.
+
+For this, a lower-level API exists on on
+:class:`~openerp.web.Session` objects (usually available through
+``openerp.session``): the ``rpc`` method.
+
+This method simply takes an absolute path (the absolute URL of the JSON
+:ref:`route <reference/http/routing>` to call) and a mapping of attributes to
+values (passed as keyword arguments to the Python method). This function
+fetches the return value of the Python methods, converted to JSON.
+
+For instance, to call the ``resequence`` of the
+:class:`~web.controllers.main.DataSet` controller::
+
+    openerp.session.rpc('/web/dataset/resequence', {
+        model: some_model,
+        ids: array_of_ids,
+        offset: 42
+    }).then(function (result) {
+        // resequence didn't error out
+    }, function () {
+        // an error occured during during call
+    });
+
 .. _reference/javascript/client:
 
 Web Client
 ==========
 
+Testing in Odoo Web Client
+==========================
+
+Javascript Unit Testing
+-----------------------
+
+Odoo Web includes means to unit-test both the core code of
+Odoo Web and your own javascript modules. On the javascript side,
+unit-testing is based on QUnit_ with a number of helpers and
+extensions for better integration with Odoo.
+
+To see what the runner looks like, find (or start) an Odoo server
+with the web client enabled, and navigate to ``/web/tests``
+This will show the runner selector, which lists all modules with javascript
+unit tests, and allows starting any of them (or all javascript tests in all
+modules at once).
+
+.. image:: ./images/runner.png
+    :align: center
+
+Clicking any runner button will launch the corresponding tests in the
+bundled QUnit_ runner:
+
+.. image:: ./images/tests.png
+    :align: center
+
+Writing a test case
+-------------------
+
+The first step is to list the test file(s). This is done through the
+``test`` key of the Odoo manifest, by adding javascript files to it:
+
+.. code-block:: python
+
+    {
+        'name': "Demonstration of web/javascript tests",
+        'category': 'Hidden',
+        'depends': ['web'],
+        'test': ['static/test/demo.js'],
+    }
+
+and to create the corresponding test file(s)
+
+.. note::
+
+    Test files which do not exist will be ignored, if all test files
+    of a module are ignored (can not be found), the test runner will
+    consider that the module has no javascript tests.
+
+After that, refreshing the runner selector will display the new module
+and allow running all of its (0 so far) tests:
+
+.. image:: ./images/runner2.png
+    :align: center
+
+The next step is to create a test case::
+
+    openerp.testing.section('basic section', function (test) {
+        test('my first test', function () {
+            ok(false, "this test has run");
+        });
+    });
+
+All testing helpers and structures live in the ``openerp.testing``
+module. Odoo tests live in a :func:`~openerp.testing.section`,
+which is itself part of a module. The first argument to a section is
+the name of the section, the second one is the section body.
+
+:func:`test <openerp.testing.case>`, provided by the
+:func:`~openerp.testing.section` to the callback, is used to
+register a given test case which will be run whenever the test runner
+actually does its job. Odoo Web test case use standard `QUnit
+assertions`_ within them.
+
+Launching the test runner at this point will run the test and display
+the corresponding assertion message, with red colors indicating the
+test failed:
+
+.. image:: ./images/tests2.png
+    :align: center
+
+Fixing the test (by replacing ``false`` to ``true`` in the assertion)
+will make it pass:
+
+.. image:: ./images/tests3.png
+    :align: center
+
+Assertions
+----------
+
+As noted above, Odoo Web's tests use `qunit assertions`_. They are
+available globally (so they can just be called without references to
+anything). The following list is available:
+
+.. function:: ok(state[, message])
+
+    checks that ``state`` is truthy (in the javascript sense)
+
+.. function:: strictEqual(actual, expected[, message])
+
+    checks that the actual (produced by a method being tested) and
+    expected values are identical (roughly equivalent to ``ok(actual
+    === expected, message)``)
+
+.. function:: notStrictEqual(actual, expected[, message])
+
+    checks that the actual and expected values are *not* identical
+    (roughly equivalent to ``ok(actual !== expected, message)``)
+
+.. function:: deepEqual(actual, expected[, message])
+
+    deep comparison between actual and expected: recurse into
+    containers (objects and arrays) to ensure that they have the same
+    keys/number of elements, and the values match.
+
+.. function:: notDeepEqual(actual, expected[, message])
+
+    inverse operation to :func:`deepEqual`
+
+.. function:: throws(block[, expected][, message])
+
+    checks that, when called, the ``block`` throws an
+    error. Optionally validates that error against ``expected``.
+
+    :param Function block:
+    :param expected: if a regexp, checks that the thrown error's
+                     message matches the regular expression. If an
+                     error type, checks that the thrown error is of
+                     that type.
+    :type expected: Error | RegExp
+
+.. function:: equal(actual, expected[, message])
+
+    checks that ``actual`` and ``expected`` are loosely equal, using
+    the ``==`` operator and its coercion rules.
+
+.. function:: notEqual(actual, expected[, message])
+
+    inverse operation to :func:`equal`
+
+Getting an Odoo instance
+------------------------
+
+The Odoo instance is the base through which most Odoo Web
+modules behaviors (functions, objects, …) are accessed. As a result,
+the test framework automatically builds one, and loads the module
+being tested and all of its dependencies inside it. This new instance
+is provided as the first positional parameter to your test
+cases. Let's observe by adding javascript code (not test code) to the
+test module:
+
+.. code-block:: python
+
+    {
+        'name': "Demonstration of web/javascript tests",
+        'category': 'Hidden',
+        'depends': ['web'],
+        'js': ['static/src/js/demo.js'],
+        'test': ['static/test/demo.js'],
+    }
+
+::
+
+    // src/js/demo.js
+    openerp.web_tests_demo = function (instance) {
+        instance.web_tests_demo = {
+            value_true: true,
+            SomeType: instance.web.Class.extend({
+                init: function (value) {
+                    this.value = value;
+                }
+            })
+        };
+    };
+
+and then adding a new test case, which simply checks that the
+``instance`` contains all the expected stuff we created in the
+module::
+
+    // test/demo.js
+    test('module content', function (instance) {
+        ok(instance.web_tests_demo.value_true, "should have a true value");
+        var type_instance = new instance.web_tests_demo.SomeType(42);
+        strictEqual(type_instance.value, 42, "should have provided value");
+    });
+
+DOM Scratchpad
+--------------
+
+As in the wider client, arbitrarily accessing document content is
+strongly discouraged during tests. But DOM access is still needed to
+e.g. fully initialize :class:`widgets <~openerp.Widget>` before
+testing them.
+
+Thus, a test case gets a DOM scratchpad as its second positional
+parameter, in a jQuery instance. That scratchpad is fully cleaned up
+before each test, and as long as it doesn't do anything outside the
+scratchpad your code can do whatever it wants::
+
+    // test/demo.js
+    test('DOM content', function (instance, $scratchpad) {
+        $scratchpad.html('<div><span class="foo bar">ok</span></div>');
+        ok($scratchpad.find('span').hasClass('foo'),
+           "should have provided class");
+    });
+    test('clean scratchpad', function (instance, $scratchpad) {
+        ok(!$scratchpad.children().length, "should have no content");
+        ok(!$scratchpad.text(), "should have no text");
+    });
+
+.. note::
+
+    The top-level element of the scratchpad is not cleaned up, test
+    cases can add text or DOM children but shoud not alter
+    ``$scratchpad`` itself.
+
+Loading templates
+-----------------
+
+To avoid the corresponding processing costs, by default templates are
+not loaded into QWeb. If you need to render e.g. widgets making use of
+QWeb templates, you can request their loading through the
+:attr:`~TestOptions.templates` option to the :func:`test case
+function <openerp.testing.case>`.
+
+This will automatically load all relevant templates in the instance's
+qweb before running the test case:
+
+.. code-block:: python
+
+    {
+        'name': "Demonstration of web/javascript tests",
+        'category': 'Hidden',
+        'depends': ['web'],
+        'js': ['static/src/js/demo.js'],
+        'test': ['static/test/demo.js'],
+        'qweb': ['static/src/xml/demo.xml'],
+    }
+
+.. code-block:: xml
+
+    <!-- src/xml/demo.xml -->
+    <templates id="template" xml:space="preserve">
+        <t t-name="DemoTemplate">
+            <t t-foreach="5" t-as="value">
+                <p><t t-esc="value"/></p>
+            </t>
+        </t>
+    </templates>
+
+::
+
+    // test/demo.js
+    test('templates', {templates: true}, function (instance) {
+        var s = instance.web.qweb.render('DemoTemplate');
+        var texts = $(s).find('p').map(function () {
+            return $(this).text();
+        }).get();
+
+        deepEqual(texts, ['0', '1', '2', '3', '4']);
+    });
+
+Asynchronous cases
+------------------
+
+The test case examples so far are all synchronous, they execute from
+the first to the last line and once the last line has executed the
+test is done. But the web client is full of :ref:`asynchronous code
+<reference/async>`, and thus test cases need to be async-aware.
+
+This is done by returning a :class:`deferred <Deferred>` from the
+case callback::
+
+    // test/demo.js
+    test('asynchronous', {
+        asserts: 1
+    }, function () {
+        var d = $.Deferred();
+        setTimeout(function () {
+            ok(true);
+            d.resolve();
+        }, 100);
+        return d;
+    });
+
+This example also uses the :class:`options parameter <TestOptions>`
+to specify the number of assertions the case should expect, if less or
+more assertions are specified the case will count as failed.
+
+Asynchronous test cases *must* specify the number of assertions they
+will run. This allows more easily catching situations where e.g. the
+test architecture was not warned about asynchronous operations.
+
+.. note::
+
+    Asynchronous test cases also have a 2 seconds timeout: if the test
+    does not finish within 2 seconds, it will be considered
+    failed. This pretty much always means the test will not
+    resolve. This timeout *only* applies to the test itself, not to
+    the setup and teardown processes.
+
+.. note::
+
+    If the returned deferred is rejected, the test will be failed
+    unless :attr:`~TestOptions.fail_on_rejection` is set to
+    ``false``.
+
+RPC
+---
+
+An important subset of asynchronous test cases is test cases which
+need to perform (and chain, to an extent) RPC calls.
+
+.. note::
+
+    Because they are a subset of asynchronous cases, RPC cases must
+    also provide a valid :attr:`assertions count
+    <TestOptions.asserts>`.
+
+To enable mock RPC, set the :attr:`rpc option <TestOptions.rpc>` to
+``mock``. This will add a third parameter to the test case callback:
+
+.. function:: mock(rpc_spec, handler)
+
+    Can be used in two different ways depending on the shape of the
+    first parameter:
+
+    * If it matches the pattern ``model:method`` (if it contains a
+      colon, essentially) the call will set up the mocking of an RPC
+      call straight to the Odoo server (through XMLRPC) as
+      performed via e.g. :func:`openerp.web.Model.call`.
+
+      In that case, ``handler`` should be a function taking two
+      arguments ``args`` and ``kwargs``, matching the corresponding
+      arguments on the server side and should simply return the value
+      as if it were returned by the Python XMLRPC handler::
+
+          test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) {
+              // set up mocking
+              mock('people.famous:name_search', function (args, kwargs) {
+                  strictEqual(kwargs.name, 'bob');
+                  return [
+                      [1, "Microsoft Bob"],
+                      [2, "Bob the Builder"],
+                      [3, "Silent Bob"]
+                  ];
+              });
+
+              // actual test code
+              return new instance.web.Model('people.famous')
+                  .call('name_search', {name: 'bob'}).then(function (result) {
+                      strictEqual(result.length, 3, "shoud return 3 people");
+                      strictEqual(result[0][1], "Microsoft Bob",
+                          "the most famous bob should be Microsoft Bob");
+                  });
+          });
+
+    * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it
+      will mock a JSON-RPC call to a web client controller, such as
+      ``/web/webclient/translations``. In that case, the handler takes
+      a single ``params`` argument holding all of the parameters
+      provided over JSON-RPC.
+
+      As previously, the handler should simply return the result value
+      as if returned by the original JSON-RPC handler::
+
+          test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) {
+              var fetched_dbs = false, fetched_langs = false;
+              mock('/web/database/get_list', function () {
+                  fetched_dbs = true;
+                  return ['foo', 'bar', 'baz'];
+              });
+              mock('/web/session/get_lang_list', function () {
+                  fetched_langs = true;
+                  return [['vo_IS', 'Hopelandic / Vonlenska']];
+              });
+
+              // widget needs that or it blows up
+              instance.webclient = {toggle_bars: openerp.testing.noop};
+              var dbm = new instance.web.DatabaseManager({});
+              return dbm.appendTo($s).then(function () {
+                  ok(fetched_dbs, "should have fetched databases");
+                  ok(fetched_langs, "should have fetched languages");
+                  deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
+              });
+          });
+
+.. note::
+
+    Mock handlers can contain assertions, these assertions should be
+    part of the assertions count (and if multiple calls are made to a
+    handler containing assertions, it multiplies the effective number
+    of assertions).
+
+Testing API
+-----------
+
+.. function:: openerp.testing.section(name[, options], body)
+
+    A test section, serves as shared namespace for related tests (for
+    constants or values to only set up once). The ``body`` function
+    should contain the tests themselves.
+
+    Note that the order in which tests are run is essentially
+    undefined, do *not* rely on it.
+
+    :param String name:
+    :param TestOptions options:
+    :param body:
+    :type body: Function<:func:`~openerp.testing.case`, void>
+
+.. function:: openerp.testing.case(name[, options], callback)
+
+    Registers a test case callback in the test runner, the callback
+    will only be run once the runner is started (or maybe not at all,
+    if the test is filtered out).
+
+    :param String name:
+    :param TestOptions options:
+    :param callback:
+    :type callback: Function<instance, $, Function<String, Function, void>>
+
+.. class:: TestOptions
+
+    the various options which can be passed to
+    :func:`~openerp.testing.section` or
+    :func:`~openerp.testing.case`. Except for
+    :attr:`~TestOptions.setup` and
+    :attr:`~TestOptions.teardown`, an option on
+    :func:`~openerp.testing.case` will overwrite the corresponding
+    option on :func:`~openerp.testing.section` so
+    e.g. :attr:`~TestOptions.rpc` can be set for a
+    :func:`~openerp.testing.section` and then differently set for
+    some :func:`~openerp.testing.case` of that
+    :func:`~openerp.testing.section`
+
+    .. attribute:: TestOptions.asserts
+
+        An integer, the number of assertions which should run during a
+        normal execution of the test. Mandatory for asynchronous tests.
+
+    .. attribute:: TestOptions.setup
+
+        Test case setup, run right before each test case. A section's
+        :func:`~TestOptions.setup` is run before the case's own, if
+        both are specified.
+
+    .. attribute:: TestOptions.teardown
+
+        Test case teardown, a case's :func:`~TestOptions.teardown`
+        is run before the corresponding section if both are present.
+
+    .. attribute:: TestOptions.fail_on_rejection
+
+        If the test is asynchronous and its resulting promise is
+        rejected, fail the test. Defaults to ``true``, set to
+        ``false`` to not fail the test in case of rejection::
+
+            // test/demo.js
+            test('unfail rejection', {
+                asserts: 1,
+                fail_on_rejection: false
+            }, function () {
+                var d = $.Deferred();
+                setTimeout(function () {
+                    ok(true);
+                    d.reject();
+                }, 100);
+                return d;
+            });
+
+    .. attribute:: TestOptions.rpc
+
+        RPC method to use during tests, one of ``"mock"`` or
+        ``"rpc"``. Any other value will disable RPC for the test (if
+        they were enabled by the suite for instance).
+
+    .. attribute:: TestOptions.templates
+
+        Whether the current module (and its dependencies)'s templates
+        should be loaded into QWeb before starting the test. A
+        boolean, ``false`` by default.
+
+The test runner can also use two global configuration values set
+directly on the ``window`` object:
+
+* ``oe_all_dependencies`` is an ``Array`` of all modules with a web
+  component, ordered by dependency (for a module ``A`` with
+  dependencies ``A'``, any module of ``A'`` must come before ``A`` in
+  the array)
+
+Running through Python
+----------------------
+
+The web client includes the means to run these tests on the
+command-line (or in a CI system), but while actually running it is
+pretty simple the setup of the pre-requisite parts has some
+complexities.
+
+#. Install unittest2_ in your Python environment. Both
+   can trivially be installed via `pip <http://pip-installer.org>`_ or
+   `easy_install
+   <http://packages.python.org/distribute/easy_install.html>`_.
+
+#. Install PhantomJS_. It is a headless
+   browser which allows automating running and testing web
+   pages. QUnitSuite_ uses it to actually run the qunit_ test suite.
+
+   The PhantomJS_ website provides pre-built binaries for some
+   platforms, and your OS's package management probably provides it as
+   well.
+
+   If you're building PhantomJS_ from source, I recommend preparing
+   for some knitting time as it's not exactly fast (it needs to
+   compile both `Qt <http://qt-project.org/>`_ and `Webkit
+   <http://www.webkit.org/>`_, both being pretty big projects).
+
+   .. note::
+
+       Because PhantomJS_ is webkit-based, it will not be able to test
+       if Firefox, Opera or Internet Explorer can correctly run the
+       test suite (and it is only an approximation for Safari and
+       Chrome). It is therefore recommended to *also* run the test
+       suites in actual browsers once in a while.
+
+   .. note::
+
+       The version of PhantomJS_ this was build through is 1.7,
+       previous versions *should* work but are not actually supported
+       (and tend to just segfault when something goes wrong in
+       PhantomJS_ itself so they're a pain to debug).
+
+#. Install a new database with all relevant modules (all modules with
+   a web component at least), then restart the server
+
+   .. note::
+
+       For some tests, a source database needs to be duplicated. This
+       operation requires that there be no connection to the database
+       being duplicated, but Odoo doesn't currently break
+       existing/outstanding connections, so restarting the server is
+       the simplest way to ensure everything is in the right state.
+
+#. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct
+   addons-path specified (and replacing ``$DATABASE`` by the source
+   database you created above)
+
+   .. note::
+
+       If you leave out ``-mweb``, the runner will attempt to run all
+       the tests in all the modules, which may or may not work.
+
+If everything went correctly, you should now see a list of tests with
+(hopefully) ``ok`` next to their names, closing with a report of the
+number of tests run and the time it took:
+
+.. literalinclude:: test-report.txt
+    :language: text
+
+Congratulation, you have just performed a successful "offline" run of
+the OpenERP Web test suite.
+
+.. note::
+
+    Note that this runs all the Python tests for the ``web`` module,
+    but all the web tests for all of Odoo. This can be surprising.
+
+.. _qunit: http://qunitjs.com/
+
+.. _qunit assertions: http://api.qunitjs.com/category/assert/
+
+.. _unittest2: http://pypi.python.org/pypi/unittest2
+
+.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/
+
+.. _PhantomJS: http://phantomjs.org/
+
 .. [#eventsdelegation] not all DOM events are compatible with events delegation
 
+.. [#terminal]
+    with a small twist: :py:meth:`sqlalchemy.orm.query.Query.group_by` is not
+    terminal, it returns a query which can still be altered.
+
diff --git a/doc/reference/test-report.txt b/doc/reference/test-report.txt
new file mode 100644 (file)
index 0000000..ce52618
--- /dev/null
@@ -0,0 +1,25 @@
+test_empty_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
+test_ids_shortcut (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
+test_regular_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
+web.testing.stack: direct, value, success ... ok
+web.testing.stack: direct, deferred, success ... ok
+web.testing.stack: direct, value, error ... ok
+web.testing.stack: direct, deferred, failure ... ok
+web.testing.stack: successful setup ... ok
+web.testing.stack: successful teardown ... ok
+web.testing.stack: successful setup and teardown ... ok
+
+[snip ~150 lines]
+
+test_convert_complex_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
+test_convert_complex_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
+test_convert_literal_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
+test_convert_literal_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
+test_retrieve_nonliteral_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
+test_retrieve_nonliteral_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
+
+----------------------------------------------------------------------
+Ran 181 tests in 15.706s
+
+OK
+