From 45d26bc2572b5bdd348624936aeb1e3fbdb62c45 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Wed, 28 Dec 2016 13:52:29 +0100 Subject: [PATCH] [ADD] QWeb: xml namespaces support in static template With this patch, QWeb is able to support xml namespace declaration and prefixes. As this patch only deals with static template, the only method we have to patch is `_compile_static_node`. We make it able to serialize etree node having an nsmap by formating them to restore the xml prefix if present or to declare the xmlns attribute if needed. To know if the namespace definition is needed, we compare the tuples of (xmlprefix, xmldefinition) of the node and its parent. The namespace definition of the parent is passed by a dict in the `options` key. We took care of dealing with the mutable + recursion thing by working with copy of this namespace definition dict. Two tests have been introduced, basically what we pass to QWeb is what it has to return. --- odoo/addons/base/ir/ir_qweb/qweb.py | 41 +++++++++-- odoo/addons/base/tests/test_qweb.py | 104 ++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 6 deletions(-) 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):