QWeb Templates
QWeb 是 Odoo 使用的主要模板引擎。它是一个基于 XML 的模板引擎,主要用于生成 HTML 片段和页面。
模板指令通过以 t-
为前缀的 XML 属性来指定,例如用于条件判断的 t-if
。模板中的元素和其他属性将被直接渲染。
为了避免某些元素被渲染,QWeb 还提供了一个占位元素 <t>
,它会执行相应的指令,但本身不会在输出中生成任何内容:
<t t-if="condition">
<p>Test</p>
</t>
最终输出为:
<p>Test</p>
如果condition
为 True:
<div t-if="condition">
<p>Test</p>
</div>
最终输出为:
<div>
<p>Test</p>
</div>
数据输出
QWeb 的输出指令 out
会自动对其输入内容进行 HTML 转义,从而在显示用户提供的内容时,减少跨站脚本攻击(XSS)风险。
out
接收一个表达式,对其进行求值,并将结果插入到文档中:
<p><t t-out="value"/></p>
使用值 value
设置为 42
渲染后结果为:
<p>42</p>
请参见 高级输出 了解更高级的主题(例如注入原始 HTML 等)。
条件判断
QWeb 有一个条件指令 if
,用于计算作为属性值传入的表达式:
<div>
<t t-if="condition">
<p>ok</p>
</t>
</div>
当条件为真时,该元素会被渲染:
<div>
<p>ok</p>
</div>
但如果条件为假,该元素会从结果中移除:
<div></div>
条件渲染适用于携带该指令的元素,这个元素不一定非得是 <t>
标签:
<div>
<p t-if="condition">ok</p>
</div>
这将产生与之前示例相同的结果。
还有额外的条件分支指令 t-elif
和 t-else
可用:
<div>
<p t-if="user.birthday == today()">Happy birthday!</p>
<p t-elif="user.login == 'root'">Welcome master!</p>
<p t-else="">Welcome!</p>
</div>
循环(Loops)
QWeb 提供了一个迭代指令 t-foreach
,它接受一个返回要迭代集合的表达式,以及一个参数 t-as
,用于指定迭代中“当前项”的名称:
<t t-foreach="[1, 2, 3]" t-as="i">
<p><t t-out="i"/></p>
</t>
这将被渲染为:
<p>1</p>
<p>2</p>
<p>3</p>
和条件语句类似,t-foreach
作用于带有该属性的元素:
<p t-foreach="[1, 2, 3]" t-as="i">
<t t-out="i"/>
</p>
这与上面的示例效果相同。
foreach
可以迭代数组(当前项为当前值)或者映射(当前项为当前键)。迭代整数(等价于迭代从 0(包含)到该整数(不包含)的数组)仍然支持,但已不推荐使用。
除了通过 t-as
传递的名称外,foreach
还提供了其他一些变量来表示各种数据点:
警告
$as
会被替换成传递给 t-as
的变量名。
$as_all
(已废弃)
- 表示被迭代的对象。
注意:
此变量仅在 JavaScript 版本的 QWeb 中可用,Python 版本不可用。
$as_value
- 当前迭代的值。对于列表和整数,等同于
$as
;但对于映射(对象),提供的是当前值(而$as
是当前键)。
$as_index
- 当前迭代的索引,从0开始。
$as_size
- 集合的大小(如果可用)。
$as_first
- 当前项是否为第一个元素(等同于
$as_index == 0
)。
$as_last
- 当前项是否为最后一个元素(等同于
$as_index + 1 == $as_size
),需要迭代对象能提供大小信息。
$as_parity
(已废弃)
- 当前迭代轮次的奇偶性,值为
"even"
或"odd"
。
$as_even
(已废弃)
- 布尔值,表示当前迭代是否为偶数索引。
$as_odd
(已废弃)
- 布尔值,表示当前迭代是否为奇数索引。
这些额外变量只在 foreach
的作用域内有效。如果在 foreach
外使用相同变量名,会复制 foreach
结束时的值到全局上下文。
<t t-set="existing_variable" t-value="False"/>
<!-- existing_variable 现在是 False -->
<p t-foreach="[1, 2, 3]" t-as="i">
<t t-set="existing_variable" t-value="True"/>
<t t-set="new_variable" t-value="True"/>
<!-- existing_variable 和 new_variable 都是 True -->
</p>
<!-- existing_variable 始终是 True -->
<!-- new_variable 未定义 -->
属性Attribute
QWeb 允许动态计算属性值,并将结果设置到输出节点的属性中。通过 t-att
指令实现,有三种形式:
t-att-$name
- 创建名为
$name
的属性,计算属性值并赋值:
<div t-att-a="42"/>
- 渲染为:
<div a="42"></div>
t-attf-$name
- 类似于上面,但参数是格式化字符串(format string),适合混合文字和代码(比如动态class):
<t t-foreach="[1, 2, 3]" t-as="item">
<li t-attf-class="row {{ (item_index % 2 === 0) ? 'even' : 'odd' }}">
<t t-out="item"/>
</li>
</t>
- 渲染为:
<li class="row even">1</li>
<li class="row odd">2</li>
<li class="row even">3</li>
小技巧:
格式化字符串有两种等效语法:"plain_text "
(Jinja风格) "plain_text #{code}"
(Ruby风格)
t-att=mapping
- 如果参数是一个映射,每个(键,值)对都会生成一个新的属性及其对应的值:
<div t-att="{'a': 1, 'b': 2}"/>
- 将渲染为:
<div a="1" b="2"></div>
t-att=pair
- 如果参数是一个键值对(元组或包含两个元素的数组),键值对的第一个元素作为属性名,第二个元素作为属性值:
<div t-att="['a', 'b']"/>
- 将渲染为:
<div a="b"></div>
变量设置
QWeb 允许在模板内创建变量,用来缓存计算结果(以便多次使用)、给数据赋予更清晰的名称等。
这通过set
指令实现,需要指定变量名。变量的值可以通过两种方式提供:
使用
t-value
属性,赋值为表达式,表达式的计算结果将被设置为变量值:xml<t t-set="foo" t-value="2 + 1"/> <t t-out="foo"/>
输出为 3
如果没有
t-value
属性,则渲染该节点的内容,并将渲染结果作为变量值:xml<t t-set="foo"> <li>ok</li> </t> <t t-out="foo"/>
调用子模板
QWeb 模板可以用于顶层渲染,也可以在另一个模板内调用(避免重复或为模板部分命名),通过 t-call
指令:
<t t-call="other-template"/>
这会使用父模板的执行上下文调用名为 other-template
的模板。如果 other-template
定义为:
<p><t t-value="var"/></p>
上面的调用将渲染成 <p/>
(无内容);但如果:
<t t-set="var" t-value="1"/>
<t t-call="other-template"/>
则渲染为 <p>1</p>
。
不过这样会导致变量 var
在 t-call
外部可见。另一种做法是把内容放在 t-call
的节点体中,这些内容会在调用子模板前计算,并可以改变局部上下文:
<t t-call="other-template">
<t t-set="var" t-value="1"/>
</t>
<!-- 这里 var 不存在 -->
t-call
的节点体可以非常复杂(不仅限于 set 指令),它渲染后的内容可以在被调用模板中通过特殊变量 0
访问:
<div>
This template was called with content:
<t t-out="0"/>
</div>
如果调用时写成:
<t t-call="other-template">
<em>content</em>
</t>
则渲染结果是:
<div>
This template was called with content:
<em>content</em>
</div>
高级输出
默认情况下,out
会对需要转义的内容进行 HTML 转义,从而保护系统免受 XSS 攻击。
不需要转义的内容则会按原样注入文档,可能成为文档实际标记的一部分。
唯一跨平台“安全”的内容是 t-call 输出的内容,或者使用“body”
形式(非 t-value
或 t-valuef
)的 t-set
生成的内容。
Python 部分
通常你无需过多关心:适合生成安全内容的 API 会自动完成,保证透明地工作。
但对于需要明确处理的情况,以下 API 会输出默认不会被(重新)转义的安全内容:
html_escape()
和markupsafe.escape()
(它们是别名,不会导致双重转义)html_sanitize()
markupsafe.Markup
⚠️ 警告
markupsafe.Markup
是不安全的 API,它只是声明你希望内容是安全的标记,但并不能完全验证,需谨慎使用。to_text()
不会标记内容为安全,但也不会剥夺已有内容的安全标记。
强制双重转义
如果内容被标记为安全,但你需要强制转义(例如打印 HTML 字段的标记),可以通过转成普通字符串来“移除”安全标记,例如 Python 中的 str(content)
,JavaScript 中的 String(content)
。
注解
由于 Markup
类型比 Markup()
更丰富,有些操作会移除 Markup()
的安全信息但不会移除 Markup
的。例如 Python 中的字符串拼接 '' + content
会生成一个正确转义的 Markup
,而在 JavaScript 中会生成普通字符串,另一操作数未转义。
已废弃的输出指令
esc
- 是
out
的别名,原本也会对输入进行 HTML 转义。尚未正式废弃,主要区别是esc
的语义不太明确或有误。
raw
是
out
的不转义版本,无论内容是否安全,都会按原样输出。自版本 15.0 起已废弃,建议改用带有
markupsafe.Markup
值的out
。t-raw
被废弃的原因是随着代码演进,追踪内容是否用于标记变得困难,导致代码审查复杂且风险增加。
Python 部分
专属指令(Exclusive directives)
资源包(Asset bundles)
智能记录(smart records)字段格式化
t-field
指令只能用于访问“智能记录”(即 browse
方法的返回结果)的字段(如 a.b
)。它会根据字段类型自动进行格式化,并且集成在网站的富文本编辑功能中。
t-options
可用于自定义字段,最常见的选项是 widget
,其他选项取决于字段类型或控件类型。
调试
t-debug
当其值为空时,调用 Python 的内置函数breakpoint(),通常会触发调试器(默认是 pdb)。
此行为可通过环境变量 PYTHONBREAKPOINT 或 sys.breakpointhook() 来配置。
渲染缓存
t-cache="key_cache"
- 该指令用于将模板的某部分内容在渲染时进行缓存。所有子指令只会在第一次渲染时执行一次,包括任何 SQL 查询。
t-nocache="documentation"
- 用于标记模板中每次都需要重新渲染的部分。该部分内容只能使用控制器提供的根值。
为什么以及何时使用 t-cache
?
t-cache
指令用于加快模板的渲染速度,通过缓存最终文档的部分内容,从而减少对数据库的查询请求。但是,这个指令应当谨慎使用,因为它会增加模板的复杂度(比如会影响 t-set
的使用和理解)。
不过,要真正节省数据库查询,通常需要将模板用**惰性求值(lazy evaluation)**的方式渲染。如果这些惰性值被用于已缓存的部分,那么当缓存命中时,它们就不会被重新求值。
t-cache
特别适用于那些依赖于少量数据的模板部分。建议通过性能分析器(启用 “Add qweb directive context” 选项)来分析模板渲染过程。在控制器中传入惰性值可以触发查询,并使这些值可被 t-cache
的部分使用。
使用缓存的一个问题是确保不同用户可以以相同的方式渲染被缓存的部分。另一个问题是如何在需要时使缓存失效。为此,应慎重选择缓存键表达式。例如,使用记录集的 write_date
字段可以让缓存在数据变更时自动过期,无需手动移除。
此外,还需要注意的是,t-cache
中的变量是有作用域的。这意味着,如果在该缓存部分中使用了 t-set
指令,那么模板中后续内容的渲染结果,可能会因是否使用了 t-cache
而不同。
以下是这段有关 t-cache
和 t-nocache
的中文翻译,已尽量保持准确性和语义连贯性:
如果一个 t-cache
里嵌套了另一个 t-cache
会怎样?
每个 t-cache
指令对应的部分都会被缓存,并仅包含其渲染对应的字符串。因此,内部的 t-cache
很可能被更少地读取,它的缓存键不一定会被使用。如果你希望它始终被评估并缓存,可能需要在当前节点或其父节点上添加 t-nocache
。
t-nocache
是做什么用的?
如果你希望使用 t-cache
缓存模板的一部分,但其中的某一小部分必须保持动态,并在每次渲染时重新计算,那么可以使用 t-nocache
。然而,t-nocache
中的部分无法访问模板中通过 t-set
设置的值,只能访问控制器中传递过来的值。
例如,菜单通常被缓存(因为它总是相同的,而且渲染较慢——你可以通过性能工具与 QWeb 上下文调试),但我们希望菜单中的电商购物车部分始终是最新的,因此这一部分使用 t-nocache
保持动态。
t-cache
的基础原理
t-cache
指令允许你缓存模板的渲染结果。其 key
表达式(例如 t-cache="42"
)会作为 Python 表达式求值,并用于生成缓存键。这样,同一个模板部分可能根据不同的键生成多个缓存值。
如果键是一个元组或列表,它将作为缓存键的一部分进行处理。如果表达式返回一个或多个记录集(recordset),则会使用其模型、ID 和 write_date
生成缓存键。
**特殊情况:**如果键表达式返回一个“Falsy 值”(如 False
、None
、空字符串等),该内容将不会被缓存。
示例:
<div t-cache="record, bool(condition)">
<span t-if="condition" t-field="record.partner_id.name"/>
<span t-else="" t-field="record.partner_id" t-options-widget="contact"/>
</div>
在这个例子中,对于每个 record
和不同的 condition
(真或假),缓存中可能存在一个对应的值(字符串)。如果某个模块修改了该 record
,由于 write_date
被更新了,对应的缓存值也会失效。
t-cache
与作用域变量(如 t-set
, t-foreach
...)
在 t-cache
中,值是有作用域的。这意味着在模板某个父节点上是否添加了 t-cache
,可能会影响后续部分的渲染逻辑。别忘了,Odoo 的模板系统使用了大量的 t-call
和视图继承,添加一个 t-cache
可能会影响你在当前文件中看不到的模板结构。
t-foreach
类似于t-set
,每次迭代都会设置变量。
示例:
<div>
<t t-set="a" t-value="1"/>
<inside>
<t t-set="a" t-value="2"/>
<t t-out="a"/>
</inside>
<outside t-out="a"/>
<t t-set="b" t-value="1"/>
<inside t-cache="True">
<t t-set="b" t-value="2"/>
<t t-out="b"/>
</inside>
<outside t-out="b"/>
</div>
渲染结果:
<div>
<inside>2</inside>
<outside>2</outside>
<inside>2</inside>
<outside>1</outside>
</div>
t-nocache
的基础原理
带有 t-nocache
属性的节点不会被缓存,它的内容是动态的,每次渲染都会重新计算。但该部分只能访问控制器提供的变量(即调用 _render
方法时传入的值)。
示例:
<section>
<article t-cache="record">
<title><t t-out="record.name"/> <i t-nocache="">(views: <t t-out="counter"/>)</i></title>
<content t-out="record.description"/>
</article>
</section>
当 counter = 1
时的渲染结果:
<section>
<article>
<title>The record name <i>(views: 1)</i></title>
<content>Record description</content>
</article>
</section>
在这里,<i>
标签内的内容始终动态渲染,其余部分作为字符串被缓存。
t-nocache
与作用域变量(t-set
、t-foreach
...)
t-nocache
标签中的内容可以用于文档说明,也可解释为何使用了该指令。但其内部只能访问“根作用域变量”(即控制器传入的或 _render()
调用时提供的值)。
t-set
可在其内部使用,但这些值无法在其他地方访问。
示例:
<section>
<t t-set="counter" t-value="counter * 10"/>
<header t-nocache="">
<t t-set="counter" t-value="counter + 5"/>
(views: <t t-out="counter"/>)
</header>
<article t-cache="record">
<title><t t-out="record.name"/> <i t-nocache="">(views: <t t-out="counter"/>)</i></title>
<content t-out="record.description"/>
</article>
<footer>(views: <t t-out="counter"/>)</footer>
</section>
当 counter = 1
时的渲染结果:
<section>
<header>
(views: 6)
</header>
<article>
<title>The record name <i>(views: 1)</i></title>
<content>Record description</content>
</article>
<footer>(views: 10)</footer>
</section>
说明:
<i>
标签内容始终动态渲染。- 模板中其他部分(例如
<footer>
) 保留的是原始作用域中的counter
值。
t-nocache-*
:在缓存中加入原始值(primitive values)
为了在模板中使用动态生成的值,并希望在缓存中复用这些值,可以使用 t-nocache-*="expr"
。这里的 *
是变量名,expr
是要缓存的 Python 表达式,返回的值必须是基本类型(如整数、字符串、布尔值等)。
示例:
<section t-cache="records">
<article t-foreach="records" t-as="record">
<header>
<title t-field="record.get_method_title()"/>
</header>
<footer t-nocache="This part has a dynamic counter and must be rendered all the time."
t-nocache-cached_value="record.get_base_counter()">
<span t-out="counter + cached_value"/>
</footer>
</article>
</section>
cached_value
会作为缓存模板的一部分,并在每次渲染时添加进作用域中的根变量,供 t-nocache
部分使用。
以下是你提供内容的中文翻译,涵盖了 QWeb 的辅助方法(Helpers)和两种主要使用方式:基于请求和基于视图的渲染:
辅助方法(Helpers)
基于请求(Request-based)
在 Python 代码中使用 QWeb 最常见的场景是控制器中(即 HTTP 请求期间)。此时,可以通过调用 odoo.http.HttpRequest.render()
来轻松渲染存储于数据库中的模板(视图):
response = http.request.render('my-template', {
'context_value': 42
})
这个方法会自动生成一个 Response 对象,你可以直接在控制器中返回它,或者进一步自定义以满足你的需求。
基于视图(View-based)
比起上面的方法更底层的是 ir.qweb
的 _render
方法(使用数据库模板)以及公共模块方法 render
(不使用数据库模板):
_render(id[, values])
此方法通过数据库中的 ID 或外部 ID 渲染一个 QWeb 视图/模板。模板将自动从
ir.qweb
记录中加载。在渲染上下文中,
_prepare_environment
方法会设置一些默认值。http_routing
和website
模块也会添加它们各自所需的默认值。如果你不希望使用这些默认值,可以使用
minimal_qcontext=False
选项(与公共方法render
一致)。request
当前的 Request 对象(如果有的话)
debug
当前请求(如果有的话)是否处于调试模式
quote_plus
URL 编码实用函数
对应的标准库模块
对应的标准库模块
对应的标准库模块
详见该模块
keep_query
keep_query
辅助函数参数
values
: 要传递给 QWeb 用于渲染的上下文值(context values)。engine
(str) : 用于渲染的 Odoo 模型名称,可以用来在本地扩展或自定义 QWeb(通过基于ir.qweb
创建一个“新的” qweb 并进行修改)render(template_name, values, load, **options)
load(ref)()
返回值:
etree
对象,ref
JavaScript
专属指令(Exclusive directives)
定义模板(Defining templates)
t-name
指令只能放置在模板文件的顶层(即 <templates>
的直接子元素):
<templates>
<t t-name="template-name">
<!-- 模板代码 -->
</t>
</templates>
它不接受其他参数,但可以与 <t>
元素或其他任意元素一起使用。若与 <t>
元素一起使用,该 <t>
元素应仅包含一个子节点。
模板名称是任意的字符串,不过当多个模板相互关联(例如子模板调用)时,通常使用点分隔命名方式来表示其层级关系。
模板继承(Template inheritance)
模板继承用于以下两种情况之一:
- 就地修改现有模板,例如为其他模块创建的模板添加信息。
- 从给定的父模板创建一个新模板。
模板继承是通过以下两个指令完成的:
t-inherit
:要继承的模板的名称。t-inherit-mode
:继承的行为方式。可以设置为:primary
:从父模板创建新的子模板;extension
:就地修改父模板。
可以选择添加 t-name
指令:
- 如果继承模式为
primary
,那么它将作为新创建模板的名称; - 如果为
extension
,则该名称将被添加为转换后模板的注释,便于追踪继承链。
继承本身的修改通过 XPath 指令完成。完整的指令请参阅 XPath 文档。
主继承(创建子模板)示例:
<t t-name="child.template" t-inherit="base.template" t-inherit-mode="primary">
<xpath expr="//ul" position="inside">
<li>new element</li>
</xpath>
</t>
扩展继承(就地修改)示例:
<t t-inherit="base.template" t-inherit-mode="extension">
<xpath expr="//tr[1]" position="after">
<tr><td>new cell</td></tr>
</xpath>
</t>
旧继承机制(已弃用)
模板继承也可以通过 t-extend
指令完成,该指令以要修改的模板名称作为参数。
- 若结合
t-name
使用,则t-extend
表现为主继承; - 若单独使用,则表现为扩展继承。
在这两种情况下,更改通过若干 t-jquery
子指令完成:
<t t-extend="base.template">
<t t-jquery="ul" t-operation="append">
<li>new element</li>
</t>
</t>
t-jquery
指令接受一个 CSS 选择器。该选择器将作用于被扩展的模板,用于选择上下文节点,对其应用指定的 t-operation
操作:
append
:将当前节点的内容附加到上下文节点的尾部(在其最后一个子节点之后)。prepend
:将内容插入上下文节点的头部(在其第一个子节点之前)。before
:将内容插入到上下文节点之前。after
:将内容插入到上下文节点之后。inner
:替换上下文节点的子节点内容。replace
:完全替换上下文节点本身。attributes
:当前节点的内容应为若干个具有name
属性的<attribute>
元素,属性名称和值将设置到上下文节点上(已存在则替换,不存在则新增)。
无操作(No operation)
如果未指定 t-operation
,则模板主体将被解释为 JavaScript 代码,并在上下文节点作为 this
的环境下执行。
⚠️ 警告:
虽然该模式功能强大,但也更难调试和维护,因此建议避免使用。
以下是你提供内容的原封不动翻译:
调试(debugging)
JavaScript 版 QWeb 实现提供了一些调试钩子:
t-log
接受一个表达式参数,在渲染时计算该表达式,并通过 console.log
输出其结果:
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
会在控制台打印 42
t-debug
在模板渲染时触发调试断点:
<t t-if="a_test">
<t t-debug=""/>
</t>
如果调试已激活,则会暂停执行(具体条件取决于浏览器及其开发工具)
t-js
节点的内容是渲染时执行的 JavaScript 代码。接受一个上下文参数,该参数为渲染上下文在 t-js
代码中的名称:
<t t-set="foo" t-value="42"/>
<t t-js="ctx">
console.log("Foo is", ctx.foo);
</t>
帮助函数(Helpers)
core.qweb
(core 是 web.core
模块)是一个 QWeb2.Engine()
实例,加载了所有模块定义的模板文件,并包含对标准辅助对象 _
(underscore)、_t
(翻译函数)和 JSON 的引用。
可以使用 core.qweb.render
方便地渲染基础模块模板。
API
class QWeb2.Engine()
QWeb 的“渲染器”,负责 QWeb 的大部分逻辑(加载、解析、编译和渲染模板)。
Odoo Web 在 core 模块中为用户实例化一个该类,并导出为 core.qweb
。它还加载了各模块的所有模板文件到该 QWeb 实例中。
一个 QWeb2.Engine()
也作为一个“模板命名空间”。
QWeb2.Engine.QWeb2.Engine.render(template[, context])
使用 context
(如有)渲染已加载模板为字符串,context
用于提供模板渲染时访问的变量(如待显示的字符串)。
参数:
template
(字符串):要渲染的模板名称。context
(对象):用于模板渲染的基本命名空间。
返回:
- 字符串
该引擎还暴露了另一个方法,在某些情况下可能有用(例如如果你需要单独的模板命名空间,在 Odoo Web 中,Kanban 视图拥有自己的 QWeb2.Engine()
实例,以防其模板与更通用的“模块”模板冲突):
QWeb2.Engine.QWeb2.Engine.add_template(templates)
向 QWeb 实例加载模板文件(模板集合)。模板可指定为:
XML 字符串 QWeb 会尝试将其解析为 XML 文档然后加载。
URL QWeb 会尝试下载该 URL 的内容,然后加载得到的 XML 字符串。
Document 或 Node QWeb 会遍历文档的第一层节点(提供根的子节点),加载任何命名的模板或模板覆盖。
QWeb2.Engine()
还暴露了用于行为定制的多个属性:
QWeb2.Engine.QWeb2.Engine.prefix
解析期间识别指令的前缀字符串,默认是t.
。QWeb2.Engine.QWeb2.Engine.debug
布尔标志,将引擎置为“调试模式”。通常,QWeb 会捕获模板执行期间抛出的任何错误;调试模式下,不捕获异常,允许异常冒泡。QWeb2.Engine.QWeb2.Engine.jQuery
模板继承处理时使用的 jQuery 实例,默认是window.jQuery
。QWeb2.Engine.QWeb2.Engine.preprocess_node
一个函数。如果存在,在编译每个 DOM 节点为模板代码前调用。在 Odoo Web 中,该函数用于自动翻译模板中的文本内容和部分属性。默认值为null
。
[1] 它与 Genshi 类似,但不使用(且不支持)XML 命名空间。
[2] 不过它使用一些其他的模板引擎,或因历史原因,或因其更适合该用例。Odoo 9.0 仍依赖 Jinja 和 Mako