diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py index e2ffc4bf399..5af8ac27ea0 100644 --- a/odoo/addons/base/ir/ir_qweb/qweb.py +++ b/odoo/addons/base/ir/ir_qweb/qweb.py @@ -8,7 +8,7 @@ from itertools import count from textwrap import dedent import werkzeug from werkzeug.utils import escape as _escape -from itertools import izip, tee +from itertools import chain, izip, tee import __builtin__ builtin_defaults = {name: getattr(__builtin__, name) for name in dir(__builtin__)} @@ -269,6 +269,7 @@ class QWeb(object): _options['ast_calls'] = [] _options['root'] = element.getroottree() _options['last_path_node'] = None + _options['nsmap'] = {} # generate ast @@ -700,14 +701,42 @@ class QWeb(object): def _compile_static_node(self, el, options): """ Compile a purely static element into a list of AST nodes. """ - content = self._compile_directive_content(el, options) - if el.tag == 't': + extra_attrib = {} + if not el.nsmap: + unqualified_el_tag = el_tag = el.tag + content = self._compile_directive_content(el, options) + 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 + + # 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. + original_nsmap = options['nsmap'] + options['nsmap'] = dict(options['nsmap'], **el.nsmap) + content = self._compile_directive_content(el, options) + options['nsmap'] = original_nsmap + + if unqualified_el_tag == 't': return content - tag = u'<%s%s' % (el.tag, u''.join([u' %s="%s"' % (name, escape(unicodifier(value))) for name, value in el.attrib.iteritems()])) - if el.tag in self._void_elements: + tag = u'<%s%s' % (el_tag, u''.join([u' %s="%s"' % (name, escape(unicodifier(value))) for name, value in chain(el.attrib.iteritems(), extra_attrib.iteritems())])) + if unqualified_el_tag in self._void_elements: return [self._append(ast.Str(tag + '/>'))] + content else: - return [self._append(ast.Str(tag + '>'))] + content + [self._append(ast.Str('' % el.tag))] + return [self._append(ast.Str(tag + '>'))] + content + [self._append(ast.Str('' % el_tag))] def _compile_static_attributes(self, el, options): """ Compile the static attributes of the given element into a list of diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py index 0210b03df05..7dfc56a30c5 100644 --- a/odoo/addons/base/tests/test_qweb.py +++ b/odoo/addons/base/tests/test_qweb.py @@ -15,6 +15,10 @@ from odoo.tests.common import TransactionCase from odoo.addons.base.ir.ir_qweb import QWebException +def dedent_and_strip(string): + return ''.join([line.strip() for line in string.splitlines()]) + + class TestQWebTField(TransactionCase): def setUp(self): super(TestQWebTField, self).setUp() @@ -65,6 +69,106 @@ class TestQWebTField(TransactionCase): self.engine.render(field, {'company': None}) +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 = """ + + + + Apples + Bananas + + + + 80 + + + """ + + view1 = self.env['ir.ui.view'].create({ + 'name': "dummy", + 'type': 'qweb', + 'arch': """ + %s + """ % expected_result + }) + + self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(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 = """ + + + Book Review + + + + XML: A Primer + + + + + + + + + + + +
AuthorPricePagesDate
Simon St. Laurent31.983521998/01
+
+ + + """ + + view1 = self.env['ir.ui.view'].create({ + 'name': "dummy", + 'type': 'qweb', + 'arch': """ + %s + """ % expected_result + }) + + self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(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': """ + + + + + Apples + Bananas + + + + + """ + }) + + expected_result = """ + + + + Apples + Bananas + + + + """ + + self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result)) + + from copy import deepcopy class FileSystemLoader(object): def __init__(self, path):