Skip to content

Commit 0063afc

Browse files
andreabakaj-fuentes
andcommitted
[IMP] 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. Part-of: odoo#84 Signed-off-by: Christophe Simonis (chs) <[email protected]> Co-authored-by: Alvaro Fuentes <[email protected]>
1 parent 69ca39d commit 0063afc

File tree

3 files changed

+147
-17
lines changed

3 files changed

+147
-17
lines changed

src/base/tests/test_util.py

Lines changed: 38 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,24 @@ 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", "active"), None),
55+
("res.users", ("partner_id", "active", "name"), None),
56+
("res.users", ("partner_id", "removed_field"), None),
57+
]
58+
)
59+
def test_model_of_path(self, model, path, expected):
60+
cr = self.env.cr
61+
self.assertEqual(_model_of_path(cr, model, path), expected)
62+
4563
def test_change_no_leaf(self):
4664
# testing plan: updata path of a domain where the last element is not changed
4765

@@ -670,6 +688,25 @@ def test_model_table_convertion(self):
670688
self.assertEqual(table, self.env[model]._table)
671689
self.assertEqual(util.model_of_table(cr, table), model)
672690

691+
def test_resolve_model_fields_path(self):
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+
util.FieldsPathPart("res.currency", "rate_ids", "res.currency.rate"),
698+
util.FieldsPathPart("res.currency.rate", "company_id", "res.company"),
699+
util.FieldsPathPart("res.company", "user_ids", "res.users"),
700+
util.FieldsPathPart("res.users", "partner_id", "res.partner"),
701+
]
702+
result = util.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 = [util.FieldsPathPart("res.users", "partner_id", "res.partner")]
707+
result = util.resolve_model_fields_path(cr, model, path)
708+
self.assertEqual(result, expected_result)
709+
673710

674711
@unittest.skipIf(
675712
util.version_gte("saas~17.1"),

src/util/domains.py

Lines changed: 8 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,13 @@ 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 len(resolved_parts) == len(path):
168+
return resolved_parts[-1].relation_model
169+
return None
178170

179171

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

src/util/helpers.py

Lines changed: 101 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,103 @@ def _get_theme_models():
214215
"theme.website.menu": "website.menu",
215216
"theme.ir.attachment": "ir.attachment",
216217
}
218+
219+
220+
FieldsPathPart = namedtuple("FieldsPathPart", "field_model field_name relation_model")
221+
FieldsPathPart.__doc__ = """
222+
Encapsulate information about a field within a fields path.
223+
224+
:param str field_model: model of the field
225+
:param str field_name: name of the field
226+
:param str relation_model: target model of the field, if relational, otherwise ``None``
227+
"""
228+
for _f in FieldsPathPart._fields:
229+
getattr(FieldsPathPart, _f).__doc__ = None
230+
231+
232+
def resolve_model_fields_path(cr, model, path):
233+
"""
234+
Resolve model fields paths.
235+
236+
This function returns a list of :class:`~odoo.upgrade.util.helpers.FieldsPathPart`
237+
where each item describes a field in ``path`` (in the same order). The returned list
238+
could be shorter than the original ``path`` due to a missing field or model, or
239+
because there is a non-relational field in the path. The only non-relational field
240+
allowed in a fields path is the last one, in which case the returned list has the same
241+
length as the input ``path``.
242+
243+
.. example::
244+
245+
.. code-block:: python
246+
247+
>>> util.resolve_model_fields_path(cr, "res.partner", "user_ids.partner_id.title".split("."))
248+
[FieldsPathPart(field_model='res.partner', field_name='user_ids', relation_model='res.users'),
249+
FieldsPathPart(field_model='res.users', field_name='partner_id', relation_model='res.partner'),
250+
FieldsPathPart(field_model='res.partner', field_name='title', relation_model='res.partner.title')]
251+
252+
Last field is not relational:
253+
254+
.. code-block:: python
255+
256+
>>> resolve_model_fields_path(cr, "res.partner", "user_ids.active".split("."))
257+
[FieldsPathPart(field_model='res.partner', field_name='user_ids', relation_model='res.users'),
258+
FieldsPathPart(field_model='res.users', field_name='active', relation_model=None)]
259+
260+
The path is wrong, it uses a non-relational field:
261+
262+
.. code-block:: python
263+
264+
>>> resolve_model_fields_path(cr, "res.partner", "user_ids.active.name".split("."))
265+
[FieldsPathPart(field_model='res.partner', field_name='user_ids', relation_model='res.users'),
266+
FieldsPathPart(field_model='res.users', field_name='active', relation_model=None)]
267+
268+
The path is broken, it uses a non-existing field:
269+
270+
.. code-block:: python
271+
272+
>>> resolve_model_fields_path(cr, "res.partner", "user_ids.non_existing_id.active".split("."))
273+
[FieldsPathPart(field_model='res.partner', field_name='user_ids', relation_model='res.users')]
274+
275+
:param str model: starting model of the fields path
276+
:param typing.Sequence[str] path: fields path
277+
:return: resolved fields path parts
278+
:rtype: list(:class:`~odoo.upgrade.util.helpers.FieldsPathPart`)
279+
"""
280+
path = list(path)
281+
cr.execute(
282+
"""
283+
WITH RECURSIVE resolved_fields_path AS (
284+
-- non-recursive term
285+
SELECT imf.model AS field_model,
286+
imf.name AS field_name,
287+
imf.relation AS relation_model,
288+
p.path AS path,
289+
1 AS part_index
290+
FROM (VALUES (%(model)s, %(path)s)) p(model, path)
291+
JOIN ir_model_fields imf
292+
ON imf.model = p.model
293+
AND imf.name = p.path[1]
294+
295+
UNION ALL
296+
297+
-- recursive term
298+
SELECT rimf.model AS field_model,
299+
rimf.name AS field_name,
300+
rimf.relation AS relation_model,
301+
rfp.path AS path,
302+
rfp.part_index + 1 AS part_index
303+
FROM resolved_fields_path rfp
304+
JOIN ir_model_fields rimf
305+
ON rimf.model = rfp.relation_model
306+
AND rimf.name = rfp.path[rfp.part_index + 1]
307+
WHERE cardinality(rfp.path) > rfp.part_index
308+
)
309+
SELECT field_model,
310+
field_name,
311+
relation_model
312+
FROM resolved_fields_path
313+
ORDER BY part_index
314+
""",
315+
{"model": model, "path": list(path)},
316+
)
317+
return [FieldsPathPart(**row) for row in cr.dictfetchall()]

0 commit comments

Comments
 (0)