Skip to content

Commit 7d5536d

Browse files
andreabakaj-fuentes
andcommitted
[REF] util.helpers: introduce new resolve_model_fields_path helper
It is a common occurrence to have models metadata values that use a "path of fields" approach (e.g. fields depends, domains, server actions, import/export templates, etc.) and to effectively resolve those with all the intermediate model+fields references of the path parts, either a for loop is used in python, issuing multple queries, or a recursive CTE query that does that resolution entirely within PostgreSQL. In this commit a new `resolve_model_fields_path` helper is introduced using a recursive CTE to replace some older code using the python-loop approach. An additional `FieldsPathPart` named tuple type is added to represent information of the resolved part of a fields path, and the helper will return a list of these for callers to then act upon. Co-authored-by: Alvaro Fuentes <[email protected]>
1 parent 5f83f3a commit 7d5536d

File tree

3 files changed

+132
-17
lines changed

3 files changed

+132
-17
lines changed

src/base/tests/test_util.py

+41-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from odoo.addons.base.maintenance.migrations import util
2020
from odoo.addons.base.maintenance.migrations.testing import UnitTestCase, parametrize
21-
from odoo.addons.base.maintenance.migrations.util.domains import _adapt_one_domain
21+
from odoo.addons.base.maintenance.migrations.util.domains import _adapt_one_domain, _model_of_path
2222
from odoo.addons.base.maintenance.migrations.util.exceptions import MigrationError
2323

2424

@@ -27,6 +27,22 @@ def setUp(self):
2727
super(TestAdaptOneDomain, self).setUp()
2828
self.mock_adapter = mock.Mock()
2929

30+
@parametrize(
31+
[
32+
("res.currency", [], "res.currency"),
33+
("res.currency", ["rate_ids"], "res.currency.rate"),
34+
("res.currency", ("rate_ids", "company_id"), "res.company"),
35+
("res.currency", ["rate_ids", "company_id", "user_ids"], "res.users"),
36+
("res.currency", ("rate_ids", "company_id", "user_ids", "partner_id"), "res.partner"),
37+
("res.users", ["partner_id"], "res.partner"),
38+
("res.users", ["nonexistent_field"], None),
39+
("res.users", ("partner_id", "removed_field"), None),
40+
]
41+
)
42+
def test_model_of_path(self, model, path, expected):
43+
cr = self.env.cr
44+
self.assertEqual(_model_of_path(cr, model, path), expected)
45+
3046
def test_change_no_leaf(self):
3147
# testing plan: updata path of a domain where the last element is not changed
3248

@@ -655,6 +671,30 @@ def test_model_table_convertion(self):
655671
self.assertEqual(table, self.env[model]._table)
656672
self.assertEqual(util.model_of_table(cr, table), model)
657673

674+
def test_resolve_model_fields_path(self):
675+
from odoo.addons.base.maintenance.migrations.util.helpers import FieldsPathPart, resolve_model_fields_path
676+
677+
cr = self.env.cr
678+
679+
# test with provided paths
680+
model, path = "res.currency", ["rate_ids", "company_id", "user_ids", "partner_id"]
681+
expected_result = [
682+
FieldsPathPart(model, path, 1, "res.currency", "rate_ids", "res.currency.rate"),
683+
FieldsPathPart(model, path, 2, "res.currency.rate", "company_id", "res.company"),
684+
FieldsPathPart(model, path, 3, "res.company", "user_ids", "res.users"),
685+
FieldsPathPart(model, path, 4, "res.users", "partner_id", "res.partner"),
686+
]
687+
result = resolve_model_fields_path(cr, model, path)
688+
self.assertEqual(result, expected_result)
689+
690+
model, path = "res.users", ("partner_id", "removed_field", "user_id")
691+
expected_result = [
692+
FieldsPathPart(model, list(path), 1, "res.users", "partner_id", "res.partner"),
693+
FieldsPathPart(model, list(path), 2, "res.partner", "removed_field", None),
694+
]
695+
result = resolve_model_fields_path(cr, model, path)
696+
self.assertEqual(result, expected_result)
697+
658698

