From 6748713bdd1e42bf90e825da144bd8452fafdde1 Mon Sep 17 00:00:00 2001 From: Dorin Hongu Date: Sat, 26 Sep 2026 13:37:35 +0300 Subject: [PATCH] [MIG] queue_job: Migration to 20.0 - ir.model.access + ir.rule -> ir.access - registry.clear_cache -> transaction.invalidate_ormcache - controller: use odoo.sql_db.PG_CONCURRENCY_EXCEPTIONS_TO_RETRY (odoo.service.model was removed); env.clear() -> env.transaction.clear() - jobrunner: odoo.modules.db.list_dbs (odoo.service.db was removed) - fields: _setup_attrs -> _setup_attrs__; tools.ormcache -> api.ormcache - JS: job_direct_graph widget ported to Owl 3 (useProps, signal ref) - views: remaining_days widget -> relative_date; stat button icon - tests: odoo.http.Request -> odoo.http.requestlib.Request - lint fixes required by the 20.0 template (ruff 0.16, pylint-odoo 10) Co-authored-by: Jared Kipe Co-Authored-By: Claude Opus 5.5 --- .pre-commit-config.yaml | 7 +++ queue_job/README.rst | 10 ++-- queue_job/__manifest__.py | 6 +-- queue_job/controllers/main.py | 15 +++--- queue_job/data/queue_data.xml | 6 +-- queue_job/data/queue_job_function_data.xml | 1 + queue_job/delay.py | 6 +-- queue_job/fields.py | 6 +-- queue_job/job.py | 23 +++++----- queue_job/jobrunner/__init__.py | 2 +- queue_job/jobrunner/channels.py | 2 +- queue_job/jobrunner/runner.py | 28 +++++------ queue_job/models/queue_job.py | 9 ++-- queue_job/models/queue_job_channel.py | 2 +- queue_job/models/queue_job_function.py | 15 +++--- queue_job/security/ir.access.csv | 9 ++++ queue_job/security/ir.model.access.csv | 8 ---- queue_job/security/security.xml | 46 +++++++------------ queue_job/static/description/index.html | 8 ++-- .../job_direct_graph/job_direct_graph.esm.js | 44 ++++++++++-------- .../job_direct_graph/job_direct_graph.xml | 2 +- queue_job/tests/common.py | 16 ++++--- queue_job/tests/test_delayable_split.py | 8 ++-- queue_job/tests/test_json_field.py | 4 +- .../tests/test_run_job_controller_http.py | 6 +-- queue_job/views/queue_job_channel_views.xml | 2 +- queue_job/views/queue_job_function_views.xml | 2 +- queue_job/views/queue_job_menus.xml | 2 +- queue_job/views/queue_job_views.xml | 6 +-- .../wizards/queue_jobs_to_cancelled_views.xml | 2 +- .../wizards/queue_jobs_to_done_views.xml | 2 +- queue_job/wizards/queue_requeue_job_views.xml | 2 +- 32 files changed, 151 insertions(+), 156 deletions(-) create mode 100644 queue_job/security/ir.access.csv delete mode 100644 queue_job/security/ir.model.access.csv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02bbe150e5..cdbd3cf757 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,13 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS + ^base_import_async/| + ^queue_job_batch/| + ^queue_job_cron/| + ^queue_job_cron_jobrunner/| + ^queue_job_subscribe/| + ^test_queue_job/| + ^test_queue_job_batch/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops ^setup/|/static/description/index\.html$| diff --git a/queue_job/README.rst b/queue_job/README.rst index f9a974d192..70e9f6704e 100644 --- a/queue_job/README.rst +++ b/queue_job/README.rst @@ -21,13 +21,13 @@ Job Queue :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github - :target: https://github.com/OCA/queue/tree/19.0/queue_job + :target: https://github.com/OCA/queue/tree/20.0/queue_job :alt: OCA/queue .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/queue-19-0/queue-19-0-queue_job + :target: https://translation.odoo-community.org/projects/queue-20-0/queue-20-0-queue_job :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=19.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=20.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -685,7 +685,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -747,6 +747,6 @@ Current `maintainers `__: |maintainer-guewen| |maintainer-sbidoul| -This module is part of the `OCA/queue `_ project on GitHub. +This module is part of the `OCA/queue `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py index 03ea411f2c..c890beaf9c 100644 --- a/queue_job/__manifest__.py +++ b/queue_job/__manifest__.py @@ -2,7 +2,7 @@ { "name": "Job Queue", - "version": "19.0.2.1.3", + "version": "20.0.1.0.0", "author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/queue", "license": "LGPL-3", @@ -11,7 +11,7 @@ "external_dependencies": {"python": ["requests", "openupgradelib"]}, "data": [ "security/security.xml", - "security/ir.model.access.csv", + "security/ir.access.csv", "views/queue_job_views.xml", "views/queue_job_channel_views.xml", "views/queue_job_function_views.xml", @@ -27,7 +27,7 @@ "queue_job/static/src/views/**/*", ], }, - "installable": False, + "installable": True, "development_status": "Mature", "maintainers": ["guewen", "sbidoul"], "post_init_hook": "post_init_hook", diff --git a/queue_job/controllers/main.py b/queue_job/controllers/main.py index 92ff000dc8..e9c04c26d5 100644 --- a/queue_job/controllers/main.py +++ b/queue_job/controllers/main.py @@ -13,7 +13,7 @@ from werkzeug.exceptions import BadRequest, Forbidden from odoo import SUPERUSER_ID, api, http -from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY +from odoo.sql_db import PG_CONCURRENCY_EXCEPTIONS_TO_RETRY from odoo.tools import config from ..delay import chain, group @@ -123,7 +123,7 @@ def _enqueue_dependent_jobs(cls, env, job): except OperationalError as err: # Automatically retry the typical transaction serialization # errors - if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY: + if not isinstance(err, PG_CONCURRENCY_EXCEPTIONS_TO_RETRY): raise if tries >= DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE: _logger.error( @@ -148,7 +148,7 @@ def _enqueue_dependent_jobs(cls, env, job): @classmethod def _runjob(cls, env: api.Environment, job: Job) -> None: def retry_postpone(job, message, seconds=None): - job.env.clear() + job.env.transaction.clear() with job.in_temporary_env(): job.postpone(result=message, seconds=seconds) job.set_pending(reset_retry=False) @@ -160,7 +160,7 @@ def retry_postpone(job, message, seconds=None): except OperationalError as err: # Automatically retry the typical transaction serialization # errors - if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY: + if not isinstance(err, PG_CONCURRENCY_EXCEPTIONS_TO_RETRY): raise _logger.debug("%s OperationalError, postponed", job) @@ -181,7 +181,7 @@ def retry_postpone(job, message, seconds=None): traceback.print_exc(file=buff) traceback_txt = buff.getvalue() _logger.error(traceback_txt) - job.env.clear() + job.env.transaction.clear() with job.in_temporary_env(): vals = cls._get_failure_values(job, traceback_txt, orig_exception) job.set_failed(**vals) @@ -388,6 +388,5 @@ def _create_graph_test_jobs( root_delayable.delay() - return ( - f"graph uuid: {list(root_delayable._head())[0]._generated_job.graph_uuid}" - ) + head = next(iter(root_delayable._head())) + return f"graph uuid: {head._generated_job.graph_uuid}" diff --git a/queue_job/data/queue_data.xml b/queue_job/data/queue_data.xml index 1fb70f106c..39ed16189f 100644 --- a/queue_job/data/queue_data.xml +++ b/queue_job/data/queue_data.xml @@ -1,4 +1,4 @@ - + @@ -12,14 +12,14 @@ - 1 + days code model.autovacuum() - + root diff --git a/queue_job/data/queue_job_function_data.xml b/queue_job/data/queue_job_function_data.xml index 0105dbc508..31ec7ff964 100644 --- a/queue_job/data/queue_job_function_data.xml +++ b/queue_job/data/queue_job_function_data.xml @@ -1,3 +1,4 @@ + diff --git a/queue_job/delay.py b/queue_job/delay.py index 60e98dc9d6..8abc3133bc 100644 --- a/queue_job/delay.py +++ b/queue_job/delay.py @@ -58,7 +58,7 @@ class Graph: instances, although ultimately it is used for this purpose. """ - __slots__ = "_graph" + __slots__ = ("_graph",) def __init__(self, graph=None): if graph: @@ -314,7 +314,7 @@ class DelayableChain: delayable/chain/group object of the graph. """ - __slots__ = ("_graph", "__head", "__tail") + __slots__ = ("__head", "__tail", "_graph") def __init__(self, *delayables): self._graph = DelayableGraph() @@ -371,7 +371,7 @@ class DelayableGroup: delayable/chain/group object of the graph. """ - __slots__ = ("_graph", "_delayables") + __slots__ = ("_delayables", "_graph") def __init__(self, *delayables): self._graph = DelayableGraph() diff --git a/queue_job/fields.py b/queue_job/fields.py index d9f371605f..e2fcbdfe4a 100644 --- a/queue_job/fields.py +++ b/queue_job/fields.py @@ -30,7 +30,7 @@ class JobSerialized(fields.Json): _base_type = None # these are the default values when we convert an empty value - _default_json_mapping = { + _default_json_mapping = { # noqa: RUF012 dict: "{}", list: "[]", tuple: "[]", @@ -42,8 +42,8 @@ class JobSerialized(fields.Json): def __init__(self, string=SENTINEL, base_type=SENTINEL, **kwargs): super().__init__(string=string, _base_type=base_type, **kwargs) - def _setup_attrs(self, model, name): # pylint: disable=missing-return - super()._setup_attrs(model, name) + def _setup_attrs__(self, model_class, name): # pylint: disable=missing-return + super()._setup_attrs__(model_class, name) if self._base_type not in self._default_json_mapping: msg = f"{self._base_type} is not a supported base type" raise ValueError(msg) diff --git a/queue_job/job.py b/queue_job/job.py index 04081976c5..c00f3590c2 100644 --- a/queue_job/job.py +++ b/queue_job/job.py @@ -461,7 +461,7 @@ def __init__( if self.priority is None: self.priority = DEFAULT_PRIORITY - self.date_created = datetime.now() + self.date_created = datetime.now() # noqa: DTZ005 self._description = description if isinstance(identity_key, str): @@ -524,7 +524,7 @@ def perform(self): elif not self.max_retries: # infinite retries raise elif self.retry >= self.max_retries: - type_, value, traceback = sys.exc_info() + type_, value, _traceback = sys.exc_info() # change the exception type but keep the original # traceback and message: # http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/ @@ -745,9 +745,8 @@ def job_function_name(self): @property def identity_key(self): - if self._identity_key is None: - if self._identity_key_func: - self._identity_key = self._identity_key_func(self) + if self._identity_key is None and self._identity_key_func: + self._identity_key = self._identity_key_func(self) return self._identity_key @identity_key.setter @@ -808,9 +807,9 @@ def eta(self, value): if not value: self._eta = None elif isinstance(value, timedelta): - self._eta = datetime.now() + value + self._eta = datetime.now() + value # noqa: DTZ005 elif isinstance(value, int): - self._eta = datetime.now() + timedelta(seconds=value) + self._eta = datetime.now() + timedelta(seconds=value) # noqa: DTZ005 else: self._eta = value @@ -845,13 +844,13 @@ def set_pending(self, result=None, reset_retry=True): def set_enqueued(self): self.state = ENQUEUED - self.date_enqueued = datetime.now() + self.date_enqueued = datetime.now() # noqa: DTZ005 self.date_started = None self.worker_pid = None def set_started(self): self.state = STARTED - self.date_started = datetime.now() + self.date_started = datetime.now() # noqa: DTZ005 self.worker_pid = os.getpid() self.add_lock_record() @@ -859,13 +858,13 @@ def set_done(self, result=None): self.state = DONE self.exc_name = None self.exc_info = None - self.date_done = datetime.now() + self.date_done = datetime.now() # noqa: DTZ005 if result is not None: self.result = result def set_cancelled(self, result=None): self.state = CANCELLED - self.date_cancelled = datetime.now() + self.date_cancelled = datetime.now() # noqa: DTZ005 if result is not None: self.result = result @@ -925,7 +924,7 @@ def related_action(self): if not funcname: funcname = record._default_related_action if not isinstance(funcname, str): - raise ValueError( + raise ValueError( # noqa: TRY004 "related_action must be the name of the method on queue.job as string" ) action = getattr(record, funcname) diff --git a/queue_job/jobrunner/__init__.py b/queue_job/jobrunner/__init__.py index 50dd45e39d..7923d17d69 100644 --- a/queue_job/jobrunner/__init__.py +++ b/queue_job/jobrunner/__init__.py @@ -30,7 +30,7 @@ queue_job_config = dict(cp["queue_job"]) -from .runner import QueueJobRunner, _channels # noqa: E402 +from .runner import QueueJobRunner, _channels _logger = logging.getLogger(__name__) diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py index bfc8984862..d88633ce3d 100644 --- a/queue_job/jobrunner/channels.py +++ b/queue_job/jobrunner/channels.py @@ -179,7 +179,7 @@ class ChannelJob: """ - __slots__ = ("db_name", "channel", "uuid", "_sorting_key", "__weakref__") + __slots__ = ("__weakref__", "_sorting_key", "channel", "db_name", "uuid") def __init__(self, db_name, channel, uuid, seq, date_created, priority, eta): self.db_name = db_name diff --git a/queue_job/jobrunner/runner.py b/queue_job/jobrunner/runner.py index 6ff2e2f260..65e556a6e8 100644 --- a/queue_job/jobrunner/runner.py +++ b/queue_job/jobrunner/runner.py @@ -31,6 +31,7 @@ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT import odoo +from odoo.modules.db import list_dbs from odoo.tools import config from . import queue_job_config @@ -72,7 +73,7 @@ def _odoo_now(): def _connection_info_for(db_name): - db_or_uri, connection_info = odoo.sql_db.connection_info_for(db_name) + _db_or_uri, connection_info = odoo.sql_db.connection_info_for(db_name) for p in ("host", "port", "user", "password"): cfg = os.environ.get( @@ -139,7 +140,7 @@ def close(self): # del try: self.conn.close() - except Exception: + except Exception: # noqa: BLE001, S110 pass self.conn = None @@ -382,7 +383,7 @@ def get_db_names(self): db_names = config["db_name"] if db_names: return db_names - return odoo.service.db.list_dbs(True) + return list_dbs(force=True) def close_databases(self, remove_jobs=True): for db_name, db in self.db_by_name.items(): @@ -474,17 +475,16 @@ def wait_notification(self): # if timeout remains a large negative number, it is most # probably a bug _logger.debug("select() timeout: %.2f sec", timeout) - if timeout > 0: - if conns and not self._stop: - with select() as sel: - for conn in conns: - sel.register(conn, selectors.EVENT_READ) - events = sel.select(timeout=timeout) - for key, _mask in events: - if key.fileobj == self._stop_pipe[0]: - # stop-pipe is not a conn so doesn't need poll() - continue - key.fileobj.poll() + if timeout > 0 and conns and not self._stop: + with select() as sel: + for conn in conns: + sel.register(conn, selectors.EVENT_READ) + events = sel.select(timeout=timeout) + for key, _mask in events: + if key.fileobj == self._stop_pipe[0]: + # stop-pipe is not a conn so doesn't need poll() + continue + key.fileobj.poll() def stop(self): _logger.info("graceful stop requested") diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py index 23cf5de370..013dc16c8c 100644 --- a/queue_job/models/queue_job.py +++ b/queue_job/models/queue_job.py @@ -35,7 +35,7 @@ class QueueJob(models.Model): _name = "queue.job" _description = "Queue Job" - _inherit = ["mail.thread", "mail.activity.mixin"] + _inherit = ["mail.thread", "mail.activity.mixin"] # noqa: RUF012 _log_access = False _order = "date_created DESC, date_done DESC" @@ -70,9 +70,7 @@ class QueueJob(models.Model): help="Single shared identifier of a Graph. Empty for a single job.", ) user_id = fields.Many2one(comodel_name="res.users", string="User ID") - company_id = fields.Many2one( - comodel_name="res.company", string="Company", index=True - ) + company_id = fields.Many2one(comodel_name="res.company", index=True) name = fields.Char(string="Description", readonly=True) model_name = fields.Char(string="Model", readonly=True) @@ -121,7 +119,6 @@ class QueueJob(models.Model): channel_method_name = fields.Char(string="Complete Method Name", readonly=True) job_function_id = fields.Many2one( comodel_name="queue.job.function", - string="Job Function", readonly=True, ) @@ -415,7 +412,7 @@ def autovacuum(self): Called from a cron. """ for channel in self.env["queue.job.channel"].search([]): # pylint: disable=no-search-all - deadline = datetime.now() - timedelta(days=int(channel.removal_interval)) + deadline = datetime.now() - timedelta(days=int(channel.removal_interval)) # noqa: DTZ005 # Delete in chunks using a stable order (matches composite index) while True: jobs = self.search( diff --git a/queue_job/models/queue_job_channel.py b/queue_job/models/queue_job_channel.py index 0a5c1c917b..ee5f7588fe 100644 --- a/queue_job/models/queue_job_channel.py +++ b/queue_job/models/queue_job_channel.py @@ -37,7 +37,7 @@ def _compute_complete_name(self): if not record.name: complete_name = "" # new record elif record.parent_id: - complete_name = ".".join([record.parent_id.complete_name, record.name]) + complete_name = f"{record.parent_id.complete_name}.{record.name}" else: complete_name = record.name record.complete_name = complete_name diff --git a/queue_job/models/queue_job_function.py b/queue_job/models/queue_job_function.py index 872f5831f0..d574753593 100644 --- a/queue_job/models/queue_job_function.py +++ b/queue_job/models/queue_job_function.py @@ -6,7 +6,7 @@ import re from collections import namedtuple -from odoo import api, exceptions, fields, models, tools +from odoo import api, exceptions, fields, models from ..fields import JobSerialized @@ -45,9 +45,7 @@ def _default_channel(self): # model and method should be required, but the required flag doesn't # let a chance to _inverse_name to be executed - model_id = fields.Many2one( - comodel_name="ir.model", string="Model", ondelete="cascade" - ) + model_id = fields.Many2one(comodel_name="ir.model", ondelete="cascade") method = fields.Char() on_fail_method = fields.Char( help="Model function to be called if the job is failed and will not be " @@ -56,7 +54,6 @@ def _default_channel(self): channel_id = fields.Many2one( comodel_name="queue.job.channel", - string="Channel", required=True, default=lambda r: r._default_channel(), ) @@ -187,7 +184,7 @@ def _parse_retry_pattern(self): retry_pattern = {} return retry_pattern - @tools.ormcache("name") + @api.ormcache("name") def job_config(self, name): config = self.search([("name", "=", name)], limit=1) if not config: @@ -279,15 +276,15 @@ def create(self, vals_list): new_vals_list.append(vals) vals_list = new_vals_list records |= super().create(vals_list) - self.env.registry.clear_cache() + self.env.transaction.invalidate_ormcache() return records def write(self, values): res = super().write(values) - self.env.registry.clear_cache() + self.env.transaction.invalidate_ormcache() return res def unlink(self): res = super().unlink() - self.env.registry.clear_cache() + self.env.transaction.invalidate_ormcache() return res diff --git a/queue_job/security/ir.access.csv b/queue_job/security/ir.access.csv new file mode 100644 index 0000000000..ae56a64612 --- /dev/null +++ b/queue_job/security/ir.access.csv @@ -0,0 +1,9 @@ +id,name,model_id,group_id/id,operation,domain +access_queue_job_manager,queue job manager,queue.job,queue_job.group_queue_job_manager,ru, +queue_job_comp_rule,Job Queue multi-company,queue.job,,crud,"['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]" +access_queue_job_lock_manager,queue job lock manager,queue.job.lock,queue_job.group_queue_job_manager,r, +access_queue_job_function_manager,queue job functions manager,queue.job.function,queue_job.group_queue_job_manager,crud, +access_queue_job_channel_manager,queue job channel manager,queue.job.channel,queue_job.group_queue_job_manager,crud, +access_queue_requeue_job,queue requeue job manager,queue.requeue.job,queue_job.group_queue_job_manager,crud, +access_queue_jobs_to_done,queue jobs to done manager,queue.jobs.to.done,queue_job.group_queue_job_manager,crud, +access_queue_jobs_to_cancelled,queue jobs to cancelled manager,queue.jobs.to.cancelled,queue_job.group_queue_job_manager,crud, diff --git a/queue_job/security/ir.model.access.csv b/queue_job/security/ir.model.access.csv deleted file mode 100644 index 9a451d6a78..0000000000 --- a/queue_job/security/ir.model.access.csv +++ /dev/null @@ -1,8 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_queue_job_manager,queue job manager,queue_job.model_queue_job,queue_job.group_queue_job_manager,1,1,0,0 -access_queue_job_lock_manager,queue job lock manager,queue_job.model_queue_job_lock,queue_job.group_queue_job_manager,1,0,0,0 -access_queue_job_function_manager,queue job functions manager,queue_job.model_queue_job_function,queue_job.group_queue_job_manager,1,1,1,1 -access_queue_job_channel_manager,queue job channel manager,queue_job.model_queue_job_channel,queue_job.group_queue_job_manager,1,1,1,1 -access_queue_requeue_job,queue requeue job manager,queue_job.model_queue_requeue_job,queue_job.group_queue_job_manager,1,1,1,1 -access_queue_jobs_to_done,queue jobs to done manager,queue_job.model_queue_jobs_to_done,queue_job.group_queue_job_manager,1,1,1,1 -access_queue_jobs_to_cancelled,queue jobs to cancelled manager,queue_job.model_queue_jobs_to_cancelled,queue_job.group_queue_job_manager,1,1,1,1 diff --git a/queue_job/security/security.xml b/queue_job/security/security.xml index 740226988e..b4bf2549e9 100644 --- a/queue_job/security/security.xml +++ b/queue_job/security/security.xml @@ -1,31 +1,19 @@ - + - - - Job Queue - 50 - - - Job Queue - 20 - - - Job Queue Manager - - - - - - - Job Queue multi-company - - - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] - - + + Job Queue + + + + Job Queue + + + + Job Queue Manager + + + diff --git a/queue_job/static/description/index.html b/queue_job/static/description/index.html index 58b23819d8..6267ae58a6 100644 --- a/queue_job/static/description/index.html +++ b/queue_job/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Job Queue