Merge pull request #2269 from odoo-dev/8.0-qweb-doc-and-fixes-xmo
QWeb documentation, fixes and improvements
This commit is contained in:
commit
1a29c81703
|
@ -37,7 +37,6 @@ Javascript
|
|||
widget
|
||||
rpc
|
||||
async
|
||||
qweb
|
||||
client_action
|
||||
testing
|
||||
|
||||
|
|
|
@ -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``.
|
|
@ -2,21 +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>
|
||||
<result id="format-literal"><![CDATA[<div foo="bar"></div>]]></result>
|
||||
|
||||
<t t-name="format-value">
|
||||
<div t-attf-foo="b#{value}r"/>
|
||||
<div t-attf-foo="b{{value}}r"/>
|
||||
</t>
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -1,30 +1,58 @@
|
|||
<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="0"/></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>
|
||||
|
||||
<!--
|
||||
postfix to call removed because Python impl appends all whitespace
|
||||
following called template's root to template result (+= element.tail)
|
||||
-> ends up with bunch of extra whitespace in the middle of the
|
||||
generated content. Could normalize, not sure current impl can be
|
||||
fixed as-is
|
||||
-->
|
||||
<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>
|
||||
|
|
|
@ -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 && 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 > 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 < 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 >= 1">o</t><t t-if="2 >= 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 <= 2">o</t><t t-if="2 <= 2">k</t>
|
||||
<t t-name="boolean-value-condition-missing">
|
||||
<t t-if="condition">fail</t>
|
||||
</t>
|
||||
<result id="boolean-value-condition-missing"/>
|
||||
</templates>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -3,21 +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 t-esc="var"/>
|
||||
</t>
|
||||
<params id="esc-toescape"><![CDATA[{"var": "<ok>"}]]></params>
|
||||
<result id="esc-toescape"><![CDATA[<ok>]]></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 t-raw="var"/>
|
||||
</t>
|
||||
<params id="raw-notescaped"><![CDATA[{"var": "<ok>"}]]></params>
|
||||
<result id="raw-notescaped"><![CDATA[<ok>]]></result>
|
||||
</templates>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="http://code.jquery.com/jquery-1.5.2.js"></script>
|
||||
<link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen"/>
|
||||
<script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
|
||||
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.15.0.css" type="text/css" media="screen"/>
|
||||
<script type="text/javascript" src="http://code.jquery.com/qunit/qunit-1.15.0.js"></script>
|
||||
|
||||
<script type="text/javascript" src="qweb2.js"></script>
|
||||
|
||||
|
@ -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>'}), "<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: '<ok>'}), "<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', {}), '<div foo="bar"/>',
|
||||
"Fixed name and literal attribute value");
|
||||
equals(render('fixed-variable', {value: 'ok'}), '<div foo="ok"/>',
|
||||
"Fixed name and variable attribute value");
|
||||
});
|
||||
test('Tuple-based attributes', function () {
|
||||
equals(render('tuple-literal', {}), '<div foo="bar"/>',
|
||||
"Tuple-based literal attributes");
|
||||
equals(render('tuple-variable', {att: ['foo', 'bar']}), '<div foo="bar"/>',
|
||||
"Tuple-based variable attributes");
|
||||
});
|
||||
test('Fixed name, formatted value attributes', function () {
|
||||
equals(render('format-literal', {}), '<div foo="bar"/>',
|
||||
"Literal format");
|
||||
equals(render('format-value', {value:'a'}), '<div foo="bar"/>',
|
||||
"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(), '<b/><b/><b/><b/><b/>',
|
||||
"Repetition of node content via foreach");
|
||||
equals(QWeb.render('repetition-self', {seq:seq}).toLowerCase(), '<b/><b/><b/><b/><b/>',
|
||||
"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', {}), '<hr/><ul class="main"><li>1</li><li>2</li><li>3</li></ul><footer><b>[[end]]</b></footer>',
|
||||
"Extend template with jQuery");
|
||||
equals(render('jquery-extend-clone', {}), '<ul><li>one</li><li>[[cloned template]]</li></ul>',
|
||||
"Clone template");
|
||||
});
|
||||
test('Template inheritance', 'qweb-test-extend.xml');
|
||||
});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="qunit-header">QWeb test suite</h1>
|
||||
|
||||
<h2 id="qunit-banner"></h2>
|
||||
|
||||
<div id="qunit-testrunner-toolbar"></div>
|
||||
<h2 id="qunit-userAgent"></h2>
|
||||
<ol id="qunit-tests"></ol>
|
||||
<div id="qunit-fixture">test markup, will be hidden</div>
|
||||
<div id="qunit"></div>
|
||||
<div id="qunit-fixture"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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)) + ");");
|
||||
|
|
|
@ -577,7 +577,7 @@
|
|||
</t>
|
||||
<t t-name="ViewPager">
|
||||
<div class="oe_pager_value">
|
||||
<t t-raw="__content__"/>
|
||||
<t t-raw="0"/>
|
||||
</div>
|
||||
<ul class="oe_pager_group">
|
||||
<!--
|
||||
|
@ -1373,7 +1373,7 @@
|
|||
method="post" enctype="multipart/form-data" t-att-action="fileupload_action || '/web/binary/upload'">
|
||||
<input type="hidden" name="session_id" value="" t-if="widget.session.override_session"/>
|
||||
<input type="hidden" name="callback" t-att-value="fileupload_id"/>
|
||||
<t t-raw="__content__"/>
|
||||
<t t-raw="0"/>
|
||||
<input type="file" class="oe_form_binary_file" name="ufile" t-if="widget.widget!='image'"/>
|
||||
<input type="file" class="oe_form_binary_file" name="ufile" accept="image/*" t-if="widget.widget=='image'"/>
|
||||
</form>
|
||||
|
|
|
@ -56,11 +56,13 @@ class QWeb(orm.AbstractModel):
|
|||
super(QWeb, self).add_template(qcontext, name, node)
|
||||
|
||||
def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
|
||||
att, val = super(QWeb, self).render_att_att(element, attribute_name, attribute_value, qwebcontext)
|
||||
URL_ATTRS = self.URL_ATTRS.get(element.tag)
|
||||
is_website = request.website
|
||||
for att, val in super(QWeb, self).render_att_att(element, attribute_name, attribute_value, qwebcontext):
|
||||
if is_website and att == URL_ATTRS and isinstance(val, basestring):
|
||||
val = qwebcontext.get('url_for')(val)
|
||||
yield (att, val)
|
||||
|
||||
if request.website and att == self.URL_ATTRS.get(element.tag) and isinstance(val, basestring):
|
||||
val = qwebcontext.get('url_for')(val)
|
||||
return att, val
|
||||
|
||||
def get_converter_for(self, field_type):
|
||||
return self.pool.get(
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3 class="modal-title"><t t-esc="title"/></h3>
|
||||
</div>
|
||||
<div class="modal-body"><t t-raw="__content__"/></div>
|
||||
<div class="modal-body"><t t-raw="0"/></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary save">Save</button>
|
||||
<button type="button" class="btn hidden wait" disabled="disabled"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,3 +15,5 @@ odoo developer documentation
|
|||
guides
|
||||
reference
|
||||
modules
|
||||
|
||||
.. todolist::
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -5,6 +5,9 @@ Javascript
|
|||
Widgets
|
||||
=======
|
||||
|
||||
.. qweb integration: ``template`` is an (optional) automatically rendered
|
||||
template
|
||||
|
||||
RPC
|
||||
===
|
||||
|
||||
|
|
|
@ -1,37 +1,573 @@
|
|||
.. highlight:: xml
|
||||
|
||||
.. _reference/qweb:
|
||||
|
||||
====
|
||||
QWeb
|
||||
====
|
||||
|
||||
Basics
|
||||
======
|
||||
QWeb is the primary templating_ engine used by Odoo\ [#othertemplates]_. It
|
||||
is an XML templating engine\ [#genshif]_ and used mostly to generate HTML_
|
||||
fragments and pages.
|
||||
|
||||
Template directives are specified as XML attributes prefixed with ``t-``,
|
||||
for instance ``t-if`` for :ref:`reference/qweb/conditionals`, with elements
|
||||
and other attributes being rendered directly.
|
||||
|
||||
To avoid element rendering, a placeholder element ``<t>`` is also available,
|
||||
which executes its directive but doesn't generate any output in and of
|
||||
itself::
|
||||
|
||||
<t t-if="condition">
|
||||
<p>Test</p>
|
||||
</t>
|
||||
|
||||
will result in::
|
||||
|
||||
<p>Test</p>
|
||||
|
||||
if ``condition`` is true, but::
|
||||
|
||||
<div t-if="condition">
|
||||
<p>Test</p>
|
||||
</div>
|
||||
|
||||
will result in::
|
||||
|
||||
<div>
|
||||
<p>Test</p>
|
||||
</div>
|
||||
|
||||
.. _reference/qweb/output:
|
||||
|
||||
data output
|
||||
-----------
|
||||
===========
|
||||
|
||||
QWeb has a primary output directive which automatically HTML-escape its
|
||||
content limiting XSS_ risks when displaying user-provided content: ``esc``.
|
||||
|
||||
``esc`` takes an expression, evaluates it and prints the content::
|
||||
|
||||
<p><t t-esc="value"/></p>
|
||||
|
||||
rendered with the value ``value`` set to ``42`` yields::
|
||||
|
||||
<p>42</p>
|
||||
|
||||
There is one other output directive ``raw`` which behaves the same as
|
||||
respectively ``esc`` but *does not HTML-escape its output*. It can be useful
|
||||
to display separately constructed markup (e.g. from functions) or already
|
||||
sanitized user-provided markup.
|
||||
|
||||
.. _reference/qweb/conditionals:
|
||||
|
||||
conditionals
|
||||
------------
|
||||
============
|
||||
|
||||
QWeb has a conditional directive ``if``, which evaluates an expression given
|
||||
as attribute value::
|
||||
|
||||
<div>
|
||||
<t t-if="condition">
|
||||
<p>ok</p>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
The element is rendered if the condition is true::
|
||||
|
||||
<div>
|
||||
<p>ok</p>
|
||||
</div>
|
||||
|
||||
but if the condition is false it is removed from the result::
|
||||
|
||||
<div>
|
||||
</div>
|
||||
|
||||
The conditional rendering applies to the bearer of the directive, which does
|
||||
not have to be ``<t>``::
|
||||
|
||||
<div>
|
||||
<p t-if="condition">ok</p>
|
||||
</div>
|
||||
|
||||
will give the same results as the previous example.
|
||||
|
||||
.. _reference/qweb/loops:
|
||||
|
||||
loops
|
||||
-----
|
||||
=====
|
||||
|
||||
QWeb has an iteration directive ``foreach`` which take an expression returning
|
||||
the collection to iterate on, and a second parameter ``t-as`` providing the
|
||||
name to use for the "current item" of the iteration::
|
||||
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<p>1</p>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
|
||||
Like conditions, ``foreach`` applies to the element bearing the directive's
|
||||
attribute, and
|
||||
|
||||
::
|
||||
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
``foreach`` can iterate on an array (the current item will be the current
|
||||
value), a mapping (the current item will be the current key) or an integer
|
||||
(equivalent to iterating on an array between 0 inclusive and the provided
|
||||
integer exclusive).
|
||||
|
||||
In addition to the name passed via ``t-as``, ``foreach`` provides a few other
|
||||
variables for various data points (``$as`` is the name passed to ``t-as``):
|
||||
|
||||
:samp:`{$as}_all`
|
||||
the object being iterated over
|
||||
:samp:`{$as}_value`
|
||||
the current iteration value, identical to ``$as`` for lists and integers,
|
||||
but for mappings it provides the value (where ``$as`` provides the key)
|
||||
:samp:`{$as}_index`
|
||||
the current iteration index (the first item of the iteration has index 0)
|
||||
:samp:`{$as}_size`
|
||||
the size of the collection if it is available
|
||||
:samp:`{$as}_first`
|
||||
whether the current item is the first of the iteration (equivalent to
|
||||
:samp:`{$as}_index == 0`)
|
||||
:samp:`{$as}_last`
|
||||
whether the current item is the last of the iteration (equivalent to
|
||||
:samp:`{$as}_index + 1 == {$as}_size`), requires the iteratee's size be
|
||||
available
|
||||
:samp:`{$as}_parity`
|
||||
either ``"even"`` or ``"odd"``, the parity of the current iteration round
|
||||
:samp:`{$as}_even`
|
||||
a boolean flag indicating that the current iteration round is on an even
|
||||
index
|
||||
:samp:`{$as}_odd`
|
||||
a boolean flag indicating that the current iteration round is on an odd
|
||||
index
|
||||
|
||||
.. _reference/qweb/attributes:
|
||||
|
||||
attributes
|
||||
==========
|
||||
|
||||
QWeb can compute attributes on-the-fly and set the result of the computation
|
||||
on the output node. This is done via the ``t-att`` (attribute) directive which
|
||||
exists in 3 different forms:
|
||||
|
||||
:samp:`t-att-{$name}`
|
||||
an attribute called ``$name`` is created, the attribute value is evaluated
|
||||
and the result is set as the attribute's value::
|
||||
|
||||
<div t-att-a="42"/>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<div a="42"></div>
|
||||
:samp:`t-attf-{$name}`
|
||||
same as previous, but the parameter is a :term:`format string`
|
||||
instead of just an expression, often useful to mix literal and non-literal
|
||||
string (e.g. classes)::
|
||||
|
||||
<t t-foreach="[1, 2, 3]" t-as="item">
|
||||
<li t-attf-class="row {{ item_parity }}"><t t-esc="item"/></li>
|
||||
</t>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<li class="row even">1</li>
|
||||
<li class="row odd">2</li>
|
||||
<li class="row even">3</li>
|
||||
:samp:`t-att=mapping`
|
||||
if the parameter is a mapping, each (key, value) pair generates a new
|
||||
attribute and its value::
|
||||
|
||||
<div t-att="{'a': 1, 'b': 2}"/>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<div a="1" b="2"></div>
|
||||
:samp:`t-att=pair`
|
||||
if the parameter is a pair (tuple or array of 2 element), the first
|
||||
item of the pair is the name of the attribute and the second item is the
|
||||
value::
|
||||
|
||||
<div t-att="['a', 'b']"/>
|
||||
|
||||
will be rendered as::
|
||||
|
||||
<div a="b"></div>
|
||||
|
||||
setting variables
|
||||
=================
|
||||
|
||||
QWeb allows creating variables from within the template, to memoize a
|
||||
computation (to use it multiple times), give a piece of data a clearer name,
|
||||
...
|
||||
|
||||
This is done via the ``set`` directive, which takes the name of the variable
|
||||
to create. The value to set can be provided in two ways:
|
||||
|
||||
* a ``t-value`` attribute containing an expression, and the result of its
|
||||
evaluation will be set::
|
||||
|
||||
<t t-set="foo" t-value="2 + 1"/>
|
||||
<t t-esc="foo"/>
|
||||
|
||||
will print ``3``
|
||||
* if there is no ``t-value`` attribute, the node's body is rendered and set
|
||||
as the variable's value::
|
||||
|
||||
<t t-set="foo">
|
||||
<li>ok</li>
|
||||
</t>
|
||||
<t t-esc="foo"/>
|
||||
|
||||
will generate ``<li>ok</li>`` (the content is escaped as we
|
||||
used the ``esc`` directive)
|
||||
|
||||
.. note:: using the result of this operation is a significant use-case for
|
||||
the ``raw`` directive.
|
||||
|
||||
calling sub-templates
|
||||
=====================
|
||||
|
||||
QWeb templates can be used for top-level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
templates) using the ``t-call`` directive::
|
||||
|
||||
<t t-call="other-template"/>
|
||||
|
||||
This calls the named template with the execution context of the parent, if
|
||||
``other_template`` is defined as::
|
||||
|
||||
<p><t t-value="var"/></p>
|
||||
|
||||
the call above will be rendered as ``<p/>`` (no content), but::
|
||||
|
||||
<t t-set="var" t-value="1"/>
|
||||
<t t-call="other-template"/>
|
||||
|
||||
will be rendered as ``<p>1</p>``.
|
||||
|
||||
However this has the problem of being visible from outside the ``t-call``.
|
||||
Alternatively, content set in the body of the ``call`` directive will be
|
||||
evaluated *before* calling the sub-template, and can alter a local context::
|
||||
|
||||
<t t-call="other-template">
|
||||
<t t-set="var" t-value="1"/>
|
||||
</t>
|
||||
<!-- "var" does not exist here -->
|
||||
|
||||
The body of the ``call`` directive can be arbitrarily complex (not just
|
||||
``set`` directives), and its rendered form will be available within the called
|
||||
template as a magical ``0`` variable::
|
||||
|
||||
<div>
|
||||
This template was called with content:
|
||||
<t t-raw="0"/>
|
||||
</div>
|
||||
|
||||
being called thus::
|
||||
|
||||
<t t-call="other-template">
|
||||
<em>content</em>
|
||||
</t>
|
||||
|
||||
will result in::
|
||||
|
||||
<div>
|
||||
This template was called with content:
|
||||
<em>content</em>
|
||||
</div>
|
||||
|
||||
Python
|
||||
======
|
||||
|
||||
Bundles
|
||||
Exclusive directives
|
||||
--------------------
|
||||
|
||||
asset bundles
|
||||
'''''''''''''
|
||||
|
||||
.. todo:: have fme write these up because I've no idea how they work
|
||||
|
||||
"smart records" fields formatting
|
||||
'''''''''''''''''''''''''''''''''
|
||||
|
||||
The ``t-field`` directive can only be used when performing field access
|
||||
(``a.b``) on a "smart" record (result of the ``browse`` method). It is able
|
||||
to automatically format based on field type, and is integrated in the
|
||||
website's rich text edition.
|
||||
|
||||
``t-field-options`` can be used to customize fields, the most common option
|
||||
is ``widget``, other options are field- or widget-dependent.
|
||||
|
||||
Helpers
|
||||
-------
|
||||
|
||||
Request-based
|
||||
'''''''''''''
|
||||
|
||||
Most Python-side uses of QWeb are in controllers (and during HTTP requests),
|
||||
in which case templates stored in the database (as
|
||||
:ref:`views <reference/views/qweb>`) can be trivially rendered by calling
|
||||
:meth:`openerp.http.HttpRequest.render`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response = http.request.render('my-template', {
|
||||
'context_value': 42
|
||||
})
|
||||
|
||||
This automatically creates a :class:`~openerp.http.Response` object which can
|
||||
be returned from the controller (or further customized to suit).
|
||||
|
||||
View-based
|
||||
''''''''''
|
||||
|
||||
At a deeper level than the previous helper is the ``render`` method on
|
||||
``ir.ui.view``:
|
||||
|
||||
.. py:method:: render(cr, uid, id[, values][, engine='ir.qweb][, context])
|
||||
|
||||
Renders a QWeb view/template by database id or :term:`external id`.
|
||||
Templates are automatically loaded from ``ir.ui.view`` records.
|
||||
|
||||
Sets up a number of default values in the rendering context:
|
||||
|
||||
``request``
|
||||
the current :class:`~openerp.http.WebRequest` object, if any
|
||||
``debug``
|
||||
whether the current request (if any) is in ``debug`` mode
|
||||
:func:`quote_plus <werkzeug.urls.url_quote_plus>`
|
||||
url-encoding utility function
|
||||
:mod:`json`
|
||||
the corresponding standard library module
|
||||
:mod:`time`
|
||||
the corresponding standard library module
|
||||
:mod:`datetime`
|
||||
the corresponding standard library module
|
||||
`relativedelta <https://labix.org/python-dateutil#head-ba5ffd4df8111d1b83fc194b97ebecf837add454>`_
|
||||
see module
|
||||
``keep_query``
|
||||
the ``keep_query`` helper function
|
||||
|
||||
:param values: context values to pass to QWeb for rendering
|
||||
:param str engine: name of the Odoo model to use for rendering, can be
|
||||
used to expand or customize QWeb locally (by creating
|
||||
a "new" qweb based on ``ir.qweb`` with alterations)
|
||||
|
||||
.. _reference/qweb/javascript:
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
It is also possible to use the ``ir.qweb`` model directly (and extend it, and
|
||||
inherit from it):
|
||||
|
||||
.. automodule:: openerp.addons.base.ir.ir_qweb
|
||||
:members: QWeb, QWebContext, FieldConverter, QwebWidget
|
||||
|
||||
Javascript
|
||||
==========
|
||||
|
||||
loading
|
||||
Exclusive directives
|
||||
--------------------
|
||||
|
||||
defining templates
|
||||
''''''''''''''''''
|
||||
|
||||
The ``t-name`` directive can only be placed at the top-level of a template
|
||||
file (direct children to the document root)::
|
||||
|
||||
<templates>
|
||||
<t t-name="template-name">
|
||||
<!-- template code -->
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
It takes no other parameter, but can be used with a ``<t>`` element or any
|
||||
other. With a ``<t>`` element, the ``<t>`` should have a single child.
|
||||
|
||||
The template name is an arbitrary string, although when multiple templates
|
||||
are related (e.g. called sub-templates) it is customary to use dot-separated
|
||||
names to indicate hierarchical relationships.
|
||||
|
||||
template inheritance
|
||||
''''''''''''''''''''
|
||||
|
||||
Template inheritance is used to alter existing templates in-place, e.g. to
|
||||
add information to templates created by an other modules.
|
||||
|
||||
Template inheritance is performed via the ``t-extend`` directive which takes
|
||||
the name of the template to alter as parameter.
|
||||
|
||||
The alteration is then performed with any number of ``t-jquery``
|
||||
sub-directives::
|
||||
|
||||
<t t-extends="base.template">
|
||||
<t t-jquery="ul" t-operation="append">
|
||||
<li>new element</li>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
The ``t-jquery`` directives takes a `CSS selector`_. This selector is used
|
||||
on the extended template to select *context nodes* to which the specified
|
||||
``t-operation`` is applied:
|
||||
|
||||
``append``
|
||||
the node's body is appended at the end of the context node (after the
|
||||
context node's last child)
|
||||
``prepend``
|
||||
the node's body is prepended to the context node (inserted before the
|
||||
context node's first child)
|
||||
``before``
|
||||
the node's body is inserted right before the context node
|
||||
``after``
|
||||
the node's body is inserted right after the context node
|
||||
``inner``
|
||||
the node's body replaces the context node's children
|
||||
``replace``
|
||||
the node's body is used to replace the context node itself
|
||||
No operation
|
||||
if no ``t-operation`` is specified, the template body is interpreted as
|
||||
javascript code and executed with the context node as ``this``
|
||||
|
||||
.. warning:: while much more powerful than other operations, this mode is
|
||||
also much harder to debug and maintain, it is recommended to
|
||||
avoid it
|
||||
|
||||
debugging
|
||||
---------
|
||||
|
||||
The javascript QWeb implementation provides a few debugging hooks:
|
||||
|
||||
``t-log``
|
||||
takes an expression parameter, evaluates the expression during rendering
|
||||
and logs its result with ``console.log``
|
||||
``t-debug``
|
||||
triggers a debugger breakpoint during template rendering
|
||||
``t-js``
|
||||
the node's body is javascript code executed during template rendering.
|
||||
Takes a ``context`` parameter, which is the name under which the rendering
|
||||
context will be available in the ``t-js``'s body
|
||||
|
||||
Helpers
|
||||
-------
|
||||
|
||||
.. js:attribute:: openerp.qweb
|
||||
|
||||
An instance of :js:class:`QWeb2.Engine` with all module-defined template
|
||||
files loaded, and references to standard helper objects ``_``
|
||||
(underscore), ``_t`` (translation function) and JSON_.
|
||||
|
||||
:js:func:`openerp.qweb.render <QWeb2.Engine.render>` can be used to
|
||||
easily render basic module templates
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. js:class:: QWeb2.Engine
|
||||
|
||||
The QWeb "renderer", handles most of QWeb's logic (loading,
|
||||
parsing, compiling and rendering templates).
|
||||
|
||||
OpenERP Web instantiates one for the user, and sets it to
|
||||
``instance.web.qweb``. It also loads all the template files of the
|
||||
various modules into that QWeb instance.
|
||||
|
||||
A :js:class:`QWeb2.Engine` also serves as a "template namespace".
|
||||
|
||||
.. js:function:: QWeb2.Engine.render(template[, context])
|
||||
|
||||
Renders a previously loaded template to a String, using
|
||||
``context`` (if provided) to find the variables accessed
|
||||
during template rendering (e.g. strings to display).
|
||||
|
||||
:param String template: the name of the template to render
|
||||
:param Object context: the basic namespace to use for template
|
||||
rendering
|
||||
:returns: String
|
||||
|
||||
The engine exposes an other method which may be useful in some
|
||||
cases (e.g. if you need a separate template namespace with, in
|
||||
OpenERP Web, Kanban views get their own :js:class:`QWeb2.Engine`
|
||||
instance so their templates don't collide with more general
|
||||
"module" templates):
|
||||
|
||||
.. js:function:: QWeb2.Engine.add_template(templates)
|
||||
|
||||
Loads a template file (a collection of templates) in the QWeb
|
||||
instance. The templates can be specified as:
|
||||
|
||||
An XML string
|
||||
QWeb will attempt to parse it to an XML document then load
|
||||
it.
|
||||
|
||||
A URL
|
||||
QWeb will attempt to download the URL content, then load
|
||||
the resulting XML string.
|
||||
|
||||
A ``Document`` or ``Node``
|
||||
QWeb will traverse the first level of the document (the
|
||||
child nodes of the provided root) and load any named
|
||||
template or template override.
|
||||
|
||||
:type templates: String | Document | Node
|
||||
|
||||
A :js:class:`QWeb2.Engine` also exposes various attributes for
|
||||
behavior customization:
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.prefix
|
||||
|
||||
Prefix used to recognize directives during parsing. A string. By
|
||||
default, ``t``.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.debug
|
||||
|
||||
Boolean flag putting the engine in "debug mode". Normally,
|
||||
QWeb intercepts any error raised during template execution. In
|
||||
debug mode, it leaves all exceptions go through without
|
||||
intercepting them.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.jQuery
|
||||
|
||||
The jQuery instance used during template inheritance processing.
|
||||
Defaults to ``window.jQuery``.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.preprocess_node
|
||||
|
||||
A ``Function``. If present, called before compiling each DOM
|
||||
node to template code. In OpenERP Web, this is used to
|
||||
automatically translate text content and some attributes in
|
||||
templates. Defaults to ``null``.
|
||||
|
||||
.. [#genshif] it is similar in that to Genshi_, although it does not use (and
|
||||
has no support for) `XML namespaces`_
|
||||
|
||||
.. [#othertemplates] although it uses a few others, either for historical
|
||||
reasons or because they remain better fits for the
|
||||
use case. Odoo 8.0 still depends on Jinja_ and Mako_.
|
||||
|
||||
.. _templating:
|
||||
http://en.wikipedia.org/wiki/Template_processor
|
||||
|
||||
.. _Jinja: http://jinja.pocoo.org
|
||||
.. _Mako: http://www.makotemplates.org
|
||||
.. _Genshi: http://genshi.edgewall.org
|
||||
.. _XML namespaces: http://en.wikipedia.org/wiki/XML_namespace
|
||||
.. _HTML: http://en.wikipedia.org/wiki/HTML
|
||||
.. _XSS: http://en.wikipedia.org/wiki/Cross-site_scripting
|
||||
.. _JSON: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
|
||||
.. _CSS selector: http://api.jquery.com/category/selectors/
|
||||
|
|
|
@ -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("<?xml"):
|
||||
dom = etree.fromstring(document)
|
||||
else:
|
||||
dom = etree.parse(document)
|
||||
dom = etree.parse(document).getroot()
|
||||
|
||||
for node in dom:
|
||||
if node.get('t-name'):
|
||||
|
@ -179,6 +170,12 @@ class QWeb(orm.AbstractModel):
|
|||
res_id = None
|
||||
|
||||
def get_template(self, name, qwebcontext):
|
||||
""" Tries to fetch the template ``name``, either gets it from the
|
||||
context's template cache or loads one with the context's loader (if
|
||||
any).
|
||||
|
||||
:raises QWebTemplateNotFound: if the template can not be found or loaded
|
||||
"""
|
||||
origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
|
||||
if qwebcontext.loader and name not in qwebcontext.templates:
|
||||
try:
|
||||
|
@ -231,6 +228,15 @@ class QWeb(orm.AbstractModel):
|
|||
return int(bool(self.eval(expr, qwebcontext)))
|
||||
|
||||
def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
|
||||
""" render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
|
||||
|
||||
Renders the template specified by the provided template name
|
||||
|
||||
:param qwebcontext: context for rendering the template
|
||||
:type qwebcontext: dict or :class:`QWebContext` instance
|
||||
:param loader: if ``qwebcontext`` is a dict, loader set into the
|
||||
context instantiated for rendering
|
||||
"""
|
||||
if qwebcontext is None:
|
||||
qwebcontext = {}
|
||||
|
||||
|
@ -264,8 +270,12 @@ class QWeb(orm.AbstractModel):
|
|||
if attribute_name.startswith("t-"):
|
||||
for attribute in self._render_att:
|
||||
if attribute_name[2:].startswith(attribute):
|
||||
att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
|
||||
if val:
|
||||
attrs = self._render_att[attribute](
|
||||
self, element, attribute_name, attribute_value, qwebcontext)
|
||||
for att, val in attrs:
|
||||
if not val: continue
|
||||
if not isinstance(val, str):
|
||||
val = unicode(val).encode('utf-8')
|
||||
generated_attributes += self.render_attribute(element, att, val, qwebcontext)
|
||||
break
|
||||
else:
|
||||
|
@ -335,14 +345,16 @@ class QWeb(orm.AbstractModel):
|
|||
# Attributes
|
||||
def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
|
||||
if attribute_name.startswith("t-attf-"):
|
||||
att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
|
||||
elif attribute_name.startswith("t-att-"):
|
||||
att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
|
||||
else:
|
||||
att, val = self.eval_object(attribute_value, qwebcontext)
|
||||
if val and not isinstance(val, str):
|
||||
val = unicode(val).encode("utf8")
|
||||
return att, val
|
||||
return [(attribute_name[7:], self.eval_format(attribute_value, qwebcontext))]
|
||||
|
||||
if attribute_name.startswith("t-att-"):
|
||||
return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))]
|
||||
|
||||
result = self.eval_object(attribute_value, qwebcontext)
|
||||
if isinstance(result, collections.Mapping):
|
||||
return result.iteritems()
|
||||
# assume tuple
|
||||
return [result]
|
||||
|
||||
# Tags
|
||||
def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||
|
@ -355,29 +367,40 @@ class QWeb(orm.AbstractModel):
|
|||
inner = widget.format(template_attributes['esc'], options, qwebcontext)
|
||||
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
|
||||
|
||||
def _iterate(self, iterable):
|
||||
if isinstance (iterable, collections.Mapping):
|
||||
return iterable.iteritems()
|
||||
|
||||
return itertools.izip(*itertools.tee(iterable))
|
||||
|
||||
def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||
expr = template_attributes["foreach"]
|
||||
enum = self.eval_object(expr, qwebcontext)
|
||||
if enum is None:
|
||||
template = qwebcontext.get('__template__')
|
||||
raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
|
||||
if isinstance(enum, int):
|
||||
enum = range(enum)
|
||||
|
||||
varname = template_attributes['as'].replace('.', '_')
|
||||
copy_qwebcontext = qwebcontext.copy()
|
||||
size = -1
|
||||
|
||||
size = None
|
||||
if isinstance(enum, collections.Sized):
|
||||
size = len(enum)
|
||||
copy_qwebcontext["%s_size" % varname] = size
|
||||
copy_qwebcontext["%s_size" % varname] = size
|
||||
|
||||
copy_qwebcontext["%s_all" % varname] = enum
|
||||
ru = []
|
||||
for index, item in enumerate(enum):
|
||||
for index, (item, value) in enumerate(self._iterate(enum)):
|
||||
copy_qwebcontext.update({
|
||||
varname: item,
|
||||
'%s_value' % varname: item,
|
||||
'%s_value' % varname: value,
|
||||
'%s_index' % varname: index,
|
||||
'%s_first' % varname: index == 0,
|
||||
'%s_last' % varname: index + 1 == size,
|
||||
})
|
||||
if size is not None:
|
||||
copy_qwebcontext['%s_last' % varname] = index + 1 == size
|
||||
if index % 2:
|
||||
copy_qwebcontext.update({
|
||||
'%s_parity' % varname: 'odd',
|
||||
|
@ -457,9 +480,22 @@ class QWeb(orm.AbstractModel):
|
|||
element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
|
||||
|
||||
def get_converter_for(self, field_type):
|
||||
""" returns a :class:`~openerp.models.Model` used to render a
|
||||
``t-field``.
|
||||
|
||||
By default, tries to get the model named
|
||||
:samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
|
||||
|
||||
:param str field_type: type or widget of field to render
|
||||
"""
|
||||
return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
|
||||
|
||||
def get_widget_for(self, widget):
|
||||
""" returns a :class:`~openerp.models.Model` used to render a
|
||||
``t-esc``
|
||||
|
||||
:param str widget: name of the widget to use, or ``None``
|
||||
"""
|
||||
widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
|
||||
return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
|
||||
|
||||
|
@ -491,7 +527,8 @@ class FieldConverter(osv.AbstractModel):
|
|||
def attributes(self, cr, uid, field_name, record, options,
|
||||
source_element, g_att, t_att, qweb_context,
|
||||
context=None):
|
||||
"""
|
||||
""" attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None)
|
||||
|
||||
Generates the metadata attributes (prefixed by ``data-oe-`` for the
|
||||
root node of the field conversion. Attribute values are escaped by the
|
||||
parent.
|
||||
|
@ -520,21 +557,26 @@ class FieldConverter(osv.AbstractModel):
|
|||
]
|
||||
|
||||
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||
""" Converts a single value to its HTML version/output
|
||||
""" value_to_html(cr, uid, value, column, options=None, context=None)
|
||||
|
||||
Converts a single value to its HTML version/output
|
||||
"""
|
||||
if not value: return ''
|
||||
return value
|
||||
|
||||
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
|
||||
""" Converts the specified field of the browse_record ``record`` to
|
||||
HTML
|
||||
""" record_to_html(cr, uid, field_name, record, column, options=None, context=None)
|
||||
|
||||
Converts the specified field of the browse_record ``record`` to HTML
|
||||
"""
|
||||
return self.value_to_html(
|
||||
cr, uid, record[field_name], column, options=options, context=context)
|
||||
|
||||
def to_html(self, cr, uid, field_name, record, options,
|
||||
source_element, t_att, g_att, qweb_context, context=None):
|
||||
""" Converts a ``t-field`` to its HTML output. A ``t-field`` may be
|
||||
""" to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None)
|
||||
|
||||
Converts a ``t-field`` to its HTML output. A ``t-field`` may be
|
||||
extended by a ``t-field-options``, which is a JSON-serialized mapping
|
||||
of configuration values.
|
||||
|
||||
|
@ -576,13 +618,16 @@ class FieldConverter(osv.AbstractModel):
|
|||
|
||||
def render_element(self, cr, uid, source_element, t_att, g_att,
|
||||
qweb_context, content):
|
||||
""" Final rendering hook, by default just calls ir.qweb's ``render_element``
|
||||
""" render_element(cr, uid, source_element, t_att, g_att, qweb_context, content)
|
||||
|
||||
Final rendering hook, by default just calls ir.qweb's ``render_element``
|
||||
"""
|
||||
return self.qweb_object().render_element(
|
||||
source_element, t_att, g_att, qweb_context, content or '')
|
||||
|
||||
def user_lang(self, cr, uid, context):
|
||||
"""
|
||||
""" user_lang(cr, uid, context)
|
||||
|
||||
Fetches the res.lang object corresponding to the language code stored
|
||||
in the user's context. Fallbacks to en_US if no lang is present in the
|
||||
context *or the language code is not valid*.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import cgi
|
||||
import json
|
||||
import os.path
|
||||
import glob
|
||||
import re
|
||||
import collections
|
||||
|
||||
from lxml import etree
|
||||
import openerp.addons.base.ir.ir_qweb
|
||||
import openerp.modules
|
||||
|
||||
from openerp.tests import common
|
||||
from openerp.addons.base.ir import ir_qweb
|
||||
|
@ -73,3 +80,60 @@ class TestQWebTField(common.TransactionCase):
|
|||
self.engine.render_node(field, self.context({
|
||||
'company': None
|
||||
}))
|
||||
|
||||
class TestQWeb(common.TransactionCase):
|
||||
matcher = re.compile('^qweb-test-(.*)\.xml$')
|
||||
|
||||
@classmethod
|
||||
def get_cases(cls):
|
||||
path = cls.qweb_test_file_path()
|
||||
|
||||
return (
|
||||
cls("test_qweb_{}".format(cls.matcher.match(f).group(1)))
|
||||
for f in os.listdir(path)
|
||||
# js inheritance
|
||||
if f != 'qweb-test-extend.xml'
|
||||
if cls.matcher.match(f)
|
||||
)
|
||||
@classmethod
|
||||
def qweb_test_file_path(cls):
|
||||
path = os.path.dirname(
|
||||
openerp.modules.get_module_resource(
|
||||
'web', 'static', 'lib', 'qweb', 'qweb2.js'))
|
||||
return path
|
||||
|
||||
def __getattr__(self, item):
|
||||
if not item.startswith('test_qweb_'):
|
||||
raise AttributeError("No {} on {}".format(item, self))
|
||||
|
||||
f = 'qweb-test-{}.xml'.format(item[10:])
|
||||
path = self.qweb_test_file_path()
|
||||
|
||||
return lambda: self.run_test_file(os.path.join(path, f))
|
||||
|
||||
def run_test_file(self, path):
|
||||
context = openerp.addons.base.ir.ir_qweb.QWebContext(self.cr, self.uid, {})
|
||||
qweb = self.env['ir.qweb']
|
||||
doc = etree.parse(path).getroot()
|
||||
qweb.load_document(doc, None, context)
|
||||
for template in context.templates:
|
||||
if template.startswith('_'): continue
|
||||
param = doc.find('params[@id="{}"]'.format(template))
|
||||
# OrderedDict to ensure JSON mappings are iterated in source order
|
||||
# so output is predictable & repeatable
|
||||
params = {} if param is None else json.loads(param.text, object_pairs_hook=collections.OrderedDict)
|
||||
|
||||
ctx = context.copy()
|
||||
ctx.update(params)
|
||||
result = doc.find('result[@id="{}"]'.format(template)).text
|
||||
self.assertEqual(
|
||||
qweb.render(template, qwebcontext=ctx).strip(),
|
||||
(result or u'').strip().encode('utf-8'),
|
||||
template
|
||||
)
|
||||
|
||||
def load_tests(loader, suite, _):
|
||||
# can't override TestQWeb.__dir__ because dir() called on *class* not
|
||||
# instance
|
||||
suite.addTests(TestQWeb.get_cases())
|
||||
return suite
|
||||
|
|
Loading…
Reference in New Issue