[IMP] qweb doc, tests

* document qweb based (mostly) on JS version
* convert JS qweb tests to (mostly) language-independent XML so they can be
  used for JS and Python implementations
* add some more tests (e.g. precedence between t-value and body in t-set)
* remove ``t-import``
* fix parity in foreach(dict) (and rename some variables to make array and
  object versions more similar)
This commit is contained in:
Xavier Morel 2014-09-09 09:17:32 +02:00
parent bed6b01c53
commit 494dcbd0e3
17 changed files with 730 additions and 1001 deletions

View File

@ -37,7 +37,6 @@ Javascript
widget
rpc
async
qweb
client_action
testing

View File

@ -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
<http://en.wikipedia.org/wiki/Genshi_(templating_language)>`_,
`Thymeleaf <http://en.wikipedia.org/wiki/Thymeleaf>`_ or `Facelets
<http://en.wikipedia.org/wiki/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
<templates>
<div t-name="example_template" t-attf-class="base #{cls}">
<h4 t-if="title"><t t-esc="title"/></h4>
<ul>
<li t-foreach="items" t-as="item" t-att-class="item_parity">
<t t-call="example_template.sub">
<t t-set="arg" t-value="item_value"/>
</t>
</li>
</ul>
</div>
<t t-name="example_template.sub">
<t t-esc="arg.name"/>
<dl>
<t t-foreach="arg.tags" t-as="tag" t-if="tag_index lt 5">
<dt><t t-esc="tag"/></dt>
<dd><t t-esc="tag_value"/></dd>
</t>
</dl>
</t>
</templates>
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
<div class="base foo">
<h4>Random Title</h4>
<ul>
<li class="even">
foo
<dl>
<dt>bar</dt>
<dd>baz</dd>
<dt>qux</dt>
<dd>quux</dd>
</dl>
</li>
<li class="odd">
Lorem
<dl>
<dt>ipsum</dt>
<dd>dolor</dd>
<dt>sit</dt>
<dd>amet</dd>
<dt>consectetur</dt>
<dd>adipiscing</dd>
<dt>elit</dt>
<dd>Sed</dd>
<dt>hendrerit</dt>
<dd>ullamcorper</dd>
</dl>
</li>
</ul>
</div>
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 <qweb-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
<qweb-directives-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
<t>Something something</t>
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('<templates>\
<t t-name="test1"><t>Test 1</t></t>\
<t t-name="test2"><span>Test 2</span></t>\
</templates>');
e.render('test1'); // Test 1
e.render('test2'); // <span>Test 2</span>
.. 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 ``<templates>`` for simplicity.
.. code-block:: xml
<templates>
<t t-name="template1">
<!-- template code -->
</t>
</templates>
:ref:`t-name <qweb-directive-name>` can be used on an element with
an output as well:
.. code-block:: xml
<templates>
<div t-name="template2">
<!-- template code -->
</div>
</templates>
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 <qweb-directive-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
<span t-att="{foo: 3, bar: 42}"/>
will yield
.. code-block:: html
<span foo="3" bar="42"/>
.. 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-* <qweb-directive-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 <qweb-directive-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
<http://zvon.org/xxl/XSLTreference/OutputOverview/xslt_with-param_frame.html>`_.
.. _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 <qweb-directive-name>` will create a new template
using the old one as a base.
``BODY`` should be a sequence of :ref:`t-jquery
<qweb-directive-jquery>` alteration directives.
.. note::
The inheritance in the second form is *static*: the parent
template is copied and transformed when :ref:`t-extend
<qweb-directive-extend>` is called. If it is altered later (by
a :ref:`t-extend <qweb-directive-extend>` without a
:ref:`t-name <qweb-directive-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)
<http://api.jquery.com/replaceWith/>`_, ``inner`` maps to
`html(htmlString) <http://api.jquery.com/html/>`_.
* 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``.

View File

