Files
odoo_source/odoo/addons/base/tests/test_qweb.py
T
Martin Trigaux d9287caf94 [IMP] *: convert to private methods
render, render_template, load, activity_schedule_with_view,
get_website_pages should all be private:
It should not be possible to render an aribtrary template only with
its name or id

Still need to render some qweb views from js so the method
render_template is kept public.
This explains why the website editor still need read access on
ir.ui.view as we want to allow any snippet to be rendered.
2020-05-14 13:59:10 +02:00

708 lines
27 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
from lxml import etree, html
from lxml.builder import E
from odoo.modules import get_module_resource
from odoo.tests.common import TransactionCase
from odoo.addons.base.models.qweb import QWebException
from odoo.tools import misc, ustr
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': u'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(u'<span data-oe-model="res.company" data-oe-id="%d" '
u'data-oe-field="name" data-oe-type="char" '
u'data-oe-expression="company.name">%s</span>' % (
company.id,
u"My Test Company",
)),
)
def test_i18n(self):
field = etree.Element('span', {'t-field': u'company.name'})
s = u"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(u'<span data-oe-model="res.company" data-oe-id="%d" '
u'data-oe-field="name" data-oe-type="char" '
u'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': u'company.name'})
with self.assertRaisesRegex(QWebException, r'^RTE widgets do not work correctly'):
self.engine._render(field, {'company': None})
def test_reject_t_tag(self):
field = etree.Element('t', {'t-field': u'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': u"""
<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(view1._render()).find('span').text
self.assertEqual(text, u'5.0000')
def test_xss_breakout(self):
view = self.env['ir.ui.view'].create({
'name': 'dummy', 'type': 'qweb',
'arch': u"""
<t t-name="base.dummy">
<root>
<script type="application/javascript">
var s = <t t-raw="json.dumps({'key': malicious})"/>;
</script>
</root>
</t>
"""
})
rendered = view._render({'malicious': '1</script><script>alert("pwned")</script><script>'}).decode()
self.assertIn('alert', rendered, "%r doesn't seem to be rendered" % rendered)
doc = etree.fromstring(rendered)
self.assertEqual(len(doc.xpath('//script')), 1)
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 = u"""
<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': u"""
<t t-name="base.dummy">%s</t>
""" % expected_result
})
self.assertEqual(etree.fromstring(view1._render()), 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 = u"""
<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': u"""
<t t-name="base.dummy">%s</t>
""" % expected_result
})
self.assertEqual(etree.fromstring(view1._render()), 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': u"""
<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(u"""
<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(view1._render()), expected_result)
def test_render_static_xml_with_namespace_3(self):
expected_result = u"""
<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': u"""
<t t-name="base.dummy">%s</t>
""" % expected_result
})
self.assertEqual(etree.fromstring(view1._render()), etree.fromstring(expected_result))
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': u"""
<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(u"""<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">test</Invoice>""")
self.assertEqual(etree.fromstring(view1._render()), 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': u"""
<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(u"""
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" test="test">
<cac:Test>blabla</cac:Test>
</Invoice>
""")
self.assertEqual(etree.fromstring(view1._render()), 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': u"""
<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(u"""
<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(view1._render()), 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': u"""
<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(u"""
<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(view1._render()), expected_result)
def test_render_dynamic_xml_with_namespace_2(self):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': u"""
<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(u"""
<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(view1._render({'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': u"""
<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(u"""<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(view1._render()), expected_result)
def test_render_dynamic_xml_with_namespaced_attributes(self):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': u"""
<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(view1._render()), expected_result)
def test_render_static_xml_with_t_call(self):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': u"""
<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': u"""
<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 = view2._render()
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(b'Appel'), 2)
# check that the t-call dit not output again the xmlns declaration
self.assertEqual(result.count(b'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': u"""
<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': u"""
<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(u"""
<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(view1.with_context(check_view_ids=[view1.id, view2.id])._render()),
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': u"""
<t t-name="base.dummy">
<Invoice xmlns:od="http://odoo.com/od">
<od:name t-att-test="'a' + 1"/>
</Invoice>
</t>
"""
})
try:
"" + 0
except TypeError as e:
error_msg = e.args[0]
with self.assertRaises(QWebException, msg=error_msg):
view1._render()
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': u"""
<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': u"""
<t t-name="base.calling">
<t t-call="base.callee" t-lang="'%s'" />
</t>
""" % other_lang
})
rendered = view2.with_context(lang=current_lang)._render().strip()
self.assertEqual(rendered, b'9/000/000*00')
from copy import deepcopy
class FileSystemLoader(object):
def __init__(self, path):
# TODO: support multiple files #add_file() + add cache
self.path = path
self.doc = etree.parse(path).getroot()
def __iter__(self):
for node in self.doc:
name = node.get('t-name')
if name:
yield name
def __call__(self, name, options):
for node in self.doc:
if node.get('t-name') == name:
root = etree.Element('templates')
root.append(deepcopy(node))
arch = etree.tostring(root, encoding='unicode')
return arch
class TestQWeb(TransactionCase):
matcher = re.compile(r'^qweb-test-(.*)\.xml$')
@classmethod
def get_cases(cls):
path = cls.qweb_test_file_path()
return (
cls("test_qweb_{}".format(cls.matcher.match(f).group(1)))
for f in os.listdir(path)
# js inheritance
if f != 'qweb-test-extend.xml'
if cls.matcher.match(f)
)
@classmethod
def qweb_test_file_path(cls):
return os.path.dirname(get_module_resource('web', 'static', 'lib', 'qweb', 'qweb2.js'))
def __getattr__(self, item):
if not item.startswith('test_qweb_'):
raise AttributeError("No {} on {}".format(item, self))
f = 'qweb-test-{}.xml'.format(item[10:])
path = self.qweb_test_file_path()
return lambda: self.run_test_file(os.path.join(path, f))
def run_test_file(self, path):
self.env.user.tz = 'Europe/Brussels'
doc = etree.parse(path).getroot()
loader = FileSystemLoader(path)
qweb = self.env['ir.qweb']
for template in loader:
if not template or template.startswith('_'):
continue
param = doc.find('params[@id="{}"]'.format(template))
# OrderedDict to ensure JSON mappings are iterated in source order
# so output is predictable & repeatable
params = {} if param is None else json.loads(param.text, object_pairs_hook=collections.OrderedDict)
result = doc.find('result[@id="{}"]'.format(template)).text
self.assertEqual(
qweb._render(template, values=params, load=loader).strip(),
(result or u'').strip().encode('utf-8'),
template
)
class TestPageSplit(TransactionCase):
# need to explicitly assertTreesEqual because I guess it's registered for
# equality between _Element *or* HtmlElement but we're comparing a parsed
# HtmlElement and a convenience _Element
def test_split_before(self):
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'arch_db': '''<t t-name='test'>
<div>
<table>
<tr></tr>
<tr data-pagebreak="before"></tr>
<tr></tr>
</table>
</div>
</t>
'''
})
rendered = html.fromstring(self.env['ir.qweb']._render(t.id))
ref = E.div(
E.table(E.tr()),
E.div({'style': 'page-break-after: always'}),
E.table(E.tr({'data-pagebreak': 'before'}), E.tr())
)
self.assertTreesEqual(rendered, ref)
def test_split_after(self):
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'arch_db': '''<t t-name='test'>
<div>
<table>
<tr></tr>
<tr data-pagebreak="after"></tr>
<tr></tr>
</table>
</div>
</t>
'''
})
rendered = html.fromstring(self.env['ir.qweb']._render(t.id))
self.assertTreesEqual(
rendered,
E.div(
E.table(E.tr(), E.tr({'data-pagebreak': 'after'})),
E.div({'style': 'page-break-after: always'}),
E.table(E.tr())
)
)
def test_dontsplit(self):
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'arch_db': '''<t t-name='test'>
<div>
<table>
<tr></tr>
<tr></tr>
<tr></tr>
</table>
</div>
</t>
'''
})
rendered = html.fromstring(self.env['ir.qweb']._render(t.id))
self.assertTreesEqual(
rendered,
E.div(E.table(E.tr(), E.tr(), E.tr()))
)
def load_tests(loader, suite, _):
# can't override TestQWeb.__dir__ because dir() called on *class* not
# instance
suite.addTests(TestQWeb.get_cases())
return suite