summaryrefslogtreecommitdiff
path: root/doc/reference
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-11-16 12:40:36 +0100
committerChristophe Simonis <chs@odoo.com>2017-11-16 12:40:36 +0100
commita094f6318b556298ab73df7910a70bc2a6f6a54e (patch)
tree0c9282cf8c92ed72ca8514a1c650e7966ed54b59 /doc/reference
parent6e8faf40bd1f63b8001aad11bca9ff749ca365c8 (diff)
parent745d00362ab8353bf88457b05f090c470f780ef5 (diff)
[MERGE] forward port branch saas-16 up to 745d00362ab
Diffstat (limited to 'doc/reference')
-rw-r--r--doc/reference/javascript.rst1400
1 files changed, 1400 insertions, 0 deletions
diff --git a/doc/reference/javascript.rst b/doc/reference/javascript.rst
index b7696a2e983..b2d08bb255b 100644
--- a/doc/reference/javascript.rst
+++ b/doc/reference/javascript.rst
@@ -10,3 +10,1403 @@ Javascript
.. automodule:: *
:members:
+
+=======
+Widgets
+=======
+
+.. class:: Widget
+
+ Exported in ``web.Widget``, the base class for all visual components.
+ It corresponds to an MVC view, and provides a number of service to
+ simplify handling of a section of a page:
+
+ * Handles parent/child relationships between widgets
+ * Provides extensive lifecycle management with safety features (e.g.
+ automatically destroying children widgets during the destruction of a
+ parent)
+ * Automatic rendering with :ref:`qweb <reference/qweb>`
+ * Backbone-compatible shortcuts
+
+DOM Root
+--------
+
+A :class:`~Widget` is responsible for a section of the page
+materialized by the DOM root of the widget.
+
+A widget's DOM root is available via two attributes:
+
+.. attribute:: Widget.el
+
+ raw DOM element set as root to the widget
+
+.. attribute:: Widget.$el
+
+ jQuery wrapper around :attr:`~Widget.el`
+
+There are two main ways to define and generate this DOM root:
+
+.. attribute:: Widget.template
+
+ Should be set to the name of a :ref:`QWeb template <reference/qweb>`.
+ 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.
+
+.. attribute:: 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:
+
+ .. attribute:: Widget.id
+
+ Used to generate an ``id`` attribute on the generated DOM
+ root.
+
+ .. attribute:: Widget.className
+
+ Used to generate a ``class`` attribute on the generated DOM root.
+
+ .. attribute:: 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
+
+.. function:: 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
+ :func:`~Widget.setElement` on the result.
+
+ Any override to :func:`~Widget.renderElement` which
+ does not call its ``_super`` **must** call
+ :func:`~Widget.setElement` with whatever it
+ generated or the widget's behavior is undefined.
+
+ .. note::
+
+ The default :func:`~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, :func:`~Widget.renderElement` should
+ not be called repeatedly unless the widget advertizes this feature.
+
+Using a widget
+''''''''''''''
+
+A widget's lifecycle has 3 main phases:
+
+* creation and initialization of the widget instance
+
+ .. function:: Widget.init(parent)
+
+ initialization method of widgets, synchronous, can be overridden to
+ take more parameters from the widget's creator/parent
+
+ :param parent: the new widget's parent, used to handle automatic
+ destruction and event propagation. Can be ``null`` for
+ the widget to have no parent.
+ :type parent: :class:`~Widget`
+
+* DOM injection and startup, this is done by calling one of:
+
+ .. function:: Widget.appendTo(element)
+
+ Renders the widget and inserts it as the last child of the target, uses
+ `.appendTo()`_
+
+ .. function:: Widget.prependTo(element)
+
+ Renders the widget and inserts it as the first child of the target, uses
+ `.prependTo()`_
+
+ .. function:: Widget.insertAfter(element)
+
+ Renders the widget and inserts it as the preceding sibling of the target,
+ uses `.insertAfter()`_
+
+ .. function:: Widget.insertBefore(element)
+
+ Renders the widget and inserts it as the following sibling of the target,
+ uses `.insertBefore()`_
+
+ All of these methods accept whatever the corresponding jQuery method accepts
+ (CSS selectors, DOM nodes or jQuery objects). They all return a deferred_
+ and are charged with three tasks:
+
+ * rendering the widget's root element via
+ :func:`~Widget.renderElement`
+ * inserting the widget's root element in the DOM using whichever jQuery
+ method they match
+ * starting the widget, and returning the result of starting it
+
+ .. function:: Widget.start()
+
+ asynchronous startup of the widget once it's been injected in the DOM,
+ generally used to perform asynchronous RPC calls to fetch whatever
+ remote data is necessary for the widget to do its work.
+
+ Must return a deferred_ to indicate when its work is done.
+
+ A widget is *not guaranteed* to work correctly until its
+ :func:`~Widget.start` method has finished executing. The
+ widget's parent/creator must wait for a widget to be fully started
+ before interacting with it
+
+ :returns: deferred_ object
+
+* widget destruction and cleanup
+
+ .. function:: Widget.destroy()
+
+ destroys the widget's children, unbinds its events and removes its root
+ from the DOM. Automatically called when the widget's parent is destroyed,
+ must be called explicitly if the widget has no parents or if it is
+ removed but its parent remains.
+
+ A widget being destroyed is automatically unlinked from its parent.
+
+Related to widget destruction is an important utility method:
+
+.. function:: Widget.alive(deferred[, reject=false])
+
+ A significant issue with RPC and destruction is that an RPC call may take
+ a long time to execute and return while a widget is being destroyed or
+ after it has been destroyed, trying to execute its operations on a widget
+ in a broken/invalid state.
+
+ This is a frequent source of errors or strange behaviors.
+
+ :func:`~Widget.alive` can be used to wrap an RPC call,
+ ensuring that whatever operations should be executed when the call ends
+ are only executed if the widget is still alive::
+
+ this.alive(this.model.query().all()).then(function (records) {
+ // would break if executed after the widget is destroyed, wrapping
+ // rpc in alive() prevents execution
+ _.each(records, function (record) {
+ self.$el.append(self.format(record));
+ });
+ });
+
+ :param deferred: a deferred_ object to wrap
+ :param reject: by default, if the RPC call returns after the widget has
+ been destroyed the returned deferred_ is left in limbo
+ (neither resolved nor rejected). If ``reject`` is set to
+ ``true``, the deferred_ will be rejected instead.
+ :returns: deferred_ object
+
+.. function:: Widget.isDestroyed()
+
+ :returns: ``true`` if the widget is being or has been destroyed, ``false``
+ otherwise
+
+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:
+
+.. function:: Widget.$(selector)
+
+ Applies the CSS selector specified as parameter to the widget's
+ DOM root::
+
+ this.$(selector);
+
+ is functionally identical to::
+
+ this.$el.find(selector);
+
+ :param String selector: CSS selector
+ :returns: jQuery object
+
+ .. note:: this helper method is similar to ``Backbone.View.$``
+
+Resetting the DOM root
+''''''''''''''''''''''
+
+.. function:: 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, :class:`~Widget` provides a shortcut:
+
+.. attribute:: Widget.events
+
+ Events are a mapping of an event selector (an event name and an optional
+ CSS selector separated by a space) to a callback. The callback can
+ be the name of a widget's method or a function object. In either case, the
+ ``this`` will be set to the widget::
+
+ 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\ [#eventsdelegation]_. If the selector is left out
+ (only an event name is specified), the event will be set directly on the
+ widget's DOM root.
+
+.. function:: Widget.delegateEvents
+
+ This method is in charge of binding :attr:`~Widget.events` to the
+ DOM. It is automatically called after setting the widget's DOM root.
+
+ It can be overridden to set up more complex events than the
+ :attr:`~Widget.events` map allows, but the parent should always be
+ called (or :attr:`~Widget.events` won't be handled correctly).
+
+.. function:: Widget.undelegateEvents
+
+ This method is in charge of unbinding :attr:`~Widget.events` from
+ the DOM root when the widget is destroyed or the DOM root is reset, in
+ order to avoid leaving "phantom" events.
+
+ It should be overridden to un-set any event set in an override of
+ :func:`~Widget.delegateEvents`.
+
+.. note:: this behavior should be compatible with `Backbone's
+ delegateEvents`_, apart from not accepting any argument.
+
+Subclassing Widget
+------------------
+
+:class:`~Widget` is subclassed in the standard manner (via the
+:func:`~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::
+
+ var MyWidget = Widget.extend({
+ // QWeb template to use when rendering the object
+ template: "MyQWebTemplate",
+ events: {
+ // events binding example
+ 'click .my-button': 'handle_click',
+ },
+
+ init: function(parent) {
+ this._super(parent);
+ // insert code to execute before rendering, for object
+ // initialization
+ },
+ start: function() {
+ var sup = this._super();
+ // post-rendering initialization code, at this point
+
+ // allows multiplexing deferred objects
+ return $.when(
+ // propagate asynchronous signal from parent class
+ sup,
+ // return own's asynchronous signal
+ this.rpc(/* … */))
+ }
+ });
+
+The new class can then be used in the following manner::
+
+ // 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
+:func:`~Widget.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 :func:`~Widget.start()`.
+
+ If for some reason you do not want to call these methods, you will
+ have to first call :func:`~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::
+
+ my_widget.destroy();
+
+will unbind all DOM events, remove the widget's content from the DOM and
+destroy all widget data.
+
+Development Guidelines
+----------------------
+
+* Identifiers (``id`` attribute) should be avoided. In generic applications
+ and modules, ``id`` limits the re-usability of components and tends to make
+ code more brittle. Most of the time, they can be replaced with nothing,
+ classes or keeping a reference to a DOM node or jQuery element.
+
+ If an ``id`` is absolutely necessary (because a third-party library requires
+ one), the id should be partially generated using ``_.uniqueId()`` e.g.::
+
+ this.id = _.uniqueId('my-widget-')
+* Avoid predictable/common CSS class names. Class names such as "content" or
+ "navigation" might match the desired meaning/semantics, but it is likely an
+ other developer will have the same need, creating a naming conflict and
+ unintended behavior. Generic class names should be prefixed with e.g. the
+ name of the component they belong to (creating "informal" namespaces, much
+ as in C or Objective-C).
+* Global selectors should be avoided. Because a component may be used several
+ times in a single page (an example in Odoo is dashboards), queries should be
+ restricted to a given component's scope. Unfiltered selections such as
+ ``$(selector)`` or ``document.querySelectorAll(selector)`` will generally
+ lead to unintended or incorrect behavior. Odoo Web's
+ :class:`~Widget` has an attribute providing its DOM root
+ (:attr:`~Widget.$el`), and a shortcut to select nodes directly
+ (:func:`~Widget.$`).
+* More generally, never assume your components own or controls anything beyond
+ its own personal :attr:`~Widget.$el`
+* html templating/rendering should use QWeb unless absolutely trivial.
+* All interactive components (components displaying information to the screen
+ or intercepting DOM events) must inherit from :class:`~Widget`
+ and correctly implement and use its API and life cycle.
+
+.. _.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
+
+.. _deferred: http://api.jquery.com/category/deferred-object/
+
+RPC
+===
+
+To display and interact with data, calls to the Odoo server are necessary.
+This is performed using :abbr:`RPC <Remote Procedure Call>`.
+
+Odoo Web provides two primary APIs to handle this: a low-level
+JSON-RPC based API communicating with the Python section of Odoo
+Web (and of your module, if you have a Python part) and a high-level
+API above that allowing your code to talk directly to high-level Odoo models.
+
+All networking APIs are :ref:`asynchronous <reference/async>`. As a result,
+all of them will return Deferred_ objects (whether they resolve those with
+values or not). Understanding how those work before before moving on is
+probably necessary.
+
+High-level API: calling into Odoo models
+-------------------------------------------
+
+Access to Odoo object methods (made available through XML-RPC from the server)
+is done via :class:`Model`. It maps onto the Odoo server objects via two primary
+methods, :func:`~Model.call` (exported in ``web.Model``) and :func:`~Model.query`
+(exported in ``web.DataModel``, only available in the backend client).
+
+:func:`~Model.call` is a direct mapping to the corresponding method of
+the Odoo server object. Its usage is similar to that of the Odoo Model API,
+with three differences:
+
+* The interface is :ref:`asynchronous <reference/async>`, so instead of
+ returning results directly RPC method calls will return
+ Deferred_ instances, which will themselves resolve to the
+ result of the matching RPC call.
+
+* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to
+ ``__getattr__`` or ``method_missing``, there needs to be an explicit
+ method to dispatch RPC methods.
+
+* No notion of pooler, the model proxy is instantiated where needed,
+ not fetched from an other (somewhat global) object::
+
+ var Users = new Model('res.users');
+
+ Users.call('change_password', ['oldpassword', 'newpassword'],
+ {context: some_context}).then(function (result) {
+ // do something with change_password result
+ });
+
+:func:`~Model.query` is a shortcut for a builder-style
+interface to searches (``search`` + ``read`` in Odoo RPC terms). It
+returns a :class:`~odoo.web.Query` object which is immutable but
+allows building new :class:`~odoo.web.Query` instances from the
+first one, adding new properties or modifiying the parent object's::
+
+ Users.query(['name', 'login', 'user_email', 'signature'])
+ .filter([['active', '=', true], ['company_id', '=', main_company]])
+ .limit(15)
+ .all().then(function (users) {
+ // do work with users records
+ });
+
+The query is only actually performed when calling one of the query
+serialization methods, :func:`~odoo.web.Query.all` and
+:func:`~odoo.web.Query.first`. These methods will perform a new
+RPC call every time they are called.
+
+For that reason, it's actually possible to keep "intermediate" queries
+around and use them differently/add new specifications on them.
+
+.. class:: Model(name)
+
+ .. attribute:: Model.name
+
+ name of the model this object is bound to
+
+ .. function:: Model.call(method[, args][, kwargs])
+
+ Calls the ``method`` method of the current model, with the
+ provided positional and keyword arguments.
+
+ :param String method: method to call over rpc on the
+ :attr:`~Model.name`
+ :param Array<> args: positional arguments to pass to the
+ method, optional
+ :param Object<> kwargs: keyword arguments to pass to the
+ method, optional
+ :rtype: Deferred<>
+
+ .. function:: Model.query(fields)
+
+ :param Array<String> fields: list of fields to fetch during
+ the search
+ :returns: a :class:`~odoo.web.Query` object
+ representing the search to perform
+
+.. class:: odoo.web.Query(fields)
+
+ The first set of methods is the "fetching" methods. They perform
+ RPC queries using the internal data of the object they're called
+ on.
+
+ .. function:: odoo.web.Query.all()
+
+ Fetches the result of the current :class:`~odoo.web.Query` object's
+ search.
+
+ :rtype: Deferred<Array<>>
+
+ .. function:: odoo.web.Query.first()
+
+ Fetches the **first** result of the current
+ :class:`~odoo.web.Query`, or ``null`` if the current
+ :class:`~odoo.web.Query` does have any result.
+
+ :rtype: Deferred<Object | null>
+
+ .. function:: odoo.web.Query.count()
+
+ Fetches the number of records the current
+ :class:`~odoo.web.Query` would retrieve.
+
+ :rtype: Deferred<Number>
+
+ .. function:: odoo.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<odoo.web.QueryGroup>> | null
+
+ The second set of methods is the "mutator" methods, they create a
+ **new** :class:`~odoo.web.Query` object with the relevant
+ (internal) attribute either augmented or replaced.
+
+ .. function:: odoo.web.Query.context(ctx)
+
+ Adds the provided ``ctx`` to the query, on top of any existing
+ context
+
+ .. function:: odoo.web.Query.filter(domain)
+
+ Adds the provided domain to the query, this domain is
+ ``AND``-ed to the existing query domain.
+
+ .. function:: opeenrp.web.Query.offset(offset)
+
+ Sets the provided offset on the query. The new offset
+ *replaces* the old one.
+
+ .. function:: odoo.web.Query.limit(limit)
+
+ Sets the provided limit on the query. The new limit *replaces*
+ the old one.
+
+ .. function:: odoo.web.Query.order_by(fields…)
+
+ Overrides the model's natural order with the provided field
+ specifications. Behaves much like Django's :py:meth:`QuerySet.order_by
+ <django.db.models.query.QuerySet.order_by>`:
+
+ * Takes 1..n field names, in order of most to least importance
+ (the first field is the first sorting key). Fields are
+ provided as strings.
+
+ * A field specifies an ascending order, unless it is prefixed
+ with the minus sign "``-``" in which case the field is used
+ in the descending order
+
+ Divergences from Django's sorting include a lack of random sort
+ (``?`` field) and the inability to "drill down" into relations
+ for sorting.
+
+Aggregation (grouping)
+''''''''''''''''''''''
+
+Odoo has powerful grouping capacities, but they are kind-of strange
+in that they're recursive, and level n+1 relies on data provided
+directly by the grouping at level n. As a result, while
+:py:meth:`odoo.models.Model.read_group` works it's not a very intuitive
+API.
+
+Odoo Web eschews direct calls to :py:meth:`~odoo.models.Model.read_group`
+in favor of calling a method of :class:`~odoo.web.Query`, :py:meth:`much
+in the way it is done in SQLAlchemy <sqlalchemy.orm.query.Query.group_by>`
+[#terminal]_::
+
+ some_query.group_by(['field1', 'field2']).then(function (groups) {
+ // do things with the fetched groups
+ });
+
+This method is asynchronous when provided with 1..n fields (to group
+on) as argument, but it can also be called without any field (empty
+fields collection or nothing at all). In this case, instead of
+returning a Deferred object it will return ``null``.
+
+When grouping criterion come from a third-party and may or may not
+list fields (e.g. could be an empty list), this provides two ways to
+test the presence of actual subgroups (versus the need to perform a
+regular query for records):
+
+* A check on ``group_by``'s result and two completely separate code
+ paths::
+
+ var groups;
+ if (groups = some_query.group_by(gby)) {
+ groups.then(function (gs) {
+ // groups
+ });
+ }
+ // no groups
+
+* Or a more coherent code path using :func:`when`'s ability to
+ coerce values into deferreds::
+
+ $.when(some_query.group_by(gby)).then(function (groups) {
+ if (!groups) {
+ // No grouping
+ } else {
+ // grouping, even if there are no groups (groups
+ // itself could be an empty array)
+ }
+ });
+
+The result of a (successful) :func:`~odoo.web.Query.group_by` is
+an array of :class:`~odoo.web.QueryGroup`:
+
+.. class:: odoo.web.QueryGroup
+
+ .. function:: odoo.web.QueryGroup.get(key)
+
+ returns the group's attribute ``key``. Known attributes are:
+
+ ``grouped_on``
+ which grouping field resulted from this group
+ ``value``
+ ``grouped_on``'s value for this group
+ ``length``
+ the number of records in the group
+ ``aggregates``
+ a {field: value} mapping of aggregations for the group
+
+ .. function:: odoo.web.QueryGroup.query([fields...])
+
+ equivalent to :func:`Model.query` but pre-filtered to
+ only include the records within this group. Returns a
+ :class:`~odoo.web.Query` which can be further manipulated as
+ usual.
+
+ .. function:: odoo.web.QueryGroup.subgroups()
+
+ returns a deferred to an array of :class:`~odoo.web.QueryGroup`
+ below this one
+
+Low-level API: RPC calls to Python side
+---------------------------------------
+
+While the previous section is great for calling core OpenERP code
+(models code), it does not work if you want to call the Python side of
+Odoo Web.
+
+For this, a lower-level API exists on on
+:class:`~Session` objects (the class is exported in ``web.Session``, but
+an instance isusually available through ``web.session``): the ``rpc`` method.
+
+This method simply takes an absolute path (the absolute URL of the JSON
+:ref:`route <reference/http/routing>` to call) and a mapping of attributes to
+values (passed as keyword arguments to the Python method). This function
+fetches the return value of the Python methods, converted to JSON.
+
+For instance, to call the ``resequence`` of the
+:class:`~web.controllers.main.DataSet` controller::
+
+ session.rpc('/web/dataset/resequence', {
+ model: some_model,
+ ids: array_of_ids,
+ offset: 42
+ }).then(function (result) {
+ // resequence didn't error out
+ }, function () {
+ // an error occured during during call
+ });
+
+.. _reference/javascript/client:
+
+Web Client
+==========
+
+Javascript module system overview
+---------------------------------
+
+A new module system (inspired from requirejs) has now been deployed.
+It has many advantages over the Odoo version 8 system.
+
+* loading order: dependencies are guaranteed to be loaded first, even if
+ files are not loaded in the correct order in the bundle files.
+* easier to split a file into smaller logical units.
+* no global variables: easier to reason.
+* it is possible to examine every dependencies and dependants. This
+ makes refactoring much simpler, and less risky.
+
+It has also some disadvantages:
+
+* files are required to use the module system if they want to interact
+ with odoo, since the various objects are only available in the module
+ system, and not in global variables
+* circular dependencies are not supported. It makes sense, but it means
+ that one needs to be careful.
+
+This is obviously a very large change and will require everyone to
+adopt new habits. For example, the variable odoo does not exist
+anymore. The new way of doing things is to import explicitely the module
+you need, and declaring explicitely the objects you export. Here is a
+simple example::
+
+ odoo.define('addon_name.service', function (require) {
+ var utils = require('web.utils');
+ var Model = require('web.Model');
+
+ // do things with utils and Model
+ var something_useful = 15;
+ return {
+ something_useful: something_useful,
+ };
+ });
+
+This snippet shows a module named ``addon_name.service``. It is defined
+with the ``odoo.define`` function. ``odoo.define`` takes a name and a
+function for arguments:
+
+* The name is the concatenation of the name of the addon it is defined in
+ and a name describing its purpose.
+* The function is the place where the javascript module is actually
+ defined. It takes a function ``require`` as first argument, and
+ returns something (or not, depending if it needs to export something).
+ The ``require`` function is used to get a handle on the dependencies.
+ In this case, it gives a handle on two javascript modules from the
+ ``web`` addon, namely ``web.utils`` and ``web.Model``.
+
+The idea is that you define what you need to import (by using the
+``require`` function) and declare what you export (by returning
+something). The web client will then make sure that your code is loaded
+properly.
+
+Modules are contained in a file, but a file can define several modules
+(however, it is better to keep them in separate files).
+
+Each module can return a deferred. In that case, the module is marked as loaded
+only when the deferred is resolved, and its value is equal to the resolved value.
+The module can be rejected (unloaded). This will be logged in the console as info.
+
+* ``Missing dependencies``:
+ These modules do not appear in the page. It is possible that the JavaScript
+ file is not in the page or that the module name is wrong
+* ``Failed modules``:
+ A javascript error is detected
+* ``Rejected modules``:
+ The module returns a rejected deferred. It (and its dependent modules) is not
+ loaded.
+* ``Rejected linked modules``:
+ Modules who depend on a rejected module
+* ``Non loaded modules``:
+ Modules who depend on a missing or a failed module
+
+
+Web client structure
+--------------------
+
+The web client files have been refactored into smaller and simpler files.
+Here is a description of the current file structure:
+
+* the ``framework/`` folder contains all basic low level modules. The
+ modules here are supposed to be generic. Some of them are:
+
+ * ``web.ajax`` implements rpc calls
+ * ``web.core`` is a generic modules. It exports various useful
+ objects and functions, such as ``qweb``, ``_t`` or the main bus.
+ * ``web.Widget`` contains the widget class
+ * ``web.Model`` is an abstraction over ``web.ajax`` to make
+ calls to the server model methods
+ * ``web.session`` is the former ``odoo.session``
+ * ``web.utils`` for useful code snippets
+ * ``web.time`` for every time-related generic functions
+* the ``views/`` folder contains all view definitions
+* ``widgets/`` is for standalone widgets
+
+The ``js/`` folder also contains some important files:
+
+* ``action_manager.js`` is the ActionManager class
+* ``boot.js`` is the file actually implementing the module system
+* ``menu.js`` is the definition of the top menu
+* ``web_client.js`` is for the root widget WebClient
+* ``view_manager.js`` contains the ViewManager
+
+The two other files are ``tour.js`` for the tours and ``compatibility.js``.
+The latter file is a compatibility layer bridging the old system to the
+new module system. This is where every module names are exported to the
+global variable ``odoo``. In theory, our addons should work without
+ever using the variable ``odoo``, and the compatibility module can be
+disabled safely.
+
+Javascript conventions
+----------------------
+
+Here are some basic conventions for the javascript code:
+
+* declare all your dependencies at the top of the module. Also, they
+ should be sorted alphabetically by module name. This makes it
+ easier to understand your module.
+* declare all exports at the end.
+* add the ``use strict`` statement at the beginning of every module
+* always name your module properly: ``addon_name.description``.
+* use capital letters for classes (for example, ``ActionManager`` is
+ defined in the module ``web.ActionManager``), and lowercase for
+ everything else (for example, ``ajax`` is defined in ``web.ajax``).
+* declare one module per file
+
+
+
+
+
+Testing in Odoo Web Client
+==========================
+
+Javascript Unit Testing
+-----------------------
+
+Odoo Web includes means to unit-test both the core code of
+Odoo Web and your own javascript modules. On the javascript side,
+unit-testing is based on QUnit_ with a number of helpers and
+extensions for better integration with Odoo.
+
+To see what the runner looks like, find (or start) an Odoo server
+with the web client enabled, and navigate to ``/web/tests``
+This will show the runner selector, which lists all modules with javascript
+unit tests, and allows starting any of them (or all javascript tests in all
+modules at once).
+
+.. image:: ./images/runner.png
+ :align: center
+
+Clicking any runner button will launch the corresponding tests in the
+bundled QUnit_ runner:
+
+.. image:: ./images/tests.png
+ :align: center
+
+Writing a test case
+-------------------
+
+The first step is to list the test file(s). This is done through the
+``test`` key of the Odoo manifest, by adding javascript files to it:
+
+.. code-block:: python
+
+ {
+ 'name': "Demonstration of web/javascript tests",
+ 'category': 'Hidden',
+ 'depends': ['web'],
+ 'test': ['static/tests/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::
+
+ odoo.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 ``odoo.testing``
+module. Odoo tests live in a :func:`~odoo.testing.section`,
+which is itself part of a module. The first argument to a section is
+the name of the section, the second one is the section body.
+
+:func:`test <odoo.testing.case>`, provided by the
+:func:`~odoo.testing.section` to the callback, is used to
+register a given test case which will be run whenever the test runner
+actually does its job. Odoo Web test case use standard `QUnit
+assertions`_ within them.
+
+Launching the test runner at this point will run the test and display
+the corresponding assertion message, with red colors indicating the
+test failed:
+
+.. image:: ./images/tests2.png
+ :align: center
+
+Fixing the test (by replacing ``false`` to ``true`` in the assertion)
+will make it pass:
+
+.. image:: ./images/tests3.png
+ :align: center
+
+Assertions
+----------
+
+As noted above, Odoo Web's tests use `qunit assertions`_. They are
+available globally (so they can just be called without references to
+anything). The following list is available:
+
+.. function:: ok(state[, message])
+
+ checks that ``state`` is truthy (in the javascript sense)
+
+.. function:: strictEqual(actual, expected[, message])
+
+ checks that the actual (produced by a method being tested) and
+ expected values are identical (roughly equivalent to ``ok(actual
+ === expected, message)``)
+
+.. function:: notStrictEqual(actual, expected[, message])
+
+ checks that the actual and expected values are *not* identical
+ (roughly equivalent to ``ok(actual !== expected, message)``)
+
+.. function:: deepEqual(actual, expected[, message])
+
+ deep comparison between actual and expected: recurse into
+ containers (objects and arrays) to ensure that they have the same
+ keys/number of elements, and the values match.
+
+.. function:: notDeepEqual(actual, expected[, message])
+
+ inverse operation to :func:`deepEqual`
+
+.. function:: throws(block[, expected][, message])
+
+ checks that, when called, the ``block`` throws an
+ error. Optionally validates that error against ``expected``.
+
+ :param Function block:
+ :param expected: if a regexp, checks that the thrown error's
+ message matches the regular expression. If an
+ error type, checks that the thrown error is of
+ that type.
+ :type expected: Error | RegExp
+
+.. function:: equal(actual, expected[, message])
+
+ checks that ``actual`` and ``expected`` are loosely equal, using
+ the ``==`` operator and its coercion rules.
+
+.. function:: notEqual(actual, expected[, message])
+
+ inverse operation to :func:`equal`
+
+Getting an Odoo instance
+------------------------
+
+The Odoo instance is the base through which most Odoo Web
+modules behaviors (functions, objects, …) are accessed. As a result,
+the test framework automatically builds one, and loads the module
+being tested and all of its dependencies inside it. This new instance
+is provided as the first positional parameter to your test
+cases. Let's observe by adding javascript code (not test code) to the
+test module:
+
+.. code-block:: python
+
+ {
+ 'name': "Demonstration of web/javascript tests",
+ 'category': 'Hidden',
+ 'depends': ['web'],
+ 'js': ['static/src/js/demo.js'],
+ 'test': ['static/tests/demo.js'],
+ }
+
+::
+
+ // src/js/demo.js
+ odoo.web_tests_demo = function (instance) {
+ instance.web_tests_demo = {
+ value_true: true,
+ SomeType: instance.web.Class.extend({
+ init: function (value) {
+ this.value = value;
+ }
+ })
+ };
+ };
+
+and then adding a new test case, which simply checks that the
+``instance`` contains all the expected stuff we created in the
+module::
+
+ // test/demo.js
+ test('module content', function (instance) {
+ ok(instance.web_tests_demo.value_true, "should have a true value");
+ var type_instance = new instance.web_tests_demo.SomeType(42);
+ strictEqual(type_instance.value, 42, "should have provided value");
+ });
+
+DOM Scratchpad
+--------------
+
+As in the wider client, arbitrarily accessing document content is
+strongly discouraged during tests. But DOM access is still needed to
+e.g. fully initialize :class:`widgets <~odoo.Widget>` before
+testing them.
+
+Thus, a test case gets a DOM scratchpad as its second positional
+parameter, in a jQuery instance. That scratchpad is fully cleaned up
+before each test, and as long as it doesn't do anything outside the
+scratchpad your code can do whatever it wants::
+
+ // test/demo.js
+ test('DOM content', function (instance, $scratchpad) {
+ $scratchpad.html('<div><span class="foo bar">ok</span></div>');
+ ok($scratchpad.find('span').hasClass('foo'),
+ "should have provided class");
+ });
+ test('clean scratchpad', function (instance, $scratchpad) {
+ ok(!$scratchpad.children().length, "should have no content");
+ ok(!$scratchpad.text(), "should have no text");
+ });
+
+.. note::
+
+ The top-level element of the scratchpad is not cleaned up, test
+ cases can add text or DOM children but shoud not alter
+ ``$scratchpad`` itself.
+
+Loading templates
+-----------------
+
+To avoid the corresponding processing costs, by default templates are
+not loaded into QWeb. If you need to render e.g. widgets making use of
+QWeb templates, you can request their loading through the
+:attr:`~TestOptions.templates` option to the :func:`test case
+function <odoo.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/tests/demo.js'],
+ 'qweb': ['static/src/xml/demo.xml'],
+ }
+
+.. code-block:: xml
+
+ <!-- src/xml/demo.xml -->
+ <templates id="template" xml:space="preserve">
+ <t t-name="DemoTemplate">
+ <t t-foreach="5" t-as="value">
+ <p><t t-esc="value"/></p>
+ </t>
+ </t>
+ </templates>
+
+::
+
+ // test/demo.js
+ test('templates', {templates: true}, function (instance) {
+ var s = instance.web.qweb.render('DemoTemplate');
+ var texts = $(s).find('p').map(function () {
+ return $(this).text();
+ }).get();
+
+ deepEqual(texts, ['0', '1', '2', '3', '4']);
+ });
+
+Asynchronous cases
+------------------
+
+The test case examples so far are all synchronous, they execute from
+the first to the last line and once the last line has executed the
+test is done. But the web client is full of :ref:`asynchronous code
+<reference/async>`, and thus test cases need to be async-aware.
+
+This is done by returning a :class:`deferred <Deferred>` from the
+case callback::
+
+ // test/demo.js
+ test('asynchronous', {
+ asserts: 1
+ }, function () {
+ var d = $.Deferred();
+ setTimeout(function () {
+ ok(true);
+ d.resolve();
+ }, 100);
+ return d;
+ });
+
+This example also uses the :class:`options parameter <TestOptions>`
+to specify the number of assertions the case should expect, if less or
+more assertions are specified the case will count as failed.
+
+Asynchronous test cases *must* specify the number of assertions they
+will run. This allows more easily catching situations where e.g. the
+test architecture was not warned about asynchronous operations.
+
+.. note::
+
+ Asynchronous test cases also have a 2 seconds timeout: if the test
+ does not finish within 2 seconds, it will be considered
+ failed. This pretty much always means the test will not
+ resolve. This timeout *only* applies to the test itself, not to
+ the setup and teardown processes.
+
+.. note::
+
+ If the returned deferred is rejected, the test will be failed
+ unless :attr:`~TestOptions.fail_on_rejection` is set to
+ ``false``.
+
+RPC
+---
+
+An important subset of asynchronous test cases is test cases which
+need to perform (and chain, to an extent) RPC calls.
+
+.. note::
+
+ Because they are a subset of asynchronous cases, RPC cases must
+ also provide a valid :attr:`assertions count
+ <TestOptions.asserts>`.
+
+To enable mock RPC, set the :attr:`rpc option <TestOptions.rpc>` to
+``mock``. This will add a third parameter to the test case callback:
+
+.. function:: mock(rpc_spec, handler)
+
+ Can be used in two different ways depending on the shape of the
+ first parameter:
+
+ * If it matches the pattern ``model:method`` (if it contains a
+ colon, essentially) the call will set up the mocking of an RPC
+ call straight to the Odoo server (through XMLRPC) as
+ performed via e.g. :func:`odoo.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: odoo.testing.noop};
+ var dbm = new instance.web.DatabaseManager({});
+ return dbm.appendTo($s).then(function () {
+ ok(fetched_dbs, "should have fetched databases");
+ ok(fetched_langs, "should have fetched languages");
+ deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
+ });
+ });
+
+.. note::
+
+ Mock handlers can contain assertions, these assertions should be
+ part of the assertions count (and if multiple calls are made to a
+ handler containing assertions, it multiplies the effective number
+ of assertions).
+
+Testing API
+-----------
+
+.. function:: odoo.testing.section(name[, options], body)
+
+ A test section, serves as shared namespace for related tests (for
+ constants or values to only set up once). The ``body`` function
+ should contain the tests themselves.
+
+ Note that the order in which tests are run is essentially
+ undefined, do *not* rely on it.
+
+ :param String name:
+ :param TestOptions options:
+ :param body:
+ :type body: Function<:func:`~odoo.testing.case`, void>
+
+.. function:: odoo.testing.case(name[, options], callback)
+
+ Registers a test case callback in the test runner, the callback
+ will only be run once the runner is started (or maybe not at all,
+ if the test is filtered out).
+
+ :param String name:
+ :param TestOptions options:
+ :param callback:
+ :type callback: Function<instance, $, Function<String, Function, void>>
+
+.. class:: TestOptions
+
+ the various options which can be passed to
+ :func:`~odoo.testing.section` or
+ :func:`~odoo.testing.case`. Except for
+ :attr:`~TestOptions.setup` and
+ :attr:`~TestOptions.teardown`, an option on
+ :func:`~odoo.testing.case` will overwrite the corresponding
+ option on :func:`~odoo.testing.section` so
+ e.g. :attr:`~TestOptions.rpc` can be set for a
+ :func:`~odoo.testing.section` and then differently set for
+ some :func:`~odoo.testing.case` of that
+ :func:`~odoo.testing.section`
+
+ .. attribute:: TestOptions.asserts
+
+ An integer, the number of assertions which should run during a
+ normal execution of the test. Mandatory for asynchronous tests.
+
+ .. attribute:: TestOptions.setup
+
+ Test case setup, run right before each test case. A section's
+ :func:`~TestOptions.setup` is run before the case's own, if
+ both are specified.
+
+ .. attribute:: TestOptions.teardown
+
+ Test case teardown, a case's :func:`~TestOptions.teardown`
+ is run before the corresponding section if both are present.
+
+ .. attribute:: TestOptions.fail_on_rejection
+
+ If the test is asynchronous and its resulting promise is
+ rejected, fail the test. Defaults to ``true``, set to
+ ``false`` to not fail the test in case of rejection::
+
+ // test/demo.js
+ test('unfail rejection', {
+ asserts: 1,
+ fail_on_rejection: false
+ }, function () {
+ var d = $.Deferred();
+ setTimeout(function () {
+ ok(true);
+ d.reject();
+ }, 100);
+ return d;
+ });
+
+ .. attribute:: TestOptions.rpc
+
+ RPC method to use during tests, one of ``"mock"`` or
+ ``"rpc"``. Any other value will disable RPC for the test (if
+ they were enabled by the suite for instance).
+
+ .. attribute:: TestOptions.templates
+
+ Whether the current module (and its dependencies)'s templates
+ should be loaded into QWeb before starting the test. A
+ boolean, ``false`` by default.
+
+The test runner can also use two global configuration values set
+directly on the ``window`` object:
+
+* ``oe_all_dependencies`` is an ``Array`` of all modules with a web
+ component, ordered by dependency (for a module ``A`` with
+ dependencies ``A'``, any module of ``A'`` must come before ``A`` in
+ the array)
+
+Running through Python
+----------------------
+
+The web client includes the means to run these tests on the
+command-line (or in a CI system), but while actually running it is
+pretty simple the setup of the pre-requisite parts has some
+complexities.
+
+#. Install PhantomJS_. It is a headless
+ browser which allows automating running and testing web
+ pages. QUnitSuite_ uses it to actually run the qunit_ test suite.
+
+ The PhantomJS_ website provides pre-built binaries for some
+ platforms, and your OS's package management probably provides it as
+ well.
+
+ If you're building PhantomJS_ from source, I recommend preparing
+ for some knitting time as it's not exactly fast (it needs to
+ compile both `Qt <http://qt-project.org/>`_ and `Webkit
+ <http://www.webkit.org/>`_, both being pretty big projects).
+
+ .. note::
+
+ Because PhantomJS_ is webkit-based, it will not be able to test
+ if Firefox, Opera or Internet Explorer can correctly run the
+ test suite (and it is only an approximation for Safari and
+ Chrome). It is therefore recommended to *also* run the test
+ suites in actual browsers once in a while.
+
+ .. note::
+
+ The version of PhantomJS_ this was build through is 1.7,
+ previous versions *should* work but are not actually supported
+ (and tend to just segfault when something goes wrong in
+ PhantomJS_ itself so they're a pain to debug).
+
+#. Install a new database with all relevant modules (all modules with
+ a web component at least), then restart the server
+
+ .. note::
+
+ For some tests, a source database needs to be duplicated. This
+ operation requires that there be no connection to the database
+ being duplicated, but Odoo doesn't currently break
+ existing/outstanding connections, so restarting the server is
+ the simplest way to ensure everything is in the right state.
+
+#. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct
+ addons-path specified (and replacing ``$DATABASE`` by the source
+ database you created above)
+
+ .. note::
+
+ If you leave out ``-mweb``, the runner will attempt to run all
+ the tests in all the modules, which may or may not work.
+
+If everything went correctly, you should now see a list of tests with
+(hopefully) ``ok`` next to their names, closing with a report of the
+number of tests run and the time it took:
+
+.. literalinclude:: test-report.txt
+ :language: text
+
+Congratulation, you have just performed a successful "offline" run of
+the OpenERP Web test suite.
+
+.. note::
+
+ Note that this runs all the Python tests for the ``web`` module,
+ but all the web tests for all of Odoo. This can be surprising.
+
+.. _qunit: http://qunitjs.com/
+
+.. _qunit assertions: http://api.qunitjs.com/category/assert/
+
+.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/
+
+.. _PhantomJS: http://phantomjs.org/
+
+.. [#eventsdelegation] not all DOM events are compatible with events delegation
+
+.. [#terminal]
+ with a small twist: :py:meth:`sqlalchemy.orm.query.Query.group_by` is not
+ terminal, it returns a query which can still be altered.