From 060a9da6bfbcdec1903769bef45754c36beb0d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 7 Jul 2020 23:56:10 +0200 Subject: [PATCH] Add model, record and records attribute on Wizard issue9421 review309861012 --- CHANGELOG | 2 ++ doc/ref/wizard.rst | 15 ++++++++++++ trytond/ir/action.py | 11 +++++++++ trytond/ir/message.xml | 8 +++++++ trytond/ir/translation.py | 30 ++++++++++------------- trytond/tests/test_wizard.py | 26 ++++++++++++++++++++ trytond/tests/wizard.xml | 6 +++++ trytond/wizard/wizard.py | 46 ++++++++++++++++++++++++++++++++---- 8 files changed, 122 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 90e14a17bf..a66f566123 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Add model, record and records attributes on Wizard +* Check read access of wizard records * Add cached_property in tools * Use custom class to store record data * Do not write to existing targets on xxx2Many add diff --git a/doc/ref/wizard.rst b/doc/ref/wizard.rst index 228de24a15..ef04c089ac 100644 --- a/doc/ref/wizard.rst +++ b/doc/ref/wizard.rst @@ -45,6 +45,21 @@ Class attributes are: It contains a dictionary with state name as key and :class:`State` as value. +.. attribute:: model + + It contains the :class:`~trytond.model.Model` class on which the wizard is + executed. + +.. attribute:: record + + It contains the :class:`~trytond.model.Model` instance on which the wizard + is executed. + +.. attribute:: records + + It contains the list of :class:`~trytond.model.Model` instances on which + the wizard is executed. + Class methods are: .. classmethod:: Wizard.__setup__() diff --git a/trytond/ir/action.py b/trytond/ir/action.py index 9e49bb6e2f..15a182497c 100644 --- a/trytond/ir/action.py +++ b/trytond/ir/action.py @@ -1046,6 +1046,17 @@ class ActionWizard(ActionMixin, ModelSQL, ModelView): def default_type(): return 'ir.action.wizard' + @classmethod + def get_models(cls, name, action_id=None): + # TODO add cache + domain = [ + (cls._action_name, '=', name), + ] + if action_id: + domain.append(('id', '=', action_id)) + actions = cls.search(domain) + return {a.model for a in actions if a.model} + class ActionURL(ActionMixin, ModelSQL, ModelView): "Action URL" diff --git a/trytond/ir/message.xml b/trytond/ir/message.xml index 653c20c303..2407881b2a 100644 --- a/trytond/ir/message.xml +++ b/trytond/ir/message.xml @@ -311,5 +311,13 @@ this repository contains the full copyright notices and license terms. --> Notes + + + You are not allowed to execute wizard "%(wizard)s". + + + You are not allowed to execute wizard "%(wizard)s" on "%(model)s". + + diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py index 26c8d08bc2..562b0e1bfc 100644 --- a/trytond/ir/translation.py +++ b/trytond/ir/translation.py @@ -950,11 +950,10 @@ class TranslationSet(Wizard): pool = Pool() Report = pool.get('ir.action.report') Translation = pool.get('ir.translation') - context = Transaction().context - if context.get('active_model') == Report.__name__: - reports = Report.browse(context.get('active_ids', [])) - elif context.get('active_model', 'ir.ui.menu') == 'ir.ui.menu': + if self.model == Report: + reports = self.records + elif not self.model or self.model.__name__ == 'ir.ui.menu': with Transaction().set_context(active_test=False): reports = Report.search([('translatable', '=', True)]) else: @@ -1045,11 +1044,10 @@ class TranslationSet(Wizard): pool = Pool() View = pool.get('ir.ui.view') Translation = pool.get('ir.translation') - context = Transaction().context - if context.get('active_model') == View.__name__: - views = View.browse(context.get('active_ids', [])) - elif context.get('active_model', 'ir.ui.menu') == 'ir.ui.menu': + if self.model == View: + views = self.records + elif not self.model or self.model.__name__ == 'ir.ui.menu': with Transaction().set_context(active_test=False): views = View.search([]) else: @@ -1357,19 +1355,18 @@ class TranslationUpdate(Wizard): Translation = pool.get('ir.translation') Report = pool.get('ir.action.report') View = pool.get('ir.ui.view') - context = Transaction().context cursor = Transaction().connection.cursor() cursor_update = Transaction().connection.cursor() translation = Translation.__table__() lang = self.start.language.code parent_lang = get_parent(lang) - if context.get('active_model') == Report.__name__: - reports = Report.browse(context.get('active_ids', [])) + if self.model == Report: + reports = self.records source_clause = ((translation.type == 'report') & translation.name.in_([r.report_name for r in reports])) - elif context.get('active_model') == View.__name__: - views = View.browse(context.get('active_ids', [])) + elif self.model == View: + views = self.records source_clause = ((translation.type == 'view') & translation.name.in_([v.model for v in views])) else: @@ -1420,7 +1417,7 @@ class TranslationUpdate(Wizard): & (translation.module == row['module']) & (translation.lang == lang))) - if context.get('active_model') in {Report.__name__, View.__name__}: + if self.model in {Report, View}: return columns = [translation.name.as_('name'), @@ -1592,11 +1589,8 @@ class TranslationReport(Wizard): open_ = StateAction('ir.act_translation_report') def do_open_(self, action): - pool = Pool() - Report = pool.get('ir.action.report') context = Transaction().context - assert context['active_model'] == Report.__name__ - reports = Report.browse(context['active_ids']) + reports = self.records action['pyson_domain'] = PYSONEncoder().encode([ ('type', '=', 'report'), ('name', 'in', [r.report_name for r in reports]), diff --git a/trytond/tests/test_wizard.py b/trytond/tests/test_wizard.py index f757a692c4..26c4488d34 100644 --- a/trytond/tests/test_wizard.py +++ b/trytond/tests/test_wizard.py @@ -127,6 +127,32 @@ class WizardTestCase(unittest.TestCase): with Transaction().set_context(active_model='test.access'): Wizard.execute(session_id, {}, start_state) + @with_transaction() + def test_execute_without_read_access(self): + "Execute wizard without read access" + pool = Pool() + Wizard = pool.get('test.test_wizard', type='wizard') + + session_id, start_state, end_state = Wizard.create() + + with self.assertRaises(AccessError): + with Transaction().set_context( + active_model='test.access', active_id=1): + Wizard.execute(session_id, {}, start_state) + + @with_transaction() + def test_execute_wrong_model(self): + "Execute wizard on wrong model" + pool = Pool() + Wizard = pool.get('test.test_wizard', type='wizard') + + session_id, start_state, end_state = Wizard.create() + + with self.assertRaises(AccessError): + with Transaction().set_context( + active_model='test.test_wizard.start'): + Wizard.execute(session_id, {}, start_state) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(WizardTestCase) diff --git a/trytond/tests/wizard.xml b/trytond/tests/wizard.xml index 3fd2593fe4..c4e7a72c42 100644 --- a/trytond/tests/wizard.xml +++ b/trytond/tests/wizard.xml @@ -15,5 +15,11 @@ this repository contains the full copyright notices and license terms. --> ]]> + + + Test Wizard + test.test_wizard + test.access + diff --git a/trytond/wizard/wizard.py b/trytond/wizard/wizard.py index 8f0a3b6bad..bb6d401c20 100644 --- a/trytond/wizard/wizard.py +++ b/trytond/wizard/wizard.py @@ -8,15 +8,17 @@ __all__ = ['Wizard', import json import copy +from trytond.i18n import gettext from trytond.pool import Pool, PoolBase from trytond.transaction import Transaction from trytond.url import URLMixin from trytond.protocols.jsonrpc import JSONDecoder, JSONEncoder from trytond.model import ModelSQL +from trytond.model.exceptions import AccessError from trytond.model.fields import states_validate from trytond.pyson import PYSONEncoder from trytond.rpc import RPC -from trytond.exceptions import UserError +from trytond.tools import cached_property class Button(object): @@ -233,21 +235,38 @@ class Wizard(URLMixin, PoolBase): with Transaction().set_context(_check_access=True): model = context.get('active_model') + if model: + Model = pool.get(model) if model and model != 'ir.ui.menu': ModelAccess.check(model, 'read') + models = ActionWizard.get_models( + cls.__name__, action_id=context.get('action_id')) + if model and models and model not in models: + raise AccessError(gettext( + 'ir.msg_access_wizard_model_error', + wizard=cls.__name__, + model=model)) groups = set(User.get_groups()) wizard_groups = ActionWizard.get_groups(cls.__name__, action_id=context.get('action_id')) if wizard_groups: if not groups & wizard_groups: - raise UserError('Calling wizard %s is not allowed!' - % cls.__name__) + raise AccessError(gettext( + 'ir.msg_access_wizard_error', + name=cls.__name__)) elif model and model != 'ir.ui.menu': - Model = pool.get(model) if (not callable(getattr(Model, 'table_query', None)) or Model.write.__func__ != ModelSQL.write.__func__): ModelAccess.check(model, 'write') + if model: + ids = context.get('active_ids') or [] + id_ = context.get('active_id') + if id_ not in ids: + ids.append(id_) + # Check read access + Model.read(ids, ['id']) + @classmethod def create(cls): "Create a session" @@ -354,3 +373,22 @@ class Wizard(URLMixin, PoolBase): Session.write([session], { 'data': data, }) + + @cached_property + def model(self): + pool = Pool() + context = Transaction().context + if context.get('active_model'): + return pool.get(context['active_model']) + + @cached_property + def record(self): + context = Transaction().context + if context.get('active_id'): + return self.model(context['active_id']) + + @cached_property + def records(self): + context = Transaction().context + if context.get('active_ids'): + return self.model.browse(context['active_ids'])