[ADD] QWeb: xml namespaces support in dynamic template

This is very similar and almost copy pasted from the implementation in
static template, as the main method `compile_node` will either call
`_compile_static_node` (which we patched in the static implementation)
or `_compile_directives` which will call `_compile_tag` in the cases
where xmlns matter (at least that's my understanding for today). So we
patch `_compile_tag` the same way than `_compile_static_node`. It would
be nicer if the method wasn't completely different and in two parts, but
hey.

This method is not always called on leaves, so setting options['nsmap']
to a copy before processing the content of the node is necessary (a node
with a nsmap and a t-attf- having a child with an nsmap: the child has
to be aware of the nsmap of its parent).

Note that we set the xmlns attribtue before calling
`_compile_all_attribute`, which will probably break xmlns set by t-*
tag. However, we don't plan to support that the moment.
This commit is contained in:
Simon Lejeune
2017-01-02 16:05:58 +01:00
parent 45d26bc257
commit 2d3c928d47
2 changed files with 239 additions and 4 deletions
+34 -4
View File
@@ -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):
+205
View File
@@ -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': """
<t t-name="base.dummy">
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" t-esc="'test'"/>
</t>
"""
})
expected_result = """<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">test</Invoice>"""
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': """
<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 = """
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" test="test">
<cac:Test>blabla</cac:Test>
</Invoice>
"""
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': """
<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 = """
<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.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': """
<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 = """
<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.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': """
<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 = """
<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.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': """
<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': """
<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.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):