From 62fcce9054168d0da4ab857be7c928495ea1b6f3 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 12 Sep 2014 14:54:27 +0200 Subject: [PATCH 1/4] [ADD] JS widgets reference documentation Also fixed Widget#alive's behavior and tested it --- addons/web/doc/index.rst | 1 - addons/web/doc/widget.rst | 287 ------------- addons/web/static/src/js/openerpframework.js | 28 +- addons/web/static/test/framework.js | 62 ++- doc/_themes/odoodoc/static/style.css | 3 + doc/_themes/odoodoc/static/style.less | 5 + doc/reference/javascript.rst | 403 ++++++++++++++++++- 7 files changed, 484 insertions(+), 305 deletions(-) delete mode 100644 addons/web/doc/widget.rst diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index 511a271d30e..bad653e0da7 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -34,7 +34,6 @@ Javascript :maxdepth: 1 guidelines - widget rpc async client_action diff --git a/addons/web/doc/widget.rst b/addons/web/doc/widget.rst deleted file mode 100644 index 7c4a2374b3e..00000000000 --- a/addons/web/doc/widget.rst +++ /dev/null @@ -1,287 +0,0 @@ -Widget -====== - -.. js:class:: openerp.web.Widget - -This is the base class for all visual components. It corresponds to an MVC -view. It provides a number of services to handle a section of a page: - -* Rendering with QWeb - -* Parenting-child relations - -* Life-cycle management (including facilitating children destruction when a - parent object is removed) - -* DOM insertion, via jQuery-powered insertion methods. Insertion targets can - be anything the corresponding jQuery method accepts (generally selectors, - DOM nodes and jQuery objects): - - :js:func:`~openerp.web.Widget.appendTo` - Renders the widget and inserts it as the last child of the target, uses - `.appendTo()`_ - - :js:func:`~openerp.web.Widget.prependTo` - Renders the widget and inserts it as the first child of the target, uses - `.prependTo()`_ - - :js:func:`~openerp.web.Widget.insertAfter` - Renders the widget and inserts it as the preceding sibling of the target, - uses `.insertAfter()`_ - - :js:func:`~openerp.web.Widget.insertBefore` - Renders the widget and inserts it as the following sibling of the target, - uses `.insertBefore()`_ - -* Backbone-compatible shortcuts - -.. _widget-dom_root: - -DOM Root --------- - -A :js:class:`~openerp.web.Widget` is responsible for a section of the -page materialized by the DOM root of the widget. The DOM root is -available via the :js:attr:`~openerp.web.Widget.el` and -:js:attr:`~openerp.web.Widget.$el` attributes, which are -respectively the raw DOM Element and the jQuery wrapper around the DOM -element. - -There are two main ways to define and generate this DOM root: - -.. js:attribute:: openerp.web.Widget.template - - Should be set to the name of a QWeb template (a - :js:class:`String`). If set, the template will be rendered after - the widget has been initialized but before it has been - started. The root element generated by the template will be set as - the DOM root of the widget. - -.. js:attribute:: openerp.web.Widget.tagName - - Used if the widget has no template defined. Defaults to ``div``, - will be used as the tag name to create the DOM element to set as - the widget's DOM root. It is possible to further customize this - generated DOM root with the following attributes: - - .. js:attribute:: openerp.web.Widget.id - - Used to generate an ``id`` attribute on the generated DOM - root. - - .. js:attribute:: openerp.web.Widget.className - - Used to generate a ``class`` attribute on the generated DOM root. - - .. js:attribute:: openerp.web.Widget.attributes - - Mapping (object literal) of attribute names to attribute - values. Each of these k:v pairs will be set as a DOM attribute - on the generated DOM root. - - None of these is used in case a template is specified on the widget. - -The DOM root can also be defined programmatically by overridding - -.. js:function:: openerp.web.Widget.renderElement - - Renders the widget's DOM root and sets it. The default - implementation will render a set template or generate an element - as described above, and will call - :js:func:`~openerp.web.Widget.setElement` on the result. - - Any override to :js:func:`~openerp.web.Widget.renderElement` which - does not call its ``_super`` **must** call - :js:func:`~openerp.web.Widget.setElement` with whatever it - generated or the widget's behavior is undefined. - - .. note:: - - The default :js:func:`~openerp.web.Widget.renderElement` can - be called repeatedly, it will *replace* the previous DOM root - (using ``replaceWith``). However, this requires that the - widget correctly sets and unsets its events (and children - widgets). Generally, - :js:func:`~openerp.web.Widget.renderElement` should not be - called repeatedly unless the widget advertizes this feature. - -Accessing DOM content -~~~~~~~~~~~~~~~~~~~~~ - -Because a widget is only responsible for the content below its DOM -root, there is a shortcut for selecting sub-sections of a widget's -DOM: - -.. js:function:: openerp.web.Widget.$(selector) - - Applies the CSS selector specified as parameter to the widget's - DOM root. - - .. code-block:: javascript - - this.$(selector); - - is functionally identical to: - - .. code-block:: javascript - - this.$el.find(selector); - - :param String selector: CSS selector - :returns: jQuery object - - .. note:: this helper method is compatible with - ``Backbone.View.$`` - -Resetting the DOM root -~~~~~~~~~~~~~~~~~~~~~~ - -.. js:function:: openerp.web.Widget.setElement(element) - - Re-sets the widget's DOM root to the provided element, also - handles re-setting the various aliases of the DOM root as well as - unsetting and re-setting delegated events. - - :param Element element: a DOM element or jQuery object to set as - the widget's DOM root - - .. note:: should be mostly compatible with `Backbone's - setElement`_ - -DOM events handling -------------------- - -A widget will generally need to respond to user action within its -section of the page. This entails binding events to DOM elements. - -To this end, :js:class:`~openerp.web.Widget` provides an shortcut: - -.. js:attribute:: openerp.web.Widget.events - - Events are a mapping of ``event selector`` (an event name and a - CSS selector separated by a space) to a callback. The callback can - be either a method name in the widget or a function. In either - case, the ``this`` will be set to the widget: - - .. code-block:: javascript - - events: { - 'click p.oe_some_class a': 'some_method', - 'change input': function (e) { - e.stopPropagation(); - } - }, - - The selector is used for jQuery's `event delegation`_, the - callback will only be triggered for descendants of the DOM root - matching the selector [0]_. If the selector is left out (only an - event name is specified), the event will be set directly on the - widget's DOM root. - -.. js:function:: openerp.web.Widget.delegateEvents - - This method is in charge of binding - :js:attr:`~openerp.web.Widget.events` to the DOM. It is - automatically called after setting the widget's DOM root. - - It can be overridden to set up more complex events than the - :js:attr:`~openerp.web.Widget.events` map allows, but the parent - should always be called (or :js:attr:`~openerp.web.Widget.events` - won't be handled correctly). - -.. js:function:: openerp.web.Widget.undelegateEvents - - This method is in charge of unbinding - :js:attr:`~openerp.web.Widget.events` from the DOM root when the - widget is destroyed or the DOM root is reset, in order to avoid - leaving "phantom" events. - - It should be overridden to un-set any event set in an override of - :js:func:`~openerp.web.Widget.delegateEvents`. - -.. note:: this behavior should be compatible with `Backbone's - delegateEvents`_, apart from not accepting any argument. - -Subclassing Widget ------------------- - -:js:class:`~openerp.base.Widget` is subclassed in the standard manner (via the -:js:func:`~openerp.base.Class.extend` method), and provides a number of -abstract properties and concrete methods (which you may or may not want to -override). Creating a subclass looks like this: - -.. code-block:: javascript - - var MyWidget = openerp.base.Widget.extend({ - // QWeb template to use when rendering the object - template: "MyQWebTemplate", - - init: function(parent) { - this._super(parent); - // insert code to execute before rendering, for object - // initialization - }, - start: function() { - this._super(); - // post-rendering initialization code, at this point - // ``this.$element`` has been initialized - this.$element.find(".my_button").click(/* an example of event binding * /); - - // if ``start`` is asynchronous, return a promise object so callers - // know when the object is done initializing - return this.rpc(/* … */) - } - }); - -The new class can then be used in the following manner: - -.. code-block:: javascript - - // Create the instance - var my_widget = new MyWidget(this); - // Render and insert into DOM - my_widget.appendTo(".some-div"); - -After these two lines have executed (and any promise returned by ``appendTo`` -has been resolved if needed), the widget is ready to be used. - -.. note:: the insertion methods will start the widget themselves, and will - return the result of :js:func:`~openerp.base.Widget.start()`. - - If for some reason you do not want to call these methods, you will - have to first call :js:func:`~openerp.base.Widget.render()` on the - widget, then insert it into your DOM and start it. - -If the widget is not needed anymore (because it's transient), simply terminate -it: - -.. code-block:: javascript - - my_widget.destroy(); - -will unbind all DOM events, remove the widget's content from the DOM and -destroy all widget data. - -.. [0] not all DOM events are compatible with events delegation - -.. _.appendTo(): - http://api.jquery.com/appendTo/ - -.. _.prependTo(): - http://api.jquery.com/prependTo/ - -.. _.insertAfter(): - http://api.jquery.com/insertAfter/ - -.. _.insertBefore(): - http://api.jquery.com/insertBefore/ - -.. _event delegation: - http://api.jquery.com/delegate/ - -.. _Backbone's setElement: - http://backbonejs.org/#View-setElement - -.. _Backbone's delegateEvents: - http://backbonejs.org/#View-delegateEvents - diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js index f190fefeba3..de8f5f660a3 100644 --- a/addons/web/static/src/js/openerpframework.js +++ b/addons/web/static/src/js/openerpframework.js @@ -258,24 +258,24 @@ openerp.ParentedMixin = { current object is destroyed. */ alive: function(promise, reject) { - var def = $.Deferred(); var self = this; - promise.done(function() { - if (! self.isDestroyed()) { - if (! reject) + return $.Deferred(function (def) { + promise.then(function () { + if (!self.isDestroyed()) { def.resolve.apply(def, arguments); - else - def.reject(); - } - }).fail(function() { - if (! self.isDestroyed()) { - if (! reject) + } + }, function () { + if (!self.isDestroyed()) { def.reject.apply(def, arguments); - else + } + }).always(function () { + if (reject) { + // noop if def already resolved or rejected def.reject(); - } - }); - return def.promise(); + } + // otherwise leave promise in limbo + }); + }).promise(); }, /** * Inform the object it should destroy itself, releasing any diff --git a/addons/web/static/test/framework.js b/addons/web/static/test/framework.js index 237bdc195ca..3fff6b7bcb3 100644 --- a/addons/web/static/test/framework.js +++ b/addons/web/static/test/framework.js @@ -400,8 +400,68 @@ ropenerp.testing.section('Widget.events', { ok(newclicked, "undelegate should only unbind events it created"); }); }); +ropenerp.testing.section('Widget.async', { -ropenerp.testing.section('server-formats', { +}, function (test) { + test("alive(alive)", {asserts: 1}, function () { + var w = new (openerp.Widget.extend({})); + return $.async_when(w.start()) + .then(function () { return w.alive($.async_when()) }) + .then(function () { ok(true); }); + }); + test("alive(dead)", {asserts: 1}, function () { + var w = new (openerp.Widget.extend({})); + + return $.Deferred(function (d) { + $.async_when(w.start()) + .then(function () { + // destroy widget + w.destroy(); + var promise = $.async_when(); + // leave time for alive() to do its stuff + promise.then(function () { + return $.async_when(); + }).then(function () { + ok(true); + d.resolve(); + }); + // ensure that w.alive() refuses to resolve or reject + return w.alive(promise); + }).always(function () { + d.reject(); + ok(false, "alive() should not terminate by default"); + }) + }); + }); + + + test("alive(alive, true)", {asserts: 1}, function () { + var w = new (openerp.Widget.extend({})); + return $.async_when(w.start()) + .then(function () { return w.alive($.async_when(), true) }) + .then(function () { ok(true); }); + }); + test("alive(dead, true)", {asserts: 1, fail_on_rejection: false}, function () { + var w = new (openerp.Widget.extend({})); + + return $.async_when(w.start()) + .then(function () { + // destroy widget + w.destroy(); + console.log('destroyed'); + return w.alive($.async_when().done(function () { console.log('when'); }), true); + }).then(function () { + console.log('unfailed') + ok(false, "alive(p, true) should fail its promise"); + }, function () { + console.log('failed') + ok(true, "alive(p, true) should fail its promise"); + }); + }); + +}); + + ropenerp.testing.section('server-formats', { dependencies: ['web.core', 'web.dates'] }, function (test) { test('Parse server datetime', function () { diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css index 9fb81c99d74..9b5468f1bdd 100644 --- a/doc/_themes/odoodoc/static/style.css +++ b/doc/_themes/odoodoc/static/style.css @@ -6714,3 +6714,6 @@ pre { word-break: normal; word-wrap: normal; } +.descclassname { + opacity: 0.5; +} diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less index e5c0030b1eb..ca8be90d4c2 100644 --- a/doc/_themes/odoodoc/static/style.less +++ b/doc/_themes/odoodoc/static/style.less @@ -551,3 +551,8 @@ pre { word-break: normal; word-wrap: normal; } + +// lighten js namespace/class name +.descclassname { + opacity: 0.5; +} diff --git a/doc/reference/javascript.rst b/doc/reference/javascript.rst index 07f38b77fc1..18491eae98d 100644 --- a/doc/reference/javascript.rst +++ b/doc/reference/javascript.rst @@ -1,3 +1,7 @@ +.. highlight:: javascript + +.. default-domain:: js + ========== Javascript ========== @@ -5,8 +9,400 @@ Javascript Widgets ======= -.. qweb integration: ``template`` is an (optional) automatically rendered - template +.. class:: openerp.Widget + +This is the base class for all visual components. It corresponds to an MVC +view. It provides a number of services to handle a section of a page: + +* Rendering with QWeb + +* Parenting-child relations + +* Life-cycle management (including facilitating children destruction when a + parent object is removed) + +* DOM insertion, via jQuery-powered insertion methods. Insertion targets can + be anything the corresponding jQuery method accepts (generally selectors, + DOM nodes and jQuery objects): + + :func:`~openerp.Widget.appendTo` + Renders the widget and inserts it as the last child of the target, uses + `.appendTo()`_ + + :func:`~openerp.Widget.prependTo` + Renders the widget and inserts it as the first child of the target, uses + `.prependTo()`_ + + :func:`~openerp.Widget.insertAfter` + Renders the widget and inserts it as the preceding sibling of the target, + uses `.insertAfter()`_ + + :func:`~openerp.Widget.insertBefore` + Renders the widget and inserts it as the following sibling of the target, + uses `.insertBefore()`_ + +* Backbone-compatible shortcuts + +.. _widget-dom_root: + +DOM Root +-------- + +A :class:`~openerp.Widget` is responsible for a section of the +page materialized by the DOM root of the widget. + +A widget's DOM root is available via two attributes: + +.. attribute:: openerp.Widget.el + + raw DOM element set as root to the widget + +.. attribute:: openerp.Widget.$el + + jQuery wrapper around :attr:`~openerp.Widget.el` + +There are two main ways to define and generate this DOM root: + +.. attribute:: openerp.Widget.template + + Should be set to the name of a :ref:`QWeb template `. + If set, the template will be rendered after the widget has been + initialized but before it has been started. The root element generated by + the template will be set as the DOM root of the widget. + +.. attribute:: openerp.Widget.tagName + + Used if the widget has no template defined. Defaults to ``div``, + will be used as the tag name to create the DOM element to set as + the widget's DOM root. It is possible to further customize this + generated DOM root with the following attributes: + + .. attribute:: openerp.Widget.id + + Used to generate an ``id`` attribute on the generated DOM + root. + + .. attribute:: openerp.Widget.className + + Used to generate a ``class`` attribute on the generated DOM root. + + .. attribute:: openerp.Widget.attributes + + Mapping (object literal) of attribute names to attribute + values. Each of these k:v pairs will be set as a DOM attribute + on the generated DOM root. + + None of these is used in case a template is specified on the widget. + +The DOM root can also be defined programmatically by overridding + +.. function:: openerp.Widget.renderElement + + Renders the widget's DOM root and sets it. The default + implementation will render a set template or generate an element + as described above, and will call + :func:`~openerp.Widget.setElement` on the result. + + Any override to :func:`~openerp.Widget.renderElement` which + does not call its ``_super`` **must** call + :func:`~openerp.Widget.setElement` with whatever it + generated or the widget's behavior is undefined. + + .. note:: + + The default :func:`~openerp.Widget.renderElement` can + be called repeatedly, it will *replace* the previous DOM root + (using ``replaceWith``). However, this requires that the + widget correctly sets and unsets its events (and children + widgets). Generally, :func:`~openerp.Widget.renderElement` should + not be called repeatedly unless the widget advertizes this feature. + +Using a widget +'''''''''''''' + +A widget's lifecycle has 3 main phases: + +* creation and initialization of the widget instance + + .. function:: openerp.Widget.init(parent) + + initialization method of widgets, synchronous, can be overridden to + take more parameters from the widget's creator/parent + + :param parent: the current widget's parent, used to handle automatic + destruction and even propagation. Can be ``null`` for + the widget to have no parent. + :type parent: :class:`~openerp.Widget` + +* DOM injection and startup, this is done by calling one of: + + .. function:: openerp.Widget.appendTo(element) + + Renders the widget and inserts it as the last child of the target, uses + `.appendTo()`_ + + .. function:: openerp.Widget.prependTo(element) + + Renders the widget and inserts it as the first child of the target, uses + `.prependTo()`_ + + .. function:: openerp.Widget.insertAfter(element) + + Renders the widget and inserts it as the preceding sibling of the target, + uses `.insertAfter()`_ + + .. function:: openerp.Widget.insertBefore(element) + + Renders the widget and inserts it as the following sibling of the target, + uses `.insertBefore()`_ + + All of these methods accept whatever the corresponding jQuery method accepts + (CSS selectors, DOM nodes or jQuery objects). They all return a promise and + are charged with three tasks: + + * render the widget's root element via + :func:`~openerp.Widget.renderElement` + * insert the widget's root element in the DOM using whichever jQuery method + they match + * start the widget, and return the result of starting it + + .. function:: openerp.Widget.start() + + asynchronous startup of the widget once it's been injected in the DOM, + generally used to perform asynchronous RPC calls to fetch whatever + remote data is necessary for the widget to do its work. + + Must return a deferred_ to indicate when its work is done. + + A widget is *not guaranteed* to work correctly until its + :func:`~openerp.Widget.start` method has finished executing. The + widget's parent/creator must wait for a widget to be fully started + before interacting with it + + :returns: deferred_ object + +* widget destruction and cleanup + + .. function:: openerp.Widget.destroy() + + destroys the widget's children, unbinds its events and removes its root + from the DOM. Automatically called when the widget's parent is destroyed, + must be called explicitly if the widget has no parents or if it is + removed but its parent remains. + + A widget being destroyed is automatically unlinked from its parent. + +Because a widget can be destroyed at any time, widgets also have utility +methods to handle this case: + +.. function:: openerp.Widget.alive(deferred[, reject=false]) + + A significant issue with RPC and destruction is that an RPC call may take + a long time to execute and return while a widget is being destroyed or + after it has been destroyed, trying to execute its operations on a widget + in a broken/invalid state. + + This is a frequent source of errors or strange behaviors. + + :func:`~openerp.Widget.alive` can be used to wrap an RPC call, + ensuring that whatever operations should be executed when the call ends + are only executed if the widget is still alive:: + + this.alive(this.model.query().all()).then(function (records) { + // would break if executed after the widget is destroyed, wrapping + // rpc in alive() prevents execution + _.each(records, function (record) { + self.$el.append(self.format(record)); + }); + }); + + :param deferred: a deferred_ object to wrap + :param reject: by default, if the RPC call returns after the widget has + been destroyed the returned deferred_ is left in limbo + (neither resolved nor rejected). If ``reject`` is set to + ``true``, the deferred_ will be rejected instead. + :returns: deferred_ object + +.. function:: openerp.Widget.isDestroyed() + + :returns: ``true`` if the widget is being or has been destroyed, ``false`` + otherwise + +Accessing DOM content +''''''''''''''''''''' + +Because a widget is only responsible for the content below its DOM +root, there is a shortcut for selecting sub-sections of a widget's +DOM: + +.. function:: openerp.Widget.$(selector) + + Applies the CSS selector specified as parameter to the widget's + DOM root. + + :: + + this.$(selector); + + is functionally identical to:: + + this.$el.find(selector); + + :param String selector: CSS selector + :returns: jQuery object + + .. note:: this helper method is compatible with + ``Backbone.View.$`` + +Resetting the DOM root +'''''''''''''''''''''' + +.. function:: openerp.Widget.setElement(element) + + Re-sets the widget's DOM root to the provided element, also + handles re-setting the various aliases of the DOM root as well as + unsetting and re-setting delegated events. + + :param Element element: a DOM element or jQuery object to set as + the widget's DOM root + + .. note:: should be mostly compatible with `Backbone's + setElement`_ + +DOM events handling +------------------- + +A widget will generally need to respond to user action within its +section of the page. This entails binding events to DOM elements. + +To this end, :class:`~openerp.Widget` provides an shortcut: + +.. attribute:: openerp.Widget.events + + Events are a mapping of ``event selector`` (an event name and a + CSS selector separated by a space) to a callback. The callback can + be the name of a widget's method or a function object. In either case, the + ``this`` will be set to the widget:: + + events: { + 'click p.oe_some_class a': 'some_method', + 'change input': function (e) { + e.stopPropagation(); + } + }, + + The selector is used for jQuery's `event delegation`_, the + callback will only be triggered for descendants of the DOM root + matching the selector\ [#eventsdelegation]_. If the selector is left out + (only an event name is specified), the event will be set directly on the + widget's DOM root. + +.. function:: openerp.Widget.delegateEvents + + This method is in charge of binding + :attr:`~openerp.Widget.events` to the DOM. It is + automatically called after setting the widget's DOM root. + + It can be overridden to set up more complex events than the + :attr:`~openerp.Widget.events` map allows, but the parent + should always be called (or :attr:`~openerp.Widget.events` + won't be handled correctly). + +.. function:: openerp.Widget.undelegateEvents + + This method is in charge of unbinding + :attr:`~openerp.Widget.events` from the DOM root when the + widget is destroyed or the DOM root is reset, in order to avoid + leaving "phantom" events. + + It should be overridden to un-set any event set in an override of + :func:`~openerp.Widget.delegateEvents`. + +.. note:: this behavior should be compatible with `Backbone's + delegateEvents`_, apart from not accepting any argument. + +Subclassing Widget +------------------ + +:class:`~openerp.Widget` is subclassed in the standard manner (via the +:func:`~openerp.Class.extend` method), and provides a number of +abstract properties and concrete methods (which you may or may not want to +override). Creating a subclass looks like this:: + + var MyWidget = openerp.Widget.extend({ + // QWeb template to use when rendering the object + template: "MyQWebTemplate", + events: { + // events binding example + 'click .my-button': 'handle_click', + }, + + init: function(parent) { + this._super(parent); + // insert code to execute before rendering, for object + // initialization + }, + start: function() { + var sup = this._super(); + // post-rendering initialization code, at this point + + // allows multiplexing deferred objects + return $.when( + // propagate asynchronous signal from parent class + sup, + // return own's asynchronous signal + this.rpc(/* … */)) + } + }); + +The new class can then be used in the following manner:: + + // Create the instance + var my_widget = new MyWidget(this); + // Render and insert into DOM + my_widget.appendTo(".some-div"); + +After these two lines have executed (and any promise returned by +:func:`~openerp.Widget.appendTo` has been resolved if needed), the widget is +ready to be used. + +.. note:: the insertion methods will start the widget themselves, and will + return the result of :func:`~openerp.Widget.start()`. + + If for some reason you do not want to call these methods, you will + have to first call :func:`~openerp.Widget.render()` on the + widget, then insert it into your DOM and start it. + +If the widget is not needed anymore (because it's transient), simply terminate +it:: + + my_widget.destroy(); + +will unbind all DOM events, remove the widget's content from the DOM and +destroy all widget data. + +.. _.appendTo(): + http://api.jquery.com/appendTo/ + +.. _.prependTo(): + http://api.jquery.com/prependTo/ + +.. _.insertAfter(): + http://api.jquery.com/insertAfter/ + +.. _.insertBefore(): + http://api.jquery.com/insertBefore/ + +.. _event delegation: + http://api.jquery.com/delegate/ + +.. _Backbone's setElement: + http://backbonejs.org/#View-setElement + +.. _Backbone's delegateEvents: + http://backbonejs.org/#View-delegateEvents + +.. _deferred: http://api.jquery.com/category/deferred-object/ RPC === @@ -15,3 +411,6 @@ RPC Web Client ========== + +.. [#eventsdelegation] not all DOM events are compatible with events delegation + From 62c95894851335302e210809255d10f142b6b244 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 12 Sep 2014 15:40:50 +0200 Subject: [PATCH 2/4] [ADD] port webclient RPC doc from web/doc --- addons/web/doc/index.rst | 2 - addons/web/doc/rpc.rst | 279 ----- addons/web/doc/testing.rst | 697 ------------- addons/web/doc/web_controllers.rst | 173 ---- doc/conf.py | 4 +- {addons/web/doc => doc/reference}/async.rst | 33 +- doc/reference/http.rst | 2 + doc/reference/images/runner.png | Bin 0 -> 3963 bytes doc/reference/images/runner2.png | Bin 0 -> 6807 bytes doc/reference/images/tests.png | Bin 0 -> 65388 bytes doc/reference/images/tests2.png | Bin 0 -> 20114 bytes doc/reference/images/tests3.png | Bin 0 -> 20382 bytes doc/reference/javascript.rst | 960 ++++++++++++++++-- .../web/doc => doc/reference}/test-report.txt | 0 14 files changed, 916 insertions(+), 1234 deletions(-) delete mode 100644 addons/web/doc/rpc.rst delete mode 100644 addons/web/doc/testing.rst delete mode 100644 addons/web/doc/web_controllers.rst rename {addons/web/doc => doc/reference}/async.rst (93%) create mode 100644 doc/reference/images/runner.png create mode 100644 doc/reference/images/runner2.png create mode 100644 doc/reference/images/tests.png create mode 100644 doc/reference/images/tests2.png create mode 100644 doc/reference/images/tests3.png rename {addons/web/doc => doc/reference}/test-report.txt (100%) diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index bad653e0da7..5a862d2937d 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -34,8 +34,6 @@ Javascript :maxdepth: 1 guidelines - rpc - async client_action testing diff --git a/addons/web/doc/rpc.rst b/addons/web/doc/rpc.rst deleted file mode 100644 index d5fc414d6cf..00000000000 --- a/addons/web/doc/rpc.rst +++ /dev/null @@ -1,279 +0,0 @@ -RPC Calls -========= - -Building static displays is all nice and good and allows for neat -effects (and sometimes you're given data to display from third parties -so you don't have to make any effort), but a point generally comes -where you'll want to talk to the world and make some network requests. - -OpenERP Web provides two primary APIs to handle this, a low-level -JSON-RPC based API communicating with the Python section of OpenERP -Web (and of your addon, if you have a Python part) and a high-level -API above that allowing your code to talk directly to the OpenERP -server, using familiar-looking calls. - -All networking APIs are :doc:`asynchronous `. As a result, all -of them will return :js:class:`Deferred` objects (whether they resolve -those with values or not). Understanding how those work before before -moving on is probably necessary. - -High-level API: calling into OpenERP models -------------------------------------------- - -Access to OpenERP object methods (made available through XML-RPC from -the server) is done via the :js:class:`openerp.web.Model` class. This -class maps onto the OpenERP server objects via two primary methods, -:js:func:`~openerp.web.Model.call` and -:js:func:`~openerp.web.Model.query`. - -:js:func:`~openerp.web.Model.call` is a direct mapping to the -corresponding method of the OpenERP server object. Its usage is -similar to that of the OpenERP Model API, with three differences: - -* The interface is :doc:`asynchronous `, so instead of - returning results directly RPC method calls will return - :js:class:`Deferred` instances, which will themselves resolve to the - result of the matching RPC call. - -* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to - ``__getattr__`` or ``method_missing``, there needs to be an explicit - method to dispatch RPC methods. - -* No notion of pooler, the model proxy is instantiated where needed, - not fetched from an other (somewhat global) object - -.. code-block:: javascript - - var Users = new Model('res.users'); - - Users.call('change_password', ['oldpassword', 'newpassword'], - {context: some_context}).then(function (result) { - // do something with change_password result - }); - -:js:func:`~openerp.web.Model.query` is a shortcut for a builder-style -interface to searches (``search`` + ``read`` in OpenERP RPC terms). It -returns a :js:class:`~openerp.web.Query` object which is immutable but -allows building new :js:class:`~openerp.web.Query` instances from the -first one, adding new properties or modifiying the parent object's: - -.. code-block:: javascript - - Users.query(['name', 'login', 'user_email', 'signature']) - .filter([['active', '=', true], ['company_id', '=', main_company]]) - .limit(15) - .all().then(function (users) { - // do work with users records - }); - -The query is only actually performed when calling one of the query -serialization methods, :js:func:`~openerp.web.Query.all` and -:js:func:`~openerp.web.Query.first`. These methods will perform a new -RPC call every time they are called. - -For that reason, it's actually possible to keep "intermediate" queries -around and use them differently/add new specifications on them. - -.. js:class:: openerp.web.Model(name) - - .. js:attribute:: openerp.web.Model.name - - name of the OpenERP model this object is bound to - - .. js:function:: openerp.web.Model.call(method[, args][, kwargs]) - - Calls the ``method`` method of the current model, with the - provided positional and keyword arguments. - - :param String method: method to call over rpc on the - :js:attr:`~openerp.web.Model.name` - :param Array<> args: positional arguments to pass to the - method, optional - :param Object<> kwargs: keyword arguments to pass to the - method, optional - :rtype: Deferred<> - - .. js:function:: openerp.web.Model.query(fields) - - :param Array fields: list of fields to fetch during - the search - :returns: a :js:class:`~openerp.web.Query` object - representing the search to perform - -.. js:class:: openerp.web.Query(fields) - - The first set of methods is the "fetching" methods. They perform - RPC queries using the internal data of the object they're called - on. - - .. js:function:: openerp.web.Query.all() - - Fetches the result of the current - :js:class:`~openerp.web.Query` object's search. - - :rtype: Deferred> - - .. js:function:: openerp.web.Query.first() - - Fetches the **first** result of the current - :js:class:`~openerp.web.Query`, or ``null`` if the current - :js:class:`~openerp.web.Query` does have any result. - - :rtype: Deferred - - .. js:function:: openerp.web.Query.count() - - Fetches the number of records the current - :js:class:`~openerp.web.Query` would retrieve. - - :rtype: Deferred - - .. js:function:: openerp.web.Query.group_by(grouping...) - - Fetches the groups for the query, using the first specified - grouping parameter - - :param Array grouping: Lists the levels of grouping - asked of the server. Grouping - can actually be an array or - varargs. - :rtype: Deferred> | null - - The second set of methods is the "mutator" methods, they create a - **new** :js:class:`~openerp.web.Query` object with the relevant - (internal) attribute either augmented or replaced. - - .. js:function:: openerp.web.Query.context(ctx) - - Adds the provided ``ctx`` to the query, on top of any existing - context - - .. js:function:: openerp.web.Query.filter(domain) - - Adds the provided domain to the query, this domain is - ``AND``-ed to the existing query domain. - - .. js:function:: opeenrp.web.Query.offset(offset) - - Sets the provided offset on the query. The new offset - *replaces* the old one. - - .. js:function:: openerp.web.Query.limit(limit) - - Sets the provided limit on the query. The new limit *replaces* - the old one. - - .. js:function:: openerp.web.Query.order_by(fields…) - - Overrides the model's natural order with the provided field - specifications. Behaves much like Django's `QuerySet.order_by - `_: - - * Takes 1..n field names, in order of most to least importance - (the first field is the first sorting key). Fields are - provided as strings. - - * A field specifies an ascending order, unless it is prefixed - with the minus sign "``-``" in which case the field is used - in the descending order - - Divergences from Django's sorting include a lack of random sort - (``?`` field) and the inability to "drill down" into relations - for sorting. - -Aggregation (grouping) -~~~~~~~~~~~~~~~~~~~~~~ - -OpenERP has powerful grouping capacities, but they are kind-of strange -in that they're recursive, and level n+1 relies on data provided -directly by the grouping at level n. As a result, while ``read_group`` -works it's not a very intuitive API. - -OpenERP Web 7.0 eschews direct calls to ``read_group`` in favor of -calling a method of :js:class:`~openerp.web.Query`, `much in the way -it is one in SQLAlchemy -`_ [#]_: - -.. code-block:: javascript - - some_query.group_by(['field1', 'field2']).then(function (groups) { - // do things with the fetched groups - }); - -This method is asynchronous when provided with 1..n fields (to group -on) as argument, but it can also be called without any field (empty -fields collection or nothing at all). In this case, instead of -returning a Deferred object it will return ``null``. - -When grouping criterion come from a third-party and may or may not -list fields (e.g. could be an empty list), this provides two ways to -test the presence of actual subgroups (versus the need to perform a -regular query for records): - -* A check on ``group_by``'s result and two completely separate code - paths - - .. code-block:: javascript - - var groups; - if (groups = some_query.group_by(gby)) { - groups.then(function (gs) { - // groups - }); - } - // no groups - -* Or a more coherent code path using :js:func:`when`'s ability to - coerce values into deferreds: - - .. code-block:: javascript - - $.when(some_query.group_by(gby)).then(function (groups) { - if (!groups) { - // No grouping - } else { - // grouping, even if there are no groups (groups - // itself could be an empty array) - } - }); - -The result of a (successful) :js:func:`~openerp.web.Query.group_by` is -an array of :js:class:`~openerp.web.QueryGroup`. - -.. _rpc_rpc: - -Low-level API: RPC calls to Python side ---------------------------------------- - -While the previous section is great for calling core OpenERP code -(models code), it does not work if you want to call the Python side of -OpenERP Web. - -For this, a lower-level API exists on on -:js:class:`~openerp.web.Connection` objects (usually available through -``openerp.connection``): the ``rpc`` method. - -This method simply takes an absolute path (which is the combination of -the Python controller's ``_cp_path`` attribute and the name of the -method you want to call) and a mapping of attributes to values (applied -as keyword arguments on the Python method [#]_). This function fetches -the return value of the Python methods, converted to JSON. - -For instance, to call the ``resequence`` of the -:class:`~web.controllers.main.DataSet` controller: - -.. code-block:: javascript - - openerp.connection.rpc('/web/dataset/resequence', { - model: some_model, - ids: array_of_ids, - offset: 42 - }).then(function (result) { - // resequenced on server - }); - -.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by`` - is not terminal, it returns a query which can still be altered. - -.. [#] except for ``context``, which is extracted and stored in the - request object itself. diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst deleted file mode 100644 index a8f78bd210e..00000000000 --- a/addons/web/doc/testing.rst +++ /dev/null @@ -1,697 +0,0 @@ -.. highlight:: javascript - -.. _testing: - -Testing in OpenERP Web -====================== - -Javascript Unit Testing ------------------------ - -OpenERP Web 7.0 includes means to unit-test both the core code of -OpenERP Web and your own javascript modules. On the javascript side, -unit-testing is based on QUnit_ with a number of helpers and -extensions for better integration with OpenERP. - -To see what the runner looks like, find (or start) an OpenERP server -with the web client enabled, and navigate to ``/web/tests`` e.g. `on -OpenERP's CI `_. This will -show the runner selector, which lists all modules with javascript unit -tests, and allows starting any of them (or all javascript tests in all -modules at once). - -.. image:: ./images/runner.png - :align: center - -Clicking any runner button will launch the corresponding tests in the -bundled QUnit_ runner: - -.. image:: ./images/tests.png - :align: center - -Writing a test case -------------------- - -The first step is to list the test file(s). This is done through the -``test`` key of the openerp manifest, by adding javascript files to it -(next to the usual YAML files, if any): - -.. code-block:: python - - { - 'name': "Demonstration of web/javascript tests", - 'category': 'Hidden', - 'depends': ['web'], - 'test': ['static/test/demo.js'], - } - -and to create the corresponding test file(s) - -.. note:: - - Test files which do not exist will be ignored, if all test files - of a module are ignored (can not be found), the test runner will - consider that the module has no javascript tests. - -After that, refreshing the runner selector will display the new module -and allow running all of its (0 so far) tests: - -.. image:: ./images/runner2.png - :align: center - -The next step is to create a test case:: - - openerp.testing.section('basic section', function (test) { - test('my first test', function () { - ok(false, "this test has run"); - }); - }); - -All testing helpers and structures live in the ``openerp.testing`` -module. OpenERP tests live in a :js:func:`~openerp.testing.section`, -which is itself part of a module. The first argument to a section is -the name of the section, the second one is the section body. - -:js:func:`test `, provided by the -:js:func:`~openerp.testing.section` to the callback, is used to -register a given test case which will be run whenever the test runner -actually does its job. OpenERP Web test case use standard `QUnit -assertions`_ within them. - -Launching the test runner at this point will run the test and display -the corresponding assertion message, with red colors indicating the -test failed: - -.. image:: ./images/tests2.png - :align: center - -Fixing the test (by replacing ``false`` to ``true`` in the assertion) -will make it pass: - -.. image:: ./images/tests3.png - :align: center - -Assertions ----------- - -As noted above, OpenERP Web's tests use `qunit assertions`_. They are -available globally (so they can just be called without references to -anything). The following list is available: - -.. js:function:: ok(state[, message]) - - checks that ``state`` is truthy (in the javascript sense) - -.. js:function:: strictEqual(actual, expected[, message]) - - checks that the actual (produced by a method being tested) and - expected values are identical (roughly equivalent to ``ok(actual - === expected, message)``) - -.. js:function:: notStrictEqual(actual, expected[, message]) - - checks that the actual and expected values are *not* identical - (roughly equivalent to ``ok(actual !== expected, message)``) - -.. js:function:: deepEqual(actual, expected[, message]) - - deep comparison between actual and expected: recurse into - containers (objects and arrays) to ensure that they have the same - keys/number of elements, and the values match. - -.. js:function:: notDeepEqual(actual, expected[, message]) - - inverse operation to :js:func:`deepEqual` - -.. js:function:: throws(block[, expected][, message]) - - checks that, when called, the ``block`` throws an - error. Optionally validates that error against ``expected``. - - :param Function block: - :param expected: if a regexp, checks that the thrown error's - message matches the regular expression. If an - error type, checks that the thrown error is of - that type. - :type expected: Error | RegExp - -.. js:function:: equal(actual, expected[, message]) - - checks that ``actual`` and ``expected`` are loosely equal, using - the ``==`` operator and its coercion rules. - -.. js:function:: notEqual(actual, expected[, message]) - - inverse operation to :js:func:`equal` - -Getting an OpenERP instance ---------------------------- - -The OpenERP instance is the base through which most OpenERP Web -modules behaviors (functions, objects, …) are accessed. As a result, -the test framework automatically builds one, and loads the module -being tested and all of its dependencies inside it. This new instance -is provided as the first positional parameter to your test -cases. Let's observe by adding javascript code (not test code) to the -test module: - -.. code-block:: python - - { - 'name': "Demonstration of web/javascript tests", - 'category': 'Hidden', - 'depends': ['web'], - 'js': ['static/src/js/demo.js'], - 'test': ['static/test/demo.js'], - } - -:: - - // src/js/demo.js - openerp.web_tests_demo = function (instance) { - instance.web_tests_demo = { - value_true: true, - SomeType: instance.web.Class.extend({ - init: function (value) { - this.value = value; - } - }) - }; - }; - -and then adding a new test case, which simply checks that the -``instance`` contains all the expected stuff we created in the -module:: - - // test/demo.js - test('module content', function (instance) { - ok(instance.web_tests_demo.value_true, "should have a true value"); - var type_instance = new instance.web_tests_demo.SomeType(42); - strictEqual(type_instance.value, 42, "should have provided value"); - }); - -DOM Scratchpad --------------- - -As in the wider client, arbitrarily accessing document content is -strongly discouraged during tests. But DOM access is still needed to -e.g. fully initialize :js:class:`widgets <~openerp.web.Widget>` before -testing them. - -Thus, a test case gets a DOM scratchpad as its second positional -parameter, in a jQuery instance. That scratchpad is fully cleaned up -before each test, and as long as it doesn't do anything outside the -scratchpad your code can do whatever it wants:: - - // test/demo.js - test('DOM content', function (instance, $scratchpad) { - $scratchpad.html('
ok
'); - ok($scratchpad.find('span').hasClass('foo'), - "should have provided class"); - }); - test('clean scratchpad', function (instance, $scratchpad) { - ok(!$scratchpad.children().length, "should have no content"); - ok(!$scratchpad.text(), "should have no text"); - }); - -.. note:: - - The top-level element of the scratchpad is not cleaned up, test - cases can add text or DOM children but shoud not alter - ``$scratchpad`` itself. - -Loading templates ------------------ - -To avoid the corresponding processing costs, by default templates are -not loaded into QWeb. If you need to render e.g. widgets making use of -QWeb templates, you can request their loading through the -:js:attr:`~TestOptions.templates` option to the :js:func:`test case -function `. - -This will automatically load all relevant templates in the instance's -qweb before running the test case: - -.. code-block:: python - - { - 'name': "Demonstration of web/javascript tests", - 'category': 'Hidden', - 'depends': ['web'], - 'js': ['static/src/js/demo.js'], - 'test': ['static/test/demo.js'], - 'qweb': ['static/src/xml/demo.xml'], - } - -.. code-block:: xml - - - - - -

