diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py index 5af8ac27ea0..67488586538 100644 --- a/odoo/addons/base/ir/ir_qweb/qweb.py +++ b/odoo/addons/base/ir/ir_qweb/qweb.py @@ -876,17 +876,37 @@ class QWeb(object): def _compile_tag(self, el, content, options, attr_already_created=False): """ Compile the tag of the given element into a list of AST nodes. """ - if el.tag == 't': + extra_attrib = {} + if not el.nsmap: + unqualified_el_tag = el_tag = el.tag + else: + # Etree will remove the ns prefixes indirection by inlining the corresponding + # nsmap definition into the tag attribute. Restore the tag and prefix here. + unqualified_el_tag = etree.QName(el.tag).localname + el_tag = unqualified_el_tag + if el.prefix: + el_tag = '%s:%s' % (el.prefix, el_tag) + + # If `el` introduced new namespaces, write them as attribute by using the + # `extra_attrib` dict. + for ns_prefix, ns_definition in set(el.nsmap.items()) - set(options['nsmap'].items()): + if ns_prefix is None: + extra_attrib['xmlns'] = ns_definition + else: + extra_attrib['xmlns:%s' % ns_prefix] = ns_definition + + if unqualified_el_tag == 't': return content - body = [self._append(ast.Str(u'<%s' % el.tag))] + + body = [self._append(ast.Str(u'<%s%s' % (el_tag, u''.join([u' %s="%s"' % (name, escape(unicodifier(value))) for name, value in extra_attrib.iteritems()]))))] body.extend(self._compile_all_attributes(el, options, attr_already_created)) - if el.tag in self._void_elements: + if unqualified_el_tag in self._void_elements: body.append(self._append(ast.Str(u'/>'))) body.extend(content) else: body.append(self._append(ast.Str(u'>'))) body.extend(content) - body.append(self._append(ast.Str(u'' % el.tag))) + body.append(self._append(ast.Str(u'' % el_tag))) return body # compile directives @@ -902,7 +922,17 @@ class QWeb(object): def _compile_directive_tag(self, el, options): el.attrib.pop('t-tag', None) + + # Update the dict of inherited namespaces before continuing the recursion. Note: + # since `options['nsmap']` is a dict (and therefore mutable) and we do **not** + # want changes done in deeper recursion to bevisible in earlier ones, we'll pass + # a copy before continuing the recursion and restore the original afterwards. + if el.nsmap: + original_nsmap = options['nsmap'] + options['nsmap'] = dict(options['nsmap'], **el.nsmap) content = self._compile_directives(el, options) + if el.nsmap: + options['nsmap'] = original_nsmap return self._compile_tag(el, content, options, False) def _compile_directive_set(self, el, options): diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py index 7dfc56a30c5..c4b279c2f43 100644 --- a/odoo/addons/base/tests/test_qweb.py +++ b/odoo/addons/base/tests/test_qweb.py @@ -168,6 +168,211 @@ class TestQWebNS(TransactionCase): self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(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': """ + + + + """ + }) + + expected_result = """test""" + + self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(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': """ + + + blabla + + + """ + }) + + expected_result = """ + + blabla + + """ + + self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(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': """ + + + + + Apples + Bananas + + + + 80 + + + + """ + }) + + expected_result = """ + + + + Apples + Bananas + + + + 80 + + + """ + + self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(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': """ + + + + + Apples + Bananas + + + + 80 + + + + + """ + }) + + expected_result = """ + + + + Apples + Bananas + + + + 80 + + + + """ + + self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result)) + + def test_render_dynamic_xml_with_namespace_2(self): + view1 = self.env['ir.ui.view'].create({ + 'name': "dummy", + 'type': 'qweb', + 'arch': """ + + + + + Oasis + + + + """ + }) + + expected_result = """ + + 1.0 + + Oasis 1 + + Oasis 2 + + Oasis 3 + + Oasis 4 + + + """ + + self.assertEquals(dedent_and_strip(view1.render(dict(version_id=1.0))), dedent_and_strip(expected_result)) + + def test_render_static_xml_with_t_call(self): + view1 = self.env['ir.ui.view'].create({ + 'name': "dummy", + 'type': 'qweb', + 'arch': """ + + + + Appel + Pineappel + + + + """ + }) + 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': """ + + + + + + """ + }) + + 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.assertSetEqual(set(expected_ns.items()) - set(result_etree.nsmap.items()), set()) + + # 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.assertEquals(len(cac_lines), 2) + self.assertEquals(result.count('Appel'), 2) + from copy import deepcopy class FileSystemLoader(object):