Skip to content

Commit 6aa4106

Browse files
KangOlnseinlet
andcommitted
[FIX] util.convert_html_content: correctly handle QWeb
The QWeb view should be parsed and dumped as XML, not HTML. Use the XHTML parser to still have `HtmlElement`s passed to the callback functions. closes #125 Signed-off-by: Christophe Simonis (chs) <[email protected]> Co-authored-by: Nicolas Seinlet <[email protected]>
1 parent aff400b commit 6aa4106

File tree

2 files changed

+95
-16
lines changed

2 files changed

+95
-16
lines changed

src/base/tests/test_util.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from odoo.addons.base.maintenance.migrations import util
2121
from odoo.addons.base.maintenance.migrations.testing import UnitTestCase, parametrize
22+
from odoo.addons.base.maintenance.migrations.util import snippets
2223
from odoo.addons.base.maintenance.migrations.util.domains import _adapt_one_domain, _model_of_path
2324
from odoo.addons.base.maintenance.migrations.util.exceptions import MigrationError
2425

@@ -1371,6 +1372,44 @@ def test_SelfPrint(self, value):
13711372
self.assertEqual(str(evaluated), value)
13721373

13731374

1375+
def not_doing_anything_converter(el):
1376+
return True
1377+
1378+
1379+
class TestHTMLFormat(UnitTestCase):
1380+
def testsnip(self):
1381+
view_arch = """
1382+
<html>
1383+
<div class="fake_class_not_doing_anything"><br/></div>
1384+
<script>
1385+
(event) =&gt; {
1386+
};
1387+
</script>
1388+
</html>
1389+
"""
1390+
view_id = self.env["ir.ui.view"].create(
1391+
{
1392+
"name": "not_for_anything",
1393+
"type": "qweb",
1394+
"mode": "primary",
1395+
"key": "test.htmlconvert",
1396+
"arch_db": view_arch,
1397+
}
1398+
)
1399+
cr = self.env.cr
1400+
snippets.convert_html_content(
1401+
cr,
1402+
snippets.html_converter(
1403+
not_doing_anything_converter, selector="//*[hasclass('fake_class_not_doing_anything')]"
1404+
),
1405+
)
1406+
util.invalidate(view_id)
1407+
res = self.env["ir.ui.view"].search_read([("id", "=", view_id.id)], ["arch_db"])
1408+
self.assertEqual(len(res), 1)
1409+
oneline = lambda s: re.sub(r"\s+", " ", s.strip())
1410+
self.assertEqual(oneline(res[0]["arch_db"]), oneline(view_arch))
1411+
1412+
13741413
class TestQueryFormat(UnitTestCase):
13751414
@parametrize(
13761415
[

src/util/snippets.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from psycopg2.extensions import quote_ident
1212
from psycopg2.extras import Json
1313

14+
from .const import NEARLYWARN
1415
from .exceptions import MigrationError
1516
from .helpers import table_of_model
1617
from .misc import import_script, log_progress
@@ -157,7 +158,7 @@ def html_converter(transform_callback, selector=None):
157158
:param str selector: targets the elements to loop on
158159
:return: object HTMLConverter with callback
159160
"""
160-
return HTMLConverter(transform_callback, selector)
161+
return HTMLConverter(make_pickleable_callback(transform_callback), selector)
161162

162163

163164
def make_pickleable_callback(callback):
@@ -182,10 +183,16 @@ def make_pickleable_callback(callback):
182183
raise MigrationError(error_msg) from None
183184

184185

185-
class HTMLConverter:
186+
class BaseConverter:
186187
def __init__(self, callback, selector=None):
188+
self.callback = callback
187189
self.selector = selector
188-
self.callback = make_pickleable_callback(callback)
190+
191+
def for_html(self):
192+
return HTMLConverter(self.callback, self.selector)
193+
194+
def for_qweb(self):
195+
return QWebConverter(self.callback, self.selector)
189196

190197
def has_changed(self, els):
191198
if self.selector:
@@ -201,18 +208,39 @@ def __call__(self, content):
201208
# Wrap in <wrap> node before parsing to preserve external comments and multi-root nodes,
202209
# except for when this looks like a full html doc, because in this case the wrap tag breaks the logic in
203210
# https://github.com/lxml/lxml/blob/2ac88908ffd6df380615c0af35f2134325e4bf30/src/lxml/html/html5parser.py#L184
204-
els = html.fromstring(
205-
content if content.strip()[:5].lower() == "<html" else f"<wrap>{content}</wrap>",
206-
parser=utf8_parser,
207-
)
211+
els = self._loads(content if content.strip()[:5].lower() == "<html" else f"<wrap>{content}</wrap>")
208212
has_changed = self.has_changed(els)
209-
new_content = (
210-
re.sub(r"(^<wrap>|</wrap>$|^<wrap/>$)", "", etree.tostring(els, encoding="unicode").strip())
211-
if has_changed
212-
else content
213-
)
213+
new_content = re.sub(r"(^<wrap>|</wrap>$|^<wrap/>$)", "", self._dumps(els).strip()) if has_changed else content
214214
return (has_changed, new_content)
215215

216+
def _loads(self, string):
217+
raise NotImplementedError
218+
219+
def _dumps(self, node):
220+
raise NotImplementedError
221+
222+
223+
class HTMLConverter(BaseConverter):
224+
def for_html(self):
225+
return self
226+
227+
def _loads(self, string):
228+
return html.fromstring(string, parser=utf8_parser)
229+
230+
def _dumps(self, node):
231+
return html.tostring(node, encoding="unicode")
232+
233+
234+
class QWebConverter(BaseConverter):
235+
def for_qweb(self):
236+
return self
237+
238+
def _loads(self, string):
239+
return html.fromstring(string, parser=html.XHTMLParser(encoding="utf-8"))
240+
241+
def _dumps(self, node):
242+
return etree.tostring(node, encoding="unicode")
243+
216244

217245
class Convertor:
218246
def __init__(self, converters, callback):
@@ -320,14 +348,26 @@ def convert_html_content(
320348
- "~* '\yabc.*xyz\y'"
321349
:param dict kwargs: extra keyword arguments to pass to :func:`convert_html_column`
322350
"""
351+
if hasattr(converter_callback, "for_html"): # noqa: SIM108
352+
html_converter = converter_callback.for_html()
353+
else:
354+
# trust the given converter to handle HTML
355+
html_converter = converter_callback
356+
357+
for table, columns in html_fields(cr):
358+
convert_html_columns(cr, table, columns, html_converter, where_column=where_column, **kwargs)
359+
360+
if hasattr(converter_callback, "for_qweb"):
361+
qweb_converter = converter_callback.for_qweb()
362+
else:
363+
_logger.log(NEARLYWARN, "Cannot adapt converter callback %r for qweb; using it directly", converter_callback)
364+
qweb_converter = converter_callback
365+
323366
convert_html_columns(
324367
cr,
325368
"ir_ui_view",
326369
["arch_db"],
327-
converter_callback,
370+
qweb_converter,
328371
where_column=where_column,
329372
**dict(kwargs, extra_where="type = 'qweb'"),
330373
)
331-
332-
for table, columns in html_fields(cr):
333-
convert_html_columns(cr, table, columns, converter_callback, where_column=where_column, **kwargs)

0 commit comments

Comments
 (0)