diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index 35cd3b23afa..511a271d30e 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -37,7 +37,6 @@ Javascript widget rpc async - qweb client_action testing diff --git a/addons/web/doc/qweb.rst b/addons/web/doc/qweb.rst deleted file mode 100644 index a8225a72fee..00000000000 --- a/addons/web/doc/qweb.rst +++ /dev/null @@ -1,529 +0,0 @@ -QWeb -==== - -QWeb is the template engine used by the OpenERP Web Client. It is an -XML-based templating language, similar to `Genshi -`_, -`Thymeleaf `_ or `Facelets -`_ with a few peculiarities: - -* It's implemented fully in javascript and rendered in the browser. -* Each template file (XML files) contains multiple templates, where - template engine usually have a 1:1 mapping between template files - and templates. -* It has special support in OpenERP Web's - :class:`~instance.web.Widget`, though it can be used outside of - OpenERP Web (and it's possible to use :class:`~instance.web.Widget` - without relying on the QWeb integration). - -The rationale behind using QWeb instead of a more popular template syntax is -that its extension mechanism is very similar to the openerp view inheritance -mechanism. Like openerp views a QWeb template is an xml tree and therefore -xpath or dom manipulations are easy to performs on it. - -Here's an example demonstrating most of the basic QWeb features: - -.. code-block:: xml - - -
-

-
    -
  • - - - -
  • -
-
- - -
- -
-
-
-
-
-
- -rendered with the following context: - -.. code-block:: json - - { - "class1": "foo", - "title": "Random Title", - "items": [ - { "name": "foo", "tags": {"bar": "baz", "qux": "quux"} }, - { "name": "Lorem", "tags": { - "ipsum": "dolor", - "sit": "amet", - "consectetur": "adipiscing", - "elit": "Sed", - "hendrerit": "ullamcorper", - "ante": "id", - "vestibulum": "Lorem", - "ipsum": "dolor", - "sit": "amet" - } - } - ] - } - -will yield this section of HTML document (reformated for readability): - -.. code-block:: html - -
-

Random Title

-
    -
  • - foo -
    -
    bar
    -
    baz
    -
    qux
    -
    quux
    -
    -
  • -
  • - Lorem -
    -
    ipsum
    -
    dolor
    -
    sit
    -
    amet
    -
    consectetur
    -
    adipiscing
    -
    elit
    -
    Sed
    -
    hendrerit
    -
    ullamcorper
    -
    -
  • -
-
- -API ---- - -While QWeb implements a number of attributes and methods for -customization and configuration, only two things are really important -to the user: - -.. 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 :ref:`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 :ref:`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``. - -.. _qweb-directives: - -Directives ----------- - -A basic QWeb template is nothing more than an XHTML document (as it -must be valid XML), which will be output as-is. But the rendering can -be customized with bits of logic called "directives". Directives are -attributes elements prefixed by :js:attr:`~QWeb2.Engine.prefix` (this -document will use the default prefix ``t``, as does OpenERP Web). - -A directive will usually control or alter the output of the element it -is set on. If no suitable element is available, the prefix itself can -be used as a "no-operation" element solely for supporting directives -(or internal content, which will be rendered). This means: - -.. code-block:: xml - - Something something - -will simply output the string "Something something" (the element -itself will be skipped and "unwrapped"): - -.. code-block:: javascript - - var e = new QWeb2.Engine(); - e.add_template('\ - Test 1\ - Test 2\ - '); - e.render('test1'); // Test 1 - e.render('test2'); // Test 2 - -.. note:: - - The conventions used in directive descriptions are the following: - - * directives are described as compound functions, potentially with - optional sections. Each section of the function name is an - attribute of the element bearing the directive. - - * a special parameter is ``BODY``, which does not have a name and - designates the content of the element. - - * special parameter types (aside from ``BODY`` which remains - untyped) are ``Name``, which designates a valid javascript - variable name, ``Expression`` which designates a valid - javascript expression, and ``Format`` which designates a - Ruby-style format string (a literal string with - ``#{Expression}`` inclusions executed and replaced by their - result) - -.. note:: - - ``Expression`` actually supports a few extensions on the - javascript syntax: because some syntactic elements of javascript - are not compatible with XML and must be escaped, text - substitutions are performed from forms which don't need to be - escaped. Thus the following "keyword operators" are available in - an ``Expression``: ``and`` (maps to ``&&``), ``or`` (maps to - ``||``), ``gt`` (maps to ``>``), ``gte`` (maps to ``>=``), ``lt`` - (maps to ``<``) and ``lte`` (maps to ``<=``). - -.. _qweb-directives-templates: - -Defining Templates -++++++++++++++++++ - -.. _qweb-directive-name: - -.. function:: t-name=name - - :param String name: an arbitrary javascript string. Each template - name is unique in a given - :js:class:`QWeb2.Engine` instance, defining a - new template with an existing name will - overwrite the previous one without warning. - - When multiple templates are related, it is - customary to use dotted names as a kind of - "namespace" e.g. ``foo`` and ``foo.bar`` which - will be used either by ``foo`` or by a - sub-widget of the widget used by ``foo``. - - Templates can only be defined as the children of the document - root. The document root's name is irrelevant (it's not checked) - but is usually ```` for simplicity. - - .. code-block:: xml - - - - - - - - :ref:`t-name ` can be used on an element with - an output as well: - - .. code-block:: xml - - -
- -
-
- - which ensures the template has a single root (if a template has - multiple roots and is then passed directly to jQuery, odd things - occur). - -.. _qweb-directives-output: - -Output -++++++ - -.. _qweb-directive-esc: - -.. function:: t-esc=content - - :param Expression content: - - Evaluates, html-escapes and outputs ``content``. - -.. _qweb-directive-raw: - -.. function:: t-raw=content - - :param Expression content: - - Similar to :ref:`t-esc ` but does *not* - html-escape the result of evaluating ``content``. Should only ever - be used for known-secure content, or will be an XSS attack vector. - -.. _qweb-directive-att: - -.. function:: t-att=map - - :param Expression map: - - Evaluates ``map`` expecting an ``Object`` result, sets each - key:value pair as an attribute (and its value) on the holder - element: - - .. code-block:: xml - - - - will yield - - .. code-block:: html - - - -.. function:: t-att-ATTNAME=value - - :param Name ATTNAME: - :param Expression value: - - Evaluates ``value`` and sets it on the attribute ``ATTNAME`` on - the holder element. - - If ``value``'s result is ``undefined``, suppresses the creation of - the attribute. - -.. _qweb-directive-attf: - -.. function:: t-attf-ATTNAME=value - - :param Name ATTNAME: - :param Format value: - - Similar to :ref:`t-att-* ` but the value of - the attribute is specified via a ``Format`` instead of an - expression. Useful for specifying e.g. classes mixing literal - classes and computed ones. - -.. _qweb-directives-flow: - -Flow Control -++++++++++++ - -.. _qweb-directive-set: - -.. function:: t-set=name (t-value=value | BODY) - - :param Name name: - :param Expression value: - :param BODY: - - Creates a new binding in the template context. If ``value`` is - specified, evaluates it and sets it to the specified - ``name``. Otherwise, processes ``BODY`` and uses that instead. - -.. _qweb-directive-if: - -.. function:: t-if=condition - - :param Expression condition: - - Evaluates ``condition``, suppresses the output of the holder - element and its content of the result is falsy. - -.. _qweb-directive-foreach: - -.. function:: t-foreach=iterable [t-as=name] - - :param Expression iterable: - :param Name name: - - Evaluates ``iterable``, iterates on it and evaluates the holder - element and its body once per iteration round. - - If ``name`` is not specified, computes a ``name`` based on - ``iterable`` (by replacing non-``Name`` characters by ``_``). - - If ``iterable`` yields a ``Number``, treats it as a range from 0 - to that number (excluded). - - While iterating, :ref:`t-foreach ` adds a - number of variables in the context: - - ``#{name}`` - If iterating on an array (or a range), the current value in - the iteration. If iterating on an *object*, the current key. - ``#{name}_all`` - The collection being iterated (the array generated for a - ``Number``) - ``#{name}_value`` - The current iteration value (current item for an array, value - for the current item for an object) - ``#{name}_index`` - The 0-based index of the current iteration round. - ``#{name}_first`` - Whether the current iteration round is the first one. - ``#{name}_parity`` - ``"odd"`` if the current iteration round is odd, ``"even"`` - otherwise. ``0`` is considered even. - -.. _qweb-directive-call: - -.. function:: t-call=template [BODY] - - :param String template: - :param BODY: - - Calls the specified ``template`` and returns its result. If - ``BODY`` is specified, it is evaluated *before* calling - ``template`` and can be used to specify e.g. parameters. This - usage is similar to `call-template with with-param in XSLT - `_. - -.. _qweb-directives-inheritance: - -Template Inheritance and Extension -++++++++++++++++++++++++++++++++++ - -.. _qweb-directive-extend: - -.. function:: t-extend=template BODY - - :param String template: name of the template to extend - - Works similarly to OpenERP models: if used on its own, will alter - the specified template in-place; if used in conjunction with - :ref:`t-name ` will create a new template - using the old one as a base. - - ``BODY`` should be a sequence of :ref:`t-jquery - ` alteration directives. - - .. note:: - - The inheritance in the second form is *static*: the parent - template is copied and transformed when :ref:`t-extend - ` is called. If it is altered later (by - a :ref:`t-extend ` without a - :ref:`t-name `), these changes will *not* - appear in the "child" templates. - -.. _qweb-directive-jquery: - -.. function:: t-jquery=selector [t-operation=operation] BODY - - :param String selector: a CSS selector into the parent template - :param operation: one of ``append``, ``prepend``, ``before``, - ``after``, ``inner`` or ``replace``. - :param BODY: ``operation`` argument, or alterations to perform - - * If ``operation`` is specified, applies the selector to the - parent template to find a *context node*, then applies - ``operation`` (as a jQuery operation) to the *context node*, - passing ``BODY`` as parameter. - - .. note:: - - ``replace`` maps to jQuery's `replaceWith(newContent) - `_, ``inner`` maps to - `html(htmlString) `_. - - * If ``operation`` is not provided, ``BODY`` is evaluated as - javascript code, with the *context node* as ``this``. - - .. warning:: - - While this second form is much more powerful than the first, - it is also much harder to read and maintain and should be - avoided. It is usually possible to either avoid it or - replace it with a sequence of ``t-jquery:t-operation:``. - -Escape Hatches / debugging -++++++++++++++++++++++++++ - -.. _qweb-directive-log: - -.. function:: t-log=expression - - :param Expression expression: - - Evaluates the provided expression (in the current template - context) and logs its result via ``console.log``. - -.. _qweb-directive-debug: - -.. function:: t-debug - - Injects a debugger breakpoint (via the ``debugger;`` statement) in - the compiled template output. - -.. _qweb-directive-js: - -.. function:: t-js=context BODY - - :param Name context: - :param BODY: javascript code - - Injects the provided ``BODY`` javascript code into the compiled - template, passing it the current template context using the name - specified by ``context``. diff --git a/addons/web/static/lib/qweb/qweb-test-attributes.xml b/addons/web/static/lib/qweb/qweb-test-attributes.xml index d03c20e922f..bf6070fed08 100644 --- a/addons/web/static/lib/qweb/qweb-test-attributes.xml +++ b/addons/web/static/lib/qweb/qweb-test-attributes.xml @@ -2,40 +2,57 @@
+
]]> +
+ {"value": "ok"} +
]]>
+
]]> + -
+
+ {"value": ["foo", "bar"]} +
]]> + + +
+ + {"value": {"a": 1, "b": 2, "c": 3}} +
]]>
- -
- - -
- - -
- +
]]> - -
- - +
- + {"value": "a"} +
]]> + +
- + {"value": 5} +
]]> + +
+ { + "value1": 0, + "value2": 1, + "value3": 2 + } +
+ ]]> diff --git a/addons/web/static/lib/qweb/qweb-test-call.xml b/addons/web/static/lib/qweb/qweb-test-call.xml index 3f7bd60ffbc..f71aca0b2ca 100644 --- a/addons/web/static/lib/qweb/qweb-test-call.xml +++ b/addons/web/static/lib/qweb/qweb-test-call.xml @@ -1,30 +1,51 @@ - ok - - - + ok + + - + + ok + - WHEEE + WHEEE + ok + - + + ok + - ok + ok + ok + - + - - + ok + + + + - + + 1 - 1 + + + + + + + + ok + diff --git a/addons/web/static/lib/qweb/qweb-test-conditionals.xml b/addons/web/static/lib/qweb/qweb-test-conditionals.xml index 43d16578bfc..1d038ee5908 100644 --- a/addons/web/static/lib/qweb/qweb-test-conditionals.xml +++ b/addons/web/static/lib/qweb/qweb-test-conditionals.xml @@ -1,59 +1,18 @@ - - ok - - - ok - - - fail + + ok + {"condition": true} + ok - - ok - - - ok - - - ok - - - ok - - - ok - - - ok - - - ok + + fail + {"condition": false} + - - ok - - - ok - - - ok - - - ok - - - - ok - - - ok - - - ok - - - ok + + fail + diff --git a/addons/web/static/lib/qweb/qweb-test-extend.xml b/addons/web/static/lib/qweb/qweb-test-extend.xml index 35181eb8ee9..007caefe383 100644 --- a/addons/web/static/lib/qweb/qweb-test-extend.xml +++ b/addons/web/static/lib/qweb/qweb-test-extend.xml @@ -1,32 +1,37 @@ - -
  • one
-
- -
  • 3
  • -
    - -
  • 2
  • -
    - -
  • 1
  • -
    -
    -
    - - - this.attr('class', 'main'); + + +
    • one
    -
    - -
    -
    - - [[end]] - + +
  • 3
  • +
    + +
  • 2
  • +
    + +
  • 1
  • +
    +
    +
    + + this.attr('class', 'main'); + + +
    +
    + + [[end]] + +
    • 1
    • 2
    • 3
    [[end]]
    +]]>
    - -
  • [[cloned template]]
  • -
    + +
  • [[cloned template]]
  • +
    +
  • one
  • [[cloned template]]
  • +]]>
    diff --git a/addons/web/static/lib/qweb/qweb-test-foreach.xml b/addons/web/static/lib/qweb/qweb-test-foreach.xml index 8e335614c39..9a1e8ecb7ba 100644 --- a/addons/web/static/lib/qweb/qweb-test-foreach.xml +++ b/addons/web/static/lib/qweb/qweb-test-foreach.xml @@ -1,17 +1,46 @@ - * - - + + +[: ] + + +[0: 3 3] +[1: 2 2] +[2: 1 1] + - - - - - - - + + +- first last () + + +- first (even) +- (odd) +- (even) +- (odd) +- last (even) + - - + + + +[: ] + + +[0: 0 0] +[1: 1 1] +[2: 2 2] + + + + + +[: - ] + + {"value": {"a": 1, "b": 2, "c": 3}} + +[0: a 1 - even] +[1: b 2 - odd] +[2: c 3 - even] + diff --git a/addons/web/static/lib/qweb/qweb-test-output.xml b/addons/web/static/lib/qweb/qweb-test-output.xml index 8725bb00a8f..f91196a86cf 100644 --- a/addons/web/static/lib/qweb/qweb-test-output.xml +++ b/addons/web/static/lib/qweb/qweb-test-output.xml @@ -3,44 +3,36 @@ + ok + - + + {"var": "ok"} + ok + - - - - - - - - - - - - - - + + "}]]> + + + ok + - + + {"var": "ok"} + ok + - - - - - - - - - - - + + "}]]> + ]]> diff --git a/addons/web/static/lib/qweb/qweb-test-set.xml b/addons/web/static/lib/qweb/qweb-test-set.xml index 6a7b4f53d23..945fd4a2a0d 100644 --- a/addons/web/static/lib/qweb/qweb-test-set.xml +++ b/addons/web/static/lib/qweb/qweb-test-set.xml @@ -1,15 +1,53 @@ - + + + + ok + + - ok + ok + + + ok + - + + + + {"value": "ok"} + + + ok + + - + + + + + + {"value": "ok"} + + + ok + + + + + + + + + + 2 + + + 1 diff --git a/addons/web/static/lib/qweb/qweb-test.js.html b/addons/web/static/lib/qweb/qweb-test.js.html index e6095a6259a..aac579d04fe 100644 --- a/addons/web/static/lib/qweb/qweb-test.js.html +++ b/addons/web/static/lib/qweb/qweb-test.js.html @@ -15,286 +15,52 @@ function render(template, context) { return trim(QWeb.render(template, context)).toLowerCase(); } + + /** + * Loads the template file, and executes all the test template in a + * qunit module $title + */ + function test(title, template) { + QUnit.module(title, { + setup: function () { + var self = this; + this.qweb = new QWeb2.Engine(); + QUnit.stop(); + this.qweb.add_template(template, function (_, doc) { + self.doc = doc; + QUnit.start(); + }) + } + }); + QUnit.test('autotest', function (assert) { + var templates = this.qweb.templates; + for (var template in templates) { + if (!templates.hasOwnProperty(template)) { continue; } + // ignore templates whose name starts with _, they're + // helpers/internal + if (/^_/.test(template)) { continue; } + + var params = this.doc.querySelector('params#' + template); + var args = params ? JSON.parse(params.textContent) : {}; + + var results = this.doc.querySelector('result#' + template); + assert.equal( + trim(this.qweb.render(template, args)), + trim(results.textContent), + template); + } + }); + } $(document).ready(function() { - QUnit.module("Basic output tests", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-output.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); + test("Output", 'qweb-test-output.xml'); + test("Context-setting", 'qweb-test-set.xml'); + test("Conditionals", 'qweb-test-conditionals.xml'); + test("Attributes manipulation", 'qweb-test-attributes.xml'); + test("Template calling (to the faraway pages)", + 'qweb-test-call.xml'); + test("Foreach", 'qweb-test-foreach.xml'); - QUnit.test("Basic escaped output", function (assert) { - assert.equal(render('esc-literal', {}), "ok", "Render a literal string"); - assert.equal(render('esc-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('esc-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); - }); - QUnit.test("Formatted escaped output", function (assert) { - assert.equal(render('escf-literal', {}), "ok", "Render a literal string"); - assert.equal(render('escf-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('escf-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); - assert.equal(render('escf-mix', {ok: 'ok'}), "[ok]", "Render a string with additions around the format"); - }); - QUnit.test("Basic unescaped output", function (assert) { - assert.equal(render('raw-literal', {}), "ok", "Render a literal string"); - assert.equal(render('raw-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('raw-notescaped', {ok: ''}), "", "Render a string with data not escaped"); - }); - QUnit.test("Formatted unescaped output", function (assert) { - assert.equal(render('rawf-literal', {}), "ok", "Render a literal string"); - assert.equal(render('rawf-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('rawf-notescaped', {ok: ''}), "", "Render a string with data not escaped"); - }); - - QUnit.module("Context-setting tests", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-set.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test("Set literal value", function (assert) { - assert.equal(render('set-from-attribute-literal', {}), "ok", - "Set a literal value via @t-value"); - assert.equal(render('set-from-body-literal', {}), "ok", - "Set a literal value via @t-set body"); - }); - QUnit.test("Set value looked up from context", function (assert) { - assert.equal(render('set-from-attribute-lookup', {value: 'ok'}), "ok", - "Set a value looked up in context via @t-value"); - assert.equal(render('set-from-body-lookup', {value: 'ok'}), 'ok', - "Set a value looked up in context via @t-set body and @t-esc"); - }); - - QUnit.module("Conditionals", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-conditionals.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test('Basic (single boolean) conditionals', function (assert) { - assert.equal(render('literal-conditional', {}), 'ok', - "Test on a literal value"); - assert.equal(render('boolean-value-conditional', {value: true}), 'ok', - "Test on a truthy variable value"); - assert.equal(render('boolean-value-conditional-false', {value: false}), '', - "Test on a falsy variable value"); - }); - QUnit.test('Boolean expressions in conditionals', function (assert) { - assert.equal(render('negify', {}), 'ok', - "Negative"); - assert.equal(render('equality', {}), 'ok', - "Equality"); - assert.equal(render('difference', {}), 'ok', - "Difference"); - assert.equal(render('and', {}), 'ok', - "Boolean and"); - assert.equal(render('and-js', {}), 'ok', - "Boolean and via manually escaped JS operator"); - assert.equal(render('or', {}), 'ok', - "Boolean or"); - assert.equal(render('or-js', {}), 'ok', - "Boolean or using JS operator"); - }); - QUnit.test('Comparison boolean tests in conditionals', function (assert) { - assert.equal(render('greater', {}), 'ok', - "Greater"); - assert.equal(render('greater-js', {}), 'ok', - "Greater, JS operator"); - assert.equal(render('lower', {}), 'ok', - "Lower"); - assert.equal(render('lower-js', {}), 'ok', - "Lower, JS operator"); - assert.equal(render('greater-or-equal', {}), 'ok', - "Greater or Equal"); - assert.equal(render('greater-or-equal-js', {}), 'ok', - "Greater or Equal, JS operator"); - assert.equal(render('lower-or-equal', {}), 'ok', - "Lower or Equal"); - assert.equal(render('lower-or-equal-js', {}), 'ok', - "Lower or Equal, JS operator"); - }); - - QUnit.module("Attributes manipulation", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-attributes.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test('Fixed-name attributes', function (assert) { - assert.equal(render('fixed-literal', {}), '
    ', - "Fixed name and literal attribute value"); - assert.equal(render('fixed-variable', {value: 'ok'}), '
    ', - "Fixed name and variable attribute value"); - }); - QUnit.test('Tuple-based attributes', function (assert) { - assert.equal(render('tuple-literal', {}), '
    ', - "Tuple-based literal attributes"); - assert.equal(render('tuple-variable', {att: ['foo', 'bar']}), '
    ', - "Tuple-based variable attributes"); - }); - QUnit.test('Fixed name, formatted value attributes', function (assert) { - assert.equal(render('format-literal', {}), '
    ', - "Literal format"); - assert.equal(render('format-value', {value:'a'}), '
    ', - "Valued format"); - assert.equal( - render('format-expression', {value: 5}), - '
    ', - "Format strings are evaluated expressions"); - assert.equal(render('format-multiple', { - value1: 0, - value2: 1, - value3: 2, - }), - '
    ', - "each format string should be evaluated independently"); - }); - QUnit.test('Fixed name, jinja-formatted', function (assert) { - assert.equal(render('format2-literal', {}), '
    ', - "Literal format"); - assert.equal(render('format2-value', {value:'a'}), '
    ', - "Valued format"); - assert.equal( - render('format2-expression', {value: 5}), - '
    ', - "Format strings are evaluated expressions"); - assert.equal(render('format2-multiple', { - value1: 0, - value2: 1, - value3: 2, - }), - '
    ', - "each format string should be evaluated independently"); - }); - - QUnit.module("Template calling (including)", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-call.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test('Trivial call invocation', function (assert) { - assert.equal(render('basic-caller', {}), 'ok', - "Direct call of a second template"); - }); - QUnit.test('Call invocation with body', function (assert) { - assert.equal(render('with-unused-body', {}), 'ok', - "Call of a second template with body unused"); - assert.equal(render('with-unused-setbody', {}), 'ok', - "Call of a second template with body directives unused"); - }); - QUnit.test('Call invocation with body (used by callee)', function (assert) { - assert.equal(render('with-used-body', {}), 'ok', - "Call of a second template with body used"); - }); - QUnit.test('Call invocation with parameters set (in body)', function (assert) { - assert.equal(render('with-used-setbody', {}), 'ok', - "Call of a second template with parameters"); - }); - QUnit.test('Call invocation in-context (via import)', function (assert) { - assert.equal(render('in-context-import', {}), 'ok', - "Call with t-import (calls in current context)"); - }); - - QUnit.module("Foreach", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-foreach.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - var seq = [4,3,2,1,0]; - QUnit.test('Basic foreach repetition', function (assert) { - assert.equal(QWeb.render('repetition-text-content', {seq:seq}), '*****', - "Repetition of text content via foreach"); - assert.equal(QWeb.render('repetition-dom-content', {seq:seq}).toLowerCase(), '', - "Repetition of node content via foreach"); - assert.equal(QWeb.render('repetition-self', {seq:seq}).toLowerCase(), '', - "A node with a foreach repeats itself"); - }); - QUnit.test("Foreach scope content", function (assert) { - assert.equal(QWeb.render('scope-self', {seq:seq}), '43210', - "each value of the sequence is available via the sequence name"); - assert.equal(QWeb.render('scope-value', {seq:seq}), '43210', - "each value of the sequence is also via the _value"); - assert.equal(QWeb.render('scope-index', {seq:seq}), '01234', - "the current 0-based index is available via _index"); - assert.equal(QWeb.render('scope-first', {seq:seq}), 'true false false false false ', - "_first says whether the current item is the first of the sequence"); - assert.equal(QWeb.render('scope-last', {seq:seq}), 'false false false false true ', - "_last says whether the current item is the last of the sequence"); - assert.equal(QWeb.render('scope-parity', {seq:seq}), 'even odd even odd even ', - "the parity (odd/even) of the current row is available via _parity"); - assert.equal(QWeb.render('scope-size', {seq:seq}), '5 5 5 5 5 ', - "the total length of the sequence is available through _size"); - }); - QUnit.test('Name aliasing via t-as', function (assert) { - assert.equal(QWeb.render('aliasing', {seq:seq}), '43210', - "the inner value can be re-bound via t-as"); - assert.equal(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even ', - "inner loop variables should be rebound as well"); - }); - - QUnit.module("Template inheritance tests", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-extend.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - - QUnit.test("jQuery extend", function (assert) { - assert.equal(render('jquery-extend', {}), '
    • 1
    • 2
    • 3
    [[end]]
    ', - "Extend template with jQuery"); - assert.equal(render('jquery-extend-clone', {}), '
    • one
    • [[cloned template]]
    ', - "Clone template"); - }); + test('Template inheritance', 'qweb-test-extend.xml'); }); diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index 08208fbe3a6..e080476cec9 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -28,7 +28,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. var QWeb2 = { expressions_cache: {}, RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','), - ACTIONS_PRECEDENCE: 'foreach,if,call,set,escf,esc,rawf,raw,js,debug,log'.split(','), + ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,raw,js,debug,log'.split(','), WORD_REPLACEMENT: { 'and': '&&', 'or': '||', @@ -139,22 +139,11 @@ var QWeb2 = { if (callback) { new_dict['__content__'] = callback(context, new_dict); } - var r = context.engine._render(template, new_dict); - if (_import) { - if (_import === '*') { - this.extend(old_dict, new_dict, ['__caller__', '__template__']); - } else { - _import = _import.split(','); - for (var i = 0, ilen = _import.length; i < ilen; i++) { - var v = _import[i]; - old_dict[v] = new_dict[v]; - } - } - } - return r; + return context.engine._render(template, new_dict); }, foreach: function(context, enu, as, old_dict, callback) { if (enu != null) { + var index, jlen, cur; var size, new_dict = this.extend({}, old_dict); new_dict[as + "_all"] = enu; var as_value = as + "_value", @@ -164,13 +153,13 @@ var QWeb2 = { as_parity = as + "_parity"; if (size = enu.length) { new_dict[as + "_size"] = size; - for (var j = 0, jlen = enu.length; j < jlen; j++) { - var cur = enu[j]; + for (index = 0, jlen = enu.length; index < jlen; index++) { + cur = enu[index]; new_dict[as_value] = cur; - new_dict[as_index] = j; - new_dict[as_first] = j === 0; - new_dict[as_last] = j + 1 === size; - new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even'); + new_dict[as_index] = index; + new_dict[as_first] = index === 0; + new_dict[as_last] = index + 1 === size; + new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even'); if (cur.constructor === Object) { this.extend(new_dict, cur); } @@ -184,14 +173,14 @@ var QWeb2 = { } this.foreach(context, _enu, as, old_dict, callback); } else { - var index = 0; + index = 0; for (var k in enu) { if (enu.hasOwnProperty(k)) { - var v = enu[k]; - new_dict[as_value] = v; + cur = enu[k]; + new_dict[as_value] = cur; new_dict[as_index] = index; new_dict[as_first] = index === 0; - new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even'); + new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even'); new_dict[as] = k; callback(context, new_dict); index += 1; @@ -704,11 +693,10 @@ QWeb2.Element = (function() { this.indent(); }, compile_action_call : function(value) { - var _import = this.actions['import'] || ''; if (this.children.length === 0) { - return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + "));"); + return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict));"); } else { - this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + ", function(context, dict) {"); + this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, null, function(context, dict) {"); this.bottom("}));"); this.indent(); this.top("var r = [];"); @@ -743,17 +731,9 @@ QWeb2.Element = (function() { + this.format_expression(value) + "));"); }, - compile_action_escf : function (value) { - this.top("r.push(context.engine.tools.html_escape(" - + this.string_interpolation(value) - + '));'); - }, compile_action_raw : function(value) { this.top("r.push(" + (this.format_expression(value)) + ");"); }, - compile_action_rawf: function (value) { - this.top('r.push(' + this.string_interpolation(value) + ');'); - }, compile_action_js : function(value) { this.top("(function(" + value + ") {"); this.bottom("})(dict);"); diff --git a/doc/glossary.rst b/doc/glossary.rst index af197a2d28a..d22042b1bf7 100644 --- a/doc/glossary.rst +++ b/doc/glossary.rst @@ -12,3 +12,12 @@ External identifiers are in the form :samp:`{module}.{id}` (e.g. ``account.invoice_graph``). From within a module, the :samp:`{module}.` prefix can be left out. + + format string + inspired by `jinja variables`_, format strings allow more easily + mixing literal content and computed content (expressions): content + between ``{{`` and ``}}`` is interpreted as an expression and + evaluated, other content is interpreted as literal strings and + displayed as-is + +.. _jinja variables: http://jinja.pocoo.org/docs/dev/templates/#variables diff --git a/doc/index.rst b/doc/index.rst index 5440162574a..f83680e9ebd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,3 +15,5 @@ odoo developer documentation guides reference modules + +.. todolist:: diff --git a/doc/reference/http.rst b/doc/reference/http.rst index c179691c4cb..e36ddf9e713 100644 --- a/doc/reference/http.rst +++ b/doc/reference/http.rst @@ -59,7 +59,7 @@ and defining methods decorated with :func:`~openerp.http.route`:: return stuff() To *override* a controller, :ref:`inherit ` 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/javascript.rst b/doc/reference/javascript.rst index ae7b02c63a4..07f38b77fc1 100644 --- a/doc/reference/javascript.rst +++ b/doc/reference/javascript.rst @@ -5,6 +5,9 @@ Javascript Widgets ======= +.. qweb integration: ``template`` is an (optional) automatically rendered + template + RPC === diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst index c993c0c3c22..8e8a213af44 100644 --- a/doc/reference/qweb.rst +++ b/doc/reference/qweb.rst @@ -1,26 +1,273 @@ +.. 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 ```` is also available, +which executes its directive but doesn't generate any output in and of +itself:: + + +

    Test

    +
    + +will result in:: + +

    Test

    + +if ``condition`` is true, but:: + +
    +

    Test

    +
    + +will result in:: + +
    +

    Test

    +
    .. _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:: + +

    + +rendered with the value ``value`` set to ``42`` yields:: + +

    42

    + +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:: + +
    + +

    ok

    +
    +
    + +The element is rendered if the condition is true:: + +
    +

    ok

    +
    + +but if the condition is false it is removed from the result:: + +
    +
    + +The conditional rendering applies to the bearer of the directive, which does +not have to be ````:: + +
    +

    ok

    +
    + +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:: + + +

    +
    + +will be rendered as:: + +

    1

    +

    2

    +

    3

    + +Like conditions, ``foreach`` applies to the element bearing the directive's +attribute, and + +:: + +

    + +

    + +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:: + +
    + + will be rendered as:: + +
    +: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):: + + +
  • +
    + + will be rendered as:: + +
  • 1
  • +
  • 2
  • +
  • 3
  • +:samp:`t-att=mapping` + if the parameter is a mapping, each (key, value) pair generates a new + attribute and its value:: + +
    + + will be rendered as:: + +
    +: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:: + +
    + + will be rendered as:: + +
    + +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:: + + + + + will print ``3`` +* if there is no ``t-value`` attribute, the node's body is rendered and set + as the variable's value:: + + +
  • ok
  • +
    + + + 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:: + + + +This calls the named template with the execution context of the parent, if +``other_template`` is defined as:: + +

    + +the call above will be rendered as ``

    `` (no content), but:: + + + + +will be rendered as ``

    1

    ``. + +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:: + + + + + Python ====== @@ -33,5 +280,196 @@ Bundles Javascript ========== -loading +Exclusive directives +-------------------- + +The Javascript qweb implementation provides specific directives to handle +defining and overloading/altering templates: + +defining templates +'''''''''''''''''' + +The ``t-name`` directive can only be placed at the top-level of a template +file (direct children to the document root):: + + + + + + + +It takes no other parameter, but can be used with a ```` element or any +other. With a ```` element, the ```` 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:: + + + +
  • new element
  • +
    +
    + +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 ` 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 :ref:`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/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 61e184087a3..540832d1931 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -894,7 +894,7 @@ class view(osv.osv): e.set('data-oe-xpath', node_path) if not e.get('data-oe-model'): return - if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib): + if {'t-esc', 't-raw'}.intersection(e.attrib): # nodes which fully generate their content and have no reason to # be branded because they can not sensibly be edited self._pop_view_branding(e)