[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:
@@ -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
@@ -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
@@ -18,7 +18,8 @@ Reference
|
||||
|
||||
reference/http
|
||||
reference/qweb
|
||||
reference/javascript
|
||||
reference/javascript_cheatsheet
|
||||
reference/javascript_reference
|
||||
reference/translations
|
||||
reference/reports
|
||||
reference/mixins
|
||||
|
||||
@@ -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
@@ -0,0 +1,12 @@
|
||||
:banner: banners/javascript.jpg
|
||||
|
||||
.. highlight:: javascript
|
||||
|
||||
.. default-domain:: js
|
||||
|
||||
==============
|
||||
Javascript API
|
||||
==============
|
||||
|
||||
.. automodule:: *
|
||||
:members:
|
||||
@@ -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
@@ -1,6 +1,8 @@
|
||||
=======
|
||||
Modules
|
||||
=======
|
||||
:banner: banners/module.jpg
|
||||
|
||||
================
|
||||
Module Manifests
|
||||
================
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user