-
-
-
- -:: - - // test/demo.js - test('templates', {templates: true}, function (instance) { - var s = instance.web.qweb.render('DemoTemplate'); - var texts = $(s).find('p').map(function () { - return $(this).text(); - }).get(); - - deepEqual(texts, ['0', '1', '2', '3', '4']); - }); - -Asynchronous cases ------------------- - -The test case examples so far are all synchronous, they execute from -the first to the last line and once the last line has executed the -test is done. But the web client is full of :doc:`asynchronous code -`, and thus test cases need to be async-aware. - -This is done by returning a :js:class:`deferred ` from the -case callback:: - - // test/demo.js - test('asynchronous', { - asserts: 1 - }, function () { - var d = $.Deferred(); - setTimeout(function () { - ok(true); - d.resolve(); - }, 100); - return d; - }); - -This example also uses the :js:class:`options parameter ` -to specify the number of assertions the case should expect, if less or -more assertions are specified the case will count as failed. - -Asynchronous test cases *must* specify the number of assertions they -will run. This allows more easily catching situations where e.g. the -test architecture was not warned about asynchronous operations. - -.. note:: - - Asynchronous test cases also have a 2 seconds timeout: if the test - does not finish within 2 seconds, it will be considered - failed. This pretty much always means the test will not - resolve. This timeout *only* applies to the test itself, not to - the setup and teardown processes. - -.. note:: - - If the returned deferred is rejected, the test will be failed - unless :js:attr:`~TestOptions.fail_on_rejection` is set to - ``false``. - -RPC ---- - -An important subset of asynchronous test cases is test cases which -need to perform (and chain, to an extent) RPC calls. - -.. note:: - - Because they are a subset of asynchronous cases, RPC cases must - also provide a valid :js:attr:`assertions count - `. - -By default, test cases will fail when trying to perform an RPC -call. The ability to perform RPC calls must be explicitly requested by -a test case (or its containing test suite) through -:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or -``rpc``. - -.. _testing-rpc-mock: - -Mock RPC -++++++++ - -The preferred (and fastest from a setup and execution time point of -view) way to do RPC during tests is to mock the RPC calls: while -setting up the test case, provide what the RPC responses "should" be, -and only test the code between the "user" (the test itself) and the -RPC call, before the call is effectively done. - -To do this, set the :js:attr:`rpc option ` to -``mock``. This will add a third parameter to the test case callback: - -.. js:function:: mock(rpc_spec, handler) - - Can be used in two different ways depending on the shape of the - first parameter: - - * If it matches the pattern ``model:method`` (if it contains a - colon, essentially) the call will set up the mocking of an RPC - call straight to the OpenERP server (through XMLRPC) as - performed via e.g. :js:func:`openerp.web.Model.call`. - - In that case, ``handler`` should be a function taking two - arguments ``args`` and ``kwargs``, matching the corresponding - arguments on the server side and should simply return the value - as if it were returned by the Python XMLRPC handler:: - - test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { - // set up mocking - mock('people.famous:name_search', function (args, kwargs) { - strictEqual(kwargs.name, 'bob'); - return [ - [1, "Microsoft Bob"], - [2, "Bob the Builder"], - [3, "Silent Bob"] - ]; - }); - - // actual test code - return new instance.web.Model('people.famous') - .call('name_search', {name: 'bob'}).then(function (result) { - strictEqual(result.length, 3, "shoud return 3 people"); - strictEqual(result[0][1], "Microsoft Bob", - "the most famous bob should be Microsoft Bob"); - }); - }); - - * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it - will mock a JSON-RPC call to a web client controller, such as - ``/web/webclient/translations``. In that case, the handler takes - a single ``params`` argument holding all of the parameters - provided over JSON-RPC. - - As previously, the handler should simply return the result value - as if returned by the original JSON-RPC handler:: - - test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) { - var fetched_dbs = false, fetched_langs = false; - mock('/web/database/get_list', function () { - fetched_dbs = true; - return ['foo', 'bar', 'baz']; - }); - mock('/web/session/get_lang_list', function () { - fetched_langs = true; - return [['vo_IS', 'Hopelandic / Vonlenska']]; - }); - - // widget needs that or it blows up - instance.webclient = {toggle_bars: openerp.testing.noop}; - var dbm = new instance.web.DatabaseManager({}); - return dbm.appendTo($s).then(function () { - ok(fetched_dbs, "should have fetched databases"); - ok(fetched_langs, "should have fetched languages"); - deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); - }); - }); - -.. note:: - - Mock handlers can contain assertions, these assertions should be - part of the assertions count (and if multiple calls are made to a - handler containing assertions, it multiplies the effective number - of assertions). - -.. _testing-rpc-rpc: - -Actual RPC -++++++++++ - -A more realistic (but significantly slower and more expensive) way to -perform RPC calls is to perform actual calls to an actually running -OpenERP server. To do this, set the :js:attr:`rpc option -<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter -but will enable actual RPC, and the automatic creation and destruction -of databases (from a specified source) around tests. - -First, create a basic model we can test stuff with: - -.. code-block:: javascript - - from openerp.osv import orm, fields - - class TestObject(orm.Model): - _name = 'web_tests_demo.model' - - _columns = { - 'name': fields.char("Name", required=True), - 'thing': fields.char("Thing"), - 'other': fields.char("Other", required=True) - } - _defaults = { - 'other': "bob" - } - -then the actual test:: - - test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) { - var Model = new instance.web.Model('web_tests_demo.model'); - return Model.call('create', [{name: "Bob"}]) - .then(function (id) { - return Model.call('read', [[id]]); - }).then(function (records) { - strictEqual(records.length, 1); - var record = records[0]; - strictEqual(record.name, "Bob"); - strictEqual(record.thing, false); - // default value - strictEqual(record.other, 'bob'); - }); - }); - -This test looks like a "mock" RPC test but for the lack of mock -response (and the different ``rpc`` type), however it has further -ranging consequences in that it will copy an existing database to a -new one, run the test in full on that temporary database and destroy -the database, to simulate an isolated and transactional context and -avoid affecting other tests. One of the consequences is that it takes -a *long* time to run (5~10s, most of that time being spent waiting for -a database duplication). - -Furthermore, as the test needs to clone a database, it also has to ask -which database to clone, the database/super-admin password and the -password of the ``admin`` user (in order to authenticate as said -user). As a result, the first time the test runner encounters an -``rpc: "rpc"`` test configuration it will produce the following -prompt: - -.. image:: ./images/db-query.png - :align: center - -and stop the testing process until the necessary information has been -provided. - -The prompt will only appear once per test run, all tests will use the -same "source" database. - -.. note:: - - The handling of that information is currently rather brittle and - unchecked, incorrect values will likely crash the runner. - -.. note:: - - The runner does not currently store this information (for any - longer than a test run that is), the prompt will have to be filled - every time. - -Testing API ------------ - -.. js:function:: openerp.testing.section(name[, options], body) - - A test section, serves as shared namespace for related tests (for - constants or values to only set up once). The ``body`` function - should contain the tests themselves. - - Note that the order in which tests are run is essentially - undefined, do *not* rely on it. - - :param String name: - :param TestOptions options: - :param body: - :type body: Function<:js:func:`~openerp.testing.case`, void> - -.. js:function:: openerp.testing.case(name[, options], callback) - - Registers a test case callback in the test runner, the callback - will only be run once the runner is started (or maybe not at all, - if the test is filtered out). - - :param String name: - :param TestOptions options: - :param callback: - :type callback: Function> - -.. js:class:: TestOptions - - the various options which can be passed to - :js:func:`~openerp.testing.section` or - :js:func:`~openerp.testing.case`. Except for - :js:attr:`~TestOptions.setup` and - :js:attr:`~TestOptions.teardown`, an option on - :js:func:`~openerp.testing.case` will overwrite the corresponding - option on :js:func:`~openerp.testing.section` so - e.g. :js:attr:`~TestOptions.rpc` can be set for a - :js:func:`~openerp.testing.section` and then differently set for - some :js:func:`~openerp.testing.case` of that - :js:func:`~openerp.testing.section` - - .. js:attribute:: TestOptions.asserts - - An integer, the number of assertions which should run during a - normal execution of the test. Mandatory for asynchronous tests. - - .. js:attribute:: TestOptions.setup - - Test case setup, run right before each test case. A section's - :js:func:`~TestOptions.setup` is run before the case's own, if - both are specified. - - .. js:attribute:: TestOptions.teardown - - Test case teardown, a case's :js:func:`~TestOptions.teardown` - is run before the corresponding section if both are present. - - .. js:attribute:: TestOptions.fail_on_rejection - - If the test is asynchronous and its resulting promise is - rejected, fail the test. Defaults to ``true``, set to - ``false`` to not fail the test in case of rejection:: - - // test/demo.js - test('unfail rejection', { - asserts: 1, - fail_on_rejection: false - }, function () { - var d = $.Deferred(); - setTimeout(function () { - ok(true); - d.reject(); - }, 100); - return d; - }); - - .. js:attribute:: TestOptions.rpc - - RPC method to use during tests, one of ``"mock"`` or - ``"rpc"``. Any other value will disable RPC for the test (if - they were enabled by the suite for instance). - - .. js:attribute:: TestOptions.templates - - Whether the current module (and its dependencies)'s templates - should be loaded into QWeb before starting the test. A - boolean, ``false`` by default. - -The test runner can also use two global configuration values set -directly on the ``window`` object: - -* ``oe_all_dependencies`` is an ``Array`` of all modules with a web - component, ordered by dependency (for a module ``A`` with - dependencies ``A'``, any module of ``A'`` must come before ``A`` in - the array) - -* ``oe_db_info`` is an object with 3 keys ``source``, ``supadmin`` and - ``password``. It is used to pre-configure :ref:`actual RPC - ` tests, to avoid a prompt being displayed - (especially for headless situations). - -Running through Python ----------------------- - -The web client includes the means to run these tests on the -command-line (or in a CI system), but while actually running it is -pretty simple the setup of the pre-requisite parts has some -complexities. - -1. Install unittest2_ and QUnitSuite_ in your Python environment. Both - can trivially be installed via `pip `_ or - `easy_install - `_. - - The former is the unit-testing framework used by OpenERP, the - latter is an adapter module to run qunit_ test suites and convert - their result into something unittest2_ can understand and report. - -2. Install PhantomJS_. It is a headless - browser which allows automating running and testing web - pages. QUnitSuite_ uses it to actually run the qunit_ test suite. - - The PhantomJS_ website provides pre-built binaries for some - platforms, and your OS's package management probably provides it as - well. - - If you're building PhantomJS_ from source, I recommend preparing - for some knitting time as it's not exactly fast (it needs to - compile both `Qt `_ and `Webkit - `_, both being pretty big projects). - - .. note:: - - Because PhantomJS_ is webkit-based, it will not be able to test - if Firefox, Opera or Internet Explorer can correctly run the - test suite (and it is only an approximation for Safari and - Chrome). It is therefore recommended to *also* run the test - suites in actual browsers once in a while. - - .. note:: - - The version of PhantomJS_ this was build through is 1.7, - previous versions *should* work but are not actually supported - (and tend to just segfault when something goes wrong in - PhantomJS_ itself so they're a pain to debug). - -3. Set up :ref:`OpenERP Command `, - which will be used to actually run the tests: running the qunit_ - test suite requires a running server, so at this point OpenERP - Server isn't able to do it on its own during the building/testing - process. - -4. Install a new database with all relevant modules (all modules with - a web component at least), then restart the server - - .. note:: - - For some tests, a source database needs to be duplicated. This - operation requires that there be no connection to the database - being duplicated, but OpenERP doesn't currently break - existing/outstanding connections, so restarting the server is - the simplest way to ensure everything is in the right state. - -5. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct - addons-path specified (and replacing ``$DATABASE`` by the source - database you created above) - - .. note:: - - If you leave out ``-mweb``, the runner will attempt to run all - the tests in all the modules, which may or may not work. - -If everything went correctly, you should now see a list of tests with -(hopefully) ``ok`` next to their names, closing with a report of the -number of tests run and the time it took: - -.. literalinclude:: test-report.txt - :language: text - -Congratulation, you have just performed a successful "offline" run of -the OpenERP Web test suite. - -.. note:: - - Note that this runs all the Python tests for the ``web`` module, - but all the web tests for all of OpenERP. This can be surprising. - -.. _qunit: http://qunitjs.com/ - -.. _qunit assertions: http://api.qunitjs.com/category/assert/ - -.. _unittest2: http://pypi.python.org/pypi/unittest2 - -.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/ - -.. _PhantomJS: http://phantomjs.org/ diff --git a/addons/web/doc/web_controllers.rst b/addons/web/doc/web_controllers.rst deleted file mode 100644 index 442d2b0caa7..00000000000 --- a/addons/web/doc/web_controllers.rst +++ /dev/null @@ -1,173 +0,0 @@ - -Web Controllers -=============== - -Web controllers are classes in OpenERP able to catch the http requests sent by any browser. They allow to generate -html pages to be served like any web server, implement new methods to be used by the Javascript client, etc... - -Controllers File ----------------- - -By convention the controllers should be placed in the controllers directory of the module. Example: - -.. code-block:: text - - web_example - ├── controllers - │ ├── __init__.py - │ └── my_controllers.py - ├── __init__.py - └── __openerp__.py - -In ``__init__.py`` you must add: - -:: - - import controllers - -And here is the content of ``controllers/__init__.py``: - -:: - - import my_controllers - -Now you can put the following content in ``controllers/my_controllers.py``: - -:: - - import openerp.http as http - from openerp.http import request - - -Controller Declaration ----------------------- - -In your controllers file, you can now declare a controller this way: - -:: - - class MyController(http.Controller): - - @http.route('/my_url/some_html', type="http") - def some_html(self): - return "