@ -2,40 +2,57 @@
<t t-name="fixed-literal">
<div t-att-foo="'bar'"/>
</t>
<result id="fixed-literal"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="fixed-variable">
<div t-att-foo="value"/>
</t>
<params id="fixed-variable">{"value": "ok"}</params>
<result id="fixed-variable"><![CDATA[<div foo="ok"></div>]]></result>
<t t-name="tuple-literal">
<div t-att="['foo', 'bar']"/>
</t>
<result id="tuple-literal"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="tuple-variable">
<div t-att="att"/>
<div t-att="value"/>
</t>
<params id="tuple-variable">{"value": ["foo", "bar"]}</params>
<result id="tuple-variable"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="object">
<div t-att="value"/>
</t>
<params id="object">{"value": {"a": 1, "b": 2, "c": 3}}</params>
<result id="object"><![CDATA[<div a="1" b="2" c="3"></div>]]></result>
<t t-name="format-literal">
<div t-attf-foo="bar"/>
</t>
<t t-name="format-value">
<div t-attf-foo="b#{value}r"/>
</t>
<t t-name="format-expression">
<div t-attf-foo="#{value + 37}"/>
</t>
<t t-name="format-multiple">
<div t-attf-foo="a #{value1} is #{value2} of #{value3} ]"/>
</t>
<result id="format-literal"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="format2-literal">
<div t-attf-foo="bar"/>
</t>
<t t-name="format2-value">
<t t-name="format-value">
<div t-attf-foo="b{{value}}r"/>
</t>
<t t-name="format2-expression">
<params id="format-value">{"value": "a"}</params>
<result id="format-value"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="format-expression">
<div t-attf-foo="{{value + 37}}"/>
</t>
<t t-name="format2-multiple">
<params id="format-expression">{"value": 5}</params>
<result id="format-expression"><![CDATA[<div foo="42"></div>]]></result>
<t t-name="format-multiple">
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
</t>
<params id="format-multiple">{
"value1": 0,
"value2": 1,
"value3": 2
}</params>
<result id="format-multiple"><![CDATA[
<div foo="a 0 is 1 of 2 ]"></div>
]]></result>
</templates>

View File

@ -1,30 +1,51 @@
<templates>
<t t-name="basic-callee">ok</t>
<t t-name="callee-printsbody"><t t-esc="__content__"/></t>
<t t-name="callee-uses-foo"><t t-esc="foo"/></t>
<t t-name="callee-sets-foo"><t t-set="foo" t-value="'ok'"/></t>
<t t-name="_basic-callee">ok</t>
<t t-name="_callee-printsbody"><t t-esc="__content__"/></t>
<t t-name="_callee-uses-foo"><t t-esc="foo"/></t>
<t t-name="basic-caller">
<t t-call="basic-callee"/>
<t t-call="_basic-callee"/>
</t>
<result id="basic-caller">ok</result>
<t t-name="with-unused-body">
<t t-call="basic-callee">WHEEE</t>
<t t-call="_basic-callee">WHEEE</t>
</t>
<result id="with-unused-body">ok</result>
<t t-name="with-unused-setbody">
<t t-call="basic-callee">
<t t-call="_basic-callee">
<t t-set="qux" t-value="3"/>
</t>
</t>
<result id="with-unused-setbody">ok</result>
<t t-name="with-used-body">
<t t-call="callee-printsbody">ok</t>
<t t-call="_callee-printsbody">ok</t>
</t>
<result id="with-used-body">ok</result>
<t t-name="with-used-setbody">
<t t-call="callee-uses-foo">
<t t-call="_callee-uses-foo">
<t t-set="foo" t-value="'ok'"/>
</t>
</t>
<t t-name="in-context-import">
<t t-call="callee-sets-foo" t-import='*'/>
<result id="with-used-setbody">ok</result>
<t t-name="inherit-context">
<t t-set="foo" t-value="1"/>
<t t-call="_callee-uses-foo"/> - <t t-esc="foo"/>
</t>
<result id="inherit-context">1 - 1</result>
<t t-name="scoped-parameter">
<t t-call="_basic-callee">
<t t-set="foo" t-value="42"/>
</t>
<!-- should not print anything -->
<t t-esc="foo"/>
</t>
<result id="scoped-parameter">
ok
</result>
</templates>

