diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 371cbb17f8..54f4be5b4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,11 +3,9 @@ exclude: | # NOT INSTALLABLE ADDONS ^base_export_async/| ^base_import_async/| - ^queue_job/| ^queue_job_cron/| ^queue_job_subscribe/| ^test_base_import_async/| - ^test_queue_job/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops ^setup/|/static/description/index\.html$| diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py index 79594a2d9f..856bb3fd3d 100644 --- a/queue_job/__manifest__.py +++ b/queue_job/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Job Queue", - "version": "14.0.1.3.1", + "version": "15.0.1.0.0", "author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/queue", "license": "LGPL-3", @@ -22,7 +22,7 @@ "data/queue_data.xml", "data/queue_job_function_data.xml", ], - "installable": False, + "installable": True, "development_status": "Mature", "maintainers": ["guewen"], "post_init_hook": "post_init_hook", diff --git a/queue_job/controllers/main.py b/queue_job/controllers/main.py index 8078285ff3..a68897e7f4 100644 --- a/queue_job/controllers/main.py +++ b/queue_job/controllers/main.py @@ -9,8 +9,7 @@ from psycopg2 import OperationalError from werkzeug.exceptions import Forbidden -import odoo -from odoo import _, http, tools +from odoo import SUPERUSER_ID, _, api, http, registry, tools from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY from ..exception import FailedJobError, NothingToDoJob, RetryableJobError @@ -39,17 +38,15 @@ def _try_perform_job(self, env, job): @http.route("/queue_job/runjob", type="http", auth="none", save_session=False) def runjob(self, db, job_uuid, **kw): http.request.session.db = db - env = http.request.env(user=odoo.SUPERUSER_ID) + env = http.request.env(user=SUPERUSER_ID) def retry_postpone(job, message, seconds=None): job.env.clear() - with odoo.api.Environment.manage(): - with odoo.registry(job.env.cr.dbname).cursor() as new_cr: - job.env = job.env(cr=new_cr) - job.postpone(result=message, seconds=seconds) - job.set_pending(reset_retry=False) - job.store() - new_cr.commit() + with registry(job.env.cr.dbname).cursor() as new_cr: + job.env = api.Environment(new_cr, SUPERUSER_ID, {}) + job.postpone(result=message, seconds=seconds) + job.set_pending(reset_retry=False) + job.store() # ensure the job to run is in the correct state and lock the record env.cr.execute( @@ -101,12 +98,10 @@ def retry_postpone(job, message, seconds=None): traceback.print_exc(file=buff) _logger.error(buff.getvalue()) job.env.clear() - with odoo.api.Environment.manage(): - with odoo.registry(job.env.cr.dbname).cursor() as new_cr: - job.env = job.env(cr=new_cr) - job.set_failed(exc_info=buff.getvalue()) - job.store() - new_cr.commit() + with registry(job.env.cr.dbname).cursor() as new_cr: + job.env = api.Environment(new_cr, SUPERUSER_ID, {}) + job.set_failed(exc_info=buff.getvalue()) + job.store() raise return "" diff --git a/queue_job/fields.py b/queue_job/fields.py index 50183993d8..b3a2c470b7 100644 --- a/queue_job/fields.py +++ b/queue_job/fields.py @@ -40,7 +40,7 @@ class JobSerialized(fields.Field): def __init__(self, string=fields.Default, base_type=fields.Default, **kwargs): super().__init__(string=string, _base_type=base_type, **kwargs) - def _setup_attrs(self, model, name): + def _setup_attrs(self, model, name): # pylint: disable=missing-return super()._setup_attrs(model, name) if self._base_type not in self._default_json_mapping: raise ValueError("%s is not a supported base type" % (self._base_type)) diff --git a/queue_job/job.py b/queue_job/job.py index 51136052db..4ee90390e6 100644 --- a/queue_job/job.py +++ b/queue_job/job.py @@ -618,7 +618,7 @@ def description(self): @property def uuid(self): - """Job ID, this is an UUID """ + """Job ID, this is an UUID""" if self._uuid is None: self._uuid = str(uuid.uuid4()) return self._uuid diff --git a/queue_job/jobrunner/__init__.py b/queue_job/jobrunner/__init__.py index 7ad479ff33..4dd1ddd722 100644 --- a/queue_job/jobrunner/__init__.py +++ b/queue_job/jobrunner/__init__.py @@ -48,7 +48,7 @@ def stop(self): class WorkerJobRunner(server.Worker): - """ Jobrunner workers """ + """Jobrunner workers""" def __init__(self, multi): super().__init__(multi) @@ -58,7 +58,7 @@ def __init__(self, multi): def sleep(self): pass - def signal_handler(self, sig, frame): + def signal_handler(self, sig, frame): # pylint: disable=missing-return _logger.debug("WorkerJobRunner (%s) received signal %s", self.pid, sig) super().signal_handler(sig, frame) self.runner.stop() diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py index 1f42a10cc9..536fec5f68 100644 --- a/queue_job/jobrunner/channels.py +++ b/queue_job/jobrunner/channels.py @@ -504,7 +504,7 @@ def set_running(self, job): _logger.debug("job %s marked running in channel %s", job.uuid, self) def set_failed(self, job): - """Mark the job as failed. """ + """Mark the job as failed.""" if job not in self._failed: self._queue.remove(job) self._running.remove(job) @@ -873,11 +873,11 @@ def parse_simple_config(cls, config_string): capacity = config_items[1] try: config["capacity"] = int(capacity) - except Exception: + except Exception as ex: raise ValueError( "Invalid channel config %s: " "invalid capacity %s" % (config_string, capacity) - ) + ) from ex for config_item in config_items[2:]: kv = split_strip(config_item, "=") if len(kv) == 1: diff --git a/queue_job/models/queue_job_channel.py b/queue_job/models/queue_job_channel.py index 374e7417f7..920b021261 100644 --- a/queue_job/models/queue_job_channel.py +++ b/queue_job/models/queue_job_channel.py @@ -11,7 +11,7 @@ class QueueJobChannel(models.Model): name = fields.Char() complete_name = fields.Char( - compute="_compute_complete_name", store=True, readonly=True + compute="_compute_complete_name", store=True, readonly=True, recursive=True ) parent_id = fields.Many2one( comodel_name="queue.job.channel", string="Parent Channel", ondelete="restrict" diff --git a/queue_job/models/queue_job_function.py b/queue_job/models/queue_job_function.py index a80f4920d3..db9eea3c94 100644 --- a/queue_job/models/queue_job_function.py +++ b/queue_job/models/queue_job_function.py @@ -93,7 +93,9 @@ def _inverse_name(self): raise exceptions.UserError(_("Invalid job function: {}").format(self.name)) model_name = groups[1] method = groups[2] - model = self.env["ir.model"].search([("model", "=", model_name)], limit=1) + model = ( + self.env["ir.model"].sudo().search([("model", "=", model_name)], limit=1) + ) if not model: raise exceptions.UserError(_("Model {} not found").format(model_name)) self.model_id = model.id @@ -112,8 +114,10 @@ def _inverse_edit_retry_pattern(self): self.retry_pattern = ast.literal_eval(edited) else: self.retry_pattern = {} - except (ValueError, TypeError, SyntaxError): - raise exceptions.UserError(self._retry_pattern_format_error_message()) + except (ValueError, TypeError, SyntaxError) as ex: + raise exceptions.UserError( + self._retry_pattern_format_error_message() + ) from ex @api.depends("related_action") def _compute_edit_related_action(self): @@ -127,8 +131,10 @@ def _inverse_edit_related_action(self): self.related_action = ast.literal_eval(edited) else: self.related_action = {} - except (ValueError, TypeError, SyntaxError): - raise exceptions.UserError(self._related_action_format_error_message()) + except (ValueError, TypeError, SyntaxError) as ex: + raise exceptions.UserError( + self._related_action_format_error_message() + ) from ex @staticmethod def job_function_name(model_name, method_name): @@ -193,10 +199,10 @@ def _check_retry_pattern(self): for value in all_values: try: int(value) - except ValueError: + except ValueError as ex: raise exceptions.UserError( record._retry_pattern_format_error_message() - ) + ) from ex def _related_action_format_error_message(self): return _( diff --git a/queue_job/tests/common.py b/queue_job/tests/common.py index e1e877b0a2..dcb948e764 100644 --- a/queue_job/tests/common.py +++ b/queue_job/tests/common.py @@ -108,7 +108,9 @@ class OdooDocTestCase(doctest.DocTestCase): - output a more meaningful test name than default "DocTestCase.runTest" """ - def __init__(self, doctest, optionflags=0, setUp=None, tearDown=None, checker=None): + def __init__( + self, doctest, optionflags=0, setUp=None, tearDown=None, checker=None, seq=0 + ): super().__init__( doctest._dt_test, optionflags=optionflags, @@ -116,6 +118,7 @@ def __init__(self, doctest, optionflags=0, setUp=None, tearDown=None, checker=No tearDown=tearDown, checker=checker, ) + self.test_sequence = seq def setUp(self): """Log an extra statement which test is started.""" @@ -139,8 +142,8 @@ def load_tests(loader, tests, ignore): doctest.DocTestCase.doClassCleanups = lambda: None doctest.DocTestCase.tearDown_exceptions = [] - for test in doctest.DocTestSuite(module): - odoo_test = OdooDocTestCase(test) + for idx, test in enumerate(doctest.DocTestSuite(module)): + odoo_test = OdooDocTestCase(test, seq=idx) odoo_test.test_tags = {"standard", "at_install", "queue_job", "doctest"} tests.addTest(odoo_test) diff --git a/queue_job/tests/test_model_job_channel.py b/queue_job/tests/test_model_job_channel.py index 3fdc7b4c74..bf04ec17aa 100644 --- a/queue_job/tests/test_model_job_channel.py +++ b/queue_job/tests/test_model_job_channel.py @@ -37,11 +37,20 @@ def test_channel_complete_name_uniq(self): self.assertEqual(channel.complete_name, "root.sub") self.Channel.create({"name": "sub", "parent_id": self.root_channel.id}) - with self.assertRaises(IntegrityError): - # Flush process all the pending recomputations (or at least the - # given field and flush the pending updates to the database. - # It is normally called on commit. + + # Flush process all the pending recomputations (or at least the + # given field and flush the pending updates to the database. + # It is normally called on commit. + + # The context manager 'with self.assertRaises(IntegrityError)' purposefully + # not uses here due to its 'flush()' method inside it and exception raises + # before the line 'self.env["base"].flush()'. So, we are expecting an IntegrityError. + try: self.env["base"].flush() + except IntegrityError as ex: + self.assertIn("queue_job_channel_name_uniq", ex.pgerror) + else: + self.assertEqual(True, False) def test_channel_name_get(self): channel = self.Channel.create( diff --git a/queue_job/tests/test_model_job_function.py b/queue_job/tests/test_model_job_function.py index c9bdea56e8..965e26d8f2 100644 --- a/queue_job/tests/test_model_job_function.py +++ b/queue_job/tests/test_model_job_function.py @@ -5,7 +5,7 @@ from odoo.tests import common -class TestJobFunction(common.SavepointCase): +class TestJobFunction(common.TransactionCase): def test_function_name_compute(self): function = self.env["queue.job.function"].create( {"model_id": self.env.ref("base.model_res_users").id, "method": "read"} diff --git a/queue_job/tests/test_queue_job_protected_write.py b/queue_job/tests/test_queue_job_protected_write.py index cf8380bcec..018b3f23f4 100644 --- a/queue_job/tests/test_queue_job_protected_write.py +++ b/queue_job/tests/test_queue_job_protected_write.py @@ -5,7 +5,7 @@ from odoo.tests import common -class TestJobWriteProtected(common.SavepointCase): +class TestJobWriteProtected(common.TransactionCase): def test_create_error(self): with self.assertRaises(exceptions.AccessError): self.env["queue.job"].create( diff --git a/setup/queue_job/odoo/addons/queue_job b/setup/queue_job/odoo/addons/queue_job new file mode 120000 index 0000000000..ac796aaa1c --- /dev/null +++ b/setup/queue_job/odoo/addons/queue_job @@ -0,0 +1 @@ +../../../../queue_job \ No newline at end of file diff --git a/setup/queue_job/setup.py b/setup/queue_job/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/queue_job/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/test_queue_job/odoo/addons/test_queue_job b/setup/test_queue_job/odoo/addons/test_queue_job new file mode 120000 index 0000000000..0473d5583c --- /dev/null +++ b/setup/test_queue_job/odoo/addons/test_queue_job @@ -0,0 +1 @@ +../../../../test_queue_job \ No newline at end of file diff --git a/setup/test_queue_job/setup.py b/setup/test_queue_job/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/test_queue_job/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test_queue_job/__manifest__.py b/test_queue_job/__manifest__.py index 8f5b31c0fe..b73c65f5c5 100644 --- a/test_queue_job/__manifest__.py +++ b/test_queue_job/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Queue Job Tests", - "version": "14.0.1.3.0", + "version": "15.0.1.0.0", "author": "Camptocamp,Odoo Community Association (OCA)", "license": "LGPL-3", "category": "Generic Modules", @@ -14,5 +14,5 @@ "data/queue_job_function_data.xml", "security/ir.model.access.csv", ], - "installable": False, + "installable": True, } diff --git a/test_queue_job/tests/common.py b/test_queue_job/tests/common.py index 63ca8cca48..a32fcc380a 100644 --- a/test_queue_job/tests/common.py +++ b/test_queue_job/tests/common.py @@ -7,11 +7,12 @@ class JobCommonCase(common.TransactionCase): - def setUp(self): - super().setUp() - self.queue_job = self.env["queue.job"] - self.user = self.env["res.users"] - self.method = self.env["test.queue.job"].testing_method + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.queue_job = cls.env["queue.job"] + cls.user = cls.env["res.users"] + cls.method = cls.env["test.queue.job"].testing_method def _create_job(self): test_job = Job(self.method) diff --git a/test_queue_job/tests/test_autovacuum.py b/test_queue_job/tests/test_autovacuum.py index 851af421bc..09730a4fea 100644 --- a/test_queue_job/tests/test_autovacuum.py +++ b/test_queue_job/tests/test_autovacuum.py @@ -7,9 +7,10 @@ class TestQueueJobAutovacuumCronJob(JobCommonCase): - def setUp(self): - super().setUp() - self.cron_job = self.env.ref("queue_job.ir_cron_autovacuum_queue_jobs") + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.cron_job = cls.env.ref("queue_job.ir_cron_autovacuum_queue_jobs") def test_old_jobs_are_deleted_by_cron_job(self): """Old jobs are deleted by the autovacuum cron job.""" diff --git a/test_queue_job/tests/test_job.py b/test_queue_job/tests/test_job.py index 56d165896a..2254db9011 100644 --- a/test_queue_job/tests/test_job.py +++ b/test_queue_job/tests/test_job.py @@ -29,7 +29,7 @@ class TestJobsOnTestingMethod(JobCommonCase): - """ Test Job """ + """Test Job""" def test_new_job(self): """ @@ -39,14 +39,14 @@ def test_new_job(self): self.assertEqual(test_job.func.__func__, self.method.__func__) def test_eta(self): - """ When an `eta` is datetime, it uses it """ + """When an `eta` is datetime, it uses it""" now = datetime.now() method = self.env["res.users"].mapped job_a = Job(method, eta=now) self.assertEqual(job_a.eta, now) def test_eta_integer(self): - """ When an `eta` is an integer, it adds n seconds up to now """ + """When an `eta` is an integer, it adds n seconds up to now""" datetime_path = "odoo.addons.queue_job.job.datetime" with mock.patch(datetime_path, autospec=True) as mock_datetime: mock_datetime.now.return_value = datetime(2015, 3, 15, 16, 41, 0) @@ -54,7 +54,7 @@ def test_eta_integer(self): self.assertEqual(job_a.eta, datetime(2015, 3, 15, 16, 42, 0)) def test_eta_timedelta(self): - """ When an `eta` is a timedelta, it adds it up to now """ + """When an `eta` is a timedelta, it adds it up to now""" datetime_path = "odoo.addons.queue_job.job.datetime" with mock.patch(datetime_path, autospec=True) as mock_datetime: mock_datetime.now.return_value = datetime(2015, 3, 15, 16, 41, 0) @@ -324,7 +324,7 @@ def test_job_identity_key_func_exact(self): class TestJobs(JobCommonCase): - """ Test jobs on other methods or with different job configuration """ + """Test jobs on other methods or with different job configuration""" def test_description(self): """If no description is given to the job, it @@ -343,7 +343,7 @@ def test_description(self): self.assertEqual(job_a.description, description) def test_retry_pattern(self): - """ When we specify a retry pattern, the eta must follow it""" + """When we specify a retry pattern, the eta must follow it""" datetime_path = "odoo.addons.queue_job.job.datetime" method = self.env["test.queue.job"].job_with_retry_pattern with mock.patch(datetime_path, autospec=True) as mock_datetime: @@ -371,7 +371,7 @@ def test_retry_pattern(self): self.assertEqual(test_job.eta, datetime(2015, 6, 1, 15, 15, 0)) def test_retry_pattern_no_zero(self): - """ When we specify a retry pattern without 0, uses RETRY_INTERVAL""" + """When we specify a retry pattern without 0, uses RETRY_INTERVAL""" method = self.env["test.queue.job"].job_with_retry_pattern__no_zero test_job = Job(method, max_retries=0) test_job.retry += 1 @@ -400,7 +400,7 @@ def test_job_delay_model_method_multi(self): self.assertEqual(["test1", "test2"], job_instance.perform()) def test_job_identity_key_no_duplicate(self): - """ If a job with same identity key in queue do not add a new one """ + """If a job with same identity key in queue do not add a new one""" id_key = "e294e8444453b09d59bdb6efbfec1323" rec1 = self.env["test.queue.job"].create({"name": "test1"}) job_1 = rec1.with_delay(identity_key=id_key).mapped("name") @@ -410,7 +410,7 @@ def test_job_identity_key_no_duplicate(self): self.assertEqual(job_2.uuid, job_1.uuid) def test_job_with_mutable_arguments(self): - """ Job with mutable arguments do not mutate on perform() """ + """Job with mutable arguments do not mutate on perform()""" delayable = self.env["test.queue.job"].with_delay() job_instance = delayable.job_alter_mutable([1], mutable_kwarg={"a": 1}) self.assertTrue(job_instance) @@ -531,7 +531,7 @@ def test_job_change_user_id(self): class TestJobStorageMultiCompany(common.TransactionCase): - """ Test storage of jobs """ + """Test storage of jobs""" def setUp(self): super(TestJobStorageMultiCompany, self).setUp() diff --git a/test_queue_job/tests/test_job_channels.py b/test_queue_job/tests/test_job_channels.py index 1043e9708b..aff250d252 100644 --- a/test_queue_job/tests/test_job_channels.py +++ b/test_queue_job/tests/test_job_channels.py @@ -8,12 +8,13 @@ class TestJobChannels(common.TransactionCase): - def setUp(self): - super(TestJobChannels, self).setUp() - self.function_model = self.env["queue.job.function"] - self.channel_model = self.env["queue.job.channel"] - self.test_model = self.env["test.queue.channel"] - self.root_channel = self.env.ref("queue_job.channel_root") + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.function_model = cls.env["queue.job.function"] + cls.channel_model = cls.env["queue.job.channel"] + cls.test_model = cls.env["test.queue.channel"] + cls.root_channel = cls.env.ref("queue_job.channel_root") def test_channel_complete_name(self): channel = self.channel_model.create( diff --git a/test_queue_job/tests/test_related_actions.py b/test_queue_job/tests/test_related_actions.py index 8af89853fa..c4ad0ec3b3 100644 --- a/test_queue_job/tests/test_related_actions.py +++ b/test_queue_job/tests/test_related_actions.py @@ -5,8 +5,8 @@ from odoo import exceptions -class TestRelatedAction(common.SavepointCase): - """ Test Related Actions """ +class TestRelatedAction(common.TransactionCase): + """Test Related Actions""" @classmethod def setUpClass(cls): @@ -16,7 +16,7 @@ def setUpClass(cls): cls.records = cls.record + cls.model.create({}) def test_attributes(self): - """ Job with related action check if action returns correctly """ + """Job with related action check if action returns correctly""" job_ = self.record.with_delay().testing_related_action__kwargs() act_job, act_kwargs = job_.related_action() self.assertEqual(act_job, job_.db_record())