This is a test

" - - @http.route('/my_url/some_json', type="json") - def some_json(self): - return {"sample_dictionary": "This is a sample JSON dictionary"} - -A controller must inherit from ``http.Controller``. Each time you define a method with ``@http.route()`` it defines a -url to match. As example, the ``some_html()`` method will be called a client query the ``/my_url/some_html`` url. - -Pure HTTP Requests ------------------- - -You can define methods to get any normal http requests by passing ``'http'`` to the ``type`` argument of -``http.route()``. When doing so, you get the HTTP parameters as named parameters of the method: - -:: - - @http.route('/say_hello', type="http") - def say_hello(self, name): - return "

Hello %s

" % name - -This url could be contacted by typing this url in a browser: ``http://localhost:8069/say_hello?name=Nicolas``. - -JSON Requests -------------- - -Methods that received JSON can be defined by passing ``'json'`` to the ``type`` argument of ``http.route()``. The -OpenERP Javascript client can contact these methods using the JSON-RPC protocol. JSON methods must return JSON. Like the -HTTP methods they receive arguments as named parameters (except these arguments are JSON-RPC parameters). - -:: - - @http.route('/division', type="json") - def division(self, i, j): - return i / j # returns a number - -URL Patterns ------------- - -Any URL passed to ``http.route()`` can contain patterns. Example: - -:: - - @http.route('/files/', type="http") - def files(self, file_path): - ... # return a file identified by the path store in the 'my_path' variable - -When such patterns are used, the method will received additional parameters that correspond to the parameters defined in -the url. For exact documentation about url patterns, see Werkzeug's documentation: -http://werkzeug.pocoo.org/docs/routing/ . - -Also note you can pass multiple urls to ``http.route()``: - - -:: - - @http.route(['/files/', '/other_url/'], type="http") - def files(self, file_path): - ... - -Contacting Models ------------------ - -To use the database you must access the OpenERP models. The global ``request`` object provides the necessary objects: - -:: - - @http.route('/my_name', type="http") - def my_name(self): - my_user_record = request.registry.get("res.users").browse(request.cr, request.uid, request.uid) - return "

