Prior to this change the convert tool did could mangle
the value of html fields when importing static data.
This is because html does not support 'self-closing'
except for HTML5 where it is allowed on void elements (such as img).
Browsers will assume that they are opening tags, left unclosed.
<span/> becomes <span>
They will then try to repair them with variying degrees of success.
Example where it fails:
`<t><t/></t><t><t/></t>`
should become
`<t><t></t></t><t><t></t></t>`
but it becomes
`<t><t></t><t><t></t></t>`
i.e. instead of closing the 'self-closing' tag immediately,
it puts everything inside a single t node
More concretely:
```
<t t-if>
<t t-out />
</t>
<t t-else>
<t t-out/>
</t>
```
becomes
```
<t t-if>
<t t-out></t>
<t t-else>
<t t-out></t>
</t>
</t>
```
which is invalid
-------------------------
The fix is simply to tell lxml that we want to print the xml nodes
as HTML nodes. This will make sure the output is compliant with
the standard and keep the semantic clear for the browser.
The issue does not appear before 16.2, as jquery used to fix it
for us until an update here 9c41ee5091ac06ac3ca71aeac607195c70061e4a
task-3162320
X-original-commit: 8ff2e1018264972107f19755ecda352d78dfa829
Part-of: odoo/odoo#118710
289 lines
10 KiB
Python
289 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import collections
|
|
import unittest
|
|
|
|
from lxml import etree as ET
|
|
from lxml.builder import E
|
|
|
|
import odoo
|
|
from odoo.modules.module import get_resource_path
|
|
from odoo.tests import common
|
|
from odoo.tools.convert import convert_file, xml_import, _eval_xml
|
|
|
|
Field = E.field
|
|
Value = E.value
|
|
|
|
class TestEvalXML(common.TransactionCase):
|
|
def eval_xml(self, node, obj=None):
|
|
return _eval_xml(obj, node, self.env)
|
|
|
|
def test_char(self):
|
|
self.assertEqual(
|
|
self.eval_xml(Field("foo")),
|
|
"foo")
|
|
self.assertEqual(
|
|
self.eval_xml(Field("None")),
|
|
"None")
|
|
|
|
def test_int(self):
|
|
self.assertIsNone(
|
|
self.eval_xml(Field("None", type='int')),
|
|
"what the fuck?")
|
|
self.assertEqual(
|
|
self.eval_xml(Field(" 42 ", type="int")),
|
|
42)
|
|
|
|
with self.assertRaises(ValueError):
|
|
self.eval_xml(Field("4.82", type="int"))
|
|
|
|
with self.assertRaises(ValueError):
|
|
self.eval_xml(Field("Whelp", type="int"))
|
|
|
|
def test_float(self):
|
|
self.assertEqual(
|
|
self.eval_xml(Field("4.78", type="float")),
|
|
4.78)
|
|
|
|
with self.assertRaises(ValueError):
|
|
self.eval_xml(Field("None", type="float"))
|
|
|
|
with self.assertRaises(ValueError):
|
|
self.eval_xml(Field("Foo", type="float"))
|
|
|
|
def test_list(self):
|
|
self.assertEqual(
|
|
self.eval_xml(Field(type="list")),
|
|
[])
|
|
|
|
self.assertEqual(
|
|
self.eval_xml(Field(
|
|
Value("foo"),
|
|
Value("5", type="int"),
|
|
Value("4.76", type="float"),
|
|
Value("None", type="int"),
|
|
type="list"
|
|
)),
|
|
["foo", 5, 4.76, None])
|
|
|
|
def test_file(self):
|
|
Obj = collections.namedtuple('Obj', ['module', 'idref'])
|
|
obj = Obj('test_convert', None)
|
|
self.assertEqual(
|
|
self.eval_xml(Field('test_file.txt', type='file'), obj),
|
|
'test_convert,test_file.txt')
|
|
|
|
with self.assertRaises(IOError):
|
|
self.eval_xml(Field('test_nofile.txt', type='file'), obj)
|
|
|
|
def test_function(self):
|
|
obj = xml_import(self.env, 'test_convert', None, 'init')
|
|
|
|
# pass args in eval
|
|
xml = E.function(
|
|
model="test_convert.usered",
|
|
name="model_method",
|
|
eval="[1, 2]",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, self.env.context)
|
|
self.assertEqual(rec.ids, [])
|
|
self.assertEqual(args, (1, 2))
|
|
self.assertEqual(kwargs, {})
|
|
|
|
xml = E.function(
|
|
model="test_convert.usered",
|
|
name="method",
|
|
eval="[1, 2]",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, self.env.context)
|
|
self.assertEqual(rec.ids, [1])
|
|
self.assertEqual(args, (2,))
|
|
self.assertEqual(kwargs, {})
|
|
|
|
# pass args in child elements
|
|
xml = E.function(
|
|
E.value(eval="1"), E.value(eval="2"),
|
|
model="test_convert.usered",
|
|
name="model_method",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, self.env.context)
|
|
self.assertEqual(rec.ids, [])
|
|
self.assertEqual(args, (1, 2))
|
|
self.assertEqual(kwargs, {})
|
|
|
|
xml = E.function(
|
|
E.value(eval="1"), E.value(eval="2"),
|
|
model="test_convert.usered",
|
|
name="method",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, self.env.context)
|
|
self.assertEqual(rec.ids, [1])
|
|
self.assertEqual(args, (2,))
|
|
self.assertEqual(kwargs, {})
|
|
|
|
def test_function_kwargs(self):
|
|
obj = xml_import(self.env, 'test_convert', None, 'init')
|
|
|
|
# pass args and kwargs in child elements
|
|
xml = E.function(
|
|
E.value(eval="1"), E.value(name="foo", eval="2"),
|
|
model="test_convert.usered",
|
|
name="model_method",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, self.env.context)
|
|
self.assertEqual(rec.ids, [])
|
|
self.assertEqual(args, (1,))
|
|
self.assertEqual(kwargs, {'foo': 2})
|
|
|
|
xml = E.function(
|
|
E.value(eval="1"), E.value(name="foo", eval="2"),
|
|
model="test_convert.usered",
|
|
name="method",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, self.env.context)
|
|
self.assertEqual(rec.ids, [1])
|
|
self.assertEqual(args, ())
|
|
self.assertEqual(kwargs, {'foo': 2})
|
|
|
|
# pass args and context in kwargs
|
|
xml = E.function(
|
|
E.value(eval="1"), E.value(name="context", eval="{'foo': 2}"),
|
|
model="test_convert.usered",
|
|
name="model_method",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, {'foo': 2})
|
|
self.assertEqual(rec.ids, [])
|
|
self.assertEqual(args, (1,))
|
|
self.assertEqual(kwargs, {})
|
|
|
|
xml = E.function(
|
|
E.value(eval="1"), E.value(name="context", eval="{'foo': 2}"),
|
|
model="test_convert.usered",
|
|
name="method",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, {'foo': 2})
|
|
self.assertEqual(rec.ids, [1])
|
|
self.assertEqual(args, ())
|
|
self.assertEqual(kwargs, {})
|
|
|
|
def test_function_function(self):
|
|
obj = xml_import(self.env, 'test_convert', None, 'init')
|
|
|
|
xml = E.function(
|
|
E.function(model="test_convert.usered", name="search", eval="[[]]"),
|
|
model="test_convert.usered",
|
|
name="method",
|
|
)
|
|
rec, args, kwargs = self.eval_xml(xml, obj)
|
|
self.assertEqual(rec.env.context, {})
|
|
self.assertEqual(rec.ids, [])
|
|
self.assertEqual(args, ())
|
|
self.assertEqual(kwargs, {})
|
|
|
|
def test_o2m_sub_records(self):
|
|
# patch the model's class with a proxy that copies the argument
|
|
Model = self.registry['test_convert.test_model']
|
|
call_args = []
|
|
|
|
def _load_records(self, data_list, update=False):
|
|
call_args.append(data_list)
|
|
# pylint: disable=bad-super-call
|
|
return super(Model, self)._load_records(data_list, update=update)
|
|
|
|
self.patch(Model, '_load_records', _load_records)
|
|
|
|
# import a record with a subrecord
|
|
xml = ET.fromstring("""
|
|
<record id="test_convert.test_o2m_record" model="test_convert.test_model">
|
|
<field name="usered_ids">
|
|
<record id="test_convert.test_o2m_subrecord" model="test_convert.usered">
|
|
<field name="name">subrecord</field>
|
|
</record>
|
|
</field>
|
|
</record>
|
|
""".strip())
|
|
obj = xml_import(self.env, 'test_convert', None, 'init')
|
|
obj._tag_record(xml)
|
|
|
|
# check that field 'usered_ids' is not passed
|
|
self.assertEqual(len(call_args), 1)
|
|
for data in call_args[0]:
|
|
self.assertNotIn('usered_ids', data['values'],
|
|
"Unexpected value in O2M When loading XML with sub records")
|
|
|
|
def test_translated_field(self):
|
|
"""Tests importing a data file with a lang set in the environment sets the source (en_US)
|
|
rather than setting the translation (fr_FR)
|
|
This is currently the expected behavior.
|
|
Maybe one day we would like that `convert_file` and `xml_import` respect the environment lang,
|
|
but currently the API, and code blocks using `convert_file`/`xml_import` expect to set the source (en_US)
|
|
"""
|
|
self.env['res.lang']._activate_lang('fr_FR')
|
|
env_fr = self.env(context=dict(self.env.context, lang='fr_FR'))
|
|
record = self.env.ref('test_convert.test_translated_field')
|
|
|
|
# 1. Test xml_import, which is sometimes imported and used directly in addons' code
|
|
# Change the value of the record `name` field
|
|
record.name = 'bar'
|
|
self.assertEqual(record.name, 'bar')
|
|
# Reset the value to the one from the XML data file,
|
|
# with a lang passed in the environment.
|
|
filepath = get_resource_path('test_convert', 'data/test_translated_field/test_model_data.xml')
|
|
doc = ET.parse(filepath)
|
|
obj = xml_import(env_fr, 'test_convert', {}, mode='init', xml_filename=filepath)
|
|
obj.parse(doc.getroot())
|
|
self.assertEqual(record.with_context(lang=None).name, 'foo')
|
|
|
|
# 2. Test convert_file with an XML
|
|
# Change the value of the record `name` field
|
|
record.name = 'bar'
|
|
self.assertEqual(record.name, 'bar')
|
|
# Reset the value to the one from the XML data file,
|
|
# with a lang passed in the environment.
|
|
convert_file(env_fr, 'test_convert', 'data/test_translated_field/test_model_data.xml', {})
|
|
self.assertEqual(record.with_context(lang=None).name, 'foo')
|
|
|
|
# 3. Test convert_file with a CSV
|
|
# Change the value of the record `name` field
|
|
record.name = 'bar'
|
|
self.assertEqual(record.name, 'bar')
|
|
# Reset the value to the one from the XML data file,
|
|
# with a lang passed in the environment.
|
|
convert_file(env_fr, 'test_convert', 'data/test_translated_field/test_convert.test_model.csv', {})
|
|
self.assertEqual(record.with_context(lang=None).name, 'foo')
|
|
|
|
@unittest.skip("not tested")
|
|
def test_xml(self):
|
|
pass
|
|
|
|
def test_html(self):
|
|
self.assertEqual(
|
|
self.eval_xml(Field(ET.fromstring(
|
|
"""<parent>
|
|
<t t-if="True">
|
|
<t t-out="'text'"/>
|
|
</t>
|
|
<t t-else="">
|
|
<t t-out="'text2'"></t>
|
|
</t>
|
|
</parent>"""), type="html")),
|
|
"""<parent>
|
|
<t t-if="True">
|
|
<t t-out="'text'"></t>
|
|
</t>
|
|
<t t-else="">
|
|
<t t-out="'text2'"></t>
|
|
</t>
|
|
</parent>""",
|
|
"Evaluating an HTML field should give empty nodes instead of self-closing tags"
|
|
)
|