diff options
Diffstat (limited to 'doc/reference')
| -rw-r--r-- | doc/reference/actions.rst | 401 | ||||
| -rw-r--r-- | doc/reference/async.rst | 341 | ||||
| -rw-r--r-- | doc/reference/cmdline.rst | 4 | ||||
| -rw-r--r-- | doc/reference/http.rst | 4 | ||||
| -rw-r--r-- | doc/reference/images/runner.png | bin | 0 -> 3963 bytes | |||
| -rw-r--r-- | doc/reference/images/runner2.png | bin | 0 -> 6807 bytes | |||
| -rw-r--r-- | doc/reference/images/tests.png | bin | 0 -> 65388 bytes | |||
| -rw-r--r-- | doc/reference/images/tests2.png | bin | 0 -> 20114 bytes | |||
| -rw-r--r-- | doc/reference/images/tests3.png | bin | 0 -> 20382 bytes | |||
| -rw-r--r-- | doc/reference/javascript.rst | 1267 | ||||
| -rw-r--r-- | doc/reference/orm.rst | 49 | ||||
| -rw-r--r-- | doc/reference/qweb.rst | 550 | ||||
| -rw-r--r-- | doc/reference/test-report.txt | 25 | ||||
| -rw-r--r-- | doc/reference/testing.rst | 73 |
14 files changed, 2702 insertions, 12 deletions
diff --git a/doc/reference/actions.rst b/doc/reference/actions.rst new file mode 100644 index 00000000000..8e31d58b36c --- /dev/null +++ b/doc/reference/actions.rst @@ -0,0 +1,401 @@ +.. _reference/actions: + +======= +Actions +======= + +Actions define the behavior of the system in response to user actions: login, +action button, selection of an invoice, ... + +Actions can be stored in the database or returned directly as dictionaries in +e.g. button methods. All actions share two mandatory attributes: + +``type`` + the category of the current action, determines which fields may be + used and how the action is interpreted +``name`` + short user-readable description of the action, may be displayed in the + client's interface + +A client can get actions in 4 forms: + +``False`` + if any action dialog is currently open, close it +A string + if a :ref:`client action <reference/actions/client>` matches, interpret as + a client action's tag, otherwise treat as a number +A number + read the corresponding action record from the database, may be a database + identifier or an :term:`external id` +A dictionary + treat as a client action descriptor and execute + +.. _reference/actions/window: + +Window Actions (``ir.actions.act_window``) +========================================== + +The most common action type, used to present visualisations of a model through +:ref:`views <reference/views>`: a window action defines a set of view types +(and possibly specific views) for a model (and possibly specific record of the +model). + +Its fields are: + +``res_model`` + model to present views for +``views`` + a list of ``(view_id, view_type)`` pairs. The second element of each pair + is the category of the view (tree, form, graph, ...) and the first is + an optional database id (or ``False``). If no id is provided, the client + should fetch the default view of the specified type for the requested + model (this is automatically done by + :meth:`~openerp.models.Model.fields_view_get`). The first type of the + list is the default view type and will be open by default when the action + is executed. Each view type should be present at most once in the list +``res_id`` (optional) + if the default view is ``form``, specifies the record to load (otherwise + a new record should be created) +``search_view_id`` (optional) + ``(id, name)`` pair, ``id`` is the database identifier of a specific + search view to load for the action. Defaults to fetching the default + search view for the model +``target`` (optional) + whether the views should be open in the main content area (``current``) + or in a dialog/popup (``new``). Defaults to ``current``. +``context`` (optional) + additional context data to pass to the views +``domain`` (optional) + filtering domain to implicitly add to all view search queries +``limit`` (optional) + number of records to display in lists by default. Defaults to 80 in the + web client +``auto_search`` (optional) + whether a search should be performed immediately after loading the default + view. Defaults to ``True`` + +For instance, to open customers (partner with the ``customer`` flag set) with +list and form views:: + + { + "type": "ir.actions.act_window", + "res_model": "res.partner", + "views": [[False, "tree"], [False, "form"]], + "domain": [["customer", "=", true]], + } + +Or to open the form view of a specific product (obtained separately) in a new +dialog:: + + { + "type": "ir.actions.act_window", + "res_model": "product.product", + "views": [[False, "form"]], + "res_id": a_product_id, + "target": "new", + } + +In-database window actions have a few different fields which should be ignored +by clients, mostly to use in composing the ``views`` list: + +``view_mode`` + comma-separated list of view types (tree, form, ...) as a string. All of + these types will be present in the generated ``views`` list (with at least + a ``False`` view_id) +``view_ids`` + M2M\ [#notquitem2m]_ to view objects, defines the initial content of + ``views`` +``view_id`` + specific view added to the ``views`` list in case its type is part of the + ``view_mode`` list and not already filled by one of the views in + ``view_ids`` + +These are mostly used when defining actions from :ref:`reference/data`: + +.. code-block:: xml + + <record model="ir.actions.act_window" id="test_action"> + <field name="name">A Test Action</field> + <field name="res_model">some.model</field> + <field name="view_mode">graph</field> + <field name="view_id" ref="my_specific_view"/> + </record> + +will use the "my_specific_view" view even if that's not the default view for +the model. + +The server-side composition of the ``views`` sequence is the following: + +* get each ``(id, type)`` from ``view_ids`` (ordered by ``sequence``) +* if ``view_id`` is defined and its type isn't already filled, append its + ``(id, type)`` +* for each unfilled type in ``view_mode``, append ``(False, type)`` + +.. todo:: + + * ``src_model``, ``multi`` seem linked to "sidebar" actions? + * ``auto_refresh`` looks ignored/deprecated + * ``usage``? + * ``groups_id``? + * ``filter``? + +.. _reference/actions/url: + +URL Actions (``ir.actions.act_url``) +==================================== + +Allow opening a URL (website/web page) via an Odoo action. Can be customized +via two fields: + +``url`` + the address to open when activating the action +``target`` + opens the address in a new window/page if ``new``, replaces + the current content with the page if ``self``. Defaults to ``new`` + +:: + + { + "type": "ir.actions.act_url", + "url": "http://odoo.com", + "target": "self", + } + +will replace the current content section by the Odoo home page. + +.. _reference/actions/server: + +Server Actions (``ir.actions.server``) +====================================== + +Allow triggering complex server code from any valid action location. Only +two fields are relevant to clients: + +``id`` + the in-database identifier of the server action to run +``context`` (optional) + context data to use when running the server action + +In-database records are significantly richer and can perform a number of +specific or generic actions based on their ``state``. Some fields (and +corresponding behaviors) are shared between states: + +``model_id`` + Odoo model linked to the action, made available in + :ref:`evaluation contexts <reference/actions/server/context>` +``condition`` (optional) + evaluated as Python code using the server action's + :ref:`evaluation context <reference/actions/server/context>`. If + ``False``, prevents the action from running. Default: ``True`` + +Valid action types (``state`` field) are extensible, the default types are: + +``code`` +-------- + +The default and most flexible server action type, executes arbitrary Python +code with the action's :ref:`evaluation context +<reference/actions/server/context>`. Only uses one specific type-specific +field: + +``code`` + a piece of Python code to execute when the action is called + +.. code-block:: xml + + <record model="ir.actions.server" id="print_instance"> + <field name="model_id" ref="model_res_partner"/> + <field name="code"> + print self, object + </field> + </record> + +.. note:: + + The code segment can define a variable called ``action``, which will be + returned to the client as the next action to execute: + + .. code-block:: xml + + <record model="ir.actions.server" id="print_instance"> + <field name="model_id" ref="model_res_partner"/> + <field name="code"> + if object.some_condition(): + action = { + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": object._name, + "res_id": object.id, + } + </field> + </record> + + will ask the client to open a form for the record if it fullfils some + condition + +This tends to be the only action type created from :ref:`data files +<reference/data>`, other types aside from +:ref:`reference/actions/server/multi` are simpler than Python code to define +from the UI, but not from :ref:`data files <reference/data>`. + +.. _reference/actions/server/object_create: + +``object_create`` +----------------- + +Creates a new record, from scratch (via :meth:`~openerp.models.Model.create`) +or by copying an existing record (via :meth:`~openerp.models.Model.copy`) + +``use_create`` + the creation policy, one of: + + ``new`` + creates a record in the model specified by ``model_id`` + ``new_other`` + creates a record in the model specified by ``crud_model_id`` + ``copy_current`` + copies the record on which the action was invoked + ``copy_other`` + copies an other record, obtained via ``ref_object`` +``fields_lines`` + fields to override when creating or copying the record. + :class:`~openerp.fields.One2many` with the fields: + + ``col1`` + ``ir.model.fields`` to set in the model implied by ``use_create`` + ``value`` + value for the field, interpreted via ``type`` + ``type`` + If ``value``, the ``value`` field is interpreted as a literal value + (possibly converted), if ``equation`` the ``value`` field is + interpreted as a Python expression and evaluated +``crud_model_id`` + model in which to create a new record, if ``use_create`` is set to + ``new_other`` +``ref_object`` + :class:`~openerp.fields.Reference` to an arbitrary record to copy, used if + ``use_create`` is set to ``copy_other`` +``link_new_record`` + boolean flag linking the newly created record to the current one via a + many2one field specified through ``link_field_id``, defaults to ``False`` +``link_field_id`` + many2one to ``ir.model.fields``, specifies the current record's m2o field + on which the newly created record should be set (models should match) + +``object_write`` +---------------- + +Similar to :ref:`reference/actions/server/object_create` but alters an +existing records instead of creating one + +``use_write`` + write policy, one of: + + ``current`` + write to the current record + ``other`` + write to an other record selected via ``crud_model_id`` and + ``ref_object`` + ``expression`` + write to an other record whose model is selected via ``crud_model_id`` + and whose id is selected by evaluating ``write_expression`` +``write_expression`` + Python expression returning a record or an object id, used when + ``use_write`` is set to ``expression`` in order to decide which record + should be modified +``fields_lines`` + see :ref:`reference/actions/server/object_create` +``crud_model_id`` + see :ref:`reference/actions/server/object_create` +``ref_object`` + see :ref:`reference/actions/server/object_create` + +.. _reference/actions/server/multi: + +``multi`` +--------- + +Executes multiple actions one after the other. Actions to execute are defined +via the ``child_ids`` m2m. If sub-actions themselves return actions, the last +one will be returned to the client as the multi's own next action + +``trigger`` +----------- + +Sends a signal to a workflow. + +``wkf_transition_id`` + :class:`~openerp.fields.Many2one` to a ``workflow.transition`` to trigger +``use_relational_model`` + if ``base`` (the default), trigger the signal on behalf of the current + record. If ``relational``, trigger the signal on behalf of a field of the + current record selected through ``wkf_model_id`` and ``wkf_field_id`` + +``client_action`` +----------------- + +Indirection for directly returning an other action defined using +``action_id``. Simply returns that action to the client for execution. + +.. _reference/actions/server/context: + +Evaluation context +------------------ + +A number of keys are available in the evaluation context of or surrounding +server actions: + +``self`` + the model object linked to the action via ``model_id`` +``object``, ``obj`` + only available if ``active_model`` and ``active_id`` are provided (via + context) otherwise ``None``. The actual record selected by ``active_id`` +``pool`` + the current database registry +``datetime``, ``dateutil``, ``time`` + corresponding Python modules +``cr`` + the current cursor +``user`` + the current user record +``context`` + execution context +``Warning`` + constructor for the ``Warning`` exception + +.. ignored uid (available through ``user``), workflow (available through + workflow methods on models) + +.. _reference/actions/report: + +Report Actions +============== + +.. todo:: sle-odoo + +.. _reference/actions/client: + +Client Actions (``ir.actions.client``) +====================================== + +Triggers an action implemented entirely in the client. + +``tag`` + the client-side identifier of the action, an arbitrary string which + the client should know how to react to +``params`` (optional) + a Python dictionary of additional data to send to the client, alongside + the client action tag + +:: + + { + "type": "ir.actions.client", + "tag": "pos.ui" + } + +tells the client to start the Point of Sale interface, the server has no idea +how the POS interface works. + +.. [#notquitem2m] technically not an M2M: adds a sequence field and may be + composed of just a view type, without a view id. diff --git a/doc/reference/async.rst b/doc/reference/async.rst new file mode 100644 index 00000000000..c2aded839aa --- /dev/null +++ b/doc/reference/async.rst @@ -0,0 +1,341 @@ +:orphan: + +.. _reference/async: + +Asynchronous Operations +======================= + +As a language (and runtime), javascript is fundamentally +single-threaded. This means any blocking request or computation will +blocks the whole page (and, in older browsers, the software itself +even preventing users from switching to an other tab): a javascript +environment can be seen as an event-based runloop where application +developers have no control over the runloop itself. + +As a result, performing long-running synchronous network requests or +other types of complex and expensive accesses is frowned upon and +asynchronous APIs are used instead. + +The goal of this guide is to provide some tools to deal with +asynchronous systems, and warn against systemic issues or dangers. + +Deferreds +--------- + +Deferreds are a form of `promises`_. OpenERP Web currently uses +`jQuery's deferred`_. + +The core idea of deferreds is that potentially asynchronous methods +will return a :js:class:`Deferred` object instead of an arbitrary +value or (most commonly) nothing. + +This object can then be used to track the end of the asynchronous +operation by adding callbacks onto it, either success callbacks or +error callbacks. + +A great advantage of deferreds over simply passing callback functions +directly to asynchronous methods is the ability to :ref:`compose them +<reference/async/composition>`. + +Using deferreds +~~~~~~~~~~~~~~~ + +Deferreds's most important method is :js:func:`Deferred.then`. It is +used to attach new callbacks to the deferred object. + +* the first parameter attaches a success callback, called when the + deferred object is successfully resolved and provided with the + resolved value(s) for the asynchronous operation. + +* the second parameter attaches a failure callback, called when the + deferred object is rejected and provided with rejection values + (often some sort of error message). + +Callbacks attached to deferreds are never "lost": if a callback is +attached to an already resolved or rejected deferred, the callback +will be called (or ignored) immediately. A deferred is also only ever +resolved or rejected once, and is either resolved or rejected: a given +deferred can not call a single success callback twice, or call both a +success and a failure callbacks. + +:js:func:`~Deferred.then` should be the method you'll use most often +when interacting with deferred objects (and thus asynchronous APIs). + +Building deferreds +~~~~~~~~~~~~~~~~~~ + +After using asynchronous APIs may come the time to build them: for +mocks_, to compose deferreds from multiple source in a complex +manner, in order to let the current operations repaint the screen or +give other events the time to unfold, ... + +This is easy using jQuery's deferred objects. + +.. note:: this section is an implementation detail of jQuery Deferred + objects, the creation of promises is not part of any + standard (even tentative) that I know of. If you are using + deferred objects which are not jQuery's, their API may (and + often will) be completely different. + +Deferreds are created by invoking their constructor [#]_ without any +argument. This creates a :js:class:`Deferred` instance object with the +following methods: + +:js:func:`Deferred.resolve` + + As its name indicates, this method moves the deferred to the + "Resolved" state. It can be provided as many arguments as + necessary, these arguments will be provided to any pending success + callback. + +:js:func:`Deferred.reject` + + Similar to :js:func:`~Deferred.resolve`, but moves the deferred to + the "Rejected" state and calls pending failure handlers. + +:js:func:`Deferred.promise` + + Creates a readonly view of the deferred object. It is generally a + good idea to return a promise view of the deferred to prevent + callers from resolving or rejecting the deferred in your stead. + +:js:func:`~Deferred.reject` and :js:func:`~Deferred.resolve` are used +to inform callers that the asynchronous operation has failed (or +succeeded). These methods should simply be called when the +asynchronous operation has ended, to notify anybody interested in its +result(s). + +.. _reference/async/composition: + +Composing deferreds +~~~~~~~~~~~~~~~~~~~ + +What we've seen so far is pretty nice, but mostly doable by passing +functions to other functions (well adding functions post-facto would +probably be a chore... still, doable). + +Deferreds truly shine when code needs to compose asynchronous +operations in some way or other, as they can be used as a basis for +such composition. + +There are two main forms of compositions over deferred: multiplexing +and piping/cascading. + +Deferred multiplexing +````````````````````` + +The most common reason for multiplexing deferred is simply performing +multiple asynchronous operations and wanting to wait until all of them are +done before moving on (and executing more stuff). + +The jQuery multiplexing function for promises is :js:func:`when`. + +.. note:: the multiplexing behavior of jQuery's :js:func:`when` is an + (incompatible, mostly) extension of the behavior defined in + `CommonJS Promises/B`_. + +This function can take any number of promises [#]_ and will return a +promise. + +The returned promise will be resolved when *all* multiplexed promises +are resolved, and will be rejected as soon as one of the multiplexed +promises is rejected (it behaves like Python's ``all()``, but with +promise objects instead of boolean-ish). + +The resolved values of the various promises multiplexed via +:js:func:`when` are mapped to the arguments of :js:func:`when`'s +success callback, if they are needed. The resolved values of a promise +are at the same index in the callback's arguments as the promise in +the :js:func:`when` call so you will have: + +.. code-block:: javascript + + $.when(p0, p1, p2, p3).then( + function (results0, results1, results2, results3) { + // code + }); + +.. warning:: + + in a normal mapping, each parameter to the callback would be an + array: each promise is conceptually resolved with an array of 0..n + values and these values are passed to :js:func:`when`'s + callback. But jQuery treats deferreds resolving a single value + specially, and "unwraps" that value. + + For instance, in the code block above if the index of each promise + is the number of values it resolves (0 to 3), ``results0`` is an + empty array, ``results2`` is an array of 2 elements (a pair) but + ``results1`` is the actual value resolved by ``p1``, not an array. + +Deferred chaining +````````````````` + +A second useful composition is starting an asynchronous operation as +the result of an other asynchronous operation, and wanting the result +of both: with the tools described so far, handling e.g. OpenERP's +search/read sequence with this would require something along the lines +of: + +.. code-block:: javascript + + var result = $.Deferred(); + Model.search(condition).then(function (ids) { + Model.read(ids, fields).then(function (records) { + result.resolve(records); + }); + }); + return result.promise(); + +While it doesn't look too bad for trivial code, this quickly gets +unwieldy. + +But :js:func:`~Deferred.then` also allows handling this kind of +chains: it returns a new promise object, not the one it was called +with, and the return values of the callbacks is important to this behavior: +whichever callback is called, + +* If the callback is not set (not provided or left to null), the + resolution or rejection value(s) is simply forwarded to + :js:func:`~Deferred.then`'s promise (it's essentially a noop) + +* If the callback is set and does not return an observable object (a + deferred or a promise), the value it returns (``undefined`` if it + does not return anything) will replace the value it was given, e.g. + + .. code-block:: javascript + + promise.then(function () { + console.log('called'); + }); + + will resolve with the sole value ``undefined``. + +* If the callback is set and returns an observable object, that object + will be the actual resolution (and result) of the pipe. This means a + resolved promise from the failure callback will resolve the pipe, + and a failure promise from the success callback will reject the + pipe. + + This provides an easy way to chain operation successes, and the + previous piece of code can now be rewritten: + + .. code-block:: javascript + + return Model.search(condition).then(function (ids) { + return Model.read(ids, fields); + }); + + the result of the whole expression will encode failure if either + ``search`` or ``read`` fails (with the right rejection values), and + will be resolved with ``read``'s resolution values if the chain + executes correctly. + +:js:func:`~Deferred.then` is also useful to adapt third-party +promise-based APIs, in order to filter their resolution value counts +for instance (to take advantage of :js:func:`when` 's special +treatment of single-value promises). + +jQuery.Deferred API +~~~~~~~~~~~~~~~~~~~ + +.. js:function:: when(deferreds…) + + :param deferreds: deferred objects to multiplex + :returns: a multiplexed deferred + :rtype: :js:class:`Deferred` + +.. js:class:: Deferred + + .. js:function:: Deferred.then(doneCallback[, failCallback]) + + Attaches new callbacks to the resolution or rejection of the + deferred object. Callbacks are executed in the order they are + attached to the deferred. + + To provide only a failure callback, pass ``null`` as the + ``doneCallback``, to provide only a success callback the + second argument can just be ignored (and not passed at all). + + Returns a new deferred which resolves to the result of the + corresponding callback, if a callback returns a deferred + itself that new deferred will be used as the resolution of the + chain. + + :param doneCallback: function called when the deferred is resolved + :param failCallback: function called when the deferred is rejected + :returns: the deferred object on which it was called + :rtype: :js:class:`Deferred` + + .. js:function:: Deferred.done(doneCallback) + + Attaches a new success callback to the deferred, shortcut for + ``deferred.then(doneCallback)``. + + .. note:: a difference is the result of :js:func:`Deferred.done`'s + is ignored rather than forwarded through the chain + + This is a jQuery extension to `CommonJS Promises/A`_ providing + little value over calling :js:func:`~Deferred.then` directly, + it should be avoided. + + :param doneCallback: function called when the deferred is resolved + :type doneCallback: Function + :returns: the deferred object on which it was called + :rtype: :js:class:`Deferred` + + .. js:function:: Deferred.fail(failCallback) + + Attaches a new failure callback to the deferred, shortcut for + ``deferred.then(null, failCallback)``. + + A second jQuery extension to `Promises/A <CommonJS + Promises/A>`_. Although it provides more value than + :js:func:`~Deferred.done`, it still is not much and should be + avoided as well. + + :param failCallback: function called when the deferred is rejected + :type failCallback: Function + :returns: the deferred object on which it was called + :rtype: :js:class:`Deferred` + + .. js:function:: Deferred.promise() + + Returns a read-only view of the deferred object, with all + mutators (resolve and reject) methods removed. + + .. js:function:: Deferred.resolve(value…) + + Called to resolve a deferred, any value provided will be + passed onto the success handlers of the deferred object. + + Resolving a deferred which has already been resolved or + rejected has no effect. + + .. js:function:: Deferred.reject(value…) + + Called to reject (fail) a deferred, any value provided will be + passed onto the failure handler of the deferred object. + + Rejecting a deferred which has already been resolved or + rejected has no effect. + +.. [#] or simply calling :js:class:`Deferred` as a function, the + result is the same + +.. [#] or not-promises, the `CommonJS Promises/B`_ role of + :js:func:`when` is to be able to treat values and promises + uniformly: :js:func:`when` will pass promises through directly, + but non-promise values and objects will be transformed into a + resolved promise (resolving themselves with the value itself). + + jQuery's :js:func:`when` keeps this behavior making deferreds + easy to build from "static" values, or allowing defensive code + where expected promises are wrapped in :js:func:`when` just in + case. + +.. _promises: http://en.wikipedia.org/wiki/Promise_(programming) +.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/ +.. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A +.. _CommonJS Promises/B: http://wiki.commonjs.org/wiki/Promises/B +.. _mocks: http://en.wikipedia.org/wiki/Mock_object diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst index 140405d0037..e3f2952c5d3 100644 --- a/doc/reference/cmdline.rst +++ b/doc/reference/cmdline.rst @@ -38,6 +38,10 @@ Running the server (:file:`{$HOME}/.openerp_serverrc` by default, overridable using :option:`-c`) +.. option:: --test-enable + + runs tests after installing modules + .. _reference/cmdline/scaffold: Scaffolding diff --git a/doc/reference/http.rst b/doc/reference/http.rst index c179691c4cb..9f83a303d92 100644 --- a/doc/reference/http.rst +++ b/doc/reference/http.rst @@ -2,6 +2,8 @@ Web Controllers =============== +.. _reference/http/routing: + Routing ======= @@ -59,7 +61,7 @@ and defining methods decorated with :func:`~openerp.http.route`:: return stuff() To *override* a controller, :ref:`inherit <python:tut-inheritance>` from its -class and override relevant methods:: +class and override relevant methods, re-exposing them if necessary:: class Extension(MyController): @route() diff --git a/doc/reference/images/runner.png b/doc/reference/images/runner.png Binary files differnew file mode 100644 index 00000000000..bd48e9d2922 --- /dev/null +++ b/doc/reference/images/runner.png diff --git a/doc/reference/images/runner2.png b/doc/reference/images/runner2.png Binary files differnew file mode 100644 index 00000000000..38ea2949cfc --- /dev/null +++ b/doc/reference/images/runner2.png diff --git a/doc/reference/images/tests.png b/doc/reference/images/tests.png Binary files differnew file mode 100644 index 00000000000..84083d9e5ed --- /dev/null +++ b/doc/reference/images/tests.png diff --git a/doc/reference/images/tests2.png b/doc/reference/images/tests2.png Binary files differnew file mode 100644 index 00000000000..c8a6f8ae9ee --- /dev/null +++ b/doc/reference/images/tests2.png diff --git a/doc/reference/images/tests3.png b/doc/reference/images/tests3.png Binary files differnew file mode 100644 index 00000000000..247f70716af --- /dev/null +++ b/doc/reference/images/tests3.png diff --git a/doc/reference/javascript.rst b/doc/reference/javascript.rst index ae7b02c63a4..938c2237912 100644 --- a/doc/reference/javascript.rst +++ b/doc/reference/javascript.rst @@ -1,3 +1,7 @@ +.. highlight:: javascript + +.. default-domain:: js + ========== Javascript ========== @@ -5,10 +9,1273 @@ Javascript Widgets ======= +.. class:: openerp.Widget + + The base class for all visual components. It corresponds to an MVC + view, and provides a number of service to simplify handling of a section + of a page: + + * Handles parent/child relationships between widgets + * Provides extensive lifecycle management with safety features (e.g. + automatically destroying children widgets during the destruction of a + parent) + * Automatic rendering with :ref:`qweb <reference/qweb>` + * Backbone-compatible shortcuts + +DOM Root +-------- + +A :class:`~openerp.Widget` is responsible for a section of the page +materialized by the DOM root of the widget. + +A widget's DOM root is available via two attributes: + +.. attribute:: openerp.Widget.el + + raw DOM element set as root to the widget + +.. attribute:: openerp.Widget.$el + + jQuery wrapper around :attr:`~openerp.Widget.el` + +There are two main ways to define and generate this DOM root: + +.. attribute:: openerp.Widget.template + + Should be set to the name of a :ref:`QWeb template <reference/qweb>`. + If set, the template will be rendered after the widget has been + initialized but before it has been started. The root element generated by + the template will be set as the DOM root of the widget. + +.. attribute:: openerp.Widget.tagName + + Used if the widget has no template defined. Defaults to ``div``, + will be used as the tag name to create the DOM element to set as + the widget's DOM root. It is possible to further customize this + generated DOM root with the following attributes: + + .. attribute:: openerp.Widget.id + + Used to generate an ``id`` attribute on the generated DOM + root. + + .. attribute:: openerp.Widget.className + + Used to generate a ``class`` attribute on the generated DOM root. + + .. attribute:: openerp.Widget.attributes + + Mapping (object literal) of attribute names to attribute + values. Each of these k:v pairs will be set as a DOM attribute + on the generated DOM root. + + None of these is used in case a template is specified on the widget. + +The DOM root can also be defined programmatically by overridding + +.. function:: openerp.Widget.renderElement + + Renders the widget's DOM root and sets it. The default + implementation will render a set template or generate an element + as described above, and will call + :func:`~openerp.Widget.setElement` on the result. + + Any override to :func:`~openerp.Widget.renderElement` which + does not call its ``_super`` **must** call + :func:`~openerp.Widget.setElement` with whatever it + generated or the widget's behavior is undefined. + + .. note:: + + The default :func:`~openerp.Widget.renderElement` can + be called repeatedly, it will *replace* the previous DOM root + (using ``replaceWith``). However, this requires that the + widget correctly sets and unsets its events (and children + widgets). Generally, :func:`~openerp.Widget.renderElement` should + not be called repeatedly unless the widget advertizes this feature. + +Using a widget +'''''''''''''' + +A widget's lifecycle has 3 main phases: + +* creation and initialization of the widget instance + + .. function:: openerp.Widget.init(parent) + + initialization method of widgets, synchronous, can be overridden to + take more parameters from the widget's creator/parent + + :param parent: the new widget's parent, used to handle automatic + destruction and event propagation. Can be ``null`` for + the widget to have no parent. + :type parent: :class:`~openerp.Widget` + +* DOM injection and startup, this is done by calling one of: + + .. function:: openerp.Widget.appendTo(element) + + Renders the widget and inserts it as the last child of the target, uses + `.appendTo()`_ + + .. function:: openerp.Widget.prependTo(element) + + Renders the widget and inserts it as the first child of the target, uses + `.prependTo()`_ + + .. function:: openerp.Widget.insertAfter(element) + + Renders the widget and inserts it as the preceding sibling of the target, + uses `.insertAfter()`_ + + .. function:: openerp.Widget.insertBefore(element) + + Renders the widget and inserts it as the following sibling of the target, + uses `.insertBefore()`_ + + All of these methods accept whatever the corresponding jQuery method accepts + (CSS selectors, DOM nodes or jQuery objects). They all return a deferred_ + and are charged with three tasks: + + * rendering the widget's root element via + :func:`~openerp.Widget.renderElement` + * inserting the widget's root element in the DOM using whichever jQuery + method they match + * starting the widget, and returning the result of starting it + + .. function:: openerp.Widget.start() + + asynchronous startup of the widget once it's been injected in the DOM, + generally used to perform asynchronous RPC calls to fetch whatever + remote data is necessary for the widget to do its work. + + Must return a deferred_ to indicate when its work is done. + + A widget is *not guaranteed* to work correctly until its + :func:`~openerp.Widget.start` method has finished executing. The + widget's parent/creator must wait for a widget to be fully started + before interacting with it + + :returns: deferred_ object + +* widget destruction and cleanup + + .. function:: openerp.Widget.destroy() + + destroys the widget's children, unbinds its events and removes its root + from the DOM. Automatically called when the widget's parent is destroyed, + must be called explicitly if the widget has no parents or if it is + removed but its parent remains. + + A widget being destroyed is automatically unlinked from its parent. + +Related to widget destruction is an important utility method: + +.. function:: openerp.Widget.alive(deferred[, reject=false]) + + A significant issue with RPC and destruction is that an RPC call may take + a long time to execute and return while a widget is being destroyed or + after it has been destroyed, trying to execute its operations on a widget + in a broken/invalid state. + + This is a frequent source of errors or strange behaviors. + + :func:`~openerp.Widget.alive` can be used to wrap an RPC call, + ensuring that whatever operations should be executed when the call ends + are only executed if the widget is still alive:: + + this.alive(this.model.query().all()).then(function (records) { + // would break if executed after the widget is destroyed, wrapping + // rpc in alive() prevents execution + _.each(records, function (record) { + self.$el.append(self.format(record)); + }); + }); + + :param deferred: a deferred_ object to wrap + :param reject: by default, if the RPC call returns after the widget has + been destroyed the returned deferred_ is left in limbo + (neither resolved nor rejected). If ``reject`` is set to + ``true``, the deferred_ will be rejected instead. + :returns: deferred_ object + +.. function:: openerp.Widget.isDestroyed() + + :returns: ``true`` if the widget is being or has been destroyed, ``false`` + otherwise + +Accessing DOM content +''''''''''''''''''''' + +Because a widget is only responsible for the content below its DOM root, there + is a shortcut for selecting sub-sections of a widget's DOM: + +.. function:: openerp.Widget.$(selector) + + Applies the CSS selector specified as parameter to the widget's + DOM root:: + + this.$(selector); + + is functionally identical to:: + + this.$el.find(selector); + + :param String selector: CSS selector + :returns: jQuery object + + .. note:: this helper method is similar to ``Backbone.View.$`` + +Resetting the DOM root +'''''''''''''''''''''' + +.. function:: openerp.Widget.setElement(element) + + Re-sets the widget's DOM root to the provided element, also + handles re-setting the various aliases of the DOM root as well as + unsetting and re-setting delegated events. + + :param Element element: a DOM element or jQuery object to set as + the widget's DOM root + + .. note:: should be mostly compatible with `Backbone's + setElement`_ + +DOM events handling +------------------- + +A widget will generally need to respond to user action within its +section of the page. This entails binding events to DOM elements. + +To this end, :class:`~openerp.Widget` provides a shortcut: + +.. attribute:: openerp.Widget.events + + Events are a mapping of an event selector (an event name and an optional + CSS selector separated by a space) to a callback. The callback can + be the name of a widget's method or a function object. In either case, the + ``this`` will be set to the widget:: + + events: { + 'click p.oe_some_class a': 'some_method', + 'change input': function (e) { + e.stopPropagation(); + } + }, + + The selector is used for jQuery's `event delegation`_, the + callback will only be triggered for descendants of the DOM root + matching the selector\ [#eventsdelegation]_. If the selector is left out + (only an event name is specified), the event will be set directly on the + widget's DOM root. + +.. function:: openerp.Widget.delegateEvents + + This method is in charge of binding :attr:`~openerp.Widget.events` to the + DOM. It is automatically called after setting the widget's DOM root. + + It can be overridden to set up more complex events than the + :attr:`~openerp.Widget.events` map allows, but the parent should always be + called (or :attr:`~openerp.Widget.events` won't be handled correctly). + +.. function:: openerp.Widget.undelegateEvents + + This method is in charge of unbinding :attr:`~openerp.Widget.events` from + the DOM root when the widget is destroyed or the DOM root is reset, in + order to avoid leaving "phantom" events. + + It should be overridden to un-set any event set in an override of + :func:`~openerp.Widget.delegateEvents`. + +.. note:: this behavior should be compatible with `Backbone's + delegateEvents`_, apart from not accepting any argument. + +Subclassing Widget +------------------ + +:class:`~openerp.Widget` is subclassed in the standard manner (via the +:func:`~openerp.Class.extend` method), and provides a number of +abstract properties and concrete methods (which you may or may not want to +override). Creating a subclass looks like this:: + + var MyWidget = openerp.Widget.extend({ + // QWeb template to use when rendering the object + template: "MyQWebTemplate", + events: { + // events binding example + 'click .my-button': 'handle_click', + }, + + init: function(parent) { + this._super(parent); + // insert code to execute before rendering, for object + // initialization + }, + start: function() { + var sup = this._super(); + // post-rendering initialization code, at this point + + // allows multiplexing deferred objects + return $.when( + // propagate asynchronous signal from parent class + sup, + // return own's asynchronous signal + this.rpc(/* … */)) + } + }); + +The new class can then be used in the following manner:: + + // Create the instance + var my_widget = new MyWidget(this); + // Render and insert into DOM + my_widget.appendTo(".some-div"); + +After these two lines have executed (and any promise returned by +:func:`~openerp.Widget.appendTo` has been resolved if needed), the widget is +ready to be used. + +.. note:: the insertion methods will start the widget themselves, and will + return the result of :func:`~openerp.Widget.start()`. + + If for some reason you do not want to call these methods, you will + have to first call :func:`~openerp.Widget.render()` on the + widget, then insert it into your DOM and start it. + +If the widget is not needed anymore (because it's transient), simply terminate +it:: + + my_widget.destroy(); + +will unbind all DOM events, remove the widget's content from the DOM and +destroy all widget data. + +Development Guidelines +---------------------- + +* Identifiers (``id`` attribute) should be avoided. In generic applications + and modules, ``id`` limits the re-usability of components and tends to make + code more brittle. Most of the time, they can be replaced with nothing, + classes or keeping a reference to a DOM node or jQuery element. + + If an ``id`` is absolutely necessary (because a third-party library requires + one), the id should be partially generated using ``_.uniqueId()`` e.g.:: + + this.id = _.uniqueId('my-widget-') +* Avoid predictable/common CSS class names. Class names such as "content" or + "navigation" might match the desired meaning/semantics, but it is likely an + other developer will have the same need, creating a naming conflict and + unintended behavior. Generic class names should be prefixed with e.g. the + name of the component they belong to (creating "informal" namespaces, much + as in C or Objective-C). +* Global selectors should be avoided. Because a component may be used several + times in a single page (an example in Odoo is dashboards), queries should be + restricted to a given component's scope. Unfiltered selections such as + ``$(selector)`` or ``document.querySelectorAll(selector)`` will generally + lead to unintended or incorrect behavior. Odoo Web's + :class:`~openerp.web.Widget` has an attribute providing its DOM root + (:attr:`~openerp.web.Widget.$el`), and a shortcut to select nodes directly + (:func:`~openerp.web.Widget.$`). +* More generally, never assume your components own or controls anything beyond + its own personal :attr:`~openerp.web.Widget.$el` +* html templating/rendering should use QWeb unless absolutely trivial. +* All interactive components (components displaying information to the screen + or intercepting DOM events) must inherit from :class:`~openerp.web.Widget` + and correctly implement and use its API and life cycle. + +.. _.appendTo(): + http://api.jquery.com/appendTo/ + +.. _.prependTo(): + http://api.jquery.com/prependTo/ + +.. _.insertAfter(): + http://api.jquery.com/insertAfter/ + +.. _.insertBefore(): + http://api.jquery.com/insertBefore/ + +.. _event delegation: + http://api.jquery.com/delegate/ + +.. _Backbone's setElement: + http://backbonejs.org/#View-setElement + +.. _Backbone's delegateEvents: + http://backbonejs.org/#View-delegateEvents + +.. _deferred: http://api.jquery.com/category/deferred-object/ + RPC === +To display and interact with data, calls to the Odoo server are necessary. +This is performed using :abbr:`RPC <Remote Procedure Call>`. + +Odoo Web provides two primary APIs to handle this: a low-level +JSON-RPC based API communicating with the Python section of Odoo +Web (and of your module, if you have a Python part) and a high-level +API above that allowing your code to talk directly to high-level Odoo models. + +All networking APIs are :ref:`asynchronous <reference/async>`. As a result, +all of them will return Deferred_ objects (whether they resolve those with +values or not). Understanding how those work before before moving on is +probably necessary. + +High-level API: calling into Odoo models +------------------------------------------- + +Access to Odoo object methods (made available through XML-RPC from the server) +is done via :class:`openerp.Model`. It maps onto the Odoo server objects via +two primary methods, :func:`~openerp.Model.call` and +:func:`~openerp.Model.query`. + +:func:`~openerp.Model.call` is a direct mapping to the corresponding method of +the Odoo server object. Its usage is similar to that of the Odoo Model API, +with three differences: + +* The interface is :ref:`asynchronous <reference/async>`, so instead of + returning results directly RPC method calls will return + Deferred_ instances, which will themselves resolve to the + result of the matching RPC call. + +* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to + ``__getattr__`` or ``method_missing``, there needs to be an explicit + method to dispatch RPC methods. + +* No notion of pooler, the model proxy is instantiated where needed, + not fetched from an other (somewhat global) object:: + + var Users = new openerp.Model('res.users'); + + Users.call('change_password', ['oldpassword', 'newpassword'], + {context: some_context}).then(function (result) { + // do something with change_password result + }); + +:func:`~openerp.Model.query` is a shortcut for a builder-style +interface to searches (``search`` + ``read`` in Odoo RPC terms). It +returns a :class:`~openerp.web.Query` object which is immutable but +allows building new :class:`~openerp.web.Query` instances from the +first one, adding new properties or modifiying the parent object's:: + + Users.query(['name', 'login', 'user_email', 'signature']) + .filter([['active', '=', true], ['company_id', '=', main_company]]) + .limit(15) + .all().then(function (users) { + // do work with users records + }); + +The query is only actually performed when calling one of the query +serialization methods, :func:`~openerp.web.Query.all` and +:func:`~openerp.web.Query.first`. These methods will perform a new +RPC call every time they are called. + +For that reason, it's actually possible to keep "intermediate" queries +around and use them differently/add new specifications on them. + +.. class:: openerp.Model(name) + + .. attribute:: openerp.Model.name + + name of the OpenERP model this object is bound to + + .. function:: openerp.Model.call(method[, args][, kwargs]) + + Calls the ``method`` method of the current model, with the + provided positional and keyword arguments. + + :param String method: method to call over rpc on the + :attr:`~openerp.Model.name` + :param Array<> args: positional arguments to pass to the + method, optional + :param Object<> kwargs: keyword arguments to pass to the + method, optional + :rtype: Deferred<> + + .. function:: openerp.Model.query(fields) + + :param Array<String> fields: list of fields to fetch during + the search + :returns: a :class:`~openerp.web.Query` object + representing the search to perform + +.. class:: openerp.web.Query(fields) + + The first set of methods is the "fetching" methods. They perform + RPC queries using the internal data of the object they're called + on. + + .. function:: openerp.web.Query.all() + + Fetches the result of the current :class:`~openerp.web.Query` object's + search. + + :rtype: Deferred<Array<>> + + .. function:: openerp.web.Query.first() + + Fetches the **first** result of the current + :class:`~openerp.web.Query`, or ``null`` if the current + :class:`~openerp.web.Query` does have any result. + + :rtype: Deferred<Object | null> + + .. function:: openerp.web.Query.count() + + Fetches the number of records the current + :class:`~openerp.web.Query` would retrieve. + + :rtype: Deferred<Number> + + .. function:: openerp.web.Query.group_by(grouping...) + + Fetches the groups for the query, using the first specified + grouping parameter + + :param Array<String> grouping: Lists the levels of grouping + asked of the server. Grouping + can actually be an array or + varargs. + :rtype: Deferred<Array<openerp.web.QueryGroup>> | null + + The second set of methods is the "mutator" methods, they create a + **new** :class:`~openerp.web.Query` object with the relevant + (internal) attribute either augmented or replaced. + + .. function:: openerp.web.Query.context(ctx) + + Adds the provided ``ctx`` to the query, on top of any existing + context + + .. function:: openerp.web.Query.filter(domain) + + Adds the provided domain to the query, this domain is + ``AND``-ed to the existing query domain. + + .. function:: opeenrp.web.Query.offset(offset) + + Sets the provided offset on the query. The new offset + *replaces* the old one. + + .. function:: openerp.web.Query.limit(limit) + + Sets the provided limit on the query. The new limit *replaces* + the old one. + + .. function:: openerp.web.Query.order_by(fields…) + + Overrides the model's natural order with the provided field + specifications. Behaves much like Django's :py:meth:`QuerySet.order_by + <django.db.models.query.QuerySet.order_by>`: + + * Takes 1..n field names, in order of most to least importance + (the first field is the first sorting key). Fields are + provided as strings. + + * A field specifies an ascending order, unless it is prefixed + with the minus sign "``-``" in which case the field is used + in the descending order + + Divergences from Django's sorting include a lack of random sort + (``?`` field) and the inability to "drill down" into relations + for sorting. + +Aggregation (grouping) +'''''''''''''''''''''' + +Odoo has powerful grouping capacities, but they are kind-of strange +in that they're recursive, and level n+1 relies on data provided +directly by the grouping at level n. As a result, while +:py:meth:`openerp.models.Model.read_group` works it's not a very intuitive +API. + +Odoo Web eschews direct calls to :py:meth:`~openerp.models.Model.read_group` +in favor of calling a method of :class:`~openerp.web.Query`, :py:meth:`much +in the way it is one in SQLAlchemy <sqlalchemy.orm.query.Query.group_by>` +[#terminal]_:: + + some_query.group_by(['field1', 'field2']).then(function (groups) { + // do things with the fetched groups + }); + +This method is asynchronous when provided with 1..n fields (to group +on) as argument, but it can also be called without any field (empty +fields collection or nothing at all). In this case, instead of +returning a Deferred object it will return ``null``. + +When grouping criterion come from a third-party and may or may not +list fields (e.g. could be an empty list), this provides two ways to +test the presence of actual subgroups (versus the need to perform a +regular query for records): + +* A check on ``group_by``'s result and two completely separate code + paths:: + + var groups; + if (groups = some_query.group_by(gby)) { + groups.then(function (gs) { + // groups + }); + } + // no groups + +* Or a more coherent code path using :func:`when`'s ability to + coerce values into deferreds:: + + $.when(some_query.group_by(gby)).then(function (groups) { + if (!groups) { + // No grouping + } else { + // grouping, even if there are no groups (groups + // itself could be an empty array) + } + }); + +The result of a (successful) :func:`~openerp.web.Query.group_by` is +an array of :class:`~openerp.web.QueryGroup`: + +.. class:: openerp.web.QueryGroup + + .. function:: openerp.web.QueryGroup.get(key) + + returns the group's attribute ``key``. Known attributes are: + + ``grouped_on`` + which grouping field resulted from this group + ``value`` + ``grouped_on``'s value for this group + ``length`` + the number of records in the group + ``aggregates`` + a {field: value} mapping of aggregations for the group + + .. function:: openerp.web.QueryGroup.query([fields...]) + + equivalent to :func:`openerp.web.Model.query` but pre-filtered to + only include the records within this group. Returns a + :class:`~openerp.web.Query` which can be further manipulated as + usual. + + .. function:: openerp.web.QueryGroup.subgroups() + + returns a deferred to an array of :class:`~openerp.web.QueryGroup` + below this one + +Low-level API: RPC calls to Python side +--------------------------------------- + +While the previous section is great for calling core OpenERP code +(models code), it does not work if you want to call the Python side of +Odoo Web. + +For this, a lower-level API exists on on +:class:`~openerp.web.Session` objects (usually available through +``openerp.session``): the ``rpc`` method. + +This method simply takes an absolute path (the absolute URL of the JSON +:ref:`route <reference/http/routing>` to call) and a mapping of attributes to +values (passed as keyword arguments to the Python method). This function +fetches the return value of the Python methods, converted to JSON. + +For instance, to call the ``resequence`` of the +:class:`~web.controllers.main.DataSet` controller:: + + openerp.session.rpc('/web/dataset/resequence', { + model: some_model, + ids: array_of_ids, + offset: 42 + }).then(function (result) { + // resequence didn't error out + }, function () { + // an error occured during during call + }); + .. _reference/javascript/client: Web Client ========== + +Testing in Odoo Web Client +========================== + +Javascript Unit Testing +----------------------- + +Odoo Web includes means to unit-test both the core code of +Odoo Web and your own javascript modules. On the javascript side, +unit-testing is based on QUnit_ with a number of helpers and +extensions for better integration with Odoo. + +To see what the runner looks like, find (or start) an Odoo server +with the web client enabled, and navigate to ``/web/tests`` +This will show the runner selector, which lists all modules with javascript +unit tests, and allows starting any of them (or all javascript tests in all +modules at once). + +.. image:: ./images/runner.png + :align: center + +Clicking any runner button will launch the corresponding tests in the +bundled QUnit_ runner: + +.. image:: ./images/tests.png + :align: center + +Writing a test case +------------------- + +The first step is to list the test file(s). This is done through the +``test`` key of the Odoo manifest, by adding javascript files to it: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'test': ['static/test/demo.js'], + } + +and to create the corresponding test file(s) + +.. note:: + + Test files which do not exist will be ignored, if all test files + of a module are ignored (can not be found), the test runner will + consider that the module has no javascript tests. + +After that, refreshing the runner selector will display the new module +and allow running all of its (0 so far) tests: + +.. image:: ./images/runner2.png + :align: center + +The next step is to create a test case:: + + openerp.testing.section('basic section', function (test) { + test('my first test', function () { + ok(false, "this test has run"); + }); + }); + +All testing helpers and structures live in the ``openerp.testing`` +module. Odoo tests live in a :func:`~openerp.testing.section`, +which is itself part of a module. The first argument to a section is +the name of the section, the second one is the section body. + +:func:`test <openerp.testing.case>`, provided by the +:func:`~openerp.testing.section` to the callback, is used to +register a given test case which will be run whenever the test runner +actually does its job. Odoo Web test case use standard `QUnit +assertions`_ within them. + +Launching the test runner at this point will run the test and display +the corresponding assertion message, with red colors indicating the +test failed: + +.. image:: ./images/tests2.png + :align: center + +Fixing the test (by replacing ``false`` to ``true`` in the assertion) +will make it pass: + +.. image:: ./images/tests3.png + :align: center + +Assertions +---------- + +As noted above, Odoo Web's tests use `qunit assertions`_. They are +available globally (so they can just be called without references to +anything). The following list is available: + +.. function:: ok(state[, message]) + + checks that ``state`` is truthy (in the javascript sense) + +.. function:: strictEqual(actual, expected[, message]) + + checks that the actual (produced by a method being tested) and + expected values are identical (roughly equivalent to ``ok(actual + === expected, message)``) + +.. function:: notStrictEqual(actual, expected[, message]) + + checks that the actual and expected values are *not* identical + (roughly equivalent to ``ok(actual !== expected, message)``) + +.. function:: deepEqual(actual, expected[, message]) + + deep comparison between actual and expected: recurse into + containers (objects and arrays) to ensure that they have the same + keys/number of elements, and the values match. + +.. function:: notDeepEqual(actual, expected[, message]) + + inverse operation to :func:`deepEqual` + +.. function:: throws(block[, expected][, message]) + + checks that, when called, the ``block`` throws an + error. Optionally validates that error against ``expected``. + + :param Function block: + :param expected: if a regexp, checks that the thrown error's + message matches the regular expression. If an + error type, checks that the thrown error is of + that type. + :type expected: Error | RegExp + +.. function:: equal(actual, expected[, message]) + + checks that ``actual`` and ``expected`` are loosely equal, using + the ``==`` operator and its coercion rules. + +.. function:: notEqual(actual, expected[, message]) + + inverse operation to :func:`equal` + +Getting an Odoo instance +------------------------ + +The Odoo instance is the base through which most Odoo Web +modules behaviors (functions, objects, …) are accessed. As a result, +the test framework automatically builds one, and loads the module +being tested and all of its dependencies inside it. This new instance +is provided as the first positional parameter to your test +cases. Let's observe by adding javascript code (not test code) to the +test module: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'js': ['static/src/js/demo.js'], + 'test': ['static/test/demo.js'], + } + +:: + + // src/js/demo.js + openerp.web_tests_demo = function (instance) { + instance.web_tests_demo = { + value_true: true, + SomeType: instance.web.Class.extend({ + init: function (value) { + this.value = value; + } + }) + }; + }; + +and then adding a new test case, which simply checks that the +``instance`` contains all the expected stuff we created in the +module:: + + // test/demo.js + test('module content', function (instance) { + ok(instance.web_tests_demo.value_true, "should have a true value"); + var type_instance = new instance.web_tests_demo.SomeType(42); + strictEqual(type_instance.value, 42, "should have provided value"); + }); + +DOM Scratchpad +-------------- + +As in the wider client, arbitrarily accessing document content is +strongly discouraged during tests. But DOM access is still needed to +e.g. fully initialize :class:`widgets <~openerp.Widget>` before +testing them. + +Thus, a test case gets a DOM scratchpad as its second positional +parameter, in a jQuery instance. That scratchpad is fully cleaned up +before each test, and as long as it doesn't do anything outside the +scratchpad your code can do whatever it wants:: + + // test/demo.js + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('<div><span class="foo bar">ok</span></div>'); + ok($scratchpad.find('span').hasClass('foo'), + "should have provided class"); + }); + test('clean scratchpad', function (instance, $scratchpad) { + ok(!$scratchpad.children().length, "should have no content"); + ok(!$scratchpad.text(), "should have no text"); + }); + +.. note:: + + The top-level element of the scratchpad is not cleaned up, test + cases can add text or DOM children but shoud not alter + ``$scratchpad`` itself. + +Loading templates +----------------- + +To avoid the corresponding processing costs, by default templates are +not loaded into QWeb. If you need to render e.g. widgets making use of +QWeb templates, you can request their loading through the +:attr:`~TestOptions.templates` option to the :func:`test case +function <openerp.testing.case>`. + +This will automatically load all relevant templates in the instance's +qweb before running the test case: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'js': ['static/src/js/demo.js'], + 'test': ['static/test/demo.js'], + 'qweb': ['static/src/xml/demo.xml'], + } + +.. code-block:: xml + + <!-- src/xml/demo.xml --> + <templates id="template" xml:space="preserve"> + <t t-name="DemoTemplate"> + <t t-foreach="5" t-as="value"> + <p><t t-esc="value"/></p> + </t> + </t> + </templates> + +:: + + // test/demo.js + test('templates', {templates: true}, function (instance) { + var s = instance.web.qweb.render('DemoTemplate'); + var texts = $(s).find('p').map(function () { + return $(this).text(); + }).get(); + + deepEqual(texts, ['0', '1', '2', '3', '4']); + }); + +Asynchronous cases +------------------ + +The test case examples so far are all synchronous, they execute from +the first to the last line and once the last line has executed the +test is done. But the web client is full of :ref:`asynchronous code +<reference/async>`, and thus test cases need to be async-aware. + +This is done by returning a :class:`deferred <Deferred>` from the +case callback:: + + // test/demo.js + test('asynchronous', { + asserts: 1 + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.resolve(); + }, 100); + return d; + }); + +This example also uses the :class:`options parameter <TestOptions>` +to specify the number of assertions the case should expect, if less or +more assertions are specified the case will count as failed. + +Asynchronous test cases *must* specify the number of assertions they +will run. This allows more easily catching situations where e.g. the +test architecture was not warned about asynchronous operations. + +.. note:: + + Asynchronous test cases also have a 2 seconds timeout: if the test + does not finish within 2 seconds, it will be considered + failed. This pretty much always means the test will not + resolve. This timeout *only* applies to the test itself, not to + the setup and teardown processes. + +.. note:: + + If the returned deferred is rejected, the test will be failed + unless :attr:`~TestOptions.fail_on_rejection` is set to + ``false``. + +RPC +--- + +An important subset of asynchronous test cases is test cases which +need to perform (and chain, to an extent) RPC calls. + +.. note:: + + Because they are a subset of asynchronous cases, RPC cases must + also provide a valid :attr:`assertions count + <TestOptions.asserts>`. + +To enable mock RPC, set the :attr:`rpc option <TestOptions.rpc>` to +``mock``. This will add a third parameter to the test case callback: + +.. function:: mock(rpc_spec, handler) + + Can be used in two different ways depending on the shape of the + first parameter: + + * If it matches the pattern ``model:method`` (if it contains a + colon, essentially) the call will set up the mocking of an RPC + call straight to the Odoo server (through XMLRPC) as + performed via e.g. :func:`openerp.web.Model.call`. + + In that case, ``handler`` should be a function taking two + arguments ``args`` and ``kwargs``, matching the corresponding + arguments on the server side and should simply return the value + as if it were returned by the Python XMLRPC handler:: + + test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { + // set up mocking + mock('people.famous:name_search', function (args, kwargs) { + strictEqual(kwargs.name, 'bob'); + return [ + [1, "Microsoft Bob"], + [2, "Bob the Builder"], + [3, "Silent Bob"] + ]; + }); + + // actual test code + return new instance.web.Model('people.famous') + .call('name_search', {name: 'bob'}).then(function (result) { + strictEqual(result.length, 3, "shoud return 3 people"); + strictEqual(result[0][1], "Microsoft Bob", + "the most famous bob should be Microsoft Bob"); + }); + }); + + * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it + will mock a JSON-RPC call to a web client controller, such as + ``/web/webclient/translations``. In that case, the handler takes + a single ``params`` argument holding all of the parameters + provided over JSON-RPC. + + As previously, the handler should simply return the result value + as if returned by the original JSON-RPC handler:: + + test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) { + var fetched_dbs = false, fetched_langs = false; + mock('/web/database/get_list', function () { + fetched_dbs = true; + return ['foo', 'bar', 'baz']; + }); + mock('/web/session/get_lang_list', function () { + fetched_langs = true; + return [['vo_IS', 'Hopelandic / Vonlenska']]; + }); + + // widget needs that or it blows up + instance.webclient = {toggle_bars: openerp.testing.noop}; + var dbm = new instance.web.DatabaseManager({}); + return dbm.appendTo($s).then(function () { + ok(fetched_dbs, "should have fetched databases"); + ok(fetched_langs, "should have fetched languages"); + deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); + }); + }); + +.. note:: + + Mock handlers can contain assertions, these assertions should be + part of the assertions count (and if multiple calls are made to a + handler containing assertions, it multiplies the effective number + of assertions). + +Testing API +----------- + +.. function:: openerp.testing.section(name[, options], body) + + A test section, serves as shared namespace for related tests (for + constants or values to only set up once). The ``body`` function + should contain the tests themselves. + + Note that the order in which tests are run is essentially + undefined, do *not* rely on it. + + :param String name: + :param TestOptions options: + :param body: + :type body: Function<:func:`~openerp.testing.case`, void> + +.. function:: openerp.testing.case(name[, options], callback) + + Registers a test case callback in the test runner, the callback + will only be run once the runner is started (or maybe not at all, + if the test is filtered out). + + :param String name: + :param TestOptions options: + :param callback: + :type callback: Function<instance, $, Function<String, Function, void>> + +.. class:: TestOptions + + the various options which can be passed to + :func:`~openerp.testing.section` or + :func:`~openerp.testing.case`. Except for + :attr:`~TestOptions.setup` and + :attr:`~TestOptions.teardown`, an option on + :func:`~openerp.testing.case` will overwrite the corresponding + option on :func:`~openerp.testing.section` so + e.g. :attr:`~TestOptions.rpc` can be set for a + :func:`~openerp.testing.section` and then differently set for + some :func:`~openerp.testing.case` of that + :func:`~openerp.testing.section` + + .. attribute:: TestOptions.asserts + + An integer, the number of assertions which should run during a + normal execution of the test. Mandatory for asynchronous tests. + + .. attribute:: TestOptions.setup + + Test case setup, run right before each test case. A section's + :func:`~TestOptions.setup` is run before the case's own, if + both are specified. + + .. attribute:: TestOptions.teardown + + Test case teardown, a case's :func:`~TestOptions.teardown` + is run before the corresponding section if both are present. + + .. attribute:: TestOptions.fail_on_rejection + + If the test is asynchronous and its resulting promise is + rejected, fail the test. Defaults to ``true``, set to + ``false`` to not fail the test in case of rejection:: + + // test/demo.js + test('unfail rejection', { + asserts: 1, + fail_on_rejection: false + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.reject(); + }, 100); + return d; + }); + + .. attribute:: TestOptions.rpc + + RPC method to use during tests, one of ``"mock"`` or + ``"rpc"``. Any other value will disable RPC for the test (if + they were enabled by the suite for instance). + + .. attribute:: TestOptions.templates + + Whether the current module (and its dependencies)'s templates + should be loaded into QWeb before starting the test. A + boolean, ``false`` by default. + +The test runner can also use two global configuration values set +directly on the ``window`` object: + +* ``oe_all_dependencies`` is an ``Array`` of all modules with a web + component, ordered by dependency (for a module ``A`` with + dependencies ``A'``, any module of ``A'`` must come before ``A`` in + the array) + +Running through Python +---------------------- + +The web client includes the means to run these tests on the +command-line (or in a CI system), but while actually running it is +pretty simple the setup of the pre-requisite parts has some +complexities. + +#. Install unittest2_ in your Python environment. Both + can trivially be installed via `pip <http://pip-installer.org>`_ or + `easy_install + <http://packages.python.org/distribute/easy_install.html>`_. + +#. Install PhantomJS_. It is a headless + browser which allows automating running and testing web + pages. QUnitSuite_ uses it to actually run the qunit_ test suite. + + The PhantomJS_ website provides pre-built binaries for some + platforms, and your OS's package management probably provides it as + well. + + If you're building PhantomJS_ from source, I recommend preparing + for some knitting time as it's not exactly fast (it needs to + compile both `Qt <http://qt-project.org/>`_ and `Webkit + <http://www.webkit.org/>`_, both being pretty big projects). + + .. note:: + + Because PhantomJS_ is webkit-based, it will not be able to test + if Firefox, Opera or Internet Explorer can correctly run the + test suite (and it is only an approximation for Safari and + Chrome). It is therefore recommended to *also* run the test + suites in actual browsers once in a while. + + .. note:: + + The version of PhantomJS_ this was build through is 1.7, + previous versions *should* work but are not actually supported + (and tend to just segfault when something goes wrong in + PhantomJS_ itself so they're a pain to debug). + +#. Install a new database with all relevant modules (all modules with + a web component at least), then restart the server + + .. note:: + + For some tests, a source database needs to be duplicated. This + operation requires that there be no connection to the database + being duplicated, but Odoo doesn't currently break + existing/outstanding connections, so restarting the server is + the simplest way to ensure everything is in the right state. + +#. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct + addons-path specified (and replacing ``$DATABASE`` by the source + database you created above) + + .. note:: + + If you leave out ``-mweb``, the runner will attempt to run all + the tests in all the modules, which may or may not work. + +If everything went correctly, you should now see a list of tests with +(hopefully) ``ok`` next to their names, closing with a report of the +number of tests run and the time it took: + +.. literalinclude:: test-report.txt + :language: text + +Congratulation, you have just performed a successful "offline" run of +the OpenERP Web test suite. + +.. note:: + + Note that this runs all the Python tests for the ``web`` module, + but all the web tests for all of Odoo. This can be surprising. + +.. _qunit: http://qunitjs.com/ + +.. _qunit assertions: http://api.qunitjs.com/category/assert/ + +.. _unittest2: http://pypi.python.org/pypi/unittest2 + +.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/ + +.. _PhantomJS: http://phantomjs.org/ + +.. [#eventsdelegation] not all DOM events are compatible with events delegation + +.. [#terminal] + with a small twist: :py:meth:`sqlalchemy.orm.query.Query.group_by` is not + terminal, it returns a query which can still be altered. + diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst index d010e8f5d0e..03548d1e399 100644 --- a/doc/reference/orm.rst +++ b/doc/reference/orm.rst @@ -138,7 +138,6 @@ Model .. automethod:: default_get .. automethod:: copy - .. automethod:: add_default_value .. automethod:: name_get .. automethod:: name_create @@ -410,7 +409,7 @@ and it's possible to write directly on the delegated field: Domains ======= -A domain is a list of criterion, each criterion being a triple (either a +A domain is a list of criteria, each criterion being a triple (either a ``list`` or a ``tuple``) of ``(field_name, operator, value)`` where: ``field_name`` (``str``) @@ -418,9 +417,51 @@ A domain is a list of criterion, each criterion being a triple (either a a :class:`~openerp.fields.Many2one` using dot-notation e.g. ``'street'`` or ``'partner_id.country'`` ``operator`` (``str``) - a comparison operator between the criterion's field and value. + an operator used to compare the ``field_name`` with the ``value``. Valid + operators are: + + ``=`` + equals to + ``!=`` + not equals to + ``>`` + greater than + ``>=`` + greater than or equal to + ``<`` + less than + ``<=`` + less than or equal to + ``=?`` + unset or equals to (returns true if ``value`` is either ``None`` or + ``False``, otherwise behaves like ``=``) + ``=like`` + matches ``field_name`` against the ``value`` pattern. An underscore + ``_`` in the pattern stands for (matches) any single character; a + percent sign ``%`` matches any string of zero or more characters. + ``like`` + matches ``field_name`` against the ``%value%`` pattern. Similar to + ``=like`` but wraps ``value`` with '%' before matching + ``not like`` + doesn't match against the ``%value%`` pattern + ``ilike`` + case insensitive ``like`` + ``not ilike`` + case insensitive ``not like`` + ``=ilike`` + case insensitive ``=like`` + ``in`` + is equal to any of the items from ``value``, ``value`` should be a + list of items + ``not in`` + is unequal to all of the items from ``value`` + ``child_of`` + is a child (descendant) of a ``value`` record. + + Takes the semantics of the model into account (i.e following the + relationship field named by + :attr:`~openerp.models.Model._parent_name`). - .. todo:: list and detail operators, original list is way incomplete ``value`` variable type, must be comparable (through ``operator``) to the named field diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst index c993c0c3c22..47a4d0a2774 100644 --- a/doc/reference/qweb.rst +++ b/doc/reference/qweb.rst @@ -1,37 +1,573 @@ +.. highlight:: xml + .. _reference/qweb: ==== QWeb ==== -Basics -====== +QWeb is the primary templating_ engine used by Odoo\ [#othertemplates]_. It +is an XML templating engine\ [#genshif]_ and used mostly to generate HTML_ +fragments and pages. + +Template directives are specified as XML attributes prefixed with ``t-``, +for instance ``t-if`` for :ref:`reference/qweb/conditionals`, with elements +and other attributes being rendered directly. + +To avoid element rendering, a placeholder element ``<t>`` is also available, +which executes its directive but doesn't generate any output in and of +itself:: + + <t t-if="condition"> + <p>Test</p> + </t> + +will result in:: + + <p>Test</p> + +if ``condition`` is true, but:: + + <div t-if="condition"> + <p>Test</p> + </div> + +will result in:: + + <div> + <p>Test</p> + </div> .. _reference/qweb/output: data output ------------ +=========== + +QWeb has a primary output directive which automatically HTML-escape its +content limiting XSS_ risks when displaying user-provided content: ``esc``. + +``esc`` takes an expression, evaluates it and prints the content:: + + <p><t t-esc="value"/></p> + +rendered with the value ``value`` set to ``42`` yields:: + + <p>42</p> + +There is one other output directive ``raw`` which behaves the same as +respectively ``esc`` but *does not HTML-escape its output*. It can be useful +to display separately constructed markup (e.g. from functions) or already +sanitized user-provided markup. .. _reference/qweb/conditionals: conditionals ------------- +============ + +QWeb has a conditional directive ``if``, which evaluates an expression given +as attribute value:: + + <div> + <t t-if="condition"> + <p>ok</p> + </t> + </div> + +The element is rendered if the condition is true:: + + <div> + <p>ok</p> + </div> + +but if the condition is false it is removed from the result:: + + <div> + </div> + +The conditional rendering applies to the bearer of the directive, which does +not have to be ``<t>``:: + + <div> + <p t-if="condition">ok</p> + </div> + +will give the same results as the previous example. .. _reference/qweb/loops: loops ------ +===== + +QWeb has an iteration directive ``foreach`` which take an expression returning +the collection to iterate on, and a second parameter ``t-as`` providing the +name to use for the "current item" of the iteration:: + + <t t-foreach="[1, 2, 3]" t-as="i"> + <p><t t-esc="i"/></p> + </t> + +will be rendered as:: + + <p>1</p> + <p>2</p> + <p>3</p> + +Like conditions, ``foreach`` applies to the element bearing the directive's +attribute, and + +:: + + <p t-foreach="[1, 2, 3]" t-as="i"> + <t t-esc="i"/> + </p> + +is equivalent to the previous example. + +``foreach`` can iterate on an array (the current item will be the current +value), a mapping (the current item will be the current key) or an integer +(equivalent to iterating on an array between 0 inclusive and the provided +integer exclusive). + +In addition to the name passed via ``t-as``, ``foreach`` provides a few other +variables for various data points (``$as`` is the name passed to ``t-as``): + +:samp:`{$as}_all` + the object being iterated over +:samp:`{$as}_value` + the current iteration value, identical to ``$as`` for lists and integers, + but for mappings it provides the value (where ``$as`` provides the key) +:samp:`{$as}_index` + the current iteration index (the first item of the iteration has index 0) +:samp:`{$as}_size` + the size of the collection if it is available +:samp:`{$as}_first` + whether the current item is the first of the iteration (equivalent to + :samp:`{$as}_index == 0`) +:samp:`{$as}_last` + whether the current item is the last of the iteration (equivalent to + :samp:`{$as}_index + 1 == {$as}_size`), requires the iteratee's size be + available +:samp:`{$as}_parity` + either ``"even"`` or ``"odd"``, the parity of the current iteration round +:samp:`{$as}_even` + a boolean flag indicating that the current iteration round is on an even + index +:samp:`{$as}_odd` + a boolean flag indicating that the current iteration round is on an odd + index + +.. _reference/qweb/attributes: + +attributes +========== + +QWeb can compute attributes on-the-fly and set the result of the computation +on the output node. This is done via the ``t-att`` (attribute) directive which +exists in 3 different forms: + +:samp:`t-att-{$name}` + an attribute called ``$name`` is created, the attribute value is evaluated + and the result is set as the attribute's value:: + + <div t-att-a="42"/> + + will be rendered as:: + + <div a="42"></div> +:samp:`t-attf-{$name}` + same as previous, but the parameter is a :term:`format string` + instead of just an expression, often useful to mix literal and non-literal + string (e.g. classes):: + + <t t-foreach="[1, 2, 3]" t-as="item"> + <li t-attf-class="row {{ item_parity }}"><t t-esc="item"/></li> + </t> + + will be rendered as:: + + <li class="row even">1</li> + <li class="row odd">2</li> + <li class="row even">3</li> +:samp:`t-att=mapping` + if the parameter is a mapping, each (key, value) pair generates a new + attribute and its value:: + + <div t-att="{'a': 1, 'b': 2}"/> + + will be rendered as:: + + <div a="1" b="2"></div> +:samp:`t-att=pair` + if the parameter is a pair (tuple or array of 2 element), the first + item of the pair is the name of the attribute and the second item is the + value:: + + <div t-att="['a', 'b']"/> + + will be rendered as:: + + <div a="b"></div> + +setting variables +================= + +QWeb allows creating variables from within the template, to memoize a +computation (to use it multiple times), give a piece of data a clearer name, +... + +This is done via the ``set`` directive, which takes the name of the variable +to create. The value to set can be provided in two ways: + +* a ``t-value`` attribute containing an expression, and the result of its + evaluation will be set:: + + <t t-set="foo" t-value="2 + 1"/> + <t t-esc="foo"/> + + will print ``3`` +* if there is no ``t-value`` attribute, the node's body is rendered and set + as the variable's value:: + + <t t-set="foo"> + <li>ok</li> + </t> + <t t-esc="foo"/> + + will generate ``<li>ok</li>`` (the content is escaped as we + used the ``esc`` directive) + + .. note:: using the result of this operation is a significant use-case for + the ``raw`` directive. + +calling sub-templates +===================== + +QWeb templates can be used for top-level rendering, but they can also be used +from within another template (to avoid duplication or give names to parts of +templates) using the ``t-call`` directive:: + + <t t-call="other-template"/> + +This calls the named template with the execution context of the parent, if +``other_template`` is defined as:: + + <p><t t-value="var"/></p> + +the call above will be rendered as ``<p/>`` (no content), but:: + + <t t-set="var" t-value="1"/> + <t t-call="other-template"/> + +will be rendered as ``<p>1</p>``. + +However this has the problem of being visible from outside the ``t-call``. +Alternatively, content set in the body of the ``call`` directive will be +evaluated *before* calling the sub-template, and can alter a local context:: + + <t t-call="other-template"> + <t t-set="var" t-value="1"/> + </t> + <!-- "var" does not exist here --> + +The body of the ``call`` directive can be arbitrarily complex (not just +``set`` directives), and its rendered form will be available within the called +template as a magical ``0`` variable:: + + <div> + This template was called with content: + <t t-raw="0"/> + </div> + +being called thus:: + + <t t-call="other-template"> + <em>content</em> + </t> + +will result in:: + + <div> + This template was called with content: + <em>content</em> + </div> Python ====== -Bundles +Exclusive directives +-------------------- + +asset bundles +''''''''''''' + +.. todo:: have fme write these up because I've no idea how they work + +"smart records" fields formatting +''''''''''''''''''''''''''''''''' + +The ``t-field`` directive can only be used when performing field access +(``a.b``) on a "smart" record (result of the ``browse`` method). It is able +to automatically format based on field type, and is integrated in the +website's rich text edition. + +``t-field-options`` can be used to customize fields, the most common option +is ``widget``, other options are field- or widget-dependent. + +Helpers ------- +Request-based +''''''''''''' + +Most Python-side uses of QWeb are in controllers (and during HTTP requests), +in which case templates stored in the database (as +:ref:`views <reference/views/qweb>`) can be trivially rendered by calling +:meth:`openerp.http.HttpRequest.render`: + +.. code-block:: python + + response = http.request.render('my-template', { + 'context_value': 42 + }) + +This automatically creates a :class:`~openerp.http.Response` object which can +be returned from the controller (or further customized to suit). + +View-based +'''''''''' + +At a deeper level than the previous helper is the ``render`` method on +``ir.ui.view``: + +.. py:method:: render(cr, uid, id[, values][, engine='ir.qweb][, context]) + + Renders a QWeb view/template by database id or :term:`external id`. + Templates are automatically loaded from ``ir.ui.view`` records. + + Sets up a number of default values in the rendering context: + + ``request`` + the current :class:`~openerp.http.WebRequest` object, if any + ``debug`` + whether the current request (if any) is in ``debug`` mode + :func:`quote_plus <werkzeug.urls.url_quote_plus>` + url-encoding utility function + :mod:`json` + the corresponding standard library module + :mod:`time` + the corresponding standard library module + :mod:`datetime` + the corresponding standard library module + `relativedelta <https://labix.org/python-dateutil#head-ba5ffd4df8111d1b83fc194b97ebecf837add454>`_ + see module + ``keep_query`` + the ``keep_query`` helper function + + :param values: context values to pass to QWeb for rendering + :param str engine: name of the Odoo model to use for rendering, can be + used to expand or customize QWeb locally (by creating + a "new" qweb based on ``ir.qweb`` with alterations) + .. _reference/qweb/javascript: +API +--- + +It is also possible to use the ``ir.qweb`` model directly (and extend it, and +inherit from it): + +.. automodule:: openerp.addons.base.ir.ir_qweb + :members: QWeb, QWebContext, FieldConverter, QwebWidget + Javascript ========== -loading +Exclusive directives +-------------------- + +defining templates +'''''''''''''''''' + +The ``t-name`` directive can only be placed at the top-level of a template +file (direct children to the document root):: + + <templates> + <t t-name="template-name"> + <!-- template code --> + </t> + </templates> + +It takes no other parameter, but can be used with a ``<t>`` element or any +other. With a ``<t>`` element, the ``<t>`` should have a single child. + +The template name is an arbitrary string, although when multiple templates +are related (e.g. called sub-templates) it is customary to use dot-separated +names to indicate hierarchical relationships. + +template inheritance +'''''''''''''''''''' + +Template inheritance is used to alter existing templates in-place, e.g. to +add information to templates created by an other modules. + +Template inheritance is performed via the ``t-extend`` directive which takes +the name of the template to alter as parameter. + +The alteration is then performed with any number of ``t-jquery`` +sub-directives:: + + <t t-extends="base.template"> + <t t-jquery="ul" t-operation="append"> + <li>new element</li> + </t> + </t> + +The ``t-jquery`` directives takes a `CSS selector`_. This selector is used +on the extended template to select *context nodes* to which the specified +``t-operation`` is applied: + +``append`` + the node's body is appended at the end of the context node (after the + context node's last child) +``prepend`` + the node's body is prepended to the context node (inserted before the + context node's first child) +``before`` + the node's body is inserted right before the context node +``after`` + the node's body is inserted right after the context node +``inner`` + the node's body replaces the context node's children +``replace`` + the node's body is used to replace the context node itself +No operation + if no ``t-operation`` is specified, the template body is interpreted as + javascript code and executed with the context node as ``this`` + + .. warning:: while much more powerful than other operations, this mode is + also much harder to debug and maintain, it is recommended to + avoid it + +debugging +--------- + +The javascript QWeb implementation provides a few debugging hooks: + +``t-log`` + takes an expression parameter, evaluates the expression during rendering + and logs its result with ``console.log`` +``t-debug`` + triggers a debugger breakpoint during template rendering +``t-js`` + the node's body is javascript code executed during template rendering. + Takes a ``context`` parameter, which is the name under which the rendering + context will be available in the ``t-js``'s body + +Helpers ------- + +.. js:attribute:: openerp.qweb + + An instance of :js:class:`QWeb2.Engine` with all module-defined template + files loaded, and references to standard helper objects ``_`` + (underscore), ``_t`` (translation function) and JSON_. + + :js:func:`openerp.qweb.render <QWeb2.Engine.render>` can be used to + easily render basic module templates + +API +--- + +.. js:class:: QWeb2.Engine + + The QWeb "renderer", handles most of QWeb's logic (loading, + parsing, compiling and rendering templates). + + OpenERP Web instantiates one for the user, and sets it to + ``instance.web.qweb``. It also loads all the template files of the + various modules into that QWeb instance. + + A :js:class:`QWeb2.Engine` also serves as a "template namespace". + + .. js:function:: QWeb2.Engine.render(template[, context]) + + Renders a previously loaded template to a String, using + ``context`` (if provided) to find the variables accessed + during template rendering (e.g. strings to display). + + :param String template: the name of the template to render + :param Object context: the basic namespace to use for template + rendering + :returns: String + + The engine exposes an other method which may be useful in some + cases (e.g. if you need a separate template namespace with, in + OpenERP Web, Kanban views get their own :js:class:`QWeb2.Engine` + instance so their templates don't collide with more general + "module" templates): + + .. js:function:: QWeb2.Engine.add_template(templates) + + Loads a template file (a collection of templates) in the QWeb + instance. The templates can be specified as: + + An XML string + QWeb will attempt to parse it to an XML document then load + it. + + A URL + QWeb will attempt to download the URL content, then load + the resulting XML string. + + A ``Document`` or ``Node`` + QWeb will traverse the first level of the document (the + child nodes of the provided root) and load any named + template or template override. + + :type templates: String | Document | Node + + A :js:class:`QWeb2.Engine` also exposes various attributes for + behavior customization: + + .. js:attribute:: QWeb2.Engine.prefix + + Prefix used to recognize directives during parsing. A string. By + default, ``t``. + + .. js:attribute:: QWeb2.Engine.debug + + Boolean flag putting the engine in "debug mode". Normally, + QWeb intercepts any error raised during template execution. In + debug mode, it leaves all exceptions go through without + intercepting them. + + .. js:attribute:: QWeb2.Engine.jQuery + + The jQuery instance used during template inheritance processing. + Defaults to ``window.jQuery``. + + .. js:attribute:: QWeb2.Engine.preprocess_node + + A ``Function``. If present, called before compiling each DOM + node to template code. In OpenERP Web, this is used to + automatically translate text content and some attributes in + templates. Defaults to ``null``. + +.. [#genshif] it is similar in that to Genshi_, although it does not use (and + has no support for) `XML namespaces`_ + +.. [#othertemplates] although it uses a few others, either for historical + reasons or because they remain better fits for the + use case. Odoo 8.0 still depends on Jinja_ and Mako_. + +.. _templating: + http://en.wikipedia.org/wiki/Template_processor + +.. _Jinja: http://jinja.pocoo.org +.. _Mako: http://www.makotemplates.org +.. _Genshi: http://genshi.edgewall.org +.. _XML namespaces: http://en.wikipedia.org/wiki/XML_namespace +.. _HTML: http://en.wikipedia.org/wiki/HTML +.. _XSS: http://en.wikipedia.org/wiki/Cross-site_scripting +.. _JSON: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON +.. _CSS selector: http://api.jquery.com/category/selectors/ diff --git a/doc/reference/test-report.txt b/doc/reference/test-report.txt new file mode 100644 index 00000000000..ce52618fe20 --- /dev/null +++ b/doc/reference/test-report.txt @@ -0,0 +1,25 @@ +test_empty_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok +test_ids_shortcut (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok +test_regular_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok +web.testing.stack: direct, value, success ... ok +web.testing.stack: direct, deferred, success ... ok +web.testing.stack: direct, value, error ... ok +web.testing.stack: direct, deferred, failure ... ok +web.testing.stack: successful setup ... ok +web.testing.stack: successful teardown ... ok +web.testing.stack: successful setup and teardown ... ok + +[snip ~150 lines] + +test_convert_complex_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_convert_complex_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_convert_literal_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_convert_literal_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_retrieve_nonliteral_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_retrieve_nonliteral_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok + +---------------------------------------------------------------------- +Ran 181 tests in 15.706s + +OK + diff --git a/doc/reference/testing.rst b/doc/reference/testing.rst new file mode 100644 index 00000000000..2676c2b4084 --- /dev/null +++ b/doc/reference/testing.rst @@ -0,0 +1,73 @@ +.. _reference/testing: + +=============== +Testing Modules +=============== + +Odoo provides support for testing modules using unittest2_. + +To write tests, simply define a ``tests`` sub-package in your module, it will +be automatically inspected for test modules. Test modules should have a name +starting with ``test_`` and should be imported from ``tests/__init__.py``, +e.g. + +.. code-block:: text + + your_module + |-- ... + `-- tests + |-- __init__.py + |-- test_bar.py + `-- test_foo.py + +and ``__init__.py`` contains:: + + from . import test_foo, test_bar + +.. note:: test modules which are not imported from ``tests/__init__.py`` will + not be run + +The test runner will simply run any test case, as described in the official +`unittest documentation`_, but Odoo provides a number of utilities and helpers +related to testing Odoo content (modules, mainly): + +.. autoclass:: openerp.tests.common.TransactionCase + :members: browse_ref, ref + +.. autoclass:: openerp.tests.common.SingleTransactionCase + :members: browse_ref, ref + +By default, tests are run once right after the corresponding module has been +installed. Test cases can also be configured to run after all modules have +been installed, and not run right after the module installation: + +.. autofunction:: openerp.tests.common.at_install + +.. autofunction:: openerp.tests.common.post_install + +The most common situation is to use +:class:`~openerp.tests.common.TransactionCase` and test a property of a of a +model in each method:: + + class TestModelA(common.TransactionCase): + def test_some_action(self): + record = self.env['model.a'].create({'field': 'value'}) + record.some_action() + self.assertEqual( + record.field, + expected_field_value) + + # other tests... + +Running tests +------------- + +Tests are automatically run when installing or updating modules if +:option:`--test-enable <odoo.py --test-enable>` was enabled when starting the +Odoo server. + +As of Odoo 8, running tests outside of the install/update cycle is not + + +.. _unittest2: http://pypi.python.org/pypi/unittest2 +.. _unittest documentation: https://docs.python.org/2/library/unittest.html |
