PageRenderTime 69ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 1ms

/doc/howtos/web.rst

https://gitlab.com/thanhchatvn/cloud-odoo
ReStructuredText | 1457 lines | 1100 code | 357 blank | 0 comment | 0 complexity | 3c0a28578f96239e77b68f6cc30eb8dd MD5 | raw file
  1. :banner: banners/build_interface_ext.jpg
  2. =============================
  3. Building Interface Extensions
  4. =============================
  5. .. highlight:: javascript
  6. .. default-domain:: js
  7. This guide is about creating modules for Odoo's web client.
  8. To create websites with Odoo, see :doc:`website`; to add business capabilities
  9. or extend existing business systems of Odoo, see :doc:`backend`.
  10. .. warning::
  11. This guide assumes knowledge of:
  12. * Javascript basics and good practices
  13. * jQuery_
  14. * `Underscore.js`_
  15. It also requires :ref:`an installed Odoo <setup/install>`, and Git_.
  16. A Simple Module
  17. ===============
  18. Let's start with a simple Odoo module holding basic web component
  19. configuration and letting us test the web framework.
  20. The example module is available online and can be downloaded using the
  21. following command:
  22. .. code-block:: console
  23. $ git clone http://github.com/odoo/petstore
  24. This will create a ``petstore`` folder wherever you executed the command.
  25. You then need to add that folder to Odoo's
  26. :option:`addons path <odoo.py --addons-path>`, create a new database and
  27. install the ``oepetstore`` module.
  28. If you browse the ``petstore`` folder, you should see the following content:
  29. .. code-block:: text
  30. oepetstore
  31. |-- images
  32. | |-- alligator.jpg
  33. | |-- ball.jpg
  34. | |-- crazy_circle.jpg
  35. | |-- fish.jpg
  36. | `-- mice.jpg
  37. |-- __init__.py
  38. |-- oepetstore.message_of_the_day.csv
  39. |-- __openerp__.py
  40. |-- petstore_data.xml
  41. |-- petstore.py
  42. |-- petstore.xml
  43. `-- static
  44. `-- src
  45. |-- css
  46. | `-- petstore.css
  47. |-- js
  48. | `-- petstore.js
  49. `-- xml
  50. `-- petstore.xml
  51. The module already holds various server customizations. We'll come back to
  52. these later, for now let's focus on the web-related content, in the ``static``
  53. folder.
  54. Files used in the "web" side of an Odoo module must be placed in a ``static``
  55. folder so they are available to a web browser, files outside that folder can
  56. not be fetched by browsers. The ``src/css``, ``src/js`` and ``src/xml``
  57. sub-folders are conventional and not strictly necessary.
  58. ``oepetstore/static/css/petstore.css``
  59. Currently empty, will hold the CSS_ for pet store content
  60. ``oepetstore/static/xml/petstore.xml``
  61. Mostly empty, will hold :ref:`reference/qweb` templates
  62. ``oepetstore/static/js/petstore.js``
  63. The most important (and interesting) part, contains the logic of the
  64. application (or at least its web-browser side) as javascript. It should
  65. currently look like::
  66. openerp.oepetstore = function(instance, local) {
  67. var _t = instance.web._t,
  68. _lt = instance.web._lt;
  69. var QWeb = instance.web.qweb;
  70. local.HomePage = instance.Widget.extend({
  71. start: function() {
  72. console.log("pet store home page loaded");
  73. },
  74. });
  75. instance.web.client_actions.add(
  76. 'petstore.homepage', 'instance.oepetstore.HomePage');
  77. }
  78. Which only prints a small message in the browser's console.
  79. The files in the ``static`` folder, need to be defined within the module in order for them to be loaded correctly. Everything in ``src/xml`` is defined in ``__openerp__.py`` while the contents of ``src/css`` and ``src/js`` are defined in ``petstore.xml``, or a similar file.
  80. .. warning::
  81. All JavaScript files are concatenated and :term:`minified` to improve
  82. application load time.
  83. One of the drawback is debugging becomes more difficult as
  84. individual files disappear and the code is made significantly less
  85. readable. It is possible to disable this process by enabling the
  86. "developer mode": log into your Odoo instance (user *admin* password
  87. *admin* by default) open the user menu (in the top-right corner of the
  88. Odoo screen) and select :guilabel:`About Odoo` then :guilabel:`Activate
  89. the developer mode`:
  90. .. image:: web/about_odoo.png
  91. :align: center
  92. .. image:: web/devmode.png
  93. :align: center
  94. This will reload the web client with optimizations disabled, making
  95. development and debugging significantly more comfortable.
  96. .. todo:: qweb files hooked via __openerp__.py, but js and CSS use bundles
  97. Odoo JavaScript Module
  98. ======================
  99. Javascript doesn't have built-in modules. As a result variables defined in
  100. different files are all mashed together and may conflict. This has given rise
  101. to various module patterns used to build clean namespaces and limit risks of
  102. naming conflicts.
  103. The Odoo framework uses one such pattern to define modules within web addons,
  104. in order to both namespace code and correctly order its loading.
  105. ``oepetstore/static/js/petstore.js`` contains a module declaration::
  106. openerp.oepetstore = function(instance, local) {
  107. local.xxx = ...;
  108. }
  109. In Odoo web, modules are declared as functions set on the global ``openerp``
  110. variable. The function's name must be the same as the addon (in this case
  111. ``oepetstore``) so the framework can find it, and automatically initialize it.
  112. When the web client loads your module it will call the root function
  113. and provide two parameters:
  114. * the first parameter is the current instance of the Odoo web client, it gives
  115. access to various capabilities defined by the Odoo (translations,
  116. network services) as well as objects defined by the core or by other
  117. modules.
  118. * the second parameter is your own local namespace automatically created by
  119. the web client. Objects and variables which should be accessible from
  120. outside your module (either because the Odoo web client needs to call them
  121. or because others may want to customize them) should be set inside that
  122. namespace.
  123. Classes
  124. =======
  125. Much as modules, and contrary to most object-oriented languages, javascript
  126. does not build in *classes*\ [#classes]_ although it provides roughly
  127. equivalent (if lower-level and more verbose) mechanisms.
  128. For simplicity and developer-friendliness Odoo web provides a class
  129. system based on John Resig's `Simple JavaScript Inheritance`_.
  130. New classes are defined by calling the :func:`~openerp.web.Class.extend`
  131. method of :class:`openerp.web.Class`::
  132. var MyClass = instance.web.Class.extend({
  133. say_hello: function() {
  134. console.log("hello");
  135. },
  136. });
  137. The :func:`~openerp.web.Class.extend` method takes a dictionary describing
  138. the new class's content (methods and static attributes). In this case, it will
  139. only have a ``say_hello`` method which takes no parameters.
  140. Classes are instantiated using the ``new`` operator::
  141. var my_object = new MyClass();
  142. my_object.say_hello();
  143. // print "hello" in the console
  144. And attributes of the instance can be accessed via ``this``::
  145. var MyClass = instance.web.Class.extend({
  146. say_hello: function() {
  147. console.log("hello", this.name);
  148. },
  149. });
  150. var my_object = new MyClass();
  151. my_object.name = "Bob";
  152. my_object.say_hello();
  153. // print "hello Bob" in the console
  154. Classes can provide an initializer to perform the initial setup of the
  155. instance, by defining an ``init()`` method. The initializer receives the
  156. parameters passed when using the ``new`` operator::
  157. var MyClass = instance.web.Class.extend({
  158. init: function(name) {
  159. this.name = name;
  160. },
  161. say_hello: function() {
  162. console.log("hello", this.name);
  163. },
  164. });
  165. var my_object = new MyClass("Bob");
  166. my_object.say_hello();
  167. // print "hello Bob" in the console
  168. It is also possible to create subclasses from existing (used-defined) classes
  169. by calling :func:`~openerp.web.Class.extend` on the parent class, as is done
  170. to subclass :class:`~openerp.web.Class`::
  171. var MySpanishClass = MyClass.extend({
  172. say_hello: function() {
  173. console.log("hola", this.name);
  174. },
  175. });
  176. var my_object = new MySpanishClass("Bob");
  177. my_object.say_hello();
  178. // print "hola Bob" in the console
  179. When overriding a method using inheritance, you can use ``this._super()`` to
  180. call the original method::
  181. var MySpanishClass = MyClass.extend({
  182. say_hello: function() {
  183. this._super();
  184. console.log("translation in Spanish: hola", this.name);
  185. },
  186. });
  187. var my_object = new MySpanishClass("Bob");
  188. my_object.say_hello();
  189. // print "hello Bob \n translation in Spanish: hola Bob" in the console
  190. .. warning::
  191. ``_super`` is not a standard method, it is set on-the-fly to the next
  192. method in the current inheritance chain, if any. It is only defined
  193. during the *synchronous* part of a method call, for use in asynchronous
  194. handlers (after network calls or in ``setTimeout`` callbacks) a reference
  195. to its value should be retained, it should not be accessed via ``this``::
  196. // broken, will generate an error
  197. say_hello: function () {
  198. setTimeout(function () {
  199. this._super();
  200. }.bind(this), 0);
  201. }
  202. // correct
  203. say_hello: function () {
  204. // don't forget .bind()
  205. var _super = this._super.bind(this);
  206. setTimeout(function () {
  207. _super();
  208. }.bind(this), 0);
  209. }
  210. Widgets Basics
  211. ==============
  212. The Odoo web client bundles jQuery_ for easy DOM manipulation. It is useful
  213. and provides a better API than standard `W3C DOM`_\ [#dombugs]_, but
  214. insufficient to structure complex applications leading to difficult
  215. maintenance.
  216. Much like object-oriented desktop UI toolkits (e.g. Qt_, Cocoa_ or GTK_),
  217. Odoo Web makes specific components responsible for sections of a page. In
  218. Odoo web, the base for such components is the :class:`~openerp.Widget`
  219. class, a component specialized in handling a page section and displaying
  220. information for the user.
  221. Your First Widget
  222. -----------------
  223. The initial demonstration module already provides a basic widget::
  224. local.HomePage = instance.Widget.extend({
  225. start: function() {
  226. console.log("pet store home page loaded");
  227. },
  228. });
  229. It extends :class:`~openerp.Widget` and overrides the standard method
  230. :func:`~openerp.Widget.start`, which — much like the previous ``MyClass``
  231. — does little for now.
  232. This line at the end of the file::
  233. instance.web.client_actions.add(
  234. 'petstore.homepage', 'instance.oepetstore.HomePage');
  235. registers our basic widget as a client action. Client actions will be
  236. explained later, for now this is just what allows our widget to
  237. be called and displayed when we select the
  238. :menuselection:`Pet Store --> Pet Store --> Home Page` menu.
  239. .. warning::
  240. because the widget will be called from outside our module, the web client
  241. needs its "fully qualified" name, not the local version.
  242. Display Content
  243. ---------------
  244. Widgets have a number of methods and features, but the basics are simple:
  245. * set up a widget
  246. * format the widget's data
  247. * display the widget
  248. The ``HomePage`` widget already has a :func:`~openerp.Widget.start`
  249. method. That method is part of the normal widget lifecycle and automatically
  250. called once the widget is inserted in the page. We can use it to display some
  251. content.
  252. All widgets have a :attr:`~openerp.Widget.$el` which represents the
  253. section of page they're in charge of (as a jQuery_ object). Widget content
  254. should be inserted there. By default, :attr:`~openerp.Widget.$el` is an
  255. empty ``<div>`` element.
  256. A ``<div>`` element is usually invisible to the user if it has no content (or
  257. without specific styles giving it a size) which is why nothing is displayed
  258. on the page when ``HomePage`` is launched.
  259. Let's add some content to the widget's root element, using jQuery::
  260. local.HomePage = instance.Widget.extend({
  261. start: function() {
  262. this.$el.append("<div>Hello dear Odoo user!</div>");
  263. },
  264. });
  265. That message will now appear when you open :menuselection:`Pet Store
  266. --> Pet Store --> Home Page`
  267. .. note::
  268. to refresh the javascript code loaded in Odoo Web, you will need to reload
  269. the page. There is no need to restart the Odoo server.
  270. The ``HomePage`` widget is used by Odoo Web and managed automatically.
  271. To learn how to use a widget "from scratch" let's create a new one::
  272. local.GreetingsWidget = instance.Widget.extend({
  273. start: function() {
  274. this.$el.append("<div>We are so happy to see you again in this menu!</div>");
  275. },
  276. });
  277. We can now add our ``GreetingsWidget`` to the ``HomePage`` by using the
  278. ``GreetingsWidget``'s :func:`~openerp.Widget.appendTo` method::
  279. local.HomePage = instance.Widget.extend({
  280. start: function() {
  281. this.$el.append("<div>Hello dear Odoo user!</div>");
  282. var greeting = new local.GreetingsWidget(this);
  283. return greeting.appendTo(this.$el);
  284. },
  285. });
  286. * ``HomePage`` first adds its own content to its DOM root
  287. * ``HomePage`` then instantiates ``GreetingsWidget``
  288. * Finally it tells ``GreetingsWidget`` where to insert itself, delegating part
  289. of its :attr:`~openerp.Widget.$el` to the ``GreetingsWidget``.
  290. When the :func:`~openerp.Widget.appendTo` method is called, it asks the
  291. widget to insert itself at the specified position and to display its content.
  292. The :func:`~openerp.Widget.start` method will be called during the call
  293. to :func:`~openerp.Widget.appendTo`.
  294. To see what happens under the displayed interface, we will use the browser's
  295. DOM Explorer. But first let's alter our widgets slightly so we can more easily
  296. find where they are, by :attr:`adding a class to their root elements
  297. <openerp.Widget.className>`::
  298. local.HomePage = instance.Widget.extend({
  299. className: 'oe_petstore_homepage',
  300. ...
  301. });
  302. local.GreetingsWidget = instance.Widget.extend({
  303. className: 'oe_petstore_greetings',
  304. ...
  305. });
  306. If you can find the relevant section of the DOM (right-click on the text
  307. then :guilabel:`Inspect Element`), it should look like this:
  308. .. code-block:: html
  309. <div class="oe_petstore_homepage">
  310. <div>Hello dear Odoo user!</div>
  311. <div class="oe_petstore_greetings">
  312. <div>We are so happy to see you again in this menu!</div>
  313. </div>
  314. </div>
  315. Which clearly shows the two ``<div>`` elements automatically created by
  316. :class:`~openerp.Widget`, because we added some classes on them.
  317. We can also see the two message-holding divs we added ourselves
  318. Finally, note the ``<div class="oe_petstore_greetings">`` element which
  319. represents the ``GreetingsWidget`` instance is *inside* the
  320. ``<div class="oe_petstore_homepage">`` which represents the ``HomePage``
  321. instance, since we appended
  322. Widget Parents and Children
  323. ---------------------------
  324. In the previous part, we instantiated a widget using this syntax::
  325. new local.GreetingsWidget(this);
  326. The first argument is ``this``, which in that case was a ``HomePage``
  327. instance. This tells the widget being created which other widget is its
  328. *parent*.
  329. As we've seen, widgets are usually inserted in the DOM by another widget and
  330. *inside* that other widget's root element. This means most widgets are "part"
  331. of another widget, and exist on behalf of it. We call the container the
  332. *parent*, and the contained widget the *child*.
  333. Due to multiple technical and conceptual reasons, it is necessary for a widget
  334. to know who is its parent and who are its children.
  335. :func:`~openerp.Widget.getParent`
  336. can be used to get the parent of a widget::
  337. local.GreetingsWidget = instance.Widget.extend({
  338. start: function() {
  339. console.log(this.getParent().$el );
  340. // will print "div.oe_petstore_homepage" in the console
  341. },
  342. });
  343. :func:`~openerp.Widget.getChildren`
  344. can be used to get a list of its children::
  345. local.HomePage = instance.Widget.extend({
  346. start: function() {
  347. var greeting = new local.GreetingsWidget(this);
  348. greeting.appendTo(this.$el);
  349. console.log(this.getChildren()[0].$el);
  350. // will print "div.oe_petstore_greetings" in the console
  351. },
  352. });
  353. When overriding the :func:`~openerp.Widget.init` method of a widget it is
  354. *of the utmost importance* to pass the parent to the ``this._super()`` call,
  355. otherwise the relation will not be set up correctly::
  356. local.GreetingsWidget = instance.Widget.extend({
  357. init: function(parent, name) {
  358. this._super(parent);
  359. this.name = name;
  360. },
  361. });
  362. Finally, if a widget does not have a parent (e.g. because it's the root
  363. widget of the application), ``null`` can be provided as parent::
  364. new local.GreetingsWidget(null);
  365. Destroying Widgets
  366. ------------------
  367. If you can display content to your users, you should also be able to erase
  368. it. This is done via the :func:`~openerp.Widget.destroy` method::
  369. greeting.destroy();
  370. When a widget is destroyed it will first call
  371. :func:`~openerp.Widget.destroy` on all its children. Then it erases itself
  372. from the DOM. If you have set up permanent structures in
  373. :func:`~openerp.Widget.init` or :func:`~openerp.Widget.start` which
  374. must be explicitly cleaned up (because the garbage collector will not handle
  375. them), you can override :func:`~openerp.Widget.destroy`.
  376. .. danger::
  377. when overriding :func:`~openerp.Widget.destroy`, ``_super()``
  378. *must always* be called otherwise the widget and its children are not
  379. correctly cleaned up leaving possible memory leaks and "phantom events",
  380. even if no error is displayed
  381. The QWeb Template Engine
  382. ========================
  383. In the previous section we added content to our widgets by directly
  384. manipulating (and adding to) their DOM::
  385. this.$el.append("<div>Hello dear Odoo user!</div>");
  386. This allows generating and displaying any type of content, but gets unwieldy
  387. when generating significant amounts of DOM (lots of duplication, quoting
  388. issues, ...)
  389. As many other environments, Odoo's solution is to use a `template engine`_.
  390. Odoo's template engine is called :ref:`reference/qweb`.
  391. QWeb is an XML-based templating language, similar to `Genshi
  392. <http://en.wikipedia.org/wiki/Genshi_(templating_language)>`_, `Thymeleaf
  393. <http://en.wikipedia.org/wiki/Thymeleaf>`_ or `Facelets
  394. <http://en.wikipedia.org/wiki/Facelets>`_. It has the following
  395. characteristics:
  396. * It's implemented fully in JavaScript and rendered in the browser
  397. * Each template file (XML files) contains multiple templates
  398. * It has special support in Odoo Web's :class:`~openerp.Widget`, though it
  399. can be used outside of Odoo's web client (and it's possible to use
  400. :class:`~openerp.Widget` without relying on QWeb)
  401. .. note::
  402. The rationale behind using QWeb instead of existing javascript template
  403. engines is the extensibility of pre-existing (third-party) templates, much
  404. like Odoo :ref:`views <reference/views>`.
  405. Most javascript template engines are text-based which precludes easy
  406. structural extensibility where an XML-based templating engine can be
  407. generically altered using e.g. XPath or CSS and a tree-alteration DSL (or
  408. even just XSLT). This flexibility and extensibility is a core
  409. characteristic of Odoo, and losing it was considered unacceptable.
  410. Using QWeb
  411. ----------
  412. First let's define a simple QWeb template in the almost-empty
  413. ``oepetstore/static/src/xml/petstore.xml`` file:
  414. .. code-block:: xml
  415. <?xml version="1.0" encoding="UTF-8"?>
  416. <templates xml:space="preserve">
  417. <t t-name="HomePageTemplate">
  418. <div style="background-color: red;">This is some simple HTML</div>
  419. </t>
  420. </templates>
  421. Now we can use this template inside of the ``HomePage`` widget. Using the
  422. ``QWeb`` loader variable defined at the top of the page, we can call to the
  423. template defined in the XML file::
  424. local.HomePage = instance.Widget.extend({
  425. start: function() {
  426. this.$el.append(QWeb.render("HomePageTemplate"));
  427. },
  428. });
  429. :func:`QWeb.render` looks for the specified template, renders it to a string
  430. and returns the result.
  431. However, because :class:`~openerp.Widget` has special integration for QWeb
  432. the template can be set directly on the widget via its
  433. :attr:`~openerp.Widget.template` attribute::
  434. local.HomePage = instance.Widget.extend({
  435. template: "HomePageTemplate",
  436. start: function() {
  437. ...
  438. },
  439. });
  440. Although the result looks similar, there are two differences between these
  441. usages:
  442. * with the second version, the template is rendered right before
  443. :func:`~openerp.Widget.start` is called
  444. * in the first version the template's content is added to the widget's root
  445. element, whereas in the second version the template's root element is
  446. directly *set as* the widget's root element. Which is why the "greetings"
  447. sub-widget also gets a red background
  448. .. warning::
  449. templates should have a single non-``t`` root element, especially if
  450. they're set as a widget's :attr:`~openerp.Widget.template`. If there are
  451. multiple "root elements", results are undefined (usually only the first
  452. root element will be used and the others will be ignored)
  453. QWeb Context
  454. ''''''''''''
  455. QWeb templates can be given data and can contain basic display logic.
  456. For explicit calls to :func:`QWeb.render`, the template data is passed as
  457. second parameter::
  458. QWeb.render("HomePageTemplate", {name: "Klaus"});
  459. with the template modified to:
  460. .. code-block:: xml
  461. <t t-name="HomePageTemplate">
  462. <div>Hello <t t-esc="name"/></div>
  463. </t>
  464. will result in:
  465. .. code-block:: html
  466. <div>Hello Klaus</div>
  467. When using :class:`~openerp.Widget`'s integration it is not possible to
  468. provide additional data to the template. The template will be given a single
  469. ``widget`` context variable, referencing the widget being rendered right
  470. before :func:`~openerp.Widget.start` is called (the widget's state will
  471. essentially be that set up by :func:`~openerp.Widget.init`):
  472. .. code-block:: xml
  473. <t t-name="HomePageTemplate">
  474. <div>Hello <t t-esc="widget.name"/></div>
  475. </t>
  476. ::
  477. local.HomePage = instance.Widget.extend({
  478. template: "HomePageTemplate",
  479. init: function(parent) {
  480. this._super(parent);
  481. this.name = "Mordecai";
  482. },
  483. start: function() {
  484. },
  485. });
  486. Result:
  487. .. code-block:: html
  488. <div>Hello Mordecai</div>
  489. Template Declaration
  490. ''''''''''''''''''''
  491. We've seen how to *render* QWeb templates, let's now see the syntax of
  492. the templates themselves.
  493. A QWeb template is composed of regular XML mixed with QWeb *directives*. A
  494. QWeb directive is declared with XML attributes starting with ``t-``.
  495. The most basic directive is ``t-name``, used to declare new templates in
  496. a template file:
  497. .. code-block:: xml
  498. <templates>
  499. <t t-name="HomePageTemplate">
  500. <div>This is some simple HTML</div>
  501. </t>
  502. </templates>
  503. ``t-name`` takes the name of the template being defined, and declares that
  504. it can be called using :func:`QWeb.render`. It can only be used at the
  505. top-level of a template file.
  506. Escaping
  507. ''''''''
  508. The ``t-esc`` directive can be used to output text:
  509. .. code-block:: xml
  510. <div>Hello <t t-esc="name"/></div>
  511. It takes a Javascript expression which is evaluated, the result of the
  512. expression is then HTML-escaped and inserted in the document. Since it's an
  513. expression it's possible to provide just a variable name as above, or a more
  514. complex expression like a computation:
  515. .. code-block:: xml
  516. <div><t t-esc="3+5"/></div>
  517. or method calls:
  518. .. code-block:: xml
  519. <div><t t-esc="name.toUpperCase()"/></div>
  520. Outputting HTML
  521. '''''''''''''''
  522. To inject HTML in the page being rendered, use ``t-raw``. Like ``t-esc`` it
  523. takes an arbitrary Javascript expression as parameter, but it does not
  524. perform an HTML-escape step.
  525. .. code-block:: xml
  526. <div><t t-raw="name.link(user_account)"/></div>
  527. .. danger::
  528. ``t-raw`` *must not* be used on any data which may contain non-escaped
  529. user-provided content as this leads to `cross-site scripting`_
  530. vulnerabilities
  531. Conditionals
  532. ''''''''''''
  533. QWeb can have conditional blocks using ``t-if``. The directive takes an
  534. arbitrary expression, if the expression is falsy (``false``, ``null``, ``0``
  535. or an empty string) the whole block is suppressed, otherwise it is displayed.
  536. .. code-block:: xml
  537. <div>
  538. <t t-if="true == true">
  539. true is true
  540. </t>
  541. <t t-if="true == false">
  542. true is not true
  543. </t>
  544. </div>
  545. .. note::
  546. QWeb doesn't have an "else" structure, use a second ``t-if`` with the
  547. original condition inverted. You may want to store the condition in a
  548. local variable if it's a complex or expensive expression.
  549. Iteration
  550. '''''''''
  551. To iterate on a list, use ``t-foreach`` and ``t-as``. ``t-foreach`` takes an
  552. expression returning a list to iterate on ``t-as`` takes a variable name to
  553. bind to each item during iteration.
  554. .. code-block:: xml
  555. <div>
  556. <t t-foreach="names" t-as="name">
  557. <div>
  558. Hello <t t-esc="name"/>
  559. </div>
  560. </t>
  561. </div>
  562. .. note:: ``t-foreach`` can also be used with numbers and objects
  563. (dictionaries)
  564. Defining attributes
  565. '''''''''''''''''''
  566. QWeb provides two related directives to define computed attributes:
  567. :samp:`t-att-{name}` and :samp:`t-attf-{name}`. In either case, *name* is the
  568. name of the attribute to create (e.g. ``t-att-id`` defines the attribute
  569. ``id`` after rendering).
  570. ``t-att-`` takes a javascript expression whose result is set as the
  571. attribute's value, it is most useful if all of the attribute's value is
  572. computed:
  573. .. code-block:: xml
  574. <div>
  575. Input your name:
  576. <input type="text" t-att-value="defaultName"/>
  577. </div>
  578. ``t-attf-`` takes a *format string*. A format string is literal text with
  579. interpolation blocks inside, an interpolation block is a javascript
  580. expression between ``{{`` and ``}}``, which will be replaced by the result
  581. of the expression. It is most useful for attributes which are partially
  582. literal and partially computed such as a class:
  583. .. code-block:: xml
  584. <div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}">
  585. insert content here
  586. </div>
  587. Calling other templates
  588. '''''''''''''''''''''''
  589. Templates can be split into sub-templates (for simplicity, maintainability,
  590. reusability or to avoid excessive markup nesting).
  591. This is done using the ``t-call`` directive, which takes the name of the
  592. template to render:
  593. .. code-block:: xml
  594. <t t-name="A">
  595. <div class="i-am-a">
  596. <t t-call="B"/>
  597. </div>
  598. </t>
  599. <t t-name="B">
  600. <div class="i-am-b"/>
  601. </t>
  602. rendering the ``A`` template will result in:
  603. .. code-block:: xml
  604. <div class="i-am-a">
  605. <div class="i-am-b"/>
  606. </div>
  607. Sub-templates inherit the rendering context of their caller.
  608. To Learn More About QWeb
  609. ''''''''''''''''''''''''
  610. For a QWeb reference, see :ref:`reference/qweb`.
  611. Exercise
  612. ''''''''
  613. .. exercise:: Usage of QWeb in Widgets
  614. Create a widget whose constructor takes two parameters aside from
  615. ``parent``: ``product_names`` and ``color``.
  616. * ``product_names`` should an array of strings, each one the name of a
  617. product
  618. * ``color`` is a string containing a color in CSS color format (ie:
  619. ``#000000`` for black).
  620. The widget should display the given product names one under the other,
  621. each one in a separate box with a background color with the value of
  622. ``color`` and a border. You should use QWeb to render the HTML. Any
  623. necessary CSS should be in ``oepetstore/static/src/css/petstore.css``.
  624. Use the widget in ``HomePage`` with half a dozen products.
  625. .. only:: solutions
  626. ::
  627. openerp.oepetstore = function(instance, local) {
  628. var _t = instance.web._t,
  629. _lt = instance.web._lt;
  630. var QWeb = instance.web.qweb;
  631. local.HomePage = instance.Widget.extend({
  632. start: function() {
  633. var products = new local.ProductsWidget(
  634. this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00");
  635. products.appendTo(this.$el);
  636. },
  637. });
  638. local.ProductsWidget = instance.Widget.extend({
  639. template: "ProductsWidget",
  640. init: function(parent, products, color) {
  641. this._super(parent);
  642. this.products = products;
  643. this.color = color;
  644. },
  645. });
  646. instance.web.client_actions.add(
  647. 'petstore.homepage', 'instance.oepetstore.HomePage');
  648. }
  649. .. code-block:: xml
  650. <?xml version="1.0" encoding="UTF-8"?>
  651. <templates xml:space="preserve">
  652. <t t-name="ProductsWidget">
  653. <div>
  654. <t t-foreach="widget.products" t-as="product">
  655. <span class="oe_products_item"
  656. t-attf-style="background-color: {{ widget.color }};">
  657. <t t-esc="product"/>
  658. </span>
  659. <br/>
  660. </t>
  661. </div>
  662. </t>
  663. </templates>
  664. .. code-block:: css
  665. .oe_products_item {
  666. display: inline-block;
  667. padding: 3px;
  668. margin: 5px;
  669. border: 1px solid black;
  670. border-radius: 3px;
  671. }
  672. .. image:: web/qweb.*
  673. :align: center
  674. :width: 70%
  675. Widget Helpers
  676. ==============
  677. ``Widget``'s jQuery Selector
  678. ----------------------------
  679. Selecting DOM elements within a widget can be performed by calling the
  680. ``find()`` method on the widget's DOM root::
  681. this.$el.find("input.my_input")...
  682. But because it's a common operation, :class:`~openerp.Widget` provides an
  683. equivalent shortcut through the :func:`~openerp.Widget.$` method::
  684. local.MyWidget = instance.Widget.extend({
  685. start: function() {
  686. this.$("input.my_input")...
  687. },
  688. });
  689. .. warning::
  690. The global jQuery function ``$()`` should *never* be used unless it is
  691. absolutely necessary: selection on a widget's root are scoped to the
  692. widget and local to it, but selections with ``$()`` are global to the
  693. page/application and may match parts of other widgets and views, leading
  694. to odd or dangerous side-effects. Since a widget should generally act
  695. only on the DOM section it owns, there is no cause for global selection.
  696. Easier DOM Events Binding
  697. -------------------------
  698. We have previously bound DOM events using normal jQuery event handlers (e.g.
  699. ``.click()`` or ``.change()``) on widget elements::
  700. local.MyWidget = instance.Widget.extend({
  701. start: function() {
  702. var self = this;
  703. this.$(".my_button").click(function() {
  704. self.button_clicked();
  705. });
  706. },
  707. button_clicked: function() {
  708. ..
  709. },
  710. });
  711. While this works it has a few issues:
  712. 1. it is rather verbose
  713. 2. it does not support replacing the widget's root element at runtime as
  714. the binding is only performed when ``start()`` is run (during widget
  715. initialization)
  716. 3. it requires dealing with ``this``-binding issues
  717. Widgets thus provide a shortcut to DOM event binding via
  718. :attr:`~openerp.Widget.events`::
  719. local.MyWidget = instance.Widget.extend({
  720. events: {
  721. "click .my_button": "button_clicked",
  722. },
  723. button_clicked: function() {
  724. ..
  725. }
  726. });
  727. :attr:`~openerp.Widget.events` is an object (mapping) of an event to the
  728. function or method to call when the event is triggered:
  729. * the key is an event name, possibly refined with a CSS selector in which
  730. case only if the event happens on a selected sub-element will the function
  731. or method run: ``click`` will handle all clicks within the widget, but
  732. ``click .my_button`` will only handle clicks in elements bearing the
  733. ``my_button`` class
  734. * the value is the action to perform when the event is triggered
  735. It can be either a function::
  736. events: {
  737. 'click': function (e) { /* code here */ }
  738. }
  739. or the name of a method on the object (see example above).
  740. In either case, the ``this`` is the widget instance and the handler is given
  741. a single parameter, the `jQuery event object`_ for the event.
  742. Widget Events and Properties
  743. ============================
  744. Events
  745. ------
  746. Widgets provide an event system (separate from the DOM/jQuery event system
  747. described above): a widget can fire events on itself, and other widgets (or
  748. itself) can bind themselves and listen for these events::
  749. local.ConfirmWidget = instance.Widget.extend({
  750. events: {
  751. 'click button.ok_button': function () {
  752. this.trigger('user_chose', true);
  753. },
  754. 'click button.cancel_button': function () {
  755. this.trigger('user_chose', false);
  756. }
  757. },
  758. start: function() {
  759. this.$el.append("<div>Are you sure you want to perform this action?</div>" +
  760. "<button class='ok_button'>Ok</button>" +
  761. "<button class='cancel_button'>Cancel</button>");
  762. },
  763. });
  764. This widget acts as a facade, transforming user input (through DOM events)
  765. into a documentable internal event to which parent widgets can bind
  766. themselves.
  767. :func:`~openerp.Widget.trigger` takes the name of the event to trigger as
  768. its first (mandatory) argument, any further arguments are treated as event
  769. data and passed directly to listeners.
  770. We can then set up a parent event instantiating our generic widget and
  771. listening to the ``user_chose`` event using :func:`~openerp.Widget.on`::
  772. local.HomePage = instance.Widget.extend({
  773. start: function() {
  774. var widget = new local.ConfirmWidget(this);
  775. widget.on("user_chose", this, this.user_chose);
  776. widget.appendTo(this.$el);
  777. },
  778. user_chose: function(confirm) {
  779. if (confirm) {
  780. console.log("The user agreed to continue");
  781. } else {
  782. console.log("The user refused to continue");
  783. }
  784. },
  785. });
  786. :func:`~openerp.Widget.on` binds a function to be called when the
  787. event identified by ``event_name`` is. The ``func`` argument is the
  788. function to call and ``object`` is the object to which that function is
  789. related if it is a method. The bound function will be called with the
  790. additional arguments of :func:`~openerp.Widget.trigger` if it has
  791. any. Example::
  792. start: function() {
  793. var widget = ...
  794. widget.on("my_event", this, this.my_event_triggered);
  795. widget.trigger("my_event", 1, 2, 3);
  796. },
  797. my_event_triggered: function(a, b, c) {
  798. console.log(a, b, c);
  799. // will print "1 2 3"
  800. }
  801. .. note::
  802. Triggering events on an other widget is generally a bad idea. The main
  803. exception to that rule is ``openerp.web.bus`` which exists specifically
  804. to broadcasts evens in which any widget could be interested throughout
  805. the Odoo web application.
  806. Properties
  807. ----------
  808. Properties are very similar to normal object attributes in that they allow
  809. storing data on a widget instance, however they have the additional feature
  810. that they trigger events when set::
  811. start: function() {
  812. this.widget = ...
  813. this.widget.on("change:name", this, this.name_changed);
  814. this.widget.set("name", "Nicolas");
  815. },
  816. name_changed: function() {
  817. console.log("The new value of the property 'name' is", this.widget.get("name"));
  818. }
  819. * :func:`~openerp.Widget.set` sets the value of a property and triggers
  820. :samp:`change:{propname}` (where *propname* is the property name passed as
  821. first parameter to :func:`~openerp.Widget.set`) and ``change``
  822. * :func:`~openerp.Widget.get` retrieves the value of a property.
  823. Exercise
  824. --------
  825. .. exercise:: Widget Properties and Events
  826. Create a widget ``ColorInputWidget`` that will display 3 ``<input
  827. type="text">``. Each of these ``<input>`` is dedicated to type a
  828. hexadecimal number from 00 to FF. When any of these ``<input>`` is
  829. modified by the user the widget must query the content of the three
  830. ``<input>``, concatenate their values to have a complete CSS color code
  831. (ie: ``#00FF00``) and put the result in a property named ``color``. Please
  832. note the jQuery ``change()`` event that you can bind on any HTML
  833. ``<input>`` element and the ``val()`` method that can query the current
  834. value of that ``<input>`` could be useful to you for this exercise.
  835. Then, modify the ``HomePage`` widget to instantiate ``ColorInputWidget``
  836. and display it. The ``HomePage`` widget should also display an empty
  837. rectangle. That rectangle must always, at any moment, have the same
  838. background color as the color in the ``color`` property of the
  839. ``ColorInputWidget`` instance.
  840. Use QWeb to generate all HTML.
  841. .. only:: solutions
  842. ::
  843. openerp.oepetstore = function(instance, local) {
  844. var _t = instance.web._t,
  845. _lt = instance.web._lt;
  846. var QWeb = instance.web.qweb;
  847. local.ColorInputWidget = instance.Widget.extend({
  848. template: "ColorInputWidget",
  849. events: {
  850. 'change input': 'input_changed'
  851. },
  852. start: function() {
  853. this.input_changed();
  854. return this._super();
  855. },
  856. input_changed: function() {
  857. var color = [
  858. "#",
  859. this.$(".oe_color_red").val(),
  860. this.$(".oe_color_green").val(),
  861. this.$(".oe_color_blue").val()
  862. ].join('');
  863. this.set("color", color);
  864. },
  865. });
  866. local.HomePage = instance.Widget.extend({
  867. template: "HomePage",
  868. start: function() {
  869. this.colorInput = new local.ColorInputWidget(this);
  870. this.colorInput.on("change:color", this, this.color_changed);
  871. return this.colorInput.appendTo(this.$el);
  872. },
  873. color_changed: function() {
  874. this.$(".oe_color_div").css("background-color", this.colorInput.get("color"));
  875. },
  876. });
  877. instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
  878. }
  879. .. code-block:: xml
  880. <?xml version="1.0" encoding="UTF-8"?>
  881. <templates xml:space="preserve">
  882. <t t-name="ColorInputWidget">
  883. <div>
  884. Red: <input type="text" class="oe_color_red" value="00"></input><br />
  885. Green: <input type="text" class="oe_color_green" value="00"></input><br />
  886. Blue: <input type="text" class="oe_color_blue" value="00"></input><br />
  887. </div>
  888. </t>
  889. <t t-name="HomePage">
  890. <div>
  891. <div class="oe_color_div"></div>
  892. </div>
  893. </t>
  894. </templates>
  895. .. code-block:: css
  896. .oe_color_div {
  897. width: 100px;
  898. height: 100px;
  899. margin: 10px;
  900. }
  901. Modify existing widgets and classes
  902. ===================================
  903. The class system of the Odoo web framework allows direct modification of
  904. existing classes using the :func:`~openerp.web.Class.include` method::
  905. var TestClass = instance.web.Class.extend({
  906. testMethod: function() {
  907. return "hello";
  908. },
  909. });
  910. TestClass.include({
  911. testMethod: function() {
  912. return this._super() + " world";
  913. },
  914. });
  915. console.log(new TestClass().testMethod());
  916. // will print "hello world"
  917. This system is similar to the inheritance mechanism, except it will alter the
  918. target class in-place instead of creating a new class.
  919. In that case, ``this._super()`` will call the original implementation of a
  920. method being replaced/redefined. If the class already had sub-classes, all
  921. calls to ``this._super()`` in sub-classes will call the new implementations
  922. defined in the call to :func:`~openerp.web.Class.include`. This will also work
  923. if some instances of the class (or of any of its sub-classes) were created
  924. prior to the call to :func:`~openerp.Widget.include`.
  925. Translations
  926. ============
  927. The process to translate text in Python and JavaScript code is very
  928. similar. You could have noticed these lines at the beginning of the
  929. ``petstore.js`` file::
  930. var _t = instance.web._t,
  931. _lt = instance.web._lt;
  932. These lines are simply used to import the translation functions in the current
  933. JavaScript module. They are used thus::
  934. this.$el.text(_t("Hello user!"));
  935. In Odoo, translations files are automatically generated by scanning the source
  936. code. All piece of code that calls a certain function are detected and their
  937. content is added to a translation file that will then be sent to the
  938. translators. In Python, the function is ``_()``. In JavaScript the function is
  939. :func:`~openerp.web._t` (and also :func:`~openerp.web._lt`).
  940. ``_t()`` will return the translation defined for the text it is given. If no
  941. translation is defined for that text, it will return the original text as-is.
  942. .. note::
  943. To inject user-provided values in translatable strings, it is recommended
  944. to use `_.str.sprintf
  945. <http://gabceb.github.io/underscore.string.site/#sprintf>`_ with named
  946. arguments *after* the translation::
  947. this.$el.text(_.str.sprintf(
  948. _t("Hello, %(user)s!"), {
  949. user: "Ed"
  950. }));
  951. This makes translatable strings more readable to translators, and gives
  952. them more flexibility to reorder or ignore parameters.
  953. :func:`~openerp.web._lt` ("lazy translate") is similar but somewhat more
  954. complex: instead of translating its parameter immediately, it returns
  955. an object which, when converted to a string, will perform the translation.
  956. It is used to define translatable terms before the translations system is
  957. initialized, for class attributes for instance (as modules are loaded before
  958. the user's language is configured and translations are downloaded).
  959. Communication with the Odoo Server
  960. ==================================
  961. Contacting Models
  962. -----------------
  963. Most operations with Odoo involve communicating with *models* implementing
  964. business concern, these models will then (potentially) interact with some
  965. storage engine (usually PostgreSQL_).
  966. Although jQuery_ provides a `$.ajax`_ function for network interactions,
  967. communicating with Odoo requires additional metadata whose setup before every
  968. call would be verbose and error-prone. As a result, Odoo web provides
  969. higher-level communication primitives.
  970. To demonstrate this, the file ``petstore.py`` already contains a small model
  971. with a sample method:
  972. .. code-block:: python
  973. class message_of_the_day(models.Model):
  974. _name = "oepetstore.message_of_the_day"
  975. @api.model
  976. def my_method(self):
  977. return {"hello": "world"}
  978. message = fields.Text(),
  979. color = fields.Char(size=20),
  980. This declares a model with two fields, and a method ``my_method()`` which
  981. returns a literal dictionary.
  982. Here is a sample widget that calls ``my_method()`` and displays the result::
  983. local.HomePage = instance.Widget.extend({
  984. start: function() {
  985. var self = this;
  986. var model = new instance.web.Model("oepetstore.message_of_the_day");
  987. model.call("my_method", {context: new instance.web.CompoundContext()}).then(function(result) {
  988. self.$el.append("<div>Hello " + result["hello"] + "</div>");
  989. // will show "Hello world" to the user
  990. });
  991. },
  992. });
  993. The class used to call Odoo models is :class:`openerp.Model`. It is
  994. instantiated with the Odoo model's name as first parameter
  995. (``oepetstore.message_of_the_day`` here).
  996. :func:`~openerp.web.Model.call` can be used to call any (public) method of an
  997. Odoo model. It takes the following positional arguments:
  998. ``name``
  999. The name of the method to call, ``my_method`` here
  1000. ``args``
  1001. an array of `positional arguments`_ to provide to the method. Because the
  1002. example has no positional argument to provide, the ``args`` parameter is not
  1003. provided.
  1004. Here is an other example with positional arguments:
  1005. .. code-block:: python
  1006. @api.model
  1007. def my_method2(self, a, b, c): ...
  1008. .. code-block:: javascript
  1009. model.call("my_method", [1, 2, 3], ...
  1010. // with this a=1, b=2 and c=3
  1011. ``kwargs``
  1012. a mapping of `keyword arguments`_ to pass. The example provides a single
  1013. named argument ``context``.
  1014. .. code-block:: python
  1015. @api.model
  1016. def my_method2(self, a, b, c): ...
  1017. .. code-block:: javascript
  1018. model.call("my_method", [], {a: 1, b: 2, c: 3, ...
  1019. // with this a=1, b=2 and c=3
  1020. :func:`~openerp.Widget.call` returns a deferred resolved with the value
  1021. returned by the model's method as first argument.
  1022. CompoundContext
  1023. ---------------
  1024. The previous section used a ``context`` argument which was not explained in
  1025. the method call::
  1026. model.call("my_method", {context: new instance.web.CompoundContext()})
  1027. The context is like a "magic" argument that the web client will always give to
  1028. the server when calling a method. The context is a dictionary containing
  1029. multiple keys. One of the most important key is the language of the user, used
  1030. by the server to translate all the messages of the application. Another one is
  1031. the time zone of the user, used to compute correctly dates and times if Odoo
  1032. is used by people in different countries.
  1033. The ``argument`` is necessary in all methods, otherwise bad things could
  1034. happen (such as the application not being translated correctly). That's why,
  1035. when you call a model's method, you should always provide that argument. The
  1036. solution to achieve that is to use :class:`openerp.web.CompoundContext`.
  1037. :class:`~openerp.web.CompoundContext` is a class used to pass the user's
  1038. context (with language, time zone, etc...) to the server as well as adding new
  1039. keys to the context (some models' methods use arbitrary keys added to the
  1040. context). It is created by giving to its constructor any number of
  1041. dictionaries or other :class:`~openerp.web.CompoundContext` instances. It will
  1042. merge all those contexts before sending them to the server.
  1043. .. code-block:: javascript
  1044. model.call("my_method", {context: new instance.web.CompoundContext({'new_key': 'key_value'})})
  1045. .. code-block:: python
  1046. @api.model
  1047. def my_method(self):
  1048. print self.env.context
  1049. // will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1}
  1050. You can see the dictionary in the argument ``context`` contains some keys that
  1051. are related to the configuration of the current user in Odoo plus the
  1052. ``new_key`` key that was added when instantiating
  1053. :class:`~openerp.web.CompoundContext`.
  1054. Queries
  1055. -------
  1056. While :func:`~openerp.Model.call` is sufficient for any interaction with Odoo
  1057. models, Odoo Web provides a helper for simpler and clearer querying of models
  1058. (fetching of records based on various conditions):
  1059. :func:`~openerp.Model.query` which acts as a shortcut for the common
  1060. combination of :py:meth:`~openerp.models.Model.search` and
  1061. ::py:meth:`~openerp.models.Model.read`. It provides a clearer syntax to search
  1062. and read models::
  1063. model.query(['name', 'login', 'user_email', 'signature'])
  1064. .filter([['active', '=', true], ['company_id', '=', main_company]])
  1065. .limit(15)
  1066. .all().then(function (users) {
  1067. // do work with users records
  1068. });
  1069. versus::
  1070. model.call('search', [['active', '=', true], ['company_id', '=', main_company]], {limit: 15})
  1071. .then(function (ids) {
  1072. return model.call('read', [ids, ['name', 'login', 'user_email', 'signature']]);
  1073. })
  1074. .then(function (users) {
  1075. // do work with users records
  1076. });
  1077. * :func:`~openerp.web.Model.query` takes an optional list of fields as
  1078. parameter (if no field is provided, all fields of the model are fetched). It
  1079. returns a :class:`openerp.web.Query` which can be further customized before
  1080. being executed
  1081. * :class:`~openerp.web.Query` represents the query being built. It is
  1082. immutable, methods to customize the query actually return a modified copy,
  1083. so it's possible to use the original and the new version side-by-side. See
  1084. :class:`~openerp.web.Query` for its customization options.
  1085. When the query is set up as desired, simply call
  1086. :func:`~openerp.web.Query.all` to execute it and return a
  1087. deferred to its result. The result is the same as
  1088. :py:meth:`~openerp.models.Model.read`'s, an array of dictionaries where each
  1089. dictionary is a requested record, with each requested field a dictionary key.
  1090. Exercises
  1091. =========
  1092. .. exercise:: Message of the Day
  1093. Create a ``MessageOfTheDay`` widget displaying the last record of the
  1094. ``oepetstore.message_of_the_day`` model. The widget should fetch its
  1095. record as soon as it is displayed.
  1096. Display the widget in the Pet Store home page.
  1097. .. only:: solutions
  1098. .. code-block:: javascript
  1099. openerp.oepetstore = function(instance, local) {
  1100. var _