[ADD] port webclient RPC doc from web/doc

This commit is contained in:
Xavier Morel 2014-09-12 15:40:50 +02:00
parent 62fcce9054
commit 62c9589485
14 changed files with 916 additions and 1234 deletions

View File

@ -34,8 +34,6 @@ Javascript
:maxdepth: 1
guidelines
rpc
async
client_action
testing

View File

@ -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.

View File

@ -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/

View File

@ -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``.

View File

@ -167,7 +167,9 @@ html_sidebars = {
intersphinx_mapping = {
'python': ('https://docs.python.org/2/', None),
'werkzeug': ('http://werkzeug.pocoo.org/docs/0.9/', None),
'werkzeug': ('http://werkzeug.pocoo.org/docs/', None),
'sqlalchemy': ('http://docs.sqlalchemy.org/en/rel_0_9/', None),
'django': ('https://django.readthedocs.org/en/latest/', None),
}
github_user = 'odoo'

View File

@ -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.

View File

@ -2,6 +2,8 @@
Web Controllers
===============
.. _reference/http/routing:
Routing
=======

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because it is too large Load Diff