diff --git a/queue_job/fields.py b/queue_job/fields.py index 7a4bcaa6b9..2c78c18836 100644 --- a/queue_job/fields.py +++ b/queue_job/fields.py @@ -69,6 +69,9 @@ def convert_to_record(self, value, record): class JobEncoder(json.JSONEncoder): """Encode Odoo recordsets so that we can later recompose them""" + def _get_record_context(self, obj): + return obj._job_prepare_context_before_enqueue() + def default(self, obj): if isinstance(obj, models.BaseModel): return { @@ -77,6 +80,7 @@ def default(self, obj): "ids": obj.ids, "uid": obj.env.uid, "su": obj.env.su, + "context": self._get_record_context(obj), } elif isinstance(obj, datetime): return {"_type": "datetime_isoformat", "value": obj.isoformat()} diff --git a/queue_job/models/base.py b/queue_job/models/base.py index d218d0d777..16f106450a 100644 --- a/queue_job/models/base.py +++ b/queue_job/models/base.py @@ -251,3 +251,22 @@ def _job_store_values(self, job): :return: dictionary for setting job values. """ return {} + + @api.model + def _job_prepare_context_before_enqueue_keys(self): + """Keys to keep in context of stored jobs + Empty by default for backward compatibility. + """ + # TODO: when migrating to 16.0, active the base context keys: + # return ("tz", "lang", "allowed_company_ids", "force_company", "active_test") + return () + + def _job_prepare_context_before_enqueue(self): + """Return the context to store in the jobs + Can be used to keep only safe keys. + """ + return { + key: value + for key, value in self.env.context.items() + if key in self._job_prepare_context_before_enqueue_keys() + } diff --git a/queue_job/readme/USAGE.rst b/queue_job/readme/USAGE.rst index 980cb126fb..743da10236 100644 --- a/queue_job/readme/USAGE.rst +++ b/queue_job/readme/USAGE.rst @@ -242,6 +242,30 @@ Based on this configuration, we can tell that: * retries 10 to 15 postponed 30 seconds later * all subsequent retries postponed 5 minutes later +**Job Context** + +The context of the recordset of the job, or any recordset passed in arguments of +a job, is transferred to the job according to an allow-list. + +The default allow-list is empty for backward compatibility. The allow-list can +be customized in ``Base._job_prepare_context_before_enqueue_keys``. + +Example: + +.. code-block:: python + + class Base(models.AbstractModel): + + _inherit = "base" + + @api.model + def _job_prepare_context_before_enqueue_keys(self): + """Keys to keep in context of stored jobs + + Empty by default for backward compatibility. + """ + return ("tz", "lang", "allowed_company_ids", "force_company", "active_test") + **Bypass jobs on running Odoo** When you are developing (ie: connector modules) you might want diff --git a/queue_job/tests/test_json_field.py b/queue_job/tests/test_json_field.py index 3028bc0d02..802911c9eb 100644 --- a/queue_job/tests/test_json_field.py +++ b/queue_job/tests/test_json_field.py @@ -16,7 +16,8 @@ class TestJson(common.TransactionCase): def test_encoder_recordset(self): demo_user = self.env.ref("base.user_demo") - partner = self.env(user=demo_user).ref("base.main_partner") + context = demo_user.context_get() + partner = self.env(user=demo_user, context=context).ref("base.main_partner") value = partner value_json = json.dumps(value, cls=JobEncoder) expected = { @@ -25,12 +26,15 @@ def test_encoder_recordset(self): "model": "res.partner", "ids": [partner.id], "su": False, + # no allowed context by default, must be changed in 16.0 + "context": {}, } self.assertEqual(json.loads(value_json), expected) def test_encoder_recordset_list(self): demo_user = self.env.ref("base.user_demo") - partner = self.env(user=demo_user).ref("base.main_partner") + context = demo_user.context_get() + partner = self.env(user=demo_user, context=context).ref("base.main_partner") value = ["a", 1, partner] value_json = json.dumps(value, cls=JobEncoder) expected = [ @@ -42,18 +46,23 @@ def test_encoder_recordset_list(self): "model": "res.partner", "ids": [partner.id], "su": False, + # no allowed context by default, must be changed in 16.0 + "context": {}, }, ] self.assertEqual(json.loads(value_json), expected) def test_decoder_recordset(self): demo_user = self.env.ref("base.user_demo") + context = demo_user.context_get() partner = self.env(user=demo_user).ref("base.main_partner") value_json = ( '{"_type": "odoo_recordset",' '"model": "res.partner",' '"su": false,' - '"ids": [%s],"uid": %s}' % (partner.id, demo_user.id) + '"ids": [%s],"uid": %s, ' + '"context": {"tz": "%s", "lang": "%s"}}' + % (partner.id, demo_user.id, context["tz"], context["lang"]) ) expected = partner value = json.loads(value_json, cls=JobDecoder, env=self.env) @@ -62,13 +71,16 @@ def test_decoder_recordset(self): def test_decoder_recordset_list(self): demo_user = self.env.ref("base.user_demo") + context = demo_user.context_get() partner = self.env(user=demo_user).ref("base.main_partner") value_json = ( '["a", 1, ' '{"_type": "odoo_recordset",' '"model": "res.partner",' '"su": false,' - '"ids": [%s],"uid": %s}]' % (partner.id, demo_user.id) + '"ids": [%s],"uid": %s, ' + '"context": {"tz": "%s", "lang": "%s"}}]' + % (partner.id, demo_user.id, context["tz"], context["lang"]) ) expected = ["a", 1, partner] value = json.loads(value_json, cls=JobDecoder, env=self.env) diff --git a/test_queue_job/models/test_models.py b/test_queue_job/models/test_models.py index a269cbd6da..f810dba862 100644 --- a/test_queue_job/models/test_models.py +++ b/test_queue_job/models/test_models.py @@ -1,7 +1,7 @@ # Copyright 2016 Camptocamp SA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) -from odoo import fields, models +from odoo import api, fields, models from odoo.addons.queue_job.delay import chain from odoo.addons.queue_job.exception import RetryableJobError @@ -37,6 +37,11 @@ class ModelTestQueueJob(models.Model): name = fields.Char() + # to test the context is serialized/deserialized properly + @api.model + def _job_prepare_context_before_enqueue_keys(self): + return ("tz", "lang") + def testing_method(self, *args, **kwargs): """Method used for tests diff --git a/test_queue_job/tests/test_json_field.py b/test_queue_job/tests/test_json_field.py new file mode 100644 index 0000000000..59a75e3994 --- /dev/null +++ b/test_queue_job/tests/test_json_field.py @@ -0,0 +1,32 @@ +# copyright 2022 Guewen Baconnier +# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +import json + +from odoo.tests import common + +# pylint: disable=odoo-addons-relative-import +# we are testing, we want to test as if we were an external consumer of the API +from odoo.addons.queue_job.fields import JobEncoder + + +class TestJsonField(common.TransactionCase): + + # TODO: when migrating to 16.0, adapt the checks in queue_job/tests/test_json_field.py + # to verify the context keys are encoded and remove these + def test_encoder_recordset_store_context(self): + demo_user = self.env.ref("base.user_demo") + user_context = {"lang": "en_US", "tz": "Europe/Brussels"} + test_model = self.env(user=demo_user, context=user_context)["test.queue.job"] + value_json = json.dumps(test_model, cls=JobEncoder) + self.assertEqual(json.loads(value_json)["context"], user_context) + + def test_encoder_recordset_context_filter_keys(self): + demo_user = self.env.ref("base.user_demo") + user_context = {"lang": "en_US", "tz": "Europe/Brussels"} + tampered_context = dict(user_context, foo=object()) + test_model = self.env(user=demo_user, context=tampered_context)[ + "test.queue.job" + ] + value_json = json.dumps(test_model, cls=JobEncoder) + self.assertEqual(json.loads(value_json)["context"], user_context)