diff --git a/base_export_async/models/delay_export.py b/base_export_async/models/delay_export.py index a7dde9ba81..549d44a1e1 100644 --- a/base_export_async/models/delay_export.py +++ b/base_export_async/models/delay_export.py @@ -89,14 +89,18 @@ def export(self, params): export_record = self.sudo().create({"user_ids": [(6, 0, users.ids)]}) name = "{}.{}".format(model_name, export_format) - attachment = self.env["ir.attachment"].create( - { - "name": name, - "datas": base64.b64encode(content), - "type": "binary", - "res_model": self._name, - "res_id": export_record.id, - } + attachment = ( + self.env["ir.attachment"] + .sudo() + .create( + { + "name": name, + "datas": base64.b64encode(content), + "type": "binary", + "res_model": self._name, + "res_id": export_record.id, + } + ) ) url = "{}/web/content/ir.attachment/{}/datas/{}?download=true".format( diff --git a/base_export_async/tests/test_base_export_async.py b/base_export_async/tests/test_base_export_async.py index 482fabaee0..d10ca04281 100644 --- a/base_export_async/tests/test_base_export_async.py +++ b/base_export_async/tests/test_base_export_async.py @@ -23,7 +23,7 @@ "domain": [], "context": {"lang": "en_US", "tz": "Europe/Brussels", "uid": 2}, "import_compat": false, - "user_ids": [2] + "user_ids": [6] }""" } @@ -37,7 +37,7 @@ "domain": [], "context": {"lang": "en_US", "tz": "Europe/Brussels", "uid": 2}, "import_compat": false, - "user_ids": [2] + "user_ids": [6] }""" } diff --git a/base_import_async/data/queue_job_function_data.xml b/base_import_async/data/queue_job_function_data.xml index 22cc8dbab0..fb04a63613 100644 --- a/base_import_async/data/queue_job_function_data.xml +++ b/base_import_async/data/queue_job_function_data.xml @@ -1,7 +1,13 @@ + + base_import + + + _split_file + _import_one_chunk + Configure default options for job Bypass jobs on running Odoo

When you are developing (ie: connector modules) you might want to bypass the queue job and run your code immediately.

-

To do so you can set QUEUE_JOB__NO_DELAY=1 in your enviroment.

+

To do so you can set QUEUE_JOB__NO_DELAY=1 in your environment.

Bypass jobs in tests

When writing tests on job-related methods is always tricky to deal with delayed recordsets. To make your testing life easier diff --git a/queue_job/tests/common.py b/queue_job/tests/common.py index c463d3456d..13f1f5f832 100644 --- a/queue_job/tests/common.py +++ b/queue_job/tests/common.py @@ -428,7 +428,7 @@ def __init__( def setUp(self): """Log an extra statement which test is started.""" - super(OdooDocTestCase, self).setUp() + super().setUp() logging.getLogger(__name__).info("Running tests for %s", self._dt_test.name) diff --git a/queue_job/views/queue_job_views.xml b/queue_job/views/queue_job_views.xml index a7099254d0..40d060931a 100644 --- a/queue_job/views/queue_job_views.xml +++ b/queue_job/views/queue_job_views.xml @@ -250,6 +250,22 @@ string="Cancelled" domain="[('state', '=', 'cancelled')]" /> + + + + + diff --git a/queue_job_cron/models/ir_cron.py b/queue_job_cron/models/ir_cron.py index 7e4f5b848d..bb09ed075e 100644 --- a/queue_job_cron/models/ir_cron.py +++ b/queue_job_cron/models/ir_cron.py @@ -28,13 +28,16 @@ class IrCron(models.Model): comodel_name="queue.job.channel", compute="_compute_run_as_queue_job", readonly=False, + store=True, string="Channel", ) @api.depends("run_as_queue_job") def _compute_run_as_queue_job(self): for cron in self: - if cron.run_as_queue_job and not cron.channel_id: + if cron.channel_id: + continue + if cron.run_as_queue_job: cron.channel_id = self.env.ref("queue_job_cron.channel_root_ir_cron").id else: cron.channel_id = False diff --git a/queue_job_cron_jobrunner/models/queue_job.py b/queue_job_cron_jobrunner/models/queue_job.py index 2e19556b95..a3c3d721e4 100644 --- a/queue_job_cron_jobrunner/models/queue_job.py +++ b/queue_job_cron_jobrunner/models/queue_job.py @@ -40,7 +40,7 @@ def _acquire_one_job(self): FROM queue_job WHERE state = 'pending' AND (eta IS NULL OR eta <= (now() AT TIME ZONE 'UTC')) - ORDER BY date_created DESC + ORDER BY priority, date_created LIMIT 1 FOR NO KEY UPDATE SKIP LOCKED """ ) diff --git a/queue_job_cron_jobrunner/tests/test_queue_job.py b/queue_job_cron_jobrunner/tests/test_queue_job.py index 3f2e0ef637..54800b792c 100644 --- a/queue_job_cron_jobrunner/tests/test_queue_job.py +++ b/queue_job_cron_jobrunner/tests/test_queue_job.py @@ -67,5 +67,33 @@ def test_queue_job_cron_trigger_enqueue_dependencies(self): self.assertEqual(job_record.state, "done", "Processed OK") # if the state is "waiting_dependencies", it means the "enqueue_waiting()" - # step has not been doen when the parent job has been done + # step has not been done when the parent job has been done self.assertEqual(job_record_depends.state, "done", "Processed OK") + + def test_acquire_one_job_use_priority(self): + with freeze_time("2024-01-01 10:01:01"): + self.env["res.partner"].with_delay(priority=3).create({"name": "test"}) + + with freeze_time("2024-01-01 10:02:01"): + job = ( + self.env["res.partner"].with_delay(priority=1).create({"name": "test"}) + ) + + with freeze_time("2024-01-01 10:03:01"): + self.env["res.partner"].with_delay(priority=2).create({"name": "test"}) + + self.assertEqual(self.env["queue.job"]._acquire_one_job(), job.db_record()) + + def test_acquire_one_job_consume_the_oldest_first(self): + with freeze_time("2024-01-01 10:01:01"): + job = ( + self.env["res.partner"].with_delay(priority=30).create({"name": "test"}) + ) + + with freeze_time("2024-01-01 10:02:01"): + self.env["res.partner"].with_delay(priority=30).create({"name": "test"}) + + with freeze_time("2024-01-01 10:03:01"): + self.env["res.partner"].with_delay(priority=30).create({"name": "test"}) + + self.assertEqual(self.env["queue.job"]._acquire_one_job(), job.db_record()) diff --git a/queue_job_subscribe/tests/test_job_subscribe.py b/queue_job_subscribe/tests/test_job_subscribe.py index 0f1fcddf48..935f15f74a 100644 --- a/queue_job_subscribe/tests/test_job_subscribe.py +++ b/queue_job_subscribe/tests/test_job_subscribe.py @@ -8,7 +8,7 @@ class TestJobSubscribe(common.TransactionCase): def setUp(self): - super(TestJobSubscribe, self).setUp() + super().setUp() grp_queue_job_manager = self.ref("queue_job.group_queue_job_manager") self.other_partner_a = self.env["res.partner"].create( {"name": "My Company a", "is_company": True, "email": "test@tes.ttest"} diff --git a/test_queue_job/models/test_models.py b/test_queue_job/models/test_models.py index f810dba862..03fa792137 100644 --- a/test_queue_job/models/test_models.py +++ b/test_queue_job/models/test_models.py @@ -40,7 +40,7 @@ class ModelTestQueueJob(models.Model): # to test the context is serialized/deserialized properly @api.model def _job_prepare_context_before_enqueue_keys(self): - return ("tz", "lang") + return ("tz", "lang", "allowed_company_ids") def testing_method(self, *args, **kwargs): """Method used for tests @@ -76,7 +76,7 @@ def job_with_retry_pattern__no_zero(self): return def mapped(self, func): - return super(ModelTestQueueJob, self).mapped(func) + return super().mapped(func) def job_alter_mutable(self, mutable_arg, mutable_kwarg=None): mutable_arg.append(2) diff --git a/test_queue_job/tests/__init__.py b/test_queue_job/tests/__init__.py index dc59429e71..0405022ce0 100644 --- a/test_queue_job/tests/__init__.py +++ b/test_queue_job/tests/__init__.py @@ -4,5 +4,6 @@ from . import test_job from . import test_job_auto_delay from . import test_job_channels +from . import test_job_function from . import test_related_actions from . import test_delay_mocks diff --git a/test_queue_job/tests/test_job.py b/test_queue_job/tests/test_job.py index 1585f992f0..d7414ef7aa 100644 --- a/test_queue_job/tests/test_job.py +++ b/test_queue_job/tests/test_job.py @@ -15,6 +15,7 @@ RetryableJobError, ) from odoo.addons.queue_job.job import ( + CANCELLED, DONE, ENQUEUED, FAILED, @@ -88,7 +89,7 @@ def test_infinite_retryable_error(self): self.assertEqual(test_job.retry, 1) def test_on_instance_method(self): - class A(object): + class A: def method(self): pass @@ -185,6 +186,47 @@ def test_postpone(self): self.assertEqual(job_a.result, "test") self.assertFalse(job_a.exc_info) + def test_company_simple(self): + company = self.env.ref("base.main_company") + eta = datetime.now() + timedelta(hours=5) + test_job = Job( + self.env["test.queue.job"].with_company(company).testing_method, + args=("o", "k"), + kwargs={"return_context": 1}, + priority=15, + eta=eta, + description="My description", + ) + test_job.worker_pid = 99999 # normally set on "set_start" + test_job.store() + job_read = Job.load(self.env, test_job.uuid) + self.assertEqual(test_job.func.__func__, job_read.func.__func__) + result_ctx = job_read.func(*tuple(test_job.args), **test_job.kwargs) + self.assertEqual(result_ctx.get("allowed_company_ids"), company.ids) + + def test_company_complex(self): + company1 = self.env.ref("base.main_company") + company2 = company1.create({"name": "Queue job company"}) + companies = company1 | company2 + self.env.user.write({"company_ids": [(6, False, companies.ids)]}) + # Ensure the main company still the first + self.assertEqual(self.env.user.company_id, company1) + eta = datetime.now() + timedelta(hours=5) + test_job = Job( + self.env["test.queue.job"].with_company(company2).testing_method, + args=("o", "k"), + kwargs={"return_context": 1}, + priority=15, + eta=eta, + description="My description", + ) + test_job.worker_pid = 99999 # normally set on "set_start" + test_job.store() + job_read = Job.load(self.env, test_job.uuid) + self.assertEqual(test_job.func.__func__, job_read.func.__func__) + result_ctx = job_read.func(*tuple(test_job.args), **test_job.kwargs) + self.assertEqual(result_ctx.get("allowed_company_ids"), company2.ids) + def test_store(self): test_job = Job(self.method) test_job.store() @@ -489,6 +531,42 @@ def test_button_done(self): stored.result, "Manually set to done by %s" % self.env.user.name ) + def test_button_done_enqueue_waiting_dependencies(self): + job_root = Job(self.env["test.queue.job"].testing_method) + job_child = Job(self.env["test.queue.job"].testing_method) + job_child.add_depends({job_root}) + + DelayableGraph._ensure_same_graph_uuid([job_root, job_child]) + job_root.store() + job_child.store() + + self.assertEqual(job_child.state, WAIT_DEPENDENCIES) + record_root = job_root.db_record() + record_child = job_child.db_record() + # Trigger button done + record_root.button_done() + # Check the state + self.assertEqual(record_root.state, DONE) + self.assertEqual(record_child.state, PENDING) + + def test_button_cancel_dependencies(self): + job_root = Job(self.env["test.queue.job"].testing_method) + job_child = Job(self.env["test.queue.job"].testing_method) + job_child.add_depends({job_root}) + + DelayableGraph._ensure_same_graph_uuid([job_root, job_child]) + job_root.store() + job_child.store() + + self.assertEqual(job_child.state, WAIT_DEPENDENCIES) + record_root = job_root.db_record() + record_child = job_child.db_record() + # Trigger button cancelled + record_root.button_cancelled() + # Check the state + self.assertEqual(record_root.state, CANCELLED) + self.assertEqual(record_child.state, CANCELLED) + def test_requeue(self): stored = self._create_job() stored.write({"state": "failed"}) @@ -572,7 +650,7 @@ class TestJobStorageMultiCompany(common.TransactionCase): """Test storage of jobs""" def setUp(self): - super(TestJobStorageMultiCompany, self).setUp() + super().setUp() self.queue_job = self.env["queue.job"] grp_queue_job_manager = self.ref("queue_job.group_queue_job_manager") User = self.env["res.users"] diff --git a/test_queue_job/tests/test_job_function.py b/test_queue_job/tests/test_job_function.py new file mode 100644 index 0000000000..320b4973c5 --- /dev/null +++ b/test_queue_job/tests/test_job_function.py @@ -0,0 +1,35 @@ +import odoo.tests.common as common +from odoo import exceptions + + +class TestJobFunction(common.TransactionCase): + def setUp(self): + super().setUp() + self.test_function_model = self.env.ref( + "queue_job.job_function_queue_job__test_job" + ) + + def test_check_retry_pattern_randomized_case(self): + randomized_pattern = "{1: (10, 20), 2: (20, 40)}" + self.test_function_model.edit_retry_pattern = randomized_pattern + self.assertEqual( + self.test_function_model.edit_retry_pattern, randomized_pattern + ) + + def test_check_retry_pattern_fixed_case(self): + fixed_pattern = "{1: 10, 2: 20}" + self.test_function_model.edit_retry_pattern = fixed_pattern + self.assertEqual(self.test_function_model.edit_retry_pattern, fixed_pattern) + + def test_check_retry_pattern_invalid_cases(self): + invalid_time_value_pattern = "{1: a, 2: 20}" + with self.assertRaises(exceptions.UserError): + self.test_function_model.edit_retry_pattern = invalid_time_value_pattern + + invalid_retry_count_pattern = "{a: 10, 2: 20}" + with self.assertRaises(exceptions.UserError): + self.test_function_model.edit_retry_pattern = invalid_retry_count_pattern + + invalid_randomized_pattern = "{1: (1, 2, 3), 2: 20}" + with self.assertRaises(exceptions.UserError): + self.test_function_model.edit_retry_pattern = invalid_randomized_pattern