[DOC] doc: update/add documentation on js framework

The state of the JS documentation was quite sad, not updated in a long
time.  With this commit, we rework a lot of the JS doc.  Most notably,
we add a section 'Javascript Cheatsheet' (help on some common
customization tasks) and rewrite the 'Javascript Reference' totally.

Note that the JS reference page also contains a reference on all field
widgets.

This work is just the beginning, it is probably not complete, but at
least we will have a foundation.
This commit is contained in:
Géry Debongnie
2018-04-05 09:22:09 +02:00
parent 438006b56d
commit 19946294fa
10 changed files with 2258 additions and 1421 deletions
@@ -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.
+6 -1
View File
@@ -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
+2 -1
View File
@@ -18,7 +18,8 @@ Reference
reference/http
reference/qweb
reference/javascript
reference/javascript_cheatsheet
reference/javascript_reference
reference/translations
reference/reports
reference/mixins
+33 -2
View File
@@ -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_<module_name>* 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
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
:banner: banners/javascript.jpg
.. highlight:: javascript
.. default-domain:: js
==============
Javascript API
==============
.. automodule:: *
:members:
+324
View File
@@ -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
<field name="somefield" widget="my-custom-field"/>
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
<record id="my_client_action" model="ir.actions.client">
<field name="name">Some Name</field>
<field name="tag">my-custom-action</field>
</record>
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
<record id="customer_map_view" model="ir.ui.view">
<field name="name">customer.map.view</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<map latitude="partner_latitude" longitude="partner_longitude">
<field name="name"/>
</map>
</field>
</record>
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
<record id="helpdesk_team_view_kanban" model="ir.ui.view" >
...
<field name="arch" type="xml">
<kanban js_class="helpdesk_dashboard">
...
</kanban>
</field>
</record>
.. 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.
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -1,6 +1,8 @@
=======
Modules
=======
:banner: banners/module.jpg
================
Module Manifests
================
+56 -1
View File
@@ -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.