Skip to content

Commit 864afe0

Browse files
andreabakaj-fuentes
andcommitted
[ADD] util.fields: handle ir.exports model/fields renames/removal
Implement proper fixing and/or removal of ir.exports and ir.exports.line records on model/fields renames/removal: - renamed fields: update affected ir.exports.line records - removed fields: remove affected ir.exports.line records - renamed models: update affected ir.exports records `resource` - removed models: remove affected ir.exports.line / ir.exports records Some of these cases were already handled but have been improved. Specifically: - for renamed fields, previously was done by doing a simple string replacement, which is not good enough because export lines can reference fields paths, and these in turn *might* contain paths to multiple fields with the same name on different models, only some of which are affected. Now the fields path get properly resolved for renaming, only the affected fields path parts are updated. - for removed fields, previously no action was taken, leaving a broken export template and UI traceback. Now the affected export lines are removed, using fields paths resolution. - for renamed and removed models, this was already handled by the indirect_references mechanism, but now export lines for the removed models are checked with the same mechanism for fields above. Additionally, unit tests have been added to make sure these cases are properly handled by the code. Co-authored-by: Alvaro Fuentes <[email protected]>
1 parent ec8ba46 commit 864afe0

File tree

5 files changed

+121
-17
lines changed

5 files changed

+121
-17
lines changed

src/base/tests/test_util.py

+57
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,63 @@ def test_remove_field(self, domain, expected):
407407
self.assertEqual(altered_domain, expected)
408408

409409

410+
class TestIrExports(UnitTestCase):
411+
def setUp(self):
412+
super().setUp()
413+
self.export = self.env["ir.exports"].create(
414+
[
415+
{
416+
"name": "Test currency export",
417+
"resource": "res.currency",
418+
"export_fields": [
419+
(0, 0, {"name": "full_name"}),
420+
(0, 0, {"name": "rate_ids/company_id/user_ids/name"}),
421+
(0, 0, {"name": "rate_ids/company_id/user_ids/partner_id/user_ids/name"}),
422+
(0, 0, {"name": "rate_ids/name"}),
423+
],
424+
}
425+
]
426+
)
427+
util.flush(self.export)
428+
429+
def _invalidate(self):
430+
util.invalidate(self.export.export_fields)
431+
util.invalidate(self.export)
432+
433+
def test_rename_field(self):
434+
util.rename_field(self.cr, "res.partner", "user_ids", "renamed_user_ids")
435+
self._invalidate()
436+
self.assertEqual(
437+
self.export.export_fields[2].name, "rate_ids/company_id/user_ids/partner_id/renamed_user_ids/name"
438+
)
439+
440+
util.rename_field(self.cr, "res.users", "name", "new_name")
441+
self._invalidate()
442+
self.assertEqual(self.export.export_fields[1].name, "rate_ids/company_id/user_ids/new_name")
443+
444+
def test_remove_field(self):
445+
util.remove_field(self.cr, "res.currency.rate", "company_id")
446+
self._invalidate()
447+
self.assertEqual(len(self.export.export_fields), 2)
448+
self.assertEqual(self.export.export_fields[0].name, "full_name")
449+
self.assertEqual(self.export.export_fields[1].name, "rate_ids/name")
450+
451+
def test_rename_model(self):
452+
util.rename_model(self.cr, "res.currency", "res.currency2")
453+
self._invalidate()
454+
self.assertEqual(self.export.resource, "res.currency2")
455+
456+
def test_remove_model(self):
457+
util.remove_model(self.cr, "res.currency.rate")
458+
self._invalidate()
459+
self.assertEqual(len(self.export.export_fields), 1)
460+
self.assertEqual(self.export.export_fields[0].name, "full_name")
461+
462+
util.remove_model(self.cr, "res.currency")
463+
self.cr.execute("SELECT * FROM ir_exports WHERE id = %s", [self.export.id])
464+
self.assertFalse(self.cr.fetchall())
465+
466+
410467
class TestIterBrowse(UnitTestCase):
411468
def test_iter_browse_iter(self):
412469
cr = self.env.cr

src/util/fields.py

+35-15
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ def make_index_name(table_name, column_name):
4040
from .const import ENVIRON
4141
from .domains import _adapt_one_domain, _replace_path, _valid_path_to, adapt_domains
4242
from .exceptions import SleepyDeveloperError
43-
from .helpers import _dashboard_actions, _validate_model, table_of_model
43+
from .helpers import (
44+
_dashboard_actions,
45+
_remove_export_lines,
46+
_validate_model,
47+
resolve_model_fields_path,
48+
table_of_model,
49+
)
4450
from .inherit import for_each_inherit
4551
from .misc import SelfPrintEvalContext, log_progress, version_gte
4652
from .orm import env, invalidate
@@ -202,6 +208,9 @@ def clean_context(context):
202208
[(fieldname, fieldname + " desc"), model, r"\y{}\y".format(fieldname)],
203209
)
204210

