diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index ac005599bdb..ee9958406c5 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1838,7 +1838,7 @@ var StatInfo = AbstractField.extend({ var FieldPercentPie = AbstractField.extend({ template: 'FieldPercentPie', - supportedFieldTypes: ['integer'], + supportedFieldTypes: ['integer', 'float'], /** * Register some useful references for later use throughout the widget. diff --git a/doc/howtos/web.rst b/doc/howtos/web.rst index 10fb797145d..df44ff25eba 100644 --- a/doc/howtos/web.rst +++ b/doc/howtos/web.rst @@ -1,9 +1,14 @@ :banner: banners/build_interface_ext.jpg ============================= -Building Interface Extensions +Customizing the web client ============================= +Note: this section is really really out of date. It will be updated someday, +but meanwhile, this tutorial will probably be frustrating to follow, since it +was written a long time ago. + + .. highlight:: javascript .. default-domain:: js diff --git a/doc/reference.rst b/doc/reference.rst index 11712bebc6c..4e9496c2d31 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -18,7 +18,8 @@ Reference reference/http reference/qweb - reference/javascript + reference/javascript_cheatsheet + reference/javascript_reference reference/translations reference/reports reference/mixins diff --git a/doc/reference/guidelines.rst b/doc/reference/guidelines.rst index a965c3a689b..ae0ab00555d 100644 --- a/doc/reference/guidelines.rst +++ b/doc/reference/guidelines.rst @@ -890,7 +890,34 @@ Symbols and Conventions Javascript and CSS ================== -**For javascript :** + +Static files organization +-------------------------- + +Odoo addons have some conventions on how to structure various files. We explain +here in more details how web assets are supposed to be organized. + +The first thing to know is that the Odoo server will serve (statically) all files +located in a *static/* folder, but prefixed with the addon name. So, for example, +if a file is located in *addons/web/static/src/js/some_file.js*, then it will be +statically available at the url *your-odoo-server.com/web/static/src/js/some_file.js* + +The convention is to organize the code according to the following structure: + +- *static*: all static files in general +- *static/lib*: this is the place where js libs should be located, in a sub folder. + So, for example, all files from the *jquery* library are in *addons/web/static/lib/jquery* +- *static/src*: the generic static source code folder +- *static/src/css*: all css files +- *static/src/fonts* +- *static/src/img* +- *static/src/js* +- *static/src/less*: less files +- *static/src/xml*: all qweb templates that will be rendered in JS +- *static/tests*: this is where we put all test related files. + +Javascript coding guidelines +---------------------------- - ``use strict;`` is recommended for all javascript files - Use a linter (jshint, ...) @@ -908,7 +935,8 @@ Javascript and CSS }); -**For CSS :** +CSS coding guidelines +--------------------- - Prefix all your classes with *o_* where *module_name* is the technical name of the module ('sale', 'im_chat', ...) or the main route @@ -1057,3 +1085,6 @@ Finally here are some examples of correct commit messages : element being the first/last child of its parent. This was not the case because of the invisible and useless alert. + +.. note:: Use the long description to explain the *why* not the + *what*, the *what* can be seen in the diff diff --git a/doc/reference/javascript.rst b/doc/reference/javascript.rst deleted file mode 100644 index b2d08bb255b..00000000000 --- a/doc/reference/javascript.rst +++ /dev/null @@ -1,1412 +0,0 @@ -:banner: banners/javascript.jpg - -.. highlight:: javascript - -.. default-domain:: js - -========== -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 ` - * 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 `. - 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 `. - -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 `. 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 `, 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 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> - - .. 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 - - .. function:: odoo.web.Query.count() - - Fetches the number of records the current - :class:`~odoo.web.Query` would retrieve. - - :rtype: Deferred - - .. function:: odoo.web.Query.group_by(grouping...) - - Fetches the groups for the query, using the first specified - grouping parameter - - :param Array grouping: Lists the levels of grouping - asked of the server. Grouping - can actually be an array or - varargs. - :rtype: Deferred> | 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 - `: - - * 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 ` -[#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 ` 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 `, 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('
ok
'); - 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 `. - -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 - - - - - -

-
-
-
- -:: - - // 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 -`, and thus test cases need to be async-aware. - -This is done by returning a :class:`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 ` -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 - `. - -To enable mock RPC, set the :attr:`rpc option ` 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> - -.. 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 `_ and `Webkit - `_, 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. diff --git a/doc/reference/javascript_api.rst b/doc/reference/javascript_api.rst new file mode 100644 index 00000000000..c47f19b4a9a --- /dev/null +++ b/doc/reference/javascript_api.rst @@ -0,0 +1,12 @@ +:banner: banners/javascript.jpg + +.. highlight:: javascript + +.. default-domain:: js + +============== +Javascript API +============== + +.. automodule:: * + :members: \ No newline at end of file diff --git a/doc/reference/javascript_cheatsheet.rst b/doc/reference/javascript_cheatsheet.rst new file mode 100644 index 00000000000..447405ad711 --- /dev/null +++ b/doc/reference/javascript_cheatsheet.rst @@ -0,0 +1,324 @@ +:banner: banners/javascript.jpg + +===================== +Javascript Cheatsheet +===================== + +There are many ways to solve a problem in JavaScript, and in Odoo. However, the +Odoo framework was designed to be extensible (this is a pretty big constraint), +and some common problems have a nice standard solution. The standard solution +has probably the advantage of being easy to understand for an odoo developers, +and will probably keep working when Odoo is modified. + +This document tries to explain the way one could solve some of these issues. +Note that this is not a reference. This is just a random collection of recipes, +or explanations on how to proceed in some cases. + + +First of all, remember that the first rule of customizing odoo with JS is: +*try to do it in python*. This may seem strange, but the python framework is +quite extensible, and many behaviours can be done simply with a touch of xml or +python. This has usually a lower cost of maintenance than working with JS: + +- the JS framework tends to change more, so JS code needs to be more frequently + updated +- it is often more difficult to implement a customized behaviour if it needs to + communicate with the server and properly integrate with the javascript framework. + There are many small details taken care by the framework that customized code + needs to replicate. For example, responsiveness, or updating the url, or + displaying data without flickering. + + +.. note:: This document does not really explain any concepts. This is more a + cookbook. For more details, please consult the javascript reference + page (see :doc:`javascript_reference`) + +Creating a new field widget +=========================== + +This is probably a really common usecase: we want to display some information in +a form view in a really specific (maybe business dependent) way. For example, +assume that we want to change the text color depending on some business condition. + +This can be done in three steps: creating a new widget, registering it in the +field registry, then adding the widget to the field in the form view + +- creating a new widget: + This can be done by extending a widget: + + .. code-block:: javascript + + var FieldChar = require('web.basic_fields').FieldChar; + + var CustomFieldChar = Fieldchar.extend({ + renderReadonly: function () { + // implement some custom logic here + }, + }); + +- registering it in the field registry: + The web client needs to know the mapping between a widget name and its + actual class. This is done by a registry: + + .. code-block:: javascript + + var fieldRegistry = require('web.field_registry'); + + fieldRegistry.add('my-custom-field', CustomFieldChar); + +- adding the widget in the form view + .. code-block:: xml + + + + Note that only the form, list and kanban views use this field widgets registry. + These views are tightly integrated, because the list and kanban views can + appear inside a form view). + +Modifying an existing field widget +================================== + +Another use case is that we want to modify an existing field widget. For +example, the voip addon in odoo need to modify the FieldPhone widget to add the +possibility to easily call the given number on voip. This is done by *including* +the FieldPhone widget, so there is no need to change any existing form view. + +Field Widgets (instances of (subclass of) AbstractField) are like every other +widgets, so they can be monkey patched. This looks like this: + +.. code-block:: javascript + + var basic_fields = require('web.basic_fields'); + var Phone = basic_fields.FieldPhone; + + Phone.include({ + events: _.extend({}, Phone.prototype.events, { + 'click': '_onClick', + }), + + _onClick: function (e) { + if (this.mode === 'readonly') { + e.preventDefault(); + var phoneNumber = this.value; + // call the number on voip... + } + }, + }); + +Note that there is no need to add the widget to the registry, since it is already +registered. + +Modifying a main widget from the interface +========================================== + +Another common usecase is the need to customize some elements from the user +interface. For example, adding a message in the home menu. The usual process +in this case is again to *include* the widget. This is the only way to do it, +since there are no registries for those widgets. + +This is usually done with code looking like this: + +.. code-block:: javascript + + var AppSwitcher = require('web_enterprise.AppSwitcher'); + + AppSwitcher.include({ + render: function () { + this._super(); + // do something else here... + }, + }); + + +Adding a client action +====================== + +A client action is a widget which will control the part of the screen below the +menu bar. It can have a control panel, if necessary. Defining a client action +can be done in two steps: implementing a new widget, and registering the widget +in the action registry. + +- Implementing a new client action: + This is done by creating a widget: + + .. code-block:: javascript + + var ControlPanelMixin = require('web.ControlPanelMixin'); + var Widget = require('web.Widget'); + + var ClientAction = Widget.extend(ControlPanelMixin, { + ... + }); + + Do not add the controlpanel mixin if you do not need it. Note that some + code is needed to interact with the control panel (via the + ``update_control_panel`` method given by the mixin). + +- Registering the client action: + As usual, we need to make the web client aware of the mapping between + client actions and the actual class: + + .. code-block:: javascript + + var core = require('web.core'); + + core.action_registry.add('my-custom-action', ClientAction); + + + Then, to use the client action in the web client, we need to create a client + action record (a record of the model ``ir.actions.client``) with the proper + ``tag`` attribute: + + .. code-block:: xml + + + Some Name + my-custom-action + + +Creating a new view (from scratch) +================================== + +Creating a new view is a more advanced topic. This cheatsheet will only +highlight the steps that will probably need to be done (in no particular order): + +- adding a new view type to the field ``type`` of ``ir.ui.view``:: + + class View(models.Model): + _inherit = 'ir.ui.view' + + type = fields.Selection(selection_add=[('map', "Map")]) + +- adding the new view type to the field ``view_mode`` of ``ir.actions.act_window.view``:: + + class ActWindowView(models.Model): + _inherit = 'ir.actions.act_window.view' + + view_mode = fields.Selection(selection_add=[('map', "Map")]) + + +- creating the four main pieces which makes a view (in JavaScript): + we need a view (a subclass of ``AbstractView``, this is the factory), a + renderer (from ``AbstractRenderer``), a controller (from ``AbstractController``) + and a model (from ``AbstractModel``). I suggest starting by simply + extending the superclasses: + + .. code-block:: javascript + + var AbstractController = require('web.AbstractController'); + var AbstractModel = require('web.AbstractModel'); + var AbstractRenderer = require('web.AbstractRenderer'); + var AbstractView = require('web.AbstractView'); + + var MapController = AbstractController.extend({}); + var MapRenderer = AbstractRenderer.extend({}); + var MapModel = AbstractModel.extend({}); + + var MapView = AbstractView.extend({ + config: { + Model: MapModel, + Controller: MapController, + Renderer: MapRenderer, + }, + }); + +- adding the view to the registry: + As usual, the mapping between a view type and the actual class needs to be + updated: + + .. code-block:: javascript + + var viewRegistry = require('web.view_registry'); + + viewRegistry.add('map', MapView); + +- implementing the four main classes: + The ``View`` class needs to parse the ``arch`` field and setup the other + three classes. The ``Renderer`` is in charge of representing the data in + the user interface, the ``Model`` is supposed to talk to the server, to + load data and process it. And the ``Controller`` is there to coordinate, + to talk to the web client, ... + +- creating some views in the database: + .. code-block:: xml + + + customer.map.view + res.partner + + + + + + + + +Customizing an existing view +============================ + +Assume we need to create a custom version of a generic view. For example, a +kanban view with some extra *ribbon-like* widget on top (to display some +specific custom information). In that case, this can be done with 3 steps: +extend the kanban view (which also probably mean extending controllers/renderers +and/or models), then registering the view in the view registry, and finally, +using the view in the kanban arch (a specific example is the helpdesk dashboard). + +- extending a view: + Here is what it could look like: + + .. code-block:: javascript + + var HelpdeskDashboardRenderer = KanbanRenderer.extend({ + ... + }); + + var HelpdeskDashboardModel = KanbanModel.extend({ + ... + }); + + var HelpdeskDashboardController = KanbanController.extend({ + ... + }); + + var HelpdeskDashboardView = KanbanView.extend({ + config: _.extend({}, KanbanView.prototype.config, { + Model: HelpdeskDashboardModel, + Renderer: HelpdeskDashboardRenderer, + Controller: HelpdeskDashboardController, + }), + }); + +- adding it to the view registry: + as usual, we need to inform the web client of the mapping between the name + of the views and the actual class. + + .. code-block:: javascript + + var viewRegistry = require('web.view_registry'); + viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView); + +- using it in an actual view: + we now need to inform the web client that a specific ``ir.ui.view`` needs to + use our new class. Note that this is a web client specific concern. From + the point of view of the server, we still have a kanban view. The proper + way to do this is by using a special attribute ``js_class`` (which will be + renamed someday into ``widget``, because this is really not a good name) on + the root node of the arch: + + .. code-block:: xml + + + ... + + + ... + + + + +.. note:: + + Note: you can change the way the view interprets the arch structure. However, + from the server point of view, this is still a view of the same base type, + subjected to the same rules (rng validation, for example). So, your views still + need to have a valid arch field. \ No newline at end of file diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst new file mode 100644 index 00000000000..f27975742fe --- /dev/null +++ b/doc/reference/javascript_reference.rst @@ -0,0 +1,1819 @@ +:banner: banners/javascript.jpg + +===================== +Javascript Reference +===================== + +This document presents the Odoo Javascript framework. This +framework is not a large application in term of lines of code, but it is quite +generic, because it is basically a machine to turn a declarative interface +description into a live application, able to interact with every model and +records in the database. It is even possible to use the web client to modify +the interface of the web client. + +.. note:: An html version of all docstrings in Odoo is available at: + + .. toctree:: + :maxdepth: 2 + + javascript_api + + +Overview +========= + +The Javascript framework is designed to work with three main use cases: + +- the *web client*: this is the private web application, where one can view and + edit business data. This is a single page application (the page is never + reloaded, only the new data is fetched from the server whenever it is needed) +- the *website*: this is the public part of Odoo. It allows an unidentified + user to browse some content, to shop or to perform many actions, as a client. + This is a classical website: various routes with controllers and some + javascript to make it work. +- the *point of sale*: this is the interface for the point of sale. It is a + specialized single page application. + +Some javascript code is common to these three use cases, and is bundled together +(see below in the assets section). This document will focus mostly on the web +client design. + +Web client +========== + +Single Page Application +----------------------- + +In short, the *webClient*, instance of *WebClient* is the root component of the +whole user interface. Its responsability is to orchestrate all various +subcomponents, and to provide services, such as rpcs, local storage and more. + +In runtime, the web client is a single page application. It does not need to +request a full page from the server each time the user perform an action. Instead, +it only requests what it needs, and then replaces/updates the view. Also, it +manages the url: it is kept in sync with the web client state. + +It means that while a user is working on Odoo, the web client class (and the +action manager) actually creates and destroys many sub components. The state is +highly dynamic, and each widget could be destroyed at any time. + +Overview of web client JS code +------------------------------------- + +Here, we give a very quick overview on the web client code, in +the *web/static/src/js* addon. Note that it is deliberately not exhaustive. +We only cover the most important files/folders. + +- *boot.js*: this is the file that defines the module system. It needs to be + loaded first. +- *core/*: this is a collection of lower level building blocks. Notably, it + contains the class system, the widget system, concurrency utilities, and many + other class/functions. +- *chrome/*: in this folder, we have most large widgets which make up most of + the user interface. +- *chrome/abstract_web_client.js* and *chrome/web_client.js*: together, these + files define the WebClient widget, which is the root widget for the web client. +- *chrome/action_manager.js*: this is the code that will convert an action into + a widget (for example a kanban or a form view) +- *chrome/search_X.js* all these files define the search view (it is not a view + in the point of view of the web client, only from the server point of view) +- *fields*: all main view field widgets are defined here +- *views*: this is where the views are located + +Assets Management +================= + +Managing assets in Odoo is not as straightforward as it is in some other apps. +One of the reason is that we have a variety of situations where some, but not all +the assets are required. For example, the needs of the web client, the point of +sale, the website or even the mobile application are different. Also, some +assets may be large, but are seldom needed. In that case, we sometimes want them +to be loaded lazily. + +The main idea is that we define a set of *bundles* in xml. A bundle is here defined as +a collection of files (javascript, css, less). In Odoo, the most important +bundles are defined in the file *addons/web/views/webclient_templates.xml*. It looks +like this: + +.. code-block:: xml + + + + +The files in a bundle can then be inserted into a template by using the *t-call-assets* +directive: + +.. code-block:: xml + + + + +Here is what happens when a template is rendered by the server with these directives: + +- all the *less* files described in the bundle are compiled into css files. A file + named *file.less* will be compiled in a file named *file.less.css*. + +- if we are in *debug=assets* mode, + - the *t-call-assets* directive with the *t-js* attribute set to false will + be replaced by a list of stylesheet tags pointing to the css files + - the *t-call-assets* directive with the *t-css* attribute set to false will + be replaced by a list of script tags pointing to the js files + +- if we are not in *debug=assets* mode, + - the css files will be concatenated and minified, then splits into files + with no more than 4096 rules (to get around an old limitation of IE9). Then, + we generate as many stylesheet tags as necessary + - the js files are concatenated and minified, then a script tag is generated + +Note that the assets files are cached, so in theory, a browser should only load +them once. + +Main bundles +------------ +When the Odoo server is started, it checks the timestamp of each file in a bundle, +and if necessary, will create/recreate the corresponding bundles. + +Here are some important bundles that most developers will need to know: + +- *web.assets_common*: this bundle contains most assets which are common to the + web client, the website, and also the point of sale. This is supposed to contain + lower level building blocks for the odoo framework. Note that it contains the + *boot.js* file, which defines the odoo module system. + +- *web.assets_backend*: this bundle contains the code specific to the web client + (notably the web client/action manager/views) + +- *web.assets_frontend*: this bundle is about all that is specific to the public + website: ecommerce, forum, blog, event management, ... + + +Adding files in an asset bundle +------------------------------- + +The proper way to add a file located in *addons/web* to a bundle is simple: +it is just enough to add a *script* or a *stylesheet* tag to the bundle in the +file *webclient_templates.xml*. But when we work in a different addon, we need +to add a file from that addon. In that case, it should be done in three steps: + +1. add a *assets.xml* file in the *views/* folder +2. add the string 'views/assets.xml' in the 'data' key in the manifest file +3. create an inherited view of the desired bundle, and add the file(s) with an + xpath expression. For example, + +.. code-block:: xml + + + + +.. note :: + + Note that the files in a bundle are all loaded immediately when the user loads the + odoo web client. This means that the files are transferred through the network + everytime (except when the browser cache is active). In some cases, it may be + better to lazyload some assets. For example, if a widget requires a large + library, and that widget is not a core part of the experience, then it may be + a good idea to only load the library when the widget is actually created. The + widget class has actually builtin support just for this use case. (see section + :ref:`reference/javascript_reference/qweb`) + +What to do if a file is not loaded/updated +------------------------------------------ + +There are many different reasons why a file may not be properly loaded. Here +are a few things you can try to solve the issue: + +- once the server is started, it does not know if an asset file has been + modified. So, you can simply restart the server to regenerate the assets. +- check the console (in the dev tools, usually opened with F12) to make sure + there are no obvious errors +- try to add a console.log at the beginning of your file (before any module + definition), so you can see if a file has been loaded or not +- in the user interface, in debug mode (INSERT LINK HERE TO DEBUG MODE), there + is an option to force the server to update its assets files. +- use the *debug=assets* mode. This will actually bypass the asset bundles (note + that it does not actually solve the issue. The server still uses outdated bundles) +- finally, the most convenient way to do it, for a developer, is to start the + server with the *--dev=all* option. This activates the file watcher options, + which will automatically invalidate assets when necessary. Note that it does + not work very well if the OS is Windows. +- remember to refresh your page! +- or maybe to save your code file... + +.. note:: + Once an asset file has been recreated, you need to refresh the page, to reload + the proper files (if that does not work, the files may be cached). + + +Javascript Module System +======================== + +Once we are able to load our javascript files into the browser, we need to make +sure they are loaded in the correct order. In order to do that, Odoo has defined +a small module system (located in the file *addons/web/static/src/js/boot.js*, +which needs to be loaded first). + +The Odoo module system, inspired by AMD, works by defining the function *define* +on the global odoo object. We then define each javascript module by calling that +function. In the Odoo framework, a module is a piece of code that will be executed +as soon as possible. It has a name and potentially some dependencies. When its +dependencies are loaded, a module will then be loaded as well. The value of the +module is then the return value of the function defining the module. + + +As an example, it may look like this: + + +.. code-block:: javascript + + // in file a.js + odoo.define('module.A', function (require) { + "use strict"; + + var A = ...; + + return A; + }); + + // in file b.js + odoo.define('module.B', function (require) { + "use strict"; + + var A = require('module.A'); + + var B = ...; // something that involves A + + return B; + }); + +An alternative way to define a module is to give explicitely a list of dependencies +in the second argument. + +.. code-block:: javascript + + odoo.define('module.Something', ['module.A', 'module.B'], function (require) { + "use strict"; + + var A = require('module.A'); + var B = require('module.B'); + + // some code + }); + + +If some dependencies are missing/non ready, then the module will simply not be +loaded. There will be a warning in the console after a few seconds. + +Note that circular dependencies are not supported. It makes sense, but it means that one +needs to be careful. + +Defining a module +----------------- + +The *odoo.define* method is given three arguments: + +- *moduleName*: the name of the javascript module. It should be a unique string. + The convention is to have the name of the odoo addon followed by a specific + description. For example, 'web.Widget' describes a module defined in the *web* + addon, which exports a *Widget* class (because the first letter is capitalized) + + If the name is not unique, an exception will be thrown and displayed in the + console. + +- *dependencies*: the second argument is optional. If given, it should be a list + of strings, each corresponding to a javascript module. This describes the + dependencies that are required to be loaded before the module is executed. If + the dependencies are not explicitely given here, then the module system will + extract them from the function by calling toString on it, then using a regexp + to find all *require* statements. + +- finally, the last argument is a function which defines the module. Its return + value is the value of the module, which may be passed to other modules requiring + it. Note that there is a small exception for asynchronous modules, see the + next section. + +If an error happens, it will be logged (in debug mode) in the console: + +* ``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 + + + +Asynchronous modules +--------------------- + +It can happen that a module needs to perform some work before it is ready. For +example, it could do a rpc to load some data. In that case, the module can +simply return a deferred (promise). In that case, the module system will simply +wait for the deferred to complete before registering the module. + +.. code-block:: javascript + + odoo.define('module.Something', ['web.ajax'], function (require) { + "use strict"; + + var ajax = require('web.ajax'); + + return ajax.rpc(...).then(function (result) { + // some code here + return something; + }); + }); + + +Best practices +---------------- + +- remember the convention for a module name: *addon name* suffixed with *module + name*. +- 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 exported values at the end +- try to avoid exporting too much things from one module. It is usually better + to simply export one thing in one (small/smallish) module. +- asynchronous modules can be used to simplify some use cases. For example, + the *web.dom_ready* module returns a deferred which will be resolved when the + dom is actually ready. So, another module that needs the DOM could simply have + a *require('web.dom_ready')* statement somewhere, and the code will only be + executed when the DOM is ready. +- try to avoid defining more than one module in one file. It may be convenient + in the short term, but this is actually harder to maintain. + + +Class System +============ + +Odoo was developped before ECMAScript 6 classes were available. In Ecmascript 5, +the standard way to define a class is to define a function and to add methods +on its prototype object. This is fine, but it is slightly complex when we want +to use inheritance, mixins. + +For these reasons, Odoo decided to use its own class system, inspired by John +Resig. The base Class is located in *web.Class*, in the file *class.js*. + +Creating a subclass +------------------- + +Let us discuss how classes are created. The main mechanism is to use the +*extend* method (this is more or less the equivalent of *extend* in ES6 classes). + +.. code-block:: javascript + + var Class = require('web.Class'); + + var Animal = Class.extend({ + init: function () { + this.x = 0; + this.hunger = 0; + }, + move: function () { + this.x = this.x + 1; + this.hunger = this.hunger + 1; + }, + eat: function () { + this.hunger = 0; + }, + }); + + +In this example, the *init* function is the constructor. It will be called when +an instance is created. Making an instance is done by using the *new* keyword. + +Inheritance +----------- + +It is convenient to be able to inherit an existing class. This is simply done +by using the *extend* method on the superclass. When a method is called, the +framework will secretly rebind a special method: *_super* to the currently +called method. This allows us to use *this._super* whenever we need to call a +parent method. + + +.. code-block:: javascript + + var Animal = require('web.Animal'); + + var Dog = Animal.extend({ + move: function () { + this.bark(); + this._super.apply(this, arguments); + }, + bark: function () { + console.log('woof'); + }, + }); + + var dog = new Dog(); + dog.move() + +Mixins +------ + +The odoo Class system does not support multiple inheritance, but for those cases +when we need to share some behaviour, we have a mixin system: the *extend* +method can actually take an arbitrary number of arguments, and will combine all +of them in the new class. + +.. code-block:: javascript + + var Animal = require('web.Animal'); + var DanceMixin = { + dance: function () { + console.log('dancing...'); + }, + }; + + var Hamster = Hamster.extend(DanceMixin, { + sleep: function () { + console.log('sleeping'); + }, + }); + +In this example, the *Hamster* class is a subclass of Animal, but it also mix +the DanceMixin in. + + +Patching an existing class +-------------------------- + +It is not common, but we sometimes need to modify another class *in place*. The +goal is to have a mechanism to change a class and all future/present instances. +This is done by using the *include* method: + +.. code-block:: javascript + + var Hamster = require('web.Hamster'); + + Hamster.include({ + sleep: function () { + this._super.apply(this, arguments); + console.log('zzzz'); + }, + }); + + +This is obviously a dangerous operation and should be done with care. But with +the way Odoo is structured, it is sometimes necessary in one addon to modify +the behavior of a widget/class defined in another addon. Note that it will +modify all instances of the class, even if they have already been created. + + +Widgets +======= + +The *Widget* class is really an important building block of the user interface. +Pretty much everything in the user interface is under the control of a widget. +The Widget class is defined in the module *web.Widget*, in *widget.js*. + +In short, the features provided by the Widget class include: + +* parent/child relationships between widgets (*PropertiesMixin*) +* extensive lifecycle management with safety features (e.g. + automatically destroying children widgets during the destruction of a + parent) +* automatic rendering with :ref:`qweb ` +* various utility functions to help interacting with the outside environment. + +Here is an example of a basic counter widget: + +.. code-block:: javascript + + var Widget = require('web.Widget'); + + var Counter = Widget.extend({ + template: 'some.template', + events: { + 'click button': '_onClick', + }, + init: function (parent, value) { + this._super(parent); + this.count = value; + }, + _onClick: function () { + this.count++; + this.$('.val').text(this.count); + }, + }); + +For this example, assume that the template *some.template* (and is properly +loaded: the template is in a file, which is properly defined in the *qweb* key +in the module manifest) is given by: + +.. code-block:: xml + +
+ + +
+ +This example widget can be used in the following manner: + +.. code-block:: javascript + + // Create the instance + var counter = new Counter(this, 4); + // Render and insert into DOM + counter.appendTo(".some-div"); + +This example illustrates a few of the features of the *Widget* class, including +the event system, the template system, the constructor with the initial *parent* argument. + +Widget Lifecycle +---------------- + +Like many component systems, the widget class has a well defined lifecycle. The +usual lifecycle is the following: *init* is called, then *willStart*, then the +rendering takes place, then *start* and finally *destroy*. + +.. function:: Widget.init(parent) + + this is the constructor. The init method is supposed to initialize the + base state of the widget. It is synchronous and 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` + +.. function:: Widget.willStart() + + this method will be called once by the framework when a widget is created + and in the process of being appended to the DOM. The *willStart* method is a + hook that should return a deferred. The JS framework will wait for this deferred + to complete before moving on to the rendering step. Note that at this point, + the widget does not have a DOM root element. The *willStart* hook is mostly + useful to perfom some asynchronous work, such as fetching data from the server + +.. function:: [Rendering] + + This step is automatically done by the framework. What happens is + that the framework checks if a template key is defined on the widget. If that is + the case, then it will render that template with the *widget* key bound to the + widget in the rendering context (see the example above: we use *widget.count* + in the QWeb template to read the value from the widget). If no template is + defined, we read the *tagName* key and create a corresponding DOM element. + When the rendering is done, we set the result as the $el property of the widget. + After this, we automatically bind all events in the events and custom_events + keys. + +.. function:: Widget.start() + + when the rendering is complete, the framework will automatically call + the *start* method. This is useful to perform some specialized post-rendering + work. For example, setting up a library. + + Must return a deferred to indicate when its work is done. + + :returns: deferred object + +.. function:: Widget.destroy() + + This is always the final step in the life of a widget. When a + widget is destroyed, we basically perform all necessary cleanup operations: + removing the widget from the component tree, unbinding all events, ... + + Automatically called when the widget's parent is destroyed, + must be called explicitly if the widget has no parent or if it is + removed but its parent remains. + +Note that the willStart and start method are not necessarily called. A widget +can be created (the *init* method will be called) and then destroyed (*destroy* +method) without ever having been appended to the DOM. If that is the case, the +willStart and start will not even be called. + +Widget API +---------- + +.. 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. Note that this is rarely needed, and is probably not a good idea + if a widget can be used more than once. + + .. attribute:: Widget.className + + Used to generate a ``class`` attribute on the generated DOM root. Note + that it can actually contain more than one css class: + *'some-class other-class'* + + .. 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. + +.. attribute:: Widget.el + + raw DOM element set as root to the widget (only available after the start + lifecyle method) + +.. attribute:: Widget.$el + + jQuery wrapper around :attr:`~Widget.el`. (only available after the start + lifecyle method) + +.. attribute:: Widget.template + + Should be set to the name of a :ref:`QWeb template `. + 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:: xmlDependencies + + List of paths to xml files that need to be loaded before the + widget can be rendered. This will not induce loading anything that has already + been loaded. + +.. attribute:: 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: + + .. 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. If the selector is left out + (only an event name is specified), the event will be set directly on the + widget's DOM root. + + Note: the use of an inline function is discouraged, and will probably be + removed sometimes in the future. + +.. attribute:: custom_events + + this is almost the same as the *events* attribute, but the keys + are arbitrary strings. They represent business events triggered by + some sub widgets. When an event is triggered, it will 'bubble up' the widget + tree (see the section on component communication for more details). + +.. function:: Widget.isDestroyed() + + :returns: ``true`` if the widget is being or has been destroyed, ``false`` + otherwise + +.. function:: 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 similar to ``Backbone.View.$`` + +.. 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 + + +Inserting a widget in the DOM +----------------------------- + +.. 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 + +Widget 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.: + + .. code-block:: javascript + + 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` (so, avoid using a reference to the + parent widget) + +* 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. + + +.. _reference/javascript_reference/qweb: + +QWeb Template Engine +==================== + +The web client uses the :doc:`qweb` template engine to render widgets (unless they +override the *renderElement* method to do something else). +The Qweb JS template engine is based on XML, and is mostly compatible with the +python implementation. + +Now, let us explain how the templates are loaded. Whenever the web client +starts, a rpc is made to the */web/webclient/qweb* route. The server will then +return a list of all templates defined in data files for each installed modules. +The correct files are listed in the *qweb* entry in each module manifest. + +The web client will wait for that list of template to be loaded, before starting +its first widget. + +This mechanism works quite well for our needs, but sometimes, we want to lazy +load a template. For example, imagine that we have a widget which is rarely +used. In that case, maybe we prefer to not load its template in the main file, +in order to make the web client slightly lighter. In that case, we can use the +*xmlDependencies* key of the Widget: + +.. code-block:: javascript + + var Widget = require('web.Widget'); + + var Counter = Widget.extend({ + template: 'some.template', + xmlDependencies: ['/myaddon/path/to/my/file.xml'], + + ... + + }); + +With this, the *Counter* widget will load the xmlDependencies files in its +*willStart* method, so the template will be ready when the rendering is performed. + + +Event system +============ + +There are currently two event systems supported by Odoo: a simple system which +allows adding listeners and triggering events, and a more complete system that +also makes events 'bubble up'. + +Both of these event systems are implemented in the *EventDispatcherMixin*, in +the file *mixins.js*. This mixin is included in the *Widget* class. + +Base Event system +----------------- + +This event system was historically the first. It implements a simple bus +pattern. We have 4 main methods: + +- *on*: this is used to register a listener on an event. +- *off*: useful to remove events listener. +- *once*: this is used to register a listener that will only be called once. +- *trigger*: trigger an event. This will cause each listeners to be called. + +Here is an example on how this event system could be used: + +.. code-block:: javascript + + var Widget = require('web.Widget'); + var Counter = require('myModule.Counter'); + + var MyWidget = Widget.extend({ + start: function () { + this.counter = new Counter(this); + this.counter.on('valuechange', this, this._onValueChange); + var def = this.counter.appendTo(this.$el); + return $.when(def, this._super.apply(this, arguments); + }, + _onValueChange: function (val) { + // do something with val + }, + }); + + // in Counter widget, we need to call the trigger method: + + ... this.trigger('valuechange', someValue); + + +.. warning:: + the use of this event system is discouraged, we plan to replace each + *trigger* method by the *trigger_up* method from the extended event system + +Extended Event System +--------------------- + +The custom event widgets is a more advanced system, which mimic the DOM events +API. Whenever an event is triggered, it will 'bubble up' the component tree, +until it reaches the root widget, or is stopped. + +- *trigger_up*: this is the method that will create a small *OdooEvent* and + dispatch it in the component tree. Note that it will start with the component + that triggered the event +- *custom_events*: this is the equivalent of the *event* dictionary, but for + odoo events. + +The OdooEvent class is very simple. It has three public attributes: *target* +(the widget that triggered the event), *name* (the event name) and *data* (the +payload). It also has 2 methods: *stopPropagation* and *is_stopped*. + +The previous example can be updated to use the custom event system: + +.. code-block:: javascript + + var Widget = require('web.Widget'); + var Counter = require('myModule.Counter'); + + var MyWidget = Widget.extend({ + custom_events: { + valuechange: '_onValueChange' + }, + start: function () { + this.counter = new Counter(this); + var def = this.counter.appendTo(this.$el); + return $.when(def, this._super.apply(this, arguments); + }, + _onValueChange: function(event) { + // do something with event.data.val + }, + }); + + // in Counter widget, we need to call the trigger_up method: + + ... this.trigger_up('valuechange', {value: someValue}); + + +Registries +=========== + +A common need in the Odoo ecosystem is to extend/change the behaviour of the +base system from the outside (by installing an application, i.e. a different +module). For example, one may need to add a new widget type in some views. In +that case, and many others, the usual process is to create the desired component, +then add it to a registry (registering step), to make the rest of the web client +aware of its existence. + +There are a few registries available in the system: + +- field registry (exported by 'web.field_registry'). The field registry contains + all field widgets known to the web client. Whenever a view (typically, form, + or list/kanban) needs a field widget, this is where it will look. A typical + use case look like this: + + .. code-block:: javascript + + var fieldRegistry = require('web.field_registry'); + + var FieldPad = ...; + + fieldRegistry.add('pad', FieldPad); + + Note that each value should be a subclass of *AbstractField* + +- view registry: this registry contains all JS views known to the web client + (and in particular, the view manager). Each value of this registry should + be a subclass of *AbstractView* + +- action registry: we keep track of all client actions in this registry. This + is where the action manager looks up whenever it needs to create a client + action. In version 11, each value should simply be a subclass of *Widget*. + However, in version 12, the values are required to be *AbstractAction*. + + +Communication between widgets +============================= + +There are many ways to communicate between components. + +* From a parent to its child: + this is a simple case. The parent widget can simply call a method on its + child: + + .. code-block:: javascript + + this.someWidget.update(someInfo); + +* From a widget to its parent/some ancestor: + in this case, the widget's job is simply to notify its environment that + something happened. Since we do not want the widget to have a reference to + its parent (this would couple the widget with its parent's implementation), + the best way to proceed is usually to trigger an event, which will bubble up + the component tree, by using the ``trigger_up`` method: + + .. code-block:: javascript + + this.trigger_up('open_record', { record: record, id: id}); + + This event will be triggered on the widget, then will bubble up and be + eventually caught by some upstream widget: + + .. code-block:: javascript + + var SomeAncestor = Widget.extend({ + custom_events: { + 'open_record': '_onOpenRecord', + }, + _onOpenRecord: function (event) { + var record = event.data.record; + var id = event.data.id; + // do something with the event. + }, + }); + +* Cross component: + Cross component communication can be achieved by using a bus. This is not + the preferred form of communication, because it has the disadvantage of + making the code harder to maintain. However, it has the advantage of + decoupling the components. In that case, this is simply done by triggering + and listening to events on a bus. For example: + + .. code-block:: javascript + + // in WidgetA + var core = require('web.core'); + + var WidgetA = Widget.extend({ + ... + start: function () { + core.bus.on('barcode_scanned', this, this._onBarcodeScanned); + }, + }); + + // in WidgetB + var WidgetB = Widget.extend({ + ... + someFunction: function (barcode) { + core.bus.trigger('barcode_scanned', barcode); + }, + }); + + + In this example, we use the bus exported by *web.core*, but this is not + required. A bus could be created for a specific purpose. + +Services +======== + +In version 11.0, we introduced the notion of *service*. The main idea is to +give to sub components a controlled way to access their environment, in a way +that allow the framework enough control, and which is testable. + +The service system is organized around three ideas: services, service providers +and widgets. The way it works is that widgets trigger (with *trigger_up*) +events, these events bubble up to a service provider, which will ask a service +to perform a task, then maybe return an answer. + +Service +-------- + +A service is an instance of the *AbstractService* class. It basically only has +a name and a few methods. Its job is to perform some work, typically something +depending on the environment. + +For example, we have the *ajax* service (job is to perform a rpc), the +*localStorage* (interact with the browser local storage) and many others. + +Here is a simplified example on how the ajax service is implemented: + +.. code-block:: javascript + + var AbstractService = require('web.AbstractService'); + + var AjaxService = AbstractService.extend({ + name: 'ajax', + rpc: function (...) { + return ...; + }, + }); + +This service is named 'ajax' and define one method, *rpc*. + +Service Provider +---------------- + +For services to work, it is necessary that we have a service provider ready to +dispatch the custom events. In the *backend* (web client), this is done by the +main web client instance. Note that the code for the service provider comes from +the *ServiceProviderMixin*. + + +Widget +------ + +The widget is the part that requests a service. In order to do that, it simply +triggers an event *call_service* (typically by using the helper function *call*). +This event will bubble up and communicate the intent to the rest of the system. + +In practice, some functions are so frequently called that we have some helpers +functions to make them easier to use. For example, the *_rpc* method is a helper +that helps making a rpc. + +.. code-block:: javascript + + var SomeWidget = Widget.extend({ + _getActivityModelViewID: function (model) { + return this._rpc({ + model: model, + method: 'get_activity_view_id' + }); + }, + }); + +.. warning:: + If a widget is destroyed, it will be detached from the main component tree + and will not have a parent. In that case, the events will not bubble up, which + means that the work will not be done. This is usually exactly what we want from + a destroyed widget. + +RPCs +---- + +The rpc functionality is supplied by the ajax service. But most people will +probably only interact with the *_rpc* helpers. + +There are typically two usecases when working on Odoo: one may need to call a +method on a (python) model (this goes through a controller *call_kw*), or one +may need to directly call a controller (available on some route). + +* Calling a method on a python model: + +.. code-block:: javascript + + return this._rpc({ + model: 'some.model', + method: 'some_method', + args: [some, args], + }); + +* Directly calling a controller: + +.. code-block:: javascript + + return this._rpc({ + route: '/some/route/', + params: { some: kwargs}, + }); + + +Translation management +====================== + +Some translations are made on the server side (basically all text strings rendered or +processed by the server), but there are strings in the static files that need +to be translated. The way it currently works is the following: + +- each translatable string is tagged with the special function *_t* (available in + the JS module *web.core* +- these strings are used by the server to generate the proper PO files +- whenever the web client is loaded, it will call the route */web/webclient/translations*, + which returns a list of all translatable terms +- in runtime, whenever the function *_t* is called, it will look up in this list + in order to find a translation, and return it or the original string if none + is found. + +Note that translations are explained in more details, from the server point of +view, in the document :doc:`translations`. + +There are two important functions for the translations in javascript: *_t* and +*_lt*. The difference is that *_lt* is lazily evaluated. + +.. code-block:: javascript + + var core = require('web.core'); + + var _t = core._t; + var _lt = core._lt; + + var SomeWidget = Widget.extend({ + exampleString: _lt('this should be translated'), + ... + someMethod: function () { + var str = _t('some text'); + ... + }, + }); + +In this example, the *_lt* is necessary because the translations are not ready +when the module is loaded. + +Note that translation functions need some care. The string given in argument +should not be dynamic. + +Views +====== + +The word 'view' has more than one meaning. This section is about the design of +the javascript code of the views, not the structure of the *arch* or anything +else. + +In 2017, Odoo replaced the previous view code with a new architecture. The +main need was to separate the rendering logic from the model logic. + + +Views (in a generic sense) are now described with 4 pieces: a View, a +Controller, a Renderer and a Model. The API of these 4 pieces is described in +the AbstractView, AbstractController, AbstractRenderer and AbstractModel classes. + +.. raw:: html + + + + + + + + + + + View + Controller + Renderer + Model + + +- the View is the factory. Its job is to get a set of fields, arch, context and + some other parameters, then to construct a Controller/Renderer/Model triplet. + + The view's role is to properly setup each piece of the MVC pattern, with the correct + information. Usually, it has to process the arch string and extract the + data necessary for each other parts of the view. + + Note that the view is a class, not a widget. Once its job has been done, it + can be discarded. + +- the Renderer has one job: representing the data being viewed in a DOM element. + Each view can render the data in a different way. Also, it should listen on + appropriate user actions and notify its parent (the Controller) if necessary. + + The Renderer is the V in the MVC pattern. + +- the Model: its job is to fetch and hold the state of the view. Usually, it + represents in some way a set of records in the database. The Model is the + owner of the 'business data'. It is the M in the MVC pattern. + +- the Controller: its job is to coordinate the renderer and the model. Also, it + is the main entry point for the rest of the web client. For example, when + the user changes something in the search view, the *update* method of the + controller will be called with the appropriate information. + + It is the C in the MVC pattern. + +.. note:: + The JS code for the views has been designed to be usable outside of the + context of a view manager/action manager. They could be used in a client action, + or, they could be displayed in the public website (with some work on the assets). + +Field Widgets +============= + +A good part of the web client experience is about editing and creating data. Most +of that work is done with the help of field widgets, which are aware of the field +type and of the specific details on how a value should be displayed and edited. + +AbstractField +------------- + +The *AbstractField* class is the base class for all widgets in a view, for all +views that support them (currently: Form, List, Kanban). + +There are many differences between the v11 field widgets and the previous versions. +Let us mention the most important ones: + +- the widgets are shared between all views (well, Form/List/Kanban). No need to + duplicate the implementation anymore. Note that it is possible to have a + specialized version of a widget for a view, by prefixing it with the view name + in the view registry: *list.many2one* will be chosen in priority over *many2one*. +- the widgets are no longer the owner of the field value. They only represent + the data and communicate with the rest of the view. +- the widgets do no longer need to be able to switch between edit and readonly + mode. Now, when such a change is necessary, the widget will be destroyed and + rerendered again. It is not a problem, since they do not own their value + anyway +- the field widgets can be used outside of a view. Their API is slightly + awkward, but they are designed to be standalone. + +Non relational fields +--------------------- + +We document here all non relational fields available by default, in no particular +order. + +- integer (FieldInteger) + This is the default field type for fields of type *integer*. + + - Supported field types: *integer* + + +- float (FieldFloat) + This is the default field type for fields of type *float*. + + - Supported field types: *float* + + Attributes: + + - digits: displayed precision + + .. code-block:: xml + + + +- float_time (FieldFloatTime) + The goal of this widget is to display properly a float value that represents + a time interval (in hours). So, for example, 0.5 should be formatted as 0:30, + or 4.75 correspond to 4:45. + + - Supported field types: *float* + +- boolean (FieldBoolean) + This is the default field type for fields of type *boolean*. + + - Supported field types: *boolean* + +- char (FieldChar) + This is the default field type for fields of type *char*. + + - Supported field types: *char* + +- date (FieldDate) + This is the default field type for fields of type *date*. Note that it also + works with datetime fields. It uses the session timezone when formatting + dates. + + - Supported field types: *date*, *datetime* + +- datetime (FieldDateTime) + This is the default field type for fields of type *datetime*. + + - Supported field types: *date*, *datetime* + +- monetary (FieldMonetary) + This is the default field type for fields of type 'monetary'. It is used to + display a currency. If there is a currency fields given in option, it will + use that, otherwise it will fall back to the default currency (in the session) + + - Supported field types: *monetary*, *float* + + Options: + + - currency_field: another field name which should be a many2one on currency. + + .. code-block:: xml + + + +- text (FieldText) + This is the default field type for fields of type *text*. + + - Supported field types: *text* + + +- handle (HandleWidget) + This field's job is to be displayed as a *handle* in a list view, and allow + reordering the various records by drag and dropping lines. + + - Supported field types: *integer* + + +- email (FieldEmail) + This field displays email address. The main reason to use it is that it + is rendered as an anchor tag with the proper href, in readonly mode. + + - Supported field types: *char* + +- phone (FieldPhone) + This field displays a phone number. The main reason to use it is that it + is rendered as an anchor tag with the proper href, in readonly mode, but + only in some cases: we only want to make it clickable if the device can + call this particular number. + + - Supported field types: *char* + +- url (UrlWidget) + This field displays an url (in readonly mode). The main reason to use it is + that it is rendered as an anchor tag with the proper css classes and href. + + - Supported field types: *char* + + Also, the text of the anchor tag can be customized with the *text* attribute + (it won't change the href value). + + .. code-block:: xml + + + + +- domain (FieldDomain) + The "Domain" field allows the user to construct a technical-prefix domain + thanks to a tree-like interface and see the selected records in real time. + In debug mode, an input is also there to be able to enter the prefix char + domain directly (or to build advanced domains the tree-like interface does + not allow to). + + Note that this is limited to 'static' domain (no dynamic expression, or access + to context variable). + + - Supported field types: *char* + +- link_button (LinkButton) + The LinkButton widget actually simply displays a span with an icon and the + text value as content. The link is clickable and will open a new browser + window with its value as url. + + - Supported field types: *char* + +- image (FieldBinaryImage) + This widget is used to represent a binary value as an image. In some cases, + the server returns a 'bin_size' instead of the real image (a bin_size is a + string representing a file size, such as 6.5kb). In that case, the widget + will make an image with a source attribute corresponding to an image on the + server. + + - Supported field types: *binary* + + Options: + + - preview_image: if the image is only loaded as a 'bin_size', then this + option is useful to inform the web client that the default field name is + not the name of the current field, but the name of another field. + + .. code-block:: xml + + + +- binary (FieldBinaryFile) + Generic widget to allow saving/downloading a binary file. + + - Supported field types: *binary* + + Attribute: + + - filename: saving a binary file will lose its file name, since it only + saves the binary value. The filename can be saved in another field. To do + that, an attribute filename should be set to a field present in the view. + + .. code-block:: xml + + + +- priority (PriorityWidget) + This widget is rendered as a set of stars, allowing the user to click on it + to select a value or not. This is useful for example to mark a task as high + priority. + + Note that this widget also works in 'readonly' mode, which is unusual. + + - Supported field types: *selection* + +- attachment_image (AttachmentImage) + Image widget for many2one fields. If the field is set, this widget will be + rendered as an image with the proper src url. This widget does not have a + different behaviour in edit or readonly mode, it is only useful to view an + image. + + - Supported field types: *many2one* + + .. code-block:: xml + + + +- image_selection (ImageSelection) + Allow the user to select a value by clicking on an image. + + - Supported field types: *selection* + + Options: a dictionary with a mapping from a selection value to an object with + the url for an image (*image_link*) and a preview image (*preview_link*). + + Note that this option is not optional! + + .. code-block:: xml + + + +- label_selection (LabelSelection) + This widget renders a simple non-editable label. This is only useful to + display some information, not to edit it. + + - Supported field types: *selection* + + Options: + + - classes: a mapping from a selection value to a css class + + .. code-block:: xml + + + +- state_selection (StateSelectionWidget) + This is a specialized selection widget. It assumes that the record has some + hardcoded fields, present in the view: *stage_id*, *legend_normal*, + *legend_blocked*, *legend_done*. This is mostly used to display and change + the state of a task in a project, with additional information displayed in + the dropdown. + + - Supported field types: *selection* + + .. code-block:: xml + + + +- kanban_state_selection (StateSelectionWidget) + This is exactly the same widget as state_selection + + - Supported field types: *selection* + +- boolean_favorite (FavoriteWidget) + This widget is displayed as an empty (or not) star, depending on a boolean + value. Note that it also can be edited in readonly mode. + + - Supported field types: *boolean* + +- boolean_button (FieldBooleanButton) + The Boolean Button widget is meant to be used in a stat button in a form view. + The goal is to display a nice button with the current state of a boolean + field (for example, 'Active'), and allow the user to change that field when + clicking on it. + + Note that it also can be edited in readonly mode. + + - Supported field types: *boolean* + + Options: + + - terminology: it can be either 'active', 'archive', 'close' or a customized + mapping with the keys *string_true*, *string_false*, *hover_true*, *hover_false* + + .. code-block:: xml + + + +- boolean_toggle (BooleanToggle) + Displays a toggle switch to represent a boolean. This is a subfield of + FieldBoolean, mostly used to have a different look. + +- statinfo (StatInfo) + This widget is meant to represent statistical information in a *stat button*. + It is basically just a label with a number. + + - Supported field types: *integer, float* + + Options: + + - label_field: if given, the widget will use the value of the label_field as + text. + + .. code-block:: xml + + + +- percentpie (FieldPercentPie) + This widget is meant to represent statistical information in a *stat button*. + This is similar to a statinfo widget, but the information is represented in + a *pie* chart (empty to full). Note that the value is interpreted as a + percentage (a number between 0 and 100). + + - Supported field types: *integer, float* + + .. code-block:: xml + + + +- progressbar (FieldProgressBar) + Represent a value as a progress bar (from 0 to some value) + + - Supported field types: *integer, float* + + Options: + + - title: title of the bar, displayed on top of the bar options + - editable: boolean if value is editable + - current_value: get the current_value from the field that must be present in the view + - max_value: get the max_value from the field that must be present in the view + - edit_max_value: boolean if the max_value is editable + - title: title of the bar, displayed on top of the bar --> not translated, + use parameter (not option) "title" instead + + .. code-block:: xml + + + +- toggle_button (FieldToggleBoolean) + This widget is intended to be used on boolean fields. It toggles a button + switching between a green bullet / gray bullet. It also set up a tooltip, + depending on the value and some options. + + - Supported field types: *boolean* + + Options: + + - active: the string for the tooltip that should be set when boolean is true + - inactive: the tooltip that should be set when boolean is false + + .. code-block:: xml + + + +- dashboard_graph (JournalDashboardGraph) + This is a more specialized widget, useful to display a graph representing a + set of data. For example, it is used in the accounting dashboard kanban view. + + It assumes that the field is a JSON serialization of a set of data. + + - Supported field types: *char* + + Attribute + + - graph_type: string, can be either 'line' or 'bar' + + .. code-block:: xml + + + +- ace (AceEditor) + This widget is intended to be used on Text fields. It provides Ace Editor + for editing XML and Python. + + - Supported field types: *char, text* + +Relational fields +----------------- + +- selection (FieldSelection) + The FieldSelection widget is a simple select tag with a dropdown menu to + allow the selection of a range of values. It is designed to work with fields + of type 'selection' and 'many2one'. When working with a many2one, it does + not allow the creation/edition of relational data. This is the default field + for selection fields. + + - Supported field types: *selection, many2one* + + Attributes: + + - placeholder: a string which is used to display some info when no value is + selected + + .. code-block:: xml + + + +- radio (FieldRadio) + This is a subfield of FielSelection, but specialized to display all the + valid choices as radio buttons. + + Note that if used on a many2one records, then more rpcs will be done to fetch + the name_gets of the related records. + + - Supported field types: *selection, many2one* + + Options: + + - horizontal: if true, radio buttons will be diplayed horizontally. + + .. code-block:: xml + + + +- many2one (FieldMany2One) + Default widget for many2one fields. + + - Supported field types: *selection, many2one* + + Attributes: + + - can_create: allow the creation of related records (take precedence over no_create + option) + - can_write: allow the edition of related records (default: true) + + Options: + + - no_create: prevent the creation of related records + - quick_create: allow the quick creation of related records (default: true) + - no_quick_create: prevent the quick creation of related records (don't ask me) + - no_create_edit: same as no_create, maybe... + - create_name_field: when creating a related record, if this option is set, the value of the *create_name_field* will be filled with the value of the input (default: *name*) + - always_reload: boolean, default to false. If true, the widget will always + do an additional name_get to fetch its name value. This is used for the + situations where the name_get method is overridden (please do not do that) + - no_open: boolean, default to false. If set to true, the many2one will not + redirect on the record when clicking on it (in readonly mode) + + .. code-block:: xml + + + +- list.many2one (ListFieldMany2One) + Default widget for many2one fields (in list view). + + Specialization of many2one field for list views. The main reason is that we + need to render many2one fields (in readonly mode) as a text, which does not + allow opening the related records. + + - Supported field types: *many2one* + +- kanban.many2one (KanbanFieldMany2One) + Default widget for many2one fields (in kanban view). We need to disable all + edition in kanban views. + + - Supported field types: *many2one* + +- many2many (FieldMany2Many) + Defaut widget for many2many fields. + + - Supported field types: *many2many* + + Attributes: + + - mode: string, default view to display + - domain: restrict the data to a specific domain + + Options: + + - create_text: allow the customization of the text displayed when adding a + new record + +- many2many_binary (FieldMany2ManyBinaryMultiFiles) + This widget helps the user to upload or delete one or more files at the same + time. + + Note that this widget is specific to the model 'ir.attachment'. + + - Supported field types: *many2many* + +- many2many_tags (FieldMany2ManyTags) + Display many2many as a list of tags. + + - Supported field types: *many2many* + + Options: + + - color_field: the name of a numeric field, which should be present in the + view. A color will be chosen depending on its value. + + .. code-block:: xml + + + +- form.many2many_tags (FormFieldMany2ManyTags) + Specialization of many2many_tags widget for form views. It has some extra + code to allow editing the color of a tag. + + - Supported field types: *many2many* + +- kanban.many2many_tags (KanbanFieldMany2ManyTags) + Specialization of many2many_tags widget for kanban views. + + - Supported field types: *many2many* + +- many2many_checkboxes (FieldMany2ManyCheckBoxes) + This field displays a list of checkboxes and allow the user to select a + subset of the choices. + + - Supported field types: *many2many* + +- one2many (FieldOne2Many) + Defaut widget for one2many fields. + + It usually displays data in a sub list view, or a sub kanban view. + + - Supported field types: *one2many* + +- statusbar (FieldStatus) + This is a really specialized widget for the form views. It is the bar on top + of many forms which represent a flow, and allow selecting a specific state. + + - Supported field types: *selection, many2one* + +- reference (FieldReference) + The FieldReference is a combination of a select (for the model) and a + FieldMany2One (for its value). It allows the selection of a record on an + arbitrary model. + + - Supported field types: *char, reference* + +- one2many_list (FieldOne2Many) + This widget is exactly the same as a FieldOne2Many. It is registered at this + key only for backward compatibility reasons. Please avoid using this. + +.. _.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/ + +.. _deferred: http://api.jquery.com/category/deferred-object/ \ No newline at end of file diff --git a/doc/reference/module.rst b/doc/reference/module.rst index 9ec0ef39c93..360d5c94186 100644 --- a/doc/reference/module.rst +++ b/doc/reference/module.rst @@ -1,6 +1,8 @@ -======= -Modules -======= +:banner: banners/module.jpg + +================ +Module Manifests +================ diff --git a/doc/reference/testing.rst b/doc/reference/testing.rst index ada14166202..e25e8d31433 100644 --- a/doc/reference/testing.rst +++ b/doc/reference/testing.rst @@ -4,9 +4,20 @@ =============== -Testing Modules +Testing Odoo =============== +There are many ways to test an application. In Odoo, we have three kinds of +tests + +- python unit tests: useful for testing model business logic +- js unit tests: this is necessary to test the javascript code in isolation +- tours: this is a form of integration testing. The tours ensure that the + python and the javascript parts properly talk to each other. + +Testing Python code +=================== + Odoo provides support for testing modules using unittest. To write tests, simply define a ``tests`` sub-package in your module, it will @@ -99,3 +110,47 @@ As of Odoo 8, running tests outside of the install/update cycle is not supported. .. _unittest documentation: https://docs.python.org/2/library/unittest.html + +Testing JS code +=============== + +Qunit test suite +---------------- + +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 +------------------- + +This section will be updated as soon as possible. + +.. _qunit: http://qunitjs.com/ + + +Integration Testing +=================== + +Testing Python code and JS code separately is very useful, but it does not prove +that the web client and the server work together. In order to do that, we can +write another kind of test: tours. A tour is a mini scenario of some interesting +business flow. It explains a sequence of steps that should be followed. The +test runner will then create a phantom_js browser, point it to the proper url +and simulate the click and inputs, according to the scenario. \ No newline at end of file