View File

@ -1,59 +1,18 @@
<templates>
<t t-name="literal-conditional">
<t t-if="true">ok</t>
</t>
<t t-name="boolean-value-conditional">
<t t-if="value">ok</t>
</t>
<t t-name="boolean-value-conditional-false">
<t t-if="value">fail</t>
<t t-name="boolean-value-condition">
<t t-if="condition">ok</t>
</t>
<params id="boolean-value-condition">{"condition": true}</params>
<result id="boolean-value-condition">ok</result>
<t t-name="negify">
<t t-if="!false">ok</t>
</t>
<t t-name="equality">
<t t-if="1 == 1">ok</t>
</t>
<t t-name="difference">
<t t-if="1 != 2">ok</t>
</t>
<t t-name="and">
<t t-if="true and true">ok</t>
</t>
<t t-name="and-js">
<t t-if="true &amp;&amp; true">ok</t>
</t>
<t t-name="or">
<t t-if="false or true">ok</t>
</t>
<t t-name="or-js">
<t t-if="false || true">ok</t>
<t t-name="boolean-value-condition-false">
<t t-if="condition">fail</t>
</t>
<params id="boolean-value-condition-false">{"condition": false}</params>
<result id="boolean-value-condition-false"/>
<t t-name="greater">
<t t-if="2 gt 1">ok</t>
</t>
<t t-name="greater-js">
<t t-if="2 &gt; 1">ok</t>
</t>
<t t-name="lower">
<t t-if="1 lt 2">ok</t>
</t>
<t t-name="lower-js">
<t t-if="1 &lt; 2">ok</t>
</t>
<t t-name="greater-or-equal">
<t t-if="2 gte 1">o</t><t t-if="2 gte 2">k</t>
</t>
<t t-name="greater-or-equal-js">
<t t-if="2 &gt;= 1">o</t><t t-if="2 &gt;= 2">k</t>
</t>
<t t-name="lower-or-equal">
<t t-if="1 lte 2">o</t><t t-if="2 lte 2">k</t>
</t>
<t t-name="lower-or-equal-js">
<t t-if="1 &lt;= 2">o</t><t t-if="2 &lt;= 2">k</t>
<t t-name="boolean-value-condition-missing">
<t t-if="condition">fail</t>
</t>
<result id="boolean-value-condition-missing"/>
</templates>

View File

@ -1,32 +1,37 @@
<templates>
<t t-name="jquery-extend">
<ul><li>one</li></ul>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>3</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul li:first-child" t-operation="replace"><li>2</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="prepend"><li>1</li></t>
<t t-jquery="ul" t-operation="before"><hr/></t>
<t t-jquery="ul" t-operation="after"><hr/></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul">
this.attr('class', 'main');
<!-- js-only -->
<t t-name="jquery-extend">
<ul><li>one</li></ul>
</t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="hr:eq(1)" t-operation="replace"><footer></footer></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="footer" t-operation="inner"><b>[[end]]</b></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>3</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul li:first-child" t-operation="replace"><li>2</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="prepend"><li>1</li></t>
<t t-jquery="ul" t-operation="before"><hr/></t>
<t t-jquery="ul" t-operation="after"><hr/></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul">this.attr('class', 'main');</t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="hr:eq(1)" t-operation="replace"><footer></footer></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="footer" t-operation="inner"><b>[[end]]</b></t>
</t>
<result id="jquery-extend"><![CDATA[
<hr/><ul class="main"><li>1</li><li>2</li><li>3</li></ul><footer><b>[[end]]</b></footer>
]]></result>
<t t-name="jquery-extend-clone" t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>[[cloned template]]</li></t>
</t>
<t t-name="jquery-extend-clone" t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>[[cloned template]]</li></t>
</t>
<result id="jquery-extend-clone"><![CDATA[
<ul><li>one</li><li>[[cloned template]]</li></ul>
]]></result>
</templates>