Your name is %s

" % my_user_record.name - -``request.registry`` is the registry that gives you access to the models. It is the equivalent of ``self.pool`` when -working inside OpenERP models. - -``request.cr`` is the cursor object. This is the ``cr`` parameter you have to pass as first argument of every model -method in OpenERP. - -``request.uid`` is the id of the current logged in user. This is the ``uid`` parameter you have to pass as second -argument of every model method in OpenERP. - -Authorization Levels --------------------- - -By default, all access to the models will use the rights of the currently logged in user (OpenERP uses cookies to track -logged users). It is also impossible to reach an URL without being logged (the user's browser will receive an HTTP -error). - -There are some cases when the current user is not relevant, and we just want to give access to anyone to an URL. A -typical example is be the generation of a home page for a website. The home page should be visible by anyone, whether -they have an account or not. To do so, add the ``'admin'`` value to the ``auth`` parameter of ``http.route()``: - -:: - - @http.route('/hello', type="http", auth="admin") - def hello(self): - return "
Hello unknown user!
" - -When using the ``admin`` authentication the access to the OpenERP models will be performed with the ``Administrator`` -user and ``request.uid`` will be equal to ``openerp.SUPERUSER_ID`` (the id of the administrator). - -It is important to note that when using the ``Administrator`` user all security is bypassed. So the programmers -implementing such methods should take great care of not creating security issues in the application. - -Overriding Controllers ----------------------- - -Existing routes can be overridden. To do so, create a controller that inherit the controller containing the route you -want to override. Example that redefine the home page of your OpenERP application. - -:: - - import openerp.addons.web.controllers.main as main - - class Home2(main.Home): - @http.route('/', type="http", auth="db") - def index(self): - return "
This is my new home page.
" - -By re-defining the ``index()`` method, you change the behavior of the original ``Home`` class. Now the ``'/'`` route -will match the new ``index()`` method in ``Home2``. diff --git a/doc/conf.py b/doc/conf.py index f563a28cf1c..673b5f760cb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -167,7 +167,9 @@ html_sidebars = { intersphinx_mapping = { 'python': ('https://docs.python.org/2/', None), - 'werkzeug': ('http://werkzeug.pocoo.org/docs/0.9/', None), + 'werkzeug': ('http://werkzeug.pocoo.org/docs/', None), + 'sqlalchemy': ('http://docs.sqlalchemy.org/en/rel_0_9/', None), + 'django': ('https://django.readthedocs.org/en/latest/', None), } github_user = 'odoo' diff --git a/addons/web/doc/async.rst b/doc/reference/async.rst similarity index 93% rename from addons/web/doc/async.rst rename to doc/reference/async.rst index 6782fdac029..c2aded839aa 100644 --- a/addons/web/doc/async.rst +++ b/doc/reference/async.rst @@ -1,3 +1,7 @@ +:orphan: + +.. _reference/async: + Asynchronous Operations ======================= @@ -12,15 +16,8 @@ As a result, performing long-running synchronous network requests or other types of complex and expensive accesses is frowned upon and asynchronous APIs are used instead. -Asynchronous code rarely comes naturally, especially for developers -used to synchronous server-side code (in Python, Java or C#) where the -code will just block until the deed is gone. This is increased further -when asynchronous programming is not a first-class concept and is -instead implemented on top of callbacks-based programming, which is -the case in javascript. - The goal of this guide is to provide some tools to deal with -asynchronous systems, and warn against systematic issues or dangers. +asynchronous systems, and warn against systemic issues or dangers. Deferreds --------- @@ -38,7 +35,7 @@ error callbacks. A great advantage of deferreds over simply passing callback functions directly to asynchronous methods is the ability to :ref:`compose them -`. +`. Using deferreds ~~~~~~~~~~~~~~~ @@ -68,7 +65,7 @@ Building deferreds ~~~~~~~~~~~~~~~~~~ After using asynchronous APIs may come the time to build them: for -`mocks`_, to compose deferreds from multiple source in a complex +mocks_, to compose deferreds from multiple source in a complex manner, in order to let the current operations repaint the screen or give other events the time to unfold, ... @@ -108,7 +105,7 @@ succeeded). These methods should simply be called when the asynchronous operation has ended, to notify anybody interested in its result(s). -.. _deferred-composition: +.. _reference/async/composition: Composing deferreds ~~~~~~~~~~~~~~~~~~~ @@ -128,7 +125,7 @@ Deferred multiplexing ````````````````````` The most common reason for multiplexing deferred is simply performing -2+ asynchronous operations and wanting to wait until all of them are +multiple asynchronous operations and wanting to wait until all of them are done before moving on (and executing more stuff). The jQuery multiplexing function for promises is :js:func:`when`. @@ -140,7 +137,7 @@ The jQuery multiplexing function for promises is :js:func:`when`. This function can take any number of promises [#]_ and will return a promise. -This returned promise will be resolved when *all* multiplexed promises +The returned promise will be resolved when *all* multiplexed promises are resolved, and will be rejected as soon as one of the multiplexed promises is rejected (it behaves like Python's ``all()``, but with promise objects instead of boolean-ish). @@ -195,8 +192,8 @@ unwieldy. But :js:func:`~Deferred.then` also allows handling this kind of chains: it returns a new promise object, not the one it was called -with, and the return values of the callbacks is actually important to -it: whichever callback is called, +with, and the return values of the callbacks is important to this behavior: +whichever callback is called, * If the callback is not set (not provided or left to null), the resolution or rejection value(s) is simply forwarded to @@ -239,7 +236,6 @@ promise-based APIs, in order to filter their resolution value counts for instance (to take advantage of :js:func:`when` 's special treatment of single-value promises). - jQuery.Deferred API ~~~~~~~~~~~~~~~~~~~ @@ -267,9 +263,7 @@ jQuery.Deferred API chain. :param doneCallback: function called when the deferred is resolved - :type doneCallback: Function :param failCallback: function called when the deferred is rejected - :type failCallback: Function :returns: the deferred object on which it was called :rtype: :js:class:`Deferred` @@ -278,6 +272,9 @@ jQuery.Deferred API Attaches a new success callback to the deferred, shortcut for ``deferred.then(doneCallback)``. + .. note:: a difference is the result of :js:func:`Deferred.done`'s + is ignored rather than forwarded through the chain + This is a jQuery extension to `CommonJS Promises/A`_ providing little value over calling :js:func:`~Deferred.then` directly, it should be avoided. diff --git a/doc/reference/http.rst b/doc/reference/http.rst index e36ddf9e713..9f83a303d92 100644 --- a/doc/reference/http.rst +++ b/doc/reference/http.rst @@ -2,6 +2,8 @@ Web Controllers =============== +.. _reference/http/routing: + Routing ======= diff --git a/doc/reference/images/runner.png b/doc/reference/images/runner.png new file mode 100644 index 0000000000000000000000000000000000000000..bd48e9d29221767295afe05d2722ff332c93ab00 GIT binary patch literal 3963 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 C-5}fm literal 0 HcmV?d00001 diff --git a/doc/reference/images/tests.png b/doc/reference/images/tests.png new file mode 100644 index 0000000000000000000000000000000000000000..84083d9e5ed7583da33261259fb1f4e84d46a650 GIT binary patch literal 65388 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%0DZOjvNOog+{rl3Xfb#C5*7QgCjgOz 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 Wnj<@$ntjj@VI)Lkzf}n7`~4p&g3;9g literal 0 HcmV?d00001 diff --git a/doc/reference/images/tests2.png b/doc/reference/images/tests2.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a6f8ae9eefbeff46a9b45e3a9948676d820714 GIT binary patch literal 20114 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 + * Backbone-compatible shortcuts DOM Root -------- -A :class:`~openerp.Widget` is responsible for a section of the -page materialized by the DOM root of the widget. +A :class:`~openerp.Widget` is responsible for a section of the page +materialized by the DOM root of the widget. A widget's DOM root is available via two attributes: @@ -129,8 +106,8 @@ A widget's lifecycle has 3 main phases: initialization method of widgets, synchronous, can be overridden to take more parameters from the widget's creator/parent - :param parent: the current widget's parent, used to handle automatic - destruction and even propagation. Can be ``null`` for + :param parent: the new widget's parent, used to handle automatic + destruction and event propagation. Can be ``null`` for the widget to have no parent. :type parent: :class:`~openerp.Widget` @@ -157,14 +134,14 @@ A widget's lifecycle has 3 main phases: uses `.insertBefore()`_ All of these methods accept whatever the corresponding jQuery method accepts - (CSS selectors, DOM nodes or jQuery objects). They all return a promise and - are charged with three tasks: + (CSS selectors, DOM nodes or jQuery objects). They all return a deferred_ + and are charged with three tasks: - * render the widget's root element via + * rendering the widget's root element via :func:`~openerp.Widget.renderElement` - * insert the widget's root element in the DOM using whichever jQuery method - they match - * start the widget, and return the result of starting it + * inserting the widget's root element in the DOM using whichever jQuery + method they match + * starting the widget, and returning the result of starting it .. function:: openerp.Widget.start() @@ -192,8 +169,7 @@ A widget's lifecycle has 3 main phases: A widget being destroyed is automatically unlinked from its parent. -Because a widget can be destroyed at any time, widgets also have utility -methods to handle this case: +Related to widget destruction is an important utility method: .. function:: openerp.Widget.alive(deferred[, reject=false]) @@ -231,16 +207,13 @@ methods to handle this case: Accessing DOM content ''''''''''''''''''''' -Because a widget is only responsible for the content below its DOM -root, there is a shortcut for selecting sub-sections of a widget's -DOM: +Because a widget is only responsible for the content below its DOM root, there + is a shortcut for selecting sub-sections of a widget's DOM: .. function:: openerp.Widget.$(selector) Applies the CSS selector specified as parameter to the widget's - DOM root. - - :: + DOM root:: this.$(selector); @@ -251,8 +224,7 @@ DOM: :param String selector: CSS selector :returns: jQuery object - .. note:: this helper method is compatible with - ``Backbone.View.$`` + .. note:: this helper method is similar to ``Backbone.View.$`` Resetting the DOM root '''''''''''''''''''''' @@ -275,11 +247,11 @@ DOM events handling A widget will generally need to respond to user action within its section of the page. This entails binding events to DOM elements. -To this end, :class:`~openerp.Widget` provides an shortcut: +To this end, :class:`~openerp.Widget` provides a shortcut: .. attribute:: openerp.Widget.events - Events are a mapping of ``event selector`` (an event name and a + Events are a mapping of an event selector (an event name and an optional CSS selector separated by a space) to a callback. The callback can be the name of a widget's method or a function object. In either case, the ``this`` will be set to the widget:: @@ -299,21 +271,18 @@ To this end, :class:`~openerp.Widget` provides an shortcut: .. function:: openerp.Widget.delegateEvents - This method is in charge of binding - :attr:`~openerp.Widget.events` to the DOM. It is - automatically called after setting the widget's DOM root. + This method is in charge of binding :attr:`~openerp.Widget.events` to the + DOM. It is automatically called after setting the widget's DOM root. It can be overridden to set up more complex events than the - :attr:`~openerp.Widget.events` map allows, but the parent - should always be called (or :attr:`~openerp.Widget.events` - won't be handled correctly). + :attr:`~openerp.Widget.events` map allows, but the parent should always be + called (or :attr:`~openerp.Widget.events` won't be handled correctly). .. function:: openerp.Widget.undelegateEvents - This method is in charge of unbinding - :attr:`~openerp.Widget.events` from the DOM root when the - widget is destroyed or the DOM root is reset, in order to avoid - leaving "phantom" events. + This method is in charge of unbinding :attr:`~openerp.Widget.events` from + the DOM root when the widget is destroyed or the DOM root is reset, in + order to avoid leaving "phantom" events. It should be overridden to un-set any event set in an override of :func:`~openerp.Widget.delegateEvents`. @@ -407,10 +376,873 @@ destroy all widget data. RPC === +To display and interact with data, calls to the Odoo server are necessary. +This is performed using :abbr:`RPC `. + +Odoo Web provides two primary APIs to handle this: a low-level +JSON-RPC based API communicating with the Python section of Odoo +Web (and of your module, if you have a Python part) and a high-level +API above that allowing your code to talk directly to high-level Odoo models. + +All networking APIs are :ref:`asynchronous `. As a result, +all of them will return Deferred_ objects (whether they resolve those with +values or not). Understanding how those work before before moving on is +probably necessary. + +High-level API: calling into Odoo models +------------------------------------------- + +Access to Odoo object methods (made available through XML-RPC from the server) +is done via :class:`openerp.Model`. It maps onto the Odoo server objects via +two primary methods, :func:`~openerp.Model.call` and +:func:`~openerp.Model.query`. + +:func:`~openerp.Model.call` is a direct mapping to the corresponding method of +the Odoo server object. Its usage is similar to that of the Odoo Model API, +with three differences: + +* The interface is :ref:`asynchronous `, so instead of + returning results directly RPC method calls will return + Deferred_ instances, which will themselves resolve to the + result of the matching RPC call. + +* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to + ``__getattr__`` or ``method_missing``, there needs to be an explicit + method to dispatch RPC methods. + +* No notion of pooler, the model proxy is instantiated where needed, + not fetched from an other (somewhat global) object:: + + var Users = new openerp.Model('res.users'); + + Users.call('change_password', ['oldpassword', 'newpassword'], + {context: some_context}).then(function (result) { + // do something with change_password result + }); + +:func:`~openerp.Model.query` is a shortcut for a builder-style +interface to searches (``search`` + ``read`` in Odoo RPC terms). It +returns a :class:`~openerp.web.Query` object which is immutable but +allows building new :class:`~openerp.web.Query` instances from the +first one, adding new properties or modifiying the parent object's:: + + Users.query(['name', 'login', 'user_email', 'signature']) + .filter([['active', '=', true], ['company_id', '=', main_company]]) + .limit(15) + .all().then(function (users) { + // do work with users records + }); + +The query is only actually performed when calling one of the query +serialization methods, :func:`~openerp.web.Query.all` and +:func:`~openerp.web.Query.first`. These methods will perform a new +RPC call every time they are called. + +For that reason, it's actually possible to keep "intermediate" queries +around and use them differently/add new specifications on them. + +.. class:: openerp.Model(name) + + .. attribute:: openerp.Model.name + + name of the OpenERP model this object is bound to + + .. function:: openerp.Model.call(method[, args][, kwargs]) + + Calls the ``method`` method of the current model, with the + provided positional and keyword arguments. + + :param String method: method to call over rpc on the + :attr:`~openerp.Model.name` + :param Array<> args: positional arguments to pass to the + method, optional + :param Object<> kwargs: keyword arguments to pass to the + method, optional + :rtype: Deferred<> + + .. function:: openerp.Model.query(fields) + + :param Array fields: list of fields to fetch during + the search + :returns: a :class:`~openerp.web.Query` object + representing the search to perform + +.. class:: openerp.web.Query(fields) + + The first set of methods is the "fetching" methods. They perform + RPC queries using the internal data of the object they're called + on. + + .. function:: openerp.web.Query.all() + + Fetches the result of the current :class:`~openerp.web.Query` object's + search. + + :rtype: Deferred> + + .. function:: openerp.web.Query.first() + + Fetches the **first** result of the current + :class:`~openerp.web.Query`, or ``null`` if the current + :class:`~openerp.web.Query` does have any result. + + :rtype: Deferred + + .. function:: openerp.web.Query.count() + + Fetches the number of records the current + :class:`~openerp.web.Query` would retrieve. + + :rtype: Deferred + + .. function:: openerp.web.Query.group_by(grouping...) + + Fetches the groups for the query, using the first specified + grouping parameter + + :param Array grouping: Lists the levels of grouping + asked of the server. Grouping + can actually be an array or + varargs. + :rtype: Deferred> | null + + The second set of methods is the "mutator" methods, they create a + **new** :class:`~openerp.web.Query` object with the relevant + (internal) attribute either augmented or replaced. + + .. function:: openerp.web.Query.context(ctx) + + Adds the provided ``ctx`` to the query, on top of any existing + context + + .. function:: openerp.web.Query.filter(domain) + + Adds the provided domain to the query, this domain is + ``AND``-ed to the existing query domain. + + .. function:: opeenrp.web.Query.offset(offset) + + Sets the provided offset on the query. The new offset + *replaces* the old one. + + .. function:: openerp.web.Query.limit(limit) + + Sets the provided limit on the query. The new limit *replaces* + the old one. + + .. function:: openerp.web.Query.order_by(fields…) + + Overrides the model's natural order with the provided field + specifications. Behaves much like Django's :py:meth:`QuerySet.order_by + `: + + * Takes 1..n field names, in order of most to least importance + (the first field is the first sorting key). Fields are + provided as strings. + + * A field specifies an ascending order, unless it is prefixed + with the minus sign "``-``" in which case the field is used + in the descending order + + Divergences from Django's sorting include a lack of random sort + (``?`` field) and the inability to "drill down" into relations + for sorting. + +Aggregation (grouping) +'''''''''''''''''''''' + +Odoo has powerful grouping capacities, but they are kind-of strange +in that they're recursive, and level n+1 relies on data provided +directly by the grouping at level n. As a result, while +:py:meth:`openerp.models.Model.read_group` works it's not a very intuitive +API. + +Odoo Web eschews direct calls to :py:meth:`~openerp.models.Model.read_group` +in favor of calling a method of :class:`~openerp.web.Query`, :py:meth:`much +in the way it is one in SQLAlchemy ` +[#terminal]_:: + + some_query.group_by(['field1', 'field2']).then(function (groups) { + // do things with the fetched groups + }); + +This method is asynchronous when provided with 1..n fields (to group +on) as argument, but it can also be called without any field (empty +fields collection or nothing at all). In this case, instead of +returning a Deferred object it will return ``null``. + +When grouping criterion come from a third-party and may or may not +list fields (e.g. could be an empty list), this provides two ways to +test the presence of actual subgroups (versus the need to perform a +regular query for records): + +* A check on ``group_by``'s result and two completely separate code + paths:: + + var groups; + if (groups = some_query.group_by(gby)) { + groups.then(function (gs) { + // groups + }); + } + // no groups + +* Or a more coherent code path using :func:`when`'s ability to + coerce values into deferreds:: + + $.when(some_query.group_by(gby)).then(function (groups) { + if (!groups) { + // No grouping + } else { + // grouping, even if there are no groups (groups + // itself could be an empty array) + } + }); + +The result of a (successful) :func:`~openerp.web.Query.group_by` is +an array of :class:`~openerp.web.QueryGroup`: + +.. class:: openerp.web.QueryGroup + + .. function:: openerp.web.QueryGroup.get(key) + + returns the group's attribute ``key``. Known attributes are: + + ``grouped_on`` + which grouping field resulted from this group + ``value`` + ``grouped_on``'s value for this group + ``length`` + the number of records in the group + ``aggregates`` + a {field: value} mapping of aggregations for the group + + .. function:: openerp.web.QueryGroup.query([fields...]) + + equivalent to :func:`openerp.web.Model.query` but pre-filtered to + only include the records within this group. Returns a + :class:`~openerp.web.Query` which can be further manipulated as + usual. + + .. function:: openerp.web.QueryGroup.subgroups() + + returns a deferred to an array of :class:`~openerp.web.QueryGroup` + below this one + +Low-level API: RPC calls to Python side +--------------------------------------- + +While the previous section is great for calling core OpenERP code +(models code), it does not work if you want to call the Python side of +Odoo Web. + +For this, a lower-level API exists on on +:class:`~openerp.web.Session` objects (usually available through +``openerp.session``): the ``rpc`` method. + +This method simply takes an absolute path (the absolute URL of the JSON +:ref:`route ` to call) and a mapping of attributes to +values (passed as keyword arguments to the Python method). This function +fetches the return value of the Python methods, converted to JSON. + +For instance, to call the ``resequence`` of the +:class:`~web.controllers.main.DataSet` controller:: + + openerp.session.rpc('/web/dataset/resequence', { + model: some_model, + ids: array_of_ids, + offset: 42 + }).then(function (result) { + // resequence didn't error out + }, function () { + // an error occured during during call + }); + .. _reference/javascript/client: Web Client ========== +Testing in Odoo Web Client +========================== + +Javascript Unit Testing +----------------------- + +Odoo Web includes means to unit-test both the core code of +Odoo Web and your own javascript modules. On the javascript side, +unit-testing is based on QUnit_ with a number of helpers and +extensions for better integration with Odoo. + +To see what the runner looks like, find (or start) an Odoo server +with the web client enabled, and navigate to ``/web/tests`` +This will show the runner selector, which lists all modules with javascript +unit tests, and allows starting any of them (or all javascript tests in all +modules at once). + +.. image:: ./images/runner.png + :align: center + +Clicking any runner button will launch the corresponding tests in the +bundled QUnit_ runner: + +.. image:: ./images/tests.png + :align: center + +Writing a test case +------------------- + +The first step is to list the test file(s). This is done through the +``test`` key of the Odoo manifest, by adding javascript files to it: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'test': ['static/test/demo.js'], + } + +and to create the corresponding test file(s) + +.. note:: + + Test files which do not exist will be ignored, if all test files + of a module are ignored (can not be found), the test runner will + consider that the module has no javascript tests. + +After that, refreshing the runner selector will display the new module +and allow running all of its (0 so far) tests: + +.. image:: ./images/runner2.png + :align: center + +The next step is to create a test case:: + + openerp.testing.section('basic section', function (test) { + test('my first test', function () { + ok(false, "this test has run"); + }); + }); + +All testing helpers and structures live in the ``openerp.testing`` +module. Odoo tests live in a :func:`~openerp.testing.section`, +which is itself part of a module. The first argument to a section is +the name of the section, the second one is the section body. + +:func:`test `, provided by the +:func:`~openerp.testing.section` to the callback, is used to +register a given test case which will be run whenever the test runner +actually does its job. Odoo Web test case use standard `QUnit +assertions`_ within them. + +Launching the test runner at this point will run the test and display +the corresponding assertion message, with red colors indicating the +test failed: + +.. image:: ./images/tests2.png + :align: center + +Fixing the test (by replacing ``false`` to ``true`` in the assertion) +will make it pass: + +.. image:: ./images/tests3.png + :align: center + +Assertions +---------- + +As noted above, Odoo Web's tests use `qunit assertions`_. They are +available globally (so they can just be called without references to +anything). The following list is available: + +.. function:: ok(state[, message]) + + checks that ``state`` is truthy (in the javascript sense) + +.. function:: strictEqual(actual, expected[, message]) + + checks that the actual (produced by a method being tested) and + expected values are identical (roughly equivalent to ``ok(actual + === expected, message)``) + +.. function:: notStrictEqual(actual, expected[, message]) + + checks that the actual and expected values are *not* identical + (roughly equivalent to ``ok(actual !== expected, message)``) + +.. function:: deepEqual(actual, expected[, message]) + + deep comparison between actual and expected: recurse into + containers (objects and arrays) to ensure that they have the same + keys/number of elements, and the values match. + +.. function:: notDeepEqual(actual, expected[, message]) + + inverse operation to :func:`deepEqual` + +.. function:: throws(block[, expected][, message]) + + checks that, when called, the ``block`` throws an + error. Optionally validates that error against ``expected``. + + :param Function block: + :param expected: if a regexp, checks that the thrown error's + message matches the regular expression. If an + error type, checks that the thrown error is of + that type. + :type expected: Error | RegExp + +.. function:: equal(actual, expected[, message]) + + checks that ``actual`` and ``expected`` are loosely equal, using + the ``==`` operator and its coercion rules. + +.. function:: notEqual(actual, expected[, message]) + + inverse operation to :func:`equal` + +Getting an Odoo instance +------------------------ + +The Odoo instance is the base through which most Odoo Web +modules behaviors (functions, objects, …) are accessed. As a result, +the test framework automatically builds one, and loads the module +being tested and all of its dependencies inside it. This new instance +is provided as the first positional parameter to your test +cases. Let's observe by adding javascript code (not test code) to the +test module: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'js': ['static/src/js/demo.js'], + 'test': ['static/test/demo.js'], + } + +:: + + // src/js/demo.js + openerp.web_tests_demo = function (instance) { + instance.web_tests_demo = { + value_true: true, + SomeType: instance.web.Class.extend({ + init: function (value) { + this.value = value; + } + }) + }; + }; + +and then adding a new test case, which simply checks that the +``instance`` contains all the expected stuff we created in the +module:: + + // test/demo.js + test('module content', function (instance) { + ok(instance.web_tests_demo.value_true, "should have a true value"); + var type_instance = new instance.web_tests_demo.SomeType(42); + strictEqual(type_instance.value, 42, "should have provided value"); + }); + +DOM Scratchpad +-------------- + +As in the wider client, arbitrarily accessing document content is +strongly discouraged during tests. But DOM access is still needed to +e.g. fully initialize :class:`widgets <~openerp.Widget>` before +testing them. + +Thus, a test case gets a DOM scratchpad as its second positional +parameter, in a jQuery instance. That scratchpad is fully cleaned up +before each test, and as long as it doesn't do anything outside the +scratchpad your code can do whatever it wants:: + + // test/demo.js + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('
ok
'); + ok($scratchpad.find('span').hasClass('foo'), + "should have provided class"); + }); + test('clean scratchpad', function (instance, $scratchpad) { + ok(!$scratchpad.children().length, "should have no content"); + ok(!$scratchpad.text(), "should have no text"); + }); + +.. note:: + + The top-level element of the scratchpad is not cleaned up, test + cases can add text or DOM children but shoud not alter + ``$scratchpad`` itself. + +Loading templates +----------------- + +To avoid the corresponding processing costs, by default templates are +not loaded into QWeb. If you need to render e.g. widgets making use of +QWeb templates, you can request their loading through the +:attr:`~TestOptions.templates` option to the :func:`test case +function `. + +This will automatically load all relevant templates in the instance's +qweb before running the test case: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'js': ['static/src/js/demo.js'], + 'test': ['static/test/demo.js'], + 'qweb': ['static/src/xml/demo.xml'], + } + +.. code-block:: xml + + + + + +

