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'%s>' % el.tag)))
+ body.append(self._append(ast.Str(u'%s>' % 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):