In case where display_name is null, the widget contact crash - AttributeError: 'bool' object has no attribute 'split' On odoo.com we have around 3K of partner that have name with null value. So calling so widget Contact on linked res.users will crash since the display name is False closes odoo/odoo#155735 Signed-off-by: Rémy Voet (ryv) <ryv@odoo.com>
3129 lines
117 KiB
Python
3129 lines
117 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import collections
|
|
import json
|
|
import os.path
|
|
import re
|
|
import markupsafe
|
|
|
|
from lxml import etree, html
|
|
from lxml.builder import E
|
|
from copy import deepcopy
|
|
from textwrap import dedent
|
|
|
|
from odoo.tests.common import TransactionCase
|
|
from odoo.addons.base.models.ir_qweb import QWebException, render
|
|
from odoo.tools import misc, mute_logger
|
|
from odoo.tools.json import scriptsafe as json_scriptsafe
|
|
from odoo.exceptions import UserError, ValidationError, MissingError
|
|
|
|
unsafe_eval = eval
|
|
|
|
|
|
class TestQWebTField(TransactionCase):
|
|
def setUp(self):
|
|
super(TestQWebTField, self).setUp()
|
|
self.env_branding = self.env(context={'inherit_branding': True})
|
|
self.engine = self.env_branding['ir.qweb']
|
|
|
|
def test_trivial(self):
|
|
field = etree.Element('span', {'t-field': 'company.name'})
|
|
company = self.env['res.company'].create({'name': "My Test Company"})
|
|
|
|
result = self.engine._render(field, {'company': company})
|
|
self.assertEqual(
|
|
etree.fromstring(result),
|
|
etree.fromstring('<span data-oe-model="res.company" data-oe-id="%d" '
|
|
'data-oe-field="name" data-oe-type="char" '
|
|
'data-oe-expression="company.name">%s</span>' % (
|
|
company.id,
|
|
"My Test Company",
|
|
)),
|
|
)
|
|
|
|
def test_i18n(self):
|
|
field = etree.Element('span', {'t-field': 'company.name'})
|
|
s = "Testing «ταБЬℓσ»: 1<2 & 4+1>3, now 20% off!"
|
|
company = self.env['res.company'].create({'name': s})
|
|
|
|
result = self.engine._render(field, {'company': company})
|
|
self.assertEqual(
|
|
etree.fromstring(result),
|
|
etree.fromstring('<span data-oe-model="res.company" data-oe-id="%d" '
|
|
'data-oe-field="name" data-oe-type="char" '
|
|
'data-oe-expression="company.name">%s</span>' % (
|
|
company.id,
|
|
misc.html_escape(s),
|
|
)),
|
|
)
|
|
|
|
def test_reject_crummy_tags(self):
|
|
field = etree.Element('td', {'t-field': 'company.name'})
|
|
|
|
with self.assertRaisesRegex(QWebException, r'QWeb widgets do not work correctly'):
|
|
self.engine._render(field, {'company': None})
|
|
|
|
def test_reject_t_tag(self):
|
|
field = etree.Element('t', {'t-field': 'company.name'})
|
|
|
|
with self.assertRaisesRegex(QWebException, r't-field can not be used on a t element'):
|
|
self.engine._render(field, {'company': None})
|
|
|
|
def test_render_t_options(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy"><root><span t-esc="5" t-options="{'widget': 'char'}" t-options-widget="'float'" t-options-precision="4"/></root></t>
|
|
"""
|
|
})
|
|
text = etree.fromstring(self.env['ir.qweb']._render(view1.id)).find('span').text
|
|
self.assertEqual(text, '5.0000')
|
|
|
|
def test_xss_breakout(self):
|
|
view = self.env['ir.ui.view'].create({
|
|
'name': 'dummy', 'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<root>
|
|
<script type="application/javascript">
|
|
var s = <t t-esc="json.dumps({'key': malicious})"/>;
|
|
</script>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
rendered = self.env['ir.qweb']._render(view.id, {'malicious': '1</script><script>alert("pwned")</script><script>'})
|
|
self.assertIn('alert', rendered, "%r doesn't seem to be rendered" % rendered)
|
|
doc = etree.fromstring(rendered)
|
|
self.assertEqual(len(doc.xpath('//script')), 1)
|
|
|
|
def test_default_value(self):
|
|
Partner = self.env['res.partner']
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="out-field-default">
|
|
<div t-field="record.name">
|
|
DEFAULT
|
|
<t t-out="'Text'" />
|
|
</div>
|
|
</t>''',
|
|
})
|
|
|
|
# record.name is non-empty
|
|
result = """
|
|
<div>My Company</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {
|
|
'record': Partner.new({'name': 'My Company'})
|
|
})
|
|
self.assertEqual(str(rendered.strip()), result.strip(), "")
|
|
|
|
# record.name is empty but not False or None, we should render depending on force_display
|
|
result = """
|
|
<div></div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {
|
|
'record': Partner.new({'name': ''})
|
|
})
|
|
self.assertEqual(str(rendered.strip()), result.strip())
|
|
|
|
# record.name is False or None, we should render field default value
|
|
result = """
|
|
<div>
|
|
DEFAULT
|
|
Text
|
|
</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {
|
|
'record': Partner.new({})
|
|
})
|
|
self.assertEqual(str(rendered.strip()), result.strip())
|
|
|
|
def test_no_value_no_default_value(self):
|
|
# no value, no default value with attributes on t-field
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="out-field-default">
|
|
<div t-field="record.name"/>
|
|
</t>''',
|
|
})
|
|
result = """
|
|
<div data-oe-xpath="/t[1]/div[1]" data-oe-model="res.partner" data-oe-field="name" data-oe-type="char" data-oe-expression="record.name"></div>
|
|
"""
|
|
# inherit_branding puts attribute on the field tag as well as force the display in case the field is empty
|
|
rendered = self.env['ir.qweb'].with_context(inherit_branding=True)._render(t.id, {
|
|
'record': self.env['res.partner'].new({}),
|
|
})
|
|
self.assertEqual(str(rendered.strip()), result.strip())
|
|
|
|
|
|
class TestQWebNS(TransactionCase):
|
|
def test_render_static_xml_with_namespace(self):
|
|
""" Test the rendering on a namespaced view with no static content. The resulting string should be untouched.
|
|
"""
|
|
expected_result = """
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
<f:table xmlns:f="http://www.example.org/furniture">
|
|
<f:width>80</f:width>
|
|
</f:table>
|
|
</root>
|
|
"""
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">%s</t>
|
|
""" % expected_result
|
|
})
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), etree.fromstring(expected_result))
|
|
|
|
def test_render_static_xml_with_namespace_2(self):
|
|
""" Test the rendering on a namespaced view with no static content. The resulting string should be untouched.
|
|
"""
|
|
expected_result = """
|
|
<html xmlns="http://www.w3.org/HTML/1998/html4" xmlns:xdc="http://www.xml.com/books">
|
|
<head>
|
|
<title>Book Review</title>
|
|
</head>
|
|
<body>
|
|
<xdc:bookreview>
|
|
<xdc:title>XML: A Primer</xdc:title>
|
|
<table>
|
|
<tr align="center">
|
|
<td>Author</td><td>Price</td>
|
|
<td>Pages</td><td>Date</td>
|
|
</tr>
|
|
<tr align="left">
|
|
<td><xdc:author>Simon St. Laurent</xdc:author></td>
|
|
<td><xdc:price>31.98</xdc:price></td>
|
|
<td><xdc:pages>352</xdc:pages></td>
|
|
<td><xdc:date>1998/01</xdc:date></td>
|
|
</tr>
|
|
</table>
|
|
</xdc:bookreview>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">%s</t>
|
|
""" % expected_result
|
|
})
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), etree.fromstring(expected_result))
|
|
|
|
def test_render_static_xml_with_useless_distributed_namespace(self):
|
|
""" Test that redundant namespaces are stripped upon rendering.
|
|
"""
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr xmlns:h="http://www.example.org/table">
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td xmlns:h="http://www.example.org/table">Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
</root>
|
|
""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
|
|
|
|
def test_render_static_xml_with_namespace_3(self):
|
|
expected_result = """
|
|
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd"></cfdi:Comprobante>
|
|
"""
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">%s</t>
|
|
""" % expected_result
|
|
})
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), etree.fromstring(expected_result))
|
|
|
|
def test_render_static_xml_with_namespace_dynamic(self):
|
|
""" Test the rendering on a namespaced view with dynamic URI (need default namespace uri).
|
|
"""
|
|
tempate = """
|
|
<root xmlns:h="https://default.namespace.url/h">
|
|
<h:table t-att="{'xmlns:h': h1}">
|
|
<h:tr>
|
|
<h:td t-att="{'xmlns:h': h2}">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
</root>
|
|
"""
|
|
expected_result = """
|
|
<root xmlns:h="https://default.namespace.url/h">
|
|
<h:table xmlns:h="%(h1)s">
|
|
<h:tr>
|
|
<h:td xmlns:h="%(h2)s">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
</root>
|
|
"""
|
|
|
|
values = dict(h1="http://www.example.org/table", h2="http://www.w3.org/TD/html4/")
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">%s</t>
|
|
""" % tempate
|
|
})
|
|
|
|
rendering = self.env['ir.qweb']._render(view1.id, values)
|
|
|
|
self.assertEqual(etree.fromstring(rendering), etree.fromstring(expected_result % values))
|
|
|
|
def test_render_static_xml_with_namespace_dynamic_2(self):
|
|
""" Test the rendering on a namespaced view with dynamic URI (need default namespace uri).
|
|
Default URIs must be differents.
|
|
"""
|
|
tempate = """
|
|
<root xmlns:f="https://default.namespace.url/f" xmlns:h="https://default.namespace.url/h" >
|
|
<h:table t-att="{'xmlns:h': h1}">
|
|
<h:tr>
|
|
<h:td t-att="{'xmlns:h': h2}">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
<f:table t-att="{'xmlns:f': f}">
|
|
<f:width>80</f:width>
|
|
</f:table>
|
|
</root>
|
|
"""
|
|
expected_result = """
|
|
<root xmlns:f="https://default.namespace.url/f" xmlns:h="https://default.namespace.url/h">
|
|
<h:table xmlns:h="%(h1)s">
|
|
<h:tr>
|
|
<h:td xmlns:h="%(h2)s">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
<f:table xmlns:f="%(f)s">
|
|
<f:width>80</f:width>
|
|
</f:table>
|
|
</root>
|
|
"""
|
|
|
|
values = dict(h1="http://www.example.org/table", h2="http://www.w3.org/TD/html4/", f="http://www.example.org/furniture")
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">%s</t>
|
|
""" % tempate
|
|
})
|
|
|
|
rendering = self.env['ir.qweb']._render(view1.id, values)
|
|
|
|
self.assertEqual(etree.fromstring(rendering), etree.fromstring(expected_result % values))
|
|
|
|
def test_render_dynamic_xml_with_namespace_t_esc(self):
|
|
""" Test that rendering a template containing a node having both an ns declaration and a t-esc attribute correctly
|
|
handles the t-esc attribute and keep the ns declaration.
|
|
"""
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" t-esc="'test'"/>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">test</Invoice>""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
|
|
|
|
def test_render_dynamic_xml_with_namespace_t_esc_with_useless_distributed_namespace(self):
|
|
""" Test that rendering a template containing a node having both an ns declaration and a t-esc attribute correctly
|
|
handles the t-esc attribute and keep the ns declaration, and distribute correctly the ns declaration to its children.
|
|
"""
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" t-attf-test="test">
|
|
<cac:Test xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">blabla</cac:Test>
|
|
</Invoice>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""
|
|
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" test="test">
|
|
<cac:Test>blabla</cac:Test>
|
|
</Invoice>
|
|
""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
|
|
|
|
def test_render_dynamic_xml_with_namespace_t_attf(self):
|
|
""" Test that rendering a template containing a node having both an ns declaration and a t-attf attribute correctly
|
|
handles the t-attf attribute and keep the ns declaration.
|
|
"""
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
<f:table xmlns:f="http://www.example.org/furniture">
|
|
<f:width t-attf-test="1">80</f:width>
|
|
</f:table>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
<f:table xmlns:f="http://www.example.org/furniture">
|
|
<f:width test="1">80</f:width>
|
|
</f:table>
|
|
</root>
|
|
""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
|
|
|
|
def test_render_dynamic_xml_with_namespace_t_attf_with_useless_distributed_namespace(self):
|
|
""" Test that rendering a template containing a node having both an ns declaration and a t-attf attribute correctly
|
|
handles the t-attf attribute and that redundant namespaces are stripped upon rendering.
|
|
"""
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
<f:table xmlns:f="http://www.example.org/furniture">
|
|
<f:width xmlns:f="http://www.example.org/furniture" t-attf-test="1">80</f:width>
|
|
</f:table>
|
|
</root>
|
|
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
<f:table xmlns:f="http://www.example.org/furniture">
|
|
<f:width test="1">80</f:width>
|
|
</f:table>
|
|
</root>
|
|
|
|
""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
|
|
|
|
def test_render_dynamic_xml_with_namespace_2(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<cbc:UBLVersionID t-esc="version_id"/>
|
|
<t t-foreach="[1, 2, 3, 4]" t-as="value">
|
|
Oasis <cac:Test t-esc="value"/>
|
|
</t>
|
|
</Invoice>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""
|
|
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<cbc:UBLVersionID>1.0</cbc:UBLVersionID>
|
|
|
|
Oasis <cac:Test>1</cac:Test>
|
|
|
|
Oasis <cac:Test>2</cac:Test>
|
|
|
|
Oasis <cac:Test>3</cac:Test>
|
|
|
|
Oasis <cac:Test>4</cac:Test>
|
|
|
|
</Invoice>
|
|
""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id, {'version_id': 1.0})), expected_result)
|
|
|
|
def test_render_static_xml_with_namespaced_attributes(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
|
|
|
|
def test_render_dynamic_xml_with_namespaced_attributes(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd" t-esc="'abc'"/>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>""")
|
|
|
|
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
|
|
|
|
def test_render_static_xml_with_t_call(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<cac:fruit xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<cac:table>
|
|
<cbc:td>Appel</cbc:td>
|
|
<cbc:td>Pineappel</cbc:td>
|
|
</cac:table>
|
|
</cac:fruit>
|
|
</t>
|
|
"""
|
|
})
|
|
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
|
|
"VALUES ('dummy', 'ir.ui.view', %s, 'base')", [view1.id])
|
|
|
|
# view2 will t-call view1
|
|
view2 = self.env['ir.ui.view'].create({
|
|
'name': "dummy2",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy2">
|
|
<root xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<cac:line t-foreach="[1, 2]" t-as="i" t-call="base.dummy"/>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
result = self.env['ir.qweb']._render(view2.id)
|
|
result_etree = etree.fromstring(result)
|
|
|
|
# check that the root tag has all its xmlns
|
|
expected_ns = {
|
|
(None, 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'),
|
|
('cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'),
|
|
('cbc', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'),
|
|
}
|
|
self.assertEqual(set(result_etree.nsmap.items()), expected_ns)
|
|
|
|
# check that the t-call did its work
|
|
cac_lines = result_etree.findall('.//cac:line', namespaces={'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'})
|
|
self.assertEqual(len(cac_lines), 2)
|
|
self.assertEqual(result.count('Appel'), 2)
|
|
|
|
# check that the t-call dit not output again the xmlns declaration
|
|
self.assertEqual(result.count('xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"'), 1)
|
|
|
|
def test_render_static_xml_with_extension(self):
|
|
""" Test the extension of a view by an xpath expression on a ns prefixed element.
|
|
"""
|
|
# primary view
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
# extension patching the primary view
|
|
view2 = self.env['ir.ui.view'].create({
|
|
'name': "dummy_ext",
|
|
'type': 'qweb',
|
|
'inherit_id': view1.id,
|
|
'arch': """
|
|
<xpath expr="//{http://www.example.org/table}table/{http://www.example.org/table}tr">
|
|
<h:td xmlns:h="http://www.example.org/table">Oranges</h:td>
|
|
</xpath>
|
|
"""
|
|
})
|
|
|
|
expected_result = etree.fromstring("""
|
|
<root>
|
|
<h:table xmlns:h="http://www.example.org/table">
|
|
<h:tr>
|
|
<h:td xmlns:h="http://www.w3.org/TD/html4/">Apples</h:td>
|
|
<h:td>Bananas</h:td>
|
|
<h:td>Oranges</h:td>
|
|
</h:tr>
|
|
</h:table>
|
|
</root>
|
|
""")
|
|
|
|
self.assertEqual(
|
|
etree.fromstring(self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id])._render(view1.id)),
|
|
expected_result
|
|
)
|
|
|
|
def test_render_dynamic_xml_with_code_error(self):
|
|
""" Test that, when rendering a template containing a namespaced node
|
|
that evaluates code with errors, the proper exception is raised
|
|
"""
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<Invoice xmlns:od="http://odoo.com/od">
|
|
<od:name t-att-test="'a' + 1"/>
|
|
</Invoice>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
error_msg = None
|
|
try:
|
|
"" + 0
|
|
except TypeError as e:
|
|
error_msg = e.args[0]
|
|
|
|
with self.assertRaises(QWebException, msg=error_msg):
|
|
self.env['ir.qweb']._render(view1.id)
|
|
|
|
class TestQWebBasic(TransactionCase):
|
|
def test_compile_expr(self):
|
|
tests = [
|
|
#pylint: disable=C0326
|
|
# source, values, result
|
|
("1 +2+ 3", {}, 6),
|
|
("(((1 +2+ 3)))", {}, 6),
|
|
("(1) +(2+ (3))", {}, 6),
|
|
("a == 5", {'a': 5}, True),
|
|
("{'a': True}", {}, {'a': True}),
|
|
("object.count(1)", {'object': [1, 2, 1 ,1]}, 3),
|
|
("dict(a=True)", {}, {'a': True}),
|
|
("fn(a=11, b=22) or a", {'a': 1, 'fn': lambda a,b: 0}, 1),
|
|
("fn(a=11, b=22) or a", {'a': 1, 'fn': lambda a,b: b}, 22),
|
|
("(lambda a: a)(5)", {}, 5),
|
|
("(lambda a: a[0])([5])", {}, 5),
|
|
("(lambda test: len(test))('aaa')", {}, 3),
|
|
("{'a': lambda a: a[0], 'b': 3}['a']([5])", {}, 5),
|
|
("list(map(lambda a: a[0], r))", {'r': [(1,11), (2,22)]}, [1, 2]),
|
|
("z + (head or 'z')", {'z': 'a'}, "az"),
|
|
("z + (head or 'z')", {'z': 'a', 'head': 'b'}, "ab"),
|
|
("{a:b for a, b in [(1,11), (2, 22)]}", {}, {1: 11, 2: 22}),
|
|
("any({x == 2 for x in [1,2,3]})", {}, True),
|
|
("any({x == 5 for x in [1,2,3]})", {}, False),
|
|
("{x:y for x,y in [('a', 11),('b', 22)]}", {}, {'a': 11, 'b': 22}),
|
|
("[(y,x) for x,y in [(1, 11),(2, 22)]]", {}, [(11, 1), (22, 2)]),
|
|
("(lambda a: a + 5)(x)", {'x': 10}, 15),
|
|
("(lambda a: a + x)(5)", {'x': 10}, 15),
|
|
("sum(x for x in range(4)) + ((x))", {'x': 10}, 16),
|
|
("['test_' + x for x in ['a', 'b']]", {}, ['test_a', 'test_b']),
|
|
("""1 and 2 and 0
|
|
or 9""", {}, 9),
|
|
]
|
|
|
|
IrQweb = self.env['ir.qweb']
|
|
for expr, q_values, result in tests:
|
|
expr_namespace = IrQweb._compile_expr(expr)
|
|
|
|
compiled = compile("""def test(values):\n values['result'] = %s""" % expr_namespace, '<test>', 'exec')
|
|
globals_dict = IrQweb._IrQWeb__prepare_globals()
|
|
values = {}
|
|
unsafe_eval(compiled, globals_dict, values)
|
|
test = values['test']
|
|
|
|
test(q_values)
|
|
q_result = dict(q_values, result=result)
|
|
self.assertDictEqual(q_values, q_result, "Should compile: %s" % expr)
|
|
|
|
def test_foreach_as_error_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="iter-list">
|
|
<t t-foreach="[3, 2, 1]">
|
|
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
|
|
</t>'''
|
|
})
|
|
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(t.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(t.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn("KeyError: 't-as'", error)
|
|
self.assertIn('<t t-foreach="[3, 2, 1]"/>', error)
|
|
|
|
def test_foreach_as_error_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="iter-list">
|
|
<t t-foreach="[3, 2, 1]" t-as="">
|
|
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
|
|
</t>'''
|
|
})
|
|
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(t.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(t.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn("KeyError: 't-as'", error)
|
|
self.assertIn('<t t-foreach="[3, 2, 1]" t-as=""/>', error)
|
|
|
|
def test_foreach_as_error_3(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="iter-list">
|
|
<t t-foreach="[3, 2, 1]" t-as="b-2">
|
|
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
|
|
</t>'''
|
|
})
|
|
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(t.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(t.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn("The varname 'b-2' can only contain alphanumeric characters and underscores", error)
|
|
self.assertIn('<t t-foreach="[3, 2, 1]" t-as="b-2"/>', error)
|
|
|
|
def test_compile_expr_security(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-escaping">
|
|
<div>
|
|
<t t-set="o" t-value="(lambda a=open: a)()"/>
|
|
<t t-out="o('/etc/passwd').read()"/>
|
|
</div>
|
|
</t>'''
|
|
})
|
|
values = {'other': 'any value'}
|
|
with self.assertRaises(Exception): # NotImplementedError for 'lambda a=open' and Undefined value 'open'.
|
|
self.env['ir.qweb']._render(t.id, values)
|
|
|
|
def test_foreach_iter_list(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="iter-list">
|
|
<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 = """
|
|
[0: 3 3]
|
|
[1: 2 2]
|
|
[2: 1 1]
|
|
"""
|
|
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_foreach_iter_dict(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="iter-dict">
|
|
<t t-foreach="{'a': 3, 'b': 2, 'c': 1}" t-as="item">
|
|
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
[0: a 3]
|
|
[1: b 2]
|
|
[2: c 1]
|
|
"""
|
|
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_att_escaping_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-escaping">
|
|
<div t-att-bibi="json.dumps(bibi)">1</div>
|
|
<div t-att-toto="toto">2</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div bibi="{"a": "string", "b": 1}">1</div>
|
|
<div toto="a'b"c">2</div>
|
|
"""
|
|
values = {'json': json_scriptsafe, 'bibi': dict(a='string', b=1), 'toto': "a'b\"c"}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_att_escaping_2(self):
|
|
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-escaping">
|
|
<t t-set="abc"> <t t-if="add_abc"><t t-out="add_abc"/> <span a="b"> | </span></t><t t-out="efg"/> </t>
|
|
<div t-att-abc="abc">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div abc=" &#34;yes&#34; <span a="b"> | </span>-efg- ">123</div>
|
|
"""
|
|
values = {'add_abc': '"yes"', 'efg': '-efg-'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_attf_escaping_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-escaping">
|
|
<div t-attf-bibi="a, b > c > #{d}">1</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div bibi="a, b > c > a' > b"c">1</div>
|
|
"""
|
|
values = {'d': "a' > b\"c"}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_attf_escaping_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-escaping">
|
|
<a t-attf-href="/link/#{ url }/#{other and 'sub'}">link</a>
|
|
<a t-attf-href="/link/#{ url }/#{(not other) and 'sub'}">link2</a>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<a href="/link/odoo/sub">link</a>
|
|
<a href="/link/odoo/">link2</a>
|
|
"""
|
|
values = {'url': 'odoo', 'other': True}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_attf_escaping_3(self):
|
|
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-escaping">
|
|
<div t-attf-abc="abc #{val} { other }">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div abc="abc "yes" { other }">123</div>
|
|
"""
|
|
values = {'val': '"yes"'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_att_no_propagation_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="bibi">
|
|
<div t-foreach="[1, 2]" t-as="v" class="toto"/>
|
|
<t class="remove_me" t-set="data">a</t>
|
|
<div t-out="data"/>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div class="toto"></div><div class="toto"></div>
|
|
<div>a</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_att_no_propagation_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="bibi">
|
|
<section>
|
|
<div t-foreach="[1, 2]" t-as="v">
|
|
<span t-att-test="v" t-esc="v_index"/>
|
|
</div>
|
|
<div t-foreach="[1, 2]" t-as="v" class="o">
|
|
<span t-att-test="v" t-esc="v_index"/>
|
|
</div>
|
|
</section>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<section>
|
|
<div>
|
|
<span test="1">0</span>
|
|
</div>
|
|
<div>
|
|
<span test="2">1</span>
|
|
</div>
|
|
<div class="o">
|
|
<span test="1">0</span>
|
|
</div>
|
|
<div class="o">
|
|
<span test="2">1</span>
|
|
</div>
|
|
</section>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(etree.fromstring(rendered), etree.fromstring(result))
|
|
|
|
def test_set_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set="a" t-value="'abc %s' % 1"/>
|
|
<div t-out="a"/>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>abc 1</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_set_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set="a" t-valuef="abc {{1}}"/>
|
|
<div t-out="a"/>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>abc 1</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_set_3(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set='{"a": "abc %s" % 1,
|
|
"b": 2}'/>
|
|
<div t-out="a"/>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>abc 1</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_set_body_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set="abc"> <span a="b"> [%s] </span> </t>
|
|
<div t-att-abc="abc % add_abc">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div abc=" <span a="b"> [&#34;yes&#34;] </span> ">123</div>
|
|
"""
|
|
values = {'add_abc': '"yes"'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_set_body_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set="abc"> <span a="b"> toto </span> </t>
|
|
<div t-att-abc="'[%s]' % abc">123</div>
|
|
<div class="a1" t-out="abc"/>
|
|
<div class="a2" t-out="'[%s]' % abc"/>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div abc="[ <span a="b"> toto </span> ]">123</div>
|
|
<div class="a1"> <span a="b"> toto </span> </div>
|
|
<div class="a2">[ <span a="b"> toto </span> ]</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_set_error_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set="" t-value="1"/>
|
|
</t>'''
|
|
})
|
|
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(t.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(t.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn("KeyError: 't-set'", error)
|
|
self.assertIn('<t t-set="" t-value="1"/>', error)
|
|
|
|
def test_set_error_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set="b-2" t-value="1"/>
|
|
</t>'''
|
|
})
|
|
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(t.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(t.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn("The varname can only contain alphanumeric characters and underscores", error)
|
|
self.assertIn('<t t-set="b-2" t-value="1"/>', error)
|
|
|
|
def test_out(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="out-format"><div t-out="a">Default</div></t>'''
|
|
})
|
|
result = """<div>1</div>"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {'a': 1})
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_format_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="out-format">
|
|
<t t-set="final_message">Powered by %s%s</t>
|
|
<div t-out="final_message % (a, b and ('-%s' % b) or '')"/>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>Powered by 1-2</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {'a': 1, 'b': 2})
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_format_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="set">
|
|
<t t-set="toto">Toto %s</t>
|
|
<t t-set="abc"> <span a="b"> [%s , %s] </span> </t>
|
|
<div t-out="(abc % (add_abc, toto)) % 5">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div> <span a="b"> ["yes" , Toto 5] </span> </div>
|
|
"""
|
|
values = {'add_abc': '"yes"'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_format_3(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-set">
|
|
<t t-set="toto">Toto %s</t>
|
|
<t t-set="abc"> <span a="b"> a </span> </t>
|
|
<div t-out="(toto + abc) % v">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>Toto "yes" <span a="b"> a </span> </div>
|
|
"""
|
|
values = {'v': '"yes"'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_format_4(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-set">
|
|
<t t-set="abc"> <span a="b"> a </span> </t>
|
|
<div t-out="(v + abc)">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>"yes" <span a="b"> a </span> </div>
|
|
"""
|
|
values = {'v': '"yes"'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_format_5(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-set">
|
|
<t t-set="abc"> <span a="b"> a </span> </t>
|
|
<div t-out="(abc + v)">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div> <span a="b"> a </span> "yes"</div>
|
|
"""
|
|
values = {'v': '"yes"'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_format_6(self):
|
|
# Use str method will use the string value. t-out will escape this str
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-set">
|
|
<t t-set="abc"> <span a="b"> a </span> </t>
|
|
<div t-out="(abc.strip() + v)">123</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div><span a="b"> a </span>"yes"</div>
|
|
"""
|
|
values = {'v': '"yes"'}
|
|
rendered = self.env['ir.qweb']._render(t.id, values)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_escape_text(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy"><root><span t-out="text" t-options-widget="'text'"/></root></t>
|
|
"""
|
|
})
|
|
html = self.env['ir.qweb']._render(view1.id, {'text': """a
|
|
b <b>c</b>"""})
|
|
self.assertEqual(html, """<root><span data-oe-type="text" data-oe-expression="text">a<br>
|
|
b <b>c</b></span></root>""")
|
|
|
|
def test_out_markup(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="esc-markup">
|
|
<t t-set="content"><span>toto</span></t>
|
|
<div t-out="content"/>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div><span>toto</span></div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {})
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_out_default_value(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="out-default">
|
|
<span rows="10" t-out="a">
|
|
DEFAULT
|
|
<t t-out="'Text'" />
|
|
</span>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<span rows="10">Hello</span>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {'a': 'Hello'})
|
|
self.assertEqual(str(rendered.strip()), result.strip())
|
|
|
|
result = """
|
|
<span rows="10">
|
|
DEFAULT
|
|
Text
|
|
</span>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id, {})
|
|
self.assertEqual(str(rendered.strip()), result.strip())
|
|
|
|
def test_esc_markup(self):
|
|
# t-esc is equal to t-out
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="esc-markup">
|
|
<t t-set="content"><span>toto</span></t>
|
|
<div t-esc="content"/>
|
|
</t>'''
|
|
})
|
|
ref = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="esc-markup">
|
|
<t t-set="content"><span>toto</span></t>
|
|
<div t-out="content"/>
|
|
</t>'''
|
|
})
|
|
rendered = self.env['ir.qweb']._render(t.id, {})
|
|
result = self.env['ir.qweb']._render(ref.id, {})
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_if_from_body(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="attr-set">
|
|
<t t-set="abc"> <span a="b"> a </span> </t>
|
|
<div t-if="abc">123</div>
|
|
<div t-if="not abc">456</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>123</div>
|
|
"""
|
|
rendered = self.env['ir.qweb']._render(t.id)
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_if_spaces(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="test">
|
|
<div>
|
|
0
|
|
<t>1</t>
|
|
<t t-if="True">2</t>
|
|
<t>3</t>
|
|
4
|
|
<t>5</t>
|
|
6
|
|
<t t-if="True">7</t>
|
|
8
|
|
<t t-if="False">9</t>
|
|
10
|
|
<t t-if="False">11</t>
|
|
<t t-else="">12</t>
|
|
13
|
|
</div>
|
|
</t>'''
|
|
})
|
|
result = """
|
|
<div>
|
|
0
|
|
1
|
|
2
|
|
3
|
|
4
|
|
5
|
|
6
|
|
7
|
|
8
|
|
10
|
|
12
|
|
13
|
|
</div>
|
|
"""
|
|
rendered = str(self.env['ir.qweb']._render(t.id))
|
|
self.assertEqual(rendered.strip(), result.strip())
|
|
|
|
def test_error_message_1(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="test">
|
|
<section>
|
|
<div t-esc="abc + def">
|
|
<span>content</span>
|
|
</div>
|
|
</section>
|
|
</t>'''
|
|
})
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(t.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(t.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn('<div t-esc="abc + def"/>', error)
|
|
|
|
def test_error_message_2(self):
|
|
t = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name="test">
|
|
<section>
|
|
<div t-esc="abc + def + (">
|
|
<span>content</span>
|
|
</div>
|
|
</section>
|
|
</t>'''
|
|
})
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(t.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(t.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn('Can not compile expression', error)
|
|
self.assertIn('<div t-esc="abc + def + ("/>', error)
|
|
|
|
def test_error_message_3(self):
|
|
# The format error tells the developer what to do.
|
|
template = '''<section>
|
|
<div t-esc="1+2">
|
|
<span>content</span>
|
|
</div>
|
|
</section>'''
|
|
with self.assertRaises(ValueError):
|
|
self.env['ir.qweb']._render(template)
|
|
try:
|
|
self.env['ir.qweb']._render(template)
|
|
except ValueError as e:
|
|
self.assertIn('Inline templates must be passed as `etree` documents', str(e))
|
|
|
|
template = '''toto <t t-esc="content"/>'''
|
|
with self.assertRaises(ValueError):
|
|
self.env['ir.qweb']._render(template)
|
|
try:
|
|
self.env['ir.qweb']._render(template)
|
|
except ValueError as e:
|
|
self.assertIn('Inline templates must be passed as `etree` documents', str(e))
|
|
|
|
def test_error_message_4(self):
|
|
# Template record view not found.
|
|
with self.assertRaises(MissingError):
|
|
self.env['ir.qweb']._render(-999)
|
|
try:
|
|
self.env['ir.qweb']._render(-999)
|
|
except MissingError as e:
|
|
self.assertIn('Record does not exist or has been deleted.', str(e))
|
|
|
|
with self.assertRaises(ValueError):
|
|
self.env['ir.qweb']._render('not.wrong_template_xmlid')
|
|
try:
|
|
self.env['ir.qweb']._render('not.wrong_template_xmlid')
|
|
except ValueError as e:
|
|
self.assertIn('External ID not found in the system', str(e))
|
|
|
|
with self.assertRaises(AssertionError):
|
|
self.env['ir.qweb']._render(False)
|
|
try:
|
|
self.env['ir.qweb']._render(False)
|
|
except AssertionError as e:
|
|
self.assertIn('template is required', str(e))
|
|
|
|
def test_error_message_5(self):
|
|
# Error not found a first rendering.
|
|
with self.assertRaises(MissingError, msg="Not Found"):
|
|
self.env['ir.qweb']._render(-9999)
|
|
|
|
@mute_logger('odoo.addons.base.models.ir_qweb') # warning for template not found
|
|
def test_error_message_6(self):
|
|
# Error not found a second rendering (first rendering with option hide this error).
|
|
html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
|
|
self.assertEqual('', html)
|
|
|
|
# re try this rendering without any error (use cached method)
|
|
html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
|
|
self.assertEqual('', html)
|
|
|
|
# re try this rendering but raise (use cached method)
|
|
with self.assertRaises(MissingError, msg="Not Found"):
|
|
self.env['ir.qweb']._render(-9999)
|
|
|
|
def test_error_message_7(self):
|
|
# UserError not found a first rendering.
|
|
with self.assertRaises(UserError, msg="Not Found"):
|
|
self.env['ir.qweb']._render(-9999)
|
|
|
|
@mute_logger('odoo.addons.base.models.ir_qweb') # warning for template not found
|
|
def test_error_message_8(self):
|
|
# UserError not found a second rendering (first rendering with option hide this error).
|
|
html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
|
|
self.assertEqual('', html)
|
|
|
|
# re try this rendering without any error (use cached method)
|
|
html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
|
|
self.assertEqual('', html)
|
|
|
|
# re try this rendering but raise (use cached method)
|
|
with self.assertRaises(UserError, msg="Not Found"):
|
|
self.env['ir.qweb']._render(-9999)
|
|
|
|
def test_call_set(self):
|
|
view0 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<table>
|
|
<tr><td t-out="a"/></tr>
|
|
<t t-set="a">3</t>
|
|
</table>
|
|
</t>
|
|
"""
|
|
})
|
|
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
|
|
"VALUES ('dummy', 'ir.ui.view', %s, 'base')", [view0.id])
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "other",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.other">
|
|
<div>
|
|
<t t-set="a">1</t>
|
|
<t t-set="b">1</t>
|
|
<t t-call="base.dummy">
|
|
<t t-set="b">2</t>
|
|
</t>
|
|
<span t-out="a"/>
|
|
<span t-out="b"/>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
result = self.env['ir.qweb']._render(view1.id, {})
|
|
self.assertEqual(etree.fromstring(result), etree.fromstring("""
|
|
<div>
|
|
<table>
|
|
<tr><td>1</td></tr>
|
|
</table>
|
|
<span>1</span>
|
|
<span>1</span>
|
|
</div>
|
|
"""), 'render t-call use lexical scoping, t-call content use independant scoping')
|
|
|
|
def test_call_error(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "other",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.other">
|
|
<div>
|
|
<t t-call="base.dummy"/>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
with self.assertRaises(QWebException):
|
|
self.env['ir.qweb']._render(view1.id)
|
|
|
|
try:
|
|
self.env['ir.qweb']._render(view1.id)
|
|
except QWebException as e:
|
|
error = str(e)
|
|
self.assertIn('External ID not found in the system: base.dummy', error)
|
|
self.assertIn('<t t-call="base.dummy"/>', error)
|
|
|
|
def test_render_t_call_propagates_t_lang(self):
|
|
current_lang = 'en_US'
|
|
other_lang = 'fr_FR'
|
|
|
|
lang = self.env['res.lang']._activate_lang(other_lang)
|
|
lang.write({
|
|
'decimal_point': '*',
|
|
'thousands_sep': '/'
|
|
})
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "callee",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.callee">
|
|
<t t-esc="9000000.00" t-options="{'widget': 'float', 'precision': 2}" />
|
|
</t>
|
|
"""
|
|
})
|
|
self.env['ir.model.data'].create({
|
|
'name': 'callee',
|
|
'model': 'ir.ui.view',
|
|
'module': 'base',
|
|
'res_id': view1.id,
|
|
})
|
|
|
|
view2 = self.env['ir.ui.view'].create({
|
|
'name': "calling",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.calling">
|
|
<t t-call="base.callee" t-lang="'%s'" />
|
|
</t>
|
|
""" % other_lang
|
|
})
|
|
|
|
rendered = self.env['ir.qweb'].with_context(lang=current_lang)._render(view2.id).strip()
|
|
self.assertEqual(rendered, '9/000/000*00')
|
|
|
|
def test_render_barcode(self):
|
|
partner = self.env['res.partner'].create({
|
|
'name': 'bacode_test',
|
|
'barcode': 'test'
|
|
})
|
|
|
|
view = self.env['ir.ui.view'].create({
|
|
'name': "a_barcode_view",
|
|
'type': 'qweb',
|
|
})
|
|
|
|
view.arch = """<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'width': 100, 'height': 30}"/>"""
|
|
rendered = self.env['ir.qweb']._render(view.id, values={'partner': partner}).strip()
|
|
self.assertRegex(rendered, r'<div><img alt="Barcode test" src="data:image/png;base64,\S+"></div>')
|
|
|
|
partner.barcode = '4012345678901'
|
|
view.arch = """<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'symbology': 'EAN13', 'width': 100, 'height': 30, 'img_style': 'width:100%;', 'img_alt': 'Barcode'}"/>"""
|
|
ean_rendered = self.env['ir.qweb']._render(view.id, values={'partner': partner}).strip()
|
|
self.assertRegex(ean_rendered, r'<div><img style="width:100%;" alt="Barcode" src="data:image/png;base64,\S+"></div>')
|
|
|
|
view.arch = """<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'symbology': 'auto', 'width': 100, 'height': 30, 'img_style': 'width:100%;', 'img_alt': 'Barcode'}"/>"""
|
|
auto_rendered = self.env['ir.qweb']._render(view.id, values={'partner': partner}).strip()
|
|
self.assertRegex(auto_rendered, r'<div><img style="width:100%;" alt="Barcode" src="data:image/png;base64,\S+"></div>')
|
|
|
|
def test_render_comment_tail(self):
|
|
""" Test the rendering of a tail text, near a comment.
|
|
"""
|
|
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': "qweb",
|
|
'arch': """
|
|
<t>
|
|
<!-- it is a comment -->
|
|
<!-- it is another comment -->
|
|
Text 1
|
|
<!-- it is still another comment -->
|
|
Text 2
|
|
<t>ok</t>
|
|
</t>
|
|
"""
|
|
})
|
|
emptyline = '\n '
|
|
expected = markupsafe.Markup('Text 1' + emptyline + emptyline + 'Text 2' + emptyline + 'ok')
|
|
self.assertEqual(self.env['ir.qweb']._render(view1.id).strip(), expected)
|
|
|
|
def test_render_comments(self):
|
|
""" Test the rendering of comments with and without the
|
|
preserve_comments option.
|
|
"""
|
|
comment = '<!-- Hello, world! -->'
|
|
view = self.env['ir.ui.view'].create({
|
|
'name': 'dummy',
|
|
'type': 'qweb',
|
|
'arch': f'<t><p>{comment}</p></t>'
|
|
})
|
|
QWeb = self.env['ir.qweb']
|
|
self.assertEqual(
|
|
QWeb.with_context(preserve_comments=False)._render(view.id),
|
|
markupsafe.Markup('<p></p>'),
|
|
"Should not have the comment")
|
|
self.env.registry.clear_cache('templates')
|
|
self.assertEqual(
|
|
QWeb.with_context(preserve_comments=True)._render(view.id),
|
|
markupsafe.Markup(f'<p>{comment}</p>'),
|
|
"Should have the comment")
|
|
|
|
def test_render_processing_instructions(self):
|
|
""" Test the rendering of processing instructions with and without the
|
|
preserve_comments option.
|
|
"""
|
|
p_instruction = '<?hello world?>'
|
|
view = self.env['ir.ui.view'].create({
|
|
'name': 'dummy',
|
|
'type': 'qweb',
|
|
'arch': f'<t><p>{p_instruction}</p></t>'
|
|
})
|
|
QWeb = self.env['ir.qweb']
|
|
self.assertEqual(
|
|
QWeb.with_context(preserve_comments=False)._render(view.id),
|
|
markupsafe.Markup('<p></p>'),
|
|
"Should not have the processing instruction")
|
|
self.env.registry.clear_cache('templates')
|
|
self.assertEqual(
|
|
QWeb.with_context(preserve_comments=True)._render(view.id),
|
|
markupsafe.Markup(f'<p>{p_instruction}</p>'),
|
|
"Should have the processing instruction")
|
|
|
|
def test_render_widget_contact(self):
|
|
u = self.env['res.users'].create({
|
|
'name': 'Test',
|
|
'login': 'test@example.com',
|
|
})
|
|
u.name = ""
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy"><root><span t-esc="user" t-options='{"widget": "contact", "fields": ["name"]}' /></root></t>
|
|
"""
|
|
})
|
|
self.env['ir.qweb']._render(view1.id, {'user': u}) # should not crash
|
|
|
|
def test_void_element(self):
|
|
view = self.env['ir.ui.view'].create({
|
|
'name': 'master',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name='master'>
|
|
<meta name="1"/>
|
|
<t t-set="data" t-value="1"/>
|
|
<meta groups="base.group_no_one" name="2"/>
|
|
<meta t-if="False" name="3"/>
|
|
<meta t-if="True" name="4"/>
|
|
<span t-out="1"/>
|
|
</t>'''
|
|
})
|
|
|
|
result = '''
|
|
<meta name="1"/>
|
|
<meta name="4"/>
|
|
<span>1</span>
|
|
'''
|
|
rendered = self.env['ir.qweb']._render(view.id)
|
|
|
|
self.assertEqual(str(rendered).strip(), result.strip())
|
|
|
|
def test_space_remove_technical_space_t_foreach(self):
|
|
view = self.env['ir.ui.view'].create({
|
|
'name': 'master',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name='master'>
|
|
<section>
|
|
<article t-foreach="[0, 1, 2]" t-as="value" t-esc="value"/>
|
|
<t t-foreach="[0, 1, 2]" t-as="value">
|
|
<article t-esc="value"/>
|
|
</t>
|
|
</section>
|
|
</t>'''})
|
|
|
|
result = '''
|
|
<section>
|
|
<article>0</article><article>1</article><article>2</article>
|
|
<article>0</article>
|
|
<article>1</article>
|
|
<article>2</article>
|
|
</section>'''
|
|
|
|
rendered = self.env['ir.qweb']._render(view.id)
|
|
|
|
self.assertEqual(str(rendered), result)
|
|
|
|
def test_space_remove_technical_all(self):
|
|
test = self.env['ir.ui.view'].create({
|
|
'name': 'test',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name='test'>
|
|
<span t-out="value"/>
|
|
</t>'''
|
|
})
|
|
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
|
|
"VALUES ('test', 'ir.ui.view', %s, 'base')", [test.id])
|
|
|
|
view = self.env['ir.ui.view'].create({
|
|
'name': 'master',
|
|
'type': 'qweb',
|
|
'arch_db': '''<t t-name='master'>
|
|
|
|
<section>
|
|
<meta name="1"/>
|
|
<t t-set="data" t-value="1"/>
|
|
<meta groups="base.group_no_one" name="2"/>
|
|
<meta t-if="False" name="3"/>
|
|
<meta t-if="True" name="4"/>
|
|
|
|
<article>
|
|
<t t-foreach="[0, 1]" t-as="value">
|
|
<t t-call="base.test"/>
|
|
</t>
|
|
|
|
<t t-if="False">
|
|
a
|
|
</t>
|
|
|
|
b
|
|
|
|
<t t-if="True">
|
|
c <t t-out="1"/>
|
|
d
|
|
</t>
|
|
</article>
|
|
|
|
|
|
<article>
|
|
<div t-foreach="[0, 1]" t-as="value">
|
|
<t t-call="base.test"/>
|
|
</div>
|
|
|
|
<i t-if="False">
|
|
a
|
|
</i>
|
|
<u t-if="False">
|
|
a
|
|
</u>
|
|
|
|
b
|
|
|
|
<i t-if="True">
|
|
c <t t-out="1"/>
|
|
d
|
|
</i>
|
|
</article>
|
|
</section>
|
|
</t>'''
|
|
})
|
|
|
|
result = '''
|
|
<section>
|
|
<meta name="1"/>
|
|
<meta name="4"/>
|
|
|
|
<article>
|
|
<span>0</span>
|
|
<span>1</span>
|
|
|
|
|
|
b
|
|
|
|
c 1
|
|
d
|
|
</article>
|
|
|
|
|
|
<article>
|
|
<div>
|
|
<span>0</span>
|
|
</div><div>
|
|
<span>1</span>
|
|
</div>
|
|
|
|
|
|
b
|
|
|
|
<i>
|
|
c 1
|
|
d
|
|
</i>
|
|
</article>
|
|
</section>'''
|
|
|
|
rendered = self.env['ir.qweb']._render(view.id)
|
|
self.assertEqual(str(rendered), result)
|
|
|
|
class TestQwebCache(TransactionCase):
|
|
def test_render_xml_cache_base(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div t-cache="cache_id" class="toto">
|
|
<table>
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
expected_result = etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
</div>
|
|
""")
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
|
|
self.assertEqual(result, expected_result, 'First rendering (add in cache)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
|
|
self.assertEqual(result, expected_result, 'Next rendering use cache')
|
|
|
|
def test_render_xml_cache_different(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div class="toto">
|
|
<table t-cache="cache_id">
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
<table t-cache="cache_id2">
|
|
<tr><td><span t-esc="value2[0]"/></td></tr>
|
|
<tr><td><span t-esc="value2[1]"/></td></tr>
|
|
<tr><td><span t-esc="value2[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
# use same cache id, display the same content
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1,),
|
|
'cache_id2': (1,),
|
|
'value': [1, 2, 3],
|
|
'value2': [10, 20, 30]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
<table>
|
|
<tr><td><span>10</span></td></tr>
|
|
<tr><td><span>20</span></td></tr>
|
|
<tr><td><span>30</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering (add in cache with different cache)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (2, 5, 6),
|
|
'cache_id2': (2, 5, 5),
|
|
'value': [41, 42, 43],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>41</span></td></tr>
|
|
<tr><td><span>42</span></td></tr>
|
|
<tr><td><span>43</span></td></tr>
|
|
</table>
|
|
<table>
|
|
<tr><td><span>51</span></td></tr>
|
|
<tr><td><span>52</span></td></tr>
|
|
<tr><td><span>53</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Use different cache id')
|
|
|
|
def test_render_xml_cache_contains_nocache(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div t-cache="cache_id" class="toto">
|
|
<table>
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr t-nocache=""><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering add compiled values in cache')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>20</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Next rendering use cache exept for t-nocache=""')
|
|
|
|
def test_render_xml_cache_nocache_cache(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div class="toto">
|
|
<table t-cache="cache_id">
|
|
<tr><td><t t-esc="value[0]"/></td></tr>
|
|
<tr>
|
|
<td>
|
|
<table t-nocache="The content is not used, we can put documentation in it." t-cache="cache_id2">
|
|
<tr><td><t t-esc="value2[0]"/></td></tr>
|
|
<tr><td><t t-esc="value2[1]"/></td></tr>
|
|
<tr><td><t t-esc="value2[2]"/></td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td><t t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
# use same cache id, display the same content
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 0),
|
|
'value': [1, 2, 3],
|
|
'value2': [10, 20, 30]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>1</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>10</td></tr>
|
|
<tr><td>20</td></tr>
|
|
<tr><td>30</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>3</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering (add in cache)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 1),
|
|
'value': [41, 42, 43],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>1</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>51</td></tr>
|
|
<tr><td>52</td></tr>
|
|
<tr><td>53</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>3</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Second rendering (change inside cache id)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 1),
|
|
'cache_id2': (2, 0),
|
|
'value': [31, 32, 33],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>31</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>10</td></tr>
|
|
<tr><td>20</td></tr>
|
|
<tr><td>30</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>33</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Third rendering (change main cache id, old cache inside)')
|
|
|
|
def test_render_xml_cache_nocache_cache_on_same_tag(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div class="toto">
|
|
<table t-cache="cache_id">
|
|
<tr><td><t t-esc="value[0]"/></td></tr>
|
|
<tr t-nocache="">
|
|
<td>
|
|
<table t-cache="cache_id2">
|
|
<tr><td><t t-esc="value2[0]"/></td></tr>
|
|
<tr><td><t t-esc="value2[1]"/></td></tr>
|
|
<tr><td><t t-esc="value2[2]"/></td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td><t t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
# use same cache id, display the same content
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 0),
|
|
'value': [1, 2, 3],
|
|
'value2': [10, 20, 30]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>1</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>10</td></tr>
|
|
<tr><td>20</td></tr>
|
|
<tr><td>30</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>3</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering (add in cache)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 1),
|
|
'value': [41, 42, 43],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>1</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>51</td></tr>
|
|
<tr><td>52</td></tr>
|
|
<tr><td>53</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>3</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Second rendering (change inside cache id)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 1),
|
|
'cache_id2': (2, 0),
|
|
'value': [31, 32, 33],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>31</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>10</td></tr>
|
|
<tr><td>20</td></tr>
|
|
<tr><td>30</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>33</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Third rendering (change main cache id, old cache inside)')
|
|
|
|
def test_render_xml_dont_use_cache_base(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div t-cache="cache_id" class="toto">
|
|
<table>
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>10</span></td></tr>
|
|
<tr><td><span>20</span></td></tr>
|
|
<tr><td><span>30</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Next rendering cannot cache (use_qweb_t_cache is False)')
|
|
|
|
def test_render_xml_dont_use_cache_different(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div class="toto">
|
|
<table t-cache="cache_id">
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
<table t-cache="cache_id2">
|
|
<tr><td><span t-esc="value2[0]"/></td></tr>
|
|
<tr><td><span t-esc="value2[1]"/></td></tr>
|
|
<tr><td><span t-esc="value2[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
|
|
|
|
# use same cache id, display the same content
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': 1,
|
|
'cache_id2': 1,
|
|
'value': [1, 2, 3],
|
|
'value2': [10, 20, 30]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
<table>
|
|
<tr><td><span>10</span></td></tr>
|
|
<tr><td><span>20</span></td></tr>
|
|
<tr><td><span>30</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (2, 5, 6),
|
|
'cache_id2': (2, 5, 5),
|
|
'value': [41, 42, 43],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>41</span></td></tr>
|
|
<tr><td><span>42</span></td></tr>
|
|
<tr><td><span>43</span></td></tr>
|
|
</table>
|
|
<table>
|
|
<tr><td><span>51</span></td></tr>
|
|
<tr><td><span>52</span></td></tr>
|
|
<tr><td><span>53</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Use different cache id')
|
|
|
|
def test_render_xml_dont_use_cache_contains_nocache(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div t-cache="cache_id" class="toto">
|
|
<table>
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr t-nocache=""><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>10</span></td></tr>
|
|
<tr><td><span>20</span></td></tr>
|
|
<tr><td><span>30</span></td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Next rendering cannot use cache (use_qweb_t_cache is False)')
|
|
|
|
def test_render_xml_dont_use_cache_recursive(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div class="toto">
|
|
<table t-cache="cache_id">
|
|
<tr><td><t t-esc="value[0]"/></td></tr>
|
|
<tr>
|
|
<td>
|
|
<table t-nocache="" t-cache="cache_id2">
|
|
<tr><td><t t-esc="value2[0]"/></td></tr>
|
|
<tr><td><t t-esc="value2[1]"/></td></tr>
|
|
<tr><td><t t-esc="value2[2]"/></td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td><t t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
|
|
|
|
# use same cache id, display the same content
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 0),
|
|
'value': [1, 2, 3],
|
|
'value2': [10, 20, 30]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>1</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>10</td></tr>
|
|
<tr><td>20</td></tr>
|
|
<tr><td>30</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>3</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 1),
|
|
'value': [41, 42, 43],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>41</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>51</td></tr>
|
|
<tr><td>52</td></tr>
|
|
<tr><td>53</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>43</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Next rendering cannot use cache (use_qweb_t_cache is False)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 1),
|
|
'cache_id2': (2, 0),
|
|
'value': [31, 32, 33],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>31</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>51</td></tr>
|
|
<tr><td>52</td></tr>
|
|
<tr><td>53</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>33</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Third rendering cannot use cache (use_qweb_t_cache is False)')
|
|
|
|
def test_render_xml_dont_use_cache_false_recursive(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div class="toto">
|
|
<table t-cache="cache_id">
|
|
<tr><td><t t-esc="value[0]"/></td></tr>
|
|
<tr t-nocache="">
|
|
<td>
|
|
<table t-cache="cache_id2">
|
|
<tr><td><t t-esc="value2[0]"/></td></tr>
|
|
<tr><td><t t-esc="value2[1]"/></td></tr>
|
|
<tr><td><t t-esc="value2[2]"/></td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td><t t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
|
|
|
|
# use same cache id, display the same content
|
|
result = etree.fromstring(IrQweb._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 0),
|
|
'value': [1, 2, 3],
|
|
'value2': [10, 20, 30]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>1</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>10</td></tr>
|
|
<tr><td>20</td></tr>
|
|
<tr><td>30</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>3</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'First rendering')
|
|
|
|
result = etree.fromstring(self.env['ir.qweb']._render(view1.id, {
|
|
'cache_id': (1, 0),
|
|
'cache_id2': (2, 1),
|
|
'value': [41, 42, 43],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>41</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>51</td></tr>
|
|
<tr><td>52</td></tr>
|
|
<tr><td>53</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>43</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Next rendering cannot use cache (use_qweb_t_cache is False)')
|
|
|
|
result = etree.fromstring(self.env['ir.qweb']._render(view1.id, {
|
|
'cache_id': (1, 1),
|
|
'cache_id2': (2, 0),
|
|
'value': [31, 32, 33],
|
|
'value2': [51, 52, 53]
|
|
}))
|
|
self.assertEqual(result, etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td>31</td></tr>
|
|
<tr>
|
|
<td>
|
|
<table>
|
|
<tr><td>51</td></tr>
|
|
<tr><td>52</td></tr>
|
|
<tr><td>53</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr><td>33</td></tr>
|
|
</table>
|
|
</div>
|
|
"""), 'Third rendering cannot use cache (use_qweb_t_cache is False)')
|
|
|
|
def test_render_xml_nocache_use_the_root_values(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<section t-cache="cache_id">
|
|
<t t-set="counter" t-value="counter + 100"/>
|
|
<article t-nocache=""><t t-out="counter"/></article>
|
|
<div>cache: <t t-out="counter"/></div>
|
|
</section>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 1,
|
|
})
|
|
result = """
|
|
<section>
|
|
<article>1</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1 (101 != 1: cached t-set should never be applied on root rendering)')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 2,
|
|
})
|
|
result = """
|
|
<section>
|
|
<article>2</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2 (102 != 2: cached t-set should never be applied on root rendering)')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 3,
|
|
'counter': 3,
|
|
})
|
|
result = """
|
|
<section>
|
|
<article>3</article>
|
|
<div>cache: 103</div>
|
|
</section>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3 (103 != 3: cached t-set should never be applied on root rendering)')
|
|
|
|
def test_render_xml_nocache_use_the_root_values_and_cached_values(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<section t-cache="cache_id">
|
|
<t t-set="counter" t-value="counter + 100"/>
|
|
<article t-nocache="" t-nocache-counter="counter"><t t-out="counter"/></article>
|
|
<div>cache: <t t-out="counter"/></div>
|
|
</section>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 1,
|
|
})
|
|
result = """
|
|
<section>
|
|
<article>101</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1 (1 != 101: new cached values should be add to the root rendering)')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 2,
|
|
})
|
|
result = """
|
|
<section>
|
|
<article>101</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2 (102 != 2: cached values should be used)')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 3,
|
|
'counter': 3,
|
|
})
|
|
result = """
|
|
<section>
|
|
<article>103</article>
|
|
<div>cache: 103</div>
|
|
</section>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3 (3 != 103: new cached values should be add to the root rendering)')
|
|
|
|
def test_render_xml_nocache_use_the_root_values_and_cached_values_error(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<section t-cache="cache_id">
|
|
<article t-nocache="" t-nocache-record="view_record"><t t-out="view_record"/></article>
|
|
</section>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
with self.assertRaisesRegex(QWebException, "The value type of 't-nocache-record' cannot be cached"):
|
|
self.env['ir.qweb'].with_context(is_t_cache_disabled=False)._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'view_record': self.env['ir.ui.view'].search([], limit=1),
|
|
})
|
|
|
|
def test_render_xml_cache_with_t_set_out_of_cache(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<root>
|
|
<t t-set="counter" t-value="counter + 100"/>
|
|
<section t-cache="cache_id">
|
|
<article t-nocache=""><t t-out="counter"/></article>
|
|
<div>cache: <t t-out="counter"/></div>
|
|
</section>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 1,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<article>1</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1 (1 != 101: cached t-set should is applied on first rendering)')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 2,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<article>2</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2 (2 != 102: cached t-set should be applied the template part are rendered every time)')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 3,
|
|
'counter': 3,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<article>3</article>
|
|
<div>cache: 103</div>
|
|
</section>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3 (3 != 103: cached t-set should applied because the new cache key is created)')
|
|
|
|
def test_render_xml_cache_with_t_set_in_cache(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<root>
|
|
<section t-cache="cache_id">
|
|
<t t-set="counter" t-value="counter + 100"/>
|
|
<article t-nocache=""><t t-out="counter"/></article>
|
|
<div>cache: <t t-out="counter"/></div>
|
|
</section>
|
|
<div>out of cache: <t t-out="counter"/></div>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 1,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<article>1</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
<div>out of cache: 1</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 2,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<article>2</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
<div>out of cache: 2</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 3,
|
|
'counter': 3,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<article>3</article>
|
|
<div>cache: 103</div>
|
|
</section>
|
|
<div>out of cache: 3</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
|
|
|
|
def test_render_xml_cache_with_t_set_wrap_t_cache(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<root t-cache="cache_1">
|
|
<t t-set="a">
|
|
<section t-cache="cache_2">
|
|
<t t-set="counter" t-value="counter + 100"/>
|
|
<article t-nocache="" class="no_cache"><t t-out="counter"/></article>
|
|
<div>cache: <t t-out="counter"/></div>
|
|
</section>
|
|
<footer t-nocache="" class="no_cache"><t t-out="counter * 10"/></footer>
|
|
</t>
|
|
<div>
|
|
<t t-out="a"/>
|
|
</div>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_1': 1,
|
|
'cache_2': 1,
|
|
'counter': 1,
|
|
})
|
|
result = """
|
|
<root>
|
|
<div>
|
|
<section>
|
|
<article class="no_cache">1</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
<footer class="no_cache">10</footer>
|
|
</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_1': 2,
|
|
'cache_2': 1,
|
|
'counter': 2,
|
|
})
|
|
result = """
|
|
<root>
|
|
<div>
|
|
<section>
|
|
<article class="no_cache">2</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
<footer class="no_cache">20</footer>
|
|
</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_1': 2,
|
|
'cache_2': 3,
|
|
'counter': 3,
|
|
})
|
|
result = """
|
|
<root>
|
|
<div>
|
|
<section>
|
|
<article class="no_cache">2</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
<footer class="no_cache">20</footer>
|
|
</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_1': 3,
|
|
'cache_2': 3,
|
|
'counter': 3,
|
|
})
|
|
result = """
|
|
<root>
|
|
<div>
|
|
<section>
|
|
<article class="no_cache">3</article>
|
|
<div>cache: 103</div>
|
|
</section>
|
|
<footer class="no_cache">30</footer>
|
|
</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 4')
|
|
|
|
def test_render_xml_t_set_wrap_t_cache(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<root>
|
|
<t t-set="a">
|
|
<section t-cache="cache_id">
|
|
<t t-set="counter" t-value="counter + 100"/>
|
|
<article t-nocache="" class="no_cache"><t t-out="counter"/></article>
|
|
<div>cache: <t t-out="counter"/></div>
|
|
</section>
|
|
<footer t-nocache="" class="no_cache"><t t-out="counter * 10"/></footer>
|
|
</t>
|
|
<div>
|
|
<t t-out="a"/>
|
|
</div>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 1,
|
|
})
|
|
result = """
|
|
<root>
|
|
<div>
|
|
<section>
|
|
<article class="no_cache">1</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
<footer class="no_cache">10</footer>
|
|
</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 1,
|
|
'counter': 2,
|
|
})
|
|
result = """
|
|
<root>
|
|
<div>
|
|
<section>
|
|
<article class="no_cache">2</article>
|
|
<div>cache: 101</div>
|
|
</section>
|
|
<footer class="no_cache">20</footer>
|
|
</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'cache_id': 3,
|
|
'counter': 3,
|
|
})
|
|
result = """
|
|
<root>
|
|
<div>
|
|
<section>
|
|
<article class="no_cache">3</article>
|
|
<div>cache: 103</div>
|
|
</section>
|
|
<footer class="no_cache">30</footer>
|
|
</div>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
|
|
|
|
def test_render_xml_nocache_in_cache_in_cache(self):
|
|
template_page = self.env['ir.ui.view'].create({
|
|
'name': "template_page",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="template_page">
|
|
<root>
|
|
<section t-cache="key1">
|
|
<span t-out="val"/>
|
|
<article t-cache="key2">
|
|
<span t-nocache="" t-out="val"/>
|
|
</article>
|
|
</section>
|
|
</root>
|
|
</t>
|
|
"""
|
|
})
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'key1': (1,),
|
|
'key2': (1,),
|
|
'val': 1,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<span>1</span>
|
|
<article>
|
|
<span>1</span>
|
|
</article>
|
|
</section>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'key1': (1,),
|
|
'key2': (1,),
|
|
'val': 2,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<span>1</span>
|
|
<article>
|
|
<span>2</span>
|
|
</article>
|
|
</section>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
|
|
|
|
render = IrQweb._render(template_page.id, {
|
|
'key1': (1,),
|
|
'key2': (2,),
|
|
'val': 3,
|
|
})
|
|
result = """
|
|
<root>
|
|
<section>
|
|
<span>1</span>
|
|
<article>
|
|
<span>3</span>
|
|
</article>
|
|
</section>
|
|
</root>
|
|
"""
|
|
self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
|
|
|
|
def test_render_xml_conditional_cache(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div t-cache="cache_id if condition else None" class="toto">
|
|
<table>
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
expected_result = etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
</div>
|
|
""")
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'condition': True, 'value': [1, 2, 3]}))
|
|
self.assertEqual(result, expected_result, 'First rendering (add in cache)')
|
|
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'condition': True, 'value': [10, 20, 30]}))
|
|
self.assertEqual(result, expected_result, 'Next rendering use cache')
|
|
|
|
|
|
expected_result = etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>10</span></td></tr>
|
|
<tr><td><span>20</span></td></tr>
|
|
<tr><td><span>30</span></td></tr>
|
|
</table>
|
|
</div>
|
|
""")
|
|
result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
|
|
self.assertEqual(result, expected_result, 'Next rendering use cache')
|
|
|
|
def test_render_xml_cache_and_inherit_view(self):
|
|
view1 = self.env['ir.ui.view'].create({
|
|
'name': "dummy",
|
|
'type': 'qweb',
|
|
'arch': """
|
|
<t t-name="base.dummy">
|
|
<div t-cache="True" class="toto">
|
|
<table>
|
|
<tr><td><span t-esc="value[0]"/></td></tr>
|
|
<tr><td><span t-esc="value[1]"/></td></tr>
|
|
<tr><td><span t-esc="value[2]"/></td></tr>
|
|
</table>
|
|
</div>
|
|
</t>
|
|
"""
|
|
})
|
|
# t-cache value can be an interable then we can add value as a tuple (without parenthesis)
|
|
view2 = self.env['ir.ui.view'].create({
|
|
'name': 'Child View',
|
|
'mode': 'extension',
|
|
'inherit_id': view1.id,
|
|
'arch': '''
|
|
<xpath expr="//div[@t-cache]" position="attributes">
|
|
<attribute name="t-cache" add="company,value[0]" remove="True," separator=","/>
|
|
</xpath>
|
|
''',
|
|
})
|
|
|
|
IrQweb = self.env['ir.qweb'].with_context(use_qweb_t_cache=True)
|
|
|
|
expected_result = etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>1</span></td></tr>
|
|
<tr><td><span>2</span></td></tr>
|
|
<tr><td><span>3</span></td></tr>
|
|
</table>
|
|
</div>
|
|
""")
|
|
result = etree.fromstring(IrQweb._render(view2.id, {'value': [1, 2, 3]}))
|
|
self.assertEqual(result, expected_result, 'First rendering create cache from company and the value 1')
|
|
|
|
expected_result = etree.fromstring("""
|
|
<div class="toto">
|
|
<table>
|
|
<tr><td><span>10</span></td></tr>
|
|
<tr><td><span>20</span></td></tr>
|
|
<tr><td><span>30</span></td></tr>
|
|
</table>
|
|
</div>
|
|
""")
|
|
result = etree.fromstring(IrQweb._render(view2.id, {'value': [10, 20, 30]}))
|
|
self.assertEqual(result, expected_result, 'Next rendering create cache from company and the value 10')
|
|
|
|
def test_render_nodb(self):
|
|
""" Render an html page without db ans wihtout registry
|
|
"""
|
|
expected = dedent("""
|
|
<html>
|
|
<head>
|
|
<title>Odoo</title>
|
|
</head>
|
|
<body>
|
|
<section class="toto">
|
|
<div>3</div>
|
|
</section>
|
|
</body>
|
|
</html>
|
|
""").strip()
|
|
|
|
templates = {
|
|
'html': html.document_fromstring("""
|
|
<html t-name="html">
|
|
<head>
|
|
<title>Odoo</title>
|
|
</head>
|
|
<body>
|
|
<section class="toto">
|
|
<t t-call="content"/>
|
|
</section>
|
|
</body>
|
|
</html>
|
|
"""),
|
|
'content': html.fragment_fromstring("""
|
|
<t t-name="content">
|
|
<div><t t-out="val"/></div>
|
|
</t>
|
|
""")
|
|
}
|
|
def load(template_name):
|
|
return (templates[template_name], template_name)
|
|
rendering = render('html', {'val': 3}, load).strip()
|
|
|
|
self.assertEqual(html.document_fromstring(rendering), html.document_fromstring(expected))
|