From 9c339d8df3f84d2a5f4e9b2586d7cfb194d00b63 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 1 Sep 2014 14:09:10 +0200 Subject: [PATCH] [ADD] web client tutorial/training needs fixes, and corresponding web client reference --- doc/howtos/web.rst | 2348 +++++++++++++++++++++++++++ doc/howtos/web/qweb.png | Bin 0 -> 40390 bytes doc/howtos/web/viewarchitecture.dia | Bin 0 -> 1918 bytes doc/howtos/web/viewarchitecture.png | Bin 0 -> 35268 bytes doc/howtos/web/viewarchitecture.svg | 89 + doc/tutorials.rst | 1 + 6 files changed, 2438 insertions(+) create mode 100644 doc/howtos/web.rst create mode 100644 doc/howtos/web/qweb.png create mode 100644 doc/howtos/web/viewarchitecture.dia create mode 100644 doc/howtos/web/viewarchitecture.png create mode 100644 doc/howtos/web/viewarchitecture.svg diff --git a/doc/howtos/web.rst b/doc/howtos/web.rst new file mode 100644 index 00000000000..0460a791d1d --- /dev/null +++ b/doc/howtos/web.rst @@ -0,0 +1,2348 @@ +========== +Web Client +========== + +.. highlight:: javascript + +.. default-domain:: js + +This guide is about creating modules for Odoo's web client. To create websites +with Odoo, see :doc:`website`. + +.. warning:: + + This guide assumes knowledge of: + + * Javascript basics and good practices + * jQuery_ + * `Underscore.js`_ + + +A Simple Module to Test the Web Framework +----------------------------------------- + +It's not really possible to include the multiple JavaScript files that +constitute the Odoo web framework in a simple HTML file like we did in the +previous chapter. So we will create a simple module in Odoo that contains some +configuration to have a web component that will give us the possibility to +test the web framework. + +To download the example module, use this bazaar command: + +.. code-block:: sh + + bzr branch lp:~niv-openerp/+junk/oepetstore -r 1 + +Now you must add that folder to your the addons path when you launch Odoo +(``--addons-path`` parameter when you launch the ``odoo.py`` executable). Then +create a new database and install the new module ``oepetstore``. + +Now let's see what files exist in that module: + +.. code-block:: text + + oepetstore + |-- __init__.py + |-- __openerp__.py + |-- petstore_data.xml + |-- petstore.py + |-- petstore.xml + `-- static + `-- src + |-- css + | `-- petstore.css + |-- js + | `-- petstore.js + `-- xml + `-- petstore.xml + +This new module already contains some customization that should be easy to +understand if you already coded an Odoo module like a new table, some views, +menu items, etc... We'll come back to these elements later because they will +be useful to develop some example web module. Right now let's concentrate on +the essential: the files dedicated to web development. + +Please note that all files to be used in the web part of an Odoo module must +always be placed in a ``static`` folder inside the module. This is mandatory +due to possible security issues. The fact we created the folders ``css``, +``js`` and ``xml`` is just a convention. + +``oepetstore/static/css/petstore.css`` is our CSS file. It is empty right now +but we will add any CSS we need later. + +``oepetstore/static/xml/petstore.xml`` is an XML file that will contain our +QWeb templates. Right now it is almost empty too. Those templates will be +explained later, in the part dedicated to QWeb templates. + +``oepetstore/static/js/petstore.js`` is probably the most interesting part. It +contains the JavaScript of our application. Here is what it looks like right +now:: + + openerp.oepetstore = function(instance) { + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + + instance.oepetstore = {}; + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + console.log("pet store home page loaded"); + }, + }); + + instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + } + +The multiple components of that file will explained progressively. Just know +that it doesn't do much things right now except display a blank page and print +a small message in the console. + +Like Odoo's XML files containing views or data, these files must be indicated +in the ``__openerp__.py`` file. Here are the lines we added to explain to the +web client it has to load these files: + +.. code-block:: python + + 'js': ['static/src/js/*.js'], + 'css': ['static/src/css/*.css'], + 'qweb': ['static/src/xml/*.xml'], + +These configuration parameters use wildcards, so we can add new files without +altering ``__openerp__.py``: they will be loaded by the web client as long as +they have the correct extension and are in the correct folder. + +.. warning:: + + In Odoo, all JavaScript files are, by default, concatenated in a single + file. Then we apply an operation called the *minification* on that + file. The minification will remove all comments, white spaces and + line-breaks in the file. Finally, it is sent to the user's browser. + + That operation may seem complex, but it's a common procedure in big + application like Odoo with a lot of JavaScript files. It allows to load + the application a lot faster. + + It has the main drawback to make the application almost impossible to + debug, which is very bad to develop. The solution to avoid this + side-effect and still be able to debug is to append a small argument to + the URL used to load Odoo: ``?debug``. So the URL will look like this: + + .. code-block:: text + + http://localhost:8069/?debug + + When you use that type of URL, the application will not perform all that + concatenation-minification process on the JavaScript files. The + application will take more time to load but you will be able to develop + with decent debugging tools. + +Odoo JavaScript Module +------------------------- + +In the previous chapter, we explained that JavaScript do not have a correct +mechanism to namespace the variables declared in different JavaScript files +and we proposed a simple method called the Module pattern. + +In Odoo's web framework there is an equivalent of that pattern which is +integrated with the rest of the framework. Please note that **an Odoo web +module is a separate concept from an Odoo addon**. An addon is a folder with a +lot of files, a web module is not much more than a namespace for JavaScript. + +The ``oepetstore/static/js/petstore.js`` already declare such a module:: + + openerp.oepetstore = function(instance) { + instance.oepetstore = {}; + + instance.oepetstore.xxx = ...; + } + +In Odoo's web framework, you declare a JavaScript module by declaring a +function that you put in the global variable ``openerp``. The attribute you +set in that object must have the exact same name than your Odoo addon (this +addon is named ``oepetstore``, if I set ``openerp.petstore`` instead of +``openerp.oepetstore`` that will not work). + +That function will be called when the web client decides to load your +addon. It is given a parameter named ``instance``, which represents the +current Odoo web client instance and contains all the data related to the +current session as well as the variables of all web modules. + +The convention is to create a new namespace inside the ``instance`` object +which has the same name than you addon. That's why we set an empty dictionary +in ``instance.oepetstore``. That dictionary is the namespace we will use to +declare all classes and variables used inside our module. + +Classes +------- + +JavaScript doesn't have a class mechanism like most object-oriented +programming languages. To be more exact, it provides language elements to make +object-oriented programming but you have to define by yourself how you choose +to do it. Odoo's web framework provide tools to simplify this and let +programmers code in a similar way they would program in other languages like +Java. That class system is heavily inspired by John Resig's `Simple JavaScript +Inheritance `_. + +To define a new class, you need to extend the :class:`openerp.web.Class` +class:: + + instance.oepetstore.MyClass = instance.web.Class.extend({ + say_hello: function() { + console.log("hello"); + }, + }); + +As you can see, you have to call :func:`instance.web.Class.extend` and give +it a dictionary. That dictionary will contain the methods and class attributes +of our new class. Here we simply put a method named ``say_hello()``. This +class can be instantiated and used like this:: + + var my_object = new instance.oepetstore.MyClass(); + my_object.say_hello(); + // print "hello" in the console + +You can access the attributes of a class inside a method using ``this``:: + + instance.oepetstore.MyClass = instance.web.Class.extend({ + say_hello: function() { + console.log("hello", this.name); + }, + }); + + var my_object = new instance.oepetstore.MyClass(); + my_object.name = "Nicolas"; + my_object.say_hello(); + // print "hello Nicolas" in the console + +Classes can have a constructor, it is just a method named ``init()``. You can +pass parameters to the constructor like in most language:: + + instance.oepetstore.MyClass = instance.web.Class.extend({ + init: function(name) { + this.name = name; + }, + say_hello: function() { + console.log("hello", this.name); + }, + }); + + var my_object = new instance.oepetstore.MyClass("Nicolas"); + my_object.say_hello(); + // print "hello Nicolas" in the console + +Classes can be inherited. To do so, use :func:`~openerp.web.Class.extend` +directly on your class just like you extended :class:`~openerp.web.Class`:: + + instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({ + say_hello: function() { + console.log("hola", this.name); + }, + }); + + var my_object = new instance.oepetstore.MySpanishClass("Nicolas"); + my_object.say_hello(); + // print "hola Nicolas" in the console + +When overriding a method using inheritance, you can use ``this._super()`` to +call the original method. ``this._super()`` is not a normal method of your +class, you can consider it's magic. Example:: + + instance.oepetstore.MySpanishClass = instance.oepetstore.MyClass.extend({ + say_hello: function() { + this._super(); + console.log("translation in Spanish: hola", this.name); + }, + }); + + var my_object = new instance.oepetstore.MySpanishClass("Nicolas"); + my_object.say_hello(); + // print "hello Nicolas \n translation in Spanish: hola Nicolas" in the console + +Widgets Basics +-------------- + +In previous chapter we discovered jQuery and its DOM manipulation tools. It's +useful, but it's not sufficient to structure a real application. Graphical +user interface libraries like Qt, GTK or Windows Forms have classes to +represent visual components. In Odoo, we have the +:class:`~openerp.web.Widget` class. A widget is a generic component +dedicated to display content to the user. + +Your First Widget +%%%%%%%%%%%%%%%%% + +The start module you installed already contains a small widget:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + console.log("pet store home page loaded"); + }, + }); + +Here we create a simple widget by extending the :class:`openerp.web.Widget` +class. This one defines a method named :func:`~openerp.web.Widget.start` that +doesn't do anything really interesting right now. + +You may also have noticed this line at the end of the file:: + + instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + +This last line registers our basic widget as a client action. Client actions +will be explained in the next part of this guide. For now, just remember that +this is what allows our widget to be displayed when we click on the +:menuselection:`Pet Store --> Pet Store --> Home Page` menu element. + +Display Content +%%%%%%%%%%%%%%% + +Widgets have a lot of methods and features, but let's start with the basics: +display some data inside the widget and how to instantiate a widget and +display it. + +The ``HomePage`` widget already has a :func:`~openerp.web.Widget.start` +method. That method is automatically called after the widget has been +instantiated and it has received the order to display its content. We will use +it to display some content to the user. + +To do so, we will also use the :attr:`~openerp.web.Widget.$el` attribute +that all widgets contain. That attribute is a jQuery object with a reference +to the HTML element that represents the root of our widget. A widget can +contain multiple HTML elements, but they must be contained inside one single +element. By default, all widgets have an empty root element which is a +``
`` HTML element. + +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:: + + instance.oepetstore.HomePage = instance.web.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). + +Now you should learn how to instantiate a widget and display its content. To +do so, we will create a new widget:: + + instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ + start: function() { + this.$el.append("
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``:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + this.$el.append("
Hello dear Odoo user!
"); + var greeting = new instance.oepetstore.GreetingsWidget(this); + greeting.appendTo(this.$el); + }, + }); + +Here, the ``HomePage`` instantiate a ``GreetingsWidget`` (the first argument +of the constructor of ``GreetingsWidget`` will be explained in the next +part). Then it asks the ``GreetingsWidget`` to insert itself inside the DOM, +more precisely directly under the ``HomePage`` widget. + +When the :func:`~openerp.web.Widget.appendTo` method is called, it asks the +widget to insert itself and to display its content. It's during the call to +:func:`~openerp.web.Widget.appentTo` that the +:func:`~openerp.web.Widget.start` method will be called. + +To check the consequences of that code, let's use Chrome's DOM explorer. But +before that we will modify a little bit our widgets to have some classes on +some of our ``
`` elements so we can clearly see them in the explorer:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + this.$el.addClass("oe_petstore_homepage"); + ... + }, + }); + instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ + start: function() { + this.$el.addClass("oe_petstore_greetings"); + ... + }, + }); + +The result will be this if you can find the correct DOM part in the DOM explorer: + +.. code-block:: html + +
+
Hello dear Odoo user!
+
+
We are so happy to see you again in this menu!
+
+
+ +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. + +Widget Parents and Children +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +In the previous part, we instantiated a widget using this syntax:: + + new instance.oepetstore.GreetingsWidget(this); + +The first argument is ``this``, which in that case was a ``HomePage`` +instance. This serves to indicate the Widget what other widget is his parent. + +As we've seen, widgets are usually inserted in the DOM by another widget and +*inside* that other widget. This means most widgets are always a part of +another widget. We call the container the *parent*, and the contained widget +the *child*. + +Due to multiple technical and conceptual reasons, it is necessary for a widget +to know who is his parent and who are its children. This is why we have that +first parameter in the constructor of all widgets. + +:func:`~openerp.web.Widget.getParent` can be used to get the parent of a +widget:: + + instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ + start: function() { + console.log(this.getParent().$el ); + // will print "div.oe_petstore_homepage" in the console + }, + }); + +:func:`~openerp.web.Widget.getChildren` can be used to get a list of its +children:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + var greeting = new instance.oepetstore.GreetingsWidget(this); + greeting.appendTo(this.$el); + console.log(this.getChildren()[0].$el); + // will print "div.oe_petstore_greetings" in the console + }, + }); + +You should also remember that, when you override the +:func:`~openerp.web.Widget.init` method of a widget you should always put the +parent as first parameter are pass it to ``this._super()``:: + + instance.oepetstore.GreetingsWidget = instance.web.Widget.extend({ + init: function(parent, name) { + this._super(parent); + this.name = name; + }, + }); + +Finally, if a widget does not logically have a parent (ie: because it's the +first widget you instantiate in an application), you can give null as a parent +instead:: + + new instance.oepetstore.GreetingsWidget(null); + +Destroying Widgets +%%%%%%%%%%%%%%%%%% + +If you can display content to your users, you should also be able to erase +it. This can simply be done using the :func:`~openerp.web.Widget.destroy` +method: + + greeting.destroy(); + +When a widget is destroyed it will first call +:func:`~openerp.web.Widget.destroy` on all its children. Then it erases itself +from the DOM. The recursive call to destroy from parents to children is very +useful to clean properly complex structures of widgets and avoid memory leaks +that can easily appear in big JavaScript applications. + +.. _howtos/web/qweb: + +The QWeb Template Engine +------------------------ + +The previous part of the guide showed how to define widgets that are able to +display HTML to the user. The example ``GreetingsWidget`` used a syntax like +this:: + + this.$el.append("
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... + +The problem is exactly the same in most programming languages needing to +generate HTML. That's why they typically use template engines. Example of +template engines are Velocity, JSP (Java), Mako, Jinja (Python), Smarty (PHP), +etc... + +In Odoo we use a template engine developed specifically for Odoo's web +client. Its name is QWeb. + +QWeb is an XML-based templating language, similar to `Genshi +`_, `Thymeleaf +`_ or `Facelets +`_ with a few peculiarities: + +* It's implemented fully in JavaScript and rendered in the browser. +* Each template file (XML files) contains multiple templates, where template + engine usually have a 1:1 mapping between template files and templates. +* It has special support in Odoo Web's :class:`~openerp.web.Widget`, though it + can be used outside of Odoo's web client (and it's possible to use + :class:`~openerp.web.Widget` without relying on QWeb). + +The rationale behind using QWeb instead of existing javascript template +engines is that its extension mechanism is very similar to the Odoo view +inheritance mechanism. Like Odoo views a QWeb template is an XML tree and +therefore XPath or DOM manipulations are easy to perform on it. + +Using QWeb inside a Widget +%%%%%%%%%%%%%%%%%%%%%%%%%% + +First let's define a simple QWeb template in +``oepetstore/static/src/xml/petstore.xml`` file, the exact meaning will be +explained later: + +.. code-block:: xml + + + + + +
This is some simple HTML
+
+
+ +Now let's modify the ``HomePage`` class. Remember that enigmatic line at the +beginning the the JavaScript source file? + +:: + + var QWeb = instance.web.qweb; + +This is a line we recommend to copy-paste in all Odoo web modules. It is the +object giving access to all templates defined in template files that were +loaded by the web client. We can use the template we defined in our XML +template file like this:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + this.$el.append(QWeb.render("HomePageTemplate")); + }, + }); + +Calling the ``QWeb.render()`` method asks to render the template identified by +the string passed as first parameter. + +Another possibility commonly seen in Odoo code is to use ``Widget``'s +integration with QWeb:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + template: "HomePageTemplate", + start: function() { + ... + }, + }); + +When you put a ``template`` class attribute in a widget, the widget knows it +has to call ``QWeb.render()`` to render that template. + +Please note there is a difference between those two syntaxes. When you use +``Widget``'s QWeb integration the ``QWeb.render()`` method is called *before* +the widget calls :func:`~openerp.web.Widget.start`. It will also take the root +element of the rendered template and put it as a replacement of the default +root element generated by the :class:`~openerp.web.Widget` class. This will +alter the behavior, so you should remember it. + +QWeb Context +'''''''''''' + +Like with all template engines, QWeb templates can contain code able to +manipulate data that is given to the template. To pass data to QWeb, use the +second argument to ``QWeb.render()``: + +.. code-block:: xml + + +
Hello
+
+ +:: + + QWeb.render("HomePageTemplate", {name: "Nicolas"}); + +Result: + +.. code-block:: html + +
Hello Nicolas
+ +When you use :class:`~openerp.web.Widget`'s integration you can not pass +additional data to the template. Instead the template will have a unique +``widget`` variable which is a reference to the current widget: + +.. code-block:: xml + + +
Hello
+
+ +:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + template: "HomePageTemplate", + init: function(parent) { + this._super(parent); + this.name = "Nicolas"; + }, + start: function() { + }, + }); + +Result: + +.. code-block:: html + +
Hello Nicolas
+ +Template Declaration +'''''''''''''''''''' + +Now that we know everything about rendering templates we can try to understand +QWeb's syntax. + +All QWeb directives use XML attributes beginning with the prefix ``t-``. To +declare new templates, we add a ```` element into the XML +template file inside the root element ````:: + + + +
This is some simple HTML
+
+
+ +``t-name`` simply declares a template that can be called using +``QWeb.render()``. + +Escaping +'''''''' + +To put some text in the HTML, use ``t-esc``: + +.. code-block:: xml + + +
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: + +.. code-block:: xml + + +
+
+ +Will render: + +.. code-block:: html + +
8
+ +Outputting HTML +''''''''''''''' + +If you know you have some HTML contained in a variable, use ``t-raw`` instead +of ``t-esc``: + +.. code-block:: xml + + +
+
+ +If +'' + +The basic alternative block of QWeb is ``t-if``: + +.. code-block:: xml + + +
+ + true is true + + + true is not true + +
+
+ +Although QWeb does not contains any structure for else. + +Foreach +''''''' + +To iterate on a list, use ``t-foreach`` and ``t-as``: + +.. 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: + +
+
+ +To Learn More About QWeb +'''''''''''''''''''''''' + +For a QWeb reference, see :ref:`reference/qweb`. + +Exercise +'''''''' + +.. exercise:: Usage of QWeb in Widgets + + Create a widget whose constructor contains two parameters aside from + ``parent``: ``product_names`` and ``color``. ``product_names`` is a list + of strings, each one being a name of product. ``color`` is a string + containing a color in CSS color format (ie: ``#000000`` for black). That + widget should display the given product names one under the other, each + one in a separate box with a background color with the value of ``color`` + and a border. You must use QWeb to render the HTML. This exercise will + necessitate some CSS that you should put in + ``oepetstore/static/src/css/petstore.css``. Display that widget in the + ``HomePage`` widget with a list of five products and green as the + background color for boxes. + + .. only:: solutions + + :: + + openerp.oepetstore = function(instance) { + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + + instance.oepetstore = {}; + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + var products = new instance.oepetstore.ProductsWidget(this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00"); + products.appendTo(this.$el); + }, + }); + + instance.oepetstore.ProductsWidget = instance.web.Widget.extend({ + template: "ProductsWidget", + init: function(parent, products, color) { + this._super(parent); + this.products = products; + this.color = color; + }, + }); + + instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + } + + .. code-block:: xml + + + + + +
+ +
+
+
+
+
+ + .. code-block:: css + + .oe_products_item { + display: inline-block; + padding: 3px; + margin: 5px; + border: 1px solid black; + border-radius: 3px; + } + + .. image:: web/qweb.* + :align: center + :width: 70% + +Widget Events and Properties +---------------------------- + +Widgets still have more helper to learn. One of the more complex (and useful) +one is the event system. Events are also closely related to the widget +properties. + +Events +%%%%%% + +Widgets are able to fire events in a similar way most components in existing +graphical user interfaces libraries (Qt, GTK, Swing,...) handle +them. Example:: + + instance.oepetstore.ConfirmWidget = instance.web.Widget.extend({ + start: function() { + var self = this; + this.$el.append("
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); + }); + }, + }); + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + var widget = new instance.oepetstore.ConfirmWidget(this); + widget.on("user_choose", this, this.user_choose); + widget.appendTo(this.$el); + }, + user_choose: function(confirm) { + if (confirm) { + console.log("The user agreed to continue"); + } else { + console.log("The user refused to continue"); + } + }, + }); + +First, we will explain what this example is supposed to do. We create a +generic widget to ask the user if he really wants to do an action that could +have important consequences (a type widget heavily used in Windows). To do so, +we put two buttons in the widget. Then we bind jQuery events to know when the +user click these buttons. + +.. note:: + + It could be hard to understand this particular line:: + + var self = this; + + Remember, in JavaScript the variable ``this`` is a variable that is passed + implicitly to all functions. It allows us to know which is the object if + function is used like a method. Each declared function has its own + ``this``. So, when we declare a function inside a function, that new + function will have its own ``this`` that could be different from the + ``this`` of the parent function. If we want to remember the original + object the simplest method is to store a reference in a variable. By + convention in Odoo we very often name that variable ``self`` because it's + the equivalent of ``this`` in Python. + +Since our widget is supposed to be generic, it should not perform any precise +action by itself. So, we simply make it trigger and event named +``user_choose`` by using the :func:`~openerp.web.Widget.trigger` method. + +:func:`~openerp.web.Widget.trigger` takes as first argument the name of the +event to trigger. Then it can takes any number of additional arguments. These +arguments will be passed to all the event listeners. + +Then we modify the ``HomePage`` widget to instantiate a ``ConfirmWidget`` and +listen to its ``user_choose`` event by calling the +:func:`~openerp.web.Widget.on` method. + +:func:`~openerp.web.Widget.on` allows to bind a function to be called when the +event identified by event_name is ``triggered``. The ``func`` argument is the +function to call and ``object`` is the object to which that function is +related if it is a method. The binded function will be called with the +additional arguments of :func:`~openerp.web.Widget.trigger` if it has +any. Example:: + + start: function() { + var widget = ... + widget.on("my_event", this, this.my_event_triggered); + widget.trigger("my_event", 1, 2, 3); + }, + my_event_triggered: function(a, b, c) { + console.log(a, b, c); + // will print "1 2 3" + } + +Properties +%%%%%%%%%% + +Properties are very similar to normal object attributes. They allow to set +data on an object but with an additional feature: it triggers events when a +property's value has changed:: + + start: function() { + this.widget = ... + this.widget.on("change:name", this, this.name_changed); + this.widget.set("name", "Nicolas"); + }, + name_changed: function() { + console.log("The new value of the property 'name' is", this.widget.get("name")); + } + +:func:`~openerp.web.Widget.set` allows to set the value of property. If the +value changed (or it didn't had a value previously) the object will trigger a +``change:xxx`` where ``xxx`` is the name of the property. + +:func:`~openerp.web.Widget.get` allows to retrieve the value of a property. + +Exercise +%%%%%%%% + +.. exercise:: Widget Properties and Events + + Create a widget ``ColorInputWidget`` that will display 3 ````. Each of these ```` is dedicated to type a + hexadecimal number from 00 to FF. When any of these ```` is + modified by the user the widget must query the content of the three + ````, concatenate their values to have a complete CSS color code + (ie: ``#00FF00``) and put the result in a property named ``color``. Please + note the jQuery ``change()`` event that you can bind on any HTML + ```` element and the ``val()`` method that can query the current + value of that ```` could be useful to you for this exercise. + + Then, modify the ``HomePage`` widget to instantiate ``ColorInputWidget`` + and display it. The ``HomePage`` widget should also display an empty + rectangle. That rectangle must always, at any moment, have the same + background color than the color in the ``color`` property of the + ``ColorInputWidget`` instance. + + Use QWeb to generate all HTML. + + .. only:: solutions + + :: + + openerp.oepetstore = function(instance) { + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + + instance.oepetstore = {}; + + instance.oepetstore.ColorInputWidget = instance.web.Widget.extend({ + template: "ColorInputWidget", + start: function() { + var self = this; + this.$el.find("input").change(function() { + self.input_changed(); + }); + self.input_changed(); + }, + input_changed: function() { + var color = "#"; + color += this.$el.find(".oe_color_red").val(); + color += this.$el.find(".oe_color_green").val(); + color += this.$el.find(".oe_color_blue").val(); + this.set("color", color); + }, + }); + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + template: "HomePage", + start: function() { + this.colorInput = new instance.oepetstore.ColorInputWidget(this); + this.colorInput.on("change:color", this, this.color_changed); + this.colorInput.appendTo(this.$el); + }, + color_changed: function() { + this.$el.find(".oe_color_div").css("background-color", this.colorInput.get("color")); + }, + }); + + instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + } + + .. code-block:: xml + + + + + +
+ Red:
+ Green:
+ Blue:
+
+
+ +
+
+
+
+
+ + .. code-block:: css + + .oe_color_div { + width: 100px; + height: 100px; + margin: 10px; + } + + .. note:: + + jQuery's ``css()`` method allows setting a css property. + +Widget Helpers +-------------- + +We've seen the basics of the :class:`~openerp.web.Widget` class, QWeb and the +events/properties system. There are still some more useful methods proposed by +this class. + +``Widget``'s jQuery Selector +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +It is very common to need to select a precise element inside a widget. In the +previous part of this guide we've seen a lot of uses of the ``find()`` method +of jQuery objects:: + + this.$el.find("input.my_input")... + +:class:`~openerp.web.Widget` provides a shorter syntax that does the same +thing with the :func:`~openerp.web.Widget.$` method:: + + instance.oepetstore.MyWidget = instance.web.Widget.extend({ + start: function() { + this.$("input.my_input")... + }, + }); + +.. note:: + + We strongly advise you against using directly the global jQuery function + ``$()`` like we did in the previous chapter were we explained the jQuery + library and jQuery selectors. That type of global selection is sufficient + for simple applications but is not a good idea in real, big web + applications. The reason is simple: when you create a new type of widget + you never know how many times it will be instantiated. Since the ``$()`` + global function operates in *the whole HTML displayed in the browser*, if + you instantiate a widget 2 times and use that function you will + incorrectly select the content of another instance of your widget. That's + why you must restrict the jQuery selections to HTML which is located + *inside* your widget most of the time. + + Applying the same logic, you can also guess it is a very bad idea to try + to use HTML ids in any widget. If the widget is instantiated 2 times you + will have 2 different HTML element in the whole application that have the + same + id. And that is an error by itself. So you should stick to CSS classes to mark your HTML elements in all cases. + +Easier DOM Events Binding +%%%%%%%%%%%%%%%%%%%%%%%%% + +In the previous part, we had to bind a lot of HTML element events like +``click()`` or ``change()``. Now that we have the ``$()`` method to simplify +code a little, let's see how it would look like:: + + instance.oepetstore.MyWidget = instance.web.Widget.extend({ + start: function() { + var self = this; + this.$(".my_button").click(function() { + self.button_clicked(); + }); + }, + button_clicked: function() { + .. + }, + }); + +It's still a bit long to type. That's why there is an even more simple syntax +for that:: + + instance.oepetstore.MyWidget = instance.web.Widget.extend({ + events: { + "click .my_button": "button_clicked", + }, + button_clicked: function() { + .. + } + }); + +.. warning:: + + It's important to differentiate the jQuery events that are triggered on + DOM elements and events of the widgets. The ``event`` class attribute *is + a helper to help binding jQuery events*, it has nothing to do with the + widget events that can be binded using the ``on()`` method. + +The ``event`` class attribute is a dictionary that allows to define jQuery +events with a shorter syntax. + +The key is a string with 2 different parts separated with a space. The first +part is the name of the event, the second one is the jQuery selector. So the +key ``click .my_button`` will bind the event ``click`` on the elements +matching the selector ``my_button``. + +The value is a string with the name of the method to call on the current +object. + +Development Guidelines +%%%%%%%%%%%%%%%%%%%%%% + +As explained in the prerequisites to read this guide, you should already know +HTML and CSS. But developing web applications in JavaScript or developing web +modules for Odoo require to be more strict than you will usually be when +simply creating static web pages with CSS to style them. So these guidelines +should be followed if you want to have manageable projects and avoid bugs or +common mistakes: + +* Identifiers (``id`` attribute) should be avoided. In generic applications + and modules, ``id`` limits the re-usability of components and tends to make + code more brittle. Just about all the time, they can be replaced with + nothing, with classes or with keeping a reference to a DOM node or a jQuery + element around. + + .. note:: + + If it is absolutely necessary to have an ``id`` (because a third-party + library requires one and can't take a DOM element), it should be + generated with ``_.uniqueId()``. + +* Avoid predictable/common CSS class names. Class names such as "content" or + "navigation" might match the desired meaning/semantics, but it is likely an + other developer will have the same need, creating a naming conflict and + unintended behavior. Generic class names should be prefixed with e.g. the + name of the component they belong to (creating "informal" namespaces, much + as in C or Objective-C). + +* Global selectors should be avoided. Because a component may be used several + times in a single page (an example in Odoo is dashboards), queries should be + restricted to a given component's scope. Unfiltered selections such as + ``$(selector)`` or ``document.querySelectorAll(selector)`` will generally + lead to unintended or incorrect behavior. Odoo Web's + :class:`~openerp.web.Widget` has an attribute providing its DOM root + (:attr:`~openerp.web.Widget.$el`), and a shortcut to select nodes directly + (:func:`~openerp.web.Widget.$`). + +* More generally, never assume your components own or controls anything beyond + its own personal :attr:`~openerp.web.Widget.$el` + +* html templating/rendering should use QWeb unless absolutely trivial. + +* All interactive components (components displaying information to the screen + or intercepting DOM events) must inherit from Widget and correctly implement + and use its API and life cycle. + +Modify Existent Widgets and Classes +----------------------------------- + +The class system of the Odoo web framework allows direct modification of +existing classes using the :func:`~openerp.web.Widget.include` method of a +class:: + + var TestClass = instance.web.Class.extend({ + testMethod: function() { + return "hello"; + }, + }); + + TestClass.include({ + testMethod: function() { + return this._super() + " world"; + }, + }); + + console.log(new TestClass().testMethod()); + // will print "hello world" + +This system is similar to the inheritance mechanism, except it will directly +modify the class. You can call ``this._super()`` to call the original +implementation of the methods you are redefining. If the class already had +sub-classes, all calls to ``this._super()`` in sub-classes will call the new +implementations defined in the call to ``include()``. This will also work if +some instances of the class (or of any of its sub-classes) were created prior +to the call to :func:`~openerp.web.Widget.include`. + +.. warning:: + + Please note that, even if :func:`~openerp.web.Widget.include` can be a + powerful tool, it's not considered a very good programming practice + because it can easily create problems if used in a wrong way. So you + should use it to modify the behavior of an existing component only when + there are no other options, and try to limit its usages to the strict + minimum. + +Translations +------------ + +The process to translate text in Python and JavaScript code is very +similar. You could have noticed these lines at the beginning of the +``petstore.js`` file: + + var _t = instance.web._t, + _lt = instance.web._lt; + +These lines are simply used to import the translation functions in the current +JavaScript module. The correct to use them is this one:: + + this.$el.text(_t("Hello dear user!")); + +In Odoo, translations files are automatically generated by scanning the source +code. All piece of code that calls a certain function are detected and their +content is added to a translation file that will then be sent to the +translators. In Python, the function is ``_()``. In JavaScript the function is +:func:`~openerp.web._t` (and also :func:`~openerp.web._lt`). + +If the source file as never been scanned and the translation files does not +contain any translation for the text given to ``_t()`` it will return the text +as-is. If there is a translation it will return it. + +:func:`~openerp.web._lt` does almost the exact same thing but is a little bit +more complicated. It does not return a text but returns a function that will +return the text. It is reserved for very special cases:: + + var text_func = _lt("Hello dear user!"); + this.$el.text(text_func()); + +To have more information about Odoo's translations, please take a look at the +reference documentation: https://doc.openerp.com/contribute/translations/ . + +Communication with the Odoo Server +------------------------------------- + +Now you should know everything you need to display any type of graphical user +interface with your Odoo modules. Still, Odoo is a database-centric +application so it's still not very useful if you can't query data from the +database. + +As a reminder, in Odoo you are not supposed to directly query data from the +PostgreSQL database, you will always use the build-in ORM (Object-Relational +Mapping) and more precisely the Odoo *models*. + +Contacting Models +%%%%%%%%%%%%%%%%% + +In the previous chapter we explained how to send HTTP requests to the web +server using the ``$.ajax()`` method and the JSON format. It is useful to know +how to make a JavaScript application communicate with its web server using +these tools, but it's still a little bit low-level to be used in a complex +application like Odoo. + +When the web client contacts the Odoo server it has to pass additional data +like the necessary information to authenticate the current user. There is also +some more complexity due to Odoo models that need a higher-level communication +protocol to be used. + +This is why you will not use directly ``$.ajax()`` to communicate with the +server. The web client framework provides classes to abstract that protocol. + +To demonstrate this, the file ``petstore.py`` already contains a small model +with a sample method: + +.. code-block:: python + + class message_of_the_day(osv.osv): + _name = "message_of_the_day" + + def my_method(self, cr, uid, context=None): + return {"hello": "world"} + + _columns = { + 'message': fields.text(string="Message"), + 'color': fields.char(string="Color", size=20), + } + +If you know Odoo models that code should be familiar to you. This model +declares a table named ``message_of_the_day`` with two fields. It also has a +method ``my_method()`` that doesn't do much except return a dictionary. + +Here is a sample widget that calls ``my_method()`` and displays the result:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + var self = this; + var model = new instance.web.Model("message_of_the_day"); + model.call("my_method", [], {context: new instance.web.CompoundContext()}).then(function(result) { + self.$el.append("
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``.) + +:func:`~openerp.web.Model.call` is the method of :class:`~openerp.web.Model` +used to call any method of an Odoo server-side model. Here are its arguments: + +* ``name`` is the name of the method to call on the model. Here it is the + method named ``my_method``. +* ``args`` is a list of positional arguments to give to the method. The sample + ``my_method()`` method does not contain any particular argument we want to + give to it, so here is another example: + + .. code-block:: python + + def my_method2(self, cr, uid, a, b, c, context=None): ... + + .. code-block:: javascript + + model.call("my_method", [1, 2, 3], ... + // with this a=1, b=2 and c=3 + +* ``kwargs`` is a list of named arguments to give to the method. In the + example, we have one named argument which is a bit special: + ``context``. It's given a value that may seem very strange right now: ``new + instance.web.CompoundContext()``. The meaning of that argument will be + explained later. Right now you should just know the ``kwargs`` argument + allows to give arguments to the Python method by name instead of + position. Example: + + .. code-block:: python + + def my_method2(self, cr, uid, a, b, c, context=None): ... + + .. code-block:: javascript + + model.call("my_method", [], {a: 1, b: 2, c: 3, ... + // with this a=1, b=2 and c=3 + +.. note:: + + If you take a look at the ``my_method()``'s declaration in Python, you can + see it has two arguments named ``cr`` and ``uid``: + + .. code-block:: python + + def my_method(self, cr, uid, context=None): + + You could have noticed we do not give theses arguments to the server when + we call that method from JavaScript. That is because theses arguments that + have to be declared in all models' methods are never sent from the Odoo + client. These arguments are added implicitly by the Odoo server. The + first one is an object called the *cursor* that allows communication with + the database. The second one is the id of the currently logged in user. + +:func:`~openerp.web.Widget.call` returns a deferred resolved with the value +returned by the model's method as first argument. If you don't know what +deferreds are, take a look at the previous chapter (the part about HTTP +requests in jQuery). + +CompoundContext +%%%%%%%%%%%%%%% + +In the previous part, we avoided to explain the strange ``context`` argument +in the call to our model's method: + +.. code-block:: javascript + + model.call("my_method", [], {context: new instance.web.CompoundContext()}) + +In Odoo, models' methods should always have an argument named ``context``: + +.. code-block:: python + + def my_method(self, cr, uid, context=None): ... + +The context is like a "magic" argument that the web client will always give to +the server when calling a method. The context is a dictionary containing +multiple keys. One of the most important key is the language of the user, used +by the server to translate all the messages of the application. Another one is +the time zone of the user, used to compute correctly dates and times if Odoo +is used by people in different countries. + +The ``argument`` is necessary in all methods, because if we forget it bad +things could happen (like the application not being translated +correctly). That's why, when you call a model's method, you should always give +it to that argument. The solution to achieve that is to use +:class:`openerp.web.CompoundContext`. + +:class:`~openerp.web.CompoundContext` is a class used to pass the user's +context (with language, time zone, etc...) to the server as well as adding new +keys to the context (some models' methods use arbitrary keys added to the +context). It is created by giving to its constructor any number of +dictionaries or other :class:`~openerp.web.CompoundContext` instances. It will +merge all those contexts before sending them to the server. + +.. code-block:: javascript + + model.call("my_method", [], {context: new instance.web.CompoundContext({'new_key': 'key_value'})}) + +.. code-block:: python + + def display_context(self, cr, uid, context=None): + print context + // will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1} + +You can see the dictionary in the argument ``context`` contains some keys that +are related to the configuration of the current user in Odoo plus the +``new_key`` key that was added when instantiating +:class:`~openerp.web.CompoundContext`. + +To resume, you should always add an instance of +:class:`~openerp.web.CompoundContext` in all calls to a model's method. + +Queries +%%%%%%% + +If you know Odoo module development, you should already know everything +necessary to communicate with models and make them do what you want. But there +is still a small helper that could be useful to you : +:func:`~openerp.web.Model.query`. + +:func:`~openerp.web.Model.query` is a shortcut for the usual combination of +:py:meth:`~openerp.models.Model.search` and +::py:meth:`~openerp.models.Model.read` methods in Odoo models. It allows to +:search records and get their data with a shorter syntax. Example:: + + model.query(['name', 'login', 'user_email', 'signature']) + .filter([['active', '=', true], ['company_id', '=', main_company]]) + .limit(15) + .all().then(function (users) { + // do work with users records + }); + +:func:`~openerp.web.Model.query` takes as argument a list of fields to query +in the model. It returns an instance of the :class:`openerp.web.Query` class. + +:class:`~openerp.web.Query` is a class representing the query you are trying +to construct before sending it to the server. It has multiple methods you can +call to customize the query. All these methods will return the current +instance of :class:`~openerp.web.Query`: + +* :func:`~openerp.web.Query.filter` allows to specify an Odoo *domain*. As a + reminder, a domain in Odoo is a list of conditions, each condition is a list + it self. +* :func:`~openerp.web.Query.limit` sets a limit to the number of records + returned. + +When you have customized you query, you can call the +:func:`~openerp.web.Query.all` method. It will performs the real query to the +server and return a deferred resolved with the result. The result is the same +thing return by the model's method :py:meth:`~openerp.models.Model.read` (a +list of dictionaries containing the asked fields). + +Exercises +--------- + +.. exercise:: Message of the Day + + Create a widget ``MessageOfTheDay`` that will display the message + contained in the last record of the ``message_of_the_day``. The widget + should query the message as soon as it is inserted in the DOM and display + the message to the user. Display that widget on the home page of the Odoo + Pet Store module. + + .. only:: solutions + + .. code-block:: javascript + + openerp.oepetstore = function(instance) { + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + + instance.oepetstore = {}; + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + template: "HomePage", + start: function() { + var motd = new instance.oepetstore.MessageOfTheDay(this); + motd.appendTo(this.$el); + }, + }); + + instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + + instance.oepetstore.MessageOfTheDay = instance.web.Widget.extend({ + template: "MessageofTheDay", + init: function() { + this._super.apply(this, arguments); + }, + start: function() { + var self = this; + new instance.web.Model("message_of_the_day").query(["message"]).first().then(function(result) { + self.$(".oe_mywidget_message_of_the_day").text(result.message); + }); + }, + }); + + } + + .. code-block:: xml + + + + + +
+
+
+ +
+

+
+
+
+ + .. code-block:: css + + .oe_petstore_motd { + margin: 5px; + padding: 5px; + border-radius: 3px; + background-color: #F0EEEE; + } + +.. exercise:: Pet Toys List + + Create a widget ``PetToysList`` that will display 5 toys on the home page + with their names and their images. + + In this Odoo addon, the pet toys are not stored in a new table like for + the message of the day. They are in the table ``product.product``. If you + click on the menu item :menuselection:`Pet Store --> Pet Store --> Pet + Toys` you will be able to see them. Pet toys are identified by the + category named ``Pet Toys``. You could need to document yourself on the + model ``product.product`` to be able to create a domain to select pet toys + and not all the products. + + To display the images of the pet toys, you should know that images in Odoo + can be queried from the database like any other fields, but you will + obtain a string containing Base64-encoded binary. There is a little trick + to display images in Base64 format in HTML: + + .. code-block:: html + + + + The ``PetToysList`` widget should be displayed on the home page on the + right of the ``MessageOfTheDay`` widget. You will need to make some layout + with CSS to achieve this. + + .. only:: solutions + + .. code-block:: javascript + + openerp.oepetstore = function(instance) { + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + + instance.oepetstore = {}; + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + template: "HomePage", + start: function() { + var pettoys = new instance.oepetstore.PetToysList(this); + pettoys.appendTo(this.$(".oe_petstore_homepage_left")); + var motd = new instance.oepetstore.MessageOfTheDay(this); + motd.appendTo(this.$(".oe_petstore_homepage_right")); + }, + }); + + instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + + instance.oepetstore.MessageOfTheDay = instance.web.Widget.extend({ + template: "MessageofTheDay", + init: function() { + this._super.apply(this, arguments); + }, + start: function() { + var self = this; + new instance.web.Model("message_of_the_day").query(["message"]).first().then(function(result) { + self.$(".oe_mywidget_message_of_the_day").text(result.message); + }); + }, + }); + + instance.oepetstore.PetToysList = instance.web.Widget.extend({ + template: "PetToysList", + start: function() { + var self = this; + new instance.web.Model("product.product").query(["name", "image"]) + .filter([["categ_id.name", "=", "Pet Toys"]]).limit(5).all().then(function(result) { + _.each(result, function(item) { + var $item = $(QWeb.render("PetToy", {item: item})); + self.$el.append($item); + }); + }); + }, + }); + + } + + .. code-block:: xml + + + + + +
+
+
+
+
+ +
+

+
+
+ +
+
+
+ +
+

+

+
+
+
+ + .. code-block:: css + + .oe_petstore_homepage { + display: table; + } + + .oe_petstore_homepage_left { + display: table-cell; + width : 300px; + } + + .oe_petstore_homepage_right { + display: table-cell; + width : 300px; + } + + .oe_petstore_motd { + margin: 5px; + padding: 5px; + border-radius: 3px; + background-color: #F0EEEE; + } + + .oe_petstore_pettoyslist { + padding: 5px; + } + + .oe_petstore_pettoy { + margin: 5px; + padding: 5px; + border-radius: 3px; + background-color: #F0EEEE; + } + + +Existing web components +----------------------- + +In the previous part, we explained the Odoo web framework, a development +framework to create and architecture graphical JavaScript applications. The +current part is dedicated to the existing components of the Odoo web client +and most notably those containing entry points for developers to create new +widgets that will be inserted inside existing views or components. + +The Action Manager +%%%%%%%%%%%%%%%%%% + +To display a view or show a popup, as example when you click on a menu button, +Odoo use the concept of actions. Actions are pieces of information explaining +what the web client should do. They can be loaded from the database or created +on-the-fly. The component handling actions in the web client is the *Action +Manager*. + +Using the Action Manager +'''''''''''''''''''''''' + +A way to launch an action is to use a menu element targeting an action +registered in the database. As a reminder, here is how is defined a typical +action and its associated menu item: + +.. code-block:: xml + + + 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({ + dispatch_to_new_action: function() { + this.do_action({ + type: 'ir.actions.act_window', + res_model: "product.product", + res_id: 1, + views: [[false, 'form']], + target: 'current', + context: {}, + }); + }, + }); + +The method to call to ask the action manager to execute a new action is +:func:`~openerp.web.Widget.do_action`. It receives as argument a dictionary +defining the properties of the action. Here is a description of the most usual +properties (not all of them may be used by all type of actions): + +* ``type``: The type of the action, which means the name of the model in which + the action is stored. As example, use ``ir.actions.act_window`` to show + views and ``ir.actions.client`` for client actions. +* ``res_model``: For ``act_window`` actions, it is the model used by the + views. +* ``res_id``: The ``id`` of the record to display. +* ``views``: For ``act_window`` actions, it is a list of the views to + display. This argument must be a list of tuples with two components. The + first one must be the identifier of the view (or ``false`` if you just want + to use the default view defined for the model). The second one must be the + type of the view. +* ``target``: If the value is ``current``, the action will be opened in the + main content part of the web client. The current action will be destroyed + before loading the new one. If it is ``new``, the action will appear in a + popup and the current action will not be destroyed. +* ``context``: The context to use. + +.. exercise:: Jump to Product + + Modify the ``PetToysList`` component developed in the previous part to + jump to a form view displaying the shown item when we click on the item in + the list. + + .. only:: solutions + + .. code-block:: javascript + + instance.oepetstore.PetToysList = instance.web.Widget.extend({ + template: "PetToysList", + start: function() { + var self = this; + new instance.web.Model("product.product").query(["name", "image"]) + .filter([["categ_id.name", "=", "Pet Toys"]]).limit(5).all().then(function(result) { + _.each(result, function(item) { + var $item = $(QWeb.render("PetToy", {item: item})); + self.$el.append($item); + $item.click(function() { + self.item_clicked(item); + }); + }); + }); + }, + item_clicked: function(item) { + this.do_action({ + type: 'ir.actions.act_window', + res_model: "product.product", + res_id: item.id, + views: [[false, 'form']], + target: 'current', + context: {}, + }); + }, + }); + +Client Actions +%%%%%%%%%%%%%% + +In the module installed during the previous part of this guide, we defined a +simple widget that was displayed when we clicked on a menu element. This is +because this widget was registered as a *client action*. Client actions are a +type of action that are completely defined by JavaScript code. Here is a +reminder of the way we defined this client action:: + + instance.oepetstore.HomePage = instance.web.Widget.extend({ + start: function() { + console.log("pet store home page loaded"); + }, + }); + + instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage'); + +``instance.web.client_actions`` is an instance of the +:class:`~openerp.web.Registry` class. Registries are not very different to +simple dictionaries, except they assign strings to class names. Adding the +``petstore.homepage`` key to this registry simply tells the web client "If +someone asks you to open a client action with key ``petstore.homepage``, +instantiate the ``instance.oepetstore.HomePage`` class and show it to the +user". + +Here is how the menu element to show this client action was defined: + +.. code-block:: xml + + + petstore.homepage + + + + +Client actions do not need a lot of information except their type, which is +stored in the ``tag`` field. + +When the web client wants to display a client action, it will simply show it +in the main content block of the web client. This is completely sufficient to +allow the widget to display anything and so create a completely new feature +for the web client. + +Architecture of the Views +%%%%%%%%%%%%%%%%%%%%%%%%% + +Most of the complexity of the web client resides in views. They are the basic +tools to display the data in the database. The part will explain the views +and how those are displayed in the web client. + +The View Manager +'''''''''''''''' + +Previously we already explained the purpose of the *Action Manager*. It is a +component, whose class is ``ActionManager``, that will handle the Odoo actions +(notably the actions associated with menu buttons). + +When an ``ActionManager`` instance receive an action with type +``ir.actions.act_window``, it knows it has to show one or more views +associated with a precise model. To do so, it creates a *View Manager* that +will create one or multiple *Views*. See this diagram: + +.. image:: web/viewarchitecture.* + :align: center + :width: 40% + +The ``ViewManager`` instance will instantiate each view class corresponding to +the views indicated in the ``ir.actions.act_window`` action. As example, the +class corresponding to the view type ``form`` is ``FormView``. Each view class +inherits the ``View`` abstract class. + +The Views +''''''''' + +All the typical type of views in Odoo (all those you can switch to using the +small buttons under the search input text) are represented by a class +extending the ``View`` abstract class. Note the *Search View* (the search +input text on the top right of the screen that typically appear in kanban and +list views) is also considered a type of view even if it doesn't work like the +others (you can not "switch to" the search view and it doesn't take the full +screen). + +A view has the responsibility to load its XML view description from the server +and display it. Views are also given an instance of the ``DataSet`` +class. That class contains a list of identifiers corresponding to records that +the view should display. It is filled by the search view and the current view +is supposed to display the result of each search after it was performed by the +search view. + +The Form View Fields +%%%%%%%%%%%%%%%%%%%% + +A typical need in the web client is to extend the form view to display more +specific widgets. One of the possibilities to do this is to define a new type +of *Field*. + +A field, in the form view, is a type of widget designed to display and edit +the content of *one (and only one) field* in a single record displayed by the +form view. All data types available in models have a default implementation to +display and edit them in the form view. As example, the ``FieldChar`` class +allows to edit the ``char`` data type. + +Other field classes simply provide an alternative widget to represent an +existing data type. A good example of this is the ``FieldEmail`` class. There +is no ``email`` type in the models of Odoo. That class is designed to display +a ``char`` field assuming it contains an email (it will show a clickable link +to directly send a mail to the person and will also check the validity of the +mail address). + +Also note there is nothing that disallow a field class to work with more than +one data type. As example, the ``FieldSelection`` class works with both +``selection`` and ``many2one`` field types. + +As a reminder, to indicate a precise field type in a form view XML +description, you just have to specify the ``widget`` attribute: + +.. code-block:: xml + + + +It is also a good thing to notice that the form view field classes are also +used in the editable list views. So, by defining a new field class, it make +this new widget available in both views. + +Another type of extension mechanism for the form view is the *Form Widget*, +which has fewer restrictions than the fields (even though it can be more +complicated to implement). Form widgets will be explained later in this guide. + +Fields are instantiated by the form view after it has read its XML description +and constructed the corresponding HTML representing that description. After +that, the form view will communicate with the field objects using some +methods. Theses methods are defined by the ``FieldInterface`` +interface. Almost all fields inherit the ``AbstractField`` abstract +class. That class defines some default mechanisms that need to be implemented +by most fields. + +Here are some of the responsibilities of a field class: + +* The field class must display and allow the user to edit the value of the field. +* It must correctly implement the 3 field attributes available in all fields + of Odoo. The ``AbstractField`` class already implements an algorithm that + dynamically calculates the value of these attributes (they can change at any + moment because their value change according to the value of other + fields). Their values are stored in *Widget Properties* (the widget + properties were explained earlier in this guide). It is the responsibility + of each field class to check these widget properties and dynamically adapt + depending of their values. Here is a description of each of these + attributes: + + * ``required``: The field must have a value before saving. If ``required`` + is ``true`` and the field doesn't have a value, the method + ``is_valid()`` of the field must return ``false``. + * ``invisible``: When this is ``true``, the field must be invisible. The + ``AbstractField`` class already has a basic implementation of this + behavior that fits most fields. + * ``readonly``: When ``true``, the field must not be editable by the + user. Most fields in Odoo have a completely different behavior depending + on the value of ``readonly``. As example, the ``FieldChar`` displays an + HTML ```` when it is editable and simply displays the text when + it is read-only. This also means it has much more code it would need to + implement only one behavior, but this is necessary to ensure a good user + experience. + +* Fields have two methods, ``set_value()`` and ``get_value()``, which are + called by the form view to give it the value to display and get back the new + value entered by the user. These methods must be able to handle the value as + given by the Odoo server when a ``read()`` is performed on a model and give + back a valid value for a ``write()``. Remember that the JavaScript/Python + data types used to represent the values given by ``read()`` and given to + ``write()`` is not necessarily the same in Odoo. As example, when you read a + many2one, it is always a tuple whose first value is the id of the pointed + record and the second one is the name get (ie: ``(15, "Agrolait")``). But + when you write a many2one it must be a single integer, not a tuple + anymore. ``AbstractField`` has a default implementation of these methods + that works well for simple data type and set a widget property named + ``value``. + +Please note that, to better understand how to implement fields, you are +strongly encouraged to look at the definition of the ``FieldInterface`` +interface and the ``AbstractField`` class directly in the code of the Odoo web +client. + +Creating a New Type of Field +'''''''''''''''''''''''''''' + +In this part we will explain how to create a new type of field. The example +here will be to re-implement the ``FieldChar`` class and explain progressively +each part. + +Simple Read-Only Field +"""""""""""""""""""""" + +Here is a first implementation that will only be able to display a text. The +user will not be able to modify the content of the field. + +.. code-block:: javascript + + instance.oepetstore.FieldChar2 = instance.web.form.AbstractField.extend({ + init: function() { + this._super.apply(this, arguments); + this.set("value", ""); + }, + render_value: function() { + this.$el.text(this.get("value")); + }, + }); + + instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2'); + +In this example, we declare a class named ``FieldChar2`` inheriting from +``AbstractField``. We also register this class in the registry +``instance.web.form.widgets`` under the key ``char2``. That will allow us to +use this new field in any form view by specifying ``widget="char2"`` in the +```` tag in the XML declaration of the view. + +In this example, we define a single method: ``render_value()``. All it does is +display the widget property ``value``. Those are two tools defined by the +``AbstractField`` class. As explained before, the form view will call the +method ``set_value()`` of the field to set the value to display. This method +already has a default implementation in ``AbstractField`` which simply sets +the widget property ``value``. ``AbstractField`` also watch the +``change:value`` event on itself and calls the ``render_value()`` when it +occurs. So, ``render_value()`` is a convenience method to implement in child +classes to perform some operation each time the value of the field changes. + +In the ``init()`` method, we also define the default value of the field if +none is specified by the form view (here we assume the default value of a +``char`` field should be an empty string). + +Read-Write Field +"""""""""""""""" + +Fields that only display their content and don't give the possibility to the +user to modify it can be useful, but most fields in Odoo allow edition +too. This makes the field classes more complicated, mostly because fields are +supposed to handle both and editable and non-editable mode, those modes are +often completely different (for design and usability purpose) and the fields +must be able to switch from one mode to another at any moment. + +To know in which mode the current field should be, the ``AbstractField`` class +sets a widget property named ``effective_readonly``. The field should watch +the changes in that widget property and display the correct mode +accordingly. Example:: + + instance.oepetstore.FieldChar2 = instance.web.form.AbstractField.extend({ + init: function() { + this._super.apply(this, arguments); + this.set("value", ""); + }, + start: function() { + this.on("change:effective_readonly", this, function() { + this.display_field(); + this.render_value(); + }); + this.display_field(); + return this._super(); + }, + display_field: function() { + var self = this; + this.$el.html(QWeb.render("FieldChar2", {widget: this})); + if (! this.get("effective_readonly")) { + this.$("input").change(function() { + self.internal_set_value(self.$("input").val()); + }); + } + }, + render_value: function() { + if (this.get("effective_readonly")) { + this.$el.text(this.get("value")); + } else { + this.$("input").val(this.get("value")); + } + }, + }); + + instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2'); + +.. code-block:: xml + + +
+ + + +
+
+ +In the ``start()`` method (which is called right after a widget has been +appended to the DOM), we bind on the event ``change:effective_readonly``. That +will allow use to redisplay the field each time the widget property +``effective_readonly`` changes. This event handler will call +``display_field()``, which is also called directly in ``start()``. This +``display_field()`` was created specifically for this field, it's not a method +defined in ``AbstractField`` or any other class. This is the method we will +use to display the content of the field depending we are in read-only mode or +not. + +From now on the conception of this field is quite typical, except there is a +lot of verifications to know the state of the ``effective_readonly`` property: + +* In the QWeb template used to display the content of the widget, it displays + an ```` if we are in read-write mode and nothing in + particular in read-only mode. +* In the ``display_field()`` method, we have to bind on the ``change`` event + of the ```` to know when the user has changed the + value. When it happens, we call the ``internal_set_value()`` method with the + new value of the field. This is a convenience method provided by the + ``AbstractField`` class. That method will set a new value in the ``value`` + property but will not trigger a call to ``render_value()`` (which is not + necessary since the ```` already contains the correct + value). +* In ``render_value()``, we use a completely different code to display the + value of the field depending if we are in read-only or in read-write mode. + +.. exercise:: Create a Color Field + + Create a ``FieldColor`` class. The value of this field should be a string + containing a color code like those used in CSS (example: ``#FF0000`` for + red). In read-only mode, this color field should display a little block + whose color corresponds to the value of the field. In read-write mode, you + should display an ````. That type of ```` + is an HTML5 component that doesn't work in all browsers but works well in + Google Chrome. So it's OK to use as an exercise. + + You can use that widget in the form view of the ``message_of_the_day`` + model for its field named ``color``. As a bonus, you can change the + ``MessageOfTheDay`` widget created in the previous part of this guide to + display the message of the day with the background color indicated in the + ``color`` field. + + .. only:: solutions + + .. code-block:: javascript + + instance.oepetstore.FieldColor = instance.web.form.AbstractField.extend({ + init: function() { + this._super.apply(this, arguments); + this.set("value", ""); + }, + start: function() { + this.on("change:effective_readonly", this, function() { + this.display_field(); + this.render_value(); + }); + this.display_field(); + return this._super(); + }, + display_field: function() { + var self = this; + this.$el.html(QWeb.render("FieldColor", {widget: this})); + if (! this.get("effective_readonly")) { + this.$("input").change(function() { + self.internal_set_value(self.$("input").val()); + }); + } + }, + render_value: function() { + if (this.get("effective_readonly")) { + this.$(".oe_field_color_content").css("background-color", this.get("value") || "#FFFFFF"); + } else { + this.$("input").val(this.get("value") || "#FFFFFF"); + } + }, + }); + + instance.web.form.widgets.add('color', 'instance.oepetstore.FieldColor'); + + .. code-block:: xml + + +
+ +
+ + + + +
+
+ + .. code-block:: css + + .oe_field_color_content { + height: 20px; + width: 50px; + border: 1px solid black; + } + +The Form View Custom Widgets +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +Form fields can be useful, but their purpose is to edit a single field. To +interact with the whole form view and have more liberty to integrate new +widgets in it, it is recommended to create a custom form widget. + +Custom form widgets are widgets that can be added in any form view using a +specific syntax in the XML definition of the view. Example: + +.. code-block:: xml + + + +This type of widget will simply be created by the form view during the +creation of the HTML according to the XML definition. They have properties in +common with the fields (like the ``effective_readonly`` property) but they are +not assigned a precise field. And so they don't have methods like +``get_value()`` and ``set_value()``. They must inherit from the ``FormWidget`` +abstract class. + +The custom form widgets can also interact with the fields of the form view by +getting or setting their values using the ``field_manager`` attribute of +``FormWidget``. Here is an example usage:: + + instance.oepetstore.WidgetMultiplication = instance.web.form.FormWidget.extend({ + start: function() { + this._super(); + this.field_manager.on("field_changed:integer_a", this, this.display_result); + this.field_manager.on("field_changed:integer_b", this, this.display_result); + this.display_result(); + }, + display_result: function() { + var result = this.field_manager.get_field_value("integer_a") * + this.field_manager.get_field_value("integer_b"); + this.$el.text("a*b = " + result); + } + }); + + instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication'); + +This example custom widget is designed to take the values of two existing +fields (those must exist in the form view) and print the result of their +multiplication. It also refreshes each time the value of any of those fields +changes. + +The ``field_manager`` attribute is in fact the ``FormView`` instance +representing the form view. The methods that widgets can call on that form +view are documented in the code of the web client in the ``FieldManagerMixin`` +interface. The most useful features are: + +* The method ``get_field_value()`` which returns the value of a field. +* When the value of a field is changed, for any reason, the form view will + trigger an event named ``field_changed:xxx`` where ``xxx`` is the name of + the field. +* Also, it is possible to change the value of the fields using the method + ``set_values()``. This method takes a dictionary as first and only argument + whose keys are the names of the fields to change and values are the new + values. + +.. exercise:: Show Coordinates on Google Map + + In this exercise we would like to add two new fields on the + ``product.product`` model: ``provider_latitude`` and + ``provider_longitude``. Those would represent coordinates on a map. We + also would like you to create a custom widget able to display a map + showing these coordinates. + + To display that map, you can simply use the Google Map service using an HTML code similar to this: + + .. code-block:: html + + + + Just replace ``XXX`` with the latitude and ``YYY`` with the longitude. + + You should display those two new fields as well as the map widget in a new + page of the notebook displayed in the product form view. + + .. only:: solutions + + .. code-block:: javascript + + instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({ + start: function() { + this._super(); + this.field_manager.on("field_changed:provider_latitude", this, this.display_map); + this.field_manager.on("field_changed:provider_longitude", this, this.display_map); + this.display_map(); + }, + display_map: function() { + this.$el.html(QWeb.render("WidgetCoordinates", { + "latitude": this.field_manager.get_field_value("provider_latitude") || 0, + "longitude": this.field_manager.get_field_value("provider_longitude") || 0, + })); + } + }); + + instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates'); + + .. code-block:: xml + + + + + +.. exercise:: Get the Current Coordinate + + Now we would like to display an additional button to automatically set the + coordinates to the location of the current user. + + To get the coordinates of the user, an easy way is to use the geolocation + JavaScript API. `See the online documentation to know how to use it`_. + + .. _See the online documentation to know how to use it: http://www.w3schools.com/html/html5_geolocation.asp + + Please also note that it wouldn't be very logical to allow the user to + click on that button when the form view is in read-only mode. So, this + custom widget should handle correctly the ``effective_readonly`` property + just like any field. One way to do this would be to make the button + disappear when ``effective_readonly`` is true. + + .. only:: solutions + + .. code-block:: javascript + + instance.oepetstore.WidgetCoordinates = instance.web.form.FormWidget.extend({ + start: function() { + this._super(); + this.field_manager.on("field_changed:provider_latitude", this, this.display_map); + this.field_manager.on("field_changed:provider_longitude", this, this.display_map); + this.on("change:effective_readonly", this, this.display_map); + this.display_map(); + }, + display_map: function() { + var self = this; + this.$el.html(QWeb.render("WidgetCoordinates", { + "latitude": this.field_manager.get_field_value("provider_latitude") || 0, + "longitude": this.field_manager.get_field_value("provider_longitude") || 0, + })); + this.$("button").toggle(! this.get("effective_readonly")); + this.$("button").click(function() { + navigator.geolocation.getCurrentPosition(_.bind(self.received_position, self)); + }); + }, + received_position: function(obj) { + var la = obj.coords.latitude; + var lo = obj.coords.longitude; + this.field_manager.set_values({ + "provider_latitude": la, + "provider_longitude": lo, + }); + }, + }); + + instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates'); + + .. code-block:: xml + + + + + + +.. _jQuery: http://jquery.org +.. _Underscore.js: http://underscorejs.org diff --git a/doc/howtos/web/qweb.png b/doc/howtos/web/qweb.png new file mode 100644 index 0000000000000000000000000000000000000000..2e2ca376f3caa1dac908dd923564ab6e2a797369 GIT binary patch literal 40390 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 literal 0 HcmV?d00001 diff --git a/doc/howtos/web/viewarchitecture.dia b/doc/howtos/web/viewarchitecture.dia new file mode 100644 index 0000000000000000000000000000000000000000..0c28960a1ed86c5d1eeba6c2cd8a5b57cac27b97 GIT binary patch literal 1918 zcmZ|Dc{~#g1Hf@P!sB|Bo)zYLT~P{?dp(k4JVr4`iL#LUzK=+FZESNc$HvIHX^xuf zq4KykQZvUSlCv3xM3_9E_rLd#=lOmA`^h7Pc>WhJ-v;g;`lEqV24~f!VmDY%ubB!6 z@@=4u`Bw+Q{9eu8efUK1Pdl$kZC&pxdu`;iy;i)bnEB$-uL@!%DG?q=28M?ATd6Ue z$9=arcxnN1X`pwA5`Rg!|EU^7hCNgMx<{O}-{!(mE$g8_c7!bA=l%m}ZIlV98Ry!) zKH1tzsr0rf#HcTb6tw(&3_?8e)L*;X=F&a)B2^d$N@$EN)KEEyCAeN8Xm>RafVkj7 zuiQUKFk$&G5h0t|E+I z6d2G?jHRpXPB+EJMNn`FD^Qhk+3R7pm;Fg5TUjMJ$>+nTXoq8%?+N&wus-qcc(paJ z6)aZU6NfX24~gN#f=HwZ4>{FS5yc5VmmhS_pDn^0jL?GoFTF$f96NfDDFJKGCTt6l zQCgC8mHhMr^vrN1`PsbE!tSHGAII?lgKAz}eG!;8GsF|T@39ZI0NFJL@m4Y69ZXsKKFgo4(oP3QagXR#(EBDYK4LqMK*g4URbM)ahwh$HnAx8V zkcivZb?sZYrT(y`T){oqp~G4ZvL{GEDS@Sb{rsv8t;JWO4aU{SYd_g)+TEJTf`!+F z4?LH!%uOU@L64W-F0o{*owTVpqxq*;QAHor9*vyw9svW{N#8HtIcE^fG99)+9=NxF zfI)NCIkv($S4JQ{2hvD>ASMWdc7GJolf4~7ScuhK=@5jhu2|(0KNu&)qk1pQid2^V zy2jjC2Y5)c3ZCUC75%ZF_VpTeyUS1A2v?-bK|-y;PtRgc$vo)dz5({p3}IrCGnai= zP*!xm^c`T@Hz}zj!U#%HQd+5sj$@*arUSZ-rN2FjgZC>*vdS`ys5TQi!|jp1FLkyd zmy~^5a6b=AKGX=BIk0-!Xr+=wpcS{AqV~=G=CJF~L3Us z5!nL92OhjJu18*pFDuQPF-?(aAJbfiRj%)s8u5F1Lr< zY(I8vHcbqbkpIND_)lbfxe|UFH}Gh$%w34tAskF0Wj|8t$_PoGQLMr?^032&J zo$wyKBe(fMt<&KKqCacB;AZDUE z5MaeEJ?#!>3r*&x-rKlyHhV1mT&O635Gf7Iuhu}xP`^P#y7E?eYGoxYowU2!P7 zJmx#ShqE>#DFeK$PAe-ozVEXb&F_!GcPe}prj=wn8_2YqG8%juZtpMQ9CeqA_WJ>( zVxu#NOsr`C0NV^#;Xq@XO>LjA`m*KSw|IJ@!ooT_Lkm1HuDI!DXZ*<^lPTJK5ZAxb zy5`ocmi3OA-19ar<(sE`=6qsZeYE(++baWhiU#JP8S{zi21obOzWEzh+Ny?-on;O~ TH2e}1zF)8GIp^t&nF=f literal 0 HcmV?d00001 diff --git a/doc/howtos/web/viewarchitecture.png b/doc/howtos/web/viewarchitecture.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa403bb6ec9b423417024a8cc453f61ec197e46 GIT binary patch literal 35268 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 + + + + + + ActionManager + + + + + + + + + View + + + + + + + + + ViewManager + + + + + + + + + + + + 1 + + + + + + + + 1..* + + + + + FormView + + + + + + + + + ListView + + + + + + + + + KanbanView + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/tutorials.rst b/doc/tutorials.rst index 14607cfa050..5cbfec9bae6 100644 --- a/doc/tutorials.rst +++ b/doc/tutorials.rst @@ -7,3 +7,4 @@ Tutorials howtos/website howtos/backend + howtos/web