+
+
+
+ +:: + + // test/demo.js + test('templates', {templates: true}, function (instance) { + var s = instance.web.qweb.render('DemoTemplate'); + var texts = $(s).find('p').map(function () { + return $(this).text(); + }).get(); + + deepEqual(texts, ['0', '1', '2', '3', '4']); + }); + +Asynchronous cases +------------------ + +The test case examples so far are all synchronous, they execute from +the first to the last line and once the last line has executed the +test is done. But the web client is full of :ref:`asynchronous code +`, and thus test cases need to be async-aware. + +This is done by returning a :class:`deferred ` from the +case callback:: + + // test/demo.js + test('asynchronous', { + asserts: 1 + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.resolve(); + }, 100); + return d; + }); + +This example also uses the :class:`options parameter ` +to specify the number of assertions the case should expect, if less or +more assertions are specified the case will count as failed. + +Asynchronous test cases *must* specify the number of assertions they +will run. This allows more easily catching situations where e.g. the +test architecture was not warned about asynchronous operations. + +.. note:: + + Asynchronous test cases also have a 2 seconds timeout: if the test + does not finish within 2 seconds, it will be considered + failed. This pretty much always means the test will not + resolve. This timeout *only* applies to the test itself, not to + the setup and teardown processes. + +.. note:: + + If the returned deferred is rejected, the test will be failed + unless :attr:`~TestOptions.fail_on_rejection` is set to + ``false``. + +RPC +--- + +An important subset of asynchronous test cases is test cases which +need to perform (and chain, to an extent) RPC calls. + +.. note:: + + Because they are a subset of asynchronous cases, RPC cases must + also provide a valid :attr:`assertions count + `. + +To enable mock RPC, set the :attr:`rpc option ` to +``mock``. This will add a third parameter to the test case callback: + +.. function:: mock(rpc_spec, handler) + + Can be used in two different ways depending on the shape of the + first parameter: + + * If it matches the pattern ``model:method`` (if it contains a + colon, essentially) the call will set up the mocking of an RPC + call straight to the Odoo server (through XMLRPC) as + performed via e.g. :func:`openerp.web.Model.call`. + + In that case, ``handler`` should be a function taking two + arguments ``args`` and ``kwargs``, matching the corresponding + arguments on the server side and should simply return the value + as if it were returned by the Python XMLRPC handler:: + + test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { + // set up mocking + mock('people.famous:name_search', function (args, kwargs) { + strictEqual(kwargs.name, 'bob'); + return [ + [1, "Microsoft Bob"], + [2, "Bob the Builder"], + [3, "Silent Bob"] + ]; + }); + + // actual test code + return new instance.web.Model('people.famous') + .call('name_search', {name: 'bob'}).then(function (result) { + strictEqual(result.length, 3, "shoud return 3 people"); + strictEqual(result[0][1], "Microsoft Bob", + "the most famous bob should be Microsoft Bob"); + }); + }); + + * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it + will mock a JSON-RPC call to a web client controller, such as + ``/web/webclient/translations``. In that case, the handler takes + a single ``params`` argument holding all of the parameters + provided over JSON-RPC. + + As previously, the handler should simply return the result value + as if returned by the original JSON-RPC handler:: + + test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) { + var fetched_dbs = false, fetched_langs = false; + mock('/web/database/get_list', function () { + fetched_dbs = true; + return ['foo', 'bar', 'baz']; + }); + mock('/web/session/get_lang_list', function () { + fetched_langs = true; + return [['vo_IS', 'Hopelandic / Vonlenska']]; + }); + + // widget needs that or it blows up + instance.webclient = {toggle_bars: openerp.testing.noop}; + var dbm = new instance.web.DatabaseManager({}); + return dbm.appendTo($s).then(function () { + ok(fetched_dbs, "should have fetched databases"); + ok(fetched_langs, "should have fetched languages"); + deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); + }); + }); + +.. note:: + + Mock handlers can contain assertions, these assertions should be + part of the assertions count (and if multiple calls are made to a + handler containing assertions, it multiplies the effective number + of assertions). + +Testing API +----------- + +.. function:: openerp.testing.section(name[, options], body) + + A test section, serves as shared namespace for related tests (for + constants or values to only set up once). The ``body`` function + should contain the tests themselves. + + Note that the order in which tests are run is essentially + undefined, do *not* rely on it. + + :param String name: + :param TestOptions options: + :param body: + :type body: Function<:func:`~openerp.testing.case`, void> + +.. function:: openerp.testing.case(name[, options], callback) + + Registers a test case callback in the test runner, the callback + will only be run once the runner is started (or maybe not at all, + if the test is filtered out). + + :param String name: + :param TestOptions options: + :param callback: + :type callback: Function> + +.. class:: TestOptions + + the various options which can be passed to + :func:`~openerp.testing.section` or + :func:`~openerp.testing.case`. Except for + :attr:`~TestOptions.setup` and + :attr:`~TestOptions.teardown`, an option on + :func:`~openerp.testing.case` will overwrite the corresponding + option on :func:`~openerp.testing.section` so + e.g. :attr:`~TestOptions.rpc` can be set for a + :func:`~openerp.testing.section` and then differently set for + some :func:`~openerp.testing.case` of that + :func:`~openerp.testing.section` + + .. attribute:: TestOptions.asserts + + An integer, the number of assertions which should run during a + normal execution of the test. Mandatory for asynchronous tests. + + .. attribute:: TestOptions.setup + + Test case setup, run right before each test case. A section's + :func:`~TestOptions.setup` is run before the case's own, if + both are specified. + + .. attribute:: TestOptions.teardown + + Test case teardown, a case's :func:`~TestOptions.teardown` + is run before the corresponding section if both are present. + + .. attribute:: TestOptions.fail_on_rejection + + If the test is asynchronous and its resulting promise is + rejected, fail the test. Defaults to ``true``, set to + ``false`` to not fail the test in case of rejection:: + + // test/demo.js + test('unfail rejection', { + asserts: 1, + fail_on_rejection: false + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.reject(); + }, 100); + return d; + }); + + .. attribute:: TestOptions.rpc + + RPC method to use during tests, one of ``"mock"`` or + ``"rpc"``. Any other value will disable RPC for the test (if + they were enabled by the suite for instance). + + .. attribute:: TestOptions.templates + + Whether the current module (and its dependencies)'s templates + should be loaded into QWeb before starting the test. A + boolean, ``false`` by default. + +The test runner can also use two global configuration values set +directly on the ``window`` object: + +* ``oe_all_dependencies`` is an ``Array`` of all modules with a web + component, ordered by dependency (for a module ``A`` with + dependencies ``A'``, any module of ``A'`` must come before ``A`` in + the array) + +Running through Python +---------------------- + +The web client includes the means to run these tests on the +command-line (or in a CI system), but while actually running it is +pretty simple the setup of the pre-requisite parts has some +complexities. + +#. Install unittest2_ in your Python environment. Both + can trivially be installed via `pip `_ or + `easy_install + `_. + +#. Install PhantomJS_. It is a headless + browser which allows automating running and testing web + pages. QUnitSuite_ uses it to actually run the qunit_ test suite. + + The PhantomJS_ website provides pre-built binaries for some + platforms, and your OS's package management probably provides it as + well. + + If you're building PhantomJS_ from source, I recommend preparing + for some knitting time as it's not exactly fast (it needs to + compile both `Qt `_ and `Webkit + `_, both being pretty big projects). + + .. note:: + + Because PhantomJS_ is webkit-based, it will not be able to test + if Firefox, Opera or Internet Explorer can correctly run the + test suite (and it is only an approximation for Safari and + Chrome). It is therefore recommended to *also* run the test + suites in actual browsers once in a while. + + .. note:: + + The version of PhantomJS_ this was build through is 1.7, + previous versions *should* work but are not actually supported + (and tend to just segfault when something goes wrong in + PhantomJS_ itself so they're a pain to debug). + +#. Install a new database with all relevant modules (all modules with + a web component at least), then restart the server + + .. note:: + + For some tests, a source database needs to be duplicated. This + operation requires that there be no connection to the database + being duplicated, but Odoo doesn't currently break + existing/outstanding connections, so restarting the server is + the simplest way to ensure everything is in the right state. + +#. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct + addons-path specified (and replacing ``$DATABASE`` by the source + database you created above) + + .. note:: + + If you leave out ``-mweb``, the runner will attempt to run all + the tests in all the modules, which may or may not work. + +If everything went correctly, you should now see a list of tests with +(hopefully) ``ok`` next to their names, closing with a report of the +number of tests run and the time it took: + +.. literalinclude:: test-report.txt + :language: text + +Congratulation, you have just performed a successful "offline" run of +the OpenERP Web test suite. + +.. note:: + + Note that this runs all the Python tests for the ``web`` module, + but all the web tests for all of Odoo. This can be surprising. + +.. _qunit: http://qunitjs.com/ + +.. _qunit assertions: http://api.qunitjs.com/category/assert/ + +.. _unittest2: http://pypi.python.org/pypi/unittest2 + +.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/ + +.. _PhantomJS: http://phantomjs.org/ + .. [#eventsdelegation] not all DOM events are compatible with events delegation +.. [#terminal] + with a small twist: :py:meth:`sqlalchemy.orm.query.Query.group_by` is not + terminal, it returns a query which can still be altered. + diff --git a/addons/web/doc/test-report.txt b/doc/reference/test-report.txt similarity index 100% rename from addons/web/doc/test-report.txt rename to doc/reference/test-report.txt From 393331f9484d069e390d444d530171cc1b01f061 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Sep 2014 16:13:52 +0200 Subject: [PATCH 3/4] [FIX] sphinx 'project' value The project name automatically gets the release and the literal string "documentation" appended by default (and "html_title" can be set to generate a title differently), so having "documentation" set in the project variable duplicates it in the page title. --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 673b5f760cb..169c6b64a61 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -44,7 +44,7 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'odoo developer documentation' +project = u'odoo' copyright = u'2014, OpenERP s.a.' # The version info for the project you're documenting, acts as replacement for From 9cd2693286d6ab30576bc6973ecc19c82bdf5715 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Sep 2014 16:47:04 +0200 Subject: [PATCH 4/4] [FIX] JS tutorial --- addons/web/static/src/js/view_form.js | 10 +- addons/web/static/src/js/views.js | 10 +- doc/glossary.rst | 13 + doc/howtos/web.rst | 2180 ++++++++++++------------- doc/howtos/web/about_odoo.png | Bin 0 -> 8761 bytes doc/howtos/web/devmode.png | Bin 0 -> 8030 bytes doc/reference/actions.rst | 24 + doc/reference/javascript.rst | 33 + openerp/http.py | 2 +- 9 files changed, 1149 insertions(+), 1123 deletions(-) create mode 100644 doc/howtos/web/about_odoo.png create mode 100644 doc/howtos/web/devmode.png create mode 100644 doc/reference/actions.rst diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index ee15592c924..d2ebacfd8a6 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -42,15 +42,15 @@ instance.web.form.FieldManagerMixin = { Gives new values for the fields contained in the view. The new values could not be setted right after the call to this method. Setting new values can trigger on_changes. - @param (dict) values A dictonnary with key = field name and value = new value. - @return (Deferred) Is resolved after all the values are setted. + @param {Object} values A dictonary with key = field name and value = new value. + @return {$.Deferred} Is resolved after all the values are setted. */ set_values: function(values) {}, /** Computes an OpenERP domain. - @param (list) expression An OpenERP domain. - @return (boolean) The computed value of the domain. + @param {Array} expression An OpenERP domain. + @return {boolean} The computed value of the domain. */ compute_domain: function(expression) {}, /** @@ -58,7 +58,7 @@ instance.web.form.FieldManagerMixin = { the field are only supposed to use this context to evualuate their own, they should not extend it. - @return (CompoundContext) An OpenERP context. + @return {CompoundContext} An OpenERP context. */ build_eval_context: function() {}, }; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index e534f5e3d44..e62928ff904 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -1548,15 +1548,7 @@ instance.web.View = instance.web.Widget.extend({ do_switch_view: function() { this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments))); }, - /** - * Cancels the switch to the current view, switches to the previous one - * - * @param {Object} [options] - * @param {Boolean} [options.created=false] resource was created - * @param {String} [options.default=null] view to switch to if no previous view - */ - - do_search: function(view) { + do_search: function(domain, context, group_by) { }, on_sidebar_export: function() { new instance.web.DataExport(this, this.dataset).open(); diff --git a/doc/glossary.rst b/doc/glossary.rst index d22042b1bf7..278098a5923 100644 --- a/doc/glossary.rst +++ b/doc/glossary.rst @@ -20,4 +20,17 @@ evaluated, other content is interpreted as literal strings and displayed as-is + GIS + Geographic Information System + any computer system or subsystem to capture, store, manipulate, + analyze, manage or present spatial and geographical data. + + minified + minification + process of removing extraneous/non-necessary sections of files + (comments, whitespace) and possibly recompiling them using equivalent + but shorter structures (`ternary operator`_ instead of ``if/else``) in + order to reduce network traffic + .. _jinja variables: http://jinja.pocoo.org/docs/dev/templates/#variables +.. _ternary operator: http://en.wikipedia.org/wiki/%3F: diff --git a/doc/howtos/web.rst b/doc/howtos/web.rst index 0460a791d1d..98cec4a5d3f 100644 --- a/doc/howtos/web.rst +++ b/doc/howtos/web.rst @@ -6,8 +6,10 @@ Web Client .. default-domain:: js -This guide is about creating modules for Odoo's web client. To create websites -with Odoo, see :doc:`website`. +This guide is about creating modules for Odoo's web client. + +To create websites with Odoo, see :doc:`website`; to add business capabilities +or extend existing business systems of Odoo, see :doc:`backend`. .. warning:: @@ -17,27 +19,28 @@ with Odoo, see :doc:`website`. * jQuery_ * `Underscore.js`_ + It also requires an installed Odoo, and Git_. -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. +A Simple Module +=============== -To download the example module, use this bazaar command: +Let's start with a simple Odoo module holding basic web component +configuration and letting us test the web framework. -.. code-block:: sh +The example module is available online and can be downloaded using the +following command: - bzr branch lp:~niv-openerp/+junk/oepetstore -r 1 +.. code-block:: console -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``. + $ git clone http://github.com/odoo/petstore -Now let's see what files exist in that module: +This will create a ``petstore`` folder wherever you executed the command. +You then need to add that folder to Odoo's +:option:`addons path `, create a new database and +install the ``oepetstore`` module. + +If you browse the ``petstore`` folder, you should see the following content: .. code-block:: text @@ -56,169 +59,146 @@ Now let's see what files exist in that module: `-- 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. +The module already holds various server customizations. We'll come back to +these later, for now let's focus on the web-related content, in the ``static`` +folder. -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. +Files used in the "web" side of an Odoo module must be placed in a ``static`` +folder so they are available to a web browser, files outside that folder can +not be fetched by browsers. The ``src/css``, ``src/js`` and ``src/xml`` +sub-folders are conventional and not strictly necessary. -``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/css/petstore.css`` + currently empty, will hold the CSS_ for pet store content +``oepetstore/static/xml/petstore.xml`` + Mostly empty, will hold :ref:`reference/qweb` templates +``oepetstore/static/js/petstore.js`` + The most important (and interesting) part, contains the logic of the + application (or at least its web-browser side) as javascript. It should + currently look like:: -``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. + openerp.oepetstore = function(instance, local) { + var _t = openerp.web._t, + _lt = openerp.web._lt; + var QWeb = openerp.web.qweb; -``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:: + local.HomePage = instance.Widget.extend({ + start: function() { + console.log("pet store home page loaded"); + }, + }); - openerp.oepetstore = function(instance) { - var _t = instance.web._t, - _lt = instance.web._lt; - var QWeb = instance.web.qweb; + openerp.web.client_actions.add( + 'petstore.homepage', 'local.HomePage'); + } - 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. +Which only prints a small message in the browser's console. .. 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. + All JavaScript files are concatenated and :term:`minified` to improve + application load time. - 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. + One of the drawback is debugging becomes more difficult as + individual files disappear and the code is made significantly less + readable. It is possible to disable this process by enabling the + "developer mode": log into your Odoo instance (user *admin* password + *admin* by default) open the user menu (in the top-right corner of the + Odoo screen) and select :guilabel:`About Odoo` then :guilabel:`Activate + the developer mode`: - 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: + .. image:: web/about_odoo.png + :align: center - .. code-block:: text + .. image:: web/devmode.png + :align: center - http://localhost:8069/?debug + This will reload the web client with optimizations disabled, making + development and debugging significantly more comfortable. - 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. +.. todo:: qweb files hooked via __openerp__.py, but js and CSS use bundles 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. +Javascript doesn't have built-in modules. As a result variables defined in +different files are all mashed together and may conflict. This has given rise +to various module patterns used to build clean namespaces and limit risks of +naming conflicts. -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 Odoo framework uses one such pattern to define modules within web addons, +in order to both namespace code and correctly order its loading. -The ``oepetstore/static/js/petstore.js`` already declare such a module:: +``oepetstore/static/js/petstore.js`` contains a module declaration:: - openerp.oepetstore = function(instance) { - instance.oepetstore = {}; - - instance.oepetstore.xxx = ...; + openerp.oepetstore = function(instance, local) { + local.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). +In Odoo web, modules are declared as functions set on the global ``openerp`` +variable. The function's name must be the same as the addon (in this case +``oepetstore``) so the framework can find it, and automatically initialize it. -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. +When the web client decides to load your module, it'll call the root function +and provide two parameters: -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. +* the first parameter is the current instance of the Odoo web client, it gives + access to various capabilities defined by the Odoo (translations, + network services) as well as objects defined by the core or by other + modules. +* the second parameter is your own local namespace automatically created by + the web client. Objects and variables which should be accessible from + outside your module (either because the Odoo web client needs to call them + or because others may want to customize them) should be set inside that + namespace. 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 `_. +Much as modules, and contrary to most object-oriented languages, javascript +does not build in *classes*\ [#classes]_ although it provides roughly +equivalent (if lower-level and more verbose) mechanisms. -To define a new class, you need to extend the :class:`openerp.web.Class` -class:: +For simplicity and developer-friendliness purposes, Odoo web provides a class +system based on John Resig's `Simple JavaScript Inheritance`_. - instance.oepetstore.MyClass = instance.web.Class.extend({ +New classes are defined by calling the :func:`~openerp.web.Class.extend` +method of :class:`openerp.web.Class`:: + + var 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:: +The :func:`~openerp.web.Class.extend` method takes a dictionary describing +the new class's content (methods and static attributes). In this case, it will +only have a ``say_hello`` method which takes no parameters. - var my_object = new instance.oepetstore.MyClass(); +Classes are instantiated using the ``new`` operator:: + + var my_object = new MyClass(); my_object.say_hello(); // print "hello" in the console -You can access the attributes of a class inside a method using ``this``:: +And attributes of the instance can be accessed via ``this``:: - instance.oepetstore.MyClass = instance.web.Class.extend({ + var MyClass = instance.web.Class.extend({ say_hello: function() { console.log("hello", this.name); }, }); - var my_object = new instance.oepetstore.MyClass(); - my_object.name = "Nicolas"; + var my_object = new MyClass(); + my_object.name = "Bob"; my_object.say_hello(); - // print "hello Nicolas" in the console + // print "hello Bob" 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:: +Classes can provide an initializer to perform the initial setup of the +instance, by defining an ``init()`` method. The initializer receives the +parameters passed when using the ``new`` operator:: - instance.oepetstore.MyClass = instance.web.Class.extend({ + var MyClass = instance.web.Class.extend({ init: function(name) { this.name = name; }, @@ -227,156 +207,191 @@ pass parameters to the constructor like in most language:: }, }); - var my_object = new instance.oepetstore.MyClass("Nicolas"); + var my_object = new MyClass("Bob"); my_object.say_hello(); - // print "hello Nicolas" in the console + // print "hello Bob" 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`:: +It is also possible to create subclasses from existing (used-defined) classes +by calling :func:`~openerp.web.Class.extend` on the parent class, as is done +to subclass :class:`~openerp.web.Class`:: - instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({ + var MySpanishClass = MyClass.extend({ say_hello: function() { console.log("hola", this.name); }, }); - var my_object = new instance.oepetstore.MySpanishClass("Nicolas"); + var my_object = new MySpanishClass("Bob"); my_object.say_hello(); - // print "hola Nicolas" in the console + // print "hola Bob" 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:: +call the original method:: - instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({ + var MySpanishClass = MyClass.extend({ say_hello: function() { this._super(); console.log("translation in Spanish: hola", this.name); }, }); - var my_object = new instance.oepetstore.MySpanishClass("Nicolas"); + var my_object = new MySpanishClass("Bob"); my_object.say_hello(); - // print "hello Nicolas \n translation in Spanish: hola Nicolas" in the console + // print "hello Bob \n translation in Spanish: hola Bob" in the console + +.. warning:: + + ``_super`` is not a standard method, it is set on-the-fly to the next + method in the current inheritance chain, if any. It is only defined + during the *synchronous* part of a method call, for use in asynchronous + handlers (after network calls or in ``setTimeout`` callbacks) a reference + to its value should be retained, it should not be accessed via ``this``:: + + // broken, will generate an error + say_hello: function () { + setTimeout(function () { + this._super(); + }.bind(this), 0); + } + + // correct + say_hello: function () { + // don't forget .bind() + var _super = this._super.bind(this); + setTimeout(function () { + _super(); + }.bind(this), 0); + } 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. +The Odoo web client bundles jQuery_ for easy DOM manipulation. It is useful +and provides a better API than standard `W3C DOM`_\ [#dombugs]_, but +insufficient to structure complex applications leading to difficult +maintenance. + +Much like object-oriented desktop UI toolkits (e.g. Qt_, Cocoa_ or GTK_), +Odoo Web makes specific components responsible for sections of a page. In +Odoo web, the base for such components is the :class:`~openerp.Widget` +class, a component specialized in handling a page section and displaying +information for the user. Your First Widget -%%%%%%%%%%%%%%%%% +----------------- -The start module you installed already contains a small widget:: +The initial demonstration module already provides a basic widget:: - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.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. +It extends :class:`~openerp.Widget` and overrides the standard method +:func:`~openerp.Widget.start`, which — much like the previous ``MyClass`` +— does little for now. -You may also have noticed this line at the end of the file:: +This line at the end of the file:: - instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + 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. +registers our basic widget as a client action. Client actions will be +explained later in the guide, for now this is just what allows our widget to +be called and displayed when we select the +:menuselection:`Pet Store --> Pet Store --> Home Page` menu. + +.. warning:: + + because the widget will be called from outside our module, the web client + needs its "fully qualified" name, not the local version. 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. +Widgets have a number of methods and features, but the basics are simple: -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. +* set up a widget +* format the widget's data +* display the widget -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 -``
`` HTML element. +The ``HomePage`` widget already has a :func:`~openerp.Widget.start` +method. That method is part of the normal widget lifecycle and automatically +called once the widget is inserted in the page. We can use it to display some +content. -A ``
`` 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:: +All widgets have a :attr:`~openerp.Widget.$el` which represents the +section of page they're in charge of (as a jQuery_ object). Widget content +should be inserted there. By default, :attr:`~openerp.Widget.$el` is an +empty ``
`` element. - instance.oepetstore.HomePage = instance.web.Widget.extend({ +A ``
`` element is usually invisible for the user if it does not +have any content (or specific styles giving it a size) which is why nothing +is displayed on the page when ``HomePage`` is launched. + +Let's add some content to the widget's root element, using jQuery:: + + local.HomePage = instance.Widget.extend({ start: function() { this.$el.append("
Hello dear Odoo user!
"); }, }); -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). +That message will now appear when you open :menuselection:`Pet Store +--> Pet Store --> Home Page` -Now you should learn how to instantiate a widget and display its content. To -do so, we will create a new widget:: +.. note:: - instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ + to refresh the javascript code loaded in Odoo Web, you will need to reload + the page. There is no need to restart the Odoo server + +The ``HomePage`` widget is used by Odoo Web and managed automatically, to +learn how to use a widget "from scratch" let's create a new one:: + + local.GreetingsWidget = instance.Widget.extend({ start: function() { this.$el.append("
We are so happy to see you again in this menu!
"); }, }); -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``:: +We can now add our ``GreetingsWidget`` to the ``HomePage`` by using the +``GreetingsWidget``'s :func:`~openerp.Widget.appendTo` method:: - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.Widget.extend({ start: function() { this.$el.append("
Hello dear Odoo user!
"); - var greeting = new instance.oepetstore.GreetingsWidget(this); - greeting.appendTo(this.$el); + var greeting = new local.GreetingsWidget(this); + return 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. +* ``HomePage`` first adds its own content to its DOM root +* ``HomePage`` then instantiates ``GreetingsWidget`` +* Finally it tells ``GreetingsWidget`` where to insert itself, delegating part + of its :attr:`~openerp.Widget.$el` to the ``GreetingsWidget``. -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. +When the :func:`~openerp.Widget.appendTo` method is called, it asks the +widget to insert itself at the specified position and to display its content. +The :func:`~openerp.Widget.start` method will be called during the call +to :func:`~openerp.Widget.appendTo`. -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 ``
`` elements so we can clearly see them in the explorer:: +To see what happens under the displayed interface, we will use the browser's +DOM Explorer. But first let's alter our widgets slightly so we can more easily +find where they are, by :attr:`adding a class to their root elements +`:: - instance.oepetstore.HomePage = instance.web.Widget.extend({ - start: function() { - this.$el.addClass("oe_petstore_homepage"); - ... - }, + local.HomePage = instance.Widget.extend({ + className: 'oe_petstore_homepage', + ... }); - instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ - start: function() { - this.$el.addClass("oe_petstore_greetings"); - ... - }, + local.GreetingsWidget = instance.Widget.extend({ + className: 'oe_petstore_greetings', + ... }); -The result will be this if you can find the correct DOM part in the DOM explorer: +If you can find the relevant section of the DOM (right-click on the text +then :guilabel:`Inspect Element`), it should look like this: .. code-block:: html @@ -387,192 +402,201 @@ The result will be this if you can find the correct DOM part in the DOM explorer
-Here we can clearly see the two ``
`` 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 ``
`` element -which represents the ``GreetingsWidget`` instance is *inside* the ``
`` which represents the ``HomePage`` instance. +Which clearly shows the two ``
`` elements automatically created by +:class:`~openerp.Widget`, because we added some classes on them. + +We can also see the two message-holding divs we added ourselves + +Finally, note the ``
`` element which +represents the ``GreetingsWidget`` instance is *inside* the +``
`` which represents the ``HomePage`` +instance, since we appended Widget Parents and Children -%%%%%%%%%%%%%%%%%%%%%%%%%%% +--------------------------- In the previous part, we instantiated a widget using this syntax:: - new instance.oepetstore.GreetingsWidget(this); + new local.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. +instance. This tells the widget being created which other widget is its +*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*. +*inside* that other widget's root element. This means most widgets are "part" +of another widget, and exist on behalf of it. 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. +to know who is his parent and who are its children. -:func:`~openerp.web.Widget.getParent` can be used to get the parent of a -widget:: +:func:`~openerp.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 - }, - }); + local.GreetingsWidget = instance.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:: +:func:`~openerp.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 - }, - }); + local.HomePage = instance.Widget.extend({ + start: function() { + var greeting = new local.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()``:: +When overriding the :func:`~openerp.Widget.init` method of a widget it is +*of the utmost importance* to pass the parent to the ``this._super()`` call, +otherwise the relation will not be set up correctly:: - instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ + local.GreetingsWidget = instance.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:: +Finally, if a widget does not have a parent (e.g. because it's the root +widget of the application), ``null`` can be provided as parent:: - new instance.oepetstore.GreetingsWidget(null); + new local.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: +it. This is done via the :func:`~openerp.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. +:func:`~openerp.Widget.destroy` on all its children. Then it erases itself +from the DOM. If you have set up permanent structures in +:func:`~openerp.Widget.init` or :func:`~openerp.Widget.start` which +must be explicitly cleaned up (because the garbage collector will not handle +them), you can override :func:`~openerp.Widget.destroy`. -.. _howtos/web/qweb: +.. danger:: + + when overriding :func:`~openerp.Widget.destroy`, ``_super()`` + *must always* be called otherwise the widget and its children are not + correctly cleaned up leaving possible memory leaks and "phantom events", + even if no error is displayed 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:: +In the previous section we added content to our widgets by directly +manipulating (and adding to) their DOM:: this.$el.append("
Hello dear Odoo user!
"); -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... +This allows generating and displaying any type of content, but tends to +rapidly get unwieldy when generating significant amounts of DOM (lots of +duplication, quoting issues, ...) -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. +As many other environments, Odoo's solution is to use a `template engine`_. +Odoo's template engine is called :ref:`reference/qweb`. QWeb is an XML-based templating language, similar to `Genshi `_, `Thymeleaf `_ or `Facelets -`_ with a few peculiarities: +`_. It has the following +characteristics: -* 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 +* It's implemented fully in JavaScript and rendered in the browser +* Each template file (XML files) contains multiple templates +* It has special support in Odoo Web's :class:`~openerp.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). + :class:`~openerp.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. +.. note:: -Using QWeb inside a Widget -%%%%%%%%%%%%%%%%%%%%%%%%%% + The rationale behind using QWeb instead of existing javascript template + engines is the extensibility of pre-existing (third-party) templates, much + like Odoo :ref:`views `. -First let's define a simple QWeb template in -``oepetstore/static/src/xml/petstore.xml`` file, the exact meaning will be -explained later: + Most javascript template engines are text-based which precludes easy + structural extensibility where an XML-based templating engine can be + generically altered using e.g. XPath or CSS and a tree-alteration DSL (or + even just XSLT). This flexibility and extensibility is a core + characteristic of Odoo, and losting it was considered unacceptable. + +Using QWeb +---------- + +First let's define a simple QWeb template in the almost-empty +``oepetstore/static/src/xml/petstore.xml`` file: .. code-block:: xml -
This is some simple HTML
-Now let's modify the ``HomePage`` class. Remember that enigmatic line at the -beginning the the JavaScript source file? +Now we can use this template inside of the ``HomePage`` widget. Using the +``QWeb`` loader variable defined at the top of the page, we can call to the +template defined in the XML 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({ + local.HomePage = instance.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. +:func:`QWeb.render` looks for the specified template, renders it to a string +and returns the result. -Another possibility commonly seen in Odoo code is to use ``Widget``'s -integration with QWeb:: +However, because :class:`~openerp.Widget` has special integration for QWeb +the template can be set directly on the widget via its +:attr:`~openerp.Widget.template` attribute:: - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.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. +Although the result look similar, there are two differences between these +usages: -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. +* with the second version, the template is rendered right before + :func:`~openerp.Widget.start` is called +* in the first version the template's content is added to the widget's root + element, whereas in the second version the template's root element is + directly *set as* the widget's root element. Which is why the "greetings" + sub-widget also gets a red background + +.. warning:: + + templates should have a single non-``t`` root element, especially if + they're set as a widget's :attr:`~openerp.Widget.template`. If there are + multiple "root elements", results are undefined (usually only the first + root element will be used and the others will be ignored) 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()``: +QWeb templates can be given data and can contain basic display logic. + +For explicit calls to :func:`QWeb.render`, the template data is passed as +second parameter:: + + QWeb.render("HomePageTemplate", {name: "Klaus"}); + +with the template modified to: .. code-block:: xml @@ -580,19 +604,17 @@ second argument to ``QWeb.render()``:
Hello
-:: - - QWeb.render("HomePageTemplate", {name: "Nicolas"}); - -Result: +will result in: .. code-block:: html -
Hello Nicolas
+
Hello Klaus
-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: +When using :class:`~openerp.Widget`'s integration it is not possible to +provide additional data to the template. The template will be given a single +``widget`` context variable, referencing the widget being rendered right +before :func:`~openerp.Widget.start` is called (the widget's state will +essentially be that set up by :func:`~openerp.Widget.init`): .. code-block:: xml @@ -602,11 +624,11 @@ additional data to the template. Instead the template will have a unique :: - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.Widget.extend({ template: "HomePageTemplate", init: function(parent) { this._super(parent); - this.name = "Nicolas"; + this.name = "Mordecai"; }, start: function() { }, @@ -616,17 +638,21 @@ Result: .. code-block:: html -
Hello Nicolas
+
Hello Mordecai
Template Declaration '''''''''''''''''''' -Now that we know everything about rendering templates we can try to understand -QWeb's syntax. +We've seen how to *render* QWeb templates, let's now see the syntax of +the templates themselves. -All QWeb directives use XML attributes beginning with the prefix ``t-``. To -declare new templates, we add a ```` element into the XML -template file inside the root element ````:: +A QWeb template is composed of regular XML mixed with QWeb *directives*. A +QWeb directive is declared with XML attributes starting with ``t-``. + +The most basic directive is ``t-name``, used to declare new templates in +a template file: + +.. code-block:: xml @@ -634,100 +660,155 @@ template file inside the root element ````:: -``t-name`` simply declares a template that can be called using -``QWeb.render()``. +``t-name`` takes the name of the template being defined, and declares that +it can be called using :func:`QWeb.render`. It can only be used at the +top-level of a template file. Escaping '''''''' -To put some text in the HTML, use ``t-esc``: +The ``t-esc`` directive can be used to output text: .. code-block:: xml - -
Hello
-
+
Hello
- -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: +It takes a Javascript expression which is evaluated, the result of the +expression is then HTML-escaped and inserted in the document. Since it's an +expression it's possible to provide just a variable name as above, or a more +complex expression like a computation: .. code-block:: xml - -
-
+
-Will render: +or method calls: -.. code-block:: html +.. code-block:: xml -
8
+
Outputting HTML ''''''''''''''' -If you know you have some HTML contained in a variable, use ``t-raw`` instead -of ``t-esc``: +To inject HTML in the page being rendered, use ``t-raw``. Like ``t-esc`` it +takes an arbitrary Javascript expression as parameter, but it does not +perform an HTML-escape step. .. code-block:: xml - -
-
+
-If -'' +.. danger:: -The basic alternative block of QWeb is ``t-if``: + ``t-raw`` *must not* be used on any data which may contain non-escaped + user-provided content as this leads to `cross-site scripting`_ + vulnerabilities + +Conditionals +'''''''''''' + +QWeb can have conditional blocks using ``t-if``. The directive takes an +arbitrary expression, if the expression is falsy (``false``, ``null``, ``0`` +or an empty string) the whole block is suppressed, otherwise it is displayed. .. code-block:: xml - -
- - true is true - - - true is not true - +
+ + true is true + + + true is not true + +
+ +.. note:: + + QWeb doesn't have an "else" structure, use a second ``t-if`` with the + original condition inverted. You may want to store the condition in a + local variable if it's a complex or expensive expression. + +Iteration +''''''''' + +To iterate on a list, use ``t-foreach`` and ``t-as``. ``t-foreach`` takes an +expression returning a list to iterate on ``t-as`` takes a variable name to +bind to each item during iteration. + +.. code-block:: xml + +
+ +
+ Hello +
+
+
+ +.. note:: ``t-foreach`` can also be used with numbers and objects + (dictionaries) + +Defining attributes +''''''''''''''''''' + +QWeb provides two related directives to define computed attributes: +:samp:`t-att-{name}` and :samp:`t-attf-{name}`. In either case, *name* is the +name of the attribute to create (e.g. ``t-att-id`` defines the attribute +``id`` after rendering). + +``t-att-`` takes a javascript expression whose result is set as the +attribute's value, it is most useful if all of the attribute's value is +computed: + +.. code-block:: xml + +
+ Input your name: + +
+ +``t-attf-`` takes a *format string*. A format string is literal text with +interpolation blocks inside, an interpolation block is a javascript +expression between ``{{`` and ``}}``, which will be replaced by the result +of the expression. It is most useful for attributes which are partially +literal and partially computed such as a class: + +.. code-block:: xml + +
+ insert content here +
+ +Calling other templates +''''''''''''''''''''''' + +Templates can be split into sub-templates (for simplicity, maintainability, +reusability or to avoid excessive markup nesting). + +This is done using the ``t-call`` directive, which takes the name of the +template to render: + +.. code-block:: xml + + +
+
+ +
+ -Although QWeb does not contains any structure for else. - -Foreach -''''''' - -To iterate on a list, use ``t-foreach`` and ``t-as``: +rendering the ``A`` template will result in: .. code-block:: xml - -
- -
- Hello -
-
-
-
+
+
+
-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 - - -
- Input your name: - -
-
+Sub-templates inherit the rendering context of their caller. To Learn More About QWeb '''''''''''''''''''''''' @@ -739,37 +820,39 @@ 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. + Create a widget whose constructor takes two parameters aside from + ``parent``: ``product_names`` and ``color``. + + * ``product_names`` should an array of strings, each one the name of a + product + * ``color`` is a string containing a color in CSS color format (ie: + ``#000000`` for black). + + The 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 should use QWeb to render the HTML. Any + necessary CSS should be in ``oepetstore/static/src/css/petstore.css``. + + Use the widget in ``HomePage`` with half a dozen products. .. only:: solutions :: - openerp.oepetstore = function(instance) { + openerp.oepetstore = function(instance, local) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; - instance.oepetstore = {}; - - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.Widget.extend({ start: function() { - var products = new instance.oepetstore.ProductsWidget(this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00"); + var products = new local.ProductsWidget( + this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00"); products.appendTo(this.$el); }, }); - instance.oepetstore.ProductsWidget = instance.web.Widget.extend({ + local.ProductsWidget = instance.Widget.extend({ template: "ProductsWidget", init: function(parent, products, color) { this._super(parent); @@ -778,18 +861,22 @@ Exercise }, }); - instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + instance.web.client_actions.add( + 'petstore.homepage', 'instance.oepetstore.HomePage'); } .. code-block:: xml -
-
+ + + +
@@ -809,42 +896,139 @@ Exercise :align: center :width: 70% -Widget Events and Properties +Widget Helpers +============== + +``Widget``'s jQuery Selector ---------------------------- -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. +Selecting DOM elements within a widget can be performed by calling the +``find()`` method on the widget's DOM root:: -Events -%%%%%% + this.$el.find("input.my_input")... -Widgets are able to fire events in a similar way most components in existing -graphical user interfaces libraries (Qt, GTK, Swing,...) handle -them. Example:: +But because it's an extremely common operation, :class:`~openerp.Widget` +provides an equivalent shortcut through the :func:`~openerp.Widget.$` +method:: - instance.oepetstore.ConfirmWidget = instance.web.Widget.extend({ + local.MyWidget = instance.Widget.extend({ start: function() { - var self = this; - this.$el.append("
Are you sure you want to perform this action?
" + - "" + - ""); - 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); - }); + this.$("input.my_input")... }, }); - instance.oepetstore.HomePage = instance.web.Widget.extend({ +.. warning:: + + The global jQuery function ``$()`` should *never* be used unless it is + absolutely necessary: selection on a widget's root are scoped to the + widget and local to it, but selections with ``$()`` are global to the + page/application and may match parts of other widgets and views, leading + to odd or dangerous side-effects. Since a widget should generally act + only on the DOM section it owns, there is no cause for global selection. + +Easier DOM Events Binding +------------------------- + +We have previously bound DOM events using normal jQuery event handlers (e.g. +``.click()`` or ``.change()``) on widget elements:: + + local.MyWidget = instance.Widget.extend({ start: function() { - var widget = new instance.oepetstore.ConfirmWidget(this); - widget.on("user_choose", this, this.user_choose); + var self = this; + this.$(".my_button").click(function() { + self.button_clicked(); + }); + }, + button_clicked: function() { + .. + }, + }); + +While this works it has a few issues: + +1. it is rather verbose +2. it does not support replacing the widget's root element at runtime as + the binding is only performed when ``start()`` is run (during widget + initialization) +3. it requires dealing with ``this``-binding issues + +Widgets thus provide a shortcut to DOM event binding via +:attr:`~openerp.Widget.events`:: + + local.MyWidget = instance.Widget.extend({ + events: { + "click .my_button": "button_clicked", + }, + button_clicked: function() { + .. + } + }); + +:attr:`~openerp.Widget.events` is an object (mapping) of an event to the +function or method to call when the event is triggered: + +* the key is an event name, possibly refined with a CSS selector in which + case only if the event happens on a selected sub-element will the function + or method run: ``click`` will handle all clicks within the widget, but + ``click .my_button`` will only handle clicks in elements bearing the + ``my_button`` class +* the value is the action to perform when the event is triggered + + It can be either a function:: + + events: { + 'click': function (e) { /* code here */ } + } + + or the name of a method on the object (see example above). + + In either case, the ``this`` is the widget instance and the handler is given + a single parameter, the `jQuery event object`_ for the event. + +Widget Events and Properties +============================ + +Events +------ + +Widgets provide an event system (separate from the DOM/jQuery event system +described above): a widget can fire events on itself, and other widgets (or +itself) can bind themselves and listen for these events:: + + local.ConfirmWidget = instance.Widget.extend({ + events: { + 'click button.ok_button': function () { + this.trigger('user_chose', true); + }, + 'click button.cancel_button': function () { + this.trigger('user_chose', false); + } + }, + start: function() { + this.$el.append("
Are you sure you want to perform this action?
" + + "" + + ""); + }, + }); + +This widget acts as a facade, transforming user input (through DOM events) +into a documentable internal event to which parent widgets can bind +themselves. + +:func:`~openerp.Widget.trigger` takes the name of the event to trigger as +its first (mandatory) argument, any further arguments are treated as event +data and passed directly to listeners. + +We can then set up a parent event instantiating our generic widget and +listening to the ``user_chose`` event using :func:`~openerp.Widget.on`:: + + local.HomePage = instance.Widget.extend({ + start: function() { + var widget = new local.ConfirmWidget(this); + widget.on("user_chose", this, this.user_chose); widget.appendTo(this.$el); }, - user_choose: function(confirm) { + user_chose: function(confirm) { if (confirm) { console.log("The user agreed to continue"); } else { @@ -853,45 +1037,11 @@ them. Example:: }, }); -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 +:func:`~openerp.Widget.on` binds a function to be called when the +event identified by ``event_name`` is. 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 +related if it is a method. The bound function will be called with the +additional arguments of :func:`~openerp.Widget.trigger` if it has any. Example:: start: function() { @@ -904,12 +1054,19 @@ any. Example:: // will print "1 2 3" } -Properties -%%%%%%%%%% +.. note:: -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:: + Triggering events on an other widget is generally a bad idea. The main + exception to that rule is ``openerp.web.bus`` which exists specifically + to broadcasts evens in which any widget could be interested throughout + the Odoo web application. + +Properties +---------- + +Properties are very similar to normal object attributes in that they allow +storing data on a widget instance, however they have the additional feature +that they trigger events when set:: start: function() { this.widget = ... @@ -920,14 +1077,13 @@ property's value has changed:: 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. +* :func:`~openerp.Widget.set` sets the value of a property and triggers + :samp:`change:{propname}` (where *propname* is the property name passed as + first parameter to :func:`~openerp.Widget.set`) and ``change`` +* :func:`~openerp.Widget.get` retrieves the value of a property. Exercise -%%%%%%%% +-------- .. exercise:: Widget Properties and Events @@ -953,40 +1109,39 @@ Exercise :: - openerp.oepetstore = function(instance) { + openerp.oepetstore = function(instance, local) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; - instance.oepetstore = {}; - - instance.oepetstore.ColorInputWidget = instance.web.Widget.extend({ + local.ColorInputWidget = instance.Widget.extend({ template: "ColorInputWidget", + events: { + 'change input': 'input_changed' + }, start: function() { - var self = this; - this.$el.find("input").change(function() { - self.input_changed(); - }); - self.input_changed(); + this.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(); + var color = [ + "#", + this.$(".oe_color_red").val(), + this.$(".oe_color_green").val(), + this.$(".oe_color_blue").val() + ].join(''); this.set("color", color); }, }); - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.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); + this.colorInput = new local.ColorInputWidget(this) + .on("change:color", this, this.color_changed); + .appendTo(this.$el); }, color_changed: function() { - this.$el.find(".oe_color_div").css("background-color", this.colorInput.get("color")); + this.$(".oe_color_div").css("background-color", this.colorInput.get("color")); }, }); @@ -996,7 +1151,6 @@ Exercise .. code-block:: xml -
@@ -1020,157 +1174,11 @@ Exercise 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 ------------------------------------ +Modify existing 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:: +existing classes using the :func:`~openerp.web.Class.include` method:: var TestClass = instance.web.Class.extend({ testMethod: function() { @@ -1187,37 +1195,30 @@ class:: 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`. +This system is similar to the inheritance mechanism, except it will alter the +target class in-place instead of creating a new class. -.. 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. +In that case, ``this._super()`` will call the original implementation of a +method being replaced/redefined. 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 :func:`~openerp.web.Class.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.Widget.include`. 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: +``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:: +JavaScript module. They are used thus:: - this.$el.text(_t("Hello dear user!")); + this.$el.text(_t("Hello 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 @@ -1225,159 +1226,128 @@ 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. +``_t()`` will return the translation defined for the text it is given. If no +translation is defined for that text, it will return the original text as-is. -: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:: +.. note:: - var text_func = _lt("Hello dear user!"); - this.$el.text(text_func()); + To inject user-provided values in translatable strings, it is recommended + to use `_.str.sprintf + `_ with named + arguments *after* the translation:: -To have more information about Odoo's translations, please take a look at the -reference documentation: https://doc.openerp.com/contribute/translations/ . + this.$el.text(_.str.sprintf( + _t("Hello, %(user)s!"), { + user: "Ed" + })); + + This makes translatable strings more readable to translators, and gives + them more flexibility to reorder or ignore parameters. + +:func:`~openerp.web._lt` ("lazy translate") is similar but somewhat more +complex: instead of translating its parameter immediately, it returns +an object which, when converted to a string, will perform the translation. + +It is used to define translatable terms before the translations system is +initialized, for class attributes for instance (as modules are loaded before +the user's language is configured and translations are downloaded). 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. +Most operations with Odoo involve communicating with *models* implementing +business concern, these models will then (potentially) interact with some +storage engine (usually PostgreSQL_). -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. +Although jQuery_ provides a `$.ajax`_ function for network interactions, +communicating with Odoo requires additional metadata whose setup before every +call would be verbose and error-prone. As a result, Odoo web provides +higher-level communication primitives. 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" + class message_of_the_day(models.Model): + _name = "oepetstore.message_of_the_day" - def my_method(self, cr, uid, context=None): + @api.model + def my_method(self): return {"hello": "world"} - _columns = { - 'message': fields.text(string="Message"), - 'color': fields.char(string="Color", size=20), - } + message = fields.Text(), + color = fields.Char(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. +This declares a model with two fields, and a method ``my_method()`` which +returns a literal dictionary. Here is a sample widget that calls ``my_method()`` and displays the result:: - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.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) { + var model = new instance.web.Model("oepetstore.message_of_the_day"); + model.call("my_method", {context: new instance.web.CompoundContext()}).then(function(result) { self.$el.append("
Hello " + result["hello"] + "
"); // 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``.) +The class used to call Odoo models is :class:`openerp.Model`. It is +instantiated with the Odoo model's name as first parameter +(``oepetstore.message_of_the_day`` here). -: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: +:func:`~openerp.web.Model.call` can be used to call any (public) method of an +Odoo model. It takes the following positional 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: +``name`` + The name of the method to call, ``my_method`` here +``args`` + an array of `positional arguments`_ to provide to the method. Because the + example has no positional argument to provide, the ``args`` parameter is not + provided. + + Here is an other example with positional arguments: .. code-block:: python - def my_method2(self, cr, uid, a, b, c, context=None): ... + @api.model + def my_method2(self, a, b, c): ... .. 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: +``kwargs`` + a mapping of `keyword arguments`_ to pass. The example provides a single + named argument ``context``. .. code-block:: python - def my_method2(self, cr, uid, a, b, c, context=None): ... + @api.model + def my_method2(self, a, b, c): ... .. 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). +:func:`~openerp.Widget.call` returns a deferred resolved with the value +returned by the model's method as first argument. CompoundContext -%%%%%%%%%%%%%%% +--------------- -In the previous part, we avoided to explain the strange ``context`` argument -in the call to our model's method: +The previous section used a ``context`` argument which was not explained in +the method call:: -.. 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): ... + model.call("my_method", {context: new instance.web.CompoundContext()}) 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 @@ -1401,12 +1371,13 @@ 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'})}) + 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 + @api.model + def my_method(self): + print self.env.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 @@ -1414,21 +1385,16 @@ 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:: +While :func:`~openerp.Model.call` is sufficient for any interaction with Odoo +models, Odoo Web provides a helper for simpler and clearer querying of models +(fetching of records based on various conditions): +:func:`~openerp.Model.query` which acts as a shortcut for the common +combination of :py:meth:`~openerp.models.Model.search` and +::py:meth:`~openerp.models.Model.read`. It provides a clearer syntax to search +and read models:: model.query(['name', 'login', 'user_email', 'signature']) .filter([['active', '=', true], ['company_id', '=', main_company]]) @@ -1437,68 +1403,71 @@ is still a small helper that could be useful to you : // 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. +versus:: -: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`: + model.call('search', [['active', '=', true], ['company_id', '=', main_company]], {limit: 15}) + .then(function (ids) { + return model.call('read', [ids, ['name', 'login', 'user_email', 'signature']]); + }) + .then(function (users) { + // do work with users records + }); -* :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. +* :func:`~openerp.web.Model.query` takes an optional list of fields as + parameter (if no field is provided, all fields of the model are fetched). It + returns a :class:`openerp.web.Query` which can be further customized before + being executed +* :class:`~openerp.web.Query` represents the query being built. It is + immutable, methods to customize the query actually return a modified copy, + so it's possible to use the original and the new version side-by-side. See + :class:`~openerp.web.Query` for its customization options. -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). +When the query is set up as desired, simply call +:func:`~openerp.web.Query.all` to perform the actual query and return a +deferred to its result. The result is the same as +:py:meth:`~openerp.models.Model.read`'s, an array of dictionaries where each +dictionary is a requested record, with each requested field a dictionary key. 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. + Create a ``MessageOfTheDay`` widget displaying the last record of the + ``oepetstore.message_of_the_day`` model. The widget should fetch its + record as soon as it is displayed. + + Display the widget in the Pet Store home page. .. only:: solutions .. code-block:: javascript - openerp.oepetstore = function(instance) { + openerp.oepetstore = function(instance, local) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; - instance.oepetstore = {}; - - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.Widget.extend({ template: "HomePage", start: function() { - var motd = new instance.oepetstore.MessageOfTheDay(this); - motd.appendTo(this.$el); + return new local.MessageOfTheDay(this).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); - }, + local.MessageOfTheDay = instance.Widget.extend({ + template: "MessageOfTheDay", 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); - }); + return new instance.web.Model("oepetstore.message_of_the_day") + .query(["message"]) + .order_by('-create_date', '-id') + .first() + .then(function(result) { + self.$(".oe_mywidget_message_of_the_day").text(result.message); + }); }, }); @@ -1507,13 +1476,12 @@ Exercises .. code-block:: xml -
- +

@@ -1531,25 +1499,19 @@ Exercises .. exercise:: Pet Toys List - Create a widget ``PetToysList`` that will display 5 toys on the home page - with their names and their images. + Create a ``PetToysList`` widget displaying 5 toys (using their name 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. + The pet toys are not stored in a new model, instead they're stored in + ``product.product`` using a special category *Pet Toys*. You can see the + pre-generated toys and add new ones by going to + :menuselection:`Pet Store --> Pet Store --> Pet Toys`. You will probably + need to explore ``product.product`` in order to create the right domain to + select just pet toys. - 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 - - + In Odoo, images are generally stored in regular fields encoded as + base64_, HTML supports displaying images straight from base64 with + :samp:`` The ``PetToysList`` widget should be displayed on the home page on the right of the ``MessageOfTheDay`` widget. You will need to make some layout @@ -1559,52 +1521,50 @@ Exercises .. code-block:: javascript - openerp.oepetstore = function(instance) { + openerp.oepetstore = function(instance, local) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; - instance.oepetstore = {}; - - instance.oepetstore.HomePage = instance.web.Widget.extend({ + local.HomePage = instance.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")); - }, + start: function () { + return $.when( + new local.PetToysList(this).appendTo(this.$('.oe_petstore_homepage_left')), + new local.MessageOfTheDay(this).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() { + local.MessageOfTheDay = instance.Widget.extend({ + template: 'MessageOfTheDay', + 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); + return new instance.web.Model('oepetstore.message_of_the_day') + .query(["message"]) + .order_by('-create_date', '-id') + .first() + .then(function (result) { + self.$(".oe_mywidget_message_of_the_day").text(result.message); }); - }); - }, + } }); + local.PetToysList = instance.Widget.extend({ + template: 'PetToysList', + start: function () { + var self = this; + return new instance.web.Model('product.product') + .query(['name', 'image']) + .filter([['categ_id.name', '=', "Pet Toys"]]) + .limit(5) + .all() + .then(function (result) { + self.$el.append(QWeb.render('PetToys', {item: item})); + }); + } + }); } .. code-block:: xml @@ -1671,50 +1631,33 @@ Exercises 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*. +In Odoo, many operations start from an :ref:`action `: +opening a menu item (to a view), printing a report, ... + +Actions are pieces of data describing how a client should react to the +activation of a piece of content. Actions can be stored (and read through a +model) or they can be generated on-the fly (locally to the client by +javascript code, or remotely by a method of a model). + +In Odoo Web, the component responsible for handling and reacting to these +actions 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: +The action manager can be invoked explicitly from javascript code by creating +a dictionary describing :ref:`an action ` of the right +type, and calling an action manager instance with it. -.. code-block:: xml +:func:`~openerp.Widget.do_action` is a shortcut of :class:`~openerp.Widget` +looking up the "current" action manager and executing the action:: - - Message of the day - message_of_the_day - form - tree,form - - - - -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({ + instance.web.TestWidget = instance.Widget.extend({ dispatch_to_new_action: function() { this.do_action({ type: 'ir.actions.act_window', @@ -1727,91 +1670,105 @@ the action. Here is an example call to that method:: }, }); -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): +The most common action ``type`` is ``ir.actions.act_window`` which provides +views to a model (displays a model in various manners), its most common +attributes are: -* ``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. +``res_model`` + The model to display in views +``res_id`` (optional) + For form views, a preselected record in ``res_model`` +``views`` + Lists the views available through the action. A list of + ``[view_id, view_type]``, ``view_id`` can either be the database identifier + of a view of the right type, or ``false`` to use the view by default for + the specified type. View types can not be present multiple times. The action + will open the first view of the list by default. +``target`` + Either ``current`` (the default) which replaces the "content" section of the + web client by the action, or ``new`` to open the action in a dialog box. +``context`` + Additional context data to use within the action. .. 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. + Modify the ``PetToysList`` component so clicking on a toy replaces the + homepage by the toy's form view. .. only:: solutions .. code-block:: javascript - instance.oepetstore.PetToysList = instance.web.Widget.extend({ - template: "PetToysList", - start: function() { + local.PetToysList = instance.Widget.extend({ + template: 'PetToysList', + events: { + 'click .oe_petstore_pettoy': 'selected_item', + }, + 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); + return new instance.web.Model('product.product') + .query(['name', 'image']) + .filter([['categ_id.name', '=', "Pet Toys"]]) + .limit(5) + .all() + .then(function (results) { + _(results).each(function (item) { + self.$el.append(QWeb.render('PetToy', {item: item})); }); }); - }); }, - item_clicked: function(item) { + selected_item: function (event) { this.do_action({ type: 'ir.actions.act_window', - res_model: "product.product", - res_id: item.id, + res_model: 'product.product', + res_id: $(event.currentTarget).data('id'), views: [[false, 'form']], - target: 'current', - context: {}, }); }, }); + .. code-block:: xml + + +
+

+

+
+
+ 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:: +Throughout this guide, we used a simple ``HomePage`` widget which the web +client automatically starts when we select the right menu item. But how did +the Odoo web know to start this widget? Because the widget is registered as +a *client action*. - instance.oepetstore.HomePage = instance.web.Widget.extend({ - start: function() { - console.log("pet store home page loaded"); - }, - }); +A client action is (as its name implies) an action type defined almost +entirely in the client, in javascript for Odoo web. The server simply sends +an action tag (an arbitrary name), and optionally adds a few parameters, but +beyond that *everything* is handled by custom client code. + +Our widget is registered as the handler for the client action through this:: 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: +``instance.web.client_actions`` is a :class:`~openerp.web.Registry` in which +the action manager looks up client action handlers when it needs to execute +one. The first parameter of :class:`~openerp.web.Registry.add` is the name +(tag) of the client action, and the second parameter is the path to the widget +from the Odoo web client root. + +When a client action must be executed, the action manager looks up its tag +in the registry, walks the specified path and displays the widget it finds at +the end. + +.. note:: a client action handler can also be a regular function, in whch case + it'll be called and its result (if any) will be interpreted as the + next action to execute. + +On the server side, we had simply defined an ``ir.actions.client`` action: .. code-block:: xml @@ -1819,101 +1776,85 @@ Here is how the menu element to show this client action was defined: petstore.homepage - +and a menu opening the action: -Client actions do not need a lot of information except their type, which is -stored in the ``tag`` field. +.. code-block:: xml -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. +Much of Odoo web's usefulness (and complexity) resides in views. Each view +type is a way of displaying a model in the 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: +When an ``ActionManager`` instance receive an action of type +``ir.actions.act_window``, it delegates the synchronization and handling of +the views themselves to a *view manager*, which will then set up one or +multiple views depending on the original action's requirements: .. 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). +Most :ref:`Odoo views ` are implemented through a subclass +of :class:`openerp.web.View` which provides a bit of generic basic structure +for handling events and displaying model information. -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 *search view* is considered a view type by the main Odoo framework, but +handled separately by the web client (as it's a more permanent fixture and +can interact with other views, which regular views don't do). + +A view is responsible for loading its own description XML (using +:py:class:`~openerp.models.Model.fields_view_get`) and any other data source +it needs. To that purpose, views are provided with an optional view +identifier set as the :attr:`~openerp.web.View.view_id` attribute. + +Views are also provided with a :class:`~openerp.web.DataSet` instance which +holds most necessary model information (the model name and possibly various +record ids). + +Views may also want to handle search queries by overriding +:func:`~openerp.web.View.do_search`, and updating their +:class:`~openerp.web.DataSet` as necessary. 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 common Odoo web need is the extension of the form view to add new ways of +displaying form fields. -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. +All built-in fields have a default display implementation, creating a new +form widget may be necessary to correctly interact with a new field type +(e.g. a :term:`GIS` field) or to provide new representations and ways to +interact with existing field types (e.g. validate +:py:class:`~openerp.fields.Char` fields which should contain email addresses +and display them as email links). -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: +To explicitly specify which form widget should be used to display a field, +simply use the ``widget`` attribute in the view's XML description: .. code-block:: xml -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. +.. note:: -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. + * the same widget is used in both "view" (read-only) and "edition" modes + of a form view, it's not possible to use a widget in one and an other + widget in the other + * and a given field (name) can not be used multiple times in the same form + * a widget may ignore the current mode of the form view and remain the + same in both view and edition + +.. todo:: most of this should probably move to an advanced form view guide Fields are instantiated by the form view after it has read its XML description and constructed the corresponding HTML representing that description. After @@ -1984,7 +1925,7 @@ user will not be able to modify the content of the field. .. code-block:: javascript - instance.oepetstore.FieldChar2 = instance.web.form.AbstractField.extend({ + local.FieldChar2 = instance.web.form.AbstractField.extend({ init: function() { this._super.apply(this, arguments); this.set("value", ""); @@ -2031,7 +1972,7 @@ 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({ + local.FieldChar2 = instance.web.form.AbstractField.extend({ init: function() { this._super.apply(this, arguments); this.set("value", ""); @@ -2121,7 +2062,14 @@ lot of verifications to know the state of the ``effective_readonly`` property: .. code-block:: javascript - instance.oepetstore.FieldColor = instance.web.form.AbstractField.extend({ + local.FieldColor = instance.web.form.AbstractField.extend({ + events: { + 'change input': function (e) { + if (!this.get('effective_readonly')) { + this.internal_set_value($(e.currentTarget).val()); + } + } + }, init: function() { this._super.apply(this, arguments); this.set("value", ""); @@ -2135,13 +2083,7 @@ lot of verifications to know the state of the ``effective_readonly`` property: 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")) { @@ -2151,7 +2093,6 @@ lot of verifications to know the state of the ``effective_readonly`` property: } }, }); - instance.web.form.widgets.add('color', 'instance.oepetstore.FieldColor'); .. code-block:: xml @@ -2176,14 +2117,14 @@ lot of verifications to know the state of the ``effective_readonly`` property: } 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. +Form fields are used to edit a single field, and are intrinsically linked to +a field. Because this may be limiting, it is also possible to create +*form widgets* which are not so restricted and have less ties to a specific +lifecycle. -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: +Custom form widgets can be added to a form view through the ``widget`` tag: .. code-block:: xml @@ -2196,11 +2137,11 @@ 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:: +Form widgets can interact with form fields by listening for their changes and +fetching or altering their values. They can access form fields through +their :attr:`~openerp.web.form.FormWidget.field_manager` attribute:: - instance.oepetstore.WidgetMultiplication = instance.web.form.FormWidget.extend({ + local.WidgetMultiplication = instance.web.form.FormWidget.extend({ start: function() { this._super(); this.field_manager.on("field_changed:integer_a", this, this.display_result); @@ -2209,57 +2150,49 @@ getting or setting their values using the ``field_manager`` attribute of }, display_result: function() { var result = this.field_manager.get_field_value("integer_a") * - this.field_manager.get_field_value("integer_b"); + 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. +:attr:`~openerp.web.form.FormWidget` is generally the +:class:`~openerp.web.form.FormView` itself, but features used from it should +be limited to those defined by :class:`~openerp.web.form.FieldManagerMixin`, +the most useful being: -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. +* :func:`~openerp.web.form.FieldManagerMixin.get_field_value(field_name)` + which returns the value of a field. +* :func:`~openerp.web.form.FieldManagerMixin.set_values(values)` sets multiple + field values, takes a mapping of ``{field_name: value_to_set}`` +* An event :samp:`field_changed:{field_name}` is triggered any time the value + of the field called ``field_name`` is changed .. 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. + Add two fields to ``product.product`` storing a latitude and a longitude, + then create a new form widget to display the latitude and longitude of + a product's origin on a map - To display that map, you can simply use the Google Map service using an HTML code similar to this: + To display the map, use Google Map's embedding: .. code-block:: html - Just replace ``XXX`` with the latitude and ``YYY`` with the longitude. + where ``XXX`` should be replaced by the latitude and ``YYY`` by 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. + Display the two position fields and a map widget using them in a new + notebook page of the product's form view. .. only:: solutions .. code-block:: javascript - instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({ + local.WidgetCoordinates = instance.web.form.FormWidget.extend({ start: function() { this._super(); this.field_manager.on("field_changed:provider_latitude", this, this.display_map); @@ -2274,18 +2207,22 @@ interface. The most useful features are: } }); - instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates'); + instance.web.form.custom_widgets.add('coordinates', 'local.WidgetCoordinates'); .. code-block:: xml .. exercise:: Get the Current Coordinate + Add a button resetting the product's coordinates to the location of the + user, you can get these coordinates using the + `javascript geolocation API`_. + Now we would like to display an additional button to automatically set the coordinates to the location of the current user. @@ -2304,45 +2241,72 @@ interface. The most useful features are: .. code-block:: javascript - instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({ + local.WidgetCoordinates = instance.web.form.FormWidget.extend({ + events: { + 'click button': function () { + navigator.geolocation.getCurrentPosition( + this.proxy('received_position')); + } + }, start: function() { - this._super(); + var sup = 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(); + return sup; }, 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, + "provider_latitude": obj.coords.latitude, + "provider_longitude": obj.coords.longitude, }); }, }); - instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates'); + instance.web.form.custom_widgets.add('coordinates', 'local.WidgetCoordinates'); .. code-block:: xml +.. [#classes] as a separate concept from instances. In many languages classes + are full-fledged objects and themselves instance (of + metaclasses) but there remains two fairly separate hierarchies + between classes and instances +.. [#dombugs] as well as papering over cross-browser differences, although + this has become less necessary over time + .. _jQuery: http://jquery.org .. _Underscore.js: http://underscorejs.org +.. _git: http://git-scm.com +.. _CSS: http://www.w3.org/Style/CSS/Overview.en.html +.. _Simple JavaScript Inheritance: + http://ejohn.org/blog/simple-javascript-inheritance/ +.. _W3C DOM: http://www.w3.org/TR/DOM-Level-3-Core/ +.. _Qt: http://qt-project.org +.. _Cocoa: https://developer.apple.com/technologies/mac/cocoa.html +.. _GTK: http://www.gtk.org +.. _template engine: http://en.wikipedia.org/wiki/Web_template_system +.. _cross-site scripting: http://en.wikipedia.org/wiki/Cross-site_scripting +.. _jQuery event object: http://api.jquery.com/category/events/event-object/ +.. _$.ajax: http://api.jquery.com/jquery.ajax/ +.. _base64: http://en.wikipedia.org/wiki/Base64 +.. _javascript geolocation API: + http://diveintohtml5.info/geolocation.html +.. _PostgreSQL: http://en.wikipedia.org/wiki/PostgreSQL +.. _positional arguments: +.. _keyword arguments: + https://docs.python.org/2/glossary.html#term-argument diff --git a/doc/howtos/web/about_odoo.png b/doc/howtos/web/about_odoo.png new file mode 100644 index 0000000000000000000000000000000000000000..d8a58712a832f4c0e8c33bb03efa16d634885a70 GIT binary patch literal 8761 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 Bdt?9r literal 0 HcmV?d00001 diff --git a/doc/howtos/web/devmode.png b/doc/howtos/web/devmode.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce5164984c09d59f0775d956770abd566511dc9 GIT binary patch literal 8030 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 literal 0 HcmV?d00001 diff --git a/doc/reference/actions.rst b/doc/reference/actions.rst new file mode 100644 index 00000000000..d3ab6d31b69 --- /dev/null +++ b/doc/reference/actions.rst @@ -0,0 +1,24 @@ +:orphan: + +.. _reference/actions: + +======= +Actions +======= + +.. todo:: fill in documentation, add to TOC + +Actions define the behavior of the system in response to user actions: login, +action button, selection of an invoice, ... + +Window Actions +============== + +URL Actions +=========== + +Server Actions +============== + +Report Actions +============== diff --git a/doc/reference/javascript.rst b/doc/reference/javascript.rst index 54bed0f8153..938c2237912 100644 --- a/doc/reference/javascript.rst +++ b/doc/reference/javascript.rst @@ -350,6 +350,39 @@ it:: will unbind all DOM events, remove the widget's content from the DOM and destroy all widget data. +Development Guidelines +---------------------- + +* Identifiers (``id`` attribute) should be avoided. In generic applications + and modules, ``id`` limits the re-usability of components and tends to make + code more brittle. Most of the time, they can be replaced with nothing, + classes or keeping a reference to a DOM node or jQuery element. + + If an ``id`` is absolutely necessary (because a third-party library requires + one), the id should be partially generated using ``_.uniqueId()`` e.g.:: + + this.id = _.uniqueId('my-widget-') +* Avoid predictable/common CSS class names. Class names such as "content" or + "navigation" might match the desired meaning/semantics, but it is likely an + other developer will have the same need, creating a naming conflict and + unintended behavior. Generic class names should be prefixed with e.g. the + name of the component they belong to (creating "informal" namespaces, much + as in C or Objective-C). +* Global selectors should be avoided. Because a component may be used several + times in a single page (an example in Odoo is dashboards), queries should be + restricted to a given component's scope. Unfiltered selections such as + ``$(selector)`` or ``document.querySelectorAll(selector)`` will generally + lead to unintended or incorrect behavior. Odoo Web's + :class:`~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 :class:`~openerp.web.Widget` + and correctly implement and use its API and life cycle. + .. _.appendTo(): http://api.jquery.com/appendTo/ diff --git a/openerp/http.py b/openerp/http.py index a70a9f1e852..5b1a62fb8c3 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -827,9 +827,9 @@ class Model(object): or self.session.uid != request.uid: raise Exception("Trying to use Model with badly configured database or user.") - mod = request.registry.get(self.model) if method.startswith('_'): raise Exception("Access denied") + mod = request.registry[self.model] meth = getattr(mod, method) cr = request.cr result = meth(cr, request.uid, *args, **kw)