211+
# ir.exports.line
212+
_remove_export_lines(cr, model, fieldname)
213+
205214
def adapter(leaf, is_or, negated):
206215
# replace by TRUE_LEAF, unless negated or in a OR operation but not negated
207216
if is_or ^ negated:
@@ -1071,21 +1080,32 @@ def _update_field_usage_multi(cr, models, old, new, domain_adapter=None, skip_in
10711080
cr.execute(q.format(col_prefix=col_prefix), p)
10721081

10731082
# ir.exports.line
1074-
q = """
1075-
UPDATE ir_exports_line l
1076-
SET name = regexp_replace(l.name, %(old)s, %(new)s, 'g')
1077-
"""
10781083
if only_models:
1079-
q += """
1080-
FROM ir_exports e
1081-
WHERE e.id = l.export_id
1082-
AND e.resource IN %(models)s
1083-
AND
1084-
"""
1085-
else:
1086-
q += "WHERE "
1087-
q += "l.name ~ %(old)s"
1088-
cr.execute(q, p)
1084+
cr.execute(
1085+
"""
1086+
SELECT el.id,
1087+
e.resource,
1088+
STRING_TO_ARRAY(el.name, '/')
1089+
FROM ir_exports_line el
1090+
JOIN ir_exports e
1091+
ON el.export_id = e.id
1092+
WHERE el.name ~ %s
1093+
""",
1094+
[r"\y{}\y".format(old)],
1095+
)
1096+
fixed_lines_paths = {}
1097+
for line_id, line_model, line_path in cr.fetchall():
1098+
new_path = [
1099+
new if x.field_name == old and x.field_model in only_models else x.field_name
1100+
for x in resolve_model_fields_path(cr, line_model, line_path)
1101+
]
1102+
if len(new_path) == len(line_path) and new_path != line_path:
1103+
fixed_lines_paths[line_id] = "/".join(new_path)
1104+
if fixed_lines_paths:
1105+
cr.execute(
1106+
"UPDATE ir_exports_line SET name = (%s::jsonb)->>(id::text) WHERE id IN %s",
1107+
[Json(fixed_lines_paths), tuple(fixed_lines_paths)],
1108+
)
10891109

10901110
# mail.alias
10911111
if column_exists(cr, "mail_alias", "alias_defaults"):

src/util/helpers.py

+24
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,27 @@ def resolve_model_fields_path(cr, model, path):
315315
{"model": model, "path": list(path)},
316316
)
317317
return [FieldsPathPart(**row) for row in cr.dictfetchall()]
318+
319+
320+
def _remove_export_lines(cr, model, field=None):
321+
q = """
322+
SELECT el.id,
323+
e.resource,
324+
STRING_TO_ARRAY(el.name, '/')
325+
FROM ir_exports_line el
326+
JOIN ir_exports e
327+
ON el.export_id = e.id
328+
"""
329+
if field:
330+
q = cr.mogrify(q + " WHERE el.name ~ %s ", [r"\y{}\y".format(field)]).decode()
331+
cr.execute(q)
332+
to_rem = [
333+
line_id
334+
for line_id, line_model, line_path in cr.fetchall()
335+
if any(
336+
x.field_model == model and (field is None or x.field_name == field)
337+
for x in resolve_model_fields_path(cr, line_model, line_path)
338+
)
339+
]
340+
if to_rem:
341+
cr.execute("DELETE FROM ir_exports_line WHERE id IN %s", [tuple(to_rem)])

src/util/indirect_references.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def indirect_references(cr, bound_only=False):
3939
IR("ir_model_fields", "relation", None), # destination of a relation field
4040
IR("ir_model_data", "model", "res_id"),
4141
IR("ir_filters", "model_id", None, set_unknown=True), # YUCK!, not an id
42-
IR("ir_exports", "resource", None, set_unknown=True),
42+
IR("ir_exports", "resource", None),
4343
IR("ir_ui_view", "model", None, set_unknown=True),
4444
IR("ir_values", "model", "res_id"),
4545
IR("wkf_transition", "trigger_model", None),

src/util/models.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from .const import ENVIRON
1313
from .fields import IMD_FIELD_PATTERN, remove_field
14-
from .helpers import _ir_values_value, _validate_model, model_of_table, table_of_model
14+
from .helpers import _ir_values_value, _remove_export_lines, _validate_model, model_of_table, table_of_model
1515
from .indirect_references import indirect_references
1616
from .inherit import for_each_inherit, inherit_parents
1717
from .misc import _cached, chunks, log_progress
@@ -129,6 +129,9 @@ def remove_model(cr, model, drop_table=True, ignore_m2m=()):
129129
cr.execute(query, args + (tuple(ids),))
130130
notify = notify or bool(cr.rowcount)
131131

132+
# for ir.exports.line we have to take care of "nested" references in fields "paths"
133+
_remove_export_lines(cr, model)
134+
132135
_rm_refs(cr, model)
133136

134137
cr.execute(

0 commit comments

Comments
 (0)