Skip to content

Commit 82aa584

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 69ca39d commit 82aa584

File tree

3 files changed

+132
-17
lines changed

3 files changed

+132
-17
lines changed

src/base/tests/test_util.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +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.domains import _adapt_one_domain
22+
from odoo.addons.base.maintenance.migrations.util.domains import _adapt_one_domain, _model_of_path
2323
from odoo.addons.base.maintenance.migrations.util.exceptions import MigrationError
2424

2525

@@ -42,6 +42,22 @@ def test_adapt_renamed_field(self):
4242

4343
self.assertEqual(match_domain, new_domain)
4444

45+
@parametrize(
46+
[
47+
("res.currency", [], "res.currency"),
48+
("res.currency", ["rate_ids"], "res.currency.rate"),
49+
("res.currency", ("rate_ids", "company_id"), "res.company"),
50+
("res.currency", ["rate_ids", "company_id", "user_ids"], "res.users"),
51+
("res.currency", ("rate_ids", "company_id", "user_ids", "partner_id"), "res.partner"),
52+
("res.users", ["partner_id"], "res.partner"),
53+
("res.users", ["nonexistent_field"], None),
54+
("res.users", ("partner_id", "removed_field"), None),
55+
]
56+
)
57+
def test_model_of_path(self, model, path, expected):
58+
cr = self.env.cr
59+
self.assertEqual(_model_of_path(cr, model, path), expected)
60+
4561
def test_change_no_leaf(self):
4662
# testing plan: updata path of a domain where the last element is not changed
4763

@@ -670,6 +686,30 @@ def test_model_table_convertion(self):
670686
self.assertEqual(table, self.env[model]._table)
671687
self.assertEqual(util.model_of_table(cr, table), model)
672688

689+
def test_resolve_model_fields_path(self):
690+
from odoo.addons.base.maintenance.migrations.util.helpers import FieldsPathPart, resolve_model_fields_path
691+
692+
cr = self.env.cr
693+
694+
# test with provided paths
695+
model, path = "res.currency", ["rate_ids", "company_id", "user_ids", "partner_id"]
696+
expected_result = [
697+
FieldsPathPart(model, path, 1, "res.currency", "rate_ids", "res.currency.rate"),
698+
FieldsPathPart(model, path, 2, "res.currency.rate", "company_id", "res.company"),
699+
FieldsPathPart(model, path, 3, "res.company", "user_ids", "res.users"),
700+
FieldsPathPart(model, path, 4, "res.users", "partner_id", "res.partner"),
701+
]
702+
result = resolve_model_fields_path(cr, model, path)
703+
self.assertEqual(result, expected_result)
704+
705+
model, path = "res.users", ("partner_id", "removed_field", "user_id")
706+
expected_result = [
707+
FieldsPathPart(model, list(path), 1, "res.users", "partner_id", "res.partner"),
708+
FieldsPathPart(model, list(path), 2, "res.partner", "removed_field", None),
709+
]
710+
result = resolve_model_fields_path(cr, model, path)
711+
self.assertEqual(result, expected_result)
712+
673713

674714
@unittest.skipIf(
675715
util.version_gte("saas~17.1"),

src/util/domains.py

Lines changed: 11 additions & 16 deletions
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

Lines changed: 80 additions & 0 deletions
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)