Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions queue_job/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()}
Expand Down
2 changes: 0 additions & 2 deletions queue_job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ()
Expand Down
35 changes: 34 additions & 1 deletion queue_job/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
import os

from odoo import models
from odoo import api, models

from ..job import DelayableRecordset

Expand Down Expand Up @@ -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()
}
7 changes: 7 additions & 0 deletions queue_job/readme/USAGE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions queue_job/tests/test_json_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion test_queue_job/models/test_models.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions test_queue_job/tests/test_json_field.py
Original file line number Diff line number Diff line change
@@ -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)