From b60f0b70444cbbb6bdbc017edee50223fc2c7c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 23 Feb 2024 18:40:52 +0100 Subject: [PATCH] Store model name on ir.model.field --- modules/notification_email/notification.py | 6 +- .../notification_email/tests/test_module.py | 9 +- trytond/CHANGELOG | 1 + trytond/trytond/backend/postgresql/init.sql | 8 +- trytond/trytond/backend/sqlite/init.sql | 2 +- trytond/trytond/ir/__init__.py | 2 +- trytond/trytond/ir/email_.py | 4 +- trytond/trytond/ir/model.py | 144 +++++++++--------- trytond/trytond/ir/translation.py | 8 +- trytond/trytond/ir/view/model_field_form.xml | 4 +- trytond/trytond/ir/view/model_field_list.xml | 2 +- trytond/trytond/model/fields/field.py | 22 +-- trytond/trytond/tests/test_access.py | 10 +- trytond/trytond/tests/test_ir.py | 12 +- 14 files changed, 116 insertions(+), 118 deletions(-) diff --git a/modules/notification_email/notification.py b/modules/notification_email/notification.py index 392f1a020c..99317e3228 100644 --- a/modules/notification_email/notification.py +++ b/modules/notification_email/notification.py @@ -34,7 +34,7 @@ class Email(ModelSQL, ModelView): recipients = fields.Many2One( 'ir.model.field', "Recipients", domain=[ - ('model.model', '=', Eval('model')), + ('model', '=', Eval('model')), ], help="The field that contains the recipient(s).") fallback_recipients = fields.Many2One( @@ -49,7 +49,7 @@ class Email(ModelSQL, ModelView): recipients_secondary = fields.Many2One( 'ir.model.field', "Secondary Recipients", domain=[ - ('model.model', '=', Eval('model')), + ('model', '=', Eval('model')), ], help="The field that contains the secondary recipient(s).") fallback_recipients_secondary = fields.Many2One( @@ -64,7 +64,7 @@ class Email(ModelSQL, ModelView): recipients_hidden = fields.Many2One( 'ir.model.field', "Hidden Recipients", domain=[ - ('model.model', '=', Eval('model')), + ('model', '=', Eval('model')), ], help="The field that contains the hidden recipient(s).") fallback_recipients_hidden = fields.Many2One( diff --git a/modules/notification_email/tests/test_module.py b/modules/notification_email/tests/test_module.py index abbbd1d019..f7d97e490a 100644 --- a/modules/notification_email/tests/test_module.py +++ b/modules/notification_email/tests/test_module.py @@ -34,17 +34,12 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): def _setup_notification(self, recipient_field='create_uid'): pool = Pool() - Model = pool.get('ir.model') ModelField = pool.get('ir.model.field') Action = pool.get('ir.action') Report = pool.get('ir.action.report') User = pool.get('res.user') NotificationEmail = pool.get('notification.email') - model, = Model.search([ - ('model', '=', User.__name__), - ]) - action = Action(name="Notification Email", type='ir.action.report') action.save() report = Report() @@ -52,7 +47,7 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): report.action = action report.template_extension = 'txt' report.report_content = b'Hello ${records[0].name}' - report.model = model.model + report.model = User.__name__ report.save() user = User(Transaction().user) @@ -61,7 +56,7 @@ class NotificationEmailTestCase(CompanyTestMixin, ModuleTestCase): notification_email = NotificationEmail() notification_email.recipients, = ModelField.search([ - ('model.model', '=', model.model), + ('model', '=', User.__name__), ('name', '=', recipient_field), ]) notification_email.content = report diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 7444bb3546..767b491bae 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Store model name on ir.model.field * Add fmany2one field * Allow creation of foreign key on non primary key * Include context information in access error diff --git a/trytond/trytond/backend/postgresql/init.sql b/trytond/trytond/backend/postgresql/init.sql index a0f66f4397..c229c04be3 100644 --- a/trytond/trytond/backend/postgresql/init.sql +++ b/trytond/trytond/backend/postgresql/init.sql @@ -16,10 +16,12 @@ CREATE TABLE ir_model ( PRIMARY KEY(id) ); +ALTER TABLE ir_model ADD CONSTRAINT ir_model_model_uniq UNIQUE (model); + CREATE TABLE ir_model_field ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL CONSTRAINT ir_model_field_id_positive CHECK(id >= 0), - model INTEGER, + model VARCHAR NOT NULL, name VARCHAR NOT NULL, relation VARCHAR, field_description VARCHAR, @@ -28,9 +30,11 @@ CREATE TABLE ir_model_field ( module VARCHAR, "access" BOOL, PRIMARY KEY(id), - FOREIGN KEY (model) REFERENCES ir_model(id) ON DELETE CASCADE + FOREIGN KEY (model) REFERENCES ir_model(model) ON DELETE CASCADE ); +ALTER TABLE ir_model_field ADD CONSTRAINT ir_model_field_name_model_uniq UNIQUE (name, model); + CREATE TABLE ir_ui_view ( id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL CONSTRAINT ir_ui_view_id_positive CHECK(id >= 0), diff --git a/trytond/trytond/backend/sqlite/init.sql b/trytond/trytond/backend/sqlite/init.sql index 5c569a64c0..5da37b73c8 100644 --- a/trytond/trytond/backend/sqlite/init.sql +++ b/trytond/trytond/backend/sqlite/init.sql @@ -22,7 +22,7 @@ CREATE TABLE ir_model ( CREATE TABLE ir_model_field ( id INTEGER PRIMARY KEY AUTOINCREMENT, - model INTEGER, + model VARCHAR, name VARCHAR, relation VARCHAR, field_description VARCHAR, diff --git a/trytond/trytond/ir/__init__.py b/trytond/trytond/ir/__init__.py index 77afb9c839..8595c1e2c8 100644 --- a/trytond/trytond/ir/__init__.py +++ b/trytond/trytond/ir/__init__.py @@ -12,6 +12,7 @@ __all__ = ['register', 'routes'] def register(): Pool.register( + model.ModelField, # register first for model char migration configuration.Configuration, translation.Translation, translation.TranslationSetStart, @@ -42,7 +43,6 @@ def register(): action.ActionWizard, action.ActionURL, model.Model, - model.ModelField, model.ModelAccess, model.ModelFieldAccess, model.ModelButton, diff --git a/trytond/trytond/ir/email_.py b/trytond/trytond/ir/email_.py index 550cf642eb..c5c29bb6e8 100644 --- a/trytond/trytond/ir/email_.py +++ b/trytond/trytond/ir/email_.py @@ -314,11 +314,11 @@ class EmailTemplate(ModelSQL, ModelView): ]: field = getattr(cls, field) field.domain = [ - ('model', '=', Eval('model')), + ('model_ref.id', '=', Eval('model', -1)), ['OR', ('relation', 'in', cls.email_models()), [ - ('model.model', 'in', cls.email_models()), + ('model', 'in', cls.email_models()), ('name', '=', 'id'), ], ] diff --git a/trytond/trytond/ir/model.py b/trytond/trytond/ir/model.py index c71319997b..0f47975faa 100644 --- a/trytond/trytond/ir/model.py +++ b/trytond/trytond/ir/model.py @@ -61,8 +61,7 @@ class Model(ModelSQL, ModelView): module = fields.Char('Module', help="Module in which this model is defined.", readonly=True) global_search_p = fields.Boolean('Global Search') - fields = fields.One2Many('ir.model.field', 'model', 'Fields', - required=True) + fields = fields.One2Many('ir.model.field', 'model_ref', "Fields") _get_names_cache = Cache('ir.model.get_names') @classmethod @@ -221,7 +220,11 @@ class Model(ModelSQL, ModelView): return cls.get_names().get(model, model) -class ModelField(ModelSQL, ModelView): +class ModelField( + fields.fmany2one( + 'model_ref', 'model', 'ir.model,model', "Model", + required=True, ondelete='CASCADE'), + ModelSQL, ModelView): "Model field" __name__ = 'ir.model.field' name = fields.Char('Name', required=True, @@ -234,12 +237,11 @@ class ModelField(ModelSQL, ModelView): 'readonly': Bool(Eval('module')), }, depends=['module']) - model = fields.Many2One('ir.model', 'Model', required=True, - ondelete='CASCADE', + model = fields.Char( + "Model", required=True, states={ 'readonly': Bool(Eval('module')), - }, - depends=['module']) + }) field_description = fields.Char('Field Description', translate=True, loading='lazy', states={ @@ -272,7 +274,7 @@ class ModelField(ModelSQL, ModelView): @classmethod def __setup__(cls): super(ModelField, cls).__setup__() - cls.__access__.add('model') + cls.__access__.add('model_ref') table = cls.__table__() cls._sql_constraints += [ ('name_model_uniq', Unique(table, table.name, table.model), @@ -281,17 +283,37 @@ class ModelField(ModelSQL, ModelView): cls._order.insert(0, ('name', 'ASC')) @classmethod - def register(cls, model, module_name, model_id): + def __register__(cls, module): pool = Pool() Model = pool.get('ir.model') + transaction = Transaction() + cursor = transaction.connection.cursor() + table_h = cls.__table_handler__(module) + table = cls.__table__() + model = Model.__table__() + + # Migration from 7.0: model as char + if (table_h.column_exist('model') + and table_h.column_is_type('model', 'INTEGER')): + table_h.column_rename('model', '_temp_model') + table_h.add_column('model', 'VARCHAR') + cursor.execute(*table.update( + [table.model], [model.model], + from_=[model], + where=table._temp_model == model.id)) + table_h.drop_column('_temp_model') + + super().__register__(module) + + @classmethod + def register(cls, model, module_name, model_id): cursor = Transaction().connection.cursor() ir_model_field = cls.__table__() - ir_model = Model.__table__() - cursor.execute(*ir_model_field.join(ir_model, - condition=ir_model_field.model == ir_model.id - ).select(ir_model_field.id.as_('id'), + cursor.execute(*ir_model_field + .select( + ir_model_field.id.as_('id'), ir_model_field.name.as_('name'), ir_model_field.field_description.as_('field_description'), ir_model_field.ttype.as_('ttype'), @@ -299,7 +321,7 @@ class ModelField(ModelSQL, ModelView): ir_model_field.module.as_('module'), ir_model_field.help.as_('help'), ir_model_field.access.as_('access'), - where=ir_model.model == model.__name__)) + where=ir_model_field.model == model.__name__)) model_fields = {f['name']: f for f in cursor_dict(cursor)} for field_name, field in model._fields.items(): @@ -312,28 +334,41 @@ class ModelField(ModelSQL, ModelView): access = field_name in model.__access__ if field_name not in model_fields: - cursor.execute(*ir_model_field.insert( - [ir_model_field.model, ir_model_field.name, + cursor.execute(*ir_model_field.insert([ + ir_model_field.model, + ir_model_field.name, ir_model_field.field_description, - ir_model_field.ttype, ir_model_field.relation, - ir_model_field.help, ir_model_field.module, - ir_model_field.access], - [[ - model_id, field_name, + ir_model_field.ttype, + ir_model_field.relation, + ir_model_field.help, + ir_model_field.module, + ir_model_field.access, + ], [[ + model.__name__, + field_name, field.string, - field._type, relation, - field.help, module_name, - access]])) + field._type, + relation, + field.help, + module_name, + access, + ]])) elif (model_fields[field_name]['field_description'] != field.string or model_fields[field_name]['ttype'] != field._type or model_fields[field_name]['relation'] != relation or model_fields[field_name]['help'] != field.help or model_fields[field_name]['access'] != access): - cursor.execute(*ir_model_field.update( - [ir_model_field.field_description, - ir_model_field.ttype, ir_model_field.relation, - ir_model_field.help, ir_model_field.access], - [field.string, field._type, relation, field.help, + cursor.execute(*ir_model_field.update([ + ir_model_field.field_description, + ir_model_field.ttype, + ir_model_field.relation, + ir_model_field.help, + ir_model_field.access, + ], [ + field.string, + field._type, + relation, + field.help, access], where=(ir_model_field.id == model_fields[field_name]['id']))) @@ -341,14 +376,11 @@ class ModelField(ModelSQL, ModelView): @classmethod def clean(cls): pool = Pool() - IrModel = pool.get('ir.model') transaction = Transaction() cursor = transaction.connection.cursor() - ir_model = IrModel.__table__() ir_model_field = cls.__table__() - cursor.execute(*ir_model_field - .join(ir_model, condition=ir_model_field.model == ir_model.id) - .select(ir_model.model, ir_model_field.name, ir_model_field.id)) + cursor.execute(*ir_model_field.select( + ir_model_field.model, ir_model_field.name, ir_model_field.id)) for model, field, id_ in cursor: Model = pool.get(model) if field not in Model._fields: @@ -407,7 +439,6 @@ class ModelField(ModelSQL, ModelView): def read(cls, ids, fields_names): pool = Pool() Translation = pool.get('ir.translation') - Model = pool.get('ir.model') to_delete = [] if Transaction().context.get('language'): @@ -425,46 +456,27 @@ class ModelField(ModelSQL, ModelView): if (Transaction().context.get('language') and ('field_description' in fields_names or 'help' in fields_names)): - model_ids = set() - for rec in res: - if isinstance(rec['model'], (list, tuple)): - model_ids.add(rec['model'][0]) - else: - model_ids.add(rec['model']) - model_ids = list(model_ids) - cursor = Transaction().connection.cursor() - model = Model.__table__() - cursor.execute(*model.select(model.id, model.model, - where=model.id.in_(model_ids))) - id2model = dict(cursor) - trans_args = [] for rec in res: - if isinstance(rec['model'], (list, tuple)): - model_id = rec['model'][0] - else: - model_id = rec['model'] if 'field_description' in fields_names: - trans_args.append((id2model[model_id] + ',' + rec['name'], - 'field', Transaction().language, None)) + trans_args.append(( + rec['model'] + ',' + rec['name'], + 'field', Transaction().language, None)) if 'help' in fields_names: - trans_args.append((id2model[model_id] + ',' + rec['name'], + trans_args.append(( + rec['model'] + ',' + rec['name'], 'help', Transaction().language, None)) Translation.get_sources(trans_args) for rec in res: - if isinstance(rec['model'], (list, tuple)): - model_id = rec['model'][0] - else: - model_id = rec['model'] if 'field_description' in fields_names: res_trans = Translation.get_source( - id2model[model_id] + ',' + rec['name'], + rec['model'] + ',' + rec['name'], 'field', Transaction().language) if res_trans: rec['field_description'] = res_trans if 'help' in fields_names: res_trans = Translation.get_source( - id2model[model_id] + ',' + rec['name'], + rec['model'] + ',' + rec['name'], 'help', Transaction().language) if res_trans: rec['help'] = res_trans @@ -760,11 +772,9 @@ class ModelFieldAccess(DeactivableMixin, ModelSQL, ModelView): lambda: defaultdict(lambda: True))) pool = Pool() - Model = pool.get('ir.model') ModelField = pool.get('ir.model.field') User = pool.get('res.user') field_access = cls.__table__() - ir_model = Model.__table__() model_field = ModelField.__table__() groups = User.get_groups() @@ -783,10 +793,8 @@ class ModelFieldAccess(DeactivableMixin, ModelSQL, ModelView): cursor = Transaction().connection.cursor() cursor.execute(*field_access.join(model_field, condition=field_access.field == model_field.id - ).join(ir_model, - condition=model_field.model == ir_model.id ).select( - ir_model.model, + model_field.model, model_field.name, Max(Case( (field_access.perm_read == Literal(True), 1), @@ -800,11 +808,11 @@ class ModelFieldAccess(DeactivableMixin, ModelSQL, ModelView): Max(Case( (field_access.perm_delete == Literal(True), 1), else_=0)), - where=ir_model.model.in_(models) + where=model_field.model.in_(models) & (field_access.active == Literal(True)) & (field_access.group.in_(groups or [-1]) | (field_access.group == Null)), - group_by=[ir_model.model, model_field.name])) + group_by=[model_field.model, model_field.name])) for m, f, r, w, c, d in cursor: accesses[m][f] = {'read': r, 'write': w, 'create': c, 'delete': d} for model, maccesses in accesses.items(): diff --git a/trytond/trytond/ir/translation.py b/trytond/trytond/ir/translation.py index 581c819903..42d4132a31 100644 --- a/trytond/trytond/ir/translation.py +++ b/trytond/trytond/ir/translation.py @@ -341,7 +341,7 @@ class Translation(ModelSQL, ModelView): trans_args = [] for record in records: if ttype in ('field', 'help'): - name = record.model.model + ',' + record.name + name = record.model + ',' + record.name else: name = record.model + ',' + field_name trans_args.append((name, ttype, lang, None)) @@ -349,7 +349,7 @@ class Translation(ModelSQL, ModelView): for record in records: if ttype in ('field', 'help'): - name = record.model.model + ',' + record.name + name = record.model + ',' + record.name else: name = record.model + ',' + field_name translations[record.id] = cls.get_source(name, ttype, lang) @@ -358,7 +358,7 @@ class Translation(ModelSQL, ModelView): if ttype in {'field', 'help'}: try: field = getattr( - pool.get(record.model.model), record.name) + pool.get(record.model), record.name) except KeyError: continue translations[record.id] = '' @@ -458,7 +458,7 @@ class Translation(ModelSQL, ModelView): def get_name(record): if ttype in ('field', 'help'): - return record.model.model + ',' + record.name + return record.model + ',' + record.name else: return record.model + ',' + field_name diff --git a/trytond/trytond/ir/view/model_field_form.xml b/trytond/trytond/ir/view/model_field_form.xml index 3f32b718aa..d152a914c3 100644 --- a/trytond/trytond/ir/view/model_field_form.xml +++ b/trytond/trytond/ir/view/model_field_form.xml @@ -4,8 +4,8 @@ this repository contains the full copyright notices and license terms. -->