QWeb 模板

QWeb 是 Odoo 使用的主要 templating 引擎2。它是一种 XML 模板引擎1,主要用于生成 HTML 片段和页面。

模板指令以带 t- 前缀的 XML 属性形式指定,例如用于 条件判断 的 t-if,而元素和其他属性则直接渲染。

为避免渲染元素,还提供了一个占位元素 <t>,它执行自身的指令,但本身不会产生任何输出:

<t t-if="condition">
    <p>Test</p>
</t>

结果将会是:

<p>Test</p>

如果 condition 为真,则结果如下,但如果写成:

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

循环

QWeb 有一个迭代指令 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>

与条件类似,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 的上下文之外存在,则在 foreach 结束时其值会被复制到全局上下文中。

<t t-set="existing_variable" t-value="False"/>
<!-- existing_variable now 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 and new_variable now True -->
</p>

<!-- existing_variable always True -->
<!-- new_variable undefined -->

attributes

QWeb 可以即时计算属性,并将计算结果设置到输出节点上。这通过 t-att (属性)指令完成,该指令有三种不同形式:

t-att-$name

创建一个名为 $name 的属性,对属性值求值,并将结果设置为该属性的值::

<div t-att-a="42"/>

将渲染为::

<div a="42"></div>
t-attf-$name

与上一条相同,但参数是 格式字符串 而不仅仅是表达式,通常用于混合字面量与非字面量字符串(例如类名)::

<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 {{code}}" (即 jinja 风格)和 "plain_text #{code}" (即 ruby 风格)。

t-att=mapping

如果参数是映射,则每个(键、值)对都会生成一个新属性及其值::

<div t-att="{'a': 1, 'b': 2}"/>

将渲染为::

<div a="1" b="2"></div>
t-att=pair

如果参数是一个对(2 个元素的元组或数组),则该对的第一个元素是属性名,第二个元素是值::

<div t-att="['a', 'b']"/>

将渲染为::

<div a="b"></div>

设置变量

QWeb 允许在模板内部创建变量,以缓存计算结果(以便多次使用)、为某段数据取更清晰的名称,等等。

这通过 set 指令完成,该指令接受要创建的变量名称。要设置的值可以通过两种方式提供:

  • 包含表达式的 t-value 属性,其求值结果将被设置::

    <t t-set="foo" t-value="2 + 1"/>
    <t t-out="foo"/>
    

    将打印 3

  • 如果没有 t-value 属性,则渲染节点主体并将其设置为变量的值::

    <t t-set="foo">
        <li>ok</li>
    </t>
    <t t-out="foo"/>
    

调用子模板

QWeb 模板可用于顶层渲染,但也可以使用 t-call 指令在另一个模板内部使用(以避免重复或为模板的各个部分命名)::

<t t-call="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>。

然而,这样做的问题是它在 t-call 之外也可见。另一种做法是,在 call 指令主体中设置的内容会在调用子模板 之前 求值,并且可以修改局部上下文::

<t t-call="other-template">
    <t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->

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 的输出,或与“主体”一起使用的 t-set (相对于 t-value 或 t-valuef)。

Python

通常你无需太过在意:有意义的 API 应自动生成“安全”内容,一切应当透明地工作。

对于需要更明确的情形,以下 API 会输出安全内容,这些内容在注入模板时默认不会被(再次)转义:

  • HTML 字段。

  • html_escape() 与 markupsafe.escape() (两者是别名,没有双重转义的风险)。

  • html_sanitize()。

  • markupsafe.Markup。

    警告

    markupsafe.Markup 是不安全的 API,它是一种 断言,断言你希望内容对标记安全,但必然无法检查这一点,因此应谨慎使用。

  • to_text() 不会将内容标记为安全,但也不会从安全内容中剥离该信息。

强制双重转义

如果内容被标记为安全,但出于某种原因仍需要转义(例如打印 HTML 字段的标记),只需将其转换回普通字符串即可“剥离”安全标志,例如 Python 中的 str(content) 和 Javascript 中的 String(content)。

注解

由于 Markup 是比 :js Markup 丰富得多的类型,某些操作会从 :js Markup 中剥离安全信息,但不会从 Markup 中剥离。例如在 Python 中拼接字符串('' + content)会得到 Markup,其中另一个操作数已被正确转义;而在 Javascript 中则会得到 :js String,其中另一个操作数在拼接前 未 被转义。

Python

专有指令

资源包

“智能记录”字段格式化

t-field 指令只能在对“智能”记录(browse 方法的结果)执行字段访问(a.b)时使用。它能够根据字段类型自动格式化,并且已集成到网站的富文本编辑中。

t-options 可用于自定义字段,最常见的选项是 widget,其他选项取决于字段或小组件。

调试

t-debug

值为空时,会调用 breakpoint() 内置函数,该函数通常会调用调试器(默认为 pdb)。

该行为可通过 PYTHONBREAKPOINT 或 sys.breakpointhook() 进行配置。

辅助函数

基于请求

QWeb 在 Python 端的大多数用途是在控制器中(以及 HTTP 请求期间),此时可以通过调用 odoo.http.HttpRequest.render() 轻松渲染存储在数据库中的模板(作为 视图):

response = http.request.render('my-template', {
    'context_value': 42
})

这会自动创建一个 Response 对象,可从控制器返回(或进一步定制以符合需要)。

基于视图

比上一个辅助函数更深一层的是 ir.qweb 上的 _render 方法(使用数据库)以及公共模块方法 render (不使用数据库):

_render(id[, values])

按数据库 ID 或 外部标识符 渲染 QWeb 视图/模板。模板会从 ir.qweb 记录中自动加载。

_prepare_environment 方法会在渲染上下文中设置一些默认值。http_routing 和 website 插件模块也会提供它们所需的默认值。你可以使用 minimal_qcontext=False 选项来避免这些默认值,就像公共方法 render 那样:

request

当前的 Request 对象(如果有)

debug

当前请求(如果有)是否处于 debug 模式

quote_plus

URL 编码工具函数

json

对应的标准库模块

time

对应的标准库模块

datetime

对应的标准库模块

relativedelta

参见模块

keep_query

keep_query 辅助函数

参数
  • values – 传递给 QWeb 用于渲染的上下文值

  • engine (str) – 用于渲染的 Odoo 模型名称,可用于在本地扩展或自定义 QWeb(通过基于 ir.qweb 创建带修改的“新” qweb)

render(template_name, values, load, **options)
load(ref)()

返回 etree 对象,即 ref

Javascript

专有指令

定义模板

t-name 指令只能放在模板文件的顶层(文档根的直接子元素)::

<templates>
    <t t-name="template-name">
        <!-- template code -->
    </t>
</templates>

它不接受其他参数,但可以与 <t> 元素或任何其他元素一起使用。使用 <t> 元素时,<t> 应只有一个子元素。

模板名称是任意字符串,不过当多个模板相互关联时(例如被调用的子模板),习惯上使用以点分隔的名称来表示层级关系。

模板继承

模板继承用于以下两种情况:
  • 就地修改现有模板,例如向模板中添加信息

由其他模块创建的模板。
  • 基于给定的父模板创建新模板

模板继承通过使用两个指令来完成:
  • t-inherit,即要继承的模板名称;

  • t-inherit-mode,即继承的行为:可设为 primary 以基于父模板创建新的子模板,或设为 extension 以就地修改父模板。

还可以指定可选的 t-name 指令。如果在主模式下使用,它将是新建模板的名称;否则将作为注释添加到转换后的模板中,以帮助追溯继承关系。

继承本身的修改通过 xpaths 指令完成。完整可用指令集请参阅 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-extend 指令与 t-name 组合使用时作为主继承,单独使用时作为扩展继承。

在两种情况下,修改都通过任意数量的 t-jquery 子指令来完成::

<t t-extend="base.template">
    <t t-jquery="ul" t-operation="append">
        <li>new element</li>
    </t>
</t>

t-jquery 指令的参数是一个 CSS selector :该选择器在扩展模板上用于选取 上下文节点,指定的 t-operation 会应用到这些节点上:

append

节点主体追加到上下文节点的末尾(位于上下文节点的最后一个子节点之后)

prepend

节点主体插入到上下文节点的开头(位于上下文节点的第一个子节点之前)

before

节点主体插入到上下文节点的正前方

after

节点主体插入到上下文节点的正后方

inner

节点主体替换上下文节点的子节点

replace

节点主体用于替换上下文节点本身

attributes

节点主体应为任意数量的 attribute 元素,每个元素带有一个 name 属性以及一些文本内容,上下文节点中对应名称的属性将被设置为指定的值(如果已存在则替换,否则新增)

无操作

如果未指定 t-operation,则模板主体会被解释为 JavaScript 代码,并以上下文节点作为 this 执行

警告

虽然此模式比其他操作强大得多,但也更难调试和维护,建议避免使用

调试

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 代码。它接受一个 context 参数,渲染上下文将以该名称在 t-js 主体中可用:

<t t-set="foo" t-value="42"/>
<t t-js="ctx">
    console.log("Foo is", ctx.foo);
</t>

辅助函数

core.qweb

(core 即 web.core 模块)一个 :js QWeb2.Engine 实例,已加载所有模块定义的模板文件,并引用了标准辅助对象 _ (下划线)、_t (翻译函数)以及 JSON。

core.qweb.render 可用于轻松渲染基本的模块模板

API

class QWeb2.Engine()

QWeb 的“渲染器”,负责处理 QWeb 的大部分逻辑(加载、解析、编译和渲染模板)。

Odoo Web 在 core 模块中为用户实例化一个,并将其导出为 core.qweb。它还会将各个模块的所有模板文件加载到该 QWeb 实例中。

:js QWeb2.Engine 还充当“模板命名空间”。

QWeb2.Engine.QWeb2.Engine.render(template[, context])

将先前加载的模板渲染为字符串,并使用 context (如果提供)来查找模板渲染期间访问的变量(例如要显示的字符串)。

参数
  • template (String()) – 要渲染的模板的名称

  • context (Object()) – 用于模板渲染的基础命名空间

退货

字符串

引擎还提供了另一个在某些情况下可能有用的方法(例如,如果你需要一个独立的模板命名空间:在 Odoo Web 中,看板视图会拥有自己的 :js QWeb2.Engine 实例,这样它们的模板就不会与更通用的“模块”模板冲突):

QWeb2.Engine.QWeb2.Engine.add_template(templates)

在 QWeb 实例中加载一个模板文件(一组模板的集合)。模板可以按以下方式指定:

XML 字符串

QWeb 会尝试将其解析为 XML 文档,然后加载它。

一个 URL

QWeb 会尝试下载该 URL 的内容,然后加载得到的 XML 字符串。

一个 Document 或 Node

QWeb 会遍历文档的第一层(所提供根节点的子节点),并加载其中任何命名模板或模板覆盖。

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

一个 Function。如果存在,则在将每个 DOM 节点编译为模板代码之前调用。在 Odoo Web 中,它用于自动翻译模板中的文本内容以及某些属性。默认为 null。

1

在这一点上它与 Genshi 类似,尽管它不使用(也不支持) XML namespaces

2

尽管它使用了其他几个模板引擎,可能是出于历史原因,也可能是因为它们仍然更契合该用例。Odoo 9.0 仍然依赖 Jinja 和 Mako。