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 00000000000..d8a58712a83 Binary files /dev/null and b/doc/howtos/web/about_odoo.png differ diff --git a/doc/howtos/web/devmode.png b/doc/howtos/web/devmode.png new file mode 100644 index 00000000000..2ce5164984c Binary files /dev/null and b/doc/howtos/web/devmode.png differ 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)