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/job.py b/queue_job/job.py index 4ee90390e6..36d2b13158 100644 --- a/queue_job/job.py +++ b/queue_job/job.py @@ -415,8 +415,6 @@ def __init__( :param identity_key: A hash to uniquely identify a job, or a function that returns this hash (the function takes the job as argument) - :param env: Odoo Environment - :type env: :class:`odoo.api.Environment` """ if args is None: args = () diff --git a/queue_job/models/base.py b/queue_job/models/base.py index 5b1f8fee79..98e9ae92dd 100644 --- a/queue_job/models/base.py +++ b/queue_job/models/base.py @@ -5,7 +5,7 @@ import logging import os -from odoo import models +from odoo import api, models from ..job import DelayableRecordset @@ -189,3 +189,36 @@ def auto_delay_wrapper(self, *args, **kwargs): origin = getattr(self, method_name) return functools.update_wrapper(auto_delay_wrapper, origin) + + @api.model + def _job_store_values(self, job): + """Hook for manipulating job stored values. + + You can define a more specific hook for a job function + by defining a method name with this pattern: + + `_queue_job_store_values_${func_name}` + + NOTE: values will be stored only if they match stored fields on `queue.job`. + + :param job: current queue_job.job.Job instance. + :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. + """ + return ("tz", "lang", "allowed_company_ids", "force_company", "active_test") + + 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 6c472eccf9..80a892afe4 100644 --- a/queue_job/readme/USAGE.rst +++ b/queue_job/readme/USAGE.rst @@ -118,6 +118,13 @@ 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 `("tz", "lang", "allowed_company_ids", "force_company", "active_test")`. It can +be customized in ``Base._job_prepare_context_before_enqueue_keys``. **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..dd3e09cf33 100644 --- a/queue_job/tests/test_json_field.py +++ b/queue_job/tests/test_json_field.py @@ -16,23 +16,30 @@ 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_context = context.copy() + expected_context.pop("uid") expected = { "uid": demo_user.id, "_type": "odoo_recordset", "model": "res.partner", "ids": [partner.id], "su": False, + "context": expected_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_context = context.copy() + expected_context.pop("uid") expected = [ "a", 1, @@ -42,18 +49,22 @@ def test_encoder_recordset_list(self): "model": "res.partner", "ids": [partner.id], "su": False, + "context": expected_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 +73,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 a5a3843230..389ad32832 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.exception import RetryableJobError @@ -33,6 +33,11 @@ class TestQueueJob(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)