2349 lines
91 KiB
ReStructuredText
2349 lines
91 KiB
ReStructuredText
|
==========
|
||
|
Web Client
|
||
|
==========
|
||
|
|
||
|
.. highlight:: javascript
|
||
|
|
||
|
.. default-domain:: js
|
||
|
|
||
|
This guide is about creating modules for Odoo's web client. To create websites
|
||
|
with Odoo, see :doc:`website`.
|
||
|
|
||
|
.. warning::
|
||
|
|
||
|
This guide assumes knowledge of:
|
||
|
|
||
|
* Javascript basics and good practices
|
||
|
* jQuery_
|
||
|
* `Underscore.js`_
|
||
|
|
||
|
|
||
|
A Simple Module to Test the Web Framework
|
||
|
-----------------------------------------
|
||
|
|
||
|
It's not really possible to include the multiple JavaScript files that
|
||
|
constitute the Odoo web framework in a simple HTML file like we did in the
|
||
|
previous chapter. So we will create a simple module in Odoo that contains some
|
||
|
configuration to have a web component that will give us the possibility to
|
||
|
test the web framework.
|
||
|
|
||
|
To download the example module, use this bazaar command:
|
||
|
|
||
|
.. code-block:: sh
|
||
|
|
||
|
bzr branch lp:~niv-openerp/+junk/oepetstore -r 1
|
||
|
|
||
|
Now you must add that folder to your the addons path when you launch Odoo
|
||
|
(``--addons-path`` parameter when you launch the ``odoo.py`` executable). Then
|
||
|
create a new database and install the new module ``oepetstore``.
|
||
|
|
||
|
Now let's see what files exist in that module:
|
||
|
|
||
|
.. code-block:: text
|
||
|
|
||
|
oepetstore
|
||
|
|-- __init__.py
|
||
|
|-- __openerp__.py
|
||
|
|-- petstore_data.xml
|
||
|
|-- petstore.py
|
||
|
|-- petstore.xml
|
||
|
`-- static
|
||
|
`-- src
|
||
|
|-- css
|
||
|
| `-- petstore.css
|
||
|
|-- js
|
||
|
| `-- petstore.js
|
||
|
`-- xml
|
||
|
`-- petstore.xml
|
||
|
|
||
|
This new module already contains some customization that should be easy to
|
||
|
understand if you already coded an Odoo module like a new table, some views,
|
||
|
menu items, etc... We'll come back to these elements later because they will
|
||
|
be useful to develop some example web module. Right now let's concentrate on
|
||
|
the essential: the files dedicated to web development.
|
||
|
|
||
|
Please note that all files to be used in the web part of an Odoo module must
|
||
|
always be placed in a ``static`` folder inside the module. This is mandatory
|
||
|
due to possible security issues. The fact we created the folders ``css``,
|
||
|
``js`` and ``xml`` is just a convention.
|
||
|
|
||
|
``oepetstore/static/css/petstore.css`` is our CSS file. It is empty right now
|
||
|
but we will add any CSS we need later.
|
||
|
|
||
|
``oepetstore/static/xml/petstore.xml`` is an XML file that will contain our
|
||
|
QWeb templates. Right now it is almost empty too. Those templates will be
|
||
|
explained later, in the part dedicated to QWeb templates.
|
||
|
|
||
|
``oepetstore/static/js/petstore.js`` is probably the most interesting part. It
|
||
|
contains the JavaScript of our application. Here is what it looks like right
|
||
|
now::
|
||
|
|
||
|
openerp.oepetstore = function(instance) {
|
||
|
var _t = instance.web._t,
|
||
|
_lt = instance.web._lt;
|
||
|
var QWeb = instance.web.qweb;
|
||
|
|
||
|
instance.oepetstore = {};
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
console.log("pet store home page loaded");
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
|
||
|
}
|
||
|
|
||
|
The multiple components of that file will explained progressively. Just know
|
||
|
that it doesn't do much things right now except display a blank page and print
|
||
|
a small message in the console.
|
||
|
|
||
|
Like Odoo's XML files containing views or data, these files must be indicated
|
||
|
in the ``__openerp__.py`` file. Here are the lines we added to explain to the
|
||
|
web client it has to load these files:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
'js': ['static/src/js/*.js'],
|
||
|
'css': ['static/src/css/*.css'],
|
||
|
'qweb': ['static/src/xml/*.xml'],
|
||
|
|
||
|
These configuration parameters use wildcards, so we can add new files without
|
||
|
altering ``__openerp__.py``: they will be loaded by the web client as long as
|
||
|
they have the correct extension and are in the correct folder.
|
||
|
|
||
|
.. warning::
|
||
|
|
||
|
In Odoo, all JavaScript files are, by default, concatenated in a single
|
||
|
file. Then we apply an operation called the *minification* on that
|
||
|
file. The minification will remove all comments, white spaces and
|
||
|
line-breaks in the file. Finally, it is sent to the user's browser.
|
||
|
|
||
|
That operation may seem complex, but it's a common procedure in big
|
||
|
application like Odoo with a lot of JavaScript files. It allows to load
|
||
|
the application a lot faster.
|
||
|
|
||
|
It has the main drawback to make the application almost impossible to
|
||
|
debug, which is very bad to develop. The solution to avoid this
|
||
|
side-effect and still be able to debug is to append a small argument to
|
||
|
the URL used to load Odoo: ``?debug``. So the URL will look like this:
|
||
|
|
||
|
.. code-block:: text
|
||
|
|
||
|
http://localhost:8069/?debug
|
||
|
|
||
|
When you use that type of URL, the application will not perform all that
|
||
|
concatenation-minification process on the JavaScript files. The
|
||
|
application will take more time to load but you will be able to develop
|
||
|
with decent debugging tools.
|
||
|
|
||
|
Odoo JavaScript Module
|
||
|
-------------------------
|
||
|
|
||
|
In the previous chapter, we explained that JavaScript do not have a correct
|
||
|
mechanism to namespace the variables declared in different JavaScript files
|
||
|
and we proposed a simple method called the Module pattern.
|
||
|
|
||
|
In Odoo's web framework there is an equivalent of that pattern which is
|
||
|
integrated with the rest of the framework. Please note that **an Odoo web
|
||
|
module is a separate concept from an Odoo addon**. An addon is a folder with a
|
||
|
lot of files, a web module is not much more than a namespace for JavaScript.
|
||
|
|
||
|
The ``oepetstore/static/js/petstore.js`` already declare such a module::
|
||
|
|
||
|
openerp.oepetstore = function(instance) {
|
||
|
instance.oepetstore = {};
|
||
|
|
||
|
instance.oepetstore.xxx = ...;
|
||
|
}
|
||
|
|
||
|
In Odoo's web framework, you declare a JavaScript module by declaring a
|
||
|
function that you put in the global variable ``openerp``. The attribute you
|
||
|
set in that object must have the exact same name than your Odoo addon (this
|
||
|
addon is named ``oepetstore``, if I set ``openerp.petstore`` instead of
|
||
|
``openerp.oepetstore`` that will not work).
|
||
|
|
||
|
That function will be called when the web client decides to load your
|
||
|
addon. It is given a parameter named ``instance``, which represents the
|
||
|
current Odoo web client instance and contains all the data related to the
|
||
|
current session as well as the variables of all web modules.
|
||
|
|
||
|
The convention is to create a new namespace inside the ``instance`` object
|
||
|
which has the same name than you addon. That's why we set an empty dictionary
|
||
|
in ``instance.oepetstore``. That dictionary is the namespace we will use to
|
||
|
declare all classes and variables used inside our module.
|
||
|
|
||
|
Classes
|
||
|
-------
|
||
|
|
||
|
JavaScript doesn't have a class mechanism like most object-oriented
|
||
|
programming languages. To be more exact, it provides language elements to make
|
||
|
object-oriented programming but you have to define by yourself how you choose
|
||
|
to do it. Odoo's web framework provide tools to simplify this and let
|
||
|
programmers code in a similar way they would program in other languages like
|
||
|
Java. That class system is heavily inspired by John Resig's `Simple JavaScript
|
||
|
Inheritance <http://ejohn.org/blog/simple-javascript-inheritance/>`_.
|
||
|
|
||
|
To define a new class, you need to extend the :class:`openerp.web.Class`
|
||
|
class::
|
||
|
|
||
|
instance.oepetstore.MyClass = instance.web.Class.extend({
|
||
|
say_hello: function() {
|
||
|
console.log("hello");
|
||
|
},
|
||
|
});
|
||
|
|
||
|
As you can see, you have to call :func:`instance.web.Class.extend` and give
|
||
|
it a dictionary. That dictionary will contain the methods and class attributes
|
||
|
of our new class. Here we simply put a method named ``say_hello()``. This
|
||
|
class can be instantiated and used like this::
|
||
|
|
||
|
var my_object = new instance.oepetstore.MyClass();
|
||
|
my_object.say_hello();
|
||
|
// print "hello" in the console
|
||
|
|
||
|
You can access the attributes of a class inside a method using ``this``::
|
||
|
|
||
|
instance.oepetstore.MyClass = instance.web.Class.extend({
|
||
|
say_hello: function() {
|
||
|
console.log("hello", this.name);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
var my_object = new instance.oepetstore.MyClass();
|
||
|
my_object.name = "Nicolas";
|
||
|
my_object.say_hello();
|
||
|
// print "hello Nicolas" in the console
|
||
|
|
||
|
Classes can have a constructor, it is just a method named ``init()``. You can
|
||
|
pass parameters to the constructor like in most language::
|
||
|
|
||
|
instance.oepetstore.MyClass = instance.web.Class.extend({
|
||
|
init: function(name) {
|
||
|
this.name = name;
|
||
|
},
|
||
|
say_hello: function() {
|
||
|
console.log("hello", this.name);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
var my_object = new instance.oepetstore.MyClass("Nicolas");
|
||
|
my_object.say_hello();
|
||
|
// print "hello Nicolas" in the console
|
||
|
|
||
|
Classes can be inherited. To do so, use :func:`~openerp.web.Class.extend`
|
||
|
directly on your class just like you extended :class:`~openerp.web.Class`::
|
||
|
|
||
|
instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({
|
||
|
say_hello: function() {
|
||
|
console.log("hola", this.name);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
var my_object = new instance.oepetstore.MySpanishClass("Nicolas");
|
||
|
my_object.say_hello();
|
||
|
// print "hola Nicolas" in the console
|
||
|
|
||
|
When overriding a method using inheritance, you can use ``this._super()`` to
|
||
|
call the original method. ``this._super()`` is not a normal method of your
|
||
|
class, you can consider it's magic. Example::
|
||
|
|
||
|
instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({
|
||
|
say_hello: function() {
|
||
|
this._super();
|
||
|
console.log("translation in Spanish: hola", this.name);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
var my_object = new instance.oepetstore.MySpanishClass("Nicolas");
|
||
|
my_object.say_hello();
|
||
|
// print "hello Nicolas \n translation in Spanish: hola Nicolas" in the console
|
||
|
|
||
|
Widgets Basics
|
||
|
--------------
|
||
|
|
||
|
In previous chapter we discovered jQuery and its DOM manipulation tools. It's
|
||
|
useful, but it's not sufficient to structure a real application. Graphical
|
||
|
user interface libraries like Qt, GTK or Windows Forms have classes to
|
||
|
represent visual components. In Odoo, we have the
|
||
|
:class:`~openerp.web.Widget` class. A widget is a generic component
|
||
|
dedicated to display content to the user.
|
||
|
|
||
|
Your First Widget
|
||
|
%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
The start module you installed already contains a small widget::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
console.log("pet store home page loaded");
|
||
|
},
|
||
|
});
|
||
|
|
||
|
Here we create a simple widget by extending the :class:`openerp.web.Widget`
|
||
|
class. This one defines a method named :func:`~openerp.web.Widget.start` that
|
||
|
doesn't do anything really interesting right now.
|
||
|
|
||
|
You may also have noticed this line at the end of the file::
|
||
|
|
||
|
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
|
||
|
|
||
|
This last line registers our basic widget as a client action. Client actions
|
||
|
will be explained in the next part of this guide. For now, just remember that
|
||
|
this is what allows our widget to be displayed when we click on the
|
||
|
:menuselection:`Pet Store --> Pet Store --> Home Page` menu element.
|
||
|
|
||
|
Display Content
|
||
|
%%%%%%%%%%%%%%%
|
||
|
|
||
|
Widgets have a lot of methods and features, but let's start with the basics:
|
||
|
display some data inside the widget and how to instantiate a widget and
|
||
|
display it.
|
||
|
|
||
|
The ``HomePage`` widget already has a :func:`~openerp.web.Widget.start`
|
||
|
method. That method is automatically called after the widget has been
|
||
|
instantiated and it has received the order to display its content. We will use
|
||
|
it to display some content to the user.
|
||
|
|
||
|
To do so, we will also use the :attr:`~openerp.web.Widget.$el` attribute
|
||
|
that all widgets contain. That attribute is a jQuery object with a reference
|
||
|
to the HTML element that represents the root of our widget. A widget can
|
||
|
contain multiple HTML elements, but they must be contained inside one single
|
||
|
element. By default, all widgets have an empty root element which is a
|
||
|
``<div>`` HTML element.
|
||
|
|
||
|
A ``<div>`` element in HTML is usually invisible for the user if it does not
|
||
|
have any content. That explains why when the ``instance.oepetstore.HomePage``
|
||
|
widget is displayed you can't see anything: it simply doesn't have any
|
||
|
content. To show something, we will use some simple jQuery methods on that
|
||
|
object to add some HTML in our root element::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
this.$el.append("<div>Hello dear Odoo user!</div>");
|
||
|
},
|
||
|
});
|
||
|
|
||
|
That message will now appear when you go to the menu :menuselection:`Pet Store
|
||
|
--> Pet Store --> Home Page` (remember you need to refresh your web browser,
|
||
|
although there is not need to restart Odoo's server).
|
||
|
|
||
|
Now you should learn how to instantiate a widget and display its content. To
|
||
|
do so, we will create a new widget::
|
||
|
|
||
|
instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
this.$el.append("<div>We are so happy to see you again in this menu!</div>");
|
||
|
},
|
||
|
});
|
||
|
|
||
|
Now we want to display the ``instance.oepetstore.GreetingsWidget`` inside the
|
||
|
home page. To do so we can use the :func:`~openerp.web.Widget.append`
|
||
|
method of ``Widget``::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
this.$el.append("<div>Hello dear Odoo user!</div>");
|
||
|
var greeting = new instance.oepetstore.GreetingsWidget(this);
|
||
|
greeting.appendTo(this.$el);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
Here, the ``HomePage`` instantiate a ``GreetingsWidget`` (the first argument
|
||
|
of the constructor of ``GreetingsWidget`` will be explained in the next
|
||
|
part). Then it asks the ``GreetingsWidget`` to insert itself inside the DOM,
|
||
|
more precisely directly under the ``HomePage`` widget.
|
||
|
|
||
|
When the :func:`~openerp.web.Widget.appendTo` method is called, it asks the
|
||
|
widget to insert itself and to display its content. It's during the call to
|
||
|
:func:`~openerp.web.Widget.appentTo` that the
|
||
|
:func:`~openerp.web.Widget.start` method will be called.
|
||
|
|
||
|
To check the consequences of that code, let's use Chrome's DOM explorer. But
|
||
|
before that we will modify a little bit our widgets to have some classes on
|
||
|
some of our ``<div>`` elements so we can clearly see them in the explorer::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
this.$el.addClass("oe_petstore_homepage");
|
||
|
...
|
||
|
},
|
||
|
});
|
||
|
instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
this.$el.addClass("oe_petstore_greetings");
|
||
|
...
|
||
|
},
|
||
|
});
|
||
|
|
||
|
The result will be this if you can find the correct DOM part in the DOM explorer:
|
||
|
|
||
|
.. code-block:: html
|
||
|
|
||
|
<div class="oe_petstore_homepage">
|
||
|
<div>Hello dear Odoo user!</div>
|
||
|
<div class="oe_petstore_greetings">
|
||
|
<div>We are so happy to see you again in this menu!</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
Here we can clearly see the two ``<div>`` created implicitly by
|
||
|
:class:`~openerp.web.Widget`, because we added some classes on them. We can
|
||
|
also see the two divs containing messages we created using the jQuery methods
|
||
|
on ``$el``. Finally, note the ``<div class="oe_petstore_greetings">`` element
|
||
|
which represents the ``GreetingsWidget`` instance is *inside* the ``<div
|
||
|
class="oe_petstore_homepage">`` which represents the ``HomePage`` instance.
|
||
|
|
||
|
Widget Parents and Children
|
||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
In the previous part, we instantiated a widget using this syntax::
|
||
|
|
||
|
new instance.oepetstore.GreetingsWidget(this);
|
||
|
|
||
|
The first argument is ``this``, which in that case was a ``HomePage``
|
||
|
instance. This serves to indicate the Widget what other widget is his parent.
|
||
|
|
||
|
As we've seen, widgets are usually inserted in the DOM by another widget and
|
||
|
*inside* that other widget. This means most widgets are always a part of
|
||
|
another widget. We call the container the *parent*, and the contained widget
|
||
|
the *child*.
|
||
|
|
||
|
Due to multiple technical and conceptual reasons, it is necessary for a widget
|
||
|
to know who is his parent and who are its children. This is why we have that
|
||
|
first parameter in the constructor of all widgets.
|
||
|
|
||
|
:func:`~openerp.web.Widget.getParent` can be used to get the parent of a
|
||
|
widget::
|
||
|
|
||
|
instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
console.log(this.getParent().$el );
|
||
|
// will print "div.oe_petstore_homepage" in the console
|
||
|
},
|
||
|
});
|
||
|
|
||
|
:func:`~openerp.web.Widget.getChildren` can be used to get a list of its
|
||
|
children::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
var greeting = new instance.oepetstore.GreetingsWidget(this);
|
||
|
greeting.appendTo(this.$el);
|
||
|
console.log(this.getChildren()[0].$el);
|
||
|
// will print "div.oe_petstore_greetings" in the console
|
||
|
},
|
||
|
});
|
||
|
|
||
|
You should also remember that, when you override the
|
||
|
:func:`~openerp.web.Widget.init` method of a widget you should always put the
|
||
|
parent as first parameter are pass it to ``this._super()``::
|
||
|
|
||
|
instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({
|
||
|
init: function(parent, name) {
|
||
|
this._super(parent);
|
||
|
this.name = name;
|
||
|
},
|
||
|
});
|
||
|
|
||
|
Finally, if a widget does not logically have a parent (ie: because it's the
|
||
|
first widget you instantiate in an application), you can give null as a parent
|
||
|
instead::
|
||
|
|
||
|
new instance.oepetstore.GreetingsWidget(null);
|
||
|
|
||
|
Destroying Widgets
|
||
|
%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
If you can display content to your users, you should also be able to erase
|
||
|
it. This can simply be done using the :func:`~openerp.web.Widget.destroy`
|
||
|
method:
|
||
|
|
||
|
greeting.destroy();
|
||
|
|
||
|
When a widget is destroyed it will first call
|
||
|
:func:`~openerp.web.Widget.destroy` on all its children. Then it erases itself
|
||
|
from the DOM. The recursive call to destroy from parents to children is very
|
||
|
useful to clean properly complex structures of widgets and avoid memory leaks
|
||
|
that can easily appear in big JavaScript applications.
|
||
|
|
||
|
.. _howtos/web/qweb:
|
||
|
|
||
|
The QWeb Template Engine
|
||
|
------------------------
|
||
|
|
||
|
The previous part of the guide showed how to define widgets that are able to
|
||
|
display HTML to the user. The example ``GreetingsWidget`` used a syntax like
|
||
|
this::
|
||
|
|
||
|
this.$el.append("<div>Hello dear Odoo user!</div>");
|
||
|
|
||
|
This technically allow us to display any HTML, even if it is very complex and
|
||
|
require to be generated by code. Although generating text using pure
|
||
|
JavaScript is not very nice, that would necessitate to copy-paste a lot of
|
||
|
HTML lines inside our JavaScript source file, add the ``"`` character at the
|
||
|
beginning and the end of each line, etc...
|
||
|
|
||
|
The problem is exactly the same in most programming languages needing to
|
||
|
generate HTML. That's why they typically use template engines. Example of
|
||
|
template engines are Velocity, JSP (Java), Mako, Jinja (Python), Smarty (PHP),
|
||
|
etc...
|
||
|
|
||
|
In Odoo we use a template engine developed specifically for Odoo's web
|
||
|
client. Its name is QWeb.
|
||
|
|
||
|
QWeb is an XML-based templating language, similar to `Genshi
|
||
|
<http://en.wikipedia.org/wiki/Genshi_(templating_language)>`_, `Thymeleaf
|
||
|
<http://en.wikipedia.org/wiki/Thymeleaf>`_ or `Facelets
|
||
|
<http://en.wikipedia.org/wiki/Facelets>`_ with a few peculiarities:
|
||
|
|
||
|
* It's implemented fully in JavaScript and rendered in the browser.
|
||
|
* Each template file (XML files) contains multiple templates, where template
|
||
|
engine usually have a 1:1 mapping between template files and templates.
|
||
|
* It has special support in Odoo Web's :class:`~openerp.web.Widget`, though it
|
||
|
can be used outside of Odoo's web client (and it's possible to use
|
||
|
:class:`~openerp.web.Widget` without relying on QWeb).
|
||
|
|
||
|
The rationale behind using QWeb instead of existing javascript template
|
||
|
engines is that its extension mechanism is very similar to the Odoo view
|
||
|
inheritance mechanism. Like Odoo views a QWeb template is an XML tree and
|
||
|
therefore XPath or DOM manipulations are easy to perform on it.
|
||
|
|
||
|
Using QWeb inside a Widget
|
||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
First let's define a simple QWeb template in
|
||
|
``oepetstore/static/src/xml/petstore.xml`` file, the exact meaning will be
|
||
|
explained later:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
||
|
<templates xml:space="preserve">
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div style="background-color: red;">This is some simple HTML</div>
|
||
|
</t>
|
||
|
</templates>
|
||
|
|
||
|
Now let's modify the ``HomePage`` class. Remember that enigmatic line at the
|
||
|
beginning the the JavaScript source file?
|
||
|
|
||
|
::
|
||
|
|
||
|
var QWeb = instance.web.qweb;
|
||
|
|
||
|
This is a line we recommend to copy-paste in all Odoo web modules. It is the
|
||
|
object giving access to all templates defined in template files that were
|
||
|
loaded by the web client. We can use the template we defined in our XML
|
||
|
template file like this::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
this.$el.append(QWeb.render("HomePageTemplate"));
|
||
|
},
|
||
|
});
|
||
|
|
||
|
Calling the ``QWeb.render()`` method asks to render the template identified by
|
||
|
the string passed as first parameter.
|
||
|
|
||
|
Another possibility commonly seen in Odoo code is to use ``Widget``'s
|
||
|
integration with QWeb::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
template: "HomePageTemplate",
|
||
|
start: function() {
|
||
|
...
|
||
|
},
|
||
|
});
|
||
|
|
||
|
When you put a ``template`` class attribute in a widget, the widget knows it
|
||
|
has to call ``QWeb.render()`` to render that template.
|
||
|
|
||
|
Please note there is a difference between those two syntaxes. When you use
|
||
|
``Widget``'s QWeb integration the ``QWeb.render()`` method is called *before*
|
||
|
the widget calls :func:`~openerp.web.Widget.start`. It will also take the root
|
||
|
element of the rendered template and put it as a replacement of the default
|
||
|
root element generated by the :class:`~openerp.web.Widget` class. This will
|
||
|
alter the behavior, so you should remember it.
|
||
|
|
||
|
QWeb Context
|
||
|
''''''''''''
|
||
|
|
||
|
Like with all template engines, QWeb templates can contain code able to
|
||
|
manipulate data that is given to the template. To pass data to QWeb, use the
|
||
|
second argument to ``QWeb.render()``:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div>Hello <t t-esc="name"/></div>
|
||
|
</t>
|
||
|
|
||
|
::
|
||
|
|
||
|
QWeb.render("HomePageTemplate", {name: "Nicolas"});
|
||
|
|
||
|
Result:
|
||
|
|
||
|
.. code-block:: html
|
||
|
|
||
|
<div>Hello Nicolas</div>
|
||
|
|
||
|
When you use :class:`~openerp.web.Widget`'s integration you can not pass
|
||
|
additional data to the template. Instead the template will have a unique
|
||
|
``widget`` variable which is a reference to the current widget:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div>Hello <t t-esc="widget.name"/></div>
|
||
|
</t>
|
||
|
|
||
|
::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
template: "HomePageTemplate",
|
||
|
init: function(parent) {
|
||
|
this._super(parent);
|
||
|
this.name = "Nicolas";
|
||
|
},
|
||
|
start: function() {
|
||
|
},
|
||
|
});
|
||
|
|
||
|
Result:
|
||
|
|
||
|
.. code-block:: html
|
||
|
|
||
|
<div>Hello Nicolas</div>
|
||
|
|
||
|
Template Declaration
|
||
|
''''''''''''''''''''
|
||
|
|
||
|
Now that we know everything about rendering templates we can try to understand
|
||
|
QWeb's syntax.
|
||
|
|
||
|
All QWeb directives use XML attributes beginning with the prefix ``t-``. To
|
||
|
declare new templates, we add a ``<t t-name="...">`` element into the XML
|
||
|
template file inside the root element ``<templates>``::
|
||
|
|
||
|
<templates>
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div>This is some simple HTML</div>
|
||
|
</t>
|
||
|
</templates>
|
||
|
|
||
|
``t-name`` simply declares a template that can be called using
|
||
|
``QWeb.render()``.
|
||
|
|
||
|
Escaping
|
||
|
''''''''
|
||
|
|
||
|
To put some text in the HTML, use ``t-esc``:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div>Hello <t t-esc="name"/></div>
|
||
|
</t>
|
||
|
|
||
|
|
||
|
This will output the variable ``name`` and escape its content in case it
|
||
|
contains some characters that looks like HTML. Please note the attribute
|
||
|
``t-esc`` can contain any type of JavaScript expression:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div><t t-esc="3+5"/></div>
|
||
|
</t>
|
||
|
|
||
|
Will render:
|
||
|
|
||
|
.. code-block:: html
|
||
|
|
||
|
<div>8</div>
|
||
|
|
||
|
Outputting HTML
|
||
|
'''''''''''''''
|
||
|
|
||
|
If you know you have some HTML contained in a variable, use ``t-raw`` instead
|
||
|
of ``t-esc``:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div><t t-raw="some_html"/></div>
|
||
|
</t>
|
||
|
|
||
|
If
|
||
|
''
|
||
|
|
||
|
The basic alternative block of QWeb is ``t-if``:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div>
|
||
|
<t t-if="true == true">
|
||
|
true is true
|
||
|
</t>
|
||
|
<t t-if="true == false">
|
||
|
true is not true
|
||
|
</t>
|
||
|
</div>
|
||
|
</t>
|
||
|
|
||
|
Although QWeb does not contains any structure for else.
|
||
|
|
||
|
Foreach
|
||
|
'''''''
|
||
|
|
||
|
To iterate on a list, use ``t-foreach`` and ``t-as``:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div>
|
||
|
<t t-foreach="names" t-as="name">
|
||
|
<div>
|
||
|
Hello <t t-esc="name"/>
|
||
|
</div>
|
||
|
</t>
|
||
|
</div>
|
||
|
</t>
|
||
|
|
||
|
Setting the Value of an XML Attribute
|
||
|
'''''''''''''''''''''''''''''''''''''
|
||
|
|
||
|
QWeb has a special syntax to set the value of an attribute. You must use
|
||
|
``t-att-xxx`` and replace ``xxx`` with the name of the attribute:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="HomePageTemplate">
|
||
|
<div>
|
||
|
Input your name:
|
||
|
<input type="text" t-att-value="defaultName"/>
|
||
|
</div>
|
||
|
</t>
|
||
|
|
||
|
To Learn More About QWeb
|
||
|
''''''''''''''''''''''''
|
||
|
|
||
|
For a QWeb reference, see :ref:`reference/qweb`.
|
||
|
|
||
|
Exercise
|
||
|
''''''''
|
||
|
|
||
|
.. exercise:: Usage of QWeb in Widgets
|
||
|
|
||
|
Create a widget whose constructor contains two parameters aside from
|
||
|
``parent``: ``product_names`` and ``color``. ``product_names`` is a list
|
||
|
of strings, each one being a name of product. ``color`` is a string
|
||
|
containing a color in CSS color format (ie: ``#000000`` for black). That
|
||
|
widget should display the given product names one under the other, each
|
||
|
one in a separate box with a background color with the value of ``color``
|
||
|
and a border. You must use QWeb to render the HTML. This exercise will
|
||
|
necessitate some CSS that you should put in
|
||
|
``oepetstore/static/src/css/petstore.css``. Display that widget in the
|
||
|
``HomePage`` widget with a list of five products and green as the
|
||
|
background color for boxes.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
::
|
||
|
|
||
|
openerp.oepetstore = function(instance) {
|
||
|
var _t = instance.web._t,
|
||
|
_lt = instance.web._lt;
|
||
|
var QWeb = instance.web.qweb;
|
||
|
|
||
|
instance.oepetstore = {};
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
var products = new instance.oepetstore.ProductsWidget(this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00");
|
||
|
products.appendTo(this.$el);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.oepetstore.ProductsWidget = instance.web.Widget.extend({
|
||
|
template: "ProductsWidget",
|
||
|
init: function(parent, products, color) {
|
||
|
this._super(parent);
|
||
|
this.products = products;
|
||
|
this.color = color;
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
|
||
|
}
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
||
|
<templates xml:space="preserve">
|
||
|
<t t-name="ProductsWidget">
|
||
|
<div>
|
||
|
<t t-foreach="widget.products" t-as="product">
|
||
|
<span class="oe_products_item" t-att-style="'background-color: ' + widget.color + ';'"><t t-esc="product"/></span><br/>
|
||
|
</t>
|
||
|
</div>
|
||
|
</t>
|
||
|
</templates>
|
||
|
|
||
|
.. code-block:: css
|
||
|
|
||
|
.oe_products_item {
|
||
|
display: inline-block;
|
||
|
padding: 3px;
|
||
|
margin: 5px;
|
||
|
border: 1px solid black;
|
||
|
border-radius: 3px;
|
||
|
}
|
||
|
|
||
|
.. image:: web/qweb.*
|
||
|
:align: center
|
||
|
:width: 70%
|
||
|
|
||
|
Widget Events and Properties
|
||
|
----------------------------
|
||
|
|
||
|
Widgets still have more helper to learn. One of the more complex (and useful)
|
||
|
one is the event system. Events are also closely related to the widget
|
||
|
properties.
|
||
|
|
||
|
Events
|
||
|
%%%%%%
|
||
|
|
||
|
Widgets are able to fire events in a similar way most components in existing
|
||
|
graphical user interfaces libraries (Qt, GTK, Swing,...) handle
|
||
|
them. Example::
|
||
|
|
||
|
instance.oepetstore.ConfirmWidget = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
this.$el.append("<div>Are you sure you want to perform this action?</div>" +
|
||
|
"<button class='ok_button'>Ok</button>" +
|
||
|
"<button class='cancel_button'>Cancel</button>");
|
||
|
this.$el.find("button.ok_button").click(function() {
|
||
|
self.trigger("user_choose", true);
|
||
|
});
|
||
|
this.$el.find("button.cancel_button").click(function() {
|
||
|
self.trigger("user_choose", false);
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
var widget = new instance.oepetstore.ConfirmWidget(this);
|
||
|
widget.on("user_choose", this, this.user_choose);
|
||
|
widget.appendTo(this.$el);
|
||
|
},
|
||
|
user_choose: function(confirm) {
|
||
|
if (confirm) {
|
||
|
console.log("The user agreed to continue");
|
||
|
} else {
|
||
|
console.log("The user refused to continue");
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
First, we will explain what this example is supposed to do. We create a
|
||
|
generic widget to ask the user if he really wants to do an action that could
|
||
|
have important consequences (a type widget heavily used in Windows). To do so,
|
||
|
we put two buttons in the widget. Then we bind jQuery events to know when the
|
||
|
user click these buttons.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
It could be hard to understand this particular line::
|
||
|
|
||
|
var self = this;
|
||
|
|
||
|
Remember, in JavaScript the variable ``this`` is a variable that is passed
|
||
|
implicitly to all functions. It allows us to know which is the object if
|
||
|
function is used like a method. Each declared function has its own
|
||
|
``this``. So, when we declare a function inside a function, that new
|
||
|
function will have its own ``this`` that could be different from the
|
||
|
``this`` of the parent function. If we want to remember the original
|
||
|
object the simplest method is to store a reference in a variable. By
|
||
|
convention in Odoo we very often name that variable ``self`` because it's
|
||
|
the equivalent of ``this`` in Python.
|
||
|
|
||
|
Since our widget is supposed to be generic, it should not perform any precise
|
||
|
action by itself. So, we simply make it trigger and event named
|
||
|
``user_choose`` by using the :func:`~openerp.web.Widget.trigger` method.
|
||
|
|
||
|
:func:`~openerp.web.Widget.trigger` takes as first argument the name of the
|
||
|
event to trigger. Then it can takes any number of additional arguments. These
|
||
|
arguments will be passed to all the event listeners.
|
||
|
|
||
|
Then we modify the ``HomePage`` widget to instantiate a ``ConfirmWidget`` and
|
||
|
listen to its ``user_choose`` event by calling the
|
||
|
:func:`~openerp.web.Widget.on` method.
|
||
|
|
||
|
:func:`~openerp.web.Widget.on` allows to bind a function to be called when the
|
||
|
event identified by event_name is ``triggered``. The ``func`` argument is the
|
||
|
function to call and ``object`` is the object to which that function is
|
||
|
related if it is a method. The binded function will be called with the
|
||
|
additional arguments of :func:`~openerp.web.Widget.trigger` if it has
|
||
|
any. Example::
|
||
|
|
||
|
start: function() {
|
||
|
var widget = ...
|
||
|
widget.on("my_event", this, this.my_event_triggered);
|
||
|
widget.trigger("my_event", 1, 2, 3);
|
||
|
},
|
||
|
my_event_triggered: function(a, b, c) {
|
||
|
console.log(a, b, c);
|
||
|
// will print "1 2 3"
|
||
|
}
|
||
|
|
||
|
Properties
|
||
|
%%%%%%%%%%
|
||
|
|
||
|
Properties are very similar to normal object attributes. They allow to set
|
||
|
data on an object but with an additional feature: it triggers events when a
|
||
|
property's value has changed::
|
||
|
|
||
|
start: function() {
|
||
|
this.widget = ...
|
||
|
this.widget.on("change:name", this, this.name_changed);
|
||
|
this.widget.set("name", "Nicolas");
|
||
|
},
|
||
|
name_changed: function() {
|
||
|
console.log("The new value of the property 'name' is", this.widget.get("name"));
|
||
|
}
|
||
|
|
||
|
:func:`~openerp.web.Widget.set` allows to set the value of property. If the
|
||
|
value changed (or it didn't had a value previously) the object will trigger a
|
||
|
``change:xxx`` where ``xxx`` is the name of the property.
|
||
|
|
||
|
:func:`~openerp.web.Widget.get` allows to retrieve the value of a property.
|
||
|
|
||
|
Exercise
|
||
|
%%%%%%%%
|
||
|
|
||
|
.. exercise:: Widget Properties and Events
|
||
|
|
||
|
Create a widget ``ColorInputWidget`` that will display 3 ``<input
|
||
|
type="text">``. Each of these ``<input>`` is dedicated to type a
|
||
|
hexadecimal number from 00 to FF. When any of these ``<input>`` is
|
||
|
modified by the user the widget must query the content of the three
|
||
|
``<input>``, concatenate their values to have a complete CSS color code
|
||
|
(ie: ``#00FF00``) and put the result in a property named ``color``. Please
|
||
|
note the jQuery ``change()`` event that you can bind on any HTML
|
||
|
``<input>`` element and the ``val()`` method that can query the current
|
||
|
value of that ``<input>`` could be useful to you for this exercise.
|
||
|
|
||
|
Then, modify the ``HomePage`` widget to instantiate ``ColorInputWidget``
|
||
|
and display it. The ``HomePage`` widget should also display an empty
|
||
|
rectangle. That rectangle must always, at any moment, have the same
|
||
|
background color than the color in the ``color`` property of the
|
||
|
``ColorInputWidget`` instance.
|
||
|
|
||
|
Use QWeb to generate all HTML.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
::
|
||
|
|
||
|
openerp.oepetstore = function(instance) {
|
||
|
var _t = instance.web._t,
|
||
|
_lt = instance.web._lt;
|
||
|
var QWeb = instance.web.qweb;
|
||
|
|
||
|
instance.oepetstore = {};
|
||
|
|
||
|
instance.oepetstore.ColorInputWidget = instance.web.Widget.extend({
|
||
|
template: "ColorInputWidget",
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
this.$el.find("input").change(function() {
|
||
|
self.input_changed();
|
||
|
});
|
||
|
self.input_changed();
|
||
|
},
|
||
|
input_changed: function() {
|
||
|
var color = "#";
|
||
|
color += this.$el.find(".oe_color_red").val();
|
||
|
color += this.$el.find(".oe_color_green").val();
|
||
|
color += this.$el.find(".oe_color_blue").val();
|
||
|
this.set("color", color);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
template: "HomePage",
|
||
|
start: function() {
|
||
|
this.colorInput = new instance.oepetstore.ColorInputWidget(this);
|
||
|
this.colorInput.on("change:color", this, this.color_changed);
|
||
|
this.colorInput.appendTo(this.$el);
|
||
|
},
|
||
|
color_changed: function() {
|
||
|
this.$el.find(".oe_color_div").css("background-color", this.colorInput.get("color"));
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
|
||
|
}
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
||
|
<templates xml:space="preserve">
|
||
|
<t t-name="ColorInputWidget">
|
||
|
<div>
|
||
|
Red: <input type="text" class="oe_color_red" value="00"></input><br />
|
||
|
Green: <input type="text" class="oe_color_green" value="00"></input><br />
|
||
|
Blue: <input type="text" class="oe_color_blue" value="00"></input><br />
|
||
|
</div>
|
||
|
</t>
|
||
|
<t t-name="HomePage">
|
||
|
<div>
|
||
|
<div class="oe_color_div"></div>
|
||
|
</div>
|
||
|
</t>
|
||
|
</templates>
|
||
|
|
||
|
.. code-block:: css
|
||
|
|
||
|
.oe_color_div {
|
||
|
width: 100px;
|
||
|
height: 100px;
|
||
|
margin: 10px;
|
||
|
}
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
jQuery's ``css()`` method allows setting a css property.
|
||
|
|
||
|
Widget Helpers
|
||
|
--------------
|
||
|
|
||
|
We've seen the basics of the :class:`~openerp.web.Widget` class, QWeb and the
|
||
|
events/properties system. There are still some more useful methods proposed by
|
||
|
this class.
|
||
|
|
||
|
``Widget``'s jQuery Selector
|
||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
It is very common to need to select a precise element inside a widget. In the
|
||
|
previous part of this guide we've seen a lot of uses of the ``find()`` method
|
||
|
of jQuery objects::
|
||
|
|
||
|
this.$el.find("input.my_input")...
|
||
|
|
||
|
:class:`~openerp.web.Widget` provides a shorter syntax that does the same
|
||
|
thing with the :func:`~openerp.web.Widget.$` method::
|
||
|
|
||
|
instance.oepetstore.MyWidget = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
this.$("input.my_input")...
|
||
|
},
|
||
|
});
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
We strongly advise you against using directly the global jQuery function
|
||
|
``$()`` like we did in the previous chapter were we explained the jQuery
|
||
|
library and jQuery selectors. That type of global selection is sufficient
|
||
|
for simple applications but is not a good idea in real, big web
|
||
|
applications. The reason is simple: when you create a new type of widget
|
||
|
you never know how many times it will be instantiated. Since the ``$()``
|
||
|
global function operates in *the whole HTML displayed in the browser*, if
|
||
|
you instantiate a widget 2 times and use that function you will
|
||
|
incorrectly select the content of another instance of your widget. That's
|
||
|
why you must restrict the jQuery selections to HTML which is located
|
||
|
*inside* your widget most of the time.
|
||
|
|
||
|
Applying the same logic, you can also guess it is a very bad idea to try
|
||
|
to use HTML ids in any widget. If the widget is instantiated 2 times you
|
||
|
will have 2 different HTML element in the whole application that have the
|
||
|
same
|
||
|
id. And that is an error by itself. So you should stick to CSS classes to mark your HTML elements in all cases.
|
||
|
|
||
|
Easier DOM Events Binding
|
||
|
%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
In the previous part, we had to bind a lot of HTML element events like
|
||
|
``click()`` or ``change()``. Now that we have the ``$()`` method to simplify
|
||
|
code a little, let's see how it would look like::
|
||
|
|
||
|
instance.oepetstore.MyWidget = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
this.$(".my_button").click(function() {
|
||
|
self.button_clicked();
|
||
|
});
|
||
|
},
|
||
|
button_clicked: function() {
|
||
|
..
|
||
|
},
|
||
|
});
|
||
|
|
||
|
It's still a bit long to type. That's why there is an even more simple syntax
|
||
|
for that::
|
||
|
|
||
|
instance.oepetstore.MyWidget = instance.web.Widget.extend({
|
||
|
events: {
|
||
|
"click .my_button": "button_clicked",
|
||
|
},
|
||
|
button_clicked: function() {
|
||
|
..
|
||
|
}
|
||
|
});
|
||
|
|
||
|
.. warning::
|
||
|
|
||
|
It's important to differentiate the jQuery events that are triggered on
|
||
|
DOM elements and events of the widgets. The ``event`` class attribute *is
|
||
|
a helper to help binding jQuery events*, it has nothing to do with the
|
||
|
widget events that can be binded using the ``on()`` method.
|
||
|
|
||
|
The ``event`` class attribute is a dictionary that allows to define jQuery
|
||
|
events with a shorter syntax.
|
||
|
|
||
|
The key is a string with 2 different parts separated with a space. The first
|
||
|
part is the name of the event, the second one is the jQuery selector. So the
|
||
|
key ``click .my_button`` will bind the event ``click`` on the elements
|
||
|
matching the selector ``my_button``.
|
||
|
|
||
|
The value is a string with the name of the method to call on the current
|
||
|
object.
|
||
|
|
||
|
Development Guidelines
|
||
|
%%%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
As explained in the prerequisites to read this guide, you should already know
|
||
|
HTML and CSS. But developing web applications in JavaScript or developing web
|
||
|
modules for Odoo require to be more strict than you will usually be when
|
||
|
simply creating static web pages with CSS to style them. So these guidelines
|
||
|
should be followed if you want to have manageable projects and avoid bugs or
|
||
|
common mistakes:
|
||
|
|
||
|
* 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. Just about all the time, they can be replaced with
|
||
|
nothing, with classes or with keeping a reference to a DOM node or a jQuery
|
||
|
element around.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
If it is absolutely necessary to have an ``id`` (because a third-party
|
||
|
library requires one and can't take a DOM element), it should be
|
||
|
generated with ``_.uniqueId()``.
|
||
|
|
||
|
* 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:`~openerp.web.Widget` has an attribute providing its DOM root
|
||
|
(:attr:`~openerp.web.Widget.$el`), and a shortcut to select nodes directly
|
||
|
(:func:`~openerp.web.Widget.$`).
|
||
|
|
||
|
* More generally, never assume your components own or controls anything beyond
|
||
|
its own personal :attr:`~openerp.web.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 Widget and correctly implement
|
||
|
and use its API and life cycle.
|
||
|
|
||
|
Modify Existent Widgets and Classes
|
||
|
-----------------------------------
|
||
|
|
||
|
The class system of the Odoo web framework allows direct modification of
|
||
|
existing classes using the :func:`~openerp.web.Widget.include` method of a
|
||
|
class::
|
||
|
|
||
|
var TestClass = instance.web.Class.extend({
|
||
|
testMethod: function() {
|
||
|
return "hello";
|
||
|
},
|
||
|
});
|
||
|
|
||
|
TestClass.include({
|
||
|
testMethod: function() {
|
||
|
return this._super() + " world";
|
||
|
},
|
||
|
});
|
||
|
|
||
|
console.log(new TestClass().testMethod());
|
||
|
// will print "hello world"
|
||
|
|
||
|
This system is similar to the inheritance mechanism, except it will directly
|
||
|
modify the class. You can call ``this._super()`` to call the original
|
||
|
implementation of the methods you are redefining. If the class already had
|
||
|
sub-classes, all calls to ``this._super()`` in sub-classes will call the new
|
||
|
implementations defined in the call to ``include()``. This will also work if
|
||
|
some instances of the class (or of any of its sub-classes) were created prior
|
||
|
to the call to :func:`~openerp.web.Widget.include`.
|
||
|
|
||
|
.. warning::
|
||
|
|
||
|
Please note that, even if :func:`~openerp.web.Widget.include` can be a
|
||
|
powerful tool, it's not considered a very good programming practice
|
||
|
because it can easily create problems if used in a wrong way. So you
|
||
|
should use it to modify the behavior of an existing component only when
|
||
|
there are no other options, and try to limit its usages to the strict
|
||
|
minimum.
|
||
|
|
||
|
Translations
|
||
|
------------
|
||
|
|
||
|
The process to translate text in Python and JavaScript code is very
|
||
|
similar. You could have noticed these lines at the beginning of the
|
||
|
``petstore.js`` file:
|
||
|
|
||
|
var _t = instance.web._t,
|
||
|
_lt = instance.web._lt;
|
||
|
|
||
|
These lines are simply used to import the translation functions in the current
|
||
|
JavaScript module. The correct to use them is this one::
|
||
|
|
||
|
this.$el.text(_t("Hello dear user!"));
|
||
|
|
||
|
In Odoo, translations files are automatically generated by scanning the source
|
||
|
code. All piece of code that calls a certain function are detected and their
|
||
|
content is added to a translation file that will then be sent to the
|
||
|
translators. In Python, the function is ``_()``. In JavaScript the function is
|
||
|
:func:`~openerp.web._t` (and also :func:`~openerp.web._lt`).
|
||
|
|
||
|
If the source file as never been scanned and the translation files does not
|
||
|
contain any translation for the text given to ``_t()`` it will return the text
|
||
|
as-is. If there is a translation it will return it.
|
||
|
|
||
|
:func:`~openerp.web._lt` does almost the exact same thing but is a little bit
|
||
|
more complicated. It does not return a text but returns a function that will
|
||
|
return the text. It is reserved for very special cases::
|
||
|
|
||
|
var text_func = _lt("Hello dear user!");
|
||
|
this.$el.text(text_func());
|
||
|
|
||
|
To have more information about Odoo's translations, please take a look at the
|
||
|
reference documentation: https://doc.openerp.com/contribute/translations/ .
|
||
|
|
||
|
Communication with the Odoo Server
|
||
|
-------------------------------------
|
||
|
|
||
|
Now you should know everything you need to display any type of graphical user
|
||
|
interface with your Odoo modules. Still, Odoo is a database-centric
|
||
|
application so it's still not very useful if you can't query data from the
|
||
|
database.
|
||
|
|
||
|
As a reminder, in Odoo you are not supposed to directly query data from the
|
||
|
PostgreSQL database, you will always use the build-in ORM (Object-Relational
|
||
|
Mapping) and more precisely the Odoo *models*.
|
||
|
|
||
|
Contacting Models
|
||
|
%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
In the previous chapter we explained how to send HTTP requests to the web
|
||
|
server using the ``$.ajax()`` method and the JSON format. It is useful to know
|
||
|
how to make a JavaScript application communicate with its web server using
|
||
|
these tools, but it's still a little bit low-level to be used in a complex
|
||
|
application like Odoo.
|
||
|
|
||
|
When the web client contacts the Odoo server it has to pass additional data
|
||
|
like the necessary information to authenticate the current user. There is also
|
||
|
some more complexity due to Odoo models that need a higher-level communication
|
||
|
protocol to be used.
|
||
|
|
||
|
This is why you will not use directly ``$.ajax()`` to communicate with the
|
||
|
server. The web client framework provides classes to abstract that protocol.
|
||
|
|
||
|
To demonstrate this, the file ``petstore.py`` already contains a small model
|
||
|
with a sample method:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
class message_of_the_day(osv.osv):
|
||
|
_name = "message_of_the_day"
|
||
|
|
||
|
def my_method(self, cr, uid, context=None):
|
||
|
return {"hello": "world"}
|
||
|
|
||
|
_columns = {
|
||
|
'message': fields.text(string="Message"),
|
||
|
'color': fields.char(string="Color", size=20),
|
||
|
}
|
||
|
|
||
|
If you know Odoo models that code should be familiar to you. This model
|
||
|
declares a table named ``message_of_the_day`` with two fields. It also has a
|
||
|
method ``my_method()`` that doesn't do much except return a dictionary.
|
||
|
|
||
|
Here is a sample widget that calls ``my_method()`` and displays the result::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
var model = new instance.web.Model("message_of_the_day");
|
||
|
model.call("my_method", [], {context: new instance.web.CompoundContext()}).then(function(result) {
|
||
|
self.$el.append("<div>Hello " + result["hello"] + "</div>");
|
||
|
// will show "Hello world" to the user
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
The class used to contact Odoo models is ``instance.web.Model``. When you
|
||
|
instantiate it, you must give as first argument to its constructor the name of
|
||
|
the model you want to contact in Odoo. (Here it is ``message_of_the_day``, the
|
||
|
model created for this example, but it could be any other model like
|
||
|
``res.partner``.)
|
||
|
|
||
|
:func:`~openerp.web.Model.call` is the method of :class:`~openerp.web.Model`
|
||
|
used to call any method of an Odoo server-side model. Here are its arguments:
|
||
|
|
||
|
* ``name`` is the name of the method to call on the model. Here it is the
|
||
|
method named ``my_method``.
|
||
|
* ``args`` is a list of positional arguments to give to the method. The sample
|
||
|
``my_method()`` method does not contain any particular argument we want to
|
||
|
give to it, so here is another example:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
def my_method2(self, cr, uid, a, b, c, context=None): ...
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
model.call("my_method", [1, 2, 3], ...
|
||
|
// with this a=1, b=2 and c=3
|
||
|
|
||
|
* ``kwargs`` is a list of named arguments to give to the method. In the
|
||
|
example, we have one named argument which is a bit special:
|
||
|
``context``. It's given a value that may seem very strange right now: ``new
|
||
|
instance.web.CompoundContext()``. The meaning of that argument will be
|
||
|
explained later. Right now you should just know the ``kwargs`` argument
|
||
|
allows to give arguments to the Python method by name instead of
|
||
|
position. Example:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
def my_method2(self, cr, uid, a, b, c, context=None): ...
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
model.call("my_method", [], {a: 1, b: 2, c: 3, ...
|
||
|
// with this a=1, b=2 and c=3
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
If you take a look at the ``my_method()``'s declaration in Python, you can
|
||
|
see it has two arguments named ``cr`` and ``uid``:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
def my_method(self, cr, uid, context=None):
|
||
|
|
||
|
You could have noticed we do not give theses arguments to the server when
|
||
|
we call that method from JavaScript. That is because theses arguments that
|
||
|
have to be declared in all models' methods are never sent from the Odoo
|
||
|
client. These arguments are added implicitly by the Odoo server. The
|
||
|
first one is an object called the *cursor* that allows communication with
|
||
|
the database. The second one is the id of the currently logged in user.
|
||
|
|
||
|
:func:`~openerp.web.Widget.call` returns a deferred resolved with the value
|
||
|
returned by the model's method as first argument. If you don't know what
|
||
|
deferreds are, take a look at the previous chapter (the part about HTTP
|
||
|
requests in jQuery).
|
||
|
|
||
|
CompoundContext
|
||
|
%%%%%%%%%%%%%%%
|
||
|
|
||
|
In the previous part, we avoided to explain the strange ``context`` argument
|
||
|
in the call to our model's method:
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
model.call("my_method", [], {context: new instance.web.CompoundContext()})
|
||
|
|
||
|
In Odoo, models' methods should always have an argument named ``context``:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
def my_method(self, cr, uid, context=None): ...
|
||
|
|
||
|
The context is like a "magic" argument that the web client will always give to
|
||
|
the server when calling a method. The context is a dictionary containing
|
||
|
multiple keys. One of the most important key is the language of the user, used
|
||
|
by the server to translate all the messages of the application. Another one is
|
||
|
the time zone of the user, used to compute correctly dates and times if Odoo
|
||
|
is used by people in different countries.
|
||
|
|
||
|
The ``argument`` is necessary in all methods, because if we forget it bad
|
||
|
things could happen (like the application not being translated
|
||
|
correctly). That's why, when you call a model's method, you should always give
|
||
|
it to that argument. The solution to achieve that is to use
|
||
|
:class:`openerp.web.CompoundContext`.
|
||
|
|
||
|
:class:`~openerp.web.CompoundContext` is a class used to pass the user's
|
||
|
context (with language, time zone, etc...) to the server as well as adding new
|
||
|
keys to the context (some models' methods use arbitrary keys added to the
|
||
|
context). It is created by giving to its constructor any number of
|
||
|
dictionaries or other :class:`~openerp.web.CompoundContext` instances. It will
|
||
|
merge all those contexts before sending them to the server.
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
model.call("my_method", [], {context: new instance.web.CompoundContext({'new_key': 'key_value'})})
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
def display_context(self, cr, uid, context=None):
|
||
|
print context
|
||
|
// will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1}
|
||
|
|
||
|
You can see the dictionary in the argument ``context`` contains some keys that
|
||
|
are related to the configuration of the current user in Odoo plus the
|
||
|
``new_key`` key that was added when instantiating
|
||
|
:class:`~openerp.web.CompoundContext`.
|
||
|
|
||
|
To resume, you should always add an instance of
|
||
|
:class:`~openerp.web.CompoundContext` in all calls to a model's method.
|
||
|
|
||
|
Queries
|
||
|
%%%%%%%
|
||
|
|
||
|
If you know Odoo module development, you should already know everything
|
||
|
necessary to communicate with models and make them do what you want. But there
|
||
|
is still a small helper that could be useful to you :
|
||
|
:func:`~openerp.web.Model.query`.
|
||
|
|
||
|
:func:`~openerp.web.Model.query` is a shortcut for the usual combination of
|
||
|
:py:meth:`~openerp.models.Model.search` and
|
||
|
::py:meth:`~openerp.models.Model.read` methods in Odoo models. It allows to
|
||
|
:search records and get their data with a shorter syntax. Example::
|
||
|
|
||
|
model.query(['name', 'login', 'user_email', 'signature'])
|
||
|
.filter([['active', '=', true], ['company_id', '=', main_company]])
|
||
|
.limit(15)
|
||
|
.all().then(function (users) {
|
||
|
// do work with users records
|
||
|
});
|
||
|
|
||
|
:func:`~openerp.web.Model.query` takes as argument a list of fields to query
|
||
|
in the model. It returns an instance of the :class:`openerp.web.Query` class.
|
||
|
|
||
|
:class:`~openerp.web.Query` is a class representing the query you are trying
|
||
|
to construct before sending it to the server. It has multiple methods you can
|
||
|
call to customize the query. All these methods will return the current
|
||
|
instance of :class:`~openerp.web.Query`:
|
||
|
|
||
|
* :func:`~openerp.web.Query.filter` allows to specify an Odoo *domain*. As a
|
||
|
reminder, a domain in Odoo is a list of conditions, each condition is a list
|
||
|
it self.
|
||
|
* :func:`~openerp.web.Query.limit` sets a limit to the number of records
|
||
|
returned.
|
||
|
|
||
|
When you have customized you query, you can call the
|
||
|
:func:`~openerp.web.Query.all` method. It will performs the real query to the
|
||
|
server and return a deferred resolved with the result. The result is the same
|
||
|
thing return by the model's method :py:meth:`~openerp.models.Model.read` (a
|
||
|
list of dictionaries containing the asked fields).
|
||
|
|
||
|
Exercises
|
||
|
---------
|
||
|
|
||
|
.. exercise:: Message of the Day
|
||
|
|
||
|
Create a widget ``MessageOfTheDay`` that will display the message
|
||
|
contained in the last record of the ``message_of_the_day``. The widget
|
||
|
should query the message as soon as it is inserted in the DOM and display
|
||
|
the message to the user. Display that widget on the home page of the Odoo
|
||
|
Pet Store module.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
openerp.oepetstore = function(instance) {
|
||
|
var _t = instance.web._t,
|
||
|
_lt = instance.web._lt;
|
||
|
var QWeb = instance.web.qweb;
|
||
|
|
||
|
instance.oepetstore = {};
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
template: "HomePage",
|
||
|
start: function() {
|
||
|
var motd = new instance.oepetstore.MessageOfTheDay(this);
|
||
|
motd.appendTo(this.$el);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
|
||
|
|
||
|
instance.oepetstore.MessageOfTheDay = instance.web.Widget.extend({
|
||
|
template: "MessageofTheDay",
|
||
|
init: function() {
|
||
|
this._super.apply(this, arguments);
|
||
|
},
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
new instance.web.Model("message_of_the_day").query(["message"]).first().then(function(result) {
|
||
|
self.$(".oe_mywidget_message_of_the_day").text(result.message);
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
}
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
||
|
<templates xml:space="preserve">
|
||
|
<t t-name="HomePage">
|
||
|
<div class="oe_petstore_homepage">
|
||
|
</div>
|
||
|
</t>
|
||
|
<t t-name="MessageofTheDay">
|
||
|
<div class="oe_petstore_motd">
|
||
|
<p class="oe_mywidget_message_of_the_day"></p>
|
||
|
</div>
|
||
|
</t>
|
||
|
</templates>
|
||
|
|
||
|
.. code-block:: css
|
||
|
|
||
|
.oe_petstore_motd {
|
||
|
margin: 5px;
|
||
|
padding: 5px;
|
||
|
border-radius: 3px;
|
||
|
background-color: #F0EEEE;
|
||
|
}
|
||
|
|
||
|
.. exercise:: Pet Toys List
|
||
|
|
||
|
Create a widget ``PetToysList`` that will display 5 toys on the home page
|
||
|
with their names and their images.
|
||
|
|
||
|
In this Odoo addon, the pet toys are not stored in a new table like for
|
||
|
the message of the day. They are in the table ``product.product``. If you
|
||
|
click on the menu item :menuselection:`Pet Store --> Pet Store --> Pet
|
||
|
Toys` you will be able to see them. Pet toys are identified by the
|
||
|
category named ``Pet Toys``. You could need to document yourself on the
|
||
|
model ``product.product`` to be able to create a domain to select pet toys
|
||
|
and not all the products.
|
||
|
|
||
|
To display the images of the pet toys, you should know that images in Odoo
|
||
|
can be queried from the database like any other fields, but you will
|
||
|
obtain a string containing Base64-encoded binary. There is a little trick
|
||
|
to display images in Base64 format in HTML:
|
||
|
|
||
|
.. code-block:: html
|
||
|
|
||
|
<img class="oe_kanban_image" src="data:image/png;base64,${replace this by base64}"></image>
|
||
|
|
||
|
The ``PetToysList`` widget should be displayed on the home page on the
|
||
|
right of the ``MessageOfTheDay`` widget. You will need to make some layout
|
||
|
with CSS to achieve this.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
openerp.oepetstore = function(instance) {
|
||
|
var _t = instance.web._t,
|
||
|
_lt = instance.web._lt;
|
||
|
var QWeb = instance.web.qweb;
|
||
|
|
||
|
instance.oepetstore = {};
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
template: "HomePage",
|
||
|
start: function() {
|
||
|
var pettoys = new instance.oepetstore.PetToysList(this);
|
||
|
pettoys.appendTo(this.$(".oe_petstore_homepage_left"));
|
||
|
var motd = new instance.oepetstore.MessageOfTheDay(this);
|
||
|
motd.appendTo(this.$(".oe_petstore_homepage_right"));
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
|
||
|
|
||
|
instance.oepetstore.MessageOfTheDay = instance.web.Widget.extend({
|
||
|
template: "MessageofTheDay",
|
||
|
init: function() {
|
||
|
this._super.apply(this, arguments);
|
||
|
},
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
new instance.web.Model("message_of_the_day").query(["message"]).first().then(function(result) {
|
||
|
self.$(".oe_mywidget_message_of_the_day").text(result.message);
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.oepetstore.PetToysList = instance.web.Widget.extend({
|
||
|
template: "PetToysList",
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
new instance.web.Model("product.product").query(["name", "image"])
|
||
|
.filter([["categ_id.name", "=", "Pet Toys"]]).limit(5).all().then(function(result) {
|
||
|
_.each(result, function(item) {
|
||
|
var $item = $(QWeb.render("PetToy", {item: item}));
|
||
|
self.$el.append($item);
|
||
|
});
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
}
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
||
|
<templates xml:space="preserve">
|
||
|
<t t-name="HomePage">
|
||
|
<div class="oe_petstore_homepage">
|
||
|
<div class="oe_petstore_homepage_left"></div>
|
||
|
<div class="oe_petstore_homepage_right"></div>
|
||
|
</div>
|
||
|
</t>
|
||
|
<t t-name="MessageofTheDay">
|
||
|
<div class="oe_petstore_motd">
|
||
|
<p class="oe_mywidget_message_of_the_day"></p>
|
||
|
</div>
|
||
|
</t>
|
||
|
<t t-name="PetToysList">
|
||
|
<div class="oe_petstore_pettoyslist">
|
||
|
</div>
|
||
|
</t>
|
||
|
<t t-name="PetToy">
|
||
|
<div class="oe_petstore_pettoy">
|
||
|
<p><t t-esc="item.name"/></p>
|
||
|
<p><img t-att-src="'data:image/jpg;base64,'+item.image"/></p>
|
||
|
</div>
|
||
|
</t>
|
||
|
</templates>
|
||
|
|
||
|
.. code-block:: css
|
||
|
|
||
|
.oe_petstore_homepage {
|
||
|
display: table;
|
||
|
}
|
||
|
|
||
|
.oe_petstore_homepage_left {
|
||
|
display: table-cell;
|
||
|
width : 300px;
|
||
|
}
|
||
|
|
||
|
.oe_petstore_homepage_right {
|
||
|
display: table-cell;
|
||
|
width : 300px;
|
||
|
}
|
||
|
|
||
|
.oe_petstore_motd {
|
||
|
margin: 5px;
|
||
|
padding: 5px;
|
||
|
border-radius: 3px;
|
||
|
background-color: #F0EEEE;
|
||
|
}
|
||
|
|
||
|
.oe_petstore_pettoyslist {
|
||
|
padding: 5px;
|
||
|
}
|
||
|
|
||
|
.oe_petstore_pettoy {
|
||
|
margin: 5px;
|
||
|
padding: 5px;
|
||
|
border-radius: 3px;
|
||
|
background-color: #F0EEEE;
|
||
|
}
|
||
|
|
||
|
|
||
|
Existing web components
|
||
|
-----------------------
|
||
|
|
||
|
In the previous part, we explained the Odoo web framework, a development
|
||
|
framework to create and architecture graphical JavaScript applications. The
|
||
|
current part is dedicated to the existing components of the Odoo web client
|
||
|
and most notably those containing entry points for developers to create new
|
||
|
widgets that will be inserted inside existing views or components.
|
||
|
|
||
|
The Action Manager
|
||
|
%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
To display a view or show a popup, as example when you click on a menu button,
|
||
|
Odoo use the concept of actions. Actions are pieces of information explaining
|
||
|
what the web client should do. They can be loaded from the database or created
|
||
|
on-the-fly. The component handling actions in the web client is the *Action
|
||
|
Manager*.
|
||
|
|
||
|
Using the Action Manager
|
||
|
''''''''''''''''''''''''
|
||
|
|
||
|
A way to launch an action is to use a menu element targeting an action
|
||
|
registered in the database. As a reminder, here is how is defined a typical
|
||
|
action and its associated menu item:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<record model="ir.actions.act_window" id="message_of_the_day_action">
|
||
|
<field name="name">Message of the day</field>
|
||
|
<field name="res_model">message_of_the_day</field>
|
||
|
<field name="view_type">form</field>
|
||
|
<field name="view_mode">tree,form</field>
|
||
|
</record>
|
||
|
|
||
|
<menuitem id="message_day" name="Message of the day" parent="petstore_menu"
|
||
|
action="message_of_the_day_action"/>
|
||
|
|
||
|
It is also possible to ask the Odoo client to load an action from a JavaScript
|
||
|
code. To do so you have to create a dictionary explaining the action and then
|
||
|
to ask the action manager to re-dispatch the web client to the new action. To
|
||
|
send a message to the action manager, :class:`~openerp.web.Widget` has a
|
||
|
shortcut that will automatically find the current action manager and execute
|
||
|
the action. Here is an example call to that method::
|
||
|
|
||
|
instance.web.TestWidget = instance.web.Widget.extend({
|
||
|
dispatch_to_new_action: function() {
|
||
|
this.do_action({
|
||
|
type: 'ir.actions.act_window',
|
||
|
res_model: "product.product",
|
||
|
res_id: 1,
|
||
|
views: [[false, 'form']],
|
||
|
target: 'current',
|
||
|
context: {},
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
The method to call to ask the action manager to execute a new action is
|
||
|
:func:`~openerp.web.Widget.do_action`. It receives as argument a dictionary
|
||
|
defining the properties of the action. Here is a description of the most usual
|
||
|
properties (not all of them may be used by all type of actions):
|
||
|
|
||
|
* ``type``: The type of the action, which means the name of the model in which
|
||
|
the action is stored. As example, use ``ir.actions.act_window`` to show
|
||
|
views and ``ir.actions.client`` for client actions.
|
||
|
* ``res_model``: For ``act_window`` actions, it is the model used by the
|
||
|
views.
|
||
|
* ``res_id``: The ``id`` of the record to display.
|
||
|
* ``views``: For ``act_window`` actions, it is a list of the views to
|
||
|
display. This argument must be a list of tuples with two components. The
|
||
|
first one must be the identifier of the view (or ``false`` if you just want
|
||
|
to use the default view defined for the model). The second one must be the
|
||
|
type of the view.
|
||
|
* ``target``: If the value is ``current``, the action will be opened in the
|
||
|
main content part of the web client. The current action will be destroyed
|
||
|
before loading the new one. If it is ``new``, the action will appear in a
|
||
|
popup and the current action will not be destroyed.
|
||
|
* ``context``: The context to use.
|
||
|
|
||
|
.. exercise:: Jump to Product
|
||
|
|
||
|
Modify the ``PetToysList`` component developed in the previous part to
|
||
|
jump to a form view displaying the shown item when we click on the item in
|
||
|
the list.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
instance.oepetstore.PetToysList = instance.web.Widget.extend({
|
||
|
template: "PetToysList",
|
||
|
start: function() {
|
||
|
var self = this;
|
||
|
new instance.web.Model("product.product").query(["name", "image"])
|
||
|
.filter([["categ_id.name", "=", "Pet Toys"]]).limit(5).all().then(function(result) {
|
||
|
_.each(result, function(item) {
|
||
|
var $item = $(QWeb.render("PetToy", {item: item}));
|
||
|
self.$el.append($item);
|
||
|
$item.click(function() {
|
||
|
self.item_clicked(item);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
},
|
||
|
item_clicked: function(item) {
|
||
|
this.do_action({
|
||
|
type: 'ir.actions.act_window',
|
||
|
res_model: "product.product",
|
||
|
res_id: item.id,
|
||
|
views: [[false, 'form']],
|
||
|
target: 'current',
|
||
|
context: {},
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
Client Actions
|
||
|
%%%%%%%%%%%%%%
|
||
|
|
||
|
In the module installed during the previous part of this guide, we defined a
|
||
|
simple widget that was displayed when we clicked on a menu element. This is
|
||
|
because this widget was registered as a *client action*. Client actions are a
|
||
|
type of action that are completely defined by JavaScript code. Here is a
|
||
|
reminder of the way we defined this client action::
|
||
|
|
||
|
instance.oepetstore.HomePage = instance.web.Widget.extend({
|
||
|
start: function() {
|
||
|
console.log("pet store home page loaded");
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
|
||
|
|
||
|
``instance.web.client_actions`` is an instance of the
|
||
|
:class:`~openerp.web.Registry` class. Registries are not very different to
|
||
|
simple dictionaries, except they assign strings to class names. Adding the
|
||
|
``petstore.homepage`` key to this registry simply tells the web client "If
|
||
|
someone asks you to open a client action with key ``petstore.homepage``,
|
||
|
instantiate the ``instance.oepetstore.HomePage`` class and show it to the
|
||
|
user".
|
||
|
|
||
|
Here is how the menu element to show this client action was defined:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<record id="action_home_page" model="ir.actions.client">
|
||
|
<field name="tag">petstore.homepage</field>
|
||
|
</record>
|
||
|
|
||
|
<menuitem id="home_page_petstore_menu" name="Home Page" parent="petstore_menu"
|
||
|
action="action_home_page"/>
|
||
|
|
||
|
Client actions do not need a lot of information except their type, which is
|
||
|
stored in the ``tag`` field.
|
||
|
|
||
|
When the web client wants to display a client action, it will simply show it
|
||
|
in the main content block of the web client. This is completely sufficient to
|
||
|
allow the widget to display anything and so create a completely new feature
|
||
|
for the web client.
|
||
|
|
||
|
Architecture of the Views
|
||
|
%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
Most of the complexity of the web client resides in views. They are the basic
|
||
|
tools to display the data in the database. The part will explain the views
|
||
|
and how those are displayed in the web client.
|
||
|
|
||
|
The View Manager
|
||
|
''''''''''''''''
|
||
|
|
||
|
Previously we already explained the purpose of the *Action Manager*. It is a
|
||
|
component, whose class is ``ActionManager``, that will handle the Odoo actions
|
||
|
(notably the actions associated with menu buttons).
|
||
|
|
||
|
When an ``ActionManager`` instance receive an action with type
|
||
|
``ir.actions.act_window``, it knows it has to show one or more views
|
||
|
associated with a precise model. To do so, it creates a *View Manager* that
|
||
|
will create one or multiple *Views*. See this diagram:
|
||
|
|
||
|
.. image:: web/viewarchitecture.*
|
||
|
:align: center
|
||
|
:width: 40%
|
||
|
|
||
|
The ``ViewManager`` instance will instantiate each view class corresponding to
|
||
|
the views indicated in the ``ir.actions.act_window`` action. As example, the
|
||
|
class corresponding to the view type ``form`` is ``FormView``. Each view class
|
||
|
inherits the ``View`` abstract class.
|
||
|
|
||
|
The Views
|
||
|
'''''''''
|
||
|
|
||
|
All the typical type of views in Odoo (all those you can switch to using the
|
||
|
small buttons under the search input text) are represented by a class
|
||
|
extending the ``View`` abstract class. Note the *Search View* (the search
|
||
|
input text on the top right of the screen that typically appear in kanban and
|
||
|
list views) is also considered a type of view even if it doesn't work like the
|
||
|
others (you can not "switch to" the search view and it doesn't take the full
|
||
|
screen).
|
||
|
|
||
|
A view has the responsibility to load its XML view description from the server
|
||
|
and display it. Views are also given an instance of the ``DataSet``
|
||
|
class. That class contains a list of identifiers corresponding to records that
|
||
|
the view should display. It is filled by the search view and the current view
|
||
|
is supposed to display the result of each search after it was performed by the
|
||
|
search view.
|
||
|
|
||
|
The Form View Fields
|
||
|
%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
A typical need in the web client is to extend the form view to display more
|
||
|
specific widgets. One of the possibilities to do this is to define a new type
|
||
|
of *Field*.
|
||
|
|
||
|
A field, in the form view, is a type of widget designed to display and edit
|
||
|
the content of *one (and only one) field* in a single record displayed by the
|
||
|
form view. All data types available in models have a default implementation to
|
||
|
display and edit them in the form view. As example, the ``FieldChar`` class
|
||
|
allows to edit the ``char`` data type.
|
||
|
|
||
|
Other field classes simply provide an alternative widget to represent an
|
||
|
existing data type. A good example of this is the ``FieldEmail`` class. There
|
||
|
is no ``email`` type in the models of Odoo. That class is designed to display
|
||
|
a ``char`` field assuming it contains an email (it will show a clickable link
|
||
|
to directly send a mail to the person and will also check the validity of the
|
||
|
mail address).
|
||
|
|
||
|
Also note there is nothing that disallow a field class to work with more than
|
||
|
one data type. As example, the ``FieldSelection`` class works with both
|
||
|
``selection`` and ``many2one`` field types.
|
||
|
|
||
|
As a reminder, to indicate a precise field type in a form view XML
|
||
|
description, you just have to specify the ``widget`` attribute:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<field name="contact_mail" widget="email"/>
|
||
|
|
||
|
It is also a good thing to notice that the form view field classes are also
|
||
|
used in the editable list views. So, by defining a new field class, it make
|
||
|
this new widget available in both views.
|
||
|
|
||
|
Another type of extension mechanism for the form view is the *Form Widget*,
|
||
|
which has fewer restrictions than the fields (even though it can be more
|
||
|
complicated to implement). Form widgets will be explained later in this guide.
|
||
|
|
||
|
Fields are instantiated by the form view after it has read its XML description
|
||
|
and constructed the corresponding HTML representing that description. After
|
||
|
that, the form view will communicate with the field objects using some
|
||
|
methods. Theses methods are defined by the ``FieldInterface``
|
||
|
interface. Almost all fields inherit the ``AbstractField`` abstract
|
||
|
class. That class defines some default mechanisms that need to be implemented
|
||
|
by most fields.
|
||
|
|
||
|
Here are some of the responsibilities of a field class:
|
||
|
|
||
|
* The field class must display and allow the user to edit the value of the field.
|
||
|
* It must correctly implement the 3 field attributes available in all fields
|
||
|
of Odoo. The ``AbstractField`` class already implements an algorithm that
|
||
|
dynamically calculates the value of these attributes (they can change at any
|
||
|
moment because their value change according to the value of other
|
||
|
fields). Their values are stored in *Widget Properties* (the widget
|
||
|
properties were explained earlier in this guide). It is the responsibility
|
||
|
of each field class to check these widget properties and dynamically adapt
|
||
|
depending of their values. Here is a description of each of these
|
||
|
attributes:
|
||
|
|
||
|
* ``required``: The field must have a value before saving. If ``required``
|
||
|
is ``true`` and the field doesn't have a value, the method
|
||
|
``is_valid()`` of the field must return ``false``.
|
||
|
* ``invisible``: When this is ``true``, the field must be invisible. The
|
||
|
``AbstractField`` class already has a basic implementation of this
|
||
|
behavior that fits most fields.
|
||
|
* ``readonly``: When ``true``, the field must not be editable by the
|
||
|
user. Most fields in Odoo have a completely different behavior depending
|
||
|
on the value of ``readonly``. As example, the ``FieldChar`` displays an
|
||
|
HTML ``<input>`` when it is editable and simply displays the text when
|
||
|
it is read-only. This also means it has much more code it would need to
|
||
|
implement only one behavior, but this is necessary to ensure a good user
|
||
|
experience.
|
||
|
|
||
|
* Fields have two methods, ``set_value()`` and ``get_value()``, which are
|
||
|
called by the form view to give it the value to display and get back the new
|
||
|
value entered by the user. These methods must be able to handle the value as
|
||
|
given by the Odoo server when a ``read()`` is performed on a model and give
|
||
|
back a valid value for a ``write()``. Remember that the JavaScript/Python
|
||
|
data types used to represent the values given by ``read()`` and given to
|
||
|
``write()`` is not necessarily the same in Odoo. As example, when you read a
|
||
|
many2one, it is always a tuple whose first value is the id of the pointed
|
||
|
record and the second one is the name get (ie: ``(15, "Agrolait")``). But
|
||
|
when you write a many2one it must be a single integer, not a tuple
|
||
|
anymore. ``AbstractField`` has a default implementation of these methods
|
||
|
that works well for simple data type and set a widget property named
|
||
|
``value``.
|
||
|
|
||
|
Please note that, to better understand how to implement fields, you are
|
||
|
strongly encouraged to look at the definition of the ``FieldInterface``
|
||
|
interface and the ``AbstractField`` class directly in the code of the Odoo web
|
||
|
client.
|
||
|
|
||
|
Creating a New Type of Field
|
||
|
''''''''''''''''''''''''''''
|
||
|
|
||
|
In this part we will explain how to create a new type of field. The example
|
||
|
here will be to re-implement the ``FieldChar`` class and explain progressively
|
||
|
each part.
|
||
|
|
||
|
Simple Read-Only Field
|
||
|
""""""""""""""""""""""
|
||
|
|
||
|
Here is a first implementation that will only be able to display a text. The
|
||
|
user will not be able to modify the content of the field.
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
instance.oepetstore.FieldChar2 = instance.web.form.AbstractField.extend({
|
||
|
init: function() {
|
||
|
this._super.apply(this, arguments);
|
||
|
this.set("value", "");
|
||
|
},
|
||
|
render_value: function() {
|
||
|
this.$el.text(this.get("value"));
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');
|
||
|
|
||
|
In this example, we declare a class named ``FieldChar2`` inheriting from
|
||
|
``AbstractField``. We also register this class in the registry
|
||
|
``instance.web.form.widgets`` under the key ``char2``. That will allow us to
|
||
|
use this new field in any form view by specifying ``widget="char2"`` in the
|
||
|
``<field/>`` tag in the XML declaration of the view.
|
||
|
|
||
|
In this example, we define a single method: ``render_value()``. All it does is
|
||
|
display the widget property ``value``. Those are two tools defined by the
|
||
|
``AbstractField`` class. As explained before, the form view will call the
|
||
|
method ``set_value()`` of the field to set the value to display. This method
|
||
|
already has a default implementation in ``AbstractField`` which simply sets
|
||
|
the widget property ``value``. ``AbstractField`` also watch the
|
||
|
``change:value`` event on itself and calls the ``render_value()`` when it
|
||
|
occurs. So, ``render_value()`` is a convenience method to implement in child
|
||
|
classes to perform some operation each time the value of the field changes.
|
||
|
|
||
|
In the ``init()`` method, we also define the default value of the field if
|
||
|
none is specified by the form view (here we assume the default value of a
|
||
|
``char`` field should be an empty string).
|
||
|
|
||
|
Read-Write Field
|
||
|
""""""""""""""""
|
||
|
|
||
|
Fields that only display their content and don't give the possibility to the
|
||
|
user to modify it can be useful, but most fields in Odoo allow edition
|
||
|
too. This makes the field classes more complicated, mostly because fields are
|
||
|
supposed to handle both and editable and non-editable mode, those modes are
|
||
|
often completely different (for design and usability purpose) and the fields
|
||
|
must be able to switch from one mode to another at any moment.
|
||
|
|
||
|
To know in which mode the current field should be, the ``AbstractField`` class
|
||
|
sets a widget property named ``effective_readonly``. The field should watch
|
||
|
the changes in that widget property and display the correct mode
|
||
|
accordingly. Example::
|
||
|
|
||
|
instance.oepetstore.FieldChar2 = instance.web.form.AbstractField.extend({
|
||
|
init: function() {
|
||
|
this._super.apply(this, arguments);
|
||
|
this.set("value", "");
|
||
|
},
|
||
|
start: function() {
|
||
|
this.on("change:effective_readonly", this, function() {
|
||
|
this.display_field();
|
||
|
this.render_value();
|
||
|
});
|
||
|
this.display_field();
|
||
|
return this._super();
|
||
|
},
|
||
|
display_field: function() {
|
||
|
var self = this;
|
||
|
this.$el.html(QWeb.render("FieldChar2", {widget: this}));
|
||
|
if (! this.get("effective_readonly")) {
|
||
|
this.$("input").change(function() {
|
||
|
self.internal_set_value(self.$("input").val());
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
render_value: function() {
|
||
|
if (this.get("effective_readonly")) {
|
||
|
this.$el.text(this.get("value"));
|
||
|
} else {
|
||
|
this.$("input").val(this.get("value"));
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="FieldChar2">
|
||
|
<div class="oe_field_char2">
|
||
|
<t t-if="! widget.get('effective_readonly')">
|
||
|
<input type="text"></input>
|
||
|
</t>
|
||
|
</div>
|
||
|
</t>
|
||
|
|
||
|
In the ``start()`` method (which is called right after a widget has been
|
||
|
appended to the DOM), we bind on the event ``change:effective_readonly``. That
|
||
|
will allow use to redisplay the field each time the widget property
|
||
|
``effective_readonly`` changes. This event handler will call
|
||
|
``display_field()``, which is also called directly in ``start()``. This
|
||
|
``display_field()`` was created specifically for this field, it's not a method
|
||
|
defined in ``AbstractField`` or any other class. This is the method we will
|
||
|
use to display the content of the field depending we are in read-only mode or
|
||
|
not.
|
||
|
|
||
|
From now on the conception of this field is quite typical, except there is a
|
||
|
lot of verifications to know the state of the ``effective_readonly`` property:
|
||
|
|
||
|
* In the QWeb template used to display the content of the widget, it displays
|
||
|
an ``<input type="text" />`` if we are in read-write mode and nothing in
|
||
|
particular in read-only mode.
|
||
|
* In the ``display_field()`` method, we have to bind on the ``change`` event
|
||
|
of the ``<input type="text" />`` to know when the user has changed the
|
||
|
value. When it happens, we call the ``internal_set_value()`` method with the
|
||
|
new value of the field. This is a convenience method provided by the
|
||
|
``AbstractField`` class. That method will set a new value in the ``value``
|
||
|
property but will not trigger a call to ``render_value()`` (which is not
|
||
|
necessary since the ``<input type="text" />`` already contains the correct
|
||
|
value).
|
||
|
* In ``render_value()``, we use a completely different code to display the
|
||
|
value of the field depending if we are in read-only or in read-write mode.
|
||
|
|
||
|
.. exercise:: Create a Color Field
|
||
|
|
||
|
Create a ``FieldColor`` class. The value of this field should be a string
|
||
|
containing a color code like those used in CSS (example: ``#FF0000`` for
|
||
|
red). In read-only mode, this color field should display a little block
|
||
|
whose color corresponds to the value of the field. In read-write mode, you
|
||
|
should display an ``<input type="color" />``. That type of ``<input />``
|
||
|
is an HTML5 component that doesn't work in all browsers but works well in
|
||
|
Google Chrome. So it's OK to use as an exercise.
|
||
|
|
||
|
You can use that widget in the form view of the ``message_of_the_day``
|
||
|
model for its field named ``color``. As a bonus, you can change the
|
||
|
``MessageOfTheDay`` widget created in the previous part of this guide to
|
||
|
display the message of the day with the background color indicated in the
|
||
|
``color`` field.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
instance.oepetstore.FieldColor = instance.web.form.AbstractField.extend({
|
||
|
init: function() {
|
||
|
this._super.apply(this, arguments);
|
||
|
this.set("value", "");
|
||
|
},
|
||
|
start: function() {
|
||
|
this.on("change:effective_readonly", this, function() {
|
||
|
this.display_field();
|
||
|
this.render_value();
|
||
|
});
|
||
|
this.display_field();
|
||
|
return this._super();
|
||
|
},
|
||
|
display_field: function() {
|
||
|
var self = this;
|
||
|
this.$el.html(QWeb.render("FieldColor", {widget: this}));
|
||
|
if (! this.get("effective_readonly")) {
|
||
|
this.$("input").change(function() {
|
||
|
self.internal_set_value(self.$("input").val());
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
render_value: function() {
|
||
|
if (this.get("effective_readonly")) {
|
||
|
this.$(".oe_field_color_content").css("background-color", this.get("value") || "#FFFFFF");
|
||
|
} else {
|
||
|
this.$("input").val(this.get("value") || "#FFFFFF");
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.form.widgets.add('color', 'instance.oepetstore.FieldColor');
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="FieldColor">
|
||
|
<div class="oe_field_color">
|
||
|
<t t-if="widget.get('effective_readonly')">
|
||
|
<div class="oe_field_color_content" />
|
||
|
</t>
|
||
|
<t t-if="! widget.get('effective_readonly')">
|
||
|
<input type="color"></input>
|
||
|
</t>
|
||
|
</div>
|
||
|
</t>
|
||
|
|
||
|
.. code-block:: css
|
||
|
|
||
|
.oe_field_color_content {
|
||
|
height: 20px;
|
||
|
width: 50px;
|
||
|
border: 1px solid black;
|
||
|
}
|
||
|
|
||
|
The Form View Custom Widgets
|
||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||
|
|
||
|
Form fields can be useful, but their purpose is to edit a single field. To
|
||
|
interact with the whole form view and have more liberty to integrate new
|
||
|
widgets in it, it is recommended to create a custom form widget.
|
||
|
|
||
|
Custom form widgets are widgets that can be added in any form view using a
|
||
|
specific syntax in the XML definition of the view. Example:
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<widget type="xxx" />
|
||
|
|
||
|
This type of widget will simply be created by the form view during the
|
||
|
creation of the HTML according to the XML definition. They have properties in
|
||
|
common with the fields (like the ``effective_readonly`` property) but they are
|
||
|
not assigned a precise field. And so they don't have methods like
|
||
|
``get_value()`` and ``set_value()``. They must inherit from the ``FormWidget``
|
||
|
abstract class.
|
||
|
|
||
|
The custom form widgets can also interact with the fields of the form view by
|
||
|
getting or setting their values using the ``field_manager`` attribute of
|
||
|
``FormWidget``. Here is an example usage::
|
||
|
|
||
|
instance.oepetstore.WidgetMultiplication = instance.web.form.FormWidget.extend({
|
||
|
start: function() {
|
||
|
this._super();
|
||
|
this.field_manager.on("field_changed:integer_a", this, this.display_result);
|
||
|
this.field_manager.on("field_changed:integer_b", this, this.display_result);
|
||
|
this.display_result();
|
||
|
},
|
||
|
display_result: function() {
|
||
|
var result = this.field_manager.get_field_value("integer_a") *
|
||
|
this.field_manager.get_field_value("integer_b");
|
||
|
this.$el.text("a*b = " + result);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication');
|
||
|
|
||
|
This example custom widget is designed to take the values of two existing
|
||
|
fields (those must exist in the form view) and print the result of their
|
||
|
multiplication. It also refreshes each time the value of any of those fields
|
||
|
changes.
|
||
|
|
||
|
The ``field_manager`` attribute is in fact the ``FormView`` instance
|
||
|
representing the form view. The methods that widgets can call on that form
|
||
|
view are documented in the code of the web client in the ``FieldManagerMixin``
|
||
|
interface. The most useful features are:
|
||
|
|
||
|
* The method ``get_field_value()`` which returns the value of a field.
|
||
|
* When the value of a field is changed, for any reason, the form view will
|
||
|
trigger an event named ``field_changed:xxx`` where ``xxx`` is the name of
|
||
|
the field.
|
||
|
* Also, it is possible to change the value of the fields using the method
|
||
|
``set_values()``. This method takes a dictionary as first and only argument
|
||
|
whose keys are the names of the fields to change and values are the new
|
||
|
values.
|
||
|
|
||
|
.. exercise:: Show Coordinates on Google Map
|
||
|
|
||
|
In this exercise we would like to add two new fields on the
|
||
|
``product.product`` model: ``provider_latitude`` and
|
||
|
``provider_longitude``. Those would represent coordinates on a map. We
|
||
|
also would like you to create a custom widget able to display a map
|
||
|
showing these coordinates.
|
||
|
|
||
|
To display that map, you can simply use the Google Map service using an HTML code similar to this:
|
||
|
|
||
|
.. code-block:: html
|
||
|
|
||
|
<iframe width="400" height="300" src="https://maps.google.com/?ie=UTF8&ll=XXX,YYY&output=embed">
|
||
|
</iframe>
|
||
|
|
||
|
Just replace ``XXX`` with the latitude and ``YYY`` with the longitude.
|
||
|
|
||
|
You should display those two new fields as well as the map widget in a new
|
||
|
page of the notebook displayed in the product form view.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({
|
||
|
start: function() {
|
||
|
this._super();
|
||
|
this.field_manager.on("field_changed:provider_latitude", this, this.display_map);
|
||
|
this.field_manager.on("field_changed:provider_longitude", this, this.display_map);
|
||
|
this.display_map();
|
||
|
},
|
||
|
display_map: function() {
|
||
|
this.$el.html(QWeb.render("WidgetCoordinates", {
|
||
|
"latitude": this.field_manager.get_field_value("provider_latitude") || 0,
|
||
|
"longitude": this.field_manager.get_field_value("provider_longitude") || 0,
|
||
|
}));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates');
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="WidgetCoordinates">
|
||
|
<iframe width="400" height="300"
|
||
|
t-att-src="'https://maps.google.com/?ie=UTF8&ll=' + latitude + ',' + longitude + '&output=embed'">
|
||
|
</iframe>
|
||
|
</t>
|
||
|
|
||
|
.. exercise:: Get the Current Coordinate
|
||
|
|
||
|
Now we would like to display an additional button to automatically set the
|
||
|
coordinates to the location of the current user.
|
||
|
|
||
|
To get the coordinates of the user, an easy way is to use the geolocation
|
||
|
JavaScript API. `See the online documentation to know how to use it`_.
|
||
|
|
||
|
.. _See the online documentation to know how to use it: http://www.w3schools.com/html/html5_geolocation.asp
|
||
|
|
||
|
Please also note that it wouldn't be very logical to allow the user to
|
||
|
click on that button when the form view is in read-only mode. So, this
|
||
|
custom widget should handle correctly the ``effective_readonly`` property
|
||
|
just like any field. One way to do this would be to make the button
|
||
|
disappear when ``effective_readonly`` is true.
|
||
|
|
||
|
.. only:: solutions
|
||
|
|
||
|
.. code-block:: javascript
|
||
|
|
||
|
instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({
|
||
|
start: function() {
|
||
|
this._super();
|
||
|
this.field_manager.on("field_changed:provider_latitude", this, this.display_map);
|
||
|
this.field_manager.on("field_changed:provider_longitude", this, this.display_map);
|
||
|
this.on("change:effective_readonly", this, this.display_map);
|
||
|
this.display_map();
|
||
|
},
|
||
|
display_map: function() {
|
||
|
var self = this;
|
||
|
this.$el.html(QWeb.render("WidgetCoordinates", {
|
||
|
"latitude": this.field_manager.get_field_value("provider_latitude") || 0,
|
||
|
"longitude": this.field_manager.get_field_value("provider_longitude") || 0,
|
||
|
}));
|
||
|
this.$("button").toggle(! this.get("effective_readonly"));
|
||
|
this.$("button").click(function() {
|
||
|
navigator.geolocation.getCurrentPosition(_.bind(self.received_position, self));
|
||
|
});
|
||
|
},
|
||
|
received_position: function(obj) {
|
||
|
var la = obj.coords.latitude;
|
||
|
var lo = obj.coords.longitude;
|
||
|
this.field_manager.set_values({
|
||
|
"provider_latitude": la,
|
||
|
"provider_longitude": lo,
|
||
|
});
|
||
|
},
|
||
|
});
|
||
|
|
||
|
instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates');
|
||
|
|
||
|
.. code-block:: xml
|
||
|
|
||
|
<t t-name="WidgetCoordinates">
|
||
|
<iframe width="400" height="300"
|
||
|
t-att-src="'https://maps.google.com/?ie=UTF8&ll=' + latitude + ',' + longitude + '&output=embed'">
|
||
|
</iframe>
|
||
|
<button>Get My Current Coordinate</button>
|
||
|
</t>
|
||
|
|
||
|
.. _jQuery: http://jquery.org
|
||
|
.. _Underscore.js: http://underscorejs.org
|