View File

@ -1,17 +1,46 @@
<templates xml:space="preserve">
<t t-name="repetition-text-content" t-foreach="seq">*</t>
<t t-name="repetition-dom-content" t-foreach="seq"><b/></t>
<b t-name="repetition-self" t-foreach="seq"/>
<t t-name="iter-items">
<t t-foreach="[3, 2, 1]" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
</t>
<result id="iter-items">
[0: 3 3]
[1: 2 2]
[2: 1 1]
</result>
<t t-name="scope-self" t-foreach="seq" t-esc="seq"/>
<t t-name="scope-value" t-foreach="seq" t-esc="seq_value"/>
<t t-name="scope-index" t-foreach="seq" t-esc="seq_index"/>
<t t-name="scope-first" t-foreach="seq"><t t-esc="seq_first"/> </t>
<t t-name="scope-last" t-foreach="seq"><t t-esc="seq_last"/> </t>
<t t-name="scope-parity" t-foreach="seq"><t t-esc="seq_parity"/> </t>
<t t-name="scope-size" t-foreach="seq"><t t-esc="seq_size"/> </t>
<t t-name="iter-position">
<t t-foreach="5" t-as="item">
-<t t-if="item_first"> first</t><t t-if="item_last"> last</t> (<t t-esc="item_parity"/>)</t>
</t>
<result id="iter-position">
- first (even)
- (odd)
- (even)
- (odd)
- last (even)
</result>
<t t-name="aliasing" t-foreach="seq" t-as="item" t-esc="item"/>
<t t-name="loopvars-aliasing"
t-foreach="seq" t-as="item"><t t-esc="item_parity"/> </t>
<!-- test integer param -->
<t t-name="iter-int">
<t t-foreach="3" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
</t>
<result id="iter-int">
[0: 0 0]
[1: 1 1]
[2: 2 2]
</result>
<!-- test dict param -->
<t t-name="iter-dict">
<t t-foreach="value" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]</t>
</t>
<params id="iter-dict">{"value": {"a": 1, "b": 2, "c": 3}}</params>
<result id="iter-dict">
[0: a 1 - even]
[1: b 2 - odd]
[2: c 3 - even]
</result>
</templates>

View File

@ -3,44 +3,36 @@
<t t-name="esc-literal">
<t t-esc="'ok'"/>
</t>
<result id="esc-literal">ok</result>
<t t-name="esc-variable">
<t t-esc="ok"/>
<t t-esc="var"/>
</t>
<params id="esc-variable">{"var": "ok"}</params>
<result id="esc-variable">ok</result>
<t t-name="esc-toescape">
<t t-esc="ok"/>
</t>
<!-- formatted esc -->
<t t-name="escf-literal">
<t t-escf="ok"/>
</t>
<t t-name="escf-variable">
<t t-escf="{{ ok }}"/>
</t>
<t t-name="escf-toescape">
<t t-escf="{{ ok }}"/>
</t>
<t t-name="escf-mix">
<t t-escf="[{{ ok }}]"/>
<t t-esc="var"/>
</t>
<params id="esc-toescape"><![CDATA[{"var": "<ok>"}]]></params>
<result id="esc-toescape"><![CDATA[&lt;ok&gt;]]></result>
<!-- raw, evaluates and returns @t-raw directly (no escaping) -->
<t t-name="raw-literal">
<t t-raw="'ok'"/>
</t>
<result id="raw-literal">ok</result>
<t t-name="raw-variable">
<t t-raw="ok"/>
<t t-raw="var"/>
</t>
<params id="raw-variable">{"var": "ok"}</params>
<result id="raw-variable">ok</result>
<t t-name="raw-notescaped">
<t t-raw="ok"/>
</t>
<!-- formatted raw -->
<t t-name="rawf-literal">
<t t-rawf="ok"/>
</t>
<t t-name="rawf-variable">
<t t-rawf="{{ ok }}"/>
</t>
<t t-name="rawf-notescaped">
<t t-rawf="{{ ok }}"/>
<t t-raw="var"/>
</t>
<params id="raw-notescaped"><![CDATA[{"var": "<ok>"}]]></params>
<result id="raw-notescaped"><![CDATA[<ok>]]></result>
</templates>

