Skip to content

Commit 70cdb94

Browse files
committed
fixup! [ADD] util.fields: handle ir.exports model/fields renames/removal
1 parent 162fed3 commit 70cdb94

File tree

3 files changed

+61
-127
lines changed

3 files changed

+61
-127
lines changed

src/util/fields.py

+34-124
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import psycopg2
1818
from psycopg2 import sql
19-
from psycopg2.extras import Json, execute_values
19+
from psycopg2.extras import Json
2020

2121
try:
2222
from odoo import release
@@ -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, resolve_model_fields_path, 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
@@ -79,126 +85,6 @@ def make_index_name(table_name, column_name):
7985
)
8086

8187

82-
def _get_resolved_ir_exports(cr, models=None, fields=None):
83-
"""
84-
Return a list of ir.exports.line records which models or fields match the given arguments.
85-
86-
Export lines can reference nested models through relationship field "paths"
87-
(e.g. "partner_id/country_id/name"), therefore these needs to be resolved properly.
88-
89-
Only one of ``models`` or ``fields`` arguments should be provided.
90-
91-
:param list[str] models: a list of model names to match in exports
92-
:param list[(str, str)] fields: a list of (model, field) tuples to match in exports
93-
:return: the resolved field paths parts for each matched export line id
94-
:rtype: dict[int, list[FieldsPathPart]]
95-
96-
:meta private: exclude from online docs
97-
"""
98-
assert bool(models) ^ bool(fields), "One of models or fields must be given, and not both."
99-
100-
# Get the model fields paths for exports.
101-
# When matching fields we can already broadly filter on field names (will be double-checked later).
102-
# When matching models we can't exclude anything because we don't know intermediate models.
103-
where = ""
104-
params = {}
105-
if fields:
106-
fields = {(model, fields) for model, fields in fields} # noqa: C416 # make sure set[tuple]
107-
where = "WHERE el.name ~ ANY(%(field_names)s)"
108-
params["field_names"] = [f[1] for f in fields]
109-
cr.execute(
110-
"""
111-
SELECT el.id, e.resource AS model, string_to_array(el.name, '/') AS path
112-
FROM ir_exports e
113-
JOIN ir_exports_line el ON e.id = el.export_id
114-
{where}
115-
""".format(where=where),
116-
params,
117-
)
118-
paths_to_line_ids = {}
119-
for line_id, model, path in cr.fetchall():
120-
paths_to_line_ids.setdefault((model, tuple(path)), set()).add(line_id)
121-
122-
# Resolve intermediate models for all model fields paths, filter only matching paths parts
123-
matching_paths_parts = {}
124-
for model, path in paths_to_line_ids:
125-
resolved_paths = resolve_model_fields_path(cr, model, path)
126-
if fields:
127-
matching_parts = [p for p in resolved_paths if (p.field_model, p.field_name) in fields]
128-
else:
129-
matching_parts = [p for p in resolved_paths if p.field_model in models]
130-
if not matching_parts:
131-
continue
132-
matching_paths_parts[(model, path)] = matching_parts
133-
134-
# Return the matched parts for each export line id
135-
result = {}
136-
for (model, path), matching_parts in matching_paths_parts.items():
137-
line_ids = paths_to_line_ids.get((model, path))
138-
if not line_ids:
139-
continue # wut?
140-
for line_id in line_ids:
141-
result.setdefault(line_id, []).extend(matching_parts)
142-
return result
143-
144-
145-
def rename_ir_exports_fields(cr, models_fields_map):
146-
"""
147-
Rename fields references in ir.exports.line records.
148-
149-
:param dict[str, dict[str, str]] models_fields_map: a dict of models to the fields rename dict,
150-
like: `{"model.name": {"old_field": "new_field", ...}, ...}`
151-
152-
:meta private: exclude from online docs
153-
"""
154-
matching_exports = _get_resolved_ir_exports(
155-
cr,
156-
fields=[(model, field) for model, fields_map in models_fields_map.items() for field in fields_map],
157-
)
158-
if not matching_exports:
159-
return
160-
_logger.debug("Renaming %d export template lines with renamed fields", len(matching_exports))
161-
fixed_lines_paths = {}
162-
for line_id, resolved_paths in matching_exports.items():
163-
for path_part in resolved_paths:
164-
assert path_part.field_model in models_fields_map
165-
fields_map = models_fields_map[path_part.field_model]
166-
assert path_part.field_name in fields_map
167-
assert path_part.path[path_part.part_index - 1] == path_part.field_name
168-
new_field_name = fields_map[path_part.field_name]
169-
fixed_path = fixed_lines_paths.get(line_id, list(path_part.path))
170-
fixed_path[path_part.part_index - 1] = new_field_name
171-
fixed_lines_paths[line_id] = fixed_path
172-
execute_values(
173-
cr,
174-
"""
175-
UPDATE ir_exports_line el
176-
SET name = v.name
177-
FROM (VALUES %s) AS v(id, name)
178-
WHERE el.id = v.id
179-
""",
180-
[(k, "/".join(v)) for k, v in fixed_lines_paths.items()],
181-
)
182-
183-
184-
def remove_ir_exports_lines(cr, models=None, fields=None):
185-
"""
186-
Delete ir.exports.line records that reference models or fields that are/will be removed.
187-
188-
Only one of ``models`` or ``fields`` arguments should be provided.
189-
190-
:param list[str] models: a list of model names to match in exports
191-
:param list[(str, str)] fields: a list of (model, field) tuples to match in exports
192-
193-
:meta private: exclude from online docs
194-
"""
195-
matching_exports = _get_resolved_ir_exports(cr, models=models, fields=fields)
196-
if not matching_exports:
197-
return
198-
_logger.debug("Deleting %d export template lines with removed models/fields", len(matching_exports))
199-
cr.execute("DELETE FROM ir_exports_line WHERE id IN %s", [tuple(matching_exports.keys())])
200-
201-
20288
def ensure_m2o_func_field_data(cr, src_table, column, dst_table):
20389
"""
20490
Fix broken m2o relations.
@@ -323,7 +209,7 @@ def clean_context(context):
323209
)
324210

325211
# ir.exports.line
326-
remove_ir_exports_lines(cr, fields=[(model, fieldname)])
212+
_remove_export_lines(cr, model, fieldname)
327213

328214
def adapter(leaf, is_or, negated):
329215
# replace by TRUE_LEAF, unless negated or in a OR operation but not negated
@@ -1195,7 +1081,31 @@ def _update_field_usage_multi(cr, models, old, new, domain_adapter=None, skip_in
11951081

11961082
# ir.exports.line
11971083
if only_models:
1198-
rename_ir_exports_fields(cr, {model: {old: new} for model in only_models})
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+
)
11991109

12001110
# mail.alias
12011111
if column_exists(cr, "mail_alias", "alias_defaults"):

src/util/helpers.py

+24
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,27 @@ def resolve_model_fields_path(cr, model, path):
292292
{"model": model, "path": list(path)},
293293
)
294294
return [FieldsPathPart(**row) for row in cr.dictfetchall()]
295+
296+
297+
def _remove_export_lines(cr, model, field=None):
298+
q = """
299+
SELECT el.id,
300+
e.resource,
301+
STRING_TO_ARRAY(el.name, '/')
302+
FROM ir_exports_line el
303+
JOIN ir_exports e
304+
ON el.export_id = e.id
305+
"""
306+
if field:
307+
q = cr.mogrify(q + " WHERE el.name ~ %s ", [r"\y{}\y".format(field)]).decode()
308+
cr.execute(q)
309+
to_rem = [
310+
line_id
311+
for line_id, line_model, line_path in cr.fetchall()
312+
if any(
313+
x.field_model == model and (field is None or x.field_name == field)
314+
for x in resolve_model_fields_path(cr, line_model, line_path)
315+
)
316+
]
317+
if to_rem:
318+
cr.execute("DELETE FROM ir_exports_line WHERE id IN %s", [tuple(to_rem)])

src/util/models.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import re
1111

1212
from .const import ENVIRON
13-
from .fields import IMD_FIELD_PATTERN, remove_field, remove_ir_exports_lines
14-
from .helpers import _ir_values_value, _validate_model, model_of_table, table_of_model
13+
from .fields import IMD_FIELD_PATTERN, remove_field
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
@@ -130,7 +130,7 @@ def remove_model(cr, model, drop_table=True, ignore_m2m=()):
130130
notify = notify or bool(cr.rowcount)
131131

132132
# for ir.exports.line we have to take care of "nested" references in fields "paths"
133-
remove_ir_exports_lines(cr, models=[model])
133+
_remove_export_lines(cr, model)
134134

135135
_rm_refs(cr, model)
136136

0 commit comments

Comments
 (0)