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 91317d5194b..bf6070fed08 100644 --- a/addons/web/static/lib/qweb/qweb-test-attributes.xml +++ b/addons/web/static/lib/qweb/qweb-test-attributes.xml @@ -2,21 +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..783172c73bd 100644 --- a/addons/web/static/lib/qweb/qweb-test-call.xml +++ b/addons/web/static/lib/qweb/qweb-test-call.xml @@ -1,30 +1,58 @@ - ok - - - + ok + + - + + ok + - WHEEE + WHEEE + ok + - + + ok + - ok + ok + ok + - + - - + ok + + + + + + + 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 1135ad9f251..f91196a86cf 100644 --- a/addons/web/static/lib/qweb/qweb-test-output.xml +++ b/addons/web/static/lib/qweb/qweb-test-output.xml @@ -3,21 +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 469641a1796..aac579d04fe 100644 --- a/addons/web/static/lib/qweb/qweb-test.js.html +++ b/addons/web/static/lib/qweb/qweb-test.js.html @@ -1,9 +1,9 @@ - - - + + + @@ -15,238 +15,58 @@ 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() { - module("Basic output tests", { - setup: function () { - QWeb.add_template('qweb-test-output.xml'); - }, - 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'); - test("Basic escaped output", function () { - equals(render('esc-literal', {}), "ok", "Render a literal string"); - equals(render('esc-variable', {ok: 'ok'}), "ok", "Render a string variable"); - equals(render('esc-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); - }); - test("Basic unescaped output", function () { - equals(render('raw-literal', {}), "ok", "Render a literal string"); - equals(render('raw-variable', {ok: 'ok'}), "ok", "Render a string variable"); - equals(render('raw-notescaped', {ok: ''}), "", "Render a string with data not escaped"); - }); - - module("Context-setting tests", { - setup: function () { - QWeb.add_template('qweb-test-set.xml'); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - test("Set literal value", function () { - equals(render('set-from-attribute-literal', {}), "ok", - "Set a literal value via @t-value"); - equals(render('set-from-body-literal', {}), "ok", - "Set a literal value via @t-set body"); - }); - test("Set value looked up from context", function () { - equals(render('set-from-attribute-lookup', {value: 'ok'}), "ok", - "Set a value looked up in context via @t-value"); - equals(render('set-from-body-lookup', {value: 'ok'}), 'ok', - "Set a value looked up in context via @t-set body and @t-esc"); - }); - - module("Conditionals", { - setup: function () { - QWeb.add_template('qweb-test-conditionals.xml'); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - test('Basic (single boolean) conditionals', function () { - equals(render('literal-conditional', {}), 'ok', - "Test on a literal value"); - equals(render('boolean-value-conditional', {value: true}), 'ok', - "Test on a truthy variable value"); - equals(render('boolean-value-conditional-false', {value: false}), '', - "Test on a falsy variable value"); - }); - test('Boolean expressions in conditionals', function () { - equals(render('negify', {}), 'ok', - "Negative"); - equals(render('equality', {}), 'ok', - "Equality"); - equals(render('difference', {}), 'ok', - "Difference"); - equals(render('and', {}), 'ok', - "Boolean and"); - equals(render('and-js', {}), 'ok', - "Boolean and via manually escaped JS operator"); - equals(render('or', {}), 'ok', - "Boolean or"); - equals(render('or-js', {}), 'ok', - "Boolean or using JS operator"); - }); - test('Comparison boolean tests in conditionals', function () { - equals(render('greater', {}), 'ok', - "Greater"); - equals(render('greater-js', {}), 'ok', - "Greater, JS operator"); - equals(render('lower', {}), 'ok', - "Lower"); - equals(render('lower-js', {}), 'ok', - "Lower, JS operator"); - equals(render('greater-or-equal', {}), 'ok', - "Greater or Equal"); - equals(render('greater-or-equal-js', {}), 'ok', - "Greater or Equal, JS operator"); - equals(render('lower-or-equal', {}), 'ok', - "Lower or Equal"); - equals(render('lower-or-equal-js', {}), 'ok', - "Lower or Equal, JS operator"); - }); - - module("Attributes manipulation", { - setup: function () { - QWeb.add_template('qweb-test-attributes.xml'); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - test('Fixed-name attributes', function () { - equals(render('fixed-literal', {}), '
    ', - "Fixed name and literal attribute value"); - equals(render('fixed-variable', {value: 'ok'}), '
    ', - "Fixed name and variable attribute value"); - }); - test('Tuple-based attributes', function () { - equals(render('tuple-literal', {}), '
    ', - "Tuple-based literal attributes"); - equals(render('tuple-variable', {att: ['foo', 'bar']}), '
    ', - "Tuple-based variable attributes"); - }); - test('Fixed name, formatted value attributes', function () { - equals(render('format-literal', {}), '
    ', - "Literal format"); - equals(render('format-value', {value:'a'}), '
    ', - "Valued format"); - }); - - module("Template calling (including)", { - setup: function () { - QWeb.add_template('qweb-test-call.xml'); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - test('Trivial call invocation', function () { - equals(render('basic-caller', {}), 'ok', - "Direct call of a second template"); - }); - test('Call invocation with body', function () { - equals(render('with-unused-body', {}), 'ok', - "Call of a second template with body unused"); - equals(render('with-unused-setbody', {}), 'ok', - "Call of a second template with body directives unused"); - }); - test('Call invocation with body (used by callee)', function () { - equals(render('with-used-body', {}), 'ok', - "Call of a second template with body used"); - }); - test('Call invocation with parameters set (in body)', function () { - equals(render('with-used-setbody', {}), 'ok', - "Call of a second template with parameters"); - }); - test('Call invocation in-context (via import)', function () { - equals(render('in-context-import', {}), 'ok', - "Call with t-import (calls in current context)"); - }); - - module("Foreach", { - setup: function () { - QWeb.add_template('qweb-test-foreach.xml'); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - var seq = [4,3,2,1,0]; - test('Basic foreach repetition', function () { - equals(QWeb.render('repetition-text-content', {seq:seq}), '*****', - "Repetition of text content via foreach"); - equals(QWeb.render('repetition-dom-content', {seq:seq}).toLowerCase(), '', - "Repetition of node content via foreach"); - equals(QWeb.render('repetition-self', {seq:seq}).toLowerCase(), '', - "A node with a foreach repeats itself"); - }); - test("Foreach scope content", function () { - equals(QWeb.render('scope-self', {seq:seq}), '43210', - "each value of the sequence is available via the sequence name"); - equals(QWeb.render('scope-value', {seq:seq}), '43210', - "each value of the sequence is also via the _value"); - equals(QWeb.render('scope-index', {seq:seq}), '01234', - "the current 0-based index is available via _index"); - equals(QWeb.render('scope-first', {seq:seq}), 'true false false false false ', - "_first says whether the current item is the first of the sequence"); - equals(QWeb.render('scope-last', {seq:seq}), 'false false false false true ', - "_last says whether the current item is the last of the sequence"); - equals(QWeb.render('scope-parity', {seq:seq}), 'even odd even odd even ', - "the parity (odd/even) of the current row is available via _parity"); - equals(QWeb.render('scope-size', {seq:seq}), '5 5 5 5 5 ', - "the total length of the sequence is available through _size"); - }); - test('Name aliasing via t-as', function () { - equals(QWeb.render('aliasing', {seq:seq}), '43210', - "the inner value can be re-bound via t-as"); - equals(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even ', - "inner loop variables should be rebound as well"); - }); - - module("Template inheritance tests", { - setup: function () { - QWeb.add_template('qweb-test-extend.xml'); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - - test("jQuery extend", function () { - equals(render('jquery-extend', {}), '
    • 1
    • 2
    • 3
    [[end]]
    ', - "Extend template with jQuery"); - equals(render('jquery-extend-clone', {}), '
    • one
    • [[cloned template]]
    ', - "Clone template"); - }); + test('Template inheritance', 'qweb-test-extend.xml'); }); -

    QWeb test suite

    - -

    - -
    -

    -
      -
      test markup, will be hidden
      +
      +
      diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index 78e4fd8b544..c845abd5965 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -26,7 +26,12 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // TODO: templates orverwritten could be called by t-call="__super__" ? // TODO: t-set + t-value + children node == scoped variable ? var QWeb2 = { - expressions_cache: {}, + expressions_cache: { + // special case for template bodies, __content__ doesn't work in + // Python impl because safe_eval -> assert_no_dunder_name so use + // Python impl's magical 0 variable instead + '0': 'dict[0]', + }, 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,esc,raw,js,debug,log'.split(','), WORD_REPLACEMENT: { @@ -137,24 +142,13 @@ var QWeb2 = { var new_dict = this.extend({}, old_dict); new_dict['__caller__'] = old_dict['__template__']; if (callback) { - new_dict['__content__'] = callback(context, new_dict); + new_dict[0] = 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 +158,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 +178,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; @@ -248,7 +242,6 @@ QWeb2.Engine = (function() { } self.add_template(xDoc, callback); }); - template = this.load_xml(template, callback); } var ec = (template.documentElement && template.documentElement.childNodes) || template.childNodes || []; for (var i = 0; i < ec.length; i++) { @@ -563,7 +556,7 @@ QWeb2.Element = (function() { format_expression : function(e) { /* Naive format expression builder. Replace reserved words and variables to dict[variable] * Does not handle spaces before dot yet, and causes problems for anonymous functions. Use t-js="" for that */ - if (QWeb2.expressions_cache[e]) { + if (QWeb2.expressions_cache[e]) { return QWeb2.expressions_cache[e]; } var chars = e.split(''), @@ -602,24 +595,26 @@ QWeb2.Element = (function() { return r; }, string_interpolation : function(s) { + var _this = this; if (!s) { return "''"; } - var regex = /^{(.*)}(.*)/, - src = s.split(/#/), - r = []; - for (var i = 0, ilen = src.length; i < ilen; i++) { - var val = src[i], - m = val.match(regex); - if (m) { - r.push("(" + this.format_expression(m[1]) + ")"); - if (m[2]) { - r.push(this.engine.tools.js_escape(m[2])); - } - } else if (!(i === 0 && val === '')) { - r.push(this.engine.tools.js_escape((i === 0 ? '' : '#') + val)); - } + function append_literal(s) { + s && r.push(_this.engine.tools.js_escape(s)); } + + var re = /(?:#{(.+?)}|{{(.+?)}})/g, start = 0, r = [], m; + while (m = re.exec(s)) { + // extract literal string between previous and current match + append_literal(s.slice(start, re.lastIndex - m[0].length)); + // extract matched expression + r.push('(' + this.format_expression(m[2] || m[1]) + ')'); + // update position of new matching + start = re.lastIndex; + } + // remaining text after last expression + append_literal(s.slice(start)); + return r.join(' + '); }, indent : function() { @@ -703,11 +698,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 = [];"); @@ -738,7 +732,9 @@ QWeb2.Element = (function() { } }, compile_action_esc : function(value) { - this.top("r.push(context.engine.tools.html_escape(" + (this.format_expression(value)) + "));"); + this.top("r.push(context.engine.tools.html_escape(" + + this.format_expression(value) + + "));"); }, compile_action_raw : function(value) { this.top("r.push(" + (this.format_expression(value)) + ");"); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 32ef66d697b..3eb4638ac53 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -577,7 +577,7 @@
      - +
        + +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:: + +
        + This template was called with content: + +
        + +being called thus:: + + + content + + +will result in:: + +
        + This template was called with content: + content +
        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 `) 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 ` + 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 `_ + 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):: + + + + + + + +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 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_qweb.py b/openerp/addons/base/ir/ir_qweb.py index b82fe64099a..9c90cf43bfa 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -80,6 +80,9 @@ class QWebContext(dict): return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True) def copy(self): + """ Clones the current context, conserving all data and metadata + (loader, template cache, ...) + """ return QWebContext(self.cr, self.uid, dict.copy(self), loader=self.loader, templates=self.templates, @@ -89,28 +92,12 @@ class QWebContext(dict): return self.copy() class QWeb(orm.AbstractModel): - """QWeb Xml templating engine + """ Base QWeb rendering engine - The templating engine use a very simple syntax based "magic" xml - attributes, to produce textual output (even non-xml). - - The core magic attributes are: - - flow attributes: - t-if t-foreach t-call - - output attributes: - t-att t-raw t-esc t-trim - - assignation attribute: - t-set - - QWeb can be extended like any OpenERP model and new attributes can be - added. - - If you need to customize t-fields rendering, subclass the ir.qweb.field - model (and its sub-models) then override :meth:`~.get_converter_for` to - fetch the right field converters for your qweb model. + * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and + create new models called :samp:`ir.qweb.field.{widget}` + * alternatively, override :meth:`~.get_converter_for` and return an + arbitrary model to use as field converter Beware that if you need extensions or alterations which could be incompatible with other subsystems, you should create a local object @@ -162,13 +149,17 @@ class QWeb(orm.AbstractModel): def load_document(self, document, res_id, qwebcontext): """ Loads an XML document and installs any contained template in the engine + + :type document: a parsed lxml.etree element, an unparsed XML document + (as a string) or the path of an XML file to load """ - if hasattr(document, 'documentElement'): + if not isinstance(document, basestring): + # assume lxml.etree.Element dom = document elif document.startswith("