View File

@ -1,15 +1,53 @@
<templates>
<t t-name="set-from-attribute-literal">
<t t-set="value" t-value="'ok'"/><t t-esc="value"/>
<t t-set="value" t-value="'ok'"/>
<t t-esc="value"/>
</t>
<result id="set-from-attribute-literal">
ok
</result>
<t t-name="set-from-body-literal">
<t t-set="value">ok</t><t t-esc="value"/>
<t t-set="value">ok</t>
<t t-esc="value"/>
</t>
<result id="set-from-body-literal">
ok
</result>
<t t-name="set-from-attribute-lookup">
<t t-set="stuff" t-value="value"/><t t-esc="stuff"/>
<t t-set="stuff" t-value="value"/>
<t t-esc="stuff"/>
</t>
<params id="set-from-attribute-lookup">
{"value": "ok"}
</params>
<result id="set-from-attribute-lookup">
ok
</result>
<t t-name="set-from-body-lookup">
<t t-set="stuff"><t t-esc="value"/></t><t t-esc="stuff"/>
<t t-set="stuff">
<t t-esc="value"/>
</t>
<t t-esc="stuff"/>
</t>
<params id="set-from-body-lookup">
{"value": "ok"}
</params>
<result id="set-from-body-lookup">
ok
</result>
<t t-name="set-empty-body">
<t t-set="stuff"/>
<t t-esc="stuff"/>
</t>
<result id="set-empty-body"/>
<t t-name="t-value-priority">
<t t-set="value" t-value="1">2</t>
<t t-esc="value"/>
</t>
<result id="t-value-priority">1</result>
</templates>

View File

@ -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>'}), "&lt;ok&gt;", "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>'}), "&lt;ok&gt;", "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: '<ok>'}), "<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: '<ok>'}), "<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', {}), '<div foo="bar"></div>',
"Fixed name and literal attribute value");
assert.equal(render('fixed-variable', {value: 'ok'}), '<div foo="ok"></div>',
"Fixed name and variable attribute value");
});
QUnit.test('Tuple-based attributes', function (assert) {
assert.equal(render('tuple-literal', {}), '<div foo="bar"></div>',
"Tuple-based literal attributes");
assert.equal(render('tuple-variable', {att: ['foo', 'bar']}), '<div foo="bar"></div>',
"Tuple-based variable attributes");
});
QUnit.test('Fixed name, formatted value attributes', function (assert) {
assert.equal(render('format-literal', {}), '<div foo="bar"></div>',
"Literal format");
assert.equal(render('format-value', {value:'a'}), '<div foo="bar"></div>',
"Valued format");
assert.equal(
render('format-expression', {value: 5}),
'<div foo="42"></div>',
"Format strings are evaluated expressions");
assert.equal(render('format-multiple', {
value1: 0,
value2: 1,
value3: 2,
}),
'<div foo="a 0 is 1 of 2 ]"></div>',
"each format string should be evaluated independently");
});
QUnit.test('Fixed name, jinja-formatted', function (assert) {
assert.equal(render('format2-literal', {}), '<div foo="bar"></div>',
"Literal format");
assert.equal(render('format2-value', {value:'a'}), '<div foo="bar"></div>',
"Valued format");
assert.equal(
render('format2-expression', {value: 5}),
'<div foo="42"></div>',
"Format strings are evaluated expressions");
assert.equal(render('format2-multiple', {
value1: 0,
value2: 1,
value3: 2,
}),
'<div foo="a 0 is 1 of 2 ]"></div>',
"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(), '<b></b><b></b><b></b><b></b><b></b>',
"Repetition of node content via foreach");
assert.equal(QWeb.render('repetition-self', {seq:seq}).toLowerCase(), '<b></b><b></b><b></b><b></b><b></b>',
"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', {}), '<hr/><ul class="main"><li>1</li><li>2</li><li>3</li></ul><footer><b>[[end]]</b></footer>',
"Extend template with jQuery");
assert.equal(render('jquery-extend-clone', {}), '<ul><li>one</li><li>[[cloned template]]</li></ul>',
"Clone template");
});
test('Template inheritance', 'qweb-test-extend.xml');
});
</script>

View File

@ -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);");

View File

@ -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

View File

@ -15,3 +15,5 @@ odoo developer documentation
guides
reference
modules
.. todolist::

View File

@ -59,7 +59,7 @@ and defining methods decorated with :func:`~openerp.http.route`::
return stuff()
To *override* a controller, :ref:`inherit <python:tut-inheritance>` from its
class and override relevant methods::
class and override relevant methods, re-exposing them if necessary::
class Extension(MyController):
@route()

View File

@ -5,6 +5,9 @@ Javascript
Widgets
=======
.. qweb integration: ``template`` is an (optional) automatically rendered
template
RPC
===

View File

@ -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 ``<t>`` is also available,
which executes its directive but doesn't generate any output in and of
itself::
<t t-if="condition">
<p>Test</p>
</t>
will result in::
<p>Test</p>
if ``condition`` is true, but::
<div t-if="condition">
<p>Test</p>
</div>
will result in::
<div>
<p>Test</p>
</div>
.. _reference/qweb/output:
data output
-----------
===========
QWeb has a primary output directive which automatically HTML-escape its
content limiting XSS_ risks when displaying user-provided content: ``esc``.
``esc`` takes an expression, evaluates it and prints the content::
<p><t t-esc="value"/></p>
rendered with the value ``value`` set to ``42`` yields::
<p>42</p>
There is one other output directive ``raw`` which behaves the same as
respectively ``esc`` but *does not HTML-escape its output*. It can be useful
to display separately constructed markup (e.g. from functions) or already
sanitized user-provided markup.
.. _reference/qweb/conditionals:
conditionals
------------
============
QWeb has a conditional directive ``if``, which evaluates an expression given
as attribute value::
<div>
<t t-if="condition">
<p>ok</p>
</t>
</div>
The element is rendered if the condition is true::
<div>
<p>ok</p>
</div>
but if the condition is false it is removed from the result::
<div>
</div>
The conditional rendering applies to the bearer of the directive, which does
not have to be ``<t>``::
<div>
<p t-if="condition">ok</p>
</div>
will give the same results as the previous example.
.. _reference/qweb/loops:
loops
-----
=====
QWeb has an iteration directive ``foreach`` which take an expression returning
the collection to iterate on, and a second parameter ``t-as`` providing the
name to use for the "current item" of the iteration::
<t t-foreach="[1, 2, 3]" t-as="i">
<p><t t-esc="i"/></p>
</t>
will be rendered as::
<p>1</p>
<p>2</p>
<p>3</p>
Like conditions, ``foreach`` applies to the element bearing the directive's
attribute, and
::
<p t-foreach="[1, 2, 3]" t-as="i">
<t t-esc="i"/>
</p>
is equivalent to the previous example.
``foreach`` can iterate on an array (the current item will be the current
value), a mapping (the current item will be the current key) or an integer
(equivalent to iterating on an array between 0 inclusive and the provided
integer exclusive).
In addition to the name passed via ``t-as``, ``foreach`` provides a few other
variables for various data points (``$as`` is the name passed to ``t-as``):
:samp:`{$as}_all`
the object being iterated over
:samp:`{$as}_value`
the current iteration value, identical to ``$as`` for lists and integers,
but for mappings it provides the value (where ``$as`` provides the key)
:samp:`{$as}_index`
the current iteration index (the first item of the iteration has index 0)
:samp:`{$as}_size`
the size of the collection if it is available
:samp:`{$as}_first`
whether the current item is the first of the iteration (equivalent to
:samp:`{$as}_index == 0`)
:samp:`{$as}_last`
whether the current item is the last of the iteration (equivalent to
:samp:`{$as}_index + 1 == {$as}_size`), requires the iteratee's size be
available
:samp:`{$as}_parity`
either ``"even"`` or ``"odd"``, the parity of the current iteration round
:samp:`{$as}_even`
a boolean flag indicating that the current iteration round is on an even
index
:samp:`{$as}_odd`
a boolean flag indicating that the current iteration round is on an odd
index
.. _reference/qweb/attributes:
attributes
==========
QWeb can compute attributes on-the-fly and set the result of the computation
on the output node. This is done via the ``t-att`` (attribute) directive which
exists in 3 different forms:
:samp:`t-att-{$name}`
an attribute called ``$name`` is created, the attribute value is evaluated
and the result is set as the attribute's value::
<div t-att-a="42"/>
will be rendered as::
<div a="42"></div>
:samp:`t-attf-{$name}`
same as previous, but the parameter is a :term:`format string`
instead of just an expression, often useful to mix literal and non-literal
string (e.g. classes)::
<t t-foreach="[1, 2, 3]" t-as="item">
<li t-attf-class="row {{ item_parity }}"><t t-esc="item"/></li>
</t>
will be rendered as::
<li class="row even">1</li>
<li class="row odd">2</li>
<li class="row even">3</li>
:samp:`t-att=mapping`
if the parameter is a mapping, each (key, value) pair generates a new
attribute and its value::
<div t-att="{'a': 1, 'b': 2}"/>
will be rendered as::
<div a="1" b="2"></div>
:samp:`t-att=pair`
if the parameter is a pair (tuple or array of 2 element), the first
item of the pair is the name of the attribute and the second item is the
value::
<div t-att="['a', 'b']"/>
will be rendered as::
<div a="b"></div>
setting variables
=================
QWeb allows creating variables from within the template, to memoize a
computation (to use it multiple times), give a piece of data a clearer name,
...
This is done via the ``set`` directive, which takes the name of the variable
to create. The value to set can be provided in two ways:
* a ``t-value`` attribute containing an expression, and the result of its
evaluation will be set::
<t t-set="foo" t-value="2 + 1"/>
<t t-esc="foo"/>
will print ``3``
* if there is no ``t-value`` attribute, the node's body is rendered and set
as the variable's value::
<t t-set="foo">
<li>ok</li>
</t>
<t t-esc="foo"/>
will generate ``&lt;li&gt;ok&lt;/li&gt;`` (the content is escaped as we
used the ``esc`` directive)
.. note:: using the result of this operation is a significant use-case for
the ``raw`` directive.
calling sub-templates
=====================
QWeb templates can be used for top-level rendering, but they can also be used
from within another template (to avoid duplication or give names to parts of
templates) using the ``t-call`` directive::
<t t-call="other-template"/>
This calls the named template with the execution context of the parent, if
``other_template`` is defined as::
<p><t t-value="var"/></p>
the call above will be rendered as ``<p/>`` (no content), but::
<t t-set="var" t-value="1"/>
<t t-call="other-template"/>
will be rendered as ``<p>1</p>``.
However this has the problem of being visible from outside the ``t-call``.
Alternatively, content set in the body of the ``call`` directive will be
evaluated *before* calling the sub-template, and can alter a local context::
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
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)::
<templates>
<t t-name="template-name">
<!-- template code -->
</t>
</templates>
It takes no other parameter, but can be used with a ``<t>`` element or any
other. With a ``<t>`` element, the ``<t>`` should have a single child.
The template name is an arbitrary string, although when multiple templates
are related (e.g. called sub-templates) it is customary to use dot-separated
names to indicate hierarchical relationships.
template inheritance
''''''''''''''''''''
Template inheritance is used to alter existing templates in-place, e.g. to
add information to templates created by an other modules.
Template inheritance is performed via the ``t-extend`` directive which takes
the name of the template to alter as parameter.
The alteration is then performed with any number of ``t-jquery``
sub-directives::
<t t-extends="base.template">
<t t-jquery="ul" t-operation="append">
<li>new element</li>
</t>
</t>
The ``t-jquery`` directives takes a `CSS selector`_. This selector is used
on the extended template to select *context nodes* to which the specified
``t-operation`` is applied:
``append``
the node's body is appended at the end of the context node (after the
context node's last child)
``prepend``
the node's body is prepended to the context node (inserted before the
context node's first child)
``before``
the node's body is inserted right before the context node
``after``
the node's body is inserted right after the context node
``inner``
the node's body replaces the context node's children
``replace``
the node's body is used to replace the context node itself
No operation
if no ``t-operation`` is specified, the template body is interpreted as
javascript code and executed with the context node as ``this``
.. warning:: while much more powerful than other operations, this mode is
also much harder to debug and maintain, it is recommended to
avoid it
debugging
---------
The javascript QWeb implementation provides a few debugging hooks:
``t-log``
takes an expression parameter, evaluates the expression during rendering
and logs its result with ``console.log``
``t-debug``
triggers a debugger breakpoint during template rendering
``t-js``
the node's body is javascript code executed during template rendering.
Takes a ``context`` parameter, which is the name under which the rendering
context will be available in the ``t-js``'s body
Helpers
-------
.. js:attribute:: openerp.qweb
An instance of :js:class:`QWeb2.Engine` with all module-defined template
files loaded, and references to standard helper objects ``_``
(underscore), ``_t`` (translation function) and JSON_.
:js:func:`openerp.qweb.render <QWeb2.Engine.render>` can be used to
easily render basic module templates
API
---
.. js:class:: QWeb2.Engine
The QWeb "renderer", handles most of QWeb's logic (loading,
parsing, compiling and rendering templates).
OpenERP Web instantiates one for the user, and sets it to
``instance.web.qweb``. It also loads all the template files of the
various modules into that QWeb instance.
A :js:class:`QWeb2.Engine` also serves as a "template namespace".
.. js:function:: QWeb2.Engine.render(template[, context])
Renders a previously loaded template to a String, using
``context`` (if provided) to find the variables accessed
during template rendering (e.g. strings to display).
:param String template: the name of the template to render
:param Object context: the basic namespace to use for template
rendering
:returns: String
The engine exposes an other method which may be useful in some
cases (e.g. if you need a separate template namespace with, in
OpenERP Web, Kanban views get their own :js:class:`QWeb2.Engine`
instance so their templates don't collide with more general
"module" templates):
.. js:function:: QWeb2.Engine.add_template(templates)
Loads a template file (a collection of templates) in the QWeb
instance. The templates can be specified as:
An XML string
QWeb will attempt to parse it to an XML document then load
it.
A URL
QWeb will attempt to download the URL content, then load
the resulting XML string.
A ``Document`` or ``Node``
QWeb will traverse the first level of the document (the
child nodes of the provided root) and load any named
template or template override.
:type templates: String | Document | Node
A :js:class:`QWeb2.Engine` also exposes various attributes for
behavior customization:
.. js:attribute:: QWeb2.Engine.prefix
Prefix used to recognize directives during parsing. A string. By
default, ``t``.
.. js:attribute:: QWeb2.Engine.debug
Boolean flag putting the engine in "debug mode". Normally,
QWeb intercepts any error raised during template execution. In
debug mode, it leaves all exceptions go through without
intercepting them.
.. js:attribute:: QWeb2.Engine.jQuery
The jQuery instance used during :ref:`template inheritance
<qweb-directives-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/

View File

@ -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)