Merge pull request #2375 from odoo-dev/8.0-webclient-doc-xmo
Port web client tutorial fully (-ish), improve JS doc.
|
@ -34,9 +34,6 @@ Javascript
|
|||
:maxdepth: 1
|
||||
|
||||
guidelines
|
||||
widget
|
||||
rpc
|
||||
async
|
||||
client_action
|
||||
testing
|
||||
|
||||
|
|
|
@ -1,279 +0,0 @@
|
|||
RPC Calls
|
||||
=========
|
||||
|
||||
Building static displays is all nice and good and allows for neat
|
||||
effects (and sometimes you're given data to display from third parties
|
||||
so you don't have to make any effort), but a point generally comes
|
||||
where you'll want to talk to the world and make some network requests.
|
||||
|
||||
OpenERP Web provides two primary APIs to handle this, a low-level
|
||||
JSON-RPC based API communicating with the Python section of OpenERP
|
||||
Web (and of your addon, if you have a Python part) and a high-level
|
||||
API above that allowing your code to talk directly to the OpenERP
|
||||
server, using familiar-looking calls.
|
||||
|
||||
All networking APIs are :doc:`asynchronous </async>`. As a result, all
|
||||
of them will return :js:class:`Deferred` objects (whether they resolve
|
||||
those with values or not). Understanding how those work before before
|
||||
moving on is probably necessary.
|
||||
|
||||
High-level API: calling into OpenERP models
|
||||
-------------------------------------------
|
||||
|
||||
Access to OpenERP object methods (made available through XML-RPC from
|
||||
the server) is done via the :js:class:`openerp.web.Model` class. This
|
||||
class maps onto the OpenERP server objects via two primary methods,
|
||||
:js:func:`~openerp.web.Model.call` and
|
||||
:js:func:`~openerp.web.Model.query`.
|
||||
|
||||
:js:func:`~openerp.web.Model.call` is a direct mapping to the
|
||||
corresponding method of the OpenERP server object. Its usage is
|
||||
similar to that of the OpenERP Model API, with three differences:
|
||||
|
||||
* The interface is :doc:`asynchronous </async>`, so instead of
|
||||
returning results directly RPC method calls will return
|
||||
:js:class:`Deferred` instances, which will themselves resolve to the
|
||||
result of the matching RPC call.
|
||||
|
||||
* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to
|
||||
``__getattr__`` or ``method_missing``, there needs to be an explicit
|
||||
method to dispatch RPC methods.
|
||||
|
||||
* No notion of pooler, the model proxy is instantiated where needed,
|
||||
not fetched from an other (somewhat global) object
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var Users = new Model('res.users');
|
||||
|
||||
Users.call('change_password', ['oldpassword', 'newpassword'],
|
||||
{context: some_context}).then(function (result) {
|
||||
// do something with change_password result
|
||||
});
|
||||
|
||||
:js:func:`~openerp.web.Model.query` is a shortcut for a builder-style
|
||||
interface to searches (``search`` + ``read`` in OpenERP RPC terms). It
|
||||
returns a :js:class:`~openerp.web.Query` object which is immutable but
|
||||
allows building new :js:class:`~openerp.web.Query` instances from the
|
||||
first one, adding new properties or modifiying the parent object's:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
Users.query(['name', 'login', 'user_email', 'signature'])
|
||||
.filter([['active', '=', true], ['company_id', '=', main_company]])
|
||||
.limit(15)
|
||||
.all().then(function (users) {
|
||||
// do work with users records
|
||||
});
|
||||
|
||||
The query is only actually performed when calling one of the query
|
||||
serialization methods, :js:func:`~openerp.web.Query.all` and
|
||||
:js:func:`~openerp.web.Query.first`. These methods will perform a new
|
||||
RPC call every time they are called.
|
||||
|
||||
For that reason, it's actually possible to keep "intermediate" queries
|
||||
around and use them differently/add new specifications on them.
|
||||
|
||||
.. js:class:: openerp.web.Model(name)
|
||||
|
||||
.. js:attribute:: openerp.web.Model.name
|
||||
|
||||
name of the OpenERP model this object is bound to
|
||||
|
||||
.. js:function:: openerp.web.Model.call(method[, args][, kwargs])
|
||||
|
||||
Calls the ``method`` method of the current model, with the
|
||||
provided positional and keyword arguments.
|
||||
|
||||
:param String method: method to call over rpc on the
|
||||
:js:attr:`~openerp.web.Model.name`
|
||||
:param Array<> args: positional arguments to pass to the
|
||||
method, optional
|
||||
:param Object<> kwargs: keyword arguments to pass to the
|
||||
method, optional
|
||||
:rtype: Deferred<>
|
||||
|
||||
.. js:function:: openerp.web.Model.query(fields)
|
||||
|
||||
:param Array<String> fields: list of fields to fetch during
|
||||
the search
|
||||
:returns: a :js:class:`~openerp.web.Query` object
|
||||
representing the search to perform
|
||||
|
||||
.. js:class:: openerp.web.Query(fields)
|
||||
|
||||
The first set of methods is the "fetching" methods. They perform
|
||||
RPC queries using the internal data of the object they're called
|
||||
on.
|
||||
|
||||
.. js:function:: openerp.web.Query.all()
|
||||
|
||||
Fetches the result of the current
|
||||
:js:class:`~openerp.web.Query` object's search.
|
||||
|
||||
:rtype: Deferred<Array<>>
|
||||
|
||||
.. js:function:: openerp.web.Query.first()
|
||||
|
||||
Fetches the **first** result of the current
|
||||
:js:class:`~openerp.web.Query`, or ``null`` if the current
|
||||
:js:class:`~openerp.web.Query` does have any result.
|
||||
|
||||
:rtype: Deferred<Object | null>
|
||||
|
||||
.. js:function:: openerp.web.Query.count()
|
||||
|
||||
Fetches the number of records the current
|
||||
:js:class:`~openerp.web.Query` would retrieve.
|
||||
|
||||
:rtype: Deferred<Number>
|
||||
|
||||
.. js:function:: openerp.web.Query.group_by(grouping...)
|
||||
|
||||
Fetches the groups for the query, using the first specified
|
||||
grouping parameter
|
||||
|
||||
:param Array<String> grouping: Lists the levels of grouping
|
||||
asked of the server. Grouping
|
||||
can actually be an array or
|
||||
varargs.
|
||||
:rtype: Deferred<Array<openerp.web.QueryGroup>> | null
|
||||
|
||||
The second set of methods is the "mutator" methods, they create a
|
||||
**new** :js:class:`~openerp.web.Query` object with the relevant
|
||||
(internal) attribute either augmented or replaced.
|
||||
|
||||
.. js:function:: openerp.web.Query.context(ctx)
|
||||
|
||||
Adds the provided ``ctx`` to the query, on top of any existing
|
||||
context
|
||||
|
||||
.. js:function:: openerp.web.Query.filter(domain)
|
||||
|
||||
Adds the provided domain to the query, this domain is
|
||||
``AND``-ed to the existing query domain.
|
||||
|
||||
.. js:function:: opeenrp.web.Query.offset(offset)
|
||||
|
||||
Sets the provided offset on the query. The new offset
|
||||
*replaces* the old one.
|
||||
|
||||
.. js:function:: openerp.web.Query.limit(limit)
|
||||
|
||||
Sets the provided limit on the query. The new limit *replaces*
|
||||
the old one.
|
||||
|
||||
.. js:function:: openerp.web.Query.order_by(fields…)
|
||||
|
||||
Overrides the model's natural order with the provided field
|
||||
specifications. Behaves much like Django's `QuerySet.order_by
|
||||
<https://docs.djangoproject.com/en/dev/ref/models/querysets/#order-by>`_:
|
||||
|
||||
* Takes 1..n field names, in order of most to least importance
|
||||
(the first field is the first sorting key). Fields are
|
||||
provided as strings.
|
||||
|
||||
* A field specifies an ascending order, unless it is prefixed
|
||||
with the minus sign "``-``" in which case the field is used
|
||||
in the descending order
|
||||
|
||||
Divergences from Django's sorting include a lack of random sort
|
||||
(``?`` field) and the inability to "drill down" into relations
|
||||
for sorting.
|
||||
|
||||
Aggregation (grouping)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
OpenERP has powerful grouping capacities, but they are kind-of strange
|
||||
in that they're recursive, and level n+1 relies on data provided
|
||||
directly by the grouping at level n. As a result, while ``read_group``
|
||||
works it's not a very intuitive API.
|
||||
|
||||
OpenERP Web 7.0 eschews direct calls to ``read_group`` in favor of
|
||||
calling a method of :js:class:`~openerp.web.Query`, `much in the way
|
||||
it is one in SQLAlchemy
|
||||
<http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.group_by>`_ [#]_:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
some_query.group_by(['field1', 'field2']).then(function (groups) {
|
||||
// do things with the fetched groups
|
||||
});
|
||||
|
||||
This method is asynchronous when provided with 1..n fields (to group
|
||||
on) as argument, but it can also be called without any field (empty
|
||||
fields collection or nothing at all). In this case, instead of
|
||||
returning a Deferred object it will return ``null``.
|
||||
|
||||
When grouping criterion come from a third-party and may or may not
|
||||
list fields (e.g. could be an empty list), this provides two ways to
|
||||
test the presence of actual subgroups (versus the need to perform a
|
||||
regular query for records):
|
||||
|
||||
* A check on ``group_by``'s result and two completely separate code
|
||||
paths
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var groups;
|
||||
if (groups = some_query.group_by(gby)) {
|
||||
groups.then(function (gs) {
|
||||
// groups
|
||||
});
|
||||
}
|
||||
// no groups
|
||||
|
||||
* Or a more coherent code path using :js:func:`when`'s ability to
|
||||
coerce values into deferreds:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
$.when(some_query.group_by(gby)).then(function (groups) {
|
||||
if (!groups) {
|
||||
// No grouping
|
||||
} else {
|
||||
// grouping, even if there are no groups (groups
|
||||
// itself could be an empty array)
|
||||
}
|
||||
});
|
||||
|
||||
The result of a (successful) :js:func:`~openerp.web.Query.group_by` is
|
||||
an array of :js:class:`~openerp.web.QueryGroup`.
|
||||
|
||||
.. _rpc_rpc:
|
||||
|
||||
Low-level API: RPC calls to Python side
|
||||
---------------------------------------
|
||||
|
||||
While the previous section is great for calling core OpenERP code
|
||||
(models code), it does not work if you want to call the Python side of
|
||||
OpenERP Web.
|
||||
|
||||
For this, a lower-level API exists on on
|
||||
:js:class:`~openerp.web.Connection` objects (usually available through
|
||||
``openerp.connection``): the ``rpc`` method.
|
||||
|
||||
This method simply takes an absolute path (which is the combination of
|
||||
the Python controller's ``_cp_path`` attribute and the name of the
|
||||
method you want to call) and a mapping of attributes to values (applied
|
||||
as keyword arguments on the Python method [#]_). This function fetches
|
||||
the return value of the Python methods, converted to JSON.
|
||||
|
||||
For instance, to call the ``resequence`` of the
|
||||
:class:`~web.controllers.main.DataSet` controller:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
openerp.connection.rpc('/web/dataset/resequence', {
|
||||
model: some_model,
|
||||
ids: array_of_ids,
|
||||
offset: 42
|
||||
}).then(function (result) {
|
||||
// resequenced on server
|
||||
});
|
||||
|
||||
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
|
||||
is not terminal, it returns a query which can still be altered.
|
||||
|
||||
.. [#] except for ``context``, which is extracted and stored in the
|
||||
request object itself.
|
|
@ -1,697 +0,0 @@
|
|||
.. highlight:: javascript
|
||||
|
||||
.. _testing:
|
||||
|
||||
Testing in OpenERP Web
|
||||
======================
|
||||
|
||||
Javascript Unit Testing
|
||||
-----------------------
|
||||
|
||||
OpenERP Web 7.0 includes means to unit-test both the core code of
|
||||
OpenERP Web and your own javascript modules. On the javascript side,
|
||||
unit-testing is based on QUnit_ with a number of helpers and
|
||||
extensions for better integration with OpenERP.
|
||||
|
||||
To see what the runner looks like, find (or start) an OpenERP server
|
||||
with the web client enabled, and navigate to ``/web/tests`` e.g. `on
|
||||
OpenERP's CI <http://trunk.runbot.openerp.com/web/tests>`_. This will
|
||||
show the runner selector, which lists all modules with javascript unit
|
||||
tests, and allows starting any of them (or all javascript tests in all
|
||||
modules at once).
|
||||
|
||||
.. image:: ./images/runner.png
|
||||
:align: center
|
||||
|
||||
Clicking any runner button will launch the corresponding tests in the
|
||||
bundled QUnit_ runner:
|
||||
|
||||
.. image:: ./images/tests.png
|
||||
:align: center
|
||||
|
||||
Writing a test case
|
||||
-------------------
|
||||
|
||||
The first step is to list the test file(s). This is done through the
|
||||
``test`` key of the openerp manifest, by adding javascript files to it
|
||||
(next to the usual YAML files, if any):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
'name': "Demonstration of web/javascript tests",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
'test': ['static/test/demo.js'],
|
||||
}
|
||||
|
||||
and to create the corresponding test file(s)
|
||||
|
||||
.. note::
|
||||
|
||||
Test files which do not exist will be ignored, if all test files
|
||||
of a module are ignored (can not be found), the test runner will
|
||||
consider that the module has no javascript tests.
|
||||
|
||||
After that, refreshing the runner selector will display the new module
|
||||
and allow running all of its (0 so far) tests:
|
||||
|
||||
.. image:: ./images/runner2.png
|
||||
:align: center
|
||||
|
||||
The next step is to create a test case::
|
||||
|
||||
openerp.testing.section('basic section', function (test) {
|
||||
test('my first test', function () {
|
||||
ok(false, "this test has run");
|
||||
});
|
||||
});
|
||||
|
||||
All testing helpers and structures live in the ``openerp.testing``
|
||||
module. OpenERP tests live in a :js:func:`~openerp.testing.section`,
|
||||
which is itself part of a module. The first argument to a section is
|
||||
the name of the section, the second one is the section body.
|
||||
|
||||
:js:func:`test <openerp.testing.case>`, provided by the
|
||||
:js:func:`~openerp.testing.section` to the callback, is used to
|
||||
register a given test case which will be run whenever the test runner
|
||||
actually does its job. OpenERP Web test case use standard `QUnit
|
||||
assertions`_ within them.
|
||||
|
||||
Launching the test runner at this point will run the test and display
|
||||
the corresponding assertion message, with red colors indicating the
|
||||
test failed:
|
||||
|
||||
.. image:: ./images/tests2.png
|
||||
:align: center
|
||||
|
||||
Fixing the test (by replacing ``false`` to ``true`` in the assertion)
|
||||
will make it pass:
|
||||
|
||||
.. image:: ./images/tests3.png
|
||||
:align: center
|
||||
|
||||
Assertions
|
||||
----------
|
||||
|
||||
As noted above, OpenERP Web's tests use `qunit assertions`_. They are
|
||||
available globally (so they can just be called without references to
|
||||
anything). The following list is available:
|
||||
|
||||
.. js:function:: ok(state[, message])
|
||||
|
||||
checks that ``state`` is truthy (in the javascript sense)
|
||||
|
||||
.. js:function:: strictEqual(actual, expected[, message])
|
||||
|
||||
checks that the actual (produced by a method being tested) and
|
||||
expected values are identical (roughly equivalent to ``ok(actual
|
||||
=== expected, message)``)
|
||||
|
||||
.. js:function:: notStrictEqual(actual, expected[, message])
|
||||
|
||||
checks that the actual and expected values are *not* identical
|
||||
(roughly equivalent to ``ok(actual !== expected, message)``)
|
||||
|
||||
.. js:function:: deepEqual(actual, expected[, message])
|
||||
|
||||
deep comparison between actual and expected: recurse into
|
||||
containers (objects and arrays) to ensure that they have the same
|
||||
keys/number of elements, and the values match.
|
||||
|
||||
.. js:function:: notDeepEqual(actual, expected[, message])
|
||||
|
||||
inverse operation to :js:func:`deepEqual`
|
||||
|
||||
.. js:function:: throws(block[, expected][, message])
|
||||
|
||||
checks that, when called, the ``block`` throws an
|
||||
error. Optionally validates that error against ``expected``.
|
||||
|
||||
:param Function block:
|
||||
:param expected: if a regexp, checks that the thrown error's
|
||||
message matches the regular expression. If an
|
||||
error type, checks that the thrown error is of
|
||||
that type.
|
||||
:type expected: Error | RegExp
|
||||
|
||||
.. js:function:: equal(actual, expected[, message])
|
||||
|
||||
checks that ``actual`` and ``expected`` are loosely equal, using
|
||||
the ``==`` operator and its coercion rules.
|
||||
|
||||
.. js:function:: notEqual(actual, expected[, message])
|
||||
|
||||
inverse operation to :js:func:`equal`
|
||||
|
||||
Getting an OpenERP instance
|
||||
---------------------------
|
||||
|
||||
The OpenERP instance is the base through which most OpenERP Web
|
||||
modules behaviors (functions, objects, …) are accessed. As a result,
|
||||
the test framework automatically builds one, and loads the module
|
||||
being tested and all of its dependencies inside it. This new instance
|
||||
is provided as the first positional parameter to your test
|
||||
cases. Let's observe by adding javascript code (not test code) to the
|
||||
test module:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
'name': "Demonstration of web/javascript tests",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
'js': ['static/src/js/demo.js'],
|
||||
'test': ['static/test/demo.js'],
|
||||
}
|
||||
|
||||
::
|
||||
|
||||
// src/js/demo.js
|
||||
openerp.web_tests_demo = function (instance) {
|
||||
instance.web_tests_demo = {
|
||||
value_true: true,
|
||||
SomeType: instance.web.Class.extend({
|
||||
init: function (value) {
|
||||
this.value = value;
|
||||
}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
and then adding a new test case, which simply checks that the
|
||||
``instance`` contains all the expected stuff we created in the
|
||||
module::
|
||||
|
||||
// test/demo.js
|
||||
test('module content', function (instance) {
|
||||
ok(instance.web_tests_demo.value_true, "should have a true value");
|
||||
var type_instance = new instance.web_tests_demo.SomeType(42);
|
||||
strictEqual(type_instance.value, 42, "should have provided value");
|
||||
});
|
||||
|
||||
DOM Scratchpad
|
||||
--------------
|
||||
|
||||
As in the wider client, arbitrarily accessing document content is
|
||||
strongly discouraged during tests. But DOM access is still needed to
|
||||
e.g. fully initialize :js:class:`widgets <~openerp.web.Widget>` before
|
||||
testing them.
|
||||
|
||||
Thus, a test case gets a DOM scratchpad as its second positional
|
||||
parameter, in a jQuery instance. That scratchpad is fully cleaned up
|
||||
before each test, and as long as it doesn't do anything outside the
|
||||
scratchpad your code can do whatever it wants::
|
||||
|
||||
// test/demo.js
|
||||
test('DOM content', function (instance, $scratchpad) {
|
||||
$scratchpad.html('<div><span class="foo bar">ok</span></div>');
|
||||
ok($scratchpad.find('span').hasClass('foo'),
|
||||
"should have provided class");
|
||||
});
|
||||
test('clean scratchpad', function (instance, $scratchpad) {
|
||||
ok(!$scratchpad.children().length, "should have no content");
|
||||
ok(!$scratchpad.text(), "should have no text");
|
||||
});
|
||||
|
||||
.. note::
|
||||
|
||||
The top-level element of the scratchpad is not cleaned up, test
|
||||
cases can add text or DOM children but shoud not alter
|
||||
``$scratchpad`` itself.
|
||||
|
||||
Loading templates
|
||||
-----------------
|
||||
|
||||
To avoid the corresponding processing costs, by default templates are
|
||||
not loaded into QWeb. If you need to render e.g. widgets making use of
|
||||
QWeb templates, you can request their loading through the
|
||||
:js:attr:`~TestOptions.templates` option to the :js:func:`test case
|
||||
function <openerp.testing.case>`.
|
||||
|
||||
This will automatically load all relevant templates in the instance's
|
||||
qweb before running the test case:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
'name': "Demonstration of web/javascript tests",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
'js': ['static/src/js/demo.js'],
|
||||
'test': ['static/test/demo.js'],
|
||||
'qweb': ['static/src/xml/demo.xml'],
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<!-- src/xml/demo.xml -->
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="DemoTemplate">
|
||||
<t t-foreach="5" t-as="value">
|
||||
<p><t t-esc="value"/></p>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
::
|
||||
|
||||
// test/demo.js
|
||||
test('templates', {templates: true}, function (instance) {
|
||||
var s = instance.web.qweb.render('DemoTemplate');
|
||||
var texts = $(s).find('p').map(function () {
|
||||
return $(this).text();
|
||||
}).get();
|
||||
|
||||
deepEqual(texts, ['0', '1', '2', '3', '4']);
|
||||
});
|
||||
|
||||
Asynchronous cases
|
||||
------------------
|
||||
|
||||
The test case examples so far are all synchronous, they execute from
|
||||
the first to the last line and once the last line has executed the
|
||||
test is done. But the web client is full of :doc:`asynchronous code
|
||||
</async>`, and thus test cases need to be async-aware.
|
||||
|
||||
This is done by returning a :js:class:`deferred <Deferred>` from the
|
||||
case callback::
|
||||
|
||||
// test/demo.js
|
||||
test('asynchronous', {
|
||||
asserts: 1
|
||||
}, function () {
|
||||
var d = $.Deferred();
|
||||
setTimeout(function () {
|
||||
ok(true);
|
||||
d.resolve();
|
||||
}, 100);
|
||||
return d;
|
||||
});
|
||||
|
||||
This example also uses the :js:class:`options parameter <TestOptions>`
|
||||
to specify the number of assertions the case should expect, if less or
|
||||
more assertions are specified the case will count as failed.
|
||||
|
||||
Asynchronous test cases *must* specify the number of assertions they
|
||||
will run. This allows more easily catching situations where e.g. the
|
||||
test architecture was not warned about asynchronous operations.
|
||||
|
||||
.. note::
|
||||
|
||||
Asynchronous test cases also have a 2 seconds timeout: if the test
|
||||
does not finish within 2 seconds, it will be considered
|
||||
failed. This pretty much always means the test will not
|
||||
resolve. This timeout *only* applies to the test itself, not to
|
||||
the setup and teardown processes.
|
||||
|
||||
.. note::
|
||||
|
||||
If the returned deferred is rejected, the test will be failed
|
||||
unless :js:attr:`~TestOptions.fail_on_rejection` is set to
|
||||
``false``.
|
||||
|
||||
RPC
|
||||
---
|
||||
|
||||
An important subset of asynchronous test cases is test cases which
|
||||
need to perform (and chain, to an extent) RPC calls.
|
||||
|
||||
.. note::
|
||||
|
||||
Because they are a subset of asynchronous cases, RPC cases must
|
||||
also provide a valid :js:attr:`assertions count
|
||||
<TestOptions.asserts>`.
|
||||
|
||||
By default, test cases will fail when trying to perform an RPC
|
||||
call. The ability to perform RPC calls must be explicitly requested by
|
||||
a test case (or its containing test suite) through
|
||||
:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or
|
||||
``rpc``.
|
||||
|
||||
.. _testing-rpc-mock:
|
||||
|
||||
Mock RPC
|
||||
++++++++
|
||||
|
||||
The preferred (and fastest from a setup and execution time point of
|
||||
view) way to do RPC during tests is to mock the RPC calls: while
|
||||
setting up the test case, provide what the RPC responses "should" be,
|
||||
and only test the code between the "user" (the test itself) and the
|
||||
RPC call, before the call is effectively done.
|
||||
|
||||
To do this, set the :js:attr:`rpc option <TestOptions.rpc>` to
|
||||
``mock``. This will add a third parameter to the test case callback:
|
||||
|
||||
.. js:function:: mock(rpc_spec, handler)
|
||||
|
||||
Can be used in two different ways depending on the shape of the
|
||||
first parameter:
|
||||
|
||||
* If it matches the pattern ``model:method`` (if it contains a
|
||||
colon, essentially) the call will set up the mocking of an RPC
|
||||
call straight to the OpenERP server (through XMLRPC) as
|
||||
performed via e.g. :js:func:`openerp.web.Model.call`.
|
||||
|
||||
In that case, ``handler`` should be a function taking two
|
||||
arguments ``args`` and ``kwargs``, matching the corresponding
|
||||
arguments on the server side and should simply return the value
|
||||
as if it were returned by the Python XMLRPC handler::
|
||||
|
||||
test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) {
|
||||
// set up mocking
|
||||
mock('people.famous:name_search', function (args, kwargs) {
|
||||
strictEqual(kwargs.name, 'bob');
|
||||
return [
|
||||
[1, "Microsoft Bob"],
|
||||
[2, "Bob the Builder"],
|
||||
[3, "Silent Bob"]
|
||||
];
|
||||
});
|
||||
|
||||
// actual test code
|
||||
return new instance.web.Model('people.famous')
|
||||
.call('name_search', {name: 'bob'}).then(function (result) {
|
||||
strictEqual(result.length, 3, "shoud return 3 people");
|
||||
strictEqual(result[0][1], "Microsoft Bob",
|
||||
"the most famous bob should be Microsoft Bob");
|
||||
});
|
||||
});
|
||||
|
||||
* Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it
|
||||
will mock a JSON-RPC call to a web client controller, such as
|
||||
``/web/webclient/translations``. In that case, the handler takes
|
||||
a single ``params`` argument holding all of the parameters
|
||||
provided over JSON-RPC.
|
||||
|
||||
As previously, the handler should simply return the result value
|
||||
as if returned by the original JSON-RPC handler::
|
||||
|
||||
test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) {
|
||||
var fetched_dbs = false, fetched_langs = false;
|
||||
mock('/web/database/get_list', function () {
|
||||
fetched_dbs = true;
|
||||
return ['foo', 'bar', 'baz'];
|
||||
});
|
||||
mock('/web/session/get_lang_list', function () {
|
||||
fetched_langs = true;
|
||||
return [['vo_IS', 'Hopelandic / Vonlenska']];
|
||||
});
|
||||
|
||||
// widget needs that or it blows up
|
||||
instance.webclient = {toggle_bars: openerp.testing.noop};
|
||||
var dbm = new instance.web.DatabaseManager({});
|
||||
return dbm.appendTo($s).then(function () {
|
||||
ok(fetched_dbs, "should have fetched databases");
|
||||
ok(fetched_langs, "should have fetched languages");
|
||||
deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
|
||||
});
|
||||
});
|
||||
|
||||
.. note::
|
||||
|
||||
Mock handlers can contain assertions, these assertions should be
|
||||
part of the assertions count (and if multiple calls are made to a
|
||||
handler containing assertions, it multiplies the effective number
|
||||
of assertions).
|
||||
|
||||
.. _testing-rpc-rpc:
|
||||
|
||||
Actual RPC
|
||||
++++++++++
|
||||
|
||||
A more realistic (but significantly slower and more expensive) way to
|
||||
perform RPC calls is to perform actual calls to an actually running
|
||||
OpenERP server. To do this, set the :js:attr:`rpc option
|
||||
<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter
|
||||
but will enable actual RPC, and the automatic creation and destruction
|
||||
of databases (from a specified source) around tests.
|
||||
|
||||
First, create a basic model we can test stuff with:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
|
||||
class TestObject(orm.Model):
|
||||
_name = 'web_tests_demo.model'
|
||||
|
||||
_columns = {
|
||||
'name': fields.char("Name", required=True),
|
||||
'thing': fields.char("Thing"),
|
||||
'other': fields.char("Other", required=True)
|
||||
}
|
||||
_defaults = {
|
||||
'other': "bob"
|
||||
}
|
||||
|
||||
then the actual test::
|
||||
|
||||
test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) {
|
||||
var Model = new instance.web.Model('web_tests_demo.model');
|
||||
return Model.call('create', [{name: "Bob"}])
|
||||
.then(function (id) {
|
||||
return Model.call('read', [[id]]);
|
||||
}).then(function (records) {
|
||||
strictEqual(records.length, 1);
|
||||
var record = records[0];
|
||||
strictEqual(record.name, "Bob");
|
||||
strictEqual(record.thing, false);
|
||||
// default value
|
||||
strictEqual(record.other, 'bob');
|
||||
});
|
||||
});
|
||||
|
||||
This test looks like a "mock" RPC test but for the lack of mock
|
||||
response (and the different ``rpc`` type), however it has further
|
||||
ranging consequences in that it will copy an existing database to a
|
||||
new one, run the test in full on that temporary database and destroy
|
||||
the database, to simulate an isolated and transactional context and
|
||||
avoid affecting other tests. One of the consequences is that it takes
|
||||
a *long* time to run (5~10s, most of that time being spent waiting for
|
||||
a database duplication).
|
||||
|
||||
Furthermore, as the test needs to clone a database, it also has to ask
|
||||
which database to clone, the database/super-admin password and the
|
||||
password of the ``admin`` user (in order to authenticate as said
|
||||
user). As a result, the first time the test runner encounters an
|
||||
``rpc: "rpc"`` test configuration it will produce the following
|
||||
prompt:
|
||||
|
||||
.. image:: ./images/db-query.png
|
||||
:align: center
|
||||
|
||||
and stop the testing process until the necessary information has been
|
||||
provided.
|
||||
|
||||
The prompt will only appear once per test run, all tests will use the
|
||||
same "source" database.
|
||||
|
||||
.. note::
|
||||
|
||||
The handling of that information is currently rather brittle and
|
||||
unchecked, incorrect values will likely crash the runner.
|
||||
|
||||
.. note::
|
||||
|
||||
The runner does not currently store this information (for any
|
||||
longer than a test run that is), the prompt will have to be filled
|
||||
every time.
|
||||
|
||||
Testing API
|
||||
-----------
|
||||
|
||||
.. js:function:: openerp.testing.section(name[, options], body)
|
||||
|
||||
A test section, serves as shared namespace for related tests (for
|
||||
constants or values to only set up once). The ``body`` function
|
||||
should contain the tests themselves.
|
||||
|
||||
Note that the order in which tests are run is essentially
|
||||
undefined, do *not* rely on it.
|
||||
|
||||
:param String name:
|
||||
:param TestOptions options:
|
||||
:param body:
|
||||
:type body: Function<:js:func:`~openerp.testing.case`, void>
|
||||
|
||||
.. js:function:: openerp.testing.case(name[, options], callback)
|
||||
|
||||
Registers a test case callback in the test runner, the callback
|
||||
will only be run once the runner is started (or maybe not at all,
|
||||
if the test is filtered out).
|
||||
|
||||
:param String name:
|
||||
:param TestOptions options:
|
||||
:param callback:
|
||||
:type callback: Function<instance, $, Function<String, Function, void>>
|
||||
|
||||
.. js:class:: TestOptions
|
||||
|
||||
the various options which can be passed to
|
||||
:js:func:`~openerp.testing.section` or
|
||||
:js:func:`~openerp.testing.case`. Except for
|
||||
:js:attr:`~TestOptions.setup` and
|
||||
:js:attr:`~TestOptions.teardown`, an option on
|
||||
:js:func:`~openerp.testing.case` will overwrite the corresponding
|
||||
option on :js:func:`~openerp.testing.section` so
|
||||
e.g. :js:attr:`~TestOptions.rpc` can be set for a
|
||||
:js:func:`~openerp.testing.section` and then differently set for
|
||||
some :js:func:`~openerp.testing.case` of that
|
||||
:js:func:`~openerp.testing.section`
|
||||
|
||||
.. js:attribute:: TestOptions.asserts
|
||||
|
||||
An integer, the number of assertions which should run during a
|
||||
normal execution of the test. Mandatory for asynchronous tests.
|
||||
|
||||
.. js:attribute:: TestOptions.setup
|
||||
|
||||
Test case setup, run right before each test case. A section's
|
||||
:js:func:`~TestOptions.setup` is run before the case's own, if
|
||||
both are specified.
|
||||
|
||||
.. js:attribute:: TestOptions.teardown
|
||||
|
||||
Test case teardown, a case's :js:func:`~TestOptions.teardown`
|
||||
is run before the corresponding section if both are present.
|
||||
|
||||
.. js:attribute:: TestOptions.fail_on_rejection
|
||||
|
||||
If the test is asynchronous and its resulting promise is
|
||||
rejected, fail the test. Defaults to ``true``, set to
|
||||
``false`` to not fail the test in case of rejection::
|
||||
|
||||
// test/demo.js
|
||||
test('unfail rejection', {
|
||||
asserts: 1,
|
||||
fail_on_rejection: false
|
||||
}, function () {
|
||||
var d = $.Deferred();
|
||||
setTimeout(function () {
|
||||
ok(true);
|
||||
d.reject();
|
||||
}, 100);
|
||||
return d;
|
||||
});
|
||||
|
||||
.. js:attribute:: TestOptions.rpc
|
||||
|
||||
RPC method to use during tests, one of ``"mock"`` or
|
||||
``"rpc"``. Any other value will disable RPC for the test (if
|
||||
they were enabled by the suite for instance).
|
||||
|
||||
.. js:attribute:: TestOptions.templates
|
||||
|
||||
Whether the current module (and its dependencies)'s templates
|
||||
should be loaded into QWeb before starting the test. A
|
||||
boolean, ``false`` by default.
|
||||
|
||||
The test runner can also use two global configuration values set
|
||||
directly on the ``window`` object:
|
||||
|
||||
* ``oe_all_dependencies`` is an ``Array`` of all modules with a web
|
||||
component, ordered by dependency (for a module ``A`` with
|
||||
dependencies ``A'``, any module of ``A'`` must come before ``A`` in
|
||||
the array)
|
||||
|
||||
* ``oe_db_info`` is an object with 3 keys ``source``, ``supadmin`` and
|
||||
``password``. It is used to pre-configure :ref:`actual RPC
|
||||
<testing-rpc-rpc>` tests, to avoid a prompt being displayed
|
||||
(especially for headless situations).
|
||||
|
||||
Running through Python
|
||||
----------------------
|
||||
|
||||
The web client includes the means to run these tests on the
|
||||
command-line (or in a CI system), but while actually running it is
|
||||
pretty simple the setup of the pre-requisite parts has some
|
||||
complexities.
|
||||
|
||||
1. Install unittest2_ and QUnitSuite_ in your Python environment. Both
|
||||
can trivially be installed via `pip <http://pip-installer.org>`_ or
|
||||
`easy_install
|
||||
<http://packages.python.org/distribute/easy_install.html>`_.
|
||||
|
||||
The former is the unit-testing framework used by OpenERP, the
|
||||
latter is an adapter module to run qunit_ test suites and convert
|
||||
their result into something unittest2_ can understand and report.
|
||||
|
||||
2. Install PhantomJS_. It is a headless
|
||||
browser which allows automating running and testing web
|
||||
pages. QUnitSuite_ uses it to actually run the qunit_ test suite.
|
||||
|
||||
The PhantomJS_ website provides pre-built binaries for some
|
||||
platforms, and your OS's package management probably provides it as
|
||||
well.
|
||||
|
||||
If you're building PhantomJS_ from source, I recommend preparing
|
||||
for some knitting time as it's not exactly fast (it needs to
|
||||
compile both `Qt <http://qt-project.org/>`_ and `Webkit
|
||||
<http://www.webkit.org/>`_, both being pretty big projects).
|
||||
|
||||
.. note::
|
||||
|
||||
Because PhantomJS_ is webkit-based, it will not be able to test
|
||||
if Firefox, Opera or Internet Explorer can correctly run the
|
||||
test suite (and it is only an approximation for Safari and
|
||||
Chrome). It is therefore recommended to *also* run the test
|
||||
suites in actual browsers once in a while.
|
||||
|
||||
.. note::
|
||||
|
||||
The version of PhantomJS_ this was build through is 1.7,
|
||||
previous versions *should* work but are not actually supported
|
||||
(and tend to just segfault when something goes wrong in
|
||||
PhantomJS_ itself so they're a pain to debug).
|
||||
|
||||
3. Set up :ref:`OpenERP Command <openerpcommand:openerp-command>`,
|
||||
which will be used to actually run the tests: running the qunit_
|
||||
test suite requires a running server, so at this point OpenERP
|
||||
Server isn't able to do it on its own during the building/testing
|
||||
process.
|
||||
|
||||
4. Install a new database with all relevant modules (all modules with
|
||||
a web component at least), then restart the server
|
||||
|
||||
.. note::
|
||||
|
||||
For some tests, a source database needs to be duplicated. This
|
||||
operation requires that there be no connection to the database
|
||||
being duplicated, but OpenERP doesn't currently break
|
||||
existing/outstanding connections, so restarting the server is
|
||||
the simplest way to ensure everything is in the right state.
|
||||
|
||||
5. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct
|
||||
addons-path specified (and replacing ``$DATABASE`` by the source
|
||||
database you created above)
|
||||
|
||||
.. note::
|
||||
|
||||
If you leave out ``-mweb``, the runner will attempt to run all
|
||||
the tests in all the modules, which may or may not work.
|
||||
|
||||
If everything went correctly, you should now see a list of tests with
|
||||
(hopefully) ``ok`` next to their names, closing with a report of the
|
||||
number of tests run and the time it took:
|
||||
|
||||
.. literalinclude:: test-report.txt
|
||||
:language: text
|
||||
|
||||
Congratulation, you have just performed a successful "offline" run of
|
||||
the OpenERP Web test suite.
|
||||
|
||||
.. note::
|
||||
|
||||
Note that this runs all the Python tests for the ``web`` module,
|
||||
but all the web tests for all of OpenERP. This can be surprising.
|
||||
|
||||
.. _qunit: http://qunitjs.com/
|
||||
|
||||
.. _qunit assertions: http://api.qunitjs.com/category/assert/
|
||||
|
||||
.. _unittest2: http://pypi.python.org/pypi/unittest2
|
||||
|
||||
.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/
|
||||
|
||||
.. _PhantomJS: http://phantomjs.org/
|
|
@ -1,173 +0,0 @@
|
|||
|
||||
Web Controllers
|
||||
===============
|
||||
|
||||
Web controllers are classes in OpenERP able to catch the http requests sent by any browser. They allow to generate
|
||||
html pages to be served like any web server, implement new methods to be used by the Javascript client, etc...
|
||||
|
||||
Controllers File
|
||||
----------------
|
||||
|
||||
By convention the controllers should be placed in the controllers directory of the module. Example:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
web_example
|
||||
├── controllers
|
||||
│ ├── __init__.py
|
||||
│ └── my_controllers.py
|
||||
├── __init__.py
|
||||
└── __openerp__.py
|
||||
|
||||
In ``__init__.py`` you must add:
|
||||
|
||||
::
|
||||
|
||||
import controllers
|
||||
|
||||
And here is the content of ``controllers/__init__.py``:
|
||||
|
||||
::
|
||||
|
||||
import my_controllers
|
||||
|
||||
Now you can put the following content in ``controllers/my_controllers.py``:
|
||||
|
||||
::
|
||||
|
||||
import openerp.http as http
|
||||
from openerp.http import request
|
||||
|
||||
|
||||
Controller Declaration
|
||||
----------------------
|
||||
|
||||
In your controllers file, you can now declare a controller this way:
|
||||
|
||||
::
|
||||
|
||||
class MyController(http.Controller):
|
||||
|
||||
@http.route('/my_url/some_html', type="http")
|
||||
def some_html(self):
|
||||
return "<h1>This is a test</h1>"
|
||||
|
||||
@http.route('/my_url/some_json', type="json")
|
||||
def some_json(self):
|
||||
return {"sample_dictionary": "This is a sample JSON dictionary"}
|
||||
|
||||
A controller must inherit from ``http.Controller``. Each time you define a method with ``@http.route()`` it defines a
|
||||
url to match. As example, the ``some_html()`` method will be called a client query the ``/my_url/some_html`` url.
|
||||
|
||||
Pure HTTP Requests
|
||||
------------------
|
||||
|
||||
You can define methods to get any normal http requests by passing ``'http'`` to the ``type`` argument of
|
||||
``http.route()``. When doing so, you get the HTTP parameters as named parameters of the method:
|
||||
|
||||
::
|
||||
|
||||
@http.route('/say_hello', type="http")
|
||||
def say_hello(self, name):
|
||||
return "<h1>Hello %s</h1>" % name
|
||||
|
||||
This url could be contacted by typing this url in a browser: ``http://localhost:8069/say_hello?name=Nicolas``.
|
||||
|
||||
JSON Requests
|
||||
-------------
|
||||
|
||||
Methods that received JSON can be defined by passing ``'json'`` to the ``type`` argument of ``http.route()``. The
|
||||
OpenERP Javascript client can contact these methods using the JSON-RPC protocol. JSON methods must return JSON. Like the
|
||||
HTTP methods they receive arguments as named parameters (except these arguments are JSON-RPC parameters).
|
||||
|
||||
::
|
||||
|
||||
@http.route('/division', type="json")
|
||||
def division(self, i, j):
|
||||
return i / j # returns a number
|
||||
|
||||
URL Patterns
|
||||
------------
|
||||
|
||||
Any URL passed to ``http.route()`` can contain patterns. Example:
|
||||
|
||||
::
|
||||
|
||||
@http.route('/files/<path:file_path>', type="http")
|
||||
def files(self, file_path):
|
||||
... # return a file identified by the path store in the 'my_path' variable
|
||||
|
||||
When such patterns are used, the method will received additional parameters that correspond to the parameters defined in
|
||||
the url. For exact documentation about url patterns, see Werkzeug's documentation:
|
||||
http://werkzeug.pocoo.org/docs/routing/ .
|
||||
|
||||
Also note you can pass multiple urls to ``http.route()``:
|
||||
|
||||
|
||||
::
|
||||
|
||||
@http.route(['/files/<path:file_path>', '/other_url/<path:file_path>'], type="http")
|
||||
def files(self, file_path):
|
||||
...
|
||||
|
||||
Contacting Models
|
||||
-----------------
|
||||
|
||||
To use the database you must access the OpenERP models. The global ``request`` object provides the necessary objects:
|
||||
|
||||
::
|
||||
|
||||
@http.route('/my_name', type="http")
|
||||
def my_name(self):
|
||||
my_user_record = request.registry.get("res.users").browse(request.cr, request.uid, request.uid)
|
||||
return "<h1>Your name is %s</h1>" % my_user_record.name
|
||||
|
||||
``request.registry`` is the registry that gives you access to the models. It is the equivalent of ``self.pool`` when
|
||||
working inside OpenERP models.
|
||||
|
||||
``request.cr`` is the cursor object. This is the ``cr`` parameter you have to pass as first argument of every model
|
||||
method in OpenERP.
|
||||
|
||||
``request.uid`` is the id of the current logged in user. This is the ``uid`` parameter you have to pass as second
|
||||
argument of every model method in OpenERP.
|
||||
|
||||
Authorization Levels
|
||||
--------------------
|
||||
|
||||
By default, all access to the models will use the rights of the currently logged in user (OpenERP uses cookies to track
|
||||
logged users). It is also impossible to reach an URL without being logged (the user's browser will receive an HTTP
|
||||
error).
|
||||
|
||||
There are some cases when the current user is not relevant, and we just want to give access to anyone to an URL. A
|
||||
typical example is be the generation of a home page for a website. The home page should be visible by anyone, whether
|
||||
they have an account or not. To do so, add the ``'admin'`` value to the ``auth`` parameter of ``http.route()``:
|
||||
|
||||
::
|
||||
|
||||
@http.route('/hello', type="http", auth="admin")
|
||||
def hello(self):
|
||||
return "<div>Hello unknown user!</div>"
|
||||
|
||||
When using the ``admin`` authentication the access to the OpenERP models will be performed with the ``Administrator``
|
||||
user and ``request.uid`` will be equal to ``openerp.SUPERUSER_ID`` (the id of the administrator).
|
||||
|
||||
It is important to note that when using the ``Administrator`` user all security is bypassed. So the programmers
|
||||
implementing such methods should take great care of not creating security issues in the application.
|
||||
|
||||
Overriding Controllers
|
||||
----------------------
|
||||
|
||||
Existing routes can be overridden. To do so, create a controller that inherit the controller containing the route you
|
||||
want to override. Example that redefine the home page of your OpenERP application.
|
||||
|
||||
::
|
||||
|
||||
import openerp.addons.web.controllers.main as main
|
||||
|
||||
class Home2(main.Home):
|
||||
@http.route('/', type="http", auth="db")
|
||||
def index(self):
|
||||
return "<div>This is my new home page.</div>"
|
||||
|
||||
By re-defining the ``index()`` method, you change the behavior of the original ``Home`` class. Now the ``'/'`` route
|
||||
will match the new ``index()`` method in ``Home2``.
|
|
@ -1,287 +0,0 @@
|
|||
Widget
|
||||
======
|
||||
|
||||
.. js:class:: openerp.web.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:
|
||||
|
||||
* 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):
|
||||
|
||||
:js:func:`~openerp.web.Widget.appendTo`
|
||||
Renders the widget and inserts it as the last child of the target, uses
|
||||
`.appendTo()`_
|
||||
|
||||
:js:func:`~openerp.web.Widget.prependTo`
|
||||
Renders the widget and inserts it as the first child of the target, uses
|
||||
`.prependTo()`_
|
||||
|
||||
:js:func:`~openerp.web.Widget.insertAfter`
|
||||
Renders the widget and inserts it as the preceding sibling of the target,
|
||||
uses `.insertAfter()`_
|
||||
|
||||
:js:func:`~openerp.web.Widget.insertBefore`
|
||||
Renders the widget and inserts it as the following sibling of the target,
|
||||
uses `.insertBefore()`_
|
||||
|
||||
* Backbone-compatible shortcuts
|
||||
|
||||
.. _widget-dom_root:
|
||||
|
||||
DOM Root
|
||||
--------
|
||||
|
||||
A :js:class:`~openerp.web.Widget` is responsible for a section of the
|
||||
page materialized by the DOM root of the widget. The DOM root is
|
||||
available via the :js:attr:`~openerp.web.Widget.el` and
|
||||
:js:attr:`~openerp.web.Widget.$el` attributes, which are
|
||||
respectively the raw DOM Element and the jQuery wrapper around the DOM
|
||||
element.
|
||||
|
||||
There are two main ways to define and generate this DOM root:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.template
|
||||
|
||||
Should be set to the name of a QWeb template (a
|
||||
:js:class:`String`). If set, the template will be rendered after
|
||||
the widget has been initialized but before it has been
|
||||
started. The root element generated by the template will be set as
|
||||
the DOM root of the widget.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.tagName
|
||||
|
||||
Used if the widget has no template defined. Defaults to ``div``,
|
||||
will be used as the tag name to create the DOM element to set as
|
||||
the widget's DOM root. It is possible to further customize this
|
||||
generated DOM root with the following attributes:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.id
|
||||
|
||||
Used to generate an ``id`` attribute on the generated DOM
|
||||
root.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.className
|
||||
|
||||
Used to generate a ``class`` attribute on the generated DOM root.
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.attributes
|
||||
|
||||
Mapping (object literal) of attribute names to attribute
|
||||
values. Each of these k:v pairs will be set as a DOM attribute
|
||||
on the generated DOM root.
|
||||
|
||||
None of these is used in case a template is specified on the widget.
|
||||
|
||||
The DOM root can also be defined programmatically by overridding
|
||||
|
||||
.. js:function:: openerp.web.Widget.renderElement
|
||||
|
||||
Renders the widget's DOM root and sets it. The default
|
||||
implementation will render a set template or generate an element
|
||||
as described above, and will call
|
||||
:js:func:`~openerp.web.Widget.setElement` on the result.
|
||||
|
||||
Any override to :js:func:`~openerp.web.Widget.renderElement` which
|
||||
does not call its ``_super`` **must** call
|
||||
:js:func:`~openerp.web.Widget.setElement` with whatever it
|
||||
generated or the widget's behavior is undefined.
|
||||
|
||||
.. note::
|
||||
|
||||
The default :js:func:`~openerp.web.Widget.renderElement` can
|
||||
be called repeatedly, it will *replace* the previous DOM root
|
||||
(using ``replaceWith``). However, this requires that the
|
||||
widget correctly sets and unsets its events (and children
|
||||
widgets). Generally,
|
||||
:js:func:`~openerp.web.Widget.renderElement` should not be
|
||||
called repeatedly unless the widget advertizes this feature.
|
||||
|
||||
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:
|
||||
|
||||
.. js:function:: openerp.web.Widget.$(selector)
|
||||
|
||||
Applies the CSS selector specified as parameter to the widget's
|
||||
DOM root.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.$(selector);
|
||||
|
||||
is functionally identical to:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.$el.find(selector);
|
||||
|
||||
:param String selector: CSS selector
|
||||
:returns: jQuery object
|
||||
|
||||
.. note:: this helper method is compatible with
|
||||
``Backbone.View.$``
|
||||
|
||||
Resetting the DOM root
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. js:function:: openerp.web.Widget.setElement(element)
|
||||
|
||||
Re-sets the widget's DOM root to the provided element, also
|
||||
handles re-setting the various aliases of the DOM root as well as
|
||||
unsetting and re-setting delegated events.
|
||||
|
||||
:param Element element: a DOM element or jQuery object to set as
|
||||
the widget's DOM root
|
||||
|
||||
.. note:: should be mostly compatible with `Backbone's
|
||||
setElement`_
|
||||
|
||||
DOM events handling
|
||||
-------------------
|
||||
|
||||
A widget will generally need to respond to user action within its
|
||||
section of the page. This entails binding events to DOM elements.
|
||||
|
||||
To this end, :js:class:`~openerp.web.Widget` provides an shortcut:
|
||||
|
||||
.. js:attribute:: openerp.web.Widget.events
|
||||
|
||||
Events are a mapping of ``event selector`` (an event name and a
|
||||
CSS selector separated by a space) to a callback. The callback can
|
||||
be either a method name in the widget or a function. In either
|
||||
case, the ``this`` will be set to the widget:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
events: {
|
||||
'click p.oe_some_class a': 'some_method',
|
||||
'change input': function (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
The selector is used for jQuery's `event delegation`_, the
|
||||
callback will only be triggered for descendants of the DOM root
|
||||
matching the selector [0]_. If the selector is left out (only an
|
||||
event name is specified), the event will be set directly on the
|
||||
widget's DOM root.
|
||||
|
||||
.. js:function:: openerp.web.Widget.delegateEvents
|
||||
|
||||
This method is in charge of binding
|
||||
:js:attr:`~openerp.web.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
|
||||
:js:attr:`~openerp.web.Widget.events` map allows, but the parent
|
||||
should always be called (or :js:attr:`~openerp.web.Widget.events`
|
||||
won't be handled correctly).
|
||||
|
||||
.. js:function:: openerp.web.Widget.undelegateEvents
|
||||
|
||||
This method is in charge of unbinding
|
||||
:js:attr:`~openerp.web.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
|
||||
:js:func:`~openerp.web.Widget.delegateEvents`.
|
||||
|
||||
.. note:: this behavior should be compatible with `Backbone's
|
||||
delegateEvents`_, apart from not accepting any argument.
|
||||
|
||||
Subclassing Widget
|
||||
------------------
|
||||
|
||||
:js:class:`~openerp.base.Widget` is subclassed in the standard manner (via the
|
||||
:js:func:`~openerp.base.Class.extend` method), and provides a number of
|
||||
abstract properties and concrete methods (which you may or may not want to
|
||||
override). Creating a subclass looks like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var MyWidget = openerp.base.Widget.extend({
|
||||
// QWeb template to use when rendering the object
|
||||
template: "MyQWebTemplate",
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
// insert code to execute before rendering, for object
|
||||
// initialization
|
||||
},
|
||||
start: function() {
|
||||
this._super();
|
||||
// post-rendering initialization code, at this point
|
||||
// ``this.$element`` has been initialized
|
||||
this.$element.find(".my_button").click(/* an example of event binding * /);
|
||||
|
||||
// if ``start`` is asynchronous, return a promise object so callers
|
||||
// know when the object is done initializing
|
||||
return this.rpc(/* … */)
|
||||
}
|
||||
});
|
||||
|
||||
The new class can then be used in the following manner:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// Create the instance
|
||||
var my_widget = new MyWidget(this);
|
||||
// Render and insert into DOM
|
||||
my_widget.appendTo(".some-div");
|
||||
|
||||
After these two lines have executed (and any promise returned by ``appendTo``
|
||||
has been resolved if needed), the widget is ready to be used.
|
||||
|
||||
.. note:: the insertion methods will start the widget themselves, and will
|
||||
return the result of :js:func:`~openerp.base.Widget.start()`.
|
||||
|
||||
If for some reason you do not want to call these methods, you will
|
||||
have to first call :js:func:`~openerp.base.Widget.render()` on the
|
||||
widget, then insert it into your DOM and start it.
|
||||
|
||||
If the widget is not needed anymore (because it's transient), simply terminate
|
||||
it:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
my_widget.destroy();
|
||||
|
||||
will unbind all DOM events, remove the widget's content from the DOM and
|
||||
destroy all widget data.
|
||||
|
||||
.. [0] not all DOM events are compatible with events delegation
|
||||
|
||||
.. _.appendTo():
|
||||
http://api.jquery.com/appendTo/
|
||||
|
||||
.. _.prependTo():
|
||||
http://api.jquery.com/prependTo/
|
||||
|
||||
.. _.insertAfter():
|
||||
http://api.jquery.com/insertAfter/
|
||||
|
||||
.. _.insertBefore():
|
||||
http://api.jquery.com/insertBefore/
|
||||
|
||||
.. _event delegation:
|
||||
http://api.jquery.com/delegate/
|
||||
|
||||
.. _Backbone's setElement:
|
||||
http://backbonejs.org/#View-setElement
|
||||
|
||||
.. _Backbone's delegateEvents:
|
||||
http://backbonejs.org/#View-delegateEvents
|
||||
|
|
@ -258,24 +258,24 @@ openerp.ParentedMixin = {
|
|||
current object is destroyed.
|
||||
*/
|
||||
alive: function(promise, reject) {
|
||||
var def = $.Deferred();
|
||||
var self = this;
|
||||
promise.done(function() {
|
||||
if (! self.isDestroyed()) {
|
||||
if (! reject)
|
||||
return $.Deferred(function (def) {
|
||||
promise.then(function () {
|
||||
if (!self.isDestroyed()) {
|
||||
def.resolve.apply(def, arguments);
|
||||
else
|
||||
def.reject();
|
||||
}
|
||||
}).fail(function() {
|
||||
if (! self.isDestroyed()) {
|
||||
if (! reject)
|
||||
}
|
||||
}, function () {
|
||||
if (!self.isDestroyed()) {
|
||||
def.reject.apply(def, arguments);
|
||||
else
|
||||
}
|
||||
}).always(function () {
|
||||
if (reject) {
|
||||
// noop if def already resolved or rejected
|
||||
def.reject();
|
||||
}
|
||||
});
|
||||
return def.promise();
|
||||
}
|
||||
// otherwise leave promise in limbo
|
||||
});
|
||||
}).promise();
|
||||
},
|
||||
/**
|
||||
* Inform the object it should destroy itself, releasing any
|
||||
|
|
|
@ -42,15 +42,15 @@ instance.web.form.FieldManagerMixin = {
|
|||
Gives new values for the fields contained in the view. The new values could not be setted
|
||||
right after the call to this method. Setting new values can trigger on_changes.
|
||||
|
||||
@param (dict) values A dictonnary with key = field name and value = new value.
|
||||
@return (Deferred) Is resolved after all the values are setted.
|
||||
@param {Object} values A dictonary with key = field name and value = new value.
|
||||
@return {$.Deferred} Is resolved after all the values are setted.
|
||||
*/
|
||||
set_values: function(values) {},
|
||||
/**
|
||||
Computes an OpenERP domain.
|
||||
|
||||
@param (list) expression An OpenERP domain.
|
||||
@return (boolean) The computed value of the domain.
|
||||
@param {Array} expression An OpenERP domain.
|
||||
@return {boolean} The computed value of the domain.
|
||||
*/
|
||||
compute_domain: function(expression) {},
|
||||
/**
|
||||
|
@ -58,7 +58,7 @@ instance.web.form.FieldManagerMixin = {
|
|||
the field are only supposed to use this context to evualuate their own, they should not
|
||||
extend it.
|
||||
|
||||
@return (CompoundContext) An OpenERP context.
|
||||
@return {CompoundContext} An OpenERP context.
|
||||
*/
|
||||
build_eval_context: function() {},
|
||||
};
|
||||
|
|
|
@ -1548,15 +1548,7 @@ instance.web.View = instance.web.Widget.extend({
|
|||
do_switch_view: function() {
|
||||
this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments)));
|
||||
},
|
||||
/**
|
||||
* Cancels the switch to the current view, switches to the previous one
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} [options.created=false] resource was created
|
||||
* @param {String} [options.default=null] view to switch to if no previous view
|
||||
*/
|
||||
|
||||
do_search: function(view) {
|
||||
do_search: function(domain, context, group_by) {
|
||||
},
|
||||
on_sidebar_export: function() {
|
||||
new instance.web.DataExport(this, this.dataset).open();
|
||||
|
|
|
@ -400,8 +400,68 @@ ropenerp.testing.section('Widget.events', {
|
|||
ok(newclicked, "undelegate should only unbind events it created");
|
||||
});
|
||||
});
|
||||
ropenerp.testing.section('Widget.async', {
|
||||
|
||||
ropenerp.testing.section('server-formats', {
|
||||
}, function (test) {
|
||||
test("alive(alive)", {asserts: 1}, function () {
|
||||
var w = new (openerp.Widget.extend({}));
|
||||
return $.async_when(w.start())
|
||||
.then(function () { return w.alive($.async_when()) })
|
||||
.then(function () { ok(true); });
|
||||
});
|
||||
test("alive(dead)", {asserts: 1}, function () {
|
||||
var w = new (openerp.Widget.extend({}));
|
||||
|
||||
return $.Deferred(function (d) {
|
||||
$.async_when(w.start())
|
||||
.then(function () {
|
||||
// destroy widget
|
||||
w.destroy();
|
||||
var promise = $.async_when();
|
||||
// leave time for alive() to do its stuff
|
||||
promise.then(function () {
|
||||
return $.async_when();
|
||||
}).then(function () {
|
||||
ok(true);
|
||||
d.resolve();
|
||||
});
|
||||
// ensure that w.alive() refuses to resolve or reject
|
||||
return w.alive(promise);
|
||||
}).always(function () {
|
||||
d.reject();
|
||||
ok(false, "alive() should not terminate by default");
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test("alive(alive, true)", {asserts: 1}, function () {
|
||||
var w = new (openerp.Widget.extend({}));
|
||||
return $.async_when(w.start())
|
||||
.then(function () { return w.alive($.async_when(), true) })
|
||||
.then(function () { ok(true); });
|
||||
});
|
||||
test("alive(dead, true)", {asserts: 1, fail_on_rejection: false}, function () {
|
||||
var w = new (openerp.Widget.extend({}));
|
||||
|
||||
return $.async_when(w.start())
|
||||
.then(function () {
|
||||
// destroy widget
|
||||
w.destroy();
|
||||
console.log('destroyed');
|
||||
return w.alive($.async_when().done(function () { console.log('when'); }), true);
|
||||
}).then(function () {
|
||||
console.log('unfailed')
|
||||
ok(false, "alive(p, true) should fail its promise");
|
||||
}, function () {
|
||||
console.log('failed')
|
||||
ok(true, "alive(p, true) should fail its promise");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
ropenerp.testing.section('server-formats', {
|
||||
dependencies: ['web.core', 'web.dates']
|
||||
}, function (test) {
|
||||
test('Parse server datetime', function () {
|
||||
|
|
|
@ -6714,3 +6714,6 @@ pre {
|
|||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
}
|
||||
.descclassname {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
@ -551,3 +551,8 @@ pre {
|
|||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
// lighten js namespace/class name
|
||||
.descclassname {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ source_suffix = '.rst'
|
|||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'odoo developer documentation'
|
||||
project = u'odoo'
|
||||
copyright = u'2014, OpenERP s.a.'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
@ -167,7 +167,9 @@ html_sidebars = {
|
|||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/2/', None),
|
||||
'werkzeug': ('http://werkzeug.pocoo.org/docs/0.9/', None),
|
||||
'werkzeug': ('http://werkzeug.pocoo.org/docs/', None),
|
||||
'sqlalchemy': ('http://docs.sqlalchemy.org/en/rel_0_9/', None),
|
||||
'django': ('https://django.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
github_user = 'odoo'
|
||||
|
|
|
@ -20,4 +20,17 @@
|
|||
evaluated, other content is interpreted as literal strings and
|
||||
displayed as-is
|
||||
|
||||
GIS
|
||||
Geographic Information System
|
||||
any computer system or subsystem to capture, store, manipulate,
|
||||
analyze, manage or present spatial and geographical data.
|
||||
|
||||
minified
|
||||
minification
|
||||
process of removing extraneous/non-necessary sections of files
|
||||
(comments, whitespace) and possibly recompiling them using equivalent
|
||||
but shorter structures (`ternary operator`_ instead of ``if/else``) in
|
||||
order to reduce network traffic
|
||||
|
||||
.. _jinja variables: http://jinja.pocoo.org/docs/dev/templates/#variables
|
||||
.. _ternary operator: http://en.wikipedia.org/wiki/%3F:
|
||||
|
|
2180
doc/howtos/web.rst
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 7.8 KiB |
|
@ -0,0 +1,24 @@
|
|||
:orphan:
|
||||
|
||||
.. _reference/actions:
|
||||
|
||||
=======
|
||||
Actions
|
||||
=======
|
||||
|
||||
.. todo:: fill in documentation, add to TOC
|
||||
|
||||
Actions define the behavior of the system in response to user actions: login,
|
||||
action button, selection of an invoice, ...
|
||||
|
||||
Window Actions
|
||||
==============
|
||||
|
||||
URL Actions
|
||||
===========
|
||||
|
||||
Server Actions
|
||||
==============
|
||||
|
||||
Report Actions
|
||||
==============
|
|
@ -1,3 +1,7 @@
|
|||
:orphan:
|
||||
|
||||
.. _reference/async:
|
||||
|
||||
Asynchronous Operations
|
||||
=======================
|
||||
|
||||
|
@ -12,15 +16,8 @@ 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.
|
||||
asynchronous systems, and warn against systemic issues or dangers.
|
||||
|
||||
Deferreds
|
||||
---------
|
||||
|
@ -38,7 +35,7 @@ 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>`.
|
||||
<reference/async/composition>`.
|
||||
|
||||
Using deferreds
|
||||
~~~~~~~~~~~~~~~
|
||||
|
@ -68,7 +65,7 @@ Building deferreds
|
|||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
After using asynchronous APIs may come the time to build them: for
|
||||
`mocks`_, to compose deferreds from multiple source in a complex
|
||||
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, ...
|
||||
|
||||
|
@ -108,7 +105,7 @@ succeeded). These methods should simply be called when the
|
|||
asynchronous operation has ended, to notify anybody interested in its
|
||||
result(s).
|
||||
|
||||
.. _deferred-composition:
|
||||
.. _reference/async/composition:
|
||||
|
||||
Composing deferreds
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -128,7 +125,7 @@ Deferred multiplexing
|
|||
`````````````````````
|
||||
|
||||
The most common reason for multiplexing deferred is simply performing
|
||||
2+ asynchronous operations and wanting to wait until all of them are
|
||||
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`.
|
||||
|
@ -140,7 +137,7 @@ The jQuery multiplexing function for promises is :js:func:`when`.
|
|||
This function can take any number of promises [#]_ and will return a
|
||||
promise.
|
||||
|
||||
This returned promise will be resolved when *all* multiplexed promises
|
||||
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).
|
||||
|
@ -195,8 +192,8 @@ 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,
|
||||
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
|
||||
|
@ -239,7 +236,6 @@ 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
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -267,9 +263,7 @@ jQuery.Deferred API
|
|||
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`
|
||||
|
||||
|
@ -278,6 +272,9 @@ jQuery.Deferred API
|
|||
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.
|
|
@ -2,6 +2,8 @@
|
|||
Web Controllers
|
||||
===============
|
||||
|
||||
.. _reference/http/routing:
|
||||
|
||||
Routing
|
||||
=======
|
||||
|
||||
|
|
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
|
@ -827,9 +827,9 @@ class Model(object):
|
|||
or self.session.uid != request.uid:
|
||||
raise Exception("Trying to use Model with badly configured database or user.")
|
||||
|
||||
mod = request.registry.get(self.model)
|
||||
if method.startswith('_'):
|
||||
raise Exception("Access denied")
|
||||
mod = request.registry[self.model]
|
||||
meth = getattr(mod, method)
|
||||
cr = request.cr
|
||||
result = meth(cr, request.uid, *args, **kw)
|
||||
|
|