From ba4feaaa46f5033ac6941ffa67552ed04672fc94 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 24 Jan 2018 12:28:59 +0100 Subject: [PATCH] Add default related action to open related records The related action button now opens by default the record on which the job is working. It uses the default views of the job. The exact behavior depends of the number of records in the job: * No record (job called on a model): display a message indicating that there is no available action * One record: open the default view in form * Several records: open the default view in list The related action can still be overridden by the 'related_action' decorator. --- queue_job/job.py | 19 ++- queue_job/models/queue_job.py | 37 ++++- queue_job/readme/HISTORY.rst | 21 +++ test_queue_job/tests/test_related_actions.py | 159 ++++++++++++++----- 4 files changed, 192 insertions(+), 44 deletions(-) create mode 100644 queue_job/readme/HISTORY.rst diff --git a/queue_job/job.py b/queue_job/job.py index b34a6f6b40..55e8cd5985 100644 --- a/queue_job/job.py +++ b/queue_job/job.py @@ -533,15 +533,20 @@ def postpone(self, result=None, seconds=None): self.result = result def related_action(self): - if not hasattr(self.func, 'related_action'): - return None - if not self.func.related_action: - return None - if not isinstance(self.func.related_action, basestring): + record = self.db_record() + if hasattr(self.func, 'related_action'): + funcname = self.func.related_action + # decorator is set but empty: disable the default one + if not funcname: + return None + else: + funcname = record._default_related_action + if not isinstance(funcname, basestring): raise ValueError('related_action must be the name of the ' 'method on queue.job as string') - action = getattr(self.db_record(), self.func.related_action) - return action(**self.func.kwargs) + action = getattr(record, funcname) + action_kwargs = getattr(self.func, 'kwargs', {}) + return action(**action_kwargs) def _is_model_method(func): diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py index c980cd9af4..8bc5960847 100644 --- a/queue_job/models/queue_job.py +++ b/queue_job/models/queue_job.py @@ -27,6 +27,7 @@ class QueueJob(models.Model): _order = 'date_created DESC, date_done DESC' _removal_interval = 30 # days + _default_related_action = 'related_action_open_record' uuid = fields.Char(string='UUID', readonly=True, @@ -126,7 +127,7 @@ def open_related_action(self): job = Job.load(self.env, self.uuid) action = job.related_action() if action is None: - raise exceptions.Warning(_('No action available for this job')) + raise exceptions.UserError(_('No action available for this job')) return action @api.multi @@ -216,6 +217,40 @@ def autovacuum(self): jobs.unlink() return True + @api.multi + def related_action_open_record(self): + """Open a form view with the record(s) of the job. + + For instance, for a job on a ``product.product``, it will open a + ``product.product`` form view with the product record(s) concerned by + the job. If the job concerns more than one record, it opens them in a + list. + + This is the default related action. + + """ + self.ensure_one() + model_name = self.model_name + records = self.env[model_name].browse(self.record_ids).exists() + if not records: + return None + action = { + 'name': _('Related Record'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': records._name, + } + if len(records) == 1: + action['res_id'] = records.id + else: + action.update({ + 'name': _('Related Records'), + 'view_mode': 'tree,form', + 'domain': [('id', 'in', records.ids)], + }) + return action + class RequeueJob(models.TransientModel): _name = 'queue.requeue.job' diff --git a/queue_job/readme/HISTORY.rst b/queue_job/readme/HISTORY.rst new file mode 100644 index 0000000000..67f90f61a3 --- /dev/null +++ b/queue_job/readme/HISTORY.rst @@ -0,0 +1,21 @@ +.. [ The change log. The goal of this file is to help readers + understand changes between version. The primary audience is + end users and integrators. Purely technical changes such as + code refactoring must not be mentioned here. + + This file may contain ONE level of section titles, underlined + with the ~ (tilde) character. Other section markers are + forbidden and will likely break the structure of the README.rst + or other documents where this fragment is included. ] + +Next +~~~~ + +* [ADD] Default "related action" for jobs, opening a form or list view (when + the job is linked to respectively one record on several). + (`#79 `_, backport from `#46 `_) + +10.0.1.0.0 +~~~~~~~~~~ + +* Starting the changelog from there diff --git a/test_queue_job/tests/test_related_actions.py b/test_queue_job/tests/test_related_actions.py index d8d41cf2a6..4dcbce1433 100644 --- a/test_queue_job/tests/test_related_actions.py +++ b/test_queue_job/tests/test_related_actions.py @@ -3,51 +3,138 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import odoo.tests.common as common -from odoo.addons.queue_job.job import Job +from odoo import exceptions -class TestRelatedAction(common.TransactionCase): + +class TestRelatedAction(common.SavepointCase): """ Test Related Actions """ - def setUp(self): - super(TestRelatedAction, self).setUp() - self.model = self.env['test.related.action'] - self.method = self.env['test.queue.job'].testing_method + @classmethod + def setUpClass(cls): + super(TestRelatedAction, cls).setUpClass() + cls.model = cls.env['test.related.action'] + cls.record = cls.model.create({}) + cls.records = cls.record + cls.model.create({}) - def test_return(self): + def test_attributes(self): """ Job with related action check if action returns correctly """ - job = Job(self.method) - act_job, act_kwargs = job.related_action() - self.assertEqual(act_job, job.db_record()) - self.assertEqual(act_kwargs, {}) - - def test_no_related_action(self): - """ Job without related action """ - job = Job(self.model.testing_related_action__no) - self.assertIsNone(job.related_action()) - - def test_return_none(self): - """ Job with related action returning None """ + job_ = self.record.with_delay().testing_related_action__kwargs() + act_job, act_kwargs = job_.related_action() + self.assertEqual(act_job, job_.db_record()) + self.assertEqual(act_kwargs, {'b': 4}) + + def test_decorator_empty(self): + """ Job with decorator without value disable the default action + + The function is:: + + @job + @related_action() # default action returns None + def testing_related_action__return_none(self): + return + + """ # default action returns None - job = Job(self.model.testing_related_action__return_none) - self.assertIsNone(job.related_action()) - - def test_kwargs(self): - """ Job with related action check if action propagates kwargs """ - job_ = Job(self.model.testing_related_action__kwargs) - self.assertEqual(job_.related_action(), (job_.db_record(), {'b': 4})) - - def test_store_related_action(self): - """ Call the related action on the model """ - job = Job(self.model.testing_related_action__store, - args=('Discworld',)) - job.store() - stored_job = self.env['queue.job'].search( - [('uuid', '=', job.uuid)] + job_ = self.record.with_delay().testing_related_action__return_none() + self.assertIsNone(job_.related_action()) + + def test_model_no_action(self): + """Model shows an error when no action exist""" + job_ = self.record.with_delay().testing_related_action__return_none() + with self.assertRaises(exceptions.UserError): + # db_record is the 'job.queue' record on which we click on the + # button to open the related action + job_.db_record().open_related_action() + + def test_default_no_record(self): + """Default related action called when no decorator is set + + When called on no record. + + The function is:: + + @job + def testing_related_action__no(self): + return + + """ + job_ = self.model.with_delay().testing_related_action__no() + expected = None + self.assertEquals(job_.related_action(), expected) + + def test_model_default_no_record(self): + """Model shows an error when using the default action and we have no + record linke to the job""" + job_ = self.model.with_delay().testing_related_action__no() + with self.assertRaises(exceptions.UserError): + # db_record is the 'job.queue' record on which we click on the + # button to open the related action + job_.db_record().open_related_action() + + def test_default_one_record(self): + """Default related action called when no decorator is set + + When called on one record. + + The function is:: + + @job + def testing_related_action__no(self): + return + + """ + job_ = self.record.with_delay().testing_related_action__no() + expected = { + 'name': 'Related Record', + 'res_id': self.record.id, + 'res_model': self.record._name, + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'view_type': 'form', + } + self.assertEquals(job_.related_action(), expected) + + def test_default_several_record(self): + """Default related action called when no decorator is set + + When called on several record. + + The function is:: + + @job + def testing_related_action__no(self): + return + + """ + job_ = self.records.with_delay().testing_related_action__no() + expected = { + 'name': 'Related Records', + 'domain': [('id', 'in', self.records.ids)], + 'res_model': self.record._name, + 'type': 'ir.actions.act_window', + 'view_mode': 'tree,form', + 'view_type': 'form', + } + self.assertEquals(job_.related_action(), expected) + + def test_decorator(self): + """Call the related action on the model + + The function is:: + + @job + @related_action(action='testing_related__url', + url='https://en.wikipedia.org/wiki/{subject}') + def testing_related_action__store(self): + return + + """ + job_ = self.record.with_delay().testing_related_action__store( + 'Discworld' ) - self.assertEqual(len(stored_job), 1) expected = {'type': 'ir.actions.act_url', 'target': 'new', 'url': 'https://en.wikipedia.org/wiki/Discworld', } - self.assertEquals(stored_job.open_related_action(), expected) + self.assertEquals(job_.related_action(), expected)