659699
@unittest.skipIf(
660700
util.version_gte("saas~17.1"),

src/util/domains.py

+11-16
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from openerp.tools.safe_eval import safe_eval
3434

3535
from .const import NEARLYWARN
36-
from .helpers import _dashboard_actions, _validate_model
36+
from .helpers import _dashboard_actions, _validate_model, resolve_model_fields_path
3737
from .inherit import for_each_inherit
3838
from .misc import SelfPrintEvalContext
3939
from .pg import column_exists, get_value_or_en_translation, table_exists
@@ -160,21 +160,16 @@ def _get_domain_fields(cr):
160160

161161

162162
def _model_of_path(cr, model, path):
163-
for field in path:
164-
cr.execute(
165-
"""
166-
SELECT relation
167-
FROM ir_model_fields
168-
WHERE model = %s
169-
AND name = %s
170-
""",
171-
[model, field],
172-
)
173-
if not cr.rowcount:
174-
return None
175-
[model] = cr.fetchone()
176-
177-
return model
163+
if not path:
164+
return model
165+
path = tuple(path)
166+
resolved_parts = resolve_model_fields_path(cr, model, path)
167+
if not resolved_parts:
168+
return None
169+
last_part = resolved_parts[-1]
170+
if last_part.part_index != len(last_part.path):
171+
return None
172+
return last_part.relation_model # could be None
178173

179174

180175
def _valid_path_to(cr, path, from_, to):

src/util/helpers.py

+80
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import logging
33
import os
4+
from collections import namedtuple
45

56
import lxml
67

@@ -214,3 +215,82 @@ def _get_theme_models():
214215
"theme.website.menu": "website.menu",
215216
"theme.ir.attachment": "ir.attachment",
216217
}
218+
219+
220+
FieldsPathPart = namedtuple(
221+
"FieldsPathPart",
222+
"model path part_index field_model field_name relation_model",
223+
)
224+
FieldsPathPart.__doc__ = """
225+
Encapsulate information about a field within a fields path.
226+
227+
:param str model: model to resolve the fields ``path`` from
228+
:param typing.Sequence[str] path: fields path starting from ``model``
229+
:param int part_index: index of this field in ``path``
230+
:param str field_model: model of the field
231+
:param str field_name: name of the field
232+
:param str relation_model: target model of the field, if relational, otherwise ``None``
233+
"""
234+
for _f in FieldsPathPart._fields:
235+
getattr(FieldsPathPart, _f).__doc__ = None
236+
237+
238+
def resolve_model_fields_path(cr, model, path):
239+
"""
240+
Resolve model fields paths.
241+
242+
This function returns a list of :class:`~odoo.upgrade.util.helpers.FieldsPathPart` where
243+
each item describes the field in ``path`` (in the same order).
244+
245+
.. example::
246+
247+
To get the information about the fields path ``user_ids.partner_id.title_id``
248+
from model `res.users`, we can call this function as
249+
250+
.. code-block:: python
251+
252+
resolve_model_fields_path(cr, "res.users", "user_ids.partner_id.title_id".split("."))
253+
254+
:param str model: model to resolve the fields ``path`` from
255+
:param typing.Sequence[str] path: fields path starting from ``model``
256+
:return: list of resolved fields path parts
257+
:rtype: list(:class:`~odoo.upgrade.util.helpers.FieldsPathPart`)
258+
"""
259+
path = list(path)
260+
cr.execute(
261+
"""
262+
WITH RECURSIVE resolved_fields_path AS (
263+
-- non-recursive term
264+
SELECT p.model AS model,
265+
p.path AS path,
266+
1 AS part_index,
267+
p.model AS field_model,
268+
p.path[1] AS field_name,
269+
imf.relation AS relation_model
270+
FROM (VALUES (%(model)s, %(path)s)) p(model, path)
271+
LEFT JOIN ir_model_fields imf
272+
ON imf.model = p.model
273+
AND imf.name = p.path[1]
274+
275+
UNION ALL
276+
277+
-- recursive term
278+
SELECT rfp.model,
279+
rfp.path,
280+
rfp.part_index + 1 AS part_index,
281+
rfp.relation_model AS field_model,
282+
rfp.path[rfp.part_index + 1] AS field_name,
283+
rimf.relation AS relation_model
284+
FROM resolved_fields_path rfp
285+
LEFT JOIN ir_model_fields rimf
286+
ON rimf.model = rfp.relation_model
287+
AND rimf.name = rfp.path[rfp.part_index + 1]
288+
WHERE cardinality(rfp.path) > rfp.part_index
289+
AND rfp.relation_model IS NOT NULL
290+
)
291+
SELECT * FROM resolved_fields_path
292+
ORDER BY model, path, part_index
293+
""",
294+
{"model": model, "path": list(path)},
295+
)
296+
return [FieldsPathPart(**row) for row in cr.dictfetchall()]

0 commit comments

Comments
 (0)