+++ /dev/null
-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
:maxdepth: 1
guidelines
- rpc
- async
client_action
testing
+++ /dev/null
-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.
+++ /dev/null
-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
-
+++ /dev/null
-.. 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/
+++ /dev/null
-
-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``.
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'
--- /dev/null
+: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
Web Controllers
===============
+.. _reference/http/routing:
+
Routing
=======
.. 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:
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`
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()
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])
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);
: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
''''''''''''''''''''''
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::
.. 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`.
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.
+
--- /dev/null
+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
+