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 还为各种数据点提供了其他一些变量:
Warning
$as 将被替换为传递给 t-as 的名称
$as_all(已弃用)被迭代的对象
Note
此变量仅在 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 (attribute,属性)指令实现的,它有 3 种不同形式:
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>
Tip
格式化字符串有两种等价语法:
"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 的输出,或与 “body”(而非 t-value 或 t-valuef)一起使用的 t-set 的输出。
Python¶
通常您无需太过在意:在合理的情况下,相关 API 应会自动生成”安全”内容,一切应能透明地工作。
不过对于需要更明确说明的情况,以下 API 会输出安全内容,这些内容在注入模板时默认不会(重新)转义:
HTML 字段。html_escape()和markupsafe.escape()(它们互为别名,不存在双重转义的风险)。html_sanitize()。markupsafe.Markup。Warning
markupsafe.Markup是一个不安全的 API,它是一种 断言,表示您希望内容对标记安全,但无法对此进行必要的检查,应谨慎使用。to_text()不会将内容标记为安全,但也不会从安全内容中剥离该信息。
强制双重转义¶
如果内容已标记为安全,但出于某种原因仍需要转义(例如打印 HTML 字段的标记),只需将其转换回普通字符串即可”剥离”安全标志,例如在 Python 中使用 str(content),在 Javascript 中使用 String(content)。
Note
由于 Markup 是比 Markup 丰富得多的类型,某些操作会剥离 Markup 的安全信息,但不会剥离 Markup 的。例如在 Python 中进行字符串拼接('' + content)会得到一个 Markup,另一个操作数已被正确转义;而在 Javascript 中则会得到一个 String,另一个操作数在拼接前 并未 被转义。
已弃用的输出指令¶
escout的别名,最初会对其输入进行 HTML 转义。尚未正式弃用,因为out与esc之间的唯一区别仅在于后者含义略显模糊/不正确。rawout的一个版本,绝不 转义其内容。无论内容是否安全,都会原样输出。Deprecated since version 15.0: 请改用
out配合markupsafe.Markup值。t-raw之所以被弃用,是因为随着 生成 内容的代码不断演进,很难追踪它是否会被用于标记,这会导致审查更复杂、疏漏更危险。
Python¶
独占指令¶
资源包¶
“智能记录”字段格式化¶
t-field 指令只能在”智能”记录(browse 方法的返回结果)上执行字段访问(a.b)时使用。它能够根据字段类型自动格式化,并集成在网站的富文本编辑中。
t-options 可用于自定义字段,最常见的选项是 widget,其他选项取决于字段或 widget。
调试¶
t-debug值为空时会调用内建函数
breakpoint(),它通常会启动一个调试器(默认为pdb)。该行为可通过
PYTHONBREAKPOINT或sys.breakpointhook()进行配置。
渲染缓存:¶
t-cache="key_cache" 将模板的一部分标记为在渲染时缓存。所有子指令只会在第一次渲染时被调用。这意味着在渲染这些子指令期间执行的 SQL 查询也只会执行一次。
t-nocache="documentation" 将模板的一部分标记为每次都渲染。该内容只能使用根值。
为什么以及何时使用 t-cache?¶
该指令通过缓存最终文档的部分内容来加速渲染,从而可能节省数据库查询。不过应当节制使用,因为 t-cache 不可避免地会使模板变得更复杂(例如对 t-set 的理解)。
不过,为了真正节省数据库查询,可能需要使用惰性求值的值来渲染模板。如果这些惰性值用在被缓存的部分中,当该部分在缓存中可用时,它们将不会被求值。
t-cache 指令适用于其值仅依赖少量数据的模板部分。我们建议使用性能分析器分析模板的渲染(激活”添加 qweb 指令上下文”选项)。在控制器中向渲染传递惰性值,可以让您查看使用这些值的指令并触发查询。
使用这种缓存的一个顾虑是要让不同用户都能使用它(不同用户应以相同方式渲染被缓存的部分)。另一个潜在问题是在必要时使缓存条目失效。对于后者,应明智地选择 键表达式。例如,使用记录集的 write_date 可以让缓存键自动过期,而无需显式将其从缓存中丢弃。
还应注意 t-cache 部分中的值是有作用域的。这意味着如果模板的这一部分中存在 t-set 指令,其后续内容的渲染结果可能与没有 t-cache 指令时不同。
如果 t-cache 里面还有一个 t-cache 会怎样?¶
各部分都会被缓存,每个部分只包含与其渲染结果对应的字符串。因此,里面的 t-cache 可能较少被读取,其缓存键不一定会被使用。如果必须如此,那么您可能需要添加 t-nocache 在同一节点或父节点上)。
t-nocache 有什么用途?¶
当您想用 t-cache 缓存模板的一部分,但其中一小块必须保持动态并在读取缓存时求值时,可以使用它。不过,t-nocache 中的部分将无法访问模板的 t-set 值,在那里只能访问控制器提供的值。例如,菜单会被缓存,因为它始终相同且渲染耗时(使用带有 qweb 上下文的性能开发者工具可以进行排查)。但在菜单中,我们希望电子商务购物车始终保持最新,因此使用 t-nocache 让这部分保持动态。
t-cache 的基础¶
t-cache 指令允许您存储模板的渲染结果。键表达式 (例如 42:t-cache="42")将作为 Python 表达式求值,并用于生成 缓存键。因此,同一个模板部分可以有不同的 缓存值 (缓存的渲染部分)。如果 键表达式 是元组或列表,则在生成 缓存键 时会对其进行遍历查找。如果 键表达式 返回一个或多个记录集,则会使用模型、id 及其对应的 write_date 来生成 缓存键。特殊情况:如果 键表达式 返回假值(Falsy),则内容不会被缓存。
示例:
<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>
在这种情况下,缓存中可能存在与真值条件下已返回的每条记录对应的值(字符串),以及假值条件对应的值。而且,如果某个模块修改了记录,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</inside>
<inside>2</inside>
<outside>1</inside>
</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></titlle>
<content t-out="record.description"/>
</article>
</section>
将渲染(counter = 1):
<section>
<article>
<title>The record name <i>(views: 1)</i></titlle>
<content>Record description</content>
</article>
</section>
这里包含容器的 <i> 标签将始终被渲染,而其余部分则作为单个字符串存入缓存。
t-nocache 与作用域根值(t-set、t-foreach…)¶
t-nocache 标签的内容可用于文档说明,解释为什么要添加该指令。值在 t-nocache 中具有作用域,这些值仅限根值(控制器提供的值,和/或调用 ir.qweb 的 _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></titlle>
<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></titlle>
<content>Record description</content>
</article>
<footer>(views: 10)</footer>
</section>
这里包含容器的 <i> 标签将始终被渲染,而其余部分则作为单个字符串存入缓存。t-nocache 之外的 t-set 不会更新该计数器。
t-nocache-* 在缓存中添加一些原始类型的值¶
为了能够使用模板中生成的值,可以将它们缓存起来。该指令的用法为 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-cache="records" 的缓存模板部分一起被缓存,并每次都添加到作用域根值中。
辅助方法¶
基于请求¶
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 或 external id 渲染 QWeb 视图/模板。模板会自动从
ir.qweb记录加载。_prepare_environment方法会在渲染上下文中设置许多默认值。http_routing和website插件也会设置它们所需的默认值。您可以像公开方法render一样使用minimal_qcontext=False选项来避免这些默认值:request当前的
Request对象(如有)debug当前请求(如有)是否处于
debug模式quote_plusURL 编码工具函数
json对应的标准库模块
time对应的标准库模块
datetime对应的标准库模块
- relativedelta
参见该模块
keep_querykeep_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 指令。在 primary 模式下,它将作为新建模板的名称;否则它将作为注释添加到转换后的模板上,以帮助追溯继承关系。
至于继承本身,修改是通过 xpath 指令完成的。完整的可用指令集请参见 XPATH 文档。
primary 继承(子模板):
<t t-name="child.template" t-inherit="base.template" t-inherit-mode="primary">
<xpath expr="//ul" position="inside">
<li>new element</li>
</xpath>
</t>
extension 继承(就地转换):
<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 组合使用时表现为 primary 继承,单独使用时表现为 extension 继承。
在这两种情况下,随后都使用任意数量的 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节点的主体应为任意数量的
attribute元素,每个元素都有一个name属性和一些文本内容,上下文节点的指定属性将被设置为给定的值(若已存在则替换,否则添加)- 无操作
若未指定
t-operation,模板主体将被视为 javascript 代码执行,并以上下文节点作为thisWarning
虽然比其他操作强大得多,但该模式也更难调试和维护,建议避免使用
调试¶
javascript 版 QWeb 实现提供了一些调试钩子:
t-log接受一个表达式参数,在渲染过程中求值该表达式,并用
console.log记录其结果:<t t-set="foo" t-value="42"/> <t t-log="foo"/>
会在控制台打印
42t-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>
辅助方法¶
API¶
- class QWeb2.Engine()¶
QWeb 的“渲染器”,处理 QWeb 的大部分逻辑(加载、解析、编译和渲染模板)。
Odoo Web 会在 core 模块中为用户实例化一个,并将其导出为
core.qweb。它还会将各模块的所有模板文件加载到该 QWeb 实例中。QWeb2.Engine还充当“模板命名空间”。- QWeb2.Engine.QWeb2.Engine.render(template[, context])¶
将先前加载的模板渲染为字符串,使用
context如提供)来查找模板渲染期间访问的变量(例如要显示的字符串)。- 参数
template (
String()) – 要渲染的模板的名称context (
Object()) – 模板渲染所用的基础命名空间
- 返回
字符串
引擎还暴露了另一个方法,在某些情况下可能有用(例如需要独立的模板命名空间时——在 Odoo Web 中,看板视图拥有自己的
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¶
一个
Function。如存在,会在将每个 DOM 节点编译为模板代码之前调用。在 Odoo Web 中,它用于自动翻译模板中的文本内容和部分